{{msg.user}} :
20 | {{msg.msg}} 21 |├── .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 |
2 |
3 |
4 |
21 |
--------------------------------------------------------------------------------
/resource/components/login.vue:
--------------------------------------------------------------------------------
1 |
2 |
58 | {{msg.name}}
59 | {{msg.time}}
60 | {{user.name}} {{msg.user}} :
30 |
33 |
Hi,    I    am    Leo
11 |- Web Developer -
12 |我是Leo,是一个Web开发者,目前在重庆从事Node/前端方向开发
13 |关于我
11 |12 | 我叫 李川-男-25岁 13 |
14 |15 | 毕业于 重庆邮电大学 16 |
17 |18 | 联系我 leolichuan@qq.com 19 |
20 |
21 |
22 |
专业技能
35 |工作经历与项目
44 |{{e.time}}
47 |主要工作:
48 |    {{w}}
49 |{{p.name}} - {{p.position}}
51 |项目描述: {{p.description}}
52 |项目地址: {{p.link}}
53 |项目工作: {{p.workDescription}}
54 |给我留言
62 |71 | {{"匿名"}}: 72 | {{msg.msg}} 73 |
74 |{{p.name}}
11 |