├── .autod.conf.js ├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── Dockerfile ├── Dockerfile.module ├── README.md ├── README.zh-CN.md ├── app.js ├── app ├── controller │ ├── auth.js │ ├── page.js │ ├── public.js │ ├── spider.js │ ├── visit.js │ └── wechat.js ├── extend │ ├── context.js │ └── helper.js ├── io │ ├── controller │ │ └── chat.js │ └── middleware │ │ └── auth.js ├── middleware │ ├── auth.js │ └── error.js ├── model │ ├── leave_msg.js │ ├── sf_post.js │ ├── token.js │ ├── user.js │ ├── visit.js │ └── vote.js ├── public │ └── indexData.json ├── router.js ├── schedule │ └── sf_blog.js └── service │ ├── auth.js │ ├── spider.js │ └── token.js ├── appveyor.yml ├── build ├── webpack.base.js ├── webpack.dev.js └── webpack.prod.js ├── config ├── config.default.js └── plugin.js ├── deploy ├── build.sh ├── module.sh └── update.sh ├── docker-compose.yml ├── index.js ├── package.json ├── resource ├── assets │ ├── avatar_default.jpg │ ├── axios.js │ ├── common.scss │ └── main_bg.jpg ├── components │ ├── cover.vue │ ├── login.vue │ ├── nav.vue │ └── visit.vue ├── pages │ ├── chat │ │ ├── app.vue │ │ ├── appBackup.vue │ │ ├── index.html │ │ └── index.js │ ├── example │ │ ├── app.vue │ │ ├── index.html │ │ └── index.js │ ├── index │ │ ├── app.vue │ │ ├── blog.vue │ │ ├── index.html │ │ ├── index.js │ │ ├── index.vue │ │ ├── introduce.vue │ │ └── project.vue │ └── spider │ │ ├── app.vue │ │ ├── index.html │ │ └── index.js ├── router │ ├── index.js │ └── spider.js └── store │ ├── action.js │ ├── index.js │ ├── modules │ ├── chat.js │ ├── information.js │ ├── show.js │ ├── spider.js │ ├── user.js │ └── visit.js │ ├── pages │ ├── chat.js │ └── spider.js │ └── types.js └── test └── app └── controller ├── auth.test.js └── page.test.js /.autod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | write: true, 5 | prefix: '^', 6 | plugin: 'autod-egg', 7 | test: [ 8 | 'test', 9 | 'benchmark', 10 | ], 11 | dep: [ 12 | ], 13 | devdep: [ 14 | 'egg-ci', 15 | 'egg-bin', 16 | 'autod', 17 | 'autod-egg', 18 | 'eslint', 19 | 'eslint-config-egg', 20 | 'webstorm-disable-index', 21 | ], 22 | exclude: [ 23 | './test/fixtures', 24 | './dist', 25 | ], 26 | }; 27 | 28 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": [["import", { 4 | "libraryName": "iview", 5 | "libraryDirectory": "src/components" 6 | }]] 7 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "Promise": true, 4 | "setTimeout": true, 5 | "clearTimeout": true, 6 | "module": true, 7 | "require": true, 8 | "describe": true, 9 | "beforeEach": true, 10 | "afterEach": true, 11 | "it": true, 12 | "before": true, 13 | "fis": true, 14 | "after": true, 15 | "beforeAll": true, 16 | "afterAll": true, 17 | "expect": true, 18 | "console": true, 19 | "__dirname": true, 20 | "global": true, 21 | "spyOn": true, 22 | "xit": true, 23 | "Buffer": true, 24 | "exports": true, 25 | "process": true, 26 | "window": true 27 | }, 28 | "parser": "babel-eslint", 29 | "parserOptions": { 30 | "ecmaVersion": 7, 31 | "sourceType": "module", 32 | "ecmaFeatures": { 33 | "jsx": true 34 | } 35 | }, 36 | "rules": { 37 | "no-negated-in-lhs": "error", 38 | "no-cond-assign": [ 39 | "error", 40 | "except-parens" 41 | ], 42 | "curly": [ 0 ], 43 | "object-curly-spacing": [ 44 | "error", 45 | "always" 46 | ], 47 | "eqeqeq": [ 48 | "error", 49 | "smart" 50 | ], 51 | "no-unused-expressions": "error", 52 | "no-sequences": "error", 53 | "no-unreachable": "error", 54 | "wrap-iife": [ 55 | "error", 56 | "outside" 57 | ], 58 | "no-caller": "error", 59 | "quotes": [ 60 | "error", 61 | "double" 62 | ], 63 | "no-undef": "error", 64 | "no-unused-vars": "error", 65 | "operator-linebreak": [ 66 | "error", 67 | "after" 68 | ], 69 | "comma-style": [ 70 | "error", 71 | "last" 72 | ], 73 | "camelcase": [ 74 | "error", 75 | { 76 | "properties": "never" 77 | } 78 | ], 79 | "dot-notation": [ 80 | "error", 81 | { 82 | "allowPattern": "^[a-z]+(_[a-z]+)+$" 83 | } 84 | ], 85 | "max-len": [ 86 | "error", 87 | { 88 | "code": 100, 89 | "ignoreComments": true 90 | } 91 | ], 92 | "no-mixed-spaces-and-tabs": "error", 93 | "no-trailing-spaces": "error", 94 | "comma-dangle": [ 95 | "error", 96 | "never" 97 | ], 98 | "comma-spacing": [ 99 | "error", 100 | { 101 | "before": false, 102 | "after": true 103 | } 104 | ], 105 | "keyword-spacing": [ 106 | 2 107 | ], 108 | "semi": [ 109 | "error", 110 | "always" 111 | ], 112 | "semi-spacing": [ 113 | "error", 114 | { 115 | // Because of the `for ( ; ...)` requirement 116 | // "before": true, 117 | "after": true 118 | } 119 | ], 120 | "space-infix-ops": "error", 121 | "eol-last": "error", 122 | "lines-around-comment": [ 123 | "error", 124 | { 125 | "beforeLineComment": false 126 | } 127 | ], 128 | "no-with": "error", 129 | "no-loop-func": "error", 130 | "no-spaced-func": "error", 131 | "space-unary-ops": [ 132 | "error", 133 | { 134 | "words": false, 135 | "nonwords": false 136 | } 137 | ], 138 | "no-multiple-empty-lines": 2 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs/ 2 | npm-debug.log 3 | node_modules/ 4 | coverage/ 5 | .idea/ 6 | run/ 7 | .DS_Store 8 | *.swp 9 | yarn.lock 10 | .travis.yml 11 | .vscode 12 | package-lock.json 13 | config/config.*.js 14 | jsdoc 15 | .nyc_output 16 | files/ 17 | view 18 | dist -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '6' 5 | - '8' 6 | install: 7 | - npm i npminstall && npminstall 8 | script: 9 | - npm run ci 10 | after_script: 11 | - npminstall codecov && codecov 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM module:base 2 | 3 | RUN echo "Asia/Shanghai" > /etc/timezone 4 | 5 | COPY . /app 6 | WORKDIR /app 7 | 8 | ENV NODE_ENV test 9 | 10 | RUN npm run build 11 | ENV PORT 80 12 | 13 | ENTRYPOINT npm run dev -------------------------------------------------------------------------------- /Dockerfile.module: -------------------------------------------------------------------------------- 1 | FROM node:8.0.0 2 | 3 | RUN echo "Asia/Shanghai" > /etc/timezone 4 | 5 | RUN mkdir /app 6 | WORKDIR /app 7 | 8 | RUN npm install -g cnpm 9 | 10 | COPY package.json /app 11 | RUN cnpm install 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 项目介绍 2 | 整合开发过程中用到的相关技术,构建一个个人网站(包括个人的介绍,一些小项目),主要想要提升自己在工程构建这块的能力. 3 | 4 | ## 后端 5 | + 框架: Egg.js 6 | + 数据库: mysql 7 | + ORM框架: Sequelize 8 | + 目前搭建后端服务成功,只简单写了注册登录,socket相关内容,并没有做复杂的业务 9 | 10 | ## 前端 11 | + 框架: Webpack + Vue + Vue-router + Vuex 12 | + UI框架: iView 模块化加载 13 | + 关于Webpack: 使用打包后客户端渲染,根据需求做了多入口打包成多个页面,每个SPA应用中有自己的 Router 和 Store 14 | 15 | ## 部署 16 | + 方式: 采用docker的方式进行部署 17 | + 关于docker: build了两个image,一个是项目依赖的库(每次下载太浪费时间),另外一个是基于依赖库镜像的业务代码 18 | + 持续集成: 计划采用持续集成的方式,在merge代码后,基于docker跑单测,lint等ci,然后自动部署(还未做) 19 | 20 | ### coding.... 21 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | example 4 | 5 | ## 快速入门 6 | 7 | 8 | 9 | 如需进一步了解,参见 [egg 文档][egg]。 10 | 11 | ### 本地开发 12 | ```bash 13 | $ npm install 14 | $ npm run dev 15 | $ open http://localhost:7001/news 16 | ``` 17 | 18 | ### 部署 19 | 20 | 线上正式环境用 `EGG_SERVER_ENV=prod` 来启动。 21 | 22 | ```bash 23 | $ EGG_SERVER_ENV=prod npm start 24 | ``` 25 | 26 | ### 单元测试 27 | - [egg-bin] 内置了 [mocha], [thunk-mocha], [power-assert], [istanbul] 等框架,让你可以专注于写单元测试,无需理会配套工具。 28 | - 断言库非常推荐使用 [power-assert]。 29 | - 具体参见 [egg 文档 -单元测试](https://eggjs.org/zh-cn/core/unittest)。 30 | 31 | ### 内置指令 32 | 33 | - 使用 `npm run lint` 来做代码风格检查。 34 | - 使用 `npm test` 来执行单元测试。 35 | - 使用 `npm run autod` 来自动检测依赖更新,详细参见 [autod](https://www.npmjs.com/package/autod) 。 36 | 37 | 38 | [egg]: https://eggjs.org -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | app.beforeStart(async function () { 3 | await app.model.sync({ force: false }); 4 | }); 5 | 6 | app.validator.addRule("stringNum", (rule, value) => { 7 | try { 8 | parseInt(value); 9 | } catch (err) { 10 | return "must be number"; 11 | } 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /app/controller/auth.js: -------------------------------------------------------------------------------- 1 | const userRule = { 2 | name: { 3 | type: "string", 4 | required: true, 5 | max: 24, 6 | min: 3 7 | }, 8 | passwd: { 9 | type: "string", 10 | required: true, 11 | max: 24, 12 | min: 6 13 | } 14 | }; 15 | module.exports = app => { 16 | class Auth extends app.Controller { 17 | async register(ctx) { // 注册 18 | ctx.validate(userRule); 19 | 20 | // 注册用户 21 | const user = await ctx.model.User.create(ctx.request.body); 22 | ctx.assert(user, "注册失败"); 23 | 24 | ctx.status = 204; 25 | } 26 | 27 | async login(ctx) { // 登录 28 | ctx.validate(userRule); 29 | 30 | // 用户校验 31 | const { name, passwd } = ctx.request.body; 32 | let user = await ctx.model.User.findOne({ 33 | where: { name: name } 34 | }); 35 | 36 | if (user) { ctx.error(user.passwd === passwd, "密码错误或昵称已存在", 10001); } 37 | else { user = await ctx.model.User.create({ name: name, passwd: passwd }); } 38 | 39 | // 生成token和session并存储 40 | const token = await ctx.service.token.genToken(user.id, ctx.request.ip); 41 | await app.redis.set(`${app.options.sessionPrefix}:${token.id}`, JSON.stringify({ 42 | user: user.id, 43 | token: token.id 44 | })); 45 | ctx.cookies.set("access_token", token.id); 46 | ctx.jsonBody = user; 47 | } 48 | 49 | async logout(ctx) { // 登出 50 | await app.redis.del(`${app.options.sessionPrefix}:${ctx.state.auth.token}`); 51 | ctx.cookies.set("access_token", null); 52 | 53 | ctx.status = 204; 54 | } 55 | } 56 | return Auth; 57 | }; 58 | -------------------------------------------------------------------------------- /app/controller/page.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | class Page extends app.Controller { 3 | async index(ctx) { 4 | const { path } = ctx; 5 | const token = ctx.state.auth.token; 6 | const pagesMap = { 7 | "/index": "index", 8 | "/chat": "chat", 9 | "/spider": "spider" 10 | }; 11 | 12 | await ctx.model.Visit.create({ 13 | token, page: path 14 | }); 15 | await ctx.render(pagesMap[path], { global: JSON.stringify({ user: ctx.state.auth.user }) }); 16 | } 17 | } 18 | 19 | return Page; 20 | }; 21 | -------------------------------------------------------------------------------- /app/controller/public.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | 3 | module.exports = app => { 4 | class Public extends app.Controller { 5 | async indexData(ctx) { 6 | const path = "app/public/indexData.json"; 7 | const data = fs.readFileSync(path); 8 | ctx.assert(data, 404); 9 | const blogs = await ctx.model.SfPost.findAll(); 10 | 11 | ctx.jsonBody = Object.assign({}, JSON.parse(data), { blogs }); 12 | } 13 | } 14 | 15 | return Public; 16 | }; 17 | -------------------------------------------------------------------------------- /app/controller/spider.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | class Spider extends app.Controller { 3 | get huiboRule () { 4 | return { 5 | key: { type: "string", required: true }, 6 | number: { type: "stringNum", required: false } 7 | }; 8 | } 9 | async huibo(ctx) { 10 | ctx.validate(this.huiboRule, ctx.query); 11 | const { number = 20 } = ctx.query; 12 | if (!ctx.state.auth.user) { 13 | ctx.error(number <= 20, "未登录用户最多可查询20条记录", 12001); 14 | } 15 | ctx.error(number <= 60, "最多可查询60条记录", 12002); 16 | const key = decodeURI(ctx.query.key); 17 | let timestamp = (new Date()).getTime(); 18 | let jobs = []; 19 | for (let i = 1; i <= Math.ceil(number / 20); i++) { 20 | try { 21 | const pageData = await ctx.service.spider.fetchHuiboPage({ i, word: key, timestamp }); 22 | timestamp = pageData.timestamp; 23 | jobs = jobs.concat(pageData.jobs); 24 | } catch (err) { 25 | console.log(err); 26 | } 27 | } 28 | 29 | ctx.body = { jobs }; 30 | } 31 | } 32 | 33 | return Spider; 34 | }; 35 | -------------------------------------------------------------------------------- /app/controller/visit.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | class Visit extends app.Controller { 3 | async newVote(ctx) { // 点赞; 4 | const token = ctx.state.auth.token; 5 | const [vote] = await ctx.model.Vote.findOrCreate({ where: { token } }); 6 | 7 | ctx.jsonBody = [vote]; 8 | } 9 | 10 | async newMsg(ctx) { // 留言 11 | const msgRule = { 12 | msg: { 13 | type: "string", 14 | max: 400 15 | }, 16 | connection: { 17 | type: "string", 18 | max: 24, 19 | required: false 20 | } 21 | }; 22 | ctx.validate(msgRule); 23 | const { msg, connection } = ctx.request.body; 24 | 25 | const token = ctx.state.auth.token; 26 | const user = ctx.state.auth.user ? ctx.state.auth.user.id : null; 27 | const insertDate = { token, msg, connection }; 28 | if (user) insertDate.user = user; 29 | const leaveMsg = await ctx.model.LeaveMsg.create(insertDate); 30 | 31 | ctx.jsonBody = leaveMsg; 32 | } 33 | 34 | async index(ctx) { // 访客数量及是否点赞 35 | const token = ctx.state.auth.token; 36 | 37 | const visitTimes = await ctx.model.Visit.count() || 0; 38 | const voteTimes = await ctx.model.Vote.count() || 0; 39 | const vote = await ctx.model.Vote.findOne({ 40 | where: { token } 41 | }); 42 | 43 | ctx.jsonBody = { 44 | visit_times: visitTimes, 45 | vote_times: voteTimes, 46 | vote: vote 47 | }; 48 | } 49 | 50 | async leaveMsg(ctx) { 51 | const result = await ctx.model.LeaveMsg.findAndCountAll({ 52 | order: [["created_at", "DESC"]] 53 | }); 54 | 55 | ctx.jsonBody = { 56 | count: result.count, 57 | msgs: result.rows 58 | }; 59 | } 60 | 61 | async destroy(ctx) { 62 | ctx.adminPermission(); 63 | ctx.assert(ctx.query.msgs, 400); 64 | const msgs = ctx.query.msgs.split(","); 65 | 66 | const count = await ctx.model.LeaveMsg.destroy({ where: { id: { $in: msgs } } }); 67 | 68 | ctx.jsonBody = { count }; 69 | } 70 | } 71 | return Visit; 72 | }; 73 | -------------------------------------------------------------------------------- /app/controller/wechat.js: -------------------------------------------------------------------------------- 1 | const crypto = require("crypto"); 2 | 3 | module.exports = app => { 4 | class Wechat extends app.Controller { 5 | async verify(ctx) { 6 | const { signature, timestamp, nonce, echostr } = ctx.query; 7 | const array = [app.config.wechat.token, timestamp, nonce]; 8 | array.sort(); 9 | 10 | // 将三个参数字符串拼接成一个字符串进行sha1加密 11 | const tempStr = array.join(""); 12 | const hashCode = crypto.createHash("sha1"); //创建加密类型 13 | const resultCode = hashCode.update(tempStr, "utf8").digest("hex"); //对传入的字符串进行加密 14 | 15 | // 开发者获得加密后的字符串可与signature对比,标识该请求来源于微信 16 | if (resultCode === signature){ 17 | ctx.body = echostr; 18 | } else { 19 | ctx.body = "mismatch"; 20 | } 21 | } 22 | } 23 | 24 | return Wechat; 25 | }; 26 | -------------------------------------------------------------------------------- /app/extend/context.js: -------------------------------------------------------------------------------- 1 | const moment = require("moment"); 2 | const { VError } = require("verror"); 3 | 4 | module.exports = { 5 | 6 | // 包装response 7 | set jsonBody(data) { 8 | this.assert(data && typeof data === "object"); 9 | this.body = { 10 | code: 200, 11 | msg: "success", 12 | data 13 | }; 14 | }, 15 | 16 | // error包装 17 | error(expression, message, code, status = 200, originalError) { 18 | /* istanbul ignore else */ 19 | if (expression) { 20 | return; 21 | } 22 | 23 | this.assert(message && typeof message === "string"); 24 | this.assert(code && typeof code === "number"); 25 | 26 | this.type = "json"; 27 | const err = Object.assign(new VError({ 28 | name: "custom_server_error", 29 | cause: originalError 30 | }, message), { 31 | code, 32 | status 33 | }); 34 | 35 | this.throw(err); 36 | }, 37 | 38 | // 生成token 39 | genToken (userId, ip) { 40 | const expire = new Date(moment().add(1, "days")); 41 | return this.model.Token.create({ expire, ip, user: userId, valid: true }); 42 | }, 43 | 44 | // 登录权限验证 45 | authPermission() { 46 | const ctx = this; 47 | ctx.assert(ctx.state.auth.user, 403); 48 | }, 49 | 50 | // 管理员权限验证 51 | adminPermission() { 52 | const ctx = this; 53 | ctx.assert(ctx.state.auth.user, 403); 54 | ctx.assert.equal(ctx.state.auth.user.role, 1, 403); 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /app/extend/helper.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rule: { 3 | uuid: { 4 | type: "string", 5 | pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-[1-4][0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$", 6 | } 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /app/io/controller/chat.js: -------------------------------------------------------------------------------- 1 | exports.login = async function() { 2 | const user = this.args[0]; 3 | await this.model.User.update({ login: true }, { where: { id: user.id } }); 4 | const onlineUsers = await this.model.User.findAll({ where: { login: true } }); 5 | this.app.io.emit("login", { 6 | name: user.name 7 | }); 8 | this.app.io.emit("online_user", { 9 | users: onlineUsers 10 | }); 11 | }; 12 | 13 | exports.logout = async function() { 14 | console.log("logout"); 15 | }; 16 | 17 | exports.message = async function() { 18 | const message = this.args[0]; 19 | this.app.io.emit("message", message); 20 | }; 21 | -------------------------------------------------------------------------------- /app/io/middleware/auth.js: -------------------------------------------------------------------------------- 1 | const TOKEN = "access_token"; 2 | 3 | /* istanbul ignore next */ 4 | module.exports = option => function* (next) { 5 | const token = this.cookies.get(TOKEN) || this.headers[TOKEN]; 6 | 7 | this.state.auth = Object.assign({ token }, this.state.auth); 8 | const ret = yield this.app.redis.get(`${option.prefix}:${token}`); 9 | if (!ret) { 10 | yield next; 11 | return; 12 | } 13 | 14 | let session = null; 15 | try { 16 | session = JSON.parse(ret); 17 | } catch (e) { 18 | this.error("Session已失效, 请重新登录", 10001); 19 | yield this.app.redis.set(`${option.prefix}:${token}`, null); 20 | } 21 | 22 | const user = yield this.model.User.findById(session.user); 23 | 24 | if (!user) { 25 | yield this.app.redis.set(`${option.prefix}:${token}`, null);} 26 | this.assert(user, 401, "用户不存在"); 27 | 28 | this.state.auth = Object.assign({}, this.state.auth, { 29 | token, 30 | user: user.toJSON() 31 | }); 32 | 33 | yield next; 34 | }; 35 | -------------------------------------------------------------------------------- /app/middleware/auth.js: -------------------------------------------------------------------------------- 1 | const TOKEN = "access_token"; 2 | const uuid = require("uuid"); 3 | 4 | /* istanbul ignore next */ 5 | module.exports = option => function* (next) { 6 | const sid = this.cookies.get("sid") || uuid(); 7 | const token = this.cookies.get(TOKEN) || this.headers[TOKEN] || sid; 8 | 9 | this.cookies.set("sid", sid, { maxAge: 3600 * 1000 * 24 * 365 }); 10 | this.state.auth = Object.assign({ token }, this.state.auth); 11 | const ret = yield this.app.redis.get(`${option.prefix}:${token}`); 12 | if (!ret) { 13 | yield next; 14 | return; 15 | } 16 | 17 | let session = null; 18 | try { 19 | session = JSON.parse(ret); 20 | } catch (e) { 21 | this.error("Session已失效, 请重新登录", 10001); 22 | yield this.app.redis.set(`${option.prefix}:${token}`, null); 23 | } 24 | 25 | const user = yield this.model.User.findById(session.user); 26 | 27 | if (!user) { 28 | yield this.app.redis.set(`${option.prefix}:${token}`, null);} 29 | this.assert(user, 401, "用户不存在"); 30 | 31 | this.state.auth = Object.assign({}, this.state.auth, { 32 | token, 33 | user: user.toJSON() 34 | }); 35 | 36 | yield next; 37 | }; 38 | -------------------------------------------------------------------------------- /app/middleware/error.js: -------------------------------------------------------------------------------- 1 | const { VError } = require("verror"); 2 | 3 | module.exports = () => function* (next) { 4 | try { 5 | yield next; 6 | } catch (e) { 7 | if (e instanceof VError) { 8 | this.body = { 9 | code: e.code, 10 | msg: e.message, 11 | errors: this.app.isProd ? undefined : [e] 12 | }; 13 | this.status = e.status; 14 | } else if (e.status && e.statusCode) { // http error caused by ctx.assert 15 | this.body = { 16 | code: e.status, 17 | msg: e.message, 18 | errors: this.app.isProd ? undefined : [e] 19 | }; 20 | } else { 21 | throw e; 22 | } 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /app/model/leave_msg.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | const { STRING, UUID } = app.Sequelize; 3 | const LeaveMsg = app.model.define("leave_msg", { 4 | token: { 5 | type: UUID, 6 | allowNull: false 7 | }, 8 | user: UUID, 9 | msg: STRING(1024), 10 | connection: STRING(256) 11 | }); 12 | 13 | return LeaveMsg; 14 | }; 15 | -------------------------------------------------------------------------------- /app/model/sf_post.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | const { STRING, UUID, UUIDV1, TEXT, DATE } = app.Sequelize; 3 | 4 | const SfPost = app.model.define("sf_post", { 5 | id: { 6 | type: UUID, 7 | defaultValue: UUIDV1, 8 | primaryKey : true, 9 | unique : true 10 | }, 11 | title: STRING(128), 12 | path: { 13 | type: STRING(128), 14 | primaryKey: true, 15 | unique: true 16 | }, 17 | content: TEXT, 18 | tages: STRING(512), 19 | publish_date: DATE, 20 | vote: STRING(8), 21 | save: STRING(8), 22 | hits: STRING(8) 23 | }); 24 | 25 | return SfPost; 26 | }; 27 | -------------------------------------------------------------------------------- /app/model/token.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | const { STRING, DATE, BOOLEAN, UUID, UUIDV1 } = app.Sequelize; 3 | 4 | const Token = app.model.define("token", { 5 | id: { 6 | type: UUID, 7 | defaultValue: UUIDV1, 8 | primaryKey : true, 9 | unique : true 10 | }, 11 | user: UUID, 12 | expire: DATE, 13 | ip: STRING, 14 | valid: BOOLEAN 15 | }); 16 | 17 | return Token; 18 | }; 19 | -------------------------------------------------------------------------------- /app/model/user.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | const { STRING, INTEGER, DATE, UUID, UUIDV1, BOOLEAN } = app.Sequelize; 3 | const User = app.model.define("user", { 4 | login: { 5 | type: BOOLEAN, 6 | default: false 7 | }, 8 | id: { 9 | type: UUID, 10 | defaultValue: UUIDV1, 11 | primaryKey : true, 12 | unique : true 13 | }, 14 | role: { 15 | type: INTEGER, 16 | default: 0 17 | }, 18 | passwd: STRING(32), 19 | name: { 20 | type: STRING(32), 21 | allowNull: false, 22 | unique: true 23 | }, 24 | created_at: DATE, 25 | updated_at: DATE 26 | }); 27 | 28 | User.prototype.logSignin = async function () { 29 | await this.update({ last_sign_in_at: new Date() }); 30 | }; 31 | 32 | return User; 33 | }; 34 | -------------------------------------------------------------------------------- /app/model/visit.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | const { STRING, UUID } = app.Sequelize; 3 | const VisitDetail = app.model.define("visit", { 4 | token: { 5 | type: UUID, 6 | allowNull: false 7 | }, 8 | page: STRING(256) 9 | }); 10 | 11 | return VisitDetail; 12 | }; 13 | -------------------------------------------------------------------------------- /app/model/vote.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | const { UUID } = app.Sequelize; 3 | const Vote = app.model.define("vote", { 4 | token: { 5 | type: UUID, 6 | allowNull: false, 7 | unique : true 8 | } 9 | }); 10 | 11 | return Vote; 12 | }; 13 | -------------------------------------------------------------------------------- /app/public/indexData.json: -------------------------------------------------------------------------------- 1 | { 2 | "introduce": { 3 | "skills": [ 4 | { 5 | "description": "JavaScript: 深刻理解对象、函数、闭包、原型、继承、事件流等机制", 6 | "degree": 75 7 | }, 8 | { 9 | "description": "Nodejs: 熟悉基础和特性,并对其架构有所了解", 10 | "degree": 65 11 | }, 12 | { 13 | "description": "Koa2: 熟悉相关技术栈,并能用其进行服务端开发", 14 | "degree": 70 15 | }, 16 | { 17 | "description": "DB: 使用Mysql/mongodb ORM框架进行数据存储处理", 18 | "degree": 60 19 | }, 20 | { 21 | "description": "Vuejs: 使用技术栈进行开发,对其部分细节有深入了解", 22 | "degree": 60 23 | }, 24 | { 25 | "description": "Other: 前端兼容、优化、安全有一定了解", 26 | "degree": 50 27 | } 28 | ], 29 | "experiences": [ 30 | { 31 | "time": "2017.01-至今 网达软件", 32 | "works": [ 33 | "接口业务逻辑实现", 34 | "RESTful API 维护、设计", 35 | "数据表的 维护、设计", 36 | "前端页面还原、逻辑交互实现(Vuejs)", 37 | "使用Koa2+MongoDB构建后端服务", 38 | "使用Eggjs+Postgresql构建后端服务", 39 | "与前端,测试同事合作进行联调和Bug修改", 40 | "探索解决项目中使用的相关库出现的问题" 41 | ], 42 | "projects": [ 43 | { 44 | "name": "合作联盟", 45 | "position": "后端开发", 46 | "description": "浙江移动物联网相关项目的众包平台,提供项目的发布,审核,展示,申请", 47 | "link": "http://ca.ioteams.com", 48 | "workDescription": "接口业务逻辑实现;部分数据表, API 设计、维护" 49 | }, 50 | { 51 | "name": "和物官网", 52 | "position": "后端开发", 53 | "description": "和物介绍展示,和物产品开发调试,并与OneNET信息同步", 54 | "link": "http://hewu.ioteams.com", 55 | "workDescription": "接口业务逻辑实现;部分通用功能的封装(图片压缩,加解密,等等)" 56 | } 57 | ] 58 | }, 59 | { 60 | "time": "2016.06-2017.01 海云数据", 61 | "works": [ 62 | "使用公司产品(图易)进行项目的开发、维护", 63 | "使用Vue开发一些基础组件", 64 | "使用React修改图易中一些基础组件完成用户定制需求", 65 | "使用echarts完成用户定制需求", 66 | "解决项目中遇到的比较棘手的问题" 67 | ], 68 | "projects": [ 69 | { 70 | "name": "数据可视化", 71 | "position": "前端开发", 72 | "description": "针对警务系统的大数据可视化分析。根据警务系统已有数据从不同维度进行可视化展示", 73 | "link": "内网使用", 74 | "workDescription": "使用公司产品(图易)进行项目的开发、维护;解决项目中遇到的比较棘手的问题" 75 | } 76 | ] 77 | }, 78 | { 79 | "time": "2016.01-2016.06 途宝科技(实习)", 80 | "works": [ 81 | "使用Vue+webpack+vue-router进行前端业务开发/维护", 82 | "封装部分可复用组件" 83 | ], 84 | "projects": [ 85 | { 86 | "name": "途宝旅行", 87 | "position": "前端开发", 88 | "description": "一个关于境外电话卡销售的电商平台", 89 | "link": "http://www.itourbag.com/wechat_h5shop", 90 | "workDescription": "在已搭好的项目架构下进行所有业务开发;封装可复用代码为组件" 91 | } 92 | ] 93 | } 94 | ], 95 | "projects": [ 96 | { 97 | "name": "chatRoom", 98 | "description": "一个基于sockt.io的聊天室", 99 | "date": "2017-12-25", 100 | "path": "/chat" 101 | }, { 102 | "name": "简单的职位爬虫", 103 | "description": "根据职位关键字和地区搜索相应职位,已表格方式展示,并提供职位对应职位链接,目前已添加\"汇博人才网\"搜索,其它数据来源添加中...", 104 | "date": "2018-1-16", 105 | "path": "/spider" 106 | } 107 | ] 108 | }, 109 | "spider": { 110 | "originMap": { 111 | "huibo": "汇博人才网", 112 | "lagou": "拉钩" 113 | }, 114 | "addrList": [{ 115 | "value": "chongqing", 116 | "label": "重庆", 117 | "children": [ 118 | { 119 | "value": "quanbu", 120 | "label": "全部" 121 | }, 122 | { 123 | "value": "yuzhong", 124 | "label": "渝中区" 125 | }, 126 | { 127 | "value": "dadukou", 128 | "label": "大渡口区" 129 | }, 130 | { 131 | "value": "jiangbei", 132 | "label": "江北区" 133 | }, 134 | { 135 | "value": "shapingban", 136 | "label": "沙坪坝区" 137 | }, 138 | { 139 | "value": "jiulongpo", 140 | "label": "九龙坡区" 141 | }, 142 | { 143 | "value": "nanan", 144 | "label": "南岸区" 145 | }, 146 | { 147 | "value": "yubei", 148 | "label": "渝北区" 149 | }, 150 | { 151 | "value": "banan", 152 | "label": "巴南区" 153 | }, 154 | { 155 | "value": "wanzhou", 156 | "label": "万州区" 157 | }, 158 | { 159 | "value": "beibei", 160 | "label": "北碚区" 161 | }, 162 | { 163 | "value": "shuangqiao", 164 | "label": "双桥区" 165 | }, 166 | { 167 | "value": "fuling", 168 | "label": "涪陵区" 169 | }, 170 | { 171 | "value": "qianjiang", 172 | "label": "黔江区" 173 | }, 174 | { 175 | "value": "changshou", 176 | "label": "长寿区" 177 | }, 178 | { 179 | "value": "jiangjin", 180 | "label": "江津区" 181 | }, 182 | { 183 | "value": "hechuan", 184 | "label": "合川区" 185 | }, 186 | { 187 | "value": "yongchuan", 188 | "label": "永川区" 189 | }, 190 | { 191 | "value": "nanchuan", 192 | "label": "南川区" 193 | } 194 | ] 195 | }] 196 | } 197 | } -------------------------------------------------------------------------------- /app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | const prefix = app.config.noPrefix ? "" : "/api/v1"; 3 | 4 | // api 5 | app.post(`${prefix}/auth/register`, app.controller.auth.register); 6 | app.post(`${prefix}/auth/login`, app.controller.auth.login); 7 | app.post(`${prefix}/auth/logout`, app.controller.auth.logout); 8 | app.get(`${prefix}/spider/huibo`, app.controller.spider.huibo); 9 | app.post(`${prefix}/visits/leave_msg`, app.controller.visit.newMsg); 10 | app.get(`${prefix}/visits/leave_msg`, app.controller.visit.leaveMsg); 11 | app.delete(`${prefix}/visits/leave_msg`, app.controller.visit.destroy); 12 | app.post(`${prefix}/visits/vote`, app.controller.visit.newVote); 13 | app.get(`${prefix}/visits/visit`, app.controller.visit.index); 14 | 15 | 16 | // public assets 17 | app.get("/data/index", app.controller.public.indexData); 18 | 19 | // page 20 | app.get("/index", app.controller.page.index); 21 | app.get("/chat", app.controller.page.index); 22 | app.get("/spider", app.controller.page.index); 23 | 24 | // wechat 25 | app.get("/wechat/verify", app.controller.wechat.verify); 26 | 27 | // socket 28 | app.io.route("login", require("./io/controller/chat").login); 29 | // app.io.route("disconnect", require("./io/controller/chat").logout); 30 | app.io.route("message", require("./io/controller/chat").message); 31 | }; 32 | -------------------------------------------------------------------------------- /app/schedule/sf_blog.js: -------------------------------------------------------------------------------- 1 | const cheerio = require("cheerio"); 2 | const _ = require("lodash"); 3 | 4 | module.exports = { 5 | schedule: { 6 | interval: "2d", 7 | type: "worker", 8 | immediate: false 9 | }, 10 | async task(ctx) { 11 | const res = await ctx.curl(`${ctx.app.config.custom.sfHost}/u/leodreamer/articles`); 12 | const html = Buffer.from(res.data).toString("utf8"); 13 | const $ = cheerio.load(html); 14 | const aTags = $("a.profile-mine__content--title"); // 链接a标签 15 | const dateTages = $("span.profile-mine__content--date"); // 发布日期标签 16 | const posts = []; 17 | // 提取标题&链接&发布日期 18 | Object.keys(aTags).forEach((key, index) => { 19 | if (key.length > 1) { return; }; 20 | const tag = $(aTags[key]); 21 | posts.push({ 22 | title: tag.text(), 23 | path: tag.attr("href"), 24 | publish_date: new Date(new Date().getFullYear() + 25 | "/" + $(dateTages[index]).text().replace(/年|月|日/g, () => "/")) 26 | }); 27 | }); 28 | // 获取每篇文章内容 29 | const postContents = await Promise.all( 30 | posts.map(p => ctx.curl(ctx.app.config.custom.sfHost + p.path)) 31 | ); 32 | // 提取文章内容&阅读&点赞&收藏 33 | postContents.forEach(p => { 34 | const $$ = cheerio.load(Buffer.from(p.data).toString("utf8")); 35 | const path = p.res.requestUrls[0].replace(ctx.app.config.custom.sfHost, ""); 36 | const content = $$("div.article__content").text(); 37 | const index = _.findIndex(posts, (each) => each.path === path); 38 | const vote = $$("span#sideLikeNum").text() || 0; 39 | const save = $$("span#sideBookmarkNum").text() || 0; 40 | const hits = $$("strong.no-stress").text() || 0; 41 | posts[index].content = content.toString(); 42 | Object.assign(posts[index], { 43 | content: content.toString().replace(/[\s*|/\n]/g, ""), 44 | vote, 45 | save, 46 | hits 47 | }); 48 | }); 49 | ctx.model.SfPost.bulkCreate(posts, { 50 | updateOnDuplicate: ["vote", "save", "hits"] 51 | }); 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /app/service/auth.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | class Auth extends app.Service { 3 | } 4 | return Auth; 5 | }; 6 | -------------------------------------------------------------------------------- /app/service/spider.js: -------------------------------------------------------------------------------- 1 | const requestPromise = require("request-promise"); 2 | const cheerio = require("cheerio"); 3 | 4 | module.exports = app => { 5 | const request = function(obj) { 6 | return requestPromise(Object.assign({}, { 7 | baseUrl: app.config.custom.huiboHost, 8 | methods: "GET" 9 | }, obj)) 10 | .catch(err => { 11 | if (err) { 12 | throw Error(`fetch ${err.options.baseUrl + err.options.url} fail: ${err}`); 13 | } 14 | }) 15 | .then(body => { 16 | console.log(`fetch ${obj.url} success`); 17 | return body; 18 | }); 19 | }; 20 | 21 | function analysisSearchPage(page, path) { 22 | const $ = cheerio.load(page); 23 | const jobs = []; 24 | const jobList = $(".postIntro"); 25 | for (let i = 0; i < jobList.length; i++) { 26 | const job = $(jobList[i]); 27 | const requireDomList = $(job.find("div.job-detList")[0]).find("p"); 28 | const requires = []; 29 | const title = $(job.find(".title").find("a")[0]).attr("title"); 30 | const industry = $(job.find(".title").find("span").find("a")[0]).attr("title"); 31 | const name = $(job.find("p.clearfix").find(".des_title")[0]).text().replace("\n", ""); 32 | const money = $(job.find("p.clearfix").find("span.money")[0]) 33 | .text().replace("\n", "").replace("¥", ""); 34 | const address = $(job.find("p.clearfix").find("span.address")[0]).text().replace("\n", ""); 35 | const exp = $(job.find("p.clearfix").find("span.exp")[0]).text().replace("\n", ""); 36 | const publicTime = $(job.find("p.clearfix").find("span.job_time")[0]) 37 | .text().replace("\n", ""); 38 | const url = $(job.find("a.des_title")[0]).attr("href"); 39 | for (let j = 0; j < requireDomList.length; j++) { 40 | if ($(requireDomList[j]).text() !== "") { requires.push($(requireDomList[j]).text()); } 41 | } 42 | jobs.push({ 43 | title, industry, name, money, address, exp, publicTime, requires, url, 44 | uri: app.config.custom.huiboHost + 45 | `?params=p${path.i}&key=${encodeURI(path.word)}×tamp=${path.timestamp}#list` 46 | }); 47 | } 48 | return jobs; 49 | } 50 | 51 | class Spider extends app.Service { 52 | async fetchHuiboPage (path) { 53 | const page = await request({ 54 | url: `?params=p${path.i}&key=${encodeURI(path.word)}×tamp=${path.timestamp}#list` 55 | }); 56 | if (page instanceof Error) { return; }; 57 | const $ = cheerio.load(page); 58 | const timestamp = $($("div.page").find("a")[2]).attr("href").split("=").pop().replace("#list", ""); // eslint-disable-line 59 | const jobs = analysisSearchPage(page, path); 60 | return { 61 | timestamp: timestamp * 1 || path.timestamp, 62 | jobs 63 | }; 64 | } 65 | } 66 | return Spider; 67 | }; 68 | -------------------------------------------------------------------------------- /app/service/token.js: -------------------------------------------------------------------------------- 1 | const moment = require("moment"); 2 | 3 | module.exports = app => { 4 | class Token extends app.Service { 5 | fetchOne (id) { 6 | return this.ctx.model.Token.find({ 7 | where: { id: id } 8 | }); 9 | } 10 | 11 | genToken (userId, ip) { 12 | const expire = new Date(moment().add(1, "days")); 13 | return this.ctx.model.Token.create({ expire, ip, user: userId, valid: true }); 14 | } 15 | } 16 | return Token; 17 | }; 18 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: '6' 4 | - nodejs_version: '8' 5 | 6 | install: 7 | - ps: Install-Product node $env:nodejs_version 8 | - npm i npminstall && node_modules\.bin\npminstall 9 | 10 | test_script: 11 | - node --version 12 | - npm --version 13 | - npm run test 14 | 15 | build: off 16 | -------------------------------------------------------------------------------- /build/webpack.base.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | const HtmlWebpackPlugin = require("html-webpack-plugin"); // 生成html 4 | const CleanWebpackPlugin = require("clean-webpack-plugin"); // 清除打包出的目录 5 | const ExtractTextPlugin = require("extract-text-webpack-plugin"); // 提取css 6 | const assert = require("assert"); 7 | const glob = require("glob"); 8 | const merge = require("webpack-merge"); 9 | 10 | const sourceDir = "resource"; 11 | const view = "dist/view"; 12 | const staticPath = "dist/static"; 13 | const root = process.cwd(); 14 | 15 | function sourceMap (suffix) { 16 | const maps = {}; 17 | glob.sync(`${sourceDir}/pages/**/*.${suffix}`).forEach(function (url) { 18 | const ret = url.match(`${sourceDir}\/pages\/(.*).${suffix}$`); 19 | assert(ret); 20 | 21 | maps[ret[1]] = path.resolve(root, ret[0]); 22 | }); 23 | 24 | return maps; 25 | }; 26 | 27 | const entry = sourceMap("js"); 28 | const htmls = sourceMap("html"); 29 | 30 | let config = { 31 | entry: Object.assign({}, entry, { 32 | vendors: ["vue", "vue-router", "vuex", "axios", "vue-axios", "babel-polyfill"] 33 | }), 34 | output: { 35 | path: path.resolve(root, staticPath), 36 | publicPath: "/", 37 | filename: "js/[name].[chunkhash].js", 38 | chunkFilename: "[id].[chunkhash].js" 39 | }, 40 | resolve: { 41 | extensions: [".js", ".vue", ".scss"], 42 | alias: { 43 | vue$: "vue/dist/vue.common.js", 44 | assets: path.resolve(root, sourceDir, "assets"), 45 | components: path.resolve(root, sourceDir, "components"), 46 | root 47 | } 48 | }, 49 | module: { 50 | rules: [ 51 | { 52 | test: /\.vue$/, 53 | loader: "vue-loader" 54 | }, 55 | { 56 | test: /\.js$/, 57 | loader: "babel-loader" 58 | }, 59 | { 60 | test: /\.css$/, 61 | loader: ExtractTextPlugin.extract({ 62 | fallback: "style-loader", 63 | use: "css-loader" 64 | }) 65 | }, 66 | { 67 | test: /\.scss/, 68 | loader: ExtractTextPlugin.extract({ 69 | fallback: "style-loader", 70 | use: "css-loader?importLoaders=2!postcss-loader!sass-loader" 71 | }) 72 | }, 73 | { 74 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 75 | loader: "file-loader?name=imges/[name].[ext]" 76 | }, 77 | { 78 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 79 | loader: "file-loader?name=file/[name].[ext]" 80 | } 81 | ] 82 | }, 83 | plugins: [ 84 | new CleanWebpackPlugin(path.resolve(root, "dist"), { 85 | root 86 | }), 87 | new webpack.optimize.CommonsChunkPlugin({ 88 | name: "vendors", 89 | chunks: Object.keys(entry), 90 | minChunks: entry.length 91 | }), 92 | new webpack.LoaderOptionsPlugin({ 93 | vue: { 94 | loaders: { 95 | css: ExtractTextPlugin.extract({ 96 | fallback: "vue-style-loader", 97 | use: "css-loader" 98 | }), 99 | sass: ExtractTextPlugin.extract({ 100 | fallback: "vue-style-loader", 101 | use: "css-loader!sass-loader" 102 | }) 103 | } 104 | } 105 | }), 106 | new ExtractTextPlugin({ filename: "css/[name].[contenthash].css", allChunks: true }) 107 | ] 108 | }; 109 | 110 | config = merge(config, { 111 | plugins: Object.keys(htmls).map(function (key) { 112 | return new HtmlWebpackPlugin({ 113 | filename: path.resolve(root, view, `${key.split("/")[0]}.html`), 114 | template: path.resolve(root, htmls[key]), 115 | inject: true, 116 | chunks: ["vendors", key] 117 | }); 118 | }) 119 | }); 120 | 121 | module.exports = config; 122 | -------------------------------------------------------------------------------- /build/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const HtmlWebpackPlugin = require("html-webpack-plugin"); // 生成html 3 | const baseConfig = require("./webpack.base.js"); 4 | const assert = require("assert"); 5 | const glob = require("glob"); 6 | const merge = require("webpack-merge"); 7 | 8 | const sourceDir = "resource"; 9 | const staticPath = "dist/static"; 10 | const root = process.cwd(); 11 | 12 | function sourceMap (suffix) { 13 | const maps = {}; 14 | glob.sync(`${sourceDir}/pages/**/*.${suffix}`).forEach(function (url) { 15 | const ret = url.match(`${sourceDir}\/pages\/(.*).${suffix}$`); 16 | assert(ret); 17 | 18 | maps[ret[1]] = path.resolve(root, ret[0]); 19 | }); 20 | 21 | return maps; 22 | }; 23 | 24 | 25 | const htmls = sourceMap("html"); 26 | 27 | const config = merge(baseConfig, { 28 | plugins: Object.keys(htmls).map(function (key) { 29 | return new HtmlWebpackPlugin({ 30 | filename: path.resolve(root, staticPath, `${key.split("/")[0]}.html`), 31 | template: path.resolve(root, htmls[key]), 32 | inject: true, 33 | chunks: ["vendors", key] 34 | }); 35 | }) 36 | }); 37 | 38 | module.exports = config; 39 | -------------------------------------------------------------------------------- /build/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const baseConfig = require("./webpack.base.js"); 3 | const merge = require("webpack-merge"); 4 | const OptimizeCssAssetsPlugin = require("optimize-css-assets-webpack-plugin"); 5 | 6 | const config = merge(baseConfig, { 7 | plugins: [ 8 | new webpack.optimize.OccurrenceOrderPlugin(), // 高频使用模块分配短ID 9 | new webpack.optimize.UglifyJsPlugin({ 10 | compress: { 11 | warnings: false 12 | } 13 | }), 14 | new OptimizeCssAssetsPlugin({ 15 | assetNameRegExp: /\.optimize\.css$/g, 16 | cssProcessor: require("cssnano"), 17 | cssProcessorOptions: { discardComments: { removeAll: true } }, 18 | canPrint: true 19 | }) 20 | ] 21 | }); 22 | 23 | module.exports = config; 24 | -------------------------------------------------------------------------------- /config/config.default.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const path = require("path"); 4 | const database = "egg"; 5 | const host = "192.168.189.130"; 6 | module.exports = appInfo => { 7 | const config = {}; 8 | config.sessionPrefix = "letchat"; 9 | 10 | // should change to your own 11 | config.keys = appInfo.name + "_1501817502166_7037"; 12 | config.noPrefix = true; 13 | config.middleware = ["auth", "error"]; 14 | 15 | // add your config here 16 | config.logger = { 17 | consoleLevel: "INFO" 18 | }; 19 | 20 | config.sequelize = { 21 | dialect: "mysql", // db type 22 | database: database, 23 | host: host, 24 | port: "3306", 25 | username: "root", 26 | password: "root", 27 | log: false, 28 | define: { 29 | freezeTableName: true, 30 | underscored: true, 31 | paranoid: true, 32 | charset: "utf8" 33 | } 34 | }; 35 | 36 | config.redis = { 37 | client: { 38 | port: 6379, // Redis port 39 | host: host, // Redis host 40 | password: "", 41 | db: 0 42 | } 43 | }; 44 | 45 | config.view = { 46 | defaultViewEngine: "nunjucks", // 默认渲染引擎 47 | defaultExtension: ".html", // 省略后缀名 48 | mapping: { 49 | ".html": "nunjucks" 50 | }, 51 | root: path.join(appInfo.baseDir, "dist/view") 52 | }; 53 | 54 | config.static = { 55 | prefix: "/", 56 | gzip: true, 57 | // maxAge: 60 * 60 * 24 * 30, 58 | dir: path.join(appInfo.baseDir, "dist/static") 59 | }; 60 | 61 | config.webpack = { 62 | port: 8082, 63 | appPort: 7001, 64 | proxy: true, 65 | proxyMapping: { 66 | js: "text/javascript; charset=UTF-8", 67 | css: "text/css; charset=UTF-8", 68 | json: "application/json; charset=UTF-8", 69 | html: "text/html; charset=UTF-8" 70 | }, 71 | webpackConfigList: [require("../build/webpack.dev.js")] 72 | }; 73 | 74 | config.custom = { 75 | sfHost: "https://segmentfault.com", 76 | huiboHost: "http://www.huibo.com/jobsearch/" 77 | }; 78 | 79 | config.io = { 80 | namespace: { 81 | "/": { 82 | connectionMiddleware: [], 83 | packetMiddleware: [] 84 | } 85 | }, 86 | redis: config.redis 87 | }; 88 | 89 | config.wechat = { 90 | appid: "wx4ae8afb75985097f", 91 | token: "leohandsone", 92 | key: "b5141c0eb3985d4f7ded0982b5c6f3b0" 93 | }; 94 | 95 | return config; 96 | }; 97 | -------------------------------------------------------------------------------- /config/plugin.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // had enabled by egg 4 | // exports.static = true; 5 | 6 | exports.validate = { 7 | enable: true, 8 | package: "egg-validate" 9 | }; 10 | 11 | exports.sequelize = { 12 | enable: true, 13 | package: "egg-sequelize" 14 | }; 15 | 16 | exports.redis = { 17 | enable: true, 18 | package: "egg-redis" 19 | }; 20 | 21 | exports.nunjucks = { 22 | enable: true, 23 | package: "egg-view-nunjucks" 24 | }; 25 | 26 | exports.webpack = { 27 | enable: false, 28 | package: "egg-webpack" 29 | }; 30 | 31 | exports.io = { 32 | enable: true, 33 | package: "egg-socket.io" 34 | }; 35 | 36 | exports.validate = { 37 | enable: true, 38 | package: "egg-validate" 39 | }; 40 | -------------------------------------------------------------------------------- /deploy/build.sh: -------------------------------------------------------------------------------- 1 | git checkout develop 2 | git pull develop 3 | git tag $1 4 | git push --tags 5 | rm .env 6 | echo "tag=$1" >> .env 7 | docker build -t app:$1 . -------------------------------------------------------------------------------- /deploy/module.sh: -------------------------------------------------------------------------------- 1 | docker rmi $(docker images module:base) -f 2 | docker build -t module:base -f Dockerfile.module -------------------------------------------------------------------------------- /deploy/update.sh: -------------------------------------------------------------------------------- 1 | docker-compose down 2 | if $2 3 | then 4 | docker rmi $(docker images app:$2) -f 5 | fi 6 | docker-compose up -d -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2.0" 2 | 3 | services: 4 | app: 5 | image: app:${tag} 6 | restart: always 7 | ports: 8 | - "80:7001" 9 | volumes: 10 | - "../volumes/logs:/app/logs" 11 | - ./config:/app/config 12 | links: 13 | - redis 14 | - mysql 15 | depends_on: 16 | - redis 17 | - mysql 18 | redis: 19 | image: redis:3.2 20 | restart: always 21 | ports: 22 | - "6379:6379" 23 | mysql: 24 | image: mysql:latest 25 | restart: always 26 | ports: 27 | - "3306:3306" 28 | environment: 29 | - MYSQL_DATABASE=egg 30 | - MYSQL_ROOT_PASSWORD=123456 31 | volumes: 32 | - "/data/mysql/db:/var/lib/mysql" 33 | - "/data/mysql/conf:/etc/mysql/conf.d" -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // npm run dev DO NOT read this file 4 | 5 | require("egg").startCluster({ 6 | baseDir: __dirname, 7 | workers: 1, 8 | port: process.env.PORT || 7001 // default to 7001 9 | }); 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "description": "example", 5 | "private": true, 6 | "dependencies": { 7 | "cheerio": "^1.0.0-rc.2", 8 | "egg": "^1.7.0", 9 | "egg-mysql": "^3.0.0", 10 | "egg-redis": "^1.0.2", 11 | "egg-scripts": "^1.2.0", 12 | "egg-sequelize": "^3.1.0", 13 | "egg-socket.io": "^4.0.1", 14 | "egg-validate": "^1.0.0", 15 | "egg-view-nunjucks": "^2.1.4", 16 | "lodash": "^4.17.4", 17 | "mocha": "^3.5.3", 18 | "mysql2": "^1.4.2", 19 | "node-sass": "^4.6.1", 20 | "request-promise": "^4.2.2", 21 | "uuid": "^3.1.0", 22 | "verror": "^1.10.0" 23 | }, 24 | "devDependencies": { 25 | "autod": "^2.8.0", 26 | "autod-egg": "^1.0.0", 27 | "axios": "^0.17.1", 28 | "babel-core": "^6.0.0", 29 | "babel-eslint": "^7.1.1", 30 | "babel-loader": "^6.0.0", 31 | "babel-plugin-import": "^1.6.2", 32 | "babel-plugin-transform-runtime": "^6.0.2", 33 | "babel-polyfill": "^6.26.0", 34 | "babel-preset-es2015": "^6.0.2", 35 | "clean-webpack-plugin": "", 36 | "css-loader": "^0.28.7", 37 | "egg-bin": "^3.4.0", 38 | "egg-ci": "^1.6.0", 39 | "egg-mock": "^3.7.0", 40 | "egg-webpack": "^3.0.1", 41 | "eslint": "^3.19.0", 42 | "eslint-config-egg": "^4.2.0", 43 | "extract-text-webpack-plugin": "", 44 | "file-loader": "^1.1.5", 45 | "glob": "^7.1.2", 46 | "html-webpack-plugin": "", 47 | "iview": "^2.7.3", 48 | "moment": "^2.18.1", 49 | "optimize-css-assets-webpack-plugin": "^3.2.0", 50 | "postcss-loader": "^2.0.9", 51 | "sass-loader": "^6.0.3", 52 | "socket.io-client": "^2.0.4", 53 | "style-loader": "^0.16.1", 54 | "vue": "^2.5.3", 55 | "vue-axios": "^2.0.2", 56 | "vue-loader": "^13.5.0", 57 | "vue-router": "^3.0.1", 58 | "vue-template-compiler": "^2.5.3", 59 | "vuex": "^3.0.1", 60 | "webpack": "^3.8.1", 61 | "webpack-dev-middleware": "^1.10.1", 62 | "webpack-hot-middleware": "^2.17.1", 63 | "webpack-merge": "^4.1.1", 64 | "webstorm-disable-index": "^1.2.0" 65 | }, 66 | "engines": { 67 | "node": ">=8.0.0" 68 | }, 69 | "scripts": { 70 | "start": "egg-scripts start", 71 | "stop": "egg-scripts stop", 72 | "dev": "egg-bin dev", 73 | "debug": "egg-bin debug", 74 | "build": "webpack --config build/webpack.prod.js", 75 | "watch": "webpack --config build/webpack.dev.js -w", 76 | "report": "egg-bin cov", 77 | "test": "set NODE_ENV=test&&egg-bin test", 78 | "cov": "nyc npm test && nyc check-coverage --lines 95 --functions 95 --branches 95 egg-bin test", 79 | "lint": "eslint .", 80 | "ci": "npm run lint && npm run cov", 81 | "autod": "autod", 82 | "commitmsg": "validate-commit-msg" 83 | }, 84 | "ci": { 85 | "version": "6, 8" 86 | }, 87 | "repository": { 88 | "type": "git", 89 | "url": "" 90 | }, 91 | "pre-commit": [ 92 | "test" 93 | ], 94 | "author": "leo", 95 | "license": "MIT" 96 | } 97 | -------------------------------------------------------------------------------- /resource/assets/avatar_default.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoDreamer/letchat-server/82659a5df05034f2cbc90185204d22c12e092f29/resource/assets/avatar_default.jpg -------------------------------------------------------------------------------- /resource/assets/axios.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | // import { Message } from "iview"; 3 | // window.$Message = Message; // eslint-disable-line 4 | 5 | // 获取cookie 6 | const cookies = {}; 7 | (function fetchCookie() { // eslint-disable-line 8 | const cookie = document.cookie; // eslint-disable-line 9 | cookie.split(";").forEach(c => { 10 | const field = c.split("="); 11 | cookies[field[0].replace(/(^\s*)|(\s*$)/g, "")] = field[1]; 12 | }); 13 | })(); 14 | 15 | // 创建axios实例 16 | const instance = axios.create({ 17 | headers: { 18 | "x-csrf-token": cookies.csrfToken 19 | }, 20 | timeout: 30000, 21 | validateStatus: () => { return true; } 22 | }); 23 | 24 | instance.message = true; 25 | 26 | instance.interceptors.response.use((res) => { 27 | if (res.status < 200 || res.status >= 300 || res.data.code < 200 || res.data.code >= 300) { 28 | if (instance.message) { 29 | window.$Message.info(`${res.data.msg}` || "服务器出现错误"); // eslint-disable-line 30 | } 31 | 32 | // 暂不抛出错误 33 | // const error = new Error(); 34 | // error.res = res; 35 | // throw error; 36 | } else { 37 | return res; 38 | } 39 | }); 40 | 41 | module.exports = instance; 42 | -------------------------------------------------------------------------------- /resource/assets/common.scss: -------------------------------------------------------------------------------- 1 | // 变量 2 | $gray: #777777 !default; 3 | 4 | body { 5 | overflow-x: hidden; 6 | } 7 | 8 | // 共用类样式 9 | .main_content { 10 | width: 100%; 11 | min-height: 900px; 12 | } -------------------------------------------------------------------------------- /resource/assets/main_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoDreamer/letchat-server/82659a5df05034f2cbc90185204d22c12e092f29/resource/assets/main_bg.jpg -------------------------------------------------------------------------------- /resource/components/cover.vue: -------------------------------------------------------------------------------- 1 | 4 | 21 | -------------------------------------------------------------------------------- /resource/components/login.vue: -------------------------------------------------------------------------------- 1 | 25 | 69 | -------------------------------------------------------------------------------- /resource/components/nav.vue: -------------------------------------------------------------------------------- 1 | 36 | 91 | -------------------------------------------------------------------------------- /resource/components/visit.vue: -------------------------------------------------------------------------------- 1 | 15 | 42 | -------------------------------------------------------------------------------- /resource/pages/chat/app.vue: -------------------------------------------------------------------------------- 1 | 116 | 221 | -------------------------------------------------------------------------------- /resource/pages/chat/appBackup.vue: -------------------------------------------------------------------------------- 1 | 42 | 115 | -------------------------------------------------------------------------------- /resource/pages/chat/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Chat 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 14 |
15 | 16 | -------------------------------------------------------------------------------- /resource/pages/chat/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./app"; 3 | import Vuex from "vuex"; 4 | import { stores } from "../../store/pages/chat"; 5 | import VueAxios from "vue-axios"; 6 | import axios from "assets/axios"; 7 | import "root/node_modules/iview/dist/styles/iview.css"; 8 | 9 | // vuex 10 | Vue.use(Vuex); 11 | Vue.use(VueAxios, axios); 12 | 13 | // 按需引入iview组件 14 | import { Button, Form, Input, Icon, Message, Avatar, Badge } from "iview"; 15 | Vue.component("Form", Form); 16 | Vue.component("FormItem", Form.Item); 17 | Vue.component("Input", Input); 18 | Vue.component("Button", Button); 19 | Vue.component("Icon", Icon); 20 | Vue.component("Avatar", Avatar); 21 | Vue.component("Badge", Badge); 22 | Vue.prototype.$Message = Message; 23 | window.$Message = Message; 24 | 25 | // init vuex 26 | const store = new Vuex.Store(stores); 27 | Vue.config.debug = true; 28 | 29 | // init vue 30 | new Vue({ 31 | el: "#app", 32 | template: "", 33 | components: { App }, 34 | store 35 | }); 36 | -------------------------------------------------------------------------------- /resource/pages/example/app.vue: -------------------------------------------------------------------------------- 1 | 4 | 9 | -------------------------------------------------------------------------------- /resource/pages/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Example 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 14 |
15 | 16 | -------------------------------------------------------------------------------- /resource/pages/example/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./app"; 3 | import Vuex from "vuex"; 4 | import Router from "vue-router"; 5 | import { routes } from "../../router"; 6 | import { stores } from "../../store"; 7 | import VueAxios from "vue-axios"; 8 | import axios from "assets/axios"; 9 | import "root/node_modules/iview/dist/styles/iview.css"; 10 | 11 | // vuex 12 | Vue.use(Vuex); 13 | Vue.use(Router); 14 | Vue.use(VueAxios, axios); 15 | 16 | // 按需引入iview组件 17 | import { Form, Message } from "iview"; 18 | Vue.component("Form", Form); 19 | Vue.prototype.$Message = Message; 20 | 21 | // init vuex 22 | const router = new Router({ 23 | routes 24 | }); 25 | const store = new Vuex.Store(stores); 26 | Vue.config.debug = true; 27 | 28 | // init vue 29 | new Vue({ 30 | el: "#app", 31 | template: "", 32 | components: { App }, 33 | router, 34 | store 35 | }); 36 | -------------------------------------------------------------------------------- /resource/pages/index/app.vue: -------------------------------------------------------------------------------- 1 | 5 | 14 | -------------------------------------------------------------------------------- /resource/pages/index/blog.vue: -------------------------------------------------------------------------------- 1 | 26 | 56 | -------------------------------------------------------------------------------- /resource/pages/index/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leo Website 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 |
16 | 17 | -------------------------------------------------------------------------------- /resource/pages/index/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./app"; 3 | import Router from "vue-router"; 4 | import Vuex from "vuex"; 5 | import VueAxios from "vue-axios"; 6 | import axios from "assets/axios"; 7 | import { routes } from "../../router"; 8 | import { stores } from "../../store"; 9 | import "root/node_modules/iview/dist/styles/iview.css"; 10 | 11 | // use router vuex axios 12 | Vue.use(Router); 13 | Vue.use(Vuex); 14 | Vue.use(VueAxios, axios); 15 | 16 | // 按需引入iview组件 17 | import { Card, Timeline, Menu, Icon, Progress, Button, Form, Input, Message } from "iview"; 18 | import { Row, Col } from "iview/src/components/grid"; 19 | Vue.component("Card", Card); 20 | Vue.component("Timeline", Timeline); 21 | Vue.component("MenuItem", Menu.Item); 22 | Vue.component("Menu", Menu); 23 | Vue.component("Row", Row); 24 | Vue.component("Col", Col); 25 | Vue.component("Icon", Icon); 26 | Vue.component("Button", Button); 27 | Vue.component("TimelineItem", Timeline.Item); 28 | Vue.component("Progress", Progress); 29 | Vue.component("Form", Form); 30 | Vue.component("FormItem", Form.Item); 31 | Vue.component("Input", Input); 32 | Vue.component("Message", Message); 33 | 34 | // init router and vuex 35 | const router = new Router({ 36 | routes 37 | }); 38 | const store = new Vuex.Store(stores); 39 | Vue.config.debug = true; 40 | 41 | Vue.prototype.$Message = Message; 42 | window.$Message = Message; 43 | 44 | // init vue 45 | new Vue({ 46 | el: "#app", 47 | template: "", 48 | components: { App }, 49 | router, 50 | store 51 | }); 52 | -------------------------------------------------------------------------------- /resource/pages/index/index.vue: -------------------------------------------------------------------------------- 1 | 23 | 42 | -------------------------------------------------------------------------------- /resource/pages/index/introduce.vue: -------------------------------------------------------------------------------- 1 | 81 | 123 | -------------------------------------------------------------------------------- /resource/pages/index/project.vue: -------------------------------------------------------------------------------- 1 | 17 | 46 | -------------------------------------------------------------------------------- /resource/pages/spider/app.vue: -------------------------------------------------------------------------------- 1 | 34 | 112 | -------------------------------------------------------------------------------- /resource/pages/spider/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Spider 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 14 |
15 | 16 | -------------------------------------------------------------------------------- /resource/pages/spider/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./app"; 3 | import Vuex from "vuex"; 4 | import Router from "vue-router"; 5 | import { stores } from "../../store/pages/spider"; 6 | import { routes } from "../../router/spider"; 7 | import VueAxios from "vue-axios"; 8 | import axios from "assets/axios"; 9 | import "root/node_modules/iview/dist/styles/iview.css"; 10 | 11 | // vuex 12 | Vue.use(Vuex); 13 | Vue.use(Router); 14 | Vue.use(VueAxios, axios); 15 | 16 | // 按需引入iview组件 17 | import { Table, Timeline, Menu, Icon, Progress, Cascader, 18 | Button, Form, Input, Message, Dropdown, Spin } from "iview"; 19 | import { Row, Col } from "iview/src/components/grid"; 20 | Vue.component("Table", Table); 21 | Vue.component("MenuItem", Menu.Item); 22 | Vue.component("Menu", Menu); 23 | Vue.component("Row", Row); 24 | Vue.component("Col", Col); 25 | Vue.component("Icon", Icon); 26 | Vue.component("Button", Button); 27 | Vue.component("TimelineItem", Timeline.Item); 28 | Vue.component("Progress", Progress); 29 | Vue.component("Form", Form); 30 | Vue.component("FormItem", Form.Item); 31 | Vue.component("Input", Input); 32 | Vue.component("Dropdown", Dropdown); 33 | Vue.component("DropdownMenu", Dropdown.Menu); 34 | Vue.component("DropdownItem", Dropdown.Item); 35 | Vue.component("Cascader", Cascader); 36 | Vue.component("Spin", Spin); 37 | Vue.prototype.$Message = Message; 38 | window.$Message = Message; // eslint-disable-line 39 | 40 | // init vuex 41 | const router = new Router({ 42 | routes 43 | }); 44 | const store = new Vuex.Store(stores); 45 | Vue.config.debug = true; 46 | 47 | // init vue 48 | new Vue({ 49 | el: "#app", 50 | template: "", 51 | components: { App }, 52 | router, 53 | store 54 | }); 55 | -------------------------------------------------------------------------------- /resource/router/index.js: -------------------------------------------------------------------------------- 1 | import Blog from "../pages/index/blog"; 2 | import Project from "../pages/index/project"; 3 | import Index from "../pages/index/index.vue"; 4 | import Introduce from "../pages/index/introduce"; 5 | 6 | const routes = [ 7 | { path: "/", component: Index }, 8 | { path: "/introduce", component: Introduce }, 9 | { path: "/blog", component: Blog }, 10 | { path: "/project", component: Project } 11 | ]; 12 | 13 | export { 14 | routes 15 | }; 16 | -------------------------------------------------------------------------------- /resource/router/spider.js: -------------------------------------------------------------------------------- 1 | import Index from "../pages/spider/app.vue"; 2 | 3 | const routes = [ 4 | { path: "/", component: Index } 5 | ]; 6 | 7 | export { 8 | routes 9 | }; 10 | -------------------------------------------------------------------------------- /resource/store/action.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /resource/store/index.js: -------------------------------------------------------------------------------- 1 | import * as user from "./modules/user"; 2 | import * as information from "./modules/information"; 3 | import * as show from "./modules/show"; 4 | import * as visit from "./modules/visit"; 5 | import * as actions from "./action"; 6 | 7 | const stores = { 8 | actions, 9 | modules: { 10 | user, information, visit, show 11 | } 12 | }; 13 | 14 | export { 15 | stores 16 | }; 17 | -------------------------------------------------------------------------------- /resource/store/modules/chat.js: -------------------------------------------------------------------------------- 1 | import * as types from "../types"; 2 | import io from "socket.io-client"; 3 | const socket = io(); 4 | 5 | const state = { 6 | onlineUsers: [], 7 | msgs: [ 8 | { 9 | name: "Leo_李川", 10 | time: "10:30", 11 | msg: "哈哈哈,好看", 12 | dot: true, 13 | chatMember: 2, 14 | chatId: "" 15 | }, { 16 | name: "鑫儿", 17 | time: "9:30", 18 | msg: "我饿了要吃饭" 19 | }, { 20 | name: "喜悦", 21 | time: "11:30", 22 | msg: "撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起" 23 | }, { 24 | name: "喜悦", 25 | time: "11:30", 26 | msg: "撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起" 27 | }, { 28 | name: "喜悦", 29 | time: "11:30", 30 | msg: "撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起" 31 | }, { 32 | name: "喜悦", 33 | time: "11:30", 34 | msg: "撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起撸起" 35 | } 36 | ] 37 | }; 38 | 39 | const mutations = { 40 | [types.CHAT_PATCH_ONLINEUSER] (state, users) { 41 | state.onlineUsers = []; 42 | state.onlineUsers = users; 43 | }, 44 | [types.CHAT_PATCH_MSGS] (state, { type, user, msg }) { 45 | const typeArray = ["u", "s"]; 46 | if (!~typeArray.indexOf(type)) return; 47 | state.msgs.push({ 48 | user, type, msg 49 | }); 50 | } 51 | }; 52 | 53 | const actions = { 54 | patchMsgs ({ commit }) { 55 | socket.on("message", (data) => { 56 | commit("CHAT_PATCH_MSGS", { 57 | user: data.name, 58 | type: "u", 59 | msg: data.msg 60 | }); 61 | }); 62 | }, 63 | createMsg ({ commit, state }, msg) { 64 | if (msg === "") return window.$Message.info("请输入消息内容"); 65 | socket.emit("message", { 66 | userId: this.state.user.id, 67 | name: this.state.user.name, 68 | msg: msg 69 | }); 70 | }, 71 | patchOnlineUser ({ commit, state }) { 72 | socket.on("online_user", (data) => { 73 | state.onlineUsers = []; 74 | commit("CHAT_PATCH_ONLINEUSER", data.users); 75 | }); 76 | }, 77 | userJoin ({ commit }) { 78 | socket.on("login", (data) => { 79 | commit("CHAT_PATCH_MSGS", { 80 | user: data.name, 81 | type: "s", 82 | msg: `${data.name}加入了聊天` 83 | }); 84 | }); 85 | }, 86 | userLogin () { 87 | socket.emit("login", this.state.user); 88 | } 89 | }; 90 | 91 | const getters = {}; 92 | 93 | export { 94 | state, mutations, actions, getters 95 | }; 96 | -------------------------------------------------------------------------------- /resource/store/modules/information.js: -------------------------------------------------------------------------------- 1 | import * as types from "../types"; 2 | 3 | const state = { 4 | projects: [], 5 | blogs: [], 6 | introduce: { 7 | skills: [], 8 | experiences: [] 9 | }, 10 | leaveMsgs: [] 11 | }; 12 | 13 | const getters = {}; 14 | 15 | const mutations = { 16 | [types.INFORMATION_CREATE] (state, information) { 17 | state.introduce.skills.push(...information.skills); 18 | state.introduce.experiences.push(...information.experiences); 19 | state.projects.push(...information.projects); 20 | state.blogs.push(...information.blogs); 21 | }, 22 | [types.INFORMATION_LEAVEMSG] (state, msgs) { 23 | state.leaveMsgs.unshift(...msgs); 24 | } 25 | }; 26 | 27 | const actions = { 28 | createInformation ({ commit }) { 29 | this._vm.axios.get("/data/index").then(resp => { 30 | const data = resp.data.data.introduce; 31 | commit(types.INFORMATION_CREATE, { 32 | skills: data.skills, 33 | experiences: data.experiences, 34 | projects: data.projects, 35 | blogs: resp.data.data.blogs 36 | }); 37 | }); 38 | }, 39 | fetchLeaveMsgs ({ commit }) { 40 | this._vm.axios.get("/visits/leave_msg").then(resp => { 41 | const data = resp.data.data.msgs; 42 | commit(types.INFORMATION_LEAVEMSG, data); 43 | }); 44 | } 45 | }; 46 | 47 | export { 48 | state, getters, actions, mutations 49 | }; 50 | -------------------------------------------------------------------------------- /resource/store/modules/show.js: -------------------------------------------------------------------------------- 1 | import * as types from "../types"; 2 | 3 | const state = { 4 | loginTip: false, 5 | loginContent: false, 6 | cover: false, 7 | chat: true, 8 | spin: false 9 | }; 10 | 11 | const getters = {}; 12 | 13 | const mutations = { 14 | [types.SHOW_PATCH] (state, { key, value }) { 15 | state[key] = value; 16 | } 17 | }; 18 | 19 | const actions = {}; 20 | 21 | export { 22 | state, getters, actions, mutations 23 | }; 24 | -------------------------------------------------------------------------------- /resource/store/modules/spider.js: -------------------------------------------------------------------------------- 1 | import * as types from "../types"; 2 | 3 | const state = { 4 | originMap: {}, 5 | jobs: [], 6 | addrList: [] 7 | }; 8 | 9 | const mutations = { 10 | [types.SPIDER_CREATE_ORIGINMAP] (state, map) { 11 | state.originMap = map; 12 | }, 13 | [types.SPIDER_PATCH_JOBS] (state, jobs) { 14 | state.jobs = jobs; 15 | }, 16 | [types.SPIDER_PATCH_ADDRLIST] (state, addrList) { 17 | state.addrList = addrList; 18 | } 19 | }; 20 | 21 | const actions = { 22 | createIntDate ({ commit }) { 23 | this._vm.axios.get("/data/index").then(resp => { 24 | const data = resp.data.data.spider; 25 | commit(types.SPIDER_CREATE_ORIGINMAP, data.originMap ); 26 | commit(types.SPIDER_PATCH_ADDRLIST, data.addrList ); 27 | }); 28 | }, 29 | patchJobs ({ commit }, { key, number }) { 30 | commit("SHOW_PATCH", { 31 | key: "spin", 32 | value: true 33 | }); 34 | this._vm.axios.get(`/spider/huibo?key=${key}&number=${number}`) 35 | .then(resp => { 36 | if (!resp) return; 37 | if ( resp.data.jobs.length === 0) window.$Message.info("暂未找到条件相关职位"); 38 | commit(types.SPIDER_PATCH_JOBS, resp.data.jobs); 39 | commit("SHOW_PATCH", { 40 | key: "spin", 41 | value: false 42 | }); 43 | }); 44 | } 45 | }; 46 | 47 | const getters = {}; 48 | 49 | export { 50 | state, mutations, actions, getters 51 | }; 52 | -------------------------------------------------------------------------------- /resource/store/modules/user.js: -------------------------------------------------------------------------------- 1 | import * as types from "../types"; 2 | 3 | const state = { 4 | name: "Leo", 5 | id: undefined, 6 | role: undefined, 7 | login: undefined 8 | }; 9 | 10 | const getters = {}; 11 | 12 | const mutations = { 13 | [types.USER_CREATE] (state, user) { 14 | Object.assign(state, user); 15 | }, 16 | [types.USER_DELETE] (state) { 17 | Object.assign(state, { name: "Leo" }); 18 | } 19 | }; 20 | 21 | const actions = { 22 | createUser ({ commit }, { name, passwd }) { 23 | // 后端模板注入User时从全局变量获取 24 | if (window.global.user) { 25 | commit(types.USER_CREATE, window.global.user); 26 | return; 27 | } 28 | 29 | if (!name && !passwd) return; 30 | // 登录请求时提交commit 31 | commit("SHOW_PATCH", { key: "cover", value: true }); 32 | this._vm.axios.post("/auth/login", { passwd, name }).then(resp => { 33 | if (resp.data.code !== 200) { 34 | window.$Message.info(resp.data.message); 35 | return; 36 | } 37 | commit(types.USER_CREATE, resp.data.data); 38 | commit("SHOW_PATCH", { key: "cover", value: false }); 39 | commit("SHOW_PATCH", { key: "loginContent", value: false }); 40 | }); 41 | }, 42 | deleteUser ({ commit }) { 43 | commit(types.USER_DELETE); 44 | } 45 | }; 46 | 47 | export { 48 | state, getters, actions, mutations 49 | }; 50 | -------------------------------------------------------------------------------- /resource/store/modules/visit.js: -------------------------------------------------------------------------------- 1 | import * as types from "../types"; 2 | 3 | const state = { 4 | visits: 0, 5 | vote: false, 6 | vote_times: 0, 7 | leaveMsgs: [] 8 | }; 9 | 10 | const mutations = { 11 | [types.VISIT_PATCH_VISITS] (state, num) { 12 | state.visits = num; 13 | }, 14 | [types.VISIT_PATCH_VOTETIMES] (state, num) { 15 | state.vote_times = num; 16 | }, 17 | [types.VISIT_PATCH_VOTE] (state, vote) { 18 | state.vote = vote; 19 | }, 20 | [types.VISIT_PATCH_LEAVEMSGS] (state, msgs) { 21 | state.leaveMsgs = msgs; 22 | } 23 | 24 | }; 25 | 26 | const actions = { 27 | getVisits ({ commit }) { 28 | this._vm.axios.get("/visits/visit") 29 | .then(resp => { 30 | const data = resp.data.data; 31 | commit(types.VISIT_PATCH_VISITS, data.visit_times); 32 | commit(types.VISIT_PATCH_VOTETIMES, data.vote_times); 33 | commit(types.VISIT_PATCH_VOTE, data.vote); 34 | }); 35 | }, 36 | createVote ({ commit, state }) { 37 | this._vm.axios.post("/visits/vote") 38 | .then(() => { 39 | window.$Message.info("点赞成功, 感谢支持."); 40 | commit(types.VISIT_PATCH_VOTE, true); 41 | commit(types.VISIT_PATCH_VOTETIMES, state.vote_times + 1); 42 | }); 43 | }, 44 | createLeaveMsg ({ commit }, { msg, connection }) { 45 | const _self = this; 46 | this._vm.axios.post("/visits/leave_msg", { msg, connection }) 47 | .then(resp => { 48 | _self.commit(types.INFORMATION_LEAVEMSG, [resp.data.data]); 49 | window.$Message.info("留言成功, 感谢给予宝贵意见."); 50 | }); 51 | } 52 | }; 53 | 54 | const getters = {}; 55 | 56 | export { 57 | state, mutations, actions, getters 58 | }; 59 | -------------------------------------------------------------------------------- /resource/store/pages/chat.js: -------------------------------------------------------------------------------- 1 | import * as user from "../modules/user"; 2 | import * as chat from "../modules/chat"; 3 | import * as show from "../modules/show"; 4 | import * as actions from "../action"; 5 | 6 | const stores = { 7 | actions, 8 | modules: { 9 | user, chat, show 10 | } 11 | }; 12 | 13 | export { 14 | stores 15 | }; 16 | -------------------------------------------------------------------------------- /resource/store/pages/spider.js: -------------------------------------------------------------------------------- 1 | import * as user from "../modules/user"; 2 | import * as spider from "../modules/spider"; 3 | import * as show from "../modules/show"; 4 | import * as actions from "../action"; 5 | 6 | const stores = { 7 | actions, 8 | modules: { 9 | user, spider, show 10 | } 11 | }; 12 | 13 | export { 14 | stores 15 | }; 16 | -------------------------------------------------------------------------------- /resource/store/types.js: -------------------------------------------------------------------------------- 1 | const USER_CREATE = "USER_CREATE"; 2 | const USER_DELETE = "USER_DELETE"; 3 | const INFORMATION_CREATE = "INFORMATION_CREATE"; 4 | const INFORMATION_LEAVEMSG = "INFORMATION_LEAVEMSG"; 5 | const SHOW_PATCH = "SHOW_PATCH"; 6 | const CHAT_PATCH_MSG = "CHAT_PATCH_MSG"; 7 | const CHAT_PATCH_ONLINEUSER = "CHAT_PATCH_ONLINEUSER"; 8 | const CHAT_PATCH_MSGS = "CHAT_PATCH_MSGS"; 9 | const SPIDER_CREATE_ORIGINMAP = "SPIDER_CREATE_ORIGINMAP"; 10 | const SPIDER_PATCH_JOBS = "SPIDER_PATCH_JOBS"; 11 | const SPIDER_PATCH_ADDRLIST = "SPIDER_PATCH_ADDRLIST"; 12 | const VISIT_PATCH_VISITS = "VISIT_PATCH_VISITS"; 13 | const VISIT_PATCH_VOTE = "VISIT_PATCH_VOTE"; 14 | const VISIT_PATCH_LEAVEMSGS = "VISIT_PATCH_LEAVEMSGS"; 15 | const VISIT_PATCH_VOTETIMES = "VISIT_PATCH_VOTETIMES"; 16 | 17 | export { 18 | USER_CREATE, 19 | USER_DELETE, 20 | INFORMATION_CREATE, 21 | INFORMATION_LEAVEMSG, 22 | SHOW_PATCH, 23 | CHAT_PATCH_MSG, 24 | CHAT_PATCH_ONLINEUSER, 25 | CHAT_PATCH_MSGS, 26 | SPIDER_CREATE_ORIGINMAP, 27 | SPIDER_PATCH_JOBS, 28 | SPIDER_PATCH_ADDRLIST, 29 | VISIT_PATCH_VISITS, 30 | VISIT_PATCH_VOTE, 31 | VISIT_PATCH_LEAVEMSGS, 32 | VISIT_PATCH_VOTETIMES 33 | }; 34 | -------------------------------------------------------------------------------- /test/app/controller/auth.test.js: -------------------------------------------------------------------------------- 1 | const mm = require("egg-mock"); 2 | const assert = require("assert"); 3 | 4 | describe("Auth Controller", () => { 5 | let app; 6 | const user = { 7 | name: "leo", 8 | passwd: "111111" 9 | }; 10 | before(() => { 11 | app = mm.app(); 12 | return app.ready(); 13 | }); 14 | 15 | afterEach(mm.restore); 16 | after(() => app.close()); 17 | 18 | it("should assert", () => { 19 | const pkg = require("../../../package.json"); 20 | assert(app.config.keys.startsWith(pkg.name)); 21 | }); 22 | 23 | it("注册成功", () => { 24 | app.mockCsrf(); 25 | return app.httpRequest() 26 | .post("/auth/register") 27 | .send(user) 28 | .expect(204); 29 | }); 30 | 31 | it("登录成功", () => { 32 | app.mockCsrf(); 33 | return app.httpRequest() 34 | .post("/auth/login") 35 | .send(user) 36 | .expect(204); 37 | }); 38 | 39 | }); 40 | -------------------------------------------------------------------------------- /test/app/controller/page.test.js: -------------------------------------------------------------------------------- 1 | const mm = require("egg-mock"); 2 | const assert = require("assert"); 3 | 4 | describe("Page Controller", () => { 5 | let app; 6 | before(() => { 7 | app = mm.app(); 8 | return app.ready(); 9 | }); 10 | 11 | afterEach(mm.restore); 12 | after(() => app.close()); 13 | 14 | it("获取页面", async () => { 15 | const resp = await app.httpRequest() 16 | .get("/index") 17 | .expect(200); 18 | assert.equal(resp.type, "text/html"); 19 | }); 20 | 21 | }); 22 | --------------------------------------------------------------------------------