From b978b16e6a10f745727151ae2b72f85dc27876e0 Mon Sep 17 00:00:00 2001 From: yulixing Date: Mon, 13 May 2019 20:14:39 +0800 Subject: [PATCH] init --- .babelrc | 3 + .gitignore | 22 ++ package.json | 42 ++++ src/app.js | 35 +++ src/bin/express.js | 183 ++++++++++++++++ src/controllers/common/index.js | 11 + src/models/admin/Admin.js | 149 +++++++++++++ src/models/admin/AdminLog.js | 31 +++ src/models/admin/Game.js | 30 +++ src/models/admin/SystemDic.js | 106 +++++++++ src/models/admin/TemplateMsgRecord.js | 17 ++ src/models/beagle/WeappFormID.js | 50 +++++ src/models/dalmatian/ShortUrl.js | 32 +++ src/models/ghost/Account.js | 112 ++++++++++ src/models/ghost/AppInfo.js | 32 +++ src/models/ghost/CardInfo.js | 68 ++++++ src/models/ghost/EmulatedGames.js | 257 ++++++++++++++++++++++ src/models/ghost/ExchangeRecord.js | 76 +++++++ src/models/ghost/Gift.js | 87 ++++++++ src/models/ghost/Message.js | 34 +++ src/models/ghost/RecommendGames.js | 62 ++++++ src/models/ghost/ScoreRecord.js | 50 +++++ src/models/snoopy/ChinaArea.js | 23 ++ src/models/snoopy/ChinaRegion.js | 45 ++++ src/models/snoopy/CustomerReplay.js | 49 +++++ src/models/snoopy/GameInfo.js | 116 ++++++++++ src/models/snoopy/GameShareImage.js | 97 +++++++++ src/models/snoopy/Gift.js | 96 +++++++++ src/models/snoopy/GiftHistory.js | 27 +++ src/models/snoopy/GiftPack.js | 191 ++++++++++++++++ src/models/snoopy/Puzzle.js | 58 +++++ src/models/snoopy/PuzzleLevel.js | 82 +++++++ src/router/index.js | 11 + src/schedule/weappmsg.schedule.js | 81 +++++++ src/utils/admin_keeper.js | 109 ++++++++++ src/utils/captcha.js | 75 +++++++ src/utils/cdn.utils.js | 99 +++++++++ src/utils/db.util.js | 51 +++++ src/utils/error-utils.js | 58 +++++ src/utils/express-utils.js | 53 +++++ src/utils/file.utils.js | 131 +++++++++++ src/utils/gamesvr.utils.js | 299 ++++++++++++++++++++++++++ src/utils/gm-logs.js | 21 ++ src/utils/logger.js | 122 +++++++++++ src/utils/string.utils.js | 236 ++++++++++++++++++++ src/utils/wechat.utils.js | 128 +++++++++++ test/test.js | 62 ++++++ test/test2.js | 88 ++++++++ 48 files changed, 3897 insertions(+) create mode 100644 .babelrc create mode 100644 .gitignore create mode 100644 package.json create mode 100644 src/app.js create mode 100644 src/bin/express.js create mode 100644 src/controllers/common/index.js create mode 100644 src/models/admin/Admin.js create mode 100644 src/models/admin/AdminLog.js create mode 100644 src/models/admin/Game.js create mode 100644 src/models/admin/SystemDic.js create mode 100644 src/models/admin/TemplateMsgRecord.js create mode 100644 src/models/beagle/WeappFormID.js create mode 100644 src/models/dalmatian/ShortUrl.js create mode 100644 src/models/ghost/Account.js create mode 100644 src/models/ghost/AppInfo.js create mode 100644 src/models/ghost/CardInfo.js create mode 100644 src/models/ghost/EmulatedGames.js create mode 100644 src/models/ghost/ExchangeRecord.js create mode 100644 src/models/ghost/Gift.js create mode 100644 src/models/ghost/Message.js create mode 100644 src/models/ghost/RecommendGames.js create mode 100644 src/models/ghost/ScoreRecord.js create mode 100644 src/models/snoopy/ChinaArea.js create mode 100644 src/models/snoopy/ChinaRegion.js create mode 100644 src/models/snoopy/CustomerReplay.js create mode 100644 src/models/snoopy/GameInfo.js create mode 100644 src/models/snoopy/GameShareImage.js create mode 100644 src/models/snoopy/Gift.js create mode 100644 src/models/snoopy/GiftHistory.js create mode 100644 src/models/snoopy/GiftPack.js create mode 100644 src/models/snoopy/Puzzle.js create mode 100644 src/models/snoopy/PuzzleLevel.js create mode 100644 src/router/index.js create mode 100644 src/schedule/weappmsg.schedule.js create mode 100644 src/utils/admin_keeper.js create mode 100644 src/utils/captcha.js create mode 100644 src/utils/cdn.utils.js create mode 100644 src/utils/db.util.js create mode 100644 src/utils/error-utils.js create mode 100644 src/utils/express-utils.js create mode 100644 src/utils/file.utils.js create mode 100644 src/utils/gamesvr.utils.js create mode 100644 src/utils/gm-logs.js create mode 100644 src/utils/logger.js create mode 100644 src/utils/string.utils.js create mode 100644 src/utils/wechat.utils.js create mode 100644 test/test.js create mode 100644 test/test2.js diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..69f50d5 --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["env"] +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..64e38d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +/.env +/.idea/ + +**/node_modules +**/.DS_Store + +/config/config.js +/config/game_dic.js + +/public +/logs +/build +/dist +/lib +rev-manifest.json +/yarn.lock +/nohup.out +/package-lock.json +/dist.tar.gz +*.swp +/logsgarfield +.vscode/launch.json diff --git a/package.json b/package.json new file mode 100644 index 0000000..65aae58 --- /dev/null +++ b/package.json @@ -0,0 +1,42 @@ +{ + "name": "admin-be", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "dev": "cross-env nodemon src/app.js --exec babel-node " + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "bluebird": "^3.5.4", + "body-parser": "^1.19.0", + "bunyan": "^1.8.12", + "compression": "^1.7.4", + "connect-mongo": "^2.0.3", + "cookie-parser": "^1.4.4", + "express": "^4.16.4", + "express-flash": "0.0.2", + "express-session": "^1.16.1", + "express-validator": "^5.3.1", + "file-stream-rotator": "^0.4.1", + "fs-extra": "^8.0.0", + "glob": "^7.1.4", + "helmet": "^3.18.0", + "ldapjs": "^1.0.2", + "method-override": "^3.0.0", + "mongoose": "^5.5.7", + "morgan": "^1.9.1", + "node-schedule": "^1.3.2", + "nodemon": "^1.19.0", + "request": "^2.88.0", + "serve-favicon": "^2.5.0" + }, + "devDependencies": { + "babel-cli": "^6.26.0", + "babel-preset-env": "^1.7.0", + "cross-env": "^5.2.0" + } +} diff --git a/src/app.js b/src/app.js new file mode 100644 index 0000000..43fd1cb --- /dev/null +++ b/src/app.js @@ -0,0 +1,35 @@ +'use strict'; +import mongoose from 'mongoose'; +import config from '../config/config'; +import app from './bin/express'; +import glob from 'glob'; +import Promise from 'bluebird'; +import logger from './utils/logger'; +import http from 'http'; +import msgSchedule from './schedule/weappmsg.schedule'; + +mongoose.Promise = Promise; + +const db = mongoose.connection; + +db.on('error', function(err) { + logger.error(err); + process.exit(1); +}); +db.once('open', function() { + logger.info('Connected to db.'); +}); +mongoose.connect(config.db_admin, {promiseLibrary: Promise, useNewUrlParser: true}); +const models = glob.sync(config.root + './src/models/*.js'); +models.forEach(function(model) { + require(model); +}); + + +const server = http.createServer(app); + +msgSchedule.scheduleSendAll(); + +server.listen(config.port, function() { + logger.info(`${config.app.name} garfield server listening on port ${config.port}`); +}); diff --git a/src/bin/express.js b/src/bin/express.js new file mode 100644 index 0000000..deb62b7 --- /dev/null +++ b/src/bin/express.js @@ -0,0 +1,183 @@ +'use strict'; + +import express from 'express'; +import expressValidator from 'express-validator'; +import flash from 'express-flash'; +import session from 'express-session'; +import bodyParser from 'body-parser'; +import cookieParser from 'cookie-parser'; +import compress from 'compression'; +import methodOverride from 'method-override'; +import helmet from 'helmet'; +// import favicon from 'serve-favicon'; +import FileStreamRotator from 'file-stream-rotator'; +import morgan from 'morgan'; +import fs from 'fs'; +import logger from './../utils/logger' +import expressUtils from './../utils/express-utils'; +import config from './../../config/config'; +import connectMongo from 'connect-mongo'; +import routes from './../router/index'; + +const app = express(); +const MongoStore = connectMongo(session); +const env = process.env.NODE_ENV || 'development'; +const isDev = env === 'development'; +app.locals.ENV = env; +app.locals.ENV_DEVELOPMENT = isDev; + +app.use(helmet()); + +app.disable('x-powered-by'); +// app.use(favicon(config.root + '/public/img/favicon/favicon.ico')); + +const logDir = config.logs_path; +fs.existsSync(logDir) || fs.mkdirSync(logDir); + +/** + * 记录除favicon之外的所有请求 + */ +const accessLogStream = FileStreamRotator.getStream({ + date_format: 'YYYYMMDD', + filename: logDir + '/' + config.app.name + '-access-%DATE%.log', + frequency: 'daily', + verbose: false +}); +/** + * 仅记录失败的请求 + */ +const errorLogStream = FileStreamRotator.getStream({ + date_format: 'YYYYMMDD', + filename: logDir + '/' + config.app.name + '-error-%DATE%.log', + frequency: 'daily', + verbose: false +}); +/** + * 记录非静态文件请求 + */ +const requestLogStream = FileStreamRotator.getStream({ + date_format: 'YYYYMMDD', + filename: logDir + '/' + config.app.name + '-request-%DATE%.log', + frequency: 'daily', + verbose: false +}); + +morgan.token('remote-addr', function(req, res) { + const ip = + req.headers['x-real-ip'] || + req.ip || + req._remoteAddress || + (req.connection && req.connection.remoteAddress) || + undefined; + req._remoteAddress = ip; + return ip; +}); + +if (isDev) { + // app.use(morgan('dev' )); +} +app.use( + morgan('combined', { + stream: accessLogStream, + skip: function(req, res) { + return req.method === 'HEAD'; + } + }) +); +app.use( + morgan('combined', { + stream: errorLogStream, + skip: function(req, res) { + return res.statusCode < 400; + } + }) +); + +app.get('/robots.txt', function(req, res) { + res.type('text/plain'); + res.send('User-agent: *\nDisallow: /agent/'); +}); + +// -- We don't want to serve sessions for static resources +// -- Save database write on every resources +app.use(compress()); +app.use(express.static(config.root + '/public')); + +app.use( + morgan('combined', { + stream: requestLogStream, + skip: function(req, res) { + return req.method === 'HEAD'; + } + }) +); + +app.use(bodyParser.json({ limit: '5mb' })); +app.use( + bodyParser.urlencoded({ + limit: '5mb', + parameterLimit: 50000, + extended: true + }) +); +app.use(expressValidator()); +app.use(cookieParser(config.secret)); +app.use( + session({ + secret: config.secret, + resave: false, + saveUninitialized: false, + name: config.session_name, + store: new MongoStore({ + url: config.db_admin, + autoReconnect: true, + collection: 'garfield_sessions' + }) + }) +); +app.use(flash()); +app.use(expressUtils()); +app.use(methodOverride()); +app.use(function(req, res, next) { + res.locals.user = req.user; + next(); +}); + +app.all('/uploads', function(req, res, next) { + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Methods', 'PUT, GET, POST, DELETE, OPTIONS'); + res.header('Access-Control-Allow-Headers', 'X-Requested-With'); + res.header('Access-Control-Allow-Headers', 'Content-Type'); + next(); +}); +app.use('/', routes); + +app.use(function(req, res, next) { + const err = new Error('未找到您要访问的页面 (´・_・`)'); + err.status = 404; + // next(err); + err.status = err.status || 500; + if (err.status !== 404) { + logger.error(err); + } + next(err); +}); +app.use(function(err, req, res, next) { + logger.error({ + method: req.method, + path: req.path, + err_status: err.status, + err_message: err.message + }); + if (req.path.startsWith('/api')) { + res.json({ errcode: 10, errmsg: err.message }); + } else { + res.render('error', { + message: err.status === 404 ? err.message : '服务器君开小差啦 @(・●・)@', + error: err, + title: err.status + }); + } +}); + +export default app; diff --git a/src/controllers/common/index.js b/src/controllers/common/index.js new file mode 100644 index 0000000..7ced2c2 --- /dev/null +++ b/src/controllers/common/index.js @@ -0,0 +1,11 @@ +import {Router} from 'express'; + +const router = new Router(); + +router.get('/test', async(req, res, next) => { + res.send({ + msg: 'test!' + }) +}) + +export default router; \ No newline at end of file diff --git a/src/models/admin/Admin.js b/src/models/admin/Admin.js new file mode 100644 index 0000000..a31aed0 --- /dev/null +++ b/src/models/admin/Admin.js @@ -0,0 +1,149 @@ +'use strict'; +import mongoose from 'mongoose'; +import passportLocalMongoose from 'passport-local-mongoose'; + + +const AdminActionSchema = new mongoose.Schema({ + _id: {type: String, required: true}, + name: {type: String, required: true}, + paths: [{ + method: String, + path: String, + }], + + deleted: {type: Boolean, default: false}, + deleted_time: {type: Date}, +}, { + collection: 'admin_actions', + timestamps: true, +}); + +const AdminAction = mongoose.model('AdminAction', AdminActionSchema); + +const AdminRoleSchema = new mongoose.Schema({ + _id: {type: String, required: true}, + name: {type: String, required: true}, + permissions: [{type: String, ref: 'AdminAction'}], + + deleted: {type: Boolean, default: false}, + deleted_time: {type: Date}, +}, { + collection: 'admin_roles', + timestamps: true, +}); + +const AdminRole = mongoose.model('AdminRole', AdminRoleSchema); + +/** + * 用户表不需要添加 timestamps: true + * + * 因为 createdAt 可以从ObjectID中提取, updatedAt 会因为last和ateemps随时变更, + * 如果需要跟踪关键数据的变更时间, 应添加独立自管的 updatedAt, + * 或者直接使用 immutable data 追踪变化 + */ +const AdminSchema = new mongoose.Schema({ + username: String, + password: String, + + roles: [{type: String, ref: 'AdminRole'}], + permissions: [{type: String, ref: 'AdminAction'}], + games: [{type: String}], + profile: { + name: String, + gender: String, + }, + + comment: String, + + createdBy: {type: String, ref: 'Admin', required: true}, + lastModifiedBy: {type: String, ref: 'Admin', required: true}, + + locked: {type: Boolean, default: false}, + locked_time: {type: Date}, + + deleted: {type: Boolean, default: false}, + delete_time: {type: Date}, + + lastLogin: Date, // passport-local-mongoose 添加的 last 字段记录的是最后一次登录尝试, + // 而不是最后一次成功登录 +}); + +AdminSchema.virtual('createdAt').get(function() { + return this._id.getTimestamp(); +}); + +/** + * TODO this is far away from elegant, maybe change it later. + */ +AdminSchema.virtual('hasSysAdmin').get(function() { + return this.roles.includes('sys_admin'); +}); +AdminSchema.virtual('hasNavSystem').get(function() { + let yes = this.roles.includes('admin'); + yes = yes || this.permissions.includes('edit_accounts'); + return yes; +}); + +AdminSchema.virtual('hasNavGameApi').get(function() { + let yes = this.roles.includes('admin'); + yes = yes || this.roles.includes('game_api_manager'); + yes = yes || this.permissions.includes('edit_game_apis'); + return yes; +}); + +AdminSchema.virtual('hasNavRedis').get(function() { + let yes = this.roles.includes('admin'); + yes = yes || this.roles.includes('redis_manager'); + yes = yes || this.permissions.includes('edit_redis'); + return yes; +}); + +AdminSchema.virtual('hasNavWechat').get(function() { + let yes = this.roles.includes('admin'); + yes = yes || this.roles.includes('wechat_manager'); + yes = yes || this.permissions.includes('edit_wechat'); + return yes; +}); + +AdminSchema.virtual('hasNavGM').get(function() { + let yes = this.roles.includes('admin'); + yes = yes || this.roles.includes('gm_manager'); + yes = yes || this.permissions.includes('edit_gm'); + return yes; +}); + +AdminSchema.virtual('hasNavTool').get(function() { + let yes = this.roles.includes('admin'); + yes = yes || this.roles.includes('tool_manager'); + yes = yes || this.permissions.includes('edit_tool'); + return yes; +}); + +AdminSchema.virtual('hasAdminRole').get(function() { + return this.roles.includes('admin'); +}); + +AdminSchema.methods.checkGame = function(gid) { + let yes = this.roles.includes('admin'); + yes = yes || this.games.includes(gid); + return yes; +}; + +AdminSchema.plugin(passportLocalMongoose, + { + limitAttempts: true, + usernameLowerCase: true, + maxAttempts: 10, + errorMessages: { + MissingPasswordError: '请输入密码', + AttemptTooSoonError: '您的账户当前被锁定,请稍后再试', + TooManyAttemptsError: '您的账户因错误登录次数太多而被锁定', + NoSaltValueStoredError: 'Authentication not possible. No salt value stored', + IncorrectPasswordError: '用户名或密码错误', + IncorrectUsernameError: '用户名或密码错误', + MissingUsernameError: '请输入用户名', + UserExistsError: '您输入的用户名已被使用', + }, + }); +const Admin = mongoose.model('Admin', AdminSchema); +export {Admin, AdminRole, AdminAction}; diff --git a/src/models/admin/AdminLog.js b/src/models/admin/AdminLog.js new file mode 100644 index 0000000..6a2d2cd --- /dev/null +++ b/src/models/admin/AdminLog.js @@ -0,0 +1,31 @@ +'use strict'; +import mongoose from 'mongoose'; + +const Schema = mongoose.Schema; +const ObjectId = Schema.Types.ObjectId; +/** + * 操作日志 + */ +const AdminLog = new mongoose.Schema({ + // 游戏id + admin: {type: ObjectId, ref: 'Admin'}, + username: {type: String}, + method: {type: String}, + show_name: {type: String}, + // 请求路径 + path: {type: String}, + user_agent: {type: String}, + referer: {type: String}, + // 请求的param + params: {type: Schema.Types.Mixed}, + // ip + ip: {type: String}, + // 备注 + comment: {type: String}, +}, { + collection: 'admin_logs', + timestamps: true, +}); + +export default mongoose.model('AdminLog', AdminLog); + diff --git a/src/models/admin/Game.js b/src/models/admin/Game.js new file mode 100644 index 0000000..f3553a9 --- /dev/null +++ b/src/models/admin/Game.js @@ -0,0 +1,30 @@ +'use strict'; +import mongoose from 'mongoose'; + +/** + * 游戏信息 + */ +const Game = new mongoose.Schema({ + // 游戏id + game_id: {type: String}, + // 游戏名 + name: {type: String}, + // 英文名 + name_en: {type: String}, + // 状态 + status: {type: String}, + // 平台 + platform: {type: String}, + // 备注 + comment: {type: String}, + createdBy: {type: String, ref: 'Admin'}, + lastModifiedBy: {type: String, ref: 'Admin'}, + deleted: {type: Boolean, default: false}, + delete_time: {type: Date}, +}, { + collection: 'games', + timestamps: true, +}); + +export default mongoose.model('Game', Game); + diff --git a/src/models/admin/SystemDic.js b/src/models/admin/SystemDic.js new file mode 100644 index 0000000..5d41574 --- /dev/null +++ b/src/models/admin/SystemDic.js @@ -0,0 +1,106 @@ +'use strict'; +import mongoose from 'mongoose'; + +/** + * 字典表 + */ +const Schema = mongoose.Schema; + +const SystemDicSchema = new mongoose.Schema({ + key: {type: String}, + value: {type: Schema.Types.Mixed}, + /** + * 类型 + * system: 字典类型 + * platform: 平台 + * game_cfg: 游戏配置项目 + * share_cfg: 游戏分享类型 + * game_type: 游戏类型 + * game_status: 游戏状态 + * */ + type: {type: String}, + // 备注 + comment: {type: String}, + createdBy: {type: String, ref: 'Admin'}, + lastModifiedBy: {type: String, ref: 'Admin'}, + deleted: {type: Boolean, default: false}, + delete_time: {type: Date}, + deletedBy: {type: String, ref: 'Admin'}, +}, { + collection: 'system_dics', + timestamps: true, +}); + +/** + * */ +class SystemDicClass { + /** + * 获取某一个类型的字典的Map + * @param {string} type, 字典类型 + * @return {Promise} data + * */ + static getDicMap(type) { + return this.find({type: type, deleted: false}) + .then((records) => { + const map = new Map(); + for (const record of records) { + map.set(record.key, record.value); + } + return map; + }); + } + /** + * 生成查询条件 + * @param {Object} req + * @return {Object} opt + * @return {Object} sortObj + * */ + static generateQueryOpt(req) { + let opt = {}; + const body = req.body; + const order = body.order; + const sort = body.sort ? body.sort : 'createdAt'; + const sortObj = {}; + sortObj[sort] = order === 'asc' ? 1 : -1; + if (body.keyStr) { + const orArr = [ + {key: {$regex: body.keyStr, $options: 'i'}}, + {value: {$regex: body.keyStr, $options: 'i'}}, + ]; + opt = {$or: orArr}; + } + if (body.type && body.type !== 'all') { + opt.type = body.type; + } + opt.deleted = false; + return {opt, sortObj}; + } + /** + * 保存 + * @param {Object} req + * */ + static async saveWithReq(req) { + const body = req.body; + const data = body.data; + const ignoreKeySet = new Set(['createdAt', 'updatedAt', '__v', 'deleted', 'delete_time', 'deletedBy']); + let record; + if (data._id) { + record = await this.findById(data._id); + } + if (!record) { + record = new SystemDicModel({ + createdBy: req.user.id, + }); + } + for (const key in body.data) { + if ({}.hasOwnProperty.call(body.data, key) && !ignoreKeySet.has(key)) { + record[key] = body.data[key]; + } + } + return record.save(); + } +} +SystemDicSchema.loadClass(SystemDicClass); + +const SystemDicModel = mongoose.model('SystemDic', SystemDicSchema); +export default SystemDicModel; diff --git a/src/models/admin/TemplateMsgRecord.js b/src/models/admin/TemplateMsgRecord.js new file mode 100644 index 0000000..fd12e0f --- /dev/null +++ b/src/models/admin/TemplateMsgRecord.js @@ -0,0 +1,17 @@ +'use strict'; +import mongoose from 'mongoose'; + +/** + * 模版消息发送日志 + */ +const TemplateMsgRecord = new mongoose.Schema({ + // 游戏id + app_id: {type: String}, + open_ids: [{type: String}], +}, { + collection: 'template_msg_record', + timestamps: true, +}); + +export default mongoose.model('TemplateMsgRecord', TemplateMsgRecord); + diff --git a/src/models/beagle/WeappFormID.js b/src/models/beagle/WeappFormID.js new file mode 100644 index 0000000..d43a337 --- /dev/null +++ b/src/models/beagle/WeappFormID.js @@ -0,0 +1,50 @@ +'use strict'; +import mongoose from 'mongoose'; +const Schema = mongoose.Schema; +import dbUtil from '../../utils/db.util'; +/** + * 小程序的Form ID, 用于发送推送消息 + */ +const WeappFormID = new Schema({ + account: {type: String}, + // 0: 未支付,1:已使用, -1:已过期 + status: {type: Number}, + app_id: {type: String}, + open_id: {type: String}, + form_id: {type: String}, + // 过期时间 + expire_time: {type: Date}, + // 发送时间 + send_time: {type: Date}, +}, { + collection: 'weapp_form_id', + timestamps: true, +}); + +const conn = dbUtil.getConnBeagle(); + +const WeappFormIDModel = conn.model('WeappFormID', WeappFormID); + +// 获取当天可用的formId列表, 每个用户一条 +WeappFormIDModel.getUserRecordsDay = function(appId) { + const idSet = new Set(); + return WeappFormIDModel + .find({status: 0, expire_time: {$gt: new Date()}, app_id: appId}) + .sort({createdAt: 1}) + .then((records) => { + const result = []; + for (const record of records) { + if (!idSet.has(record.open_id)) { + idSet.add(record.open_id); + result.push(record); + } + } + return result; + }); +}; +WeappFormIDModel.updateExpireTime = function() { + return WeappFormIDModel.where({status: 0, expire_time: {$lte: new Date()}}).updateMany({status: -1}).exec(); +}; +export default WeappFormIDModel; + + diff --git a/src/models/dalmatian/ShortUrl.js b/src/models/dalmatian/ShortUrl.js new file mode 100644 index 0000000..25cb51b --- /dev/null +++ b/src/models/dalmatian/ShortUrl.js @@ -0,0 +1,32 @@ +'use strict'; +import mongoose from 'mongoose'; +import shortid from 'shortid'; +import dbUtil from '../../utils/db.util'; + +const Schema = mongoose.Schema; +/** + * 短链接 + */ +const ShortUrl = new mongoose.Schema({ + _id: {type: String, default: shortid.generate}, + // 类型,page类型跳转实际url,weapp类型的话,real_url存储宣传图 + type: {type: String, required: true, default: 'page'}, + // 真实链接 + real_url: {type: String}, + image_url: {type: String}, + data: {type: Schema.Types.Mixed}, + deleted: {type: Boolean, default: false}, + deletedBy: {type: String, ref: 'Admin'}, + delete_time: {type: Date}, + createdBy: {type: String, ref: 'Admin', required: true}, + lastModifiedBy: {type: String, ref: 'Admin', required: true}, + // 备注 + comment: {type: String}, +}, { + collection: 'short_url', + timestamps: true, +}); + +const conn = dbUtil.getConnDalmatian(); + +export default conn.model('ShortUrl', ShortUrl); diff --git a/src/models/ghost/Account.js b/src/models/ghost/Account.js new file mode 100644 index 0000000..f094b89 --- /dev/null +++ b/src/models/ghost/Account.js @@ -0,0 +1,112 @@ +'use strict'; +import mongoose from 'mongoose'; +import dbUtil from '../../utils/db.util'; + +const Schema = mongoose.Schema; +/** + * 游戏账户 + */ +const Account = new Schema({ + // 公众号获取的原始信息 + wechat_info: { + wechat_id: {type: String}, + avatar: {type: String}, + nickname: {type: String}, + sex: {type: String}, + province: {type: String}, + city: {type: String}, + }, + // 金蚕游戏id + account_id: {type: String}, + session_id: {type: String}, + // 从公众号中获取的union_id + union_id: {type: String}, + // 对应的open_id + open_id: {type: String}, + comment: {type: String}, + locked: {type: Boolean, default: false}, + locked_time: {type: Date}, + // 当前积分 + score: {type: Number, default: 0}, + // 账户类型,user: 普通用户 + account_type: {type: String, default: 'user'}, + // 如果account的 is_new为true, 且invite_user等于等于用户id, 则表明该用户是这个人邀请的 + is_new: {type: Boolean, default: true}, + invite_user: {type: String, ref: 'Account'}, + last_login: {type: Date}, + last_check: {type: Date}, + // 收件人信息 + address_info: { + // 地区代码 + code: {type: String}, + // 省 + province: {type: String}, + // 市 + city: {type: String}, + // 区域 + district: {type: String}, + // 地址 + address: {type: String}, + // 收件人 + name: {type: String}, + // 联系电话 + mobile: {type: String}, + // 邮编 + postcode: {type: String}, + }, +}); + +Account.virtual('createdAt').get(function() { + return this._id.getTimestamp(); +}); + + +Account.virtual('sex_name').get(function() { + if (this.wechat_info.sex === '1') { + return '男'; + } else if (this.sex === '2') { + return '女'; + } else { + return '未指定'; + } +}); + +const conn = dbUtil.getConnGhost(); +const AccountModel = conn.model('Account', Account); + + +AccountModel.getByOpenId = (openId) => { + return AccountModel.findOne({open_id: openId}); +}; + +AccountModel.getByAccountId = (accountId) => { + return AccountModel.findOne({account_id: accountId}); +}; + +AccountModel.updateInfo = (record, body) => { + const wechatInfo = record.wechat_info || {}; + if (body.nickname) { + wechatInfo.nickname = body.nickname; + } + if (body.avatar) { + wechatInfo.avatar = body.avatar; + } + if (body.province) { + wechatInfo.province = body.province; + } + if (body.city) { + wechatInfo.city = body.city; + } + if (body.gender !== undefined) { + wechatInfo.gender = body.gender; + } + record.wechat_info = wechatInfo; + if (body.union_id) { + record.union_id = body.union_id; + } + + return record; +}; + + +export default AccountModel; diff --git a/src/models/ghost/AppInfo.js b/src/models/ghost/AppInfo.js new file mode 100644 index 0000000..2edc572 --- /dev/null +++ b/src/models/ghost/AppInfo.js @@ -0,0 +1,32 @@ +'use strict'; + +import mongoose from 'mongoose'; +import dbUtil from '../../utils/db.util'; + +const Schema = mongoose.Schema; + +/** + * 积分墙app + * */ +const AppInfo = new Schema({ + name: {type: String}, + // 已经有多少人领取 + count: {type: Number, default: 0}, + appid: {type: String}, + // 积分 + score: {type: Number}, + desc: {type: String}, + // 图标 + icon_img: {type: String}, + sort: {type: Number}, + deleted: {type: Boolean, default: false}, + delete_time: {type: Date}, +}, { + collection: 'app_info', + timestamps: true, +}); + +const conn = dbUtil.getConnGhost(); +const AppInfoModel = conn.model('AppInfo', AppInfo); + +export default AppInfoModel; diff --git a/src/models/ghost/CardInfo.js b/src/models/ghost/CardInfo.js new file mode 100644 index 0000000..3dd6f31 --- /dev/null +++ b/src/models/ghost/CardInfo.js @@ -0,0 +1,68 @@ +'use strict'; + +import mongoose from 'mongoose'; +import dbUtil from '../../utils/db.util'; + +const Schema = mongoose.Schema; + +/** + * 贺卡信息 + * */ +const CardInfo = new Schema({ + name: {type: String}, + // 已经有多少人完成纺织 + count: {type: Number, default: 0}, + count_current: {type: Number, default: 0}, + // 所需积分 + score: {type: Number}, + // 卡片故事 + desc: {type: String}, + // 图标 + icon_img: {type: String}, + // 状态, 0: 正常状态, 1: 已下架 + status: {type: Number}, + content_img: {type: String}, + images: [{type: String}], + deleted: {type: Boolean, default: false}, + delete_time: {type: Date}, + sort_index: {type: Number}, + createdBy: {type: String}, +}, { + collection: 'card_info', + timestamps: true, +}); + +const conn = dbUtil.getConnGhost(); +const CardInfoModel = conn.model('CardInfo', CardInfo); + +CardInfoModel.parse_req = (req, record) => { + if (!record) { + record = new CardInfoModel({ + createdBy: req.user.id, + count: 0, + score: 0, + }); + } + const body = req.body; + record.name = body.name; + record.desc = body.desc; + record.score = body.score; + record.count = body.count; + record.images = body.images; + record.content_img = body.content_img; + record.icon_img = body.icon_img; + record.status = body.status; + record.sort_index = body.sort_index; + return record; +}; + +CardInfoModel.edit_validate = () => { + return { + form: '#card_edit_form', + rules: [ + ['name', ['required'], '卡片名 不能为空'], + ['score', ['required'], '所需积分 不能为空'], + ], + }; +}; +export default CardInfoModel; diff --git a/src/models/ghost/EmulatedGames.js b/src/models/ghost/EmulatedGames.js new file mode 100644 index 0000000..8918628 --- /dev/null +++ b/src/models/ghost/EmulatedGames.js @@ -0,0 +1,257 @@ +import mongoose from 'mongoose'; +import dbUtil from '../../utils/db.util'; +import stringUtil from '../../utils/string.utils'; + +const Schema = mongoose.Schema; + +const EmulatedGames = new Schema({ + gid: {type: Number, required: true}, + // 游戏名 + name: {type: String}, + /** 游戏类型 + 射击 1 + 格斗 2 + 角色扮演 3 + 动作角色扮演 4 + 赛车 5 + 动作游戏 6 + 策略战棋 7 + 其他 8 + 益智游戏 9, + 体育游戏 10, + 冒险游戏 11, + 模拟战略 12, + 桌面游戏 13, + 音乐游戏 14, + 第一人称射击 15 + */ + type: {type: Number}, + // 游戏介绍 + introduce: {type: String}, + // 游戏语言 + language: {type: String}, + // 英文名 + orgname: {type: String}, + // 图片数量 + pic_count: {type: Number}, + // 图片列表 + images: [{type: String}], + // 标签 以半角逗号作分割 + taglist: {type: String}, + // 游戏评分 + score: {type: Number}, + // 是否是公开游戏, 公开游戏不需要购买即可玩 + open: {type: Boolean}, + // 游戏大类 fc, gba, h5, weapp, ad + category: {type: String}, + // 该字段只对gba游戏有效, 等于2的情况下, 使用第二个模拟器的页面, 其他值使用默认模拟器 + fixed: {type: Number}, + // 排序字段, 倒序 + sortIdx: {type: Number}, + // 游戏链接, 针对h5游戏和ad等类型, 需要该字段,以供客户端跳转 + link: {type: String}, + // 游戏的二维码, 针对weapp类型的游戏, 提供该字段已扫码跳转 + qrcode: {type: String}, + // 游戏版本 + version: {type: String}, + // 游戏真实的play数量 + playCount: {type: Number, default: 0}, + // 游戏显示的play数量 + showCount: {type: Number, default: 1000}, + createdBy: {type: String}, + deleted: {type: Boolean, default: false}, + delete_time: {type: Date}, + deletedBy: {type: String}, + published: {type: Boolean, default: false}, + publish_time: {type: Date}, + publishedBy: {type: String}, +}, { + collection: 'games', + timestamps: true, +}); + +const gameTypes = { + 1: '射击', + 2: '格斗', + 3: '角色扮演', + 4: '动作角色扮演', + 5: '赛车', + 6: '动作游戏', + 7: '策略战棋', + 8: '其他', + 9: '益智游戏', + 10: '体育游戏', + 11: '冒险游戏', + 12: '模拟战略', + 13: '桌面游戏', + 14: '音乐游戏', + 15: '第一人称射击', +}; + +EmulatedGames.pre('save', async function(next) { + if (!this.gid) { + this.gid = await EmulatedGames.nextGid(this.category); + } + next(); +}); + + +/** + * 获取显示用的游戏类型 + * */ +EmulatedGames.virtual('show_type').get(function() { + return gameTypes[this.type]; +}); +/** + * 返回所有的游戏类型 + * */ +EmulatedGames.virtual('all_type').get(function() { + return gameTypes; +}); + +/** + * 根据gid返回数据 + * @param {string} gid 游戏的短id + * @return {Object} record + * */ +EmulatedGames.query.byGid = function(gid) { + return this.where({gid: gid, deleted: false}); +}; + +EmulatedGames.statics.allType = function() { + return gameTypes; +}; +/** + * 获取下一个可能的gid + * @param {string} category 游戏的类别 + * @return {number} gid 新的游戏id + * */ +EmulatedGames.statics.nextGid = async function(category) { + const records = await this.find({category: category}).limit(1).sort({gid: -1}); + if (records && records.length > 0) { + return records[0].gid + 1; + } else { + if (category === 'gba') { + return 1; + } else if (category === 'fc') { + return 7000001; + } else if (category === 'h5') { + return 10000001; + } else { + return 20000001; + } + } +}; + +EmulatedGames.statics.allLanguage = function() { + return ['中文', '简体中文', '繁体中文', '英文', '日文', '多国语言', '德文', '法文', '欧洲', '其他'] +}; + +EmulatedGames.statics.saveWithReq = async function(req) { + const body = req.body; + const data = body.data; + const ignoreKeySet = new Set(['createdAt', 'updatedAt', '__v', 'deleted', 'delete_time', 'deletedBy']); + let record; + if (data._id) { + record = await this.findById(data._id); + } + if (!record) { + record = new EmulatedGameModel({ + createdBy: req.user.id, + }); + } + for (const key in body.data) { + if ({}.hasOwnProperty.call(body.data, key) && !ignoreKeySet.has(key)) { + record[key] = body.data[key]; + } + } + return record.save(); +}; +/** + * @param {Object} req 请求对象 + * keyStr 关键字 + * category 游戏类别, all: 所有,fc, gba + * type 游戏类型 0: 所有, 其他值: 对应类型的游戏 + * language 游戏语言 + * tag 标签 + * open 是否公开 all: 所有, 0, 1 + * @return {Object} opt 查询条件对象 + * @return {Object} sortObj 排序对象 + * */ +EmulatedGames.statics.generateQueryOpt = function(req) { + let opt = {}; + const body = req.body; + const order = body.order; + const sort = body.sort ? body.sort : 'sortIdx'; + const sortObj = {}; + sortObj[sort] = order === 'asc' ? 1 : -1; + if (body.keyStr) { + const orArr = [ + {name: {$regex: body.keyStr, $options: 'i'}}, + {orgname: {$regex: body.keyStr, $options: 'i'}}, + {taglist: {$regex: body.keyStr, $options: 'i'}}, + ]; + opt = {$or: orArr}; + } + if (body.category && body.category !== 'all') { + opt.category = body.category; + } + if (body.type) { + opt.type = body.type; + } + if (body.language) { + opt.language = {$regex: body.language, $options: 'i'}; + } + if (body.tag) { + opt.taglist = {$regex: body.tag, $options: 'i'}; + } + if (body.open && body.open !== 'all') { + opt.open = stringUtil.isTrue(body.open); + } + if (body.published !== undefined && body.published !== 'all') { + opt.published = stringUtil.isTrue(body.published); + } + opt.deleted = false; + return {opt, sortObj}; +}; + +/** + * 获取当前最大的sortIdx + * */ +EmulatedGames.statics.getMaxIndex = async function() { + const records = await this.find({deleted: false}).limit(1).sort({sortIdx: -1}); + if (records.length > 0) { + return records[0].sortIdx; + } else { + return 0; + } +}; +/** + * 获取当前数值的排名 + * @param {Number} idx 当前的sortIdx + * @param {Number} gid 游戏的gid + * */ +EmulatedGames.statics.currentSort = async function(idx, gid) { + return await this.countDocuments({deleted: false, sortIdx: {$gte: idx}, gid: {$lt: gid}}); +}; + +/** + * 更新游戏的发布状态 + * @param {Array} ids + * @param {Boolean} status + * @param {String} user + * */ +EmulatedGames.statics.updatePublishState = async function(ids, status, user) { + if (status) { + return await this.where({_id: {$in: ids}}).updateMany({ + published: true, publish_time: new Date, publishedBy: user}); + } else { + return await this.where({_id: {$in: ids}}).updateMany({published: false}); + } +}; + +const conn = dbUtil.getConnGhost(); + +const EmulatedGameModel = conn.model('EmulatedGames', EmulatedGames); + +export default EmulatedGameModel; diff --git a/src/models/ghost/ExchangeRecord.js b/src/models/ghost/ExchangeRecord.js new file mode 100644 index 0000000..5816833 --- /dev/null +++ b/src/models/ghost/ExchangeRecord.js @@ -0,0 +1,76 @@ +'use strict'; + +import mongoose from 'mongoose'; +import dbUtil from '../../utils/db.util'; + +const Schema = mongoose.Schema; +/** + * 公仔兑换&物流记录 + * */ +const ExchangeRecord = new Schema({ + user: {type: String, ref: 'Account'}, + // 接受公仔的用户 + receive_user: {type: String, ref: 'Account'}, + // 公仔信息 + item: {type: String, ref: 'Gift'}, + // 兑换类型, 0: 自己兑换, 1: 赠送给朋友,并且填写了配送地址, 2: 赠送给朋友, 未填写配送地址 + type: {type: Number, default: 0}, + // 兑换消耗积分 + score: {type: Number}, + // 寄语 + content: {type: String}, + // 收件人 + target_name: {type: String}, + // 署名 + sign: {type: String}, + // 收件人信息, 从account中继承 + address_info: { + // 地区代码 + code: {type: String}, + // 省 + province: {type: String}, + // 市 + city: {type: String}, + // 区域 + district: {type: String}, + // 地址 + address: {type: String}, + // 收件人 + name: {type: String}, + // 联系电话 + mobile: {type: String}, + // 邮编 + postcode: {type: String}, + }, + // 配送时间 + send_time: {type: Date}, + // 物流公司名称 + company: {type: String}, + // 快递单号 + invoice: {type: String}, + // 状态 0: 未领取, 1: 已领取, -1: 已退回, 2: 已配送 + status: {type: Number, default: 0}, +}, { + collection: 'exchange_record', + timestamps: true, +}); + +ExchangeRecord.virtual('statusStr').get(function() { + switch (this.status) { + case -1: + return '已退回'; + case 1: + return '未发货'; + case 0: + return '未领取'; + case 2: + return '已发货'; + case -2: + return '服务端退回'; + } +}); + +const conn = dbUtil.getConnGhost(); +const ExchangeRecordModel = conn.model('ExchangeRecord', ExchangeRecord); + +export default ExchangeRecordModel; diff --git a/src/models/ghost/Gift.js b/src/models/ghost/Gift.js new file mode 100644 index 0000000..78f8854 --- /dev/null +++ b/src/models/ghost/Gift.js @@ -0,0 +1,87 @@ +'use strict'; + +import mongoose from 'mongoose'; +import dbUtil from '../../utils/db.util'; + +const Schema = mongoose.Schema; + +/** + * 公仔信息 + * */ +const Gift = new Schema({ + // 公仔名 + name: {type: String}, + // 详细说明 + desc: {type: String}, + // 大小 + size: {type: String}, + // 总数 + amount: {type: Number}, + // 已兑换数量 + amount_used: {type: Number}, + // 当前正在编织数量 + amount_current: {type: Number, default: 0}, + // 所需积分 + score: {type: Number}, + // 实物图, 大图 + main_img: {type: String}, + // 大图 + content_img: {type: String}, + // 介绍图片列表 + images: [{type: String}], + // 小图 + icon_img: {type: String}, + areas: [{type: String}], + // 状态, 0: 正常状态, 1: 已下架, 2: 往期商品 + status: {type: Number}, + deleted: {type: Boolean, default: false}, + delete_time: {type: Date}, + // 序号 + sort_index: {type: Number}, + createdBy: {type: String}, +}, { + collection: 'gifts', + timestamps: true, +}); + +const conn = dbUtil.getConnGhost(); +const GiftModel = conn.model('Gift', Gift); + +GiftModel.parse_gift = (req, record) => { + if (!record) { + record = new GiftModel({ + createdBy: req.user.id, + amount: 0, + amount_used: 0, + score: 0, + }); + } + const body = req.body; + record.name = body.name; + record.desc = body.desc; + record.size = body.size; + record.amount = body.amount; + record.amount_used = body.amount_used; + record.score = body.score; + record.main_img = body.main_img; + record.content_img = body.content_img; + record.images = body.images; + record.areas = body.areas; + record.icon_img = body.icon_img; + record.status = body.status; + record.sort_index = body.sort_index; + return record; +}; + +GiftModel.gift_edit_validate = () => { + return { + form: '#gift_edit_form', + rules: [ + ['name', ['required'], '公仔名 不能为空'], + ['score', ['required'], '所需积分 不能为空'], + ['amount', ['required'], '库存总数 不能为空'], + ], + }; +}; + +export default GiftModel; diff --git a/src/models/ghost/Message.js b/src/models/ghost/Message.js new file mode 100644 index 0000000..d22df40 --- /dev/null +++ b/src/models/ghost/Message.js @@ -0,0 +1,34 @@ +'use strict'; + +import mongoose from 'mongoose'; +import dbUtil from '../../utils/db.util'; + +const Schema = mongoose.Schema; + +/** + * 消息记录 + * */ +const Message = new Schema({ + // 帐号信息 + user: {type: String, ref: 'Account'}, + msg: {type: String}, + // 消息状态, 0: 未下发, 1: 已下发 + status: {type: Number}, +}, { + collection: 'messages', + timestamps: true, +}); + +const conn = dbUtil.getConnGhost(); +const MessageModel = conn.model('Message', Message); + +MessageModel.addOneRecord = function(uid, msg) { + const record = new MessageModel({ + user: uid, + msg: msg, + status: 0, + }); + return record.save(); +}; + +export default MessageModel; diff --git a/src/models/ghost/RecommendGames.js b/src/models/ghost/RecommendGames.js new file mode 100644 index 0000000..8efdfcb --- /dev/null +++ b/src/models/ghost/RecommendGames.js @@ -0,0 +1,62 @@ + +import mongoose from 'mongoose'; +import dbUtil from '../../utils/db.util'; + +const Schema = mongoose.Schema; +/** + * 推荐游戏 + * */ +const RecommendGamesSchema = new Schema({ + // 游戏id + gid: {type: String, ref: 'Games'}, + // 0: 游戏, 1: 链接 + type: {type: Number, default: 0}, + link: {type: String}, + image: {type: String}, + name: {type: String}, + sortIdx: {type: Number, default: 0}, + createdBy: {type: String}, + deleted: {type: Boolean, default: false}, + delete_time: {type: Date}, + deletedBy: {type: String}, +}, { + collection: 'recommend_games', + timestamps: true, +}); + +/** + * + * */ +class RecommendGamesClass { + /** + * 分析request, 保存对记录的更改 + * @param {Object} req + * @return {Promise} record + * */ + static async saveWithReq(req) { + const body = req.body; + const data = body.data; + const ignoreKeySet = new Set(['createdAt', 'updatedAt', '__v', 'deleted', 'delete_time', 'deletedBy']); + let record; + if (data._id) { + record = await this.findById(data._id); + } + if (!record) { + record = new RecommendGamesModel({ + createdBy: req.user.id, + }); + } + for (const key in body.data) { + if ({}.hasOwnProperty.call(body.data, key) && !ignoreKeySet.has(key)) { + record[key] = body.data[key]; + } + } + return record.save(); + } +} +RecommendGamesSchema.loadClass(RecommendGamesClass); + +const conn = dbUtil.getConnGhost(); +const RecommendGamesModel = conn.model('RecommendGames', RecommendGamesSchema); + +export default RecommendGamesModel; diff --git a/src/models/ghost/ScoreRecord.js b/src/models/ghost/ScoreRecord.js new file mode 100644 index 0000000..7087c2c --- /dev/null +++ b/src/models/ghost/ScoreRecord.js @@ -0,0 +1,50 @@ +'use strict'; + +import mongoose from 'mongoose'; +import moment from 'moment'; +import dbUtil from '../../utils/db.util'; + +const Schema = mongoose.Schema; +/** + * 积分变动记录 + * */ +const ScoreRecord = new Schema({ + // 关联的帐号 + user: {type: String, ref: 'Account'}, + // 关联的幽灵 + ghost: {type: String, ref: 'Ghost'}, + // 关联的兑换记录 + exchange_record: {type: String, ref: 'ExchangeRecord'}, + // 卡片兑换记录 + card_record: {type: String, ref: 'CardRecord'}, + appid: {type: String}, + // 日期, YYYY-MM-DD + day: {type: String}, + // 类型: + // ghost: 幽灵产生, gift: 看广告加速, ghost_init: 新幽灵初始增加, click_app: 点击积分墙, + // exchange: 兑换, card: 兑换卡片, money: 兑换现金, retract: 公仔退回, retract_svr: 系统退回 + type: {type: String}, + // 操作的管理员帐号 + account_admin: {type: String}, + // 变动数量 + amount: {type: Number}, +}, { + collection: 'score_record', + timestamps: true, +}); +const conn = dbUtil.getConnGhost(); +const ScoreRecordModel = conn.model('ScoreRecord', ScoreRecord); + +// 添加一条公仔退回记录 +ScoreRecordModel.addRetractScore = function(uid, amount, eid, adminId) { + const record = new ScoreRecordModel({ + user: uid, + amount: amount, + exchange_record: eid, + account_admin: adminId, + type: 'retract_svr', + day: moment().format('YYYY-MM-DD'), + }); + return record.save(); +}; +export default ScoreRecordModel; diff --git a/src/models/snoopy/ChinaArea.js b/src/models/snoopy/ChinaArea.js new file mode 100644 index 0000000..3126063 --- /dev/null +++ b/src/models/snoopy/ChinaArea.js @@ -0,0 +1,23 @@ +'use strict'; +import mongoose from 'mongoose'; +import dbUtil from '../../utils/db.util'; + +/** + * 中国区域定义 + */ +const ChinaArea = new mongoose.Schema({ + // 区域名 + name: {type: String}, + // 包含地区 + locations: [{type: String}], + deleted: {type: Boolean, default: false}, + delete_time: {type: Date}, +}, { + collection: 'china_area', + timestamps: true, +}); + +const conn = dbUtil.getConnSnoopy(); + +export default conn.model('ChinaArea', ChinaArea); + diff --git a/src/models/snoopy/ChinaRegion.js b/src/models/snoopy/ChinaRegion.js new file mode 100644 index 0000000..cee6735 --- /dev/null +++ b/src/models/snoopy/ChinaRegion.js @@ -0,0 +1,45 @@ +'use strict'; +import mongoose from 'mongoose'; +import dbUtil from '../../utils/db.util'; + +/** + * 中国行政区划 + */ +const ChinaRegion = new mongoose.Schema({ + // 区划id, 6位数字 + _id: {type: Number}, + // 等级, 0: 国, 1: 省, 2: 市, 3: 区县 + level: {type: Number}, + // 父级id + parent_id: {type: Number}, + // 中文名 + name: {type: String}, + // 中文名, 短 + short_name: {type: String}, + // 拼音 + pinyin: {type: String, index: true}, + // 国家 + country: {type: String}, + // 省份, 冗余数据, 方便查找, 全中国总共才2000多条数据, 没什么问题 + province: {type: String}, + // 市 + city: {type: String}, + // 区县 + district: {type: String}, + // 邮编 + zipcode: {type: String}, + // 区号 + citycode: {type: String}, + // 经度 + lan: {type: Number}, + // 维度 + lat: {type: Number}, +}, { + collection: 'china_region', + timestamps: true, +}); + +const conn = dbUtil.getConnSnoopy(); + +export default conn.model('ChinaRegion', ChinaRegion); + diff --git a/src/models/snoopy/CustomerReplay.js b/src/models/snoopy/CustomerReplay.js new file mode 100644 index 0000000..7f7306e --- /dev/null +++ b/src/models/snoopy/CustomerReplay.js @@ -0,0 +1,49 @@ +'use strict'; +import mongoose from 'mongoose'; +import dbUtil from '../../utils/db.util'; + +/** + * 客服关键字回复 + */ +const CustomerReplay = new mongoose.Schema({ + // 游戏id + game_id: {type: String}, + // 响应的关键字 + keys: [{type: String}], + items: [{ + _id: false, + item_id: {type: String}, + count: {type: Number}, + }], + actived: {type: Boolean, default: true}, + // 备注 + comment: {type: String}, + createdBy: {type: String}, + deleted: {type: Boolean, default: false}, + deletedBy: {type: String}, + delete_time: {type: Date}, +}, { + collection: 'customer_replay', + timestamps: true, +}); + +const conn = dbUtil.getConnSnoopy(); +const CustomerReplayModel = conn.model('CustomerReplay', CustomerReplay); + +CustomerReplayModel.parseReq = function(req, record) { + if (!record) { + record = new CustomerReplayModel({ + createdBy: req.user.id, + }); + } + const body = req.body; + record.game_id = body.game_id; + record.keys = body.keys; + record.items = body.items; + record.actived = body.actived; + record.comment = body.comment; + return record; +}; + +export default CustomerReplayModel; + diff --git a/src/models/snoopy/GameInfo.js b/src/models/snoopy/GameInfo.js new file mode 100644 index 0000000..2346a5a --- /dev/null +++ b/src/models/snoopy/GameInfo.js @@ -0,0 +1,116 @@ +'use strict'; +import mongoose from 'mongoose'; +import dbUtil from '../../utils/db.util'; + +/** + * 游戏信息 + */ +const GameInfo = new mongoose.Schema({ + // 游戏id + game_id: {type: String}, + // 游戏名 + game_name: {type: String}, + // 英文名 + game_name_en: {type: String}, + // 游戏icon + game_icon: {type: String}, + // 游戏类型 + game_type: {type: Number}, + // app id + app_id: {type: String}, + // app secret + app_secret: {type: String}, + // 状态 + status: {type: Number, default: 0}, + // 平台 + platform: {type: String}, + // 关联的游戏列表 + linked_games: [{type: String, ref: 'GameInfo'}], + // 排序 + sort: {type: Number, index: true}, + // 是否主推 + is_recommend: {type: Boolean, default: false}, + // 是否火爆 + is_hot: {type: Boolean, default: false}, + // 是否是新游 + is_new: {type: Boolean, default: false}, + // 备注 + comment: {type: String}, + // 点击率 + click_count: {type: Number, default: 0}, + deleted: {type: Boolean, default: false}, + deletedBy: {type: String}, + delete_time: {type: Date}, +}, { + collection: 'game_info', + timestamps: true, +}); +const showTypes = { + 0: '益智解密', + 1: '手脑反应', + 2: '策略养成', +}; + +const platforms = { + 6000: '内部测试', + 6001: '微信', + 6002: 'QQ玩一玩', + 6003: 'OPPO小游戏', + 6004: 'VIVO快游戏', + 6501: 'facebook小游戏', + 6005: '百度', + 6006: '抖音', +}; + +const statusObj = { + 0: '已上线', + 1: '暂停', + 2: '新版本开发中', + 3: '外包开发中', + 4: '开发中', + 5: '未开始', + 6: '测试中', + 7: '二次开发中', +}; + +GameInfo.virtual('show_type').get(function() { + return showTypes[this.game_type]; +}); + +GameInfo.virtual('all_type').get(function() { + return showTypes; +}); + + +GameInfo.virtual('platform_show').get(function() { + return platforms[this.platform]; +}); + +GameInfo.virtual('status_show').get(function() { + return statusObj[this.status]; +}); + +GameInfo.statics = { + all_type() { + return showTypes; + }, + all_platform() { + return platforms; + }, + status_list() { + const list = []; + for (const key in statusObj) { + if ({}.hasOwnProperty.call(statusObj, key)) { + list.push({ + key: key, + name: statusObj[key], + }); + } + } + return list; + }, +}; +const conn = dbUtil.getConnSnoopy(); + +export default conn.model('GameInfo', GameInfo); + diff --git a/src/models/snoopy/GameShareImage.js b/src/models/snoopy/GameShareImage.js new file mode 100644 index 0000000..bf023a6 --- /dev/null +++ b/src/models/snoopy/GameShareImage.js @@ -0,0 +1,97 @@ +'use strict'; +import mongoose from 'mongoose'; +import dbUtil from '../../utils/db.util'; +import stringUtil from '../../utils/string.utils'; +/** + * 游戏分享图 + */ +const GameShareImage = new mongoose.Schema({ + // 游戏id + game_id: {type: String}, + // 是否是默认分享 + default_share: {type: Boolean, default: false}, + // 分享类型 + share_type: {type: String}, + // 分享图路径 + share_image: {type: String}, + // 分享语 + share_word: {type: String}, + // 多个分享图 + share_images: [{type: String}], + // 多个分享语 + share_words: [{type: String}], + // 区域 + area: {type: String}, + // 地域列表 + locations: [{type: String}], + // 性别 + sex: {type: String}, + // 广告id + ad_id: {type: String}, + // 分享次数 + share_count: {type: Number, default: 0}, + // 广告次数 + ad_count: {type: Number, default: 0}, + // 广告间隔时间 + ad_cd: {type: Number, default: 0}, + // 0: 分享图, 1: 广告 + type: {type: Number, default: 0}, + // 备注 + comment: {type: String}, + createdBy: {type: String}, + deleted: {type: Boolean, default: false}, + deletedBy: {type: String}, + delete_time: {type: Date}, +}, { + collection: 'game_share_images', + timestamps: true, +}); + +const conn = dbUtil.getConnSnoopy(); +const GameShareImageModel = conn.model('GameShareImage', GameShareImage); + +GameShareImageModel.parse_req = (req, record) => { + if (!record) { + record = new GameShareImageModel({ + createdBy: req.user.id, + }); + } + const body = req.body; + record.game_id = body.game_id; + record.default_share = stringUtil.isTrue(body.default_share); + record.share_type = body.share_type; + record.share_images = body.share_images; + record.share_words = body.share_words; + if (record.share_images && record.share_images.length > 0) { + record.share_image = record.share_images[0]; + } + if (record.share_words && record.share_words.length > 0) { + record.share_word = record.share_words[0]; + } + record.locations = body.locations; + record.sex = body.sex; + record.area = body.area; + record.comment = body.comment; + record.type = body.type; + record.ad_id = body.ad_id; + record.ad_count = body.ad_count; + record.ad_cd = body.ad_cd; + record.share_count = body.share_count; + return record; +}; + +GameShareImageModel.edit_validate = () => { + return { + form: '#edit_form', + rules: [ + ['share_type', ['required'], '请选择分享类型 '], + ['share_count', ['required'], '分享次数 不能为空'], + ['ad_count', ['required'], '广告播放次数 不能为空'], + ['ad_cd', ['required'], '广告播放间隔 不能为空'], + ['type', ['required'], '请选择优先级'], + ], + }; +}; +export default GameShareImageModel; + + diff --git a/src/models/snoopy/Gift.js b/src/models/snoopy/Gift.js new file mode 100644 index 0000000..24651ee --- /dev/null +++ b/src/models/snoopy/Gift.js @@ -0,0 +1,96 @@ +import mongoose from 'mongoose'; +import dbUtil from '../../utils/db.util'; +import moment from 'moment'; + +const GiftSchema = new mongoose.Schema({ + items: [{ + _id: false, + itemid: {type: String}, + itemnum: {type: Number, default: 0}, + itemname: {type: String}, + }], + // 礼包名 + name: {type: String}, + game_id: {type: String}, + // 状态 + status: {type: Number, default: 0}, + // 备注 + comment: {type: String}, + createdBy: {type: String}, + deleted: {type: Boolean, default: false}, + deletedBy: {type: String}, + delete_time: {type: Date}, +}, { + collection: 'gift', + timestamps: true, +}); + +/** + * + * */ +class GiftClass { + /** + * 分析request, 保存对记录的更改 + * @param {Object} req + * @param {Object} record + * @return {Promise} record + * */ + static parseReq(req, record) { + const body = req.body; + if (!record) { + record = new GiftModel({ + createdBy: req.user.id, + }); + } + record.game_id = body.game_id; + record.name = body.name; + record.items = body.items; + record.status = body.status; + record.comment = body.comment; + return record.save(); + } + /** + * 分析request, 并生成查询的条件和排序Object + * @param {Object} req + * @return {Object} opt + * @return {Object} sortObj + * */ + static parseGiftQueryOpt(req) { + let opt = {}; + const body = req.body; + const keyStr = body.name; + let timeBegin = body.timeBegin; + let timeEnd = body.timeEnd; + const order = body.order; + const sort = body.sort ? body.sort : 'createdAt'; + const sortObj = {sort: order === 'asc' ? 1 : -1}; + sortObj[sort] = order === 'asc' ? 1 : -1; + if (keyStr) { + opt = {$or: [ + {name: {$regex: keyStr, $options: 'i'}}, + {comment: {$regex: keyStr, $options: 'i'}}, + {'items.itemname': {$regex: keyStr, $options: 'i'}}, + ]}; + } + (body.gameId) && (opt.game_id = body.gameId); + if (timeBegin && !timeEnd) { + timeBegin = moment(timeBegin, 'YYYY-MM-DD').startOf('day').format('YYYY-MM-DD HH:mm:ss'); + opt.createdAt = {$gte: timeBegin}; + } else if (timeBegin && timeEnd) { + timeBegin = moment(timeBegin, 'YYYY-MM-DD').startOf('day').format('YYYY-MM-DD HH:mm:ss'); + timeEnd = moment(timeEnd, 'YYYY-MM-DD').endOf('day').format('YYYY-MM-DD HH:mm:ss'); + opt['$and'] = [{createdAt: {$gte: timeBegin}}, {createdAt: {$lte: timeEnd}}]; + } else if (!timeBegin && timeEnd) { + timeEnd = moment(timeEnd, 'YYYY-MM-DD').endOf('day').format('YYYY-MM-DD HH:mm:ss'); + opt.createdAt = {$lte: timeEnd}; + } + opt.deleted = false; + return {opt, sortObj}; + } +} + +GiftSchema.loadClass(GiftClass); + +const GiftModel = dbUtil.getConnSnoopy().model('Gift', GiftSchema); + +export default GiftModel; diff --git a/src/models/snoopy/GiftHistory.js b/src/models/snoopy/GiftHistory.js new file mode 100644 index 0000000..8347452 --- /dev/null +++ b/src/models/snoopy/GiftHistory.js @@ -0,0 +1,27 @@ +import mongoose from 'mongoose'; +import dbUtil from '../../utils/db.util'; + +/** + * 兑换码兑换记录 + */ +const GiftHistory = new mongoose.Schema({ + // 兑换码 + gift_no: {type: String}, + gift: {type: String, ref: 'Gift'}, + gift_pack: {type: String, ref: 'GiftPack'}, + account_id: {type: String}, + // 备注 + comment: {type: String}, + deleted: {type: Boolean, default: false}, + deletedBy: {type: String}, + delete_time: {type: Date}, +}, { + collection: 'gift_history', + timestamps: true, +}); + +const conn = dbUtil.getConnSnoopy(); +const GiftHistoryModel = conn.model('GiftHistory', GiftHistory); + +export default GiftHistoryModel; + diff --git a/src/models/snoopy/GiftPack.js b/src/models/snoopy/GiftPack.js new file mode 100644 index 0000000..6dff757 --- /dev/null +++ b/src/models/snoopy/GiftPack.js @@ -0,0 +1,191 @@ +import mongoose from 'mongoose'; +import dbUtil from '../../utils/db.util'; +import stringUtil from '../../utils/string.utils'; +import moment from 'moment'; +import Games from './GameInfo'; +import logger from '../../utils/logger'; +import Gift from './Gift'; + +/** + * 礼包 + */ +const GiftPack = new mongoose.Schema({ + // 兑换码 + gift_nos: [{type: String}], + gift_no_count: {type: Number}, + // 游戏id + game_id: {type: String}, + // 服务器 + svr_id: {type: String}, + svr_name: {type: String}, + // 活动名 + name: {type: String}, + gift: {type: String, ref: 'Gift'}, + // 批次号 + batch_no: {type: Number, index: true}, + // 开始时间 + begin_time: {type: Date}, + // 结束时间 + end_time: {type: Date}, + // 渠道 + platforms: [{type: String}], + // 类型, one: 一次性(只能一人兑换一次), batch: 批量(可供多人兑换,每人一次) + type: {type: String}, + // 状态 + status: {type: Number, default: 0}, + // 兑换需要的vip等级 + vip: {type: Number, default: 0}, + // 备注 + comment: {type: String}, + createdBy: {type: String}, + deleted: {type: Boolean, default: false}, + deletedBy: {type: String}, + delete_time: {type: Date}, +}, { + collection: 'gift_pack', + timestamps: true, +}); + +GiftPack.pre('save', async function(next) { + if (!this.batch_no) { + let batchNo = await GiftPackModel.nextBatchNo(); + this.batch_no = batchNo; + if (this.gift_no_count > 0) { + this.gift_nos = GiftPackModel.generateGiftNos(this.gift_no_count, batchNo); + } + } + next(); +}); + +const conn = dbUtil.getConnSnoopy(); +const GiftPackModel = conn.model('GiftPack', GiftPack); + +// 生成指定数量的兑换码 +GiftPackModel.generateGiftNos = function(count, batchNo) { + const set = new Set(); + for (let i = 0; i < count; i++) { + const str = GiftPackModel.randomGiftNos(set); + set.add(str); + } + return [...set].map((o)=> batchNo + o); +}; + +// 生成一条随机的兑换码 +GiftPackModel.randomGiftNos = function(set) { + const num = stringUtil.randomNumAdv(0, 2176782335); + const str = stringUtil.string10to36(num).padStart(6, '0'); + if (set.has(str)) { + return GiftPackModel.randomGiftNos(set); + } else { + return str; + } +}; +GiftPackModel.nextBatchNo = async () =>{ + const record = await GiftPackModel.find({deleted: false}).limit(1).sort({batch_no: -1}); + if (record.length > 0) { + return parseInt(record[0].batch_no) + 1; + } else { + return 1001; + } +}; +GiftPackModel.parseReq = async (req, record) => { + const body = req.body; + if (!record) { + record = new GiftPackModel({ + createdBy: req.user.id, + }); + record.gift_no_count = body.gift_count; + } + record.name = body.name; + record.game_id = body.game_id; + record.gift = body.gift; + if (body.begin_time) { + record.begin_time = moment(body.begin_time, 'YYYY-MM-DD HH:mm').toDate(); + } else { + record.begin_time = null; + } + if (body.end_time) { + record.end_time = moment(body.end_time, 'YYYY-MM-DD HH:mm').toDate(); + } else { + record.end_time = null; + } + record.vip = body.vip; + record.svr_id = body.svr_id; + record.svr_name = body.svr_name; + record.platforms = body.platforms; + record.type = body.type; + record.status = body.status; + return record.save(); +}; + +GiftPackModel.parseQueryOpt = async (req) => { + let opt = {}; + const query = req.query; + const keyStr = query.keyStr; + const status = parseInt(query.status); + const type = query.type; + const platform = query.platform; + let timeBegin = query.timeBegin; + let timeEnd = query.timeEnd; + const order = query.order; + const gameId = query.gameId; + const sort = query.sort ? query.sort : 'createdAt'; + const sortObj = {}; + const vip = query.vip; + sortObj[sort] = order === 'asc' ? 1 : -1; + if (keyStr) { + const orArr = [{name: {$regex: keyStr, $options: 'i'}}]; + try { + const gamesArr = [ + {game_name: {$regex: keyStr, $options: 'i'}}, + {game_name_en: {$regex: keyStr, $options: 'i'}}, + ]; + const games = await Games.find({$or: gamesArr}); + const gameIds = games.map((o) => o.id); + if (gameIds.length > 0) orArr.push({game_id: {$in: gameIds}}); + } catch (err) { + logger.error(err); + } + try { + const items = await Gift.find({name: {$regex: keyStr, $options: 'i'}}); + const itemIds = items.map((o) => o.id); + if (itemIds.length > 0) orArr.push({gift: {$in: itemIds}}); + } catch (err) { + logger.error(err); + } + + opt = {$or: orArr}; + } + if (status > -999) { + opt.status = status; + } + if (type !== 'all') { + opt.type = type; + } + if (platform !== 'all') { + opt.platforms = {$in: [platform]}; + } + if (vip > -1) { + // opt.vip = {$lte: vip}; + opt.vip = vip; + } + if (gameId) { + opt.game_id = gameId; + } + if (timeBegin && !timeEnd) { + timeBegin = moment(timeBegin, 'YYYY-MM-DD').startOf('day').format('YYYY-MM-DD HH:mm:ss'); + opt.end_time = {$gte: timeBegin}; + } else if (timeBegin && timeEnd) { + timeBegin = moment(timeBegin, 'YYYY-MM-DD').startOf('day').format('YYYY-MM-DD HH:mm:ss'); + timeEnd = moment(timeBegin, 'YYYY-MM-DD').endOf('day').format('YYYY-MM-DD HH:mm:ss'); + opt['$or'] = [{end_time: {$gte: timeBegin}}, {begin_time: {$lte: timeEnd}}]; + } else if (!timeBegin && timeEnd) { + timeEnd = moment(timeBegin, 'YYYY-MM-DD').endOf('day').format('YYYY-MM-DD HH:mm:ss'); + opt.begin_time = {$lte: timeEnd}; + } + opt.deleted = false; + return {opt, sortObj}; +}; + +export default GiftPackModel; + diff --git a/src/models/snoopy/Puzzle.js b/src/models/snoopy/Puzzle.js new file mode 100644 index 0000000..6aa4783 --- /dev/null +++ b/src/models/snoopy/Puzzle.js @@ -0,0 +1,58 @@ +'use strict'; +import mongoose from 'mongoose'; +import dbUtil from '../../utils/db.util'; + +/** + * 竞猜题库 + */ + +const Puzzle = new mongoose.Schema({ + // 类型, 1: 图片, 2: 音频, 3: 视频, 0: 纯文本 + attachtype: {type: Number}, + // 图片或者音频的url地址 + attach: {type: String}, + // 问题 + question: {type: String}, + // 答案扩展字符 + option: {type: String}, + // 答案 + answer: {type: String}, + // 扩展信息, 在猜菜名中, 该字段为菜系 + extra: {type: String}, + // 提示信息 + tip: {type: String}, + // 题目分类, food: 菜 + type: {type: String}, + // 备注 + comment: {type: String}, + createdBy: {type: String}, + deleted: {type: Boolean, default: false}, + deletedBy: {type: String}, + delete_time: {type: Date}, +}, { + collection: 'puzzle', + timestamps: true, +}); + +const conn = dbUtil.getConnSnoopy(); +const PuzzleModel = conn.model('Puzzle', Puzzle); + +PuzzleModel.parseReq = (req, record) => { + if (!record) { + record = new PuzzleModel({ + createdBy: req.user.id, + }); + } + const body = req.body; + record.attachtype = body.attachtype; + record.attach = body.attach; + record.option = body.option; + record.question = body.question; + record.answer = body.answer; + record.extra = body.extra; + record.tip = body.tip; + record.type = body.type; + return record.save(); +}; +export default PuzzleModel; + diff --git a/src/models/snoopy/PuzzleLevel.js b/src/models/snoopy/PuzzleLevel.js new file mode 100644 index 0000000..1b6305d --- /dev/null +++ b/src/models/snoopy/PuzzleLevel.js @@ -0,0 +1,82 @@ +'use strict'; +import mongoose from 'mongoose'; +import dbUtil from '../../utils/db.util'; +import Puzzle from './Puzzle'; + +/** + * 竞猜游戏关卡 + */ + +const PuzzleLevel = new mongoose.Schema({ + game_id: {type: String, ref: 'GameInfo'}, + levels: [ + { + _id: false, + name: {type: String}, + index: {type: Number}, + sub_levels: [ + { + _id: false, + name: {type: String}, + index: {type: Number}, + subjects: [{type: String, ref: 'Puzzle'}], + }, + ], + }, + ], + cdn_base: {type: String}, + createdBy: {type: String}, + deleted: {type: Boolean, default: false}, + deletedBy: {type: String}, + delete_time: {type: Date}, +}, { + collection: 'puzzle_level', + usePushEach: true, + timestamps: true, +}); + +const conn = dbUtil.getConnSnoopy(); +const PuzzleLevelModel = conn.model('PuzzleLevel', PuzzleLevel); + +PuzzleLevelModel.parseReq = (req, record) => { + if (!record) { + record = new PuzzleLevelModel({ + createdBy: req.user.id, + }); + } + return record.save(); +}; + +PuzzleLevelModel.findByGame = (gameId) => { + return PuzzleLevelModel.findOne({game_id: gameId, deleted: false}); +}; + +PuzzleLevelModel.generateLevelObj = (level) => { + const puzzles = []; + for (const subLevel of level.sub_levels) { + puzzles.push(...subLevel.subjects); + } + return Puzzle.find({_id: {$in: puzzles}}).select({attachtype: 1, attach: 1, + question: 1, option: 1, answer: 1, extra: 1, tip: 1}) + .then((topics) => { + const topicMap = new Map(); + for (const topic of topics) { + topicMap.set(topic.id, topic); + } + const resultArr = []; + for (const subLevel of level.sub_levels) { + const subjects = []; + for (const subject of subLevel.subjects) { + if (topicMap.has(subject)) { + const obj = topicMap.get(subject).toJSON(); + delete obj['_id']; + subjects.push(obj); + } + } + resultArr.push(subjects); + } + return resultArr; + }); +}; +export default PuzzleLevelModel; + diff --git a/src/router/index.js b/src/router/index.js new file mode 100644 index 0000000..6ba59cb --- /dev/null +++ b/src/router/index.js @@ -0,0 +1,11 @@ + +import {Router} from 'express'; + +import commonRouter from './../controllers/common' + +const router = new Router(); + +router.use('/common', commonRouter); + + +export default router \ No newline at end of file diff --git a/src/schedule/weappmsg.schedule.js b/src/schedule/weappmsg.schedule.js new file mode 100644 index 0000000..58da7b9 --- /dev/null +++ b/src/schedule/weappmsg.schedule.js @@ -0,0 +1,81 @@ +'use strict'; +import schedule from 'node-schedule'; +import logger from '../utils/logger'; +import wechatUtil from '../utils/wechat.utils'; +import config from '../../config/config'; +import WeappFormID from '../models/beagle/WeappFormID'; +import TemplateMsgRecord from '../models/admin/TemplateMsgRecord'; + + +const msgData = { + 'touser': '', + 'weapp_template_msg': { + 'template_id': config.pay_weapp.message_template_id, + 'page': '/pages/index', + 'form_id': '', + 'data': { + 'keyword1': { + 'value': config.pay_weapp.message_value1, + }, + 'keyword2': { + 'value': config.pay_weapp.message_value2, + }, + 'keyword3': { + 'value': config.pay_weapp.message_value3, + }, + }, + }, +}; + +const sendOneTemplateMsg = async function(openId, formId, accessToken) { + msgData.touser = openId; + msgData.weapp_template_msg.form_id = formId; + try { + await wechatUtil.sendMsgToUser(accessToken, msgData); + } catch (err) { + logger.error(err); + } +}; +export default { + async sendAll() { + logger.info('开始发送小程序模版消息'); + try { + await WeappFormID.updateExpireTime(); + } catch (err) { + logger.error(err); + } + const accessToken = await wechatUtil.getPayAccessToken(); + const records = await WeappFormID.getUserRecordsDay(config.pay_weapp.app_id); + const openIds = records.map((o) => o.open_id); + if (records && records.length > 0) { + for (const record of records) { + try { + await sendOneTemplateMsg(record.open_id, record.form_id, accessToken); + record.status = 1; + record.send_time = new Date(); + await record.save(); + } catch (err) { + logger.error(err); + } + } + } + try { + const sendRecord = new TemplateMsgRecord({ + app_id: config.pay_weapp.app_id, + open_ids: openIds, + }); + await sendRecord.save(); + } catch (err) { + logger.error(err); + } + }, + /* + * 设定每天凌晨8:30:30秒把刷新任务加入队列 + * */ + scheduleSendAll() { + logger.info('已添加发送小程序模版消息的定时任务'); + schedule.scheduleJob(config.pay_weapp.schedule_send_time, async () => { + this.sendAll(); + }); + }, +}; diff --git a/src/utils/admin_keeper.js b/src/utils/admin_keeper.js new file mode 100644 index 0000000..6278581 --- /dev/null +++ b/src/utils/admin_keeper.js @@ -0,0 +1,109 @@ +import logger from './logger'; +import Promise from 'bluebird'; +import {AdminAction, AdminRole} from '../models/admin/Admin'; + +const pathPermissions = new Map(); +const basicActions = new Set(); +const pathRoles = new Map(); +const roles = new Map(); + +Promise.all([ + AdminAction.find({deleted: false}).select('_id paths'), + AdminRole.find({deleted: false}) + .select('_id permissions') + .populate('permissions', '_id paths'), +]).then(([actions, rs]) => { + for (const action of actions) { + for (const path of action.paths) { + const uniPath = path.method + ':' + path.path; + if (action._id === '_basic_actions') { + basicActions.add(uniPath); + } else { + if (!pathPermissions.has(uniPath)) { + pathPermissions.set(uniPath, new Set()); + } + pathPermissions.get(uniPath).add(action._id); + } + } + } + for (const role of rs) { + if (!roles.has(role.id)) { + roles.set(role.id, new Set()); + } + for (const action of role.permissions) { + roles.get(role.id).add(action.id); + for (const path of action.paths) { + const uniPath = path.method + ':' + path.path; + if (!pathRoles.has(uniPath)) { + pathRoles.set(uniPath, new Set()); + } + pathRoles.get(uniPath).add(role._id); + } + } + } + logger.info('load permissions success, NEED REFRESH'); +}).catch((err) => { + throw err; +}); + +const hasPermission = function(user, path) { + let yes = false; + if (pathPermissions.has(path)) { + const allowedPermissions = pathPermissions.get(path); + for (const permission of user.permissions) { + if (!yes && allowedPermissions.has(permission)) { + yes = true; + break; + } + } + } + + if (!yes && pathRoles.has(path)) { + const allowedRoles = pathRoles.get(path); + for (const role of user.roles) { + if (!yes && allowedRoles.has(role)) { + yes = true; + break; + } + } + } + + return yes; +}; + +module.exports = { + + gatekeeper: function(req, res, next) { + const path = req.method + ':' + req.baseUrl + req.path; + if (req.isAuthenticated()) { + const params = req.method === 'GET' ? req.query : req.body; + const ip = req.headers['x-forwarded-for']; + logger.info({user: req.user.username, path: path, from: ip, params: params}); + let mayPass = false; + if (basicActions.has(path)) { + mayPass = true; + } else { + mayPass = hasPermission(req.user, path); + } + + if (mayPass) { + res.locals.lastLogin = req.session.lastLogin; + next(); + } else { + const accept = req.headers.accept || ''; + if (~accept.indexOf('html')) { + const err = new Error('您没有权限访问该功能'); + err.status = 403; + throw err; + } else { + res.status(403).send('您没有权限访问该功能'); + } + } + } else { + if (path !== '/logout') { + req.session.path_before_login = req.originalUrl; + } + res.redirect('/login.html'); + } + }, +}; diff --git a/src/utils/captcha.js b/src/utils/captcha.js new file mode 100644 index 0000000..8102b52 --- /dev/null +++ b/src/utils/captcha.js @@ -0,0 +1,75 @@ +'use strict'; +import svgCaptcha from 'svg-captcha'; + +const captchaUtil = { + generate_captcha: function() { + // const captcha = svgCaptcha.create({ + // size: 4, + // ignoreChars: '0o1i', + // }); + const captcha = svgCaptcha.createMathExpr(); + return captcha; + }, + + validate_captcha: function(req, captcha, storedCaptcha, stamp) { + if (!storedCaptcha || !captcha) { + return false; + } + if (!stamp) { + return false; + } + if (Date.now() - stamp > 600000) { + return false; + } + return storedCaptcha === captcha.toUpperCase(); + }, + + validate_register_captcha: function(req, captcha) { + return captchaUtil.validate_captcha(req, captcha, req.session.captcha_register_mobile, + req.session.captcha_register_mobile_stamp); + }, + + validate_login_captcha: function(req, captcha) { + return captchaUtil.validate_captcha(req, captcha, req.session.captcha_login_mobile, + req.session.captcha_login_mobile_stamp); + }, + + validate_findpwd_captcha: function(req, captcha) { + return captchaUtil.validate_captcha(req, captcha, req.session.captcha_findpwd_mobile, + req.session.captcha_findpwd_mobile_stamp); + }, + + validate_admin_login_captcha: function(req, captcha) { + return captchaUtil.validate_captcha(req, captcha, req.session.captcha_admin_login, + req.session.captcha_admin_login_stamp); + }, + + validate_mobile_code: function(req, stamp, storedMobile, storedCode) { + const mobile = req.body.mobile; + if (!mobile) { + return -1; + } + const mobileCode = req.body.mobile_code; + if (!mobileCode) { + return -1; + } + if (!stamp) { + return -2; + } + if (Date.now() - stamp > 600000) { + return -2; + } + if (!storedMobile) { + return -1; + } + if (!storedCode) { + return -1; + } + if (mobile !== storedMobile || mobileCode !== storedCode) { + return -1; + } + return 0; + }, +}; + +module.exports = captchaUtil; diff --git a/src/utils/cdn.utils.js b/src/utils/cdn.utils.js new file mode 100644 index 0000000..56f218f --- /dev/null +++ b/src/utils/cdn.utils.js @@ -0,0 +1,99 @@ +import config from '../../config/config'; +import request from 'request'; +import Promise from 'bluebird'; +import stringUtil from './string.utils'; +import COS from 'cos-nodejs-sdk-v5'; + +const generateNonce = function() { + return stringUtil.randomNum(10000, 99999); +}; +const generateSign = function(method, data) { + let str = `${method}cdn.api.qcloud.com/v2/index.php?`; + let i = 0; + for (const key in data) { + if ({}.hasOwnProperty.call(data, key)) { + if (i ++ > 0) str += '&'; + str += `${key}=${data[key]}`; + } + } + return stringUtil.sha1keyBase64(str, config.cos_cdn.SecretKey); +}; +const cosCDN = new COS({ + SecretId: config.cos_cdn.SecretId, + SecretKey: config.cos_cdn.SecretKey, +}); +export default { + + refreshDir(url) { + const now = Math.round(new Date()/1000); + const data = { + 'Action': 'RefreshCdnDir', + 'Nonce': generateNonce(), + 'SecretId': config.cos_cdn.SecretId, + 'Timestamp': now, + 'dirs.0': url, + }; + data.Signature = generateSign('POST', data); + return new Promise((resolve, reject) => { + const link = 'https://cdn.api.qcloud.com/v2/index.php'; + const options = {method: 'POST', + url: link, + headers: { + 'Cache-Control': 'no-cache', + 'Content-Type': 'application/x-www-form-urlencoded', + }, + form: data, + }; + request(options, (err, response, body) => { + if (err) { + return reject(err); + } + resolve(JSON.parse(body)); + }); + }); + }, + refreshOneUrl(url) { + const now = Math.round(new Date()/1000); + const data = { + 'Action': 'RefreshCdnUrl', + 'Nonce': generateNonce(), + 'SecretId': config.cos_cdn.SecretId, + 'Timestamp': now, + 'urls.0': url, + }; + data.Signature = generateSign('POST', data); + return new Promise((resolve, reject) => { + const link = 'https://cdn.api.qcloud.com/v2/index.php'; + const options = {method: 'POST', + url: link, + headers: { + 'Cache-Control': 'no-cache', + 'Content-Type': 'application/x-www-form-urlencoded', + }, + form: data, + }; + request(options, (err, response, body) => { + if (err) { + return reject(err); + } + resolve(JSON.parse(body)); + }); + }); + }, + uploadToCDN(fileName, path) { + return new Promise(function(resolve, reject) { + cosCDN.sliceUploadFile({ + Bucket: 'client-1256832210', + Region: 'ap-beijing', + Key: fileName, + FilePath: path, + }, function(err, data) { + if (err) { + reject(err); + } else { + resolve(data); + } + }); + }); + }, +}; diff --git a/src/utils/db.util.js b/src/utils/db.util.js new file mode 100644 index 0000000..7438082 --- /dev/null +++ b/src/utils/db.util.js @@ -0,0 +1,51 @@ +import mongoose from 'mongoose'; +import Promise from 'bluebird'; +import config from '../../config/config'; + + +module.exports = { + validatePost: function(req, validate) { + for (const rule of validate.rules) { + if (rule[1]) { + req.checkBody(rule[0], rule[2]).notEmpty(); + } + } + return req.validationErrors(); + }, + + leanWithId: function(docs) { + for (const doc of docs) { + doc.id = String(doc._id); + } + return docs; + }, + /** + * 根据db名,获取连接 + * @param {string} dbName + * @return {Object} dbConnection + * */ + getConnection: function(dbName) { + const url = config.db_admin.slice(0, config.db_admin.lastIndexOf('/') + 1) + dbName; + return mongoose.createConnection(url, {promiseLibrary: Promise, useNewUrlParser: true}); + }, + getConnAdmin: function() { + return mongoose.createConnection(config.db_admin, {promiseLibrary: Promise, useNewUrlParser: true}); + }, + + getConnSnoopy: function() { + return mongoose.createConnection(config.db_snoopy, {promiseLibrary: Promise, useNewUrlParser: true}); + }, + + getConnDalmatian: function() { + return mongoose.createConnection(config.db_dalmatian, {promiseLibrary: Promise, useNewUrlParser: true}); + }, + + getConnBeagle: function() { + return mongoose.createConnection(config.db_beagle, {promiseLibrary: Promise, useNewUrlParser: true}); + }, + + getConnGhost: function() { + return mongoose.createConnection(config.db_ghost, {promiseLibrary: Promise, useNewUrlParser: true}); + }, + +}; diff --git a/src/utils/error-utils.js b/src/utils/error-utils.js new file mode 100644 index 0000000..39cb072 --- /dev/null +++ b/src/utils/error-utils.js @@ -0,0 +1,58 @@ +'use strict'; + +import _ from 'lodash'; + +module.exports = { + format_alert: function(msg) { + if (msg) { + if (typeof msg === 'string') { + return msg; + } else if (typeof msg[Symbol.iterator] === 'function') { + let msgs = ''; + return msgs; + } else { + return JSON.stringify(msg); + } + } else { + return msg; + } + }, + + format_notify: function(msg) { + if (msg) { + if (typeof msg === 'string') { + return [msg]; + } else if (typeof msg[Symbol.iterator] === 'function') { + const msgs = []; + _.each(msg, function(m) { + if (typeof m === 'string') { + msgs.push(m); + } else if (_.has(m, 'msg')) { + msgs.push(m.msg); + } else if (_.has(m, 'message')) { + msgs.push(m.message); + } else { + msgs.push(JSON.stringify(m)); + } + }); + return msgs; + } else { + return [JSON.stringify(msg)]; + } + } else { + return msg; + } + }, +}; diff --git a/src/utils/express-utils.js b/src/utils/express-utils.js new file mode 100644 index 0000000..e711050 --- /dev/null +++ b/src/utils/express-utils.js @@ -0,0 +1,53 @@ +import errorUtils from './error-utils'; + +module.exports = function() { + return function(req, res, next) { + req.alert = function(msg) { + const m = errorUtils.format_alert(msg); + if (m) { + req.flash('alert', m); + } + }; + + /** + * + * @param {string} type one of ['notify', 'success', 'warning', 'error'] + * @param {string} msg + */ + req.notify = function(type, msg) { + const messages = errorUtils.format_notify(msg); + if (messages) { + const prefix = 'alertify.' + (type ? type : 'notify') + '(\''; + const postfix = '\');'; + for (const m of messages) { + req.flash('notify', prefix + m + postfix); + } + } + }; + + res.renderWithValidate = function(template, obj, validate) { + const objWithValidate = obj ? obj : {}; + objWithValidate.validate = validate; + res.render(template, objWithValidate); + }; + + /** + * 接口成功返回数据的方法, 统一返回errcode 和 errmsg + * @param {Object} data 要返回的数据 + * */ + res.successJson = function(data) { + data.errcode = 0; + data.errmsg = ''; + res.json(data); + }; + + /** + * @param {number} errcode 错误代码 + * @param {string} errmsg 错误消息 + * */ + res.errorJson = function(errcode, errmsg) { + res.json({errcode, errmsg}); + }; + next(); + }; +}; diff --git a/src/utils/file.utils.js b/src/utils/file.utils.js new file mode 100644 index 0000000..62cc53b --- /dev/null +++ b/src/utils/file.utils.js @@ -0,0 +1,131 @@ +'use strict'; +import fs from 'fs'; +import crypto from 'crypto'; +import mime from 'mime-types'; +import dateformat from 'dateformat'; +import config from '../../config/config'; +import path from 'path'; + +/** + * @param {String} str 待hash的string + * @return {String} + * */ +function cryptPwd(str) { + const md5 = crypto.createHash('md5'); + return md5.update(str).digest('hex'); +} +export default { + getFileMimeTypeWithCode(typeCode) { + let filetype = ''; + let mimetype; + switch (typeCode) { + case 'ffd8ffe1': + filetype = 'jpg'; + mimetype = ['image/jpeg', 'image/pjpeg']; + break; + case '47494638': + filetype = 'gif'; + mimetype = 'image/gif'; + break; + case '89504e47': + filetype = 'png'; + mimetype = ['image/png', 'image/x-png']; + break; + case '504b34': + filetype = 'zip'; + mimetype = ['application/x-zip', 'application/zip', 'application/x-zip-compressed']; + break; + case '2f2aae5': + filetype = 'js'; + mimetype = 'application/x-javascript'; + break; + case '2f2ae585': + filetype = 'css'; + mimetype = 'text/css'; + break; + case '5b7bda': + filetype = 'json'; + mimetype = ['application/json', 'text/json']; + break; + case '3c212d2d': + filetype = 'ejs'; + mimetype = 'text/html'; + break; + case '52494646': + filetype = 'webp'; + mimetype = 'image/webp'; + break; + default: + filetype = 'unknown'; + break; + } + return { + fileType: filetype, + mimeType: mimetype, + }; + }, + isImage(fileType) { + return fileType === 'jpg' || fileType === 'jpeg' || fileType === 'png' || fileType === 'gif' || fileType === 'webp'; + }, + getFileMimeType: function(filePath) { + const buffer = new Buffer(8); + const fd = fs.openSync(filePath, 'r'); + fs.readSync(fd, buffer, 0, 8, 0); + const newBuf = buffer.slice(0, 4); + const head1 = newBuf[0].toString(16); + const head2 = newBuf[1].toString(16); + const head3 = newBuf[2].toString(16); + const head4 = newBuf[3].toString(16); + const typeCode = head1 + head2 + head3 + head4; + const result = this.getFileMimeTypeWithCode(typeCode); + fs.closeSync(fd); + return result; + }, + /* 获取头像路径 + * wid: 微信id + * bid: 机器人id + * remote: 是否返回url, 否的话返回本地存储路径 + * */ + getAvatarUrl(wid, bid, remote) { + const widPath = cryptPwd(wid); + let localPath = this.getAvatarPath(bid, remote); + localPath += widPath + '.jpg'; + return localPath; + }, + getAvatarPath(bid, remote) { + let localPath = config.upload_to + '/'; + if (remote) { + localPath = config.upload_prefix + '/'; + } + localPath += 'b/'+ bid + '/avatar/'; + return localPath; + }, + /* 获取图片本地保存路径*/ + getFilePath() { + let localPath = config.upload_to + '/g/'; + localPath += dateformat('yyyy') + '/'; + localPath += dateformat('mm') + '/'; + localPath += dateformat('dd') + '/'; + return localPath; + }, + /* 获取图片等的本地访问url*/ + getFileUrl(type) { + let localPath = config.upload_prefix + '/g/'; + localPath += dateformat('yyyy') + '/'; + localPath += dateformat('mm') + '/'; + localPath += dateformat('dd') + '/'; + return localPath; + }, + extension(file) { + let ext = mime.extension(file.mimetype); + if (ext) { + ext = '.' + ext; + } else { + ext = path.extname(file.originalname); + ext = ext ? ext.toLowerCase() : ''; + } + + return ext; + }, + +}; diff --git a/src/utils/gamesvr.utils.js b/src/utils/gamesvr.utils.js new file mode 100644 index 0000000..2e1cd9b --- /dev/null +++ b/src/utils/gamesvr.utils.js @@ -0,0 +1,299 @@ +import request from "request"; +import Promise from "bluebird"; +import logger from "./logger"; + +const requestData = function(options) { + return new Promise((resolve, reject) => { + request(options, (err, response, body) => { + if (err) { + return reject(err); + } + if (response && response.statusCode === 200) { + const data = JSON.parse(body); + if (data.errcode) { + return reject(new Error(data.errmsg)); + } + resolve(data); + } else { + logger.error( + options, + `server response errorCode: ${response && response.statusCode}` + ); + reject( + new Error( + `server response errorCode: ${response && response.statusCode}` + ) + ); + } + }); + }); +}; +export default { + queryScrollList(svr) { + const qs = { + c: "RollMsg", + a: "getMsgList" + }; + const options = { + url: svr, + qs: qs, + headers: { "cache-control": "no-cache" } + }; + return requestData(options); + }, + updateScroll(svr, data) { + data.c = "RollMsg"; + data.a = "updateMsg"; + const options = { + url: svr, + qs: data, + headers: { "cache-control": "no-cache" } + }; + return requestData(options); + }, + addScroll(svr, data) { + data.c = "RollMsg"; + data.a = "addMsg"; + const options = { + url: svr, + qs: data, + headers: { "cache-control": "no-cache" } + }; + return requestData(options); + }, + deleteScroll(svr, msgid) { + const qs = { + c: "RollMsg", + a: "removeMsg", + msgid: msgid + }; + const options = { + url: svr, + qs: qs, + headers: { "cache-control": "no-cache" } + }; + return requestData(options); + }, + /* begin of mail*/ + queryMailList(svr) { + const qs = { + c: "Mail", + a: "getMailList" + }; + const options = { + url: svr, + qs: qs, + headers: { "cache-control": "no-cache" } + }; + return requestData(options); + }, + updateMail(svr, data) { + data.c = "Mail"; + data.a = "updateMail"; + const options = { + url: svr, + qs: data, + headers: { "cache-control": "no-cache" } + }; + return requestData(options); + }, + addMail(svr, data) { + data.c = "Mail"; + data.a = "addMail"; + const options = { + url: svr, + qs: data, + headers: { "cache-control": "no-cache" } + }; + return requestData(options); + }, + deleteMail(svr, mailid) { + const qs = { + c: "Mail", + a: "deleteMail", + mailid: mailid + }; + const options = { + url: svr, + qs: qs, + headers: { "cache-control": "no-cache" } + }; + return requestData(options); + }, + getItemNames(svr, itemIdArr) { + const itemIdStr = itemIdArr.join(","); + const qs = { + c: "Item", + a: "getItemsInfo", + item_ids: itemIdStr + }; + const options = { + url: svr, + qs: qs, + headers: { "cache-control": "no-cache" } + }; + return requestData(options); + }, + searchItem(svr, { itemId, itemName, start, limit }) { + const qs = { + c: "Item", + a: "searchItem", + start: start, + limit: limit + }; + if (itemId) qs.item_id = itemId; + if (itemName) qs.item_name = itemName; + const options = { + url: svr, + qs: qs, + headers: { "cache-control": "no-cache" } + }; + return requestData(options); + }, + /* end of mail*/ + /* begin of activity*/ + queryActivityList(svr) { + const qs = { + c: "Activity", + a: "getActivityList" + }; + const options = { + url: svr, + qs: qs, + headers: { "cache-control": "no-cache" } + }; + return requestData(options); + }, + updateActivity(svr, data) { + data.c = "Activity"; + data.a = "updateActivity"; + const options = { + url: svr, + qs: data, + headers: { "cache-control": "no-cache" } + }; + return requestData(options); + }, + /* end of activity*/ + + /* begin of recharge*/ + queryRechargeList(svr, data) { + data.c = "GMTool"; + data.a = "execSql"; + const options = { + url: svr, + qs: data, + headers: { "cache-control": "no-cache" } + }; + return requestData(options); + }, + addRecharge(svr, data) { + data.c = "GMTool"; + data.a = "addVirtualOrder"; + const options = { + url: svr, + qs: data, + headers: { "cache-control": "no-cache" } + }; + return requestData(options); + }, + + /* end of recharge*/ + + /* begin of users*/ + queryUsersList(svr, data) { + data.c = "User"; + data.a = "searchUser"; + const options = { + url: svr, + qs: data, + headers: { "cache-control": "no-cache" } + }; + return requestData(options); + }, + updateUser(svr, data) { + data.c = "Item"; + data.a = "searchUser"; + const options = { + url: svr, + qs: data, + headers: { "cache-control": "no-cache" } + }; + return requestData(options); + }, + // deleteUser(svr, accountid) { + // const qs = { + // accountid: accountid + // }; + // const options = { + // url: svr, + // qs: qs, + // headers: { "cache-control": "no-cache" } + // }; + // return requestData(options); + // }, + shutupUser(svr, data) { + data.c = "User"; + data.a = "forbidSpeak"; + const options = { + url: svr, + qs: data, + headers: { "cache-control": "no-cache" } + }; + return requestData(options); + }, + forbidUser(svr, data) { + data.c = "User"; + data.a = "forbidAccount"; + const options = { + url: svr, + qs: data, + headers: { "cache-control": "no-cache" } + }; + return requestData(options); + }, + disforbidUser(svr, data) { + data.c = "User"; + data.a = "disforbidAccount"; + const options = { + url: svr, + qs: data, + headers: { "cache-control": "no-cache" } + }; + return requestData(options); + }, + disforbidSpeak(svr, data) { + data.c = "User"; + data.a = "forbidSpeak"; + const options = { + url: svr, + qs: data, + headers: { "cache-control": "no-cache" } + }; + return requestData(options); + }, + queryForbid(svr, data) { + data.c = "User"; + data.a = "searchForbidAccount"; + const options = { + url: svr, + qs: data, + headers: { "cache-control": "no-cache" } + }; + return requestData(options); + }, + queryShutup(svr, data) { + data.c = "User"; + data.a = "searchForbidSpeak"; + const options = { + url: svr, + qs: data, + headers: { "cache-control": "no-cache" } + }; + return requestData(options); + }, + + + /* end of users*/ + +}; diff --git a/src/utils/gm-logs.js b/src/utils/gm-logs.js new file mode 100644 index 0000000..d404fe1 --- /dev/null +++ b/src/utils/gm-logs.js @@ -0,0 +1,21 @@ +export default { + type: { + servers: "服务器管理", + announces: "公告", + scroll_texts: "跑马灯", + mails: "邮件", + activity: "活动", + list: "兑换码", + gift_list: "礼包", + recharge_record: "充值记录", + recharge_request: "充值请求", + users: "用户管理", + chat_logs: "聊天信息", + op_logs: "操作记录" + }, + abstractType(path) { + const reg = /\/gm\/(.+).html/g; + const result = reg.exec(path); + return result ? result[1] : ''; + } +}; diff --git a/src/utils/logger.js b/src/utils/logger.js new file mode 100644 index 0000000..424d5f7 --- /dev/null +++ b/src/utils/logger.js @@ -0,0 +1,122 @@ +'use strict'; +import fs from 'fs-extra'; +import FileStreamRotator from 'file-stream-rotator'; +import bunyan from 'bunyan'; +import config from '../../config/config'; +import AdminLog from '../models/admin/AdminLog'; + + +const env = process.env.NODE_ENV || 'development'; +const isDev = env === 'development'; + +const logDir = config.logs_path; +fs.existsSync(logDir) || fs.mkdirSync(logDir); + +let logger = null; +const createLogger = function(appName) { + appName = !appName ? config.app.name : appName; + const streams = [{ + level: 'info', + stream: FileStreamRotator.getStream({ + date_format: 'YYYYMMDD', + filename: `${logDir}/${appName}-%DATE%.log`, + frequency: 'daily', + verbose: false, + }), + }]; + if (isDev) { + streams.push({ + level: 'debug', + stream: process.stdout, + }); + } + return bunyan.createLogger({ + name: appName, + serializers: bunyan.stdSerializers, + streams: streams, + src: false, + }); +}; +export default { + info(obj, msg) { + if (!logger) { + logger = createLogger(global.app_name); + } + if (msg) { + logger.info(obj, msg); + } else { + logger.info(obj); + } + }, + error(obj, msg) { + if (!logger) { + logger = createLogger(global.app_name); + } + if (msg) { + logger.error(obj, msg); + } else { + logger.error(obj); + } + }, + warn(obj, msg) { + if (!logger) { + logger = createLogger(global.app_name); + } + if (msg) { + logger.warn(obj, msg); + } else { + logger.warn(obj); + } + }, + debug(obj, msg) { + if (!logger) { + logger = createLogger(global.app_name); + } + if (msg) { + logger.debug(obj, msg); + } else { + logger.debug(obj); + } + }, + trace(obj, msg) { + if (!logger) { + logger = createLogger(global.app_name); + } + if (msg) { + logger.trace(obj, msg); + } else { + logger.trace(obj); + } + }, + fatal(obj, msg) { + if (!logger) { + logger = createLogger(global.app_name); + } + if (msg) { + logger.fatal(obj, msg); + } else { + logger.fatal(obj); + } + }, + db(req, logObj, name) { + const user = req.user; + const ip = req.headers['x-forwarded-for']; + const path = req.baseUrl + req.path; + const params = req.method === 'GET' ? req.query : req.body; + const dataObj = JSON.stringify(logObj) === '{}' ? params : logObj; + const obj = new AdminLog({ + admin: user.id, + username: user.username, + path: path, + method: req.method, + params: dataObj, + referer: req.headers['referer'], + user_agent: req.headers['user-agent'], + ip: ip, + show_name: name, + }); + obj.save().then(()=>{}).catch((err)=> { + logger.error(err); + }); + }, +}; diff --git a/src/utils/string.utils.js b/src/utils/string.utils.js new file mode 100644 index 0000000..ad65d83 --- /dev/null +++ b/src/utils/string.utils.js @@ -0,0 +1,236 @@ +'use strict'; +import crypto from 'crypto'; +import format from 'biguint-format'; + +const chnNumChar = { + 零: 0, + 一: 1, + 二: 2, + 三: 3, + 四: 4, + 五: 5, + 六: 6, + 七: 7, + 八: 8, + 九: 9, +}; +const chnNameValue = { + 十: {value: 10, secUnit: false}, + 百: {value: 100, secUnit: false}, + 千: {value: 1000, secUnit: false}, + 万: {value: 10000, secUnit: true}, + 亿: {value: 100000000, secUnit: true}, +}; +export default { + /** + * 判断传入的值是否为true + * @param {string} obj 传入值为'true','TRUE',1,'1','on','ON','YES','yes'时,返回true,其他值均返回false + * @return {boolean} + */ + isTrue(obj) { + return obj === 'true' || obj === 'TRUE' || obj === 'on' || obj === 'ON' || obj === true || obj === 1 + || obj === '1' || obj === 'YES' || obj === 'yes'; + }, + isNull(obj) { + return !obj || obj === 'null' || obj === 'NULL' || obj === '' || obj === 'undefined'; + }, + isObjNull(obj) { + return !obj || JSON.stringify(obj) === '{}'; + }, + splitString(text, separator, length) { + const list = []; + let lastIndex = 0; + for (let i = 0; i < length - 1; i++) { + const j = text.indexOf(separator, lastIndex); + if (j === -1) { + break; + } else { + list.push(text.slice(lastIndex, j)); + lastIndex = j + 1; + } + } + list.push(text.slice(lastIndex, text.length)); + return list; + }, + md5(text) { + return crypto.createHash('md5').update(this.toBuffer(text)).digest('hex'); + }, + sha1(str) { + return crypto.createHash('sha1').update(str).digest('hex'); + }, + sha1keyBase64(str, key) { + return crypto.createHmac('sha1', key).update(str).digest('base64'); + }, + toBuffer(data) { + if (Buffer.isBuffer(data)) return data; + if (typeof data === 'string') return new Buffer(data); + throw new Error('invalid data type, must be string or buffer'); + }, + integerToShortString(value) { + return Number(value).toString(36); + }, + isMatch(str, regStr) { + const re = new RegExp(regStr, 'gi'); + return re.test(str); + }, + /* 用正则表达式实现html转码*/ + htmlEncodeByRegExp: function(str) { + let s = ''; + if (str.length === 0) return ''; + s = str.replace(/&/g, '&'); + s = s.replace(//g, '>'); + s = s.replace(/ /g, ' '); + s = s.replace(/'/g, '''); + s = s.replace(/"/g, '"'); + return s; + }, + /* 用正则表达式实现html解码*/ + htmlDecodeByRegExp: function(str) { + let s = ''; + if (str.length === 0) return ''; + s = str.replace(/&/g, '&'); + s = s.replace(/</g, '<'); + s = s.replace(/>/g, '>'); + s = s.replace(/ /g, ' '); + s = s.replace(/'/g, '\''); + s = s.replace(/"/g, '"'); + return s; + }, + removeHtml(content, replceEnter) { + if (replceEnter) { + return content.replace(/<.+?>/g, '').replace(/\r\n/g, '
').replace(/\s/g, '') + .replace(/()+/g, '
').replace(/^/, '').replace(/$/, ''); + } else { + return content.replace(/<.+?>/g, '').replace(/\s/g, ''); + } + }, + parseUrlObj(content) { + content = this.htmlDecodeByRegExp(content); + const re = /^.+?(.+?)<\/title><des>(.+?)<\/des>.+?<url>(.+?)<\/url>.+?<thumburl>(.*?)<\/thumburl>.+?$/; + const contents = content.match(re); + const urlObj = {}; + if (contents) { + if (contents.length > 1) urlObj.title = contents[1]; + if (contents.length > 2) urlObj.desc = contents[2]; + if (contents.length > 3) urlObj.url = contents[3]; + if (contents.length > 4) urlObj.thumburl = contents[4]; + } + return urlObj; + }, + parseFileObj(content) { + content = this.htmlDecodeByRegExp(content); + const re = /^.+?<title>(.+?)<\/title>.+?<totallen>(.+?)<\/totallen>.+?<fileext>(.*?)<\/fileext>.+?$/; + + const contents = content.match(re); + const fileObj = {}; + if (contents) { + if (contents.length > 1) fileObj.title = contents[1]; + if (contents.length > 2) fileObj.totallen = contents[2]; + if (contents.length > 3) fileObj.fileext = contents[3]; + } + return fileObj; + }, + string10to62(number) { + const chars = '0123456789abcdefghigklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ'.split(''); + const radix = chars.length; + let qutient = +number; + const arr = []; + do { + const mod = qutient % radix; + qutient = (qutient - mod) / radix; + arr.unshift(chars[mod]); + } while (qutient); + return arr.join(''); + }, + string62to10(numberCode) { + const chars = '0123456789abcdefghigklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ'; + const radix = chars.length; + numberCode = String(numberCode); + const len = numberCode.length; + let i = 0; + let originNumber = 0; + while (i < len) { + originNumber += Math.pow(radix, i++) * chars.indexOf(numberCode.charAt(len - i) || 0); + } + return originNumber; + }, + checkWithRes(content, res) { + let results = null; + res.some((re) => !!(results = content.match(re))); + return !(results === null || results.length === 0); + }, + /* 移除微信图文消息中不支持的字符*/ + replaceWxUnSupportChar(str) { + return str.replace(/&/g, '%26'); + }, + isArray(object) { + return object && typeof object === 'object' && Array === object.constructor; + }, + /* 中文数字转number*/ + chineseToNumber(chnStr) { + let rtn = 0; + let section = 0; + let number = 0; + let secUnit = false; + const str = chnStr.split(''); + for (let i = 0; i < str.length; i++) { + const num = chnNumChar[str[i]]; + if (typeof num !== 'undefined') { + number = num; + if (i === str.length - 1) { + section += number; + } + } else { + const unit = chnNameValue[str[i]].value; + secUnit = chnNameValue[str[i]].secUnit; + if (secUnit) { + section = (section + number) * unit; + rtn += section; + section = 0; + } else { + section += (number * unit); + } + number = 0; + } + } + return rtn + section; + }, + getReqRemoteIp(req) { + const ip = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/; + const address = (req.headers['x-forwarded-for'] || '').split(',')[0] || req.ip; + const results = ip.exec(address); + return !results && results.length > 0 ? '' : results[0]; + }, + randomNum(minNum, maxNum) { + return parseInt(Math.random()*(maxNum-minNum+1)+minNum, 10); + }, + randomNumAdv(minNum, maxNum) { + const x = format(crypto.randomBytes(4), 'dec'); + return parseInt(x / Math.pow(2, 4 * 8) * (maxNum + 1 - minNum) + minNum); + }, + string10to36(number) { + const chars = '0123456789abcdefghigklmnopqrstuvwxyz'.split(''); + const radix = chars.length; + let qutient = +number; + const arr = []; + do { + const mod = qutient % radix; + qutient = (qutient - mod) / radix; + arr.unshift(chars[mod]); + } while (qutient); + return arr.join(''); + }, + string36to10(numberCode) { + const chars = '0123456789abcdefghigklmnopqrstuvwxyz'; + const radix = chars.length; + numberCode = String(numberCode); + const len = numberCode.length; + let i = 0; + let originNumber = 0; + while (i < len) { + originNumber += Math.pow(radix, i++) * chars.indexOf(numberCode.charAt(len - i) || 0); + } + return originNumber; + }, +}; diff --git a/src/utils/wechat.utils.js b/src/utils/wechat.utils.js new file mode 100644 index 0000000..c5251e4 --- /dev/null +++ b/src/utils/wechat.utils.js @@ -0,0 +1,128 @@ +import config from '../../config/config'; +import request from 'request'; +import Promise from 'bluebird'; +import fs from 'fs'; +import logger from './logger'; + +const refreshToken = function(appId, appSecret) { + return new Promise((resolve, reject) => { + const link = + `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appId}&secret=${appSecret}`; + request(link, (err, response, body) => { + if (err) { + return reject(err); + } + const data = JSON.parse(body); + if (data.errcode) { + return reject(new Error(data.errmsg)); + } + resolve(data.access_token); + }); + }); +}; +export default { + /** + * 获取支付小程序的access token + * 如果global中有token,且未过去(超过2个小时), 则直接返回 + * 否则刷新access token。 + * 如果globa中有token,且时间不到5分钟,则先返回token,然后刷新token + * @return {Promise} + */ + getPayAccessToken() { + const appId = config.pay_weapp.app_id; + const appSecret = config.pay_weapp.app_secret; + return new Promise((resolve, reject) => { + const now = Math.round(new Date() / 1000); + if (global.accessToken && now < global.tokenExpireTime) { + resolve(global.accessToken); + if (global.tokenExpireTime - now < 5 * 60) { + process.nextTick(function() { + refreshToken(appId, appSecret) + .then(() => { + }) + .catch((err) => { + logger.error('refresh access token error'); + }); + }); + } + } else { + refreshToken(appId, appSecret) + .then((token) => { + resolve(token); + }) + .catch((err) => { + reject(err); + }); + } + }); + }, + /* 微信登录凭证校验*/ + codeToSession(code) { + return new Promise((resolve, reject) => { + const link = + `https://api.weixin.qq.com/sns/jscode2session? + appid=${config.weapp.app_id}&secret=${config.weapp.app_secret}&js_code=${code}&grant_type=authorization_code`; + request(link, (err, response, body) => { + if (err) { + return reject(err); + } + const data = JSON.parse(body); + const sessionKey = data.session_key; + const openId = data.openid; + resolve({openId, sessionKey}); + }); + }); + }, + sendMsgToUser(accessToken, data) { + return new Promise((resolve, reject) => { + const link = `https://api.weixin.qq.com/cgi-bin/message/wxopen/template/uniform_send?access_token=${accessToken}`; + const options = { + method: 'POST', + url: link, + headers: { + 'Cache-Control': 'no-cache', + 'Content-Type': 'application/json', + }, + body: data, + json: true, + }; + request(options, (err, response, body) => { + if (err) { + return reject(err); + } + resolve(body); + }); + }); + }, + generateQr(appId, appSecret, scene, filePath) { + const stream = fs.createWriteStream(filePath); + return new Promise((resolve, reject) => { + refreshToken(appId, appSecret) + .then((token) => { + const link = `https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=${token}`; + const data = { + scene: scene, + width: 430, + auto_color: false, + line_color: {'r': '0', 'g': '0', 'b': '0'}, + }; + const options = { + method: 'POST', + url: link, + headers: { + 'Cache-Control': 'no-cache', + 'Content-Type': 'application/json', + }, + body: data, + json: true, + }; + request(options) + .pipe(stream) + .on('close', resolve); + }) + .catch((err) => { + reject(err); + }); + }); + }, +}; diff --git a/test/test.js b/test/test.js new file mode 100644 index 0000000..477a515 --- /dev/null +++ b/test/test.js @@ -0,0 +1,62 @@ +var express = require('express'); +var ldap = require('ldapjs'); + +var app = express(); + +//创建LDAP client,把服务器url传入 +var client = ldap.createClient({ + url: 'ldap://ldap.kingsome.cn:389' +}); + +//创建LDAP查询选项 +//filter的作用就是相当于SQL的条件 +var opts = { + filter: '(uid=yulixing)', //查询条件过滤器,查找uid=kxh的用户节点 + scope: 'sub', //查询范围 + timeLimit: 500 //查询超时 +}; + +var user = []; +app.get('/', function(req, res, next) { + //将client绑定LDAP Server + //第一个参数:是用户,必须是从根节点到用户节点的全路径 + //第二个参数:用户密码 + client.bind('cn=admin,dc=kingsome,dc=cn', 'milesQWE321', function(err, res1) { + //开始查询 + //第一个参数:查询基础路径,代表在查询用户信心将在这个路径下进行,这个路径是由根节开始 + //第二个参数:查询选项 + client.search('ou=people,dc=kingsome,dc=cn', opts, function(err, res2) { + console.log(res2) + //查询结果事件响应 + res2.on('searchEntry', function(entry) { + //获取查询的对象 + var user = entry.object; + var userText = JSON.stringify(user, null, 2); + users = entry + // console.log(entry) + // console.log(userText); + }); + + res2.on('searchReference', function(referral) { + console.log('referral: ' + referral.uris.join()); + }); + + //查询错误事件 + res2.on('error', function(err) { + console.error('error: ' + err.message); + //unbind操作,必须要做 + client.unbind(); + }); + + //查询结束 + res2.on('end', function(result) { + console.log('search status: ' + result); + //unbind操作,必须要做 + client.unbind(); + }); + res.send({}) + }); + }); +}); + +app.listen('6789'); diff --git a/test/test2.js b/test/test2.js new file mode 100644 index 0000000..d7f1a40 --- /dev/null +++ b/test/test2.js @@ -0,0 +1,88 @@ +var express = require('express'); +var ldap = require('ldapjs'); + +var app = express(); + +//创建LDAP client,把服务器url传入 +var client = ldap.createClient({ + url: 'ldap://ldap.kingsome.cn:389' +}); + +//创建LDAP查询选项 +//filter的作用就是相当于SQL的条件 +var opts = { + // filter: '(objectClass=posixAccount)', //查询条件过滤器,查找uid=kxh的用户节点 + filter: '(uid=yulixing1)', //查询条件过滤器,查找uid=kxh的用户节点 + scope: 'sub', //查询范围 + timeLimit: 500 //查询超时 +}; + +var user = []; +app.get('/', function(req, res, next) { + //将client绑定LDAP Server + //第一个参数:是用户,必须是从根节点到用户节点的全路径 + //第二个参数:用户密码 + client.bind('cn=admin,dc=kingsome,dc=cn', 'milesQWE321', function(err, res1) { + console.log(err); + //开始查询 + //第一个参数:查询基础路径,代表在查询用户信心将在这个路径下进行,这个路径是由根节开始 + //第二个参数:查询选项 + client.search('ou=people,dc=kingsome,dc=cn', opts, function(err, res2) { + var entries = []; + //查询结果事件响应 + res2.on('searchEntry', function(entry) { + //获取查询的对象 + var user = entry.object; + entries.push(user); + users = entry; + }); + + res2.on('searchReference', function(referral) { + console.log('referral: ' + referral.uris.join()); + }); + + //查询错误事件 + res2.on('error', function(err) { + console.error('error: ' + err.message); + //unbind操作,必须要做 + client.unbind(); + }); + + //查询结束 + res2.on('end', function(result) { + console.log('search status: ' + result); + console.log(entries) + if (entries.length !== 0) { + client.bind(entries[0].dn, 'yulixing123456', function( + err, + res3 + ) { + if (err) { + res.send({ + err: err, + errmsg: err.message + + }) + } else { + res.send({ + result: entries, + state: 0 + }); + } + }); + } else { + res.send({ + msg: '登录失败' + }) + } + // res.send({ + // entries + // }) + //unbind操作,必须要做 + client.unbind(); + }); + }); + }); +}); + +app.listen('6789');