From dc5423d356cc675a0232860b8efb00c1706563a9 Mon Sep 17 00:00:00 2001 From: chaosBreaking Date: Sat, 31 Aug 2019 14:59:13 +0800 Subject: [PATCH] init --- .eslintrc.json | 19 ++ .gitignore | 7 + CODE.md | 70 ++++ config.js | 5 + index.js | 23 ++ logger/index.js | 42 +++ package.json | 43 +++ script.js | 30 ++ src/app.js | 25 ++ src/common/Auth.js | 0 src/common/Config.js | 64 ++++ src/common/DepMap.js | 7 + src/common/Provider.js | 56 +++ src/common/dbConnect.js | 31 ++ src/jobs/index.js | 19 ++ src/jobs/jobs.js | 126 +++++++ src/modules/action/action.handler.js | 106 ++++++ src/modules/action/action.model.js | 48 +++ src/modules/estate/estate.controller.js | 104 ++++++ src/modules/estate/estate.model.js | 102 ++++++ src/modules/estate/estate.service.js | 31 ++ .../estateOrder/estateOrder.controller.js | 60 ++++ .../estateOrder/estateOrder.handler.js | 73 ++++ src/modules/estateOrder/estateOrder.model.js | 58 ++++ .../estateOrder/estateOrder.service.js | 25 ++ src/modules/fund/fund.controller.js | 141 ++++++++ src/modules/fund/fund.service.js | 36 ++ src/modules/market/market.controller.js | 5 + src/modules/market/market.model.js | 22 ++ src/modules/market/market.service.js | 11 + src/modules/msgCode/msgCode.controller.js | 89 +++++ src/modules/msgCode/msgCode.model.js | 33 ++ src/modules/otcAds/otcAds.controller.js | 108 ++++++ src/modules/otcAds/otcAds.model.js | 55 +++ src/modules/otcAds/otcAds.service.js | 30 ++ src/modules/otcOrder/otcOrder.controller.js | 87 +++++ src/modules/otcOrder/otcOrder.handler.js | 100 ++++++ src/modules/otcOrder/otcOrder.model.js | 56 +++ src/modules/otcOrder/otcOrder.service.js | 30 ++ src/modules/sign/sign.controller.js | 204 +++++++++++ src/modules/sign/sign.service.js | 40 +++ src/modules/sysBoard/sysBoard.controller.js | 81 +++++ src/modules/sysBoard/sysBoard.model.js | 31 ++ src/modules/sysBoard/sysBoard.service.js | 37 ++ src/modules/system/system.controller.js | 90 +++++ src/modules/system/system.model.js | 26 ++ src/modules/system/system.service.js | 25 ++ src/modules/ticket/ticket.controller.js | 58 ++++ src/modules/ticket/ticket.handler.js | 43 +++ src/modules/ticket/ticket.model.js | 58 ++++ src/modules/ticket/ticket.service.js | 27 ++ src/modules/user/user.controller.js | 71 ++++ src/modules/user/user.model.js | 88 +++++ src/modules/user/user.service.js | 30 ++ src/routes/index.js | 12 + src/server.js | 71 ++++ src/service/index.js | 53 +++ src/service/middleware.js | 27 ++ src/service/serviceMap.js | 20 ++ src/utils/account.js | 41 +++ src/utils/crypto.js | 323 ++++++++++++++++++ src/utils/index.js | 38 +++ src/utils/jwt.js | 30 ++ src/utils/locale.js | 262 ++++++++++++++ src/utils/messenger.js | 50 +++ 65 files changed, 3813 insertions(+) create mode 100644 .eslintrc.json create mode 100644 .gitignore create mode 100644 CODE.md create mode 100644 config.js create mode 100644 index.js create mode 100644 logger/index.js create mode 100644 package.json create mode 100644 script.js create mode 100644 src/app.js create mode 100644 src/common/Auth.js create mode 100644 src/common/Config.js create mode 100644 src/common/DepMap.js create mode 100644 src/common/Provider.js create mode 100644 src/common/dbConnect.js create mode 100644 src/jobs/index.js create mode 100644 src/jobs/jobs.js create mode 100644 src/modules/action/action.handler.js create mode 100644 src/modules/action/action.model.js create mode 100644 src/modules/estate/estate.controller.js create mode 100644 src/modules/estate/estate.model.js create mode 100644 src/modules/estate/estate.service.js create mode 100644 src/modules/estateOrder/estateOrder.controller.js create mode 100644 src/modules/estateOrder/estateOrder.handler.js create mode 100644 src/modules/estateOrder/estateOrder.model.js create mode 100644 src/modules/estateOrder/estateOrder.service.js create mode 100644 src/modules/fund/fund.controller.js create mode 100644 src/modules/fund/fund.service.js create mode 100644 src/modules/market/market.controller.js create mode 100644 src/modules/market/market.model.js create mode 100644 src/modules/market/market.service.js create mode 100644 src/modules/msgCode/msgCode.controller.js create mode 100644 src/modules/msgCode/msgCode.model.js create mode 100644 src/modules/otcAds/otcAds.controller.js create mode 100644 src/modules/otcAds/otcAds.model.js create mode 100644 src/modules/otcAds/otcAds.service.js create mode 100644 src/modules/otcOrder/otcOrder.controller.js create mode 100644 src/modules/otcOrder/otcOrder.handler.js create mode 100644 src/modules/otcOrder/otcOrder.model.js create mode 100644 src/modules/otcOrder/otcOrder.service.js create mode 100644 src/modules/sign/sign.controller.js create mode 100644 src/modules/sign/sign.service.js create mode 100644 src/modules/sysBoard/sysBoard.controller.js create mode 100644 src/modules/sysBoard/sysBoard.model.js create mode 100644 src/modules/sysBoard/sysBoard.service.js create mode 100644 src/modules/system/system.controller.js create mode 100644 src/modules/system/system.model.js create mode 100644 src/modules/system/system.service.js create mode 100644 src/modules/ticket/ticket.controller.js create mode 100644 src/modules/ticket/ticket.handler.js create mode 100644 src/modules/ticket/ticket.model.js create mode 100644 src/modules/ticket/ticket.service.js create mode 100644 src/modules/user/user.controller.js create mode 100644 src/modules/user/user.model.js create mode 100644 src/modules/user/user.service.js create mode 100644 src/routes/index.js create mode 100644 src/server.js create mode 100644 src/service/index.js create mode 100644 src/service/middleware.js create mode 100644 src/service/serviceMap.js create mode 100644 src/utils/account.js create mode 100644 src/utils/crypto.js create mode 100644 src/utils/index.js create mode 100644 src/utils/jwt.js create mode 100755 src/utils/locale.js create mode 100644 src/utils/messenger.js diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..a851d63 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,19 @@ +{ + "env": { + "commonjs": true, + "es6": true, + "node": true + }, + "extends": "eslint:recommended", + "globals": { + "Atomics": "readonly", + "SharedArrayBuffer": "readonly", + "mylog": "readonly" + }, + "parserOptions": { + "ecmaVersion": 2018 + }, + "rules": { + "semi": "error" + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f2fbc2d --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules +log +package-lock.json +.vscode +configSec.js +*test.js +build \ No newline at end of file diff --git a/CODE.md b/CODE.md new file mode 100644 index 0000000..3896648 --- /dev/null +++ b/CODE.md @@ -0,0 +1,70 @@ +地产 +Estate : { + status: { + 0 -> unavailable 已被购买,暂不可用, + 1 -> available 未被购买,可购买 + } +} + +订单 +Order: { + type: { + 0: 用户购买地产, + 1: 用户卖出地产 + }, + status: { + 0: 进行中(用户持有该订单表示的地产), + 1: 已结束(订单内的地产已不属于用户) + } +} + +交易 +Transaction: { + type: { + 0: 用户买入平台NEW, + 1: 用户场内买入NEW, + 2: 用户场内卖出NEW + }, + status: { + 0: 尚未处理, + 1: 订单成功完成(买卖达成), + 2: 等待买方处理, + 3: 等待卖方处理, + 4: 买方发起申诉, + 5: 卖方发起申诉, + 6: 客服处理中, + 7: 申诉处理完毕 + } +} + + +Action : { + * [0] 充值LOG + * [1] 用户买入地产 + * [2] 用户将LOG换为USDT + * [3] 场外买入LOG + * [4] 场外卖出LOG +} + +## 1.充值 +* 涉及模块:user模块 +* 接口 + * 1. /user/getDepositAddress +* 流程 + * 1. 生成充值地址 + * 2. 监测到用户所属充值地址到账后,发放对应比例的LOG。 + +## 2.买入地产: +* 涉及模块:estate,estateOrder,action +* 接口: + * /estateOrder/purchase +* 流程: + * 1. 生成对应的action + * 2. 将目标estate打上用户印记 + * 3. 在estateOrder内记录用户买入情况 + +## 3.将LOG换为USDT + +## 4.场外买入LOG +## 4.场外卖出LOG + diff --git a/config.js b/config.js new file mode 100644 index 0000000..61614ef --- /dev/null +++ b/config.js @@ -0,0 +1,5 @@ +module.exports = { + port: 80, + protocol: 'http', + host: 'localhost', +}; \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..248dd03 --- /dev/null +++ b/index.js @@ -0,0 +1,23 @@ +'use strict'; +/** + 1.创建数据库连接 + 2.启动任务队列 + 3.启动web服务器 + */ +const mylog = require('./logger')({ root: 'log', file: 'tic.log' }); +global.mylog = mylog; + +const runServer = require('./src/server'); +const startJob = require('./src/jobs'); +const dbConnect = require('./src/common/dbConnect'); + +const run = tasks => { + [...tasks].reduce((value, func) => { + if (value instanceof Promise) { + return value.then(func); + } + return func(value); + }); +}; + +run([dbConnect, startJob,runServer]); diff --git a/logger/index.js b/logger/index.js new file mode 100644 index 0000000..fb6e185 --- /dev/null +++ b/logger/index.js @@ -0,0 +1,42 @@ +/* +var colors = require('colors') // require后,字符串被添加了一系列方法: str.white, str.inverse, ... +// colors.styles: bold,italic,underline,inverse,yellow,cyan,white,magenta,green,red,grey,blue,rainbow,zebra,random +// 自定义的 themes: +colors.setTheme({ + logprompt: 'inverse', + logok:'green', + logerror: 'red', + logwarn: 'magenta', + logtitle: 'cyan' +}) +*/ + +const bunyan = require('bunyan'); +const PrettyStream = require('bunyan-pretty-colors'); +const path = require('path'); + +var prettyStdOut = new PrettyStream(); +prettyStdOut.pipe(process.stdout); + +var logger = function (option) { + option = option || {}; + return bunyan.createLogger({ + name: "log", + src: false, + streams: [ + { + level: 'info', + stream: prettyStdOut + }, + { + level: 'info', + type: 'rotating-file', + path: path.join(option.root || 'data.log/', '/', option.file || 'info.log'), + period: '1d', // daily rotation + count: 30 // keep 30 days + } + ] + }); +}; + +module.exports=logger; // trace, debug, info, warn, error, fatal \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..6b3ac66 --- /dev/null +++ b/package.json @@ -0,0 +1,43 @@ +{ + "name": "landserver", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Nova.xu", + "license": "ISC", + "dependencies": { + "@alicloud/sms-sdk": "^1.1.6", + "agenda": "^2.0.2", + "axios": "^0.19.0", + "bignumber": "^1.1.0", + "bitcore-mnemonic": "^8.6.0", + "body-parser": "^1.16.1", + "bs58check": "^2.1.2", + "bunyan": "^1.8.12", + "bunyan-pretty-colors": "^0.1.7", + "commander": "^2.14.1", + "compression": "^1.7.3", + "cookie-parser": "^1.4.3", + "cors": "^2.8.1", + "cron": "^1.7.1", + "deepmerge": "^4.0.0", + "errorhandler": "^1.5.0", + "express": "^4.17.1", + "js-sha3": "^0.8.0", + "jsonwebtoken": "^8.5.1", + "mongodb": "^3.3.0-beta2", + "mongoose": "^5.6.8", + "morgan": "^1.8.1", + "node-schedule": "^1.3.0", + "nodemailer": "^6.3.0", + "socket.io": "^1.0.6", + "tweetnacl": "^1.0.1", + "web3": "^1.2.1" + }, + "devDependencies": { + "eslint": "^6.1.0" + } +} diff --git a/script.js b/script.js new file mode 100644 index 0000000..78c661d --- /dev/null +++ b/script.js @@ -0,0 +1,30 @@ +'use strict'; +const commander = require('commander'); +const fs = require('fs'); +const cp = require('child_process'); +const template = function (name, item = '') { + switch (item) { + case 'controller': + return `module.exports = {\n\n};`; + case 'model': + // eslint-disable-next-line no-case-declarations + const schemaName = name.split('').map((e,i) => i===0 ? e.toUpperCase() : e).join(''); + return `const mongoose = require('mongoose');\n\nconst ${schemaName}Schema = new mongoose.Schema({\n\n},{});\n\nconst ${schemaName}Model = mongoose.model('${schemaName}', ${schemaName}Schema);\n\nmodule.exports = ${schemaName}Model;`; + case 'service': + return `const indexRoute = '/${name}';\nconst controller = require('./${name}.controller');\nconst services = [];\n\nmodule.exports = services.map(service => {\n// 可以在此处对要导出的服务map进行统一处理\n\tservice.url = indexRoute + service.url;\n\treturn service;\n});`; + } +}; + +commander +.version(this.VERSION, '-v, --version') +.option('-n, --name ', 'module name.') +.parse(process.argv); + +const name = commander.name; +const subList = ['controller', 'model', 'service']; +cp.execSync(`mkdir ./src/modules/${name}`); +subList.map(item => { + cp.execSync(`touch ./src/modules/${name}/${name}.${item}.js`); + fs.writeFile(`./src/modules/${name}/${name}.${item}.js`, `'use strict';\n\n` + template(name, item) , () => { console.log(`module ${name} - ${item} 创建完毕`); }); +}); + diff --git a/src/app.js b/src/app.js new file mode 100644 index 0000000..9a182b0 --- /dev/null +++ b/src/app.js @@ -0,0 +1,25 @@ +'use strict'; +const path = require('path'); +const app = require('express')(); +const mountRoute = require('./routes/index'); + +/** * 通用中间件 ***/ +app.use(require('morgan')(app.get('env') === 'development' ? 'dev' : 'combined')); // , {stream:require('fs').createWriteStream(path.join(__dirname+'/data.log', 'http.log'), {flags: 'a', defaultEncoding: 'utf8'})})) // format: combined, common, dev, short, tiny. +app.use(require('cookie-parser')()); +app.use(require('body-parser').json({ limit: '50mb', extended: true })); // 用于过滤 POST 参数 +app.use(require('cors')()); +app.use(require('compression')()); +app.use(require('express').static(path.join(__dirname, '../dist'), { index: 'index.html' })); // 可以指定到 node应用之外的目录上。windows里要把 \ 换成 /。 + + +//初始化所有路由 +mountRoute(app); + +if (app.get('env') === 'development') { + app.use(require('errorhandler')({ + dumpExceptions: true, + showStack: true + })); +} + +module.exports = app; \ No newline at end of file diff --git a/src/common/Auth.js b/src/common/Auth.js new file mode 100644 index 0000000..e69de29 diff --git a/src/common/Config.js b/src/common/Config.js new file mode 100644 index 0000000..de130ce --- /dev/null +++ b/src/common/Config.js @@ -0,0 +1,64 @@ +const commander = require('commander'); +const deepmerge = require('deepmerge'); +const fs = require('fs'); + +class Config { + constructor() { + mylog.info('★★★★★★★★ [Config]初始化:依次载入系统配置、用户配置、命令行参数 ★★★★★★★★'); + this.VERSION = '0.0.1'; + this._config = {}; + this.loadConfigFile(); + this.loadCommander(); + } + static getInstance() { + if (!Config.instance) { + Config.instance = new Config(); + } + return Config.instance; + } + loadConfigFile() { + // 读取配置文件 + try { + // CAUTION!!! fs.existsSync的base在文件根目录下!!!! + if (fs.existsSync('config.js')) { + this._config = require('../../config.js'); + mylog.info('基本配置加载完成'); + } + if (fs.existsSync(`configSec.js`)) { // 如果存在,覆盖掉 ConfigBasic 里的默认参数 + this._config = deepmerge(this._config, require(`../../configSec.js`)); // 注意,objectMerge后,产生了一个新的对象,而不是在原来的Config里添加 + mylog.info('隐私配置加载完成'); + } + } catch (err) { + throw new Error('配置加载出错: ' + err.message); + } + return this._config; + } + loadCommander() { + commander + .version(this.VERSION, '-v, --version') + .option('--dbType ', 'Database type: mysql|sqlite. ') + .option('--dbName ', 'Database name') + .option('-H, --host ', 'Host ip or domain name. ') + .option('-P, --protocol ', 'Server protocol: http|https|httpall. ') + .option('-p, --port ', 'Server port number.') + .option('--sslType ', `SSL provider type: file|greenlock`) + .option('--sslCert ', 'SSL certificate file. ') + .option('--sslKey ', 'SSL private key file. ') + .option('--sslCA ', 'SSL ca bundle file') + .parse(process.argv); + + // 把命令行参数 合并入配置。 + this._config.dbType = commander.dbType || this._config.dbType; + this._config.dbName = commander.dbName || this._config.dbName; + this._config.protocol = commander.protocol || this._config.protocol; + this._config.port = parseInt(commander.port) || parseInt(this._config.port) || (this._config.protocol === 'http' ? 80 : this._config.protocol === 'https' ? 443 : undefined); // 端口默认为http 80, https 443, 或80|443(httpall) + this._config.sslCert = commander.sslCert || this._config.sslCert; + this._config.sslKey = commander.sslKey || this._config.sslKey; + this._config.sslCA = commander.sslCA || this._config.sslCA; + } + get config() { + return this._config; + } +} + +module.exports = Config.getInstance(); diff --git a/src/common/DepMap.js b/src/common/DepMap.js new file mode 100644 index 0000000..6a93d2d --- /dev/null +++ b/src/common/DepMap.js @@ -0,0 +1,7 @@ + 'use strict'; +// 在此注册要放在服务容器Provider里的模块 + +module.exports = { + SysConfig: require('./Config').config, + System: require('../modules/system/system.controller').provider +}; \ No newline at end of file diff --git a/src/common/Provider.js b/src/common/Provider.js new file mode 100644 index 0000000..47b9bfe --- /dev/null +++ b/src/common/Provider.js @@ -0,0 +1,56 @@ +const { getFuncProps } = require('../utils'); +const moduleMap = require('./DepMap'); + +class Provider { + constructor() { + this._cache = {}; + this.init(); + } + static getInstance() { + if (!Provider.instance) { + Provider.instance = new Provider(); + } + return Provider.instance; + } + get cache() { + return this._cache; + } + init() { + this._cache = moduleMap; + } + regist(key, value) { + this._cache[key] = value; + } + loadDep (depName) { + const dep = moduleMap[depName]; + if (dep) { + this._cache[depName] = require(dep); + } + } + inject(fn, scope = {}) { + const deps = getFuncProps(fn) || []; + for(let i = 0, len = deps.length; i < len; i++) { + const depName = deps[i]; + const dep = this.cache[depName]; + if(dep) { + deps[i] = dep; + } else { + throw new Error('Cannot find dependence'); + } + } + return fn.apply(scope, deps); + } + getModule(depName) { + if (!this.cache[depName]) { + this.loadDep(depName); + } + return this.cache[depName] || {}; + } +} +const provider = Provider.getInstance(); +global.Provider = provider; + +module.exports = { + inject: (...args) => provider.inject(...args), + regist: (...args) => provider.regist(...args), +}; \ No newline at end of file diff --git a/src/common/dbConnect.js b/src/common/dbConnect.js new file mode 100644 index 0000000..3d2e713 --- /dev/null +++ b/src/common/dbConnect.js @@ -0,0 +1,31 @@ +'use strict'; + +const { inject } = require('./Provider'); +const mongoose = require('mongoose'); + +process.on('SIGTERM', () => { + mongoose.disconnect(); + process.exit(0); +}); +process.on('SIGINT', () => { + mongoose.disconnect(); + process.exit(0); +}); + +module.exports = inject(async function connect2db(SysConfig) { + mongoose.set('useCreateIndex', true); + const { DB_USER_NAME, DB_PASSWD, DB_HOST, DB_PORT, DB_NAME } = SysConfig; + const db = mongoose.connection; + db.on('error', mylog.error); + db.once('open', function() { + mylog.info('数据库连接成功...'); + }); + mylog.info('开始连接数据库...'); + await mongoose.connect(`mongodb://${DB_USER_NAME}:${DB_PASSWD}@${DB_HOST}:${DB_PORT}/${DB_NAME}`, { + useNewUrlParser: true, + bufferMaxEntries: 0, + autoReconnect: true, + useFindAndModify: false + }); + return db; +}); diff --git a/src/jobs/index.js b/src/jobs/index.js new file mode 100644 index 0000000..aec2f0a --- /dev/null +++ b/src/jobs/index.js @@ -0,0 +1,19 @@ +'use strict'; +const { inject } = require('../common/Provider'); +const Agenda = require('agenda'); +const mongoose = require('mongoose'); +const mountJobs = require('./jobs'); + +module.exports = () => inject(async function startJob(SysConfig) { + const { DB_USER_NAME, DB_PASSWD, DB_HOST, DB_PORT, JOB_DB_NAME } = SysConfig; + mylog.info('连接到任务队列数据库...'); + const jobConn = await mongoose.createConnection(`mongodb://${DB_USER_NAME}:${DB_PASSWD}@${DB_HOST}:${DB_PORT}/${JOB_DB_NAME}`, { + useNewUrlParser: true, + autoReconnect: true, + }); + mylog.info('连接到任务队列数据库成功'); + const Job = new Agenda({ mongo: jobConn }); + mountJobs(Job); + await Job.start(); + mylog.info('任务队列启动...'); +}); diff --git a/src/jobs/jobs.js b/src/jobs/jobs.js new file mode 100644 index 0000000..96bbd5d --- /dev/null +++ b/src/jobs/jobs.js @@ -0,0 +1,126 @@ +'use strict'; +/** + * 为何要把分属于不同业务模块的job集中到此处? + * 如果后续要把任务执行与web服务器分离,就会很方便,就算不分离,也没多大坏处。 + + * 任务包括: + * transaction : { + * 1. 每小时去清理指定startTime的地产,将 + * { + * status: -> 1, + * price: -> price * (1 + profit), + * currentOwner: -> '' + * } + * 2. 将currentOwner对应的用户的asset.log加上a + * } + */ +const mongoose = require('mongoose'); +const User = require('../modules/user/user.model'); +const Estate = require('../modules/estate/estate.model'); +const EstateOrder = require('../modules/estateOrder/estateOrder.model'); +const { createAction } = require('../modules/action/action.handler'); +const jobs = [ + { + name: 'refreshEstateAndPayBack', + processor: async (job, done) => { + //todo: 程序内检查是否可以执行,保证不会启动时错误的自动执行一次 + const now = new Date().getMinutes(); + if (now > 0 && now < 58) { + mylog.error('[Schedule Job] 不在合理时间启动...程序结束'); + return 0; + } + mylog.info('开始定时计算作业...'); + const query = { startTime: +(new Date().getUTCHours()) + 8 + 1 }; + const elist = await Estate.find(query).exec(); + if (!elist || elist.length <= 0) return 0; + const [ estateSession, userSession, orderSession ] = await Promise.all([ + mongoose.startSession(), + mongoose.startSession(), + mongoose.startSession() + ]); + await Promise.all([ + estateSession.startTransaction(), + userSession.startTransaction(), + orderSession.startTransaction() + ]); + job.touch(); + for (let i = 0, length = elist.length; i < length; i++) { + const estate = elist[i]; + const estateId = estate.id; + const userId = estate.currentOwner; + // todo: 更精确判读是否要回收地产 + if (!userId) continue; + const [ user, order ] = await Promise.all([ + User.findById(userId).exec(), + EstateOrder.findOne({ + userId, + estateId, + status: 'holding' + }).exec() + ]); + const profit = +estate.price * +estate.profit; + const payback = profit + estate.price; + const action = createAction(3, { actor: userId, estate, amount: profit, remain: user.asset }); + if (!Number.isFinite(payback)) throw new Error('非法的收益数值!'); + const userOpts = { returnOriginal: false, session: userSession }; + const orderOpts = { returnOriginal: false, session: orderSession }; + const estateOpts = { returnOriginal: false, session: estateSession }; + try { + await Promise.all([ + user.increase('profit', payback, userOpts), + estate.sell(estateOpts), + order.finish(orderOpts), + action.save() + ]); + await Promise.all([ + userSession.commitTransaction(), + orderSession.commitTransaction(), + estateSession.commitTransaction() + ]); + } catch (error) { + mylog.error(error); + await Promise.all([ + action.remove(), + userSession.abortTransaction(), + orderSession.abortTransaction(), + estateSession.abortTransaction() + ]); + } finally { + await Promise.all([ + userSession.endSession(), + orderSession.endSession(), + estateSession.endSession() + ]); + } + job.touch(); + } + mylog.info('收益计算分配完成'); + done(); + }, + type: 'every', + // 写成 '*/59 * * * *' 会出现59分执行一次,下一小时的0分再执行一次 + // 写成 '*/59 * * * *' 是每59 + // 最差的解决方案是 在58分执行 + slot: '59 */1 * * *', // 59th minute per hour + priority: 'highest', + unique: { + id: 'payback001' + }, + skipImmediate: true, + timezone: "ETC/GMT+8" + } +]; +function mountJobs (Job) { + jobs.map(jobConfig => { + const { name, processor, type, slot, priority, unique, skipImmediate, timezone } = jobConfig; + Job.define(name, { + timezone, + skipImmediate, + priority, + unique: { 'data.type': 'active', 'data.userId': unique.id } + }, processor); + Job[type](slot, name); + }); +} + +module.exports = Job => mountJobs(Job); \ No newline at end of file diff --git a/src/modules/action/action.handler.js b/src/modules/action/action.handler.js new file mode 100644 index 0000000..bc011ea --- /dev/null +++ b/src/modules/action/action.handler.js @@ -0,0 +1,106 @@ +'use strict'; +const Action = require('./action.model'); + +/** + * [0] 充值LOG + * [1] 提现 + * [2] 购买地产 + * [3] 分红 + * [4] 场外买入 + * [5] 场外卖出 + */ +function createAction (actCode, data) { + switch (actCode) { + case 0: return createDepositAction(data); + case 1: return createWithdrawAction(data); + case 2: return createPurchaseAction(data); + case 3: return createBonusAction(data); + case 4: return createOtcBuyAction(data); + case 5: return createOtcSellAction(data); + } +} + +function createDepositAction(data = {}) { + // 充值要记录: 发起人,充值目标地址,充值货币类型,充值金额 + // 只创建action并返回,不保存,保存交给具体的业务在事务内执行 + const { actor, amount, tokenType, toAddress, remain } = data; + if (!actor || !amount || !tokenType || !toAddress) return false; + return new Action({ + actor, + type: 0, + op: 1, + amount, + remain, + detail: { toAddress, tokenType } + }); +} +function createWithdrawAction(data = {}) { + const { actor, amount, remain } = data; + return new Action({ + actor, + type: 1, + op: 0, + amount, + remain, + detail: { + } + }); +} +function createPurchaseAction(data = {}) { + const { actor, estate, amount, remain } = data; + return new Action({ + actor, + type: 2, + op: 0, + amount, + remain, + detail: { + ...estate + } + }); +} +function createBonusAction(data = {}) { + const { actor, estate, amount, remain } = data; + return new Action({ + actor, + type: 3, + op: 1, + amount, + remain, + detail: { + ...estate + } + }); +} +function createOtcBuyAction(data = {}) { + // 场外交易的actor是交易活动的发起者,也就是去买市场挂单的人。 + const { actor, otcOrder, adv, amount, remain } = data; + return new Action({ + actor, + type: 4, + op: 1, + amount, + remain, + detail: { + order: otcOrder, + adv + } + }); +} +function createOtcSellAction(data = {}) { + const { actor, otcOrder, amount, remain } = data; + return new Action({ + actor, + type: 5, + op: 0, + amount, + remain, + detail: { + ...otcOrder + } + }); +} + +module.exports = { + createAction, +}; diff --git a/src/modules/action/action.model.js b/src/modules/action/action.model.js new file mode 100644 index 0000000..b8f19fe --- /dev/null +++ b/src/modules/action/action.model.js @@ -0,0 +1,48 @@ +'use strict'; +// 用户事务模型,记录用户的买入、获得收益、兑换等一系列财务交易操作。 +/** + * [0] 充值LOG + * [1] LOG提现为USDT + * [2] 用户买入地产 + * [3] 地产收益分配 + * [4] 场外买入LOG + * [5] 场外卖出LOG + */ + +const mongoose = require('mongoose'); + +const ActionSchema = new mongoose.Schema({ + actor: { + type: String, + required: true, + }, + type: { + type: String, + required: true + }, + amount: { + type: String + }, + remain: { + // 操作前资产快照 + type: {} + }, + op: { + // 0支出 + // 1收入 + // note: 这里的支出和收入是针对平台内的财产 也就是log而言的 + type: Number, + }, + detail: { + type: {} + } +}, { + timestamps: { + createdAt: 'createdAt', + updatedAt: 'updatedAt' + } +}); + +const actionSchema = mongoose.model('Action', ActionSchema); + +module.exports = actionSchema; \ No newline at end of file diff --git a/src/modules/estate/estate.controller.js b/src/modules/estate/estate.controller.js new file mode 100644 index 0000000..7964870 --- /dev/null +++ b/src/modules/estate/estate.controller.js @@ -0,0 +1,104 @@ +'use strict'; + +const Estate = require('./estate.model'); +const filter = estate => { + // 用于返回前过滤敏感数据 +}; +let getGlobalState = (() => { + let GlobalState = []; + let lastUpdateAt = Date.now(); + let freq = 1000 * 60 * 5; // 5min更新一次 + let timezones = Array.from({length:24}); + const refresh = async () => { + const jobs= timezones.map(async (v, i) => Estate.find({ startTime: i, status: 1 }).countDocuments()); + const data = await Promise.all(jobs).catch(err => { + mylog.error('地产全局状态获取失败:', err); + return null; + }); + // eslint-disable-next-line require-atomic-updates + if (data) GlobalState = data; + lastUpdateAt = Date.now(); + }; + refresh(); + return async () => { + if (GlobalState.length === 0 || Date.now() - lastUpdateAt >= freq) { + await refresh(); + } + return GlobalState; + }; +})(); + +module.exports = { + getEstateList: async function (req, res) { + const { pageSize, pageIndex, timezone } = req.query; + if (pageSize && pageIndex && timezone) { + const list = await Estate.find({ + timezone, + }, { + skip: +pageSize * (+pageIndex - 1), + limit: +pageSize + }).exec(); + if (list && Array.isArray(list)) { + res.json({ + code: 0, + msg: '', + data: list + }); + } + } else { + res.json({ + code: 1, + msg: 'Param required' + }); + } + }, + getEstateDetail: async function (req, res) { + const { id } = req.query; + if (id) { + const estate = await Estate.findById(id).exec(); + if (estate) { + res.json({ + code: 0, + msg: '', + data: estate + }); + } + } + res.json({ + code: 1, + msg: 'Cannot find estate, invalid id', + data: null + }); + }, + publishEstate: function (req, res) { + const { data = {}} = req.body; + const invalidEstate = !data || !data.name || (!data.basePrice && !data.price) || !data.startTime || !data.profit || data.profit > 1; + if (!data.basePrice && data.price) data.basePrice = data.price; + if (!data.price && data.basePrice) data.price = data.basePrice; + if (invalidEstate) { + return res.json({ + code: 1, + msg: 'Invalid parameter' + }); + } + const estate = new Estate(data); + return estate.save().then(newEstate => { + res.json({ + code: 0, + msg: 'success', + data: newEstate + }); + }).catch(err => { + res.json({ + code: 1, + msg: JSON.stringify(err) + }); + }); + }, + getState: async function (req, res) { + return res.json({ + code: 0, + data: await getGlobalState() + }); + } +}; \ No newline at end of file diff --git a/src/modules/estate/estate.model.js b/src/modules/estate/estate.model.js new file mode 100644 index 0000000..3f852c8 --- /dev/null +++ b/src/modules/estate/estate.model.js @@ -0,0 +1,102 @@ +'use strict'; + +const mongoose = require('mongoose'); +const EstateSchema = new mongoose.Schema({ + name: { + // 地皮名 + type: String, + required: true, + unique: true + }, + type: { + // 为edition 2保留,分为系统生成的地皮和玩家自建的地皮 + type: String, + default: 'v1' + }, + basePrice: { + type: String, + required: true + }, + price: { + // 地皮的市场标价 + type: Number, + required: true + }, + profit: { + // 收益率 + type: Number, + required: true + }, + startTime: { + // 开始抢购时间 + type: String, + required: true + }, + period: { + // 换手周期,目前默认为24(hour) + type: Number, + default: 24, + }, + status: { + // 当前状态 不可买:可买 -> 0 : 1 + type: Number, + default: 1 + }, + desc: { + // 地皮描述 + type: String, + default: '' + }, + image: { + // 图片链接 + type: String + }, + currentOwner: { + type: String + }, + lastTurnAt: { + type: String + }, + turnTimes: { + type: Number, + default: 0 + }, + option: { + type: {} + } +}, { + timestamps: { + createdAt: 'createdAt', + updatedAt: 'updatedAt' + } +}); + +const EstateModel = mongoose.model('Estate', EstateSchema); + +EstateModel.prototype.purchase = async function (userId, option = {}) { + return EstateModel.findByIdAndUpdate(this.id, { + status: 0, + currentOwner: userId, + lastTurnAt: new Date().toISOString(), + $inc: { turnTimes: 1 } + }, option).exec().catch(err => { + mylog.error(err); + return null; + }); +}; + +EstateModel.prototype.sell = async function (option = {}) { + const price = +this.price * (1 + +this.profit); + return EstateModel.findByIdAndUpdate(this.id, { + $set: { + status: 1, + currentOwner: '', + price, + } + }, option).exec().catch(err => { + mylog.error(err); + return null; + }); +}; + +module.exports = EstateModel; \ No newline at end of file diff --git a/src/modules/estate/estate.service.js b/src/modules/estate/estate.service.js new file mode 100644 index 0000000..4067444 --- /dev/null +++ b/src/modules/estate/estate.service.js @@ -0,0 +1,31 @@ +const indexRoute = '/estate'; +const controller = require('./estate.controller'); + +const services = [ + { + url: '/list', + method: 'GET', + controller: controller.getEstateList + }, + { + url: '/detail', + method: 'GET', + controller: controller.getEstateDetail + }, + { + url: '/publish', + method: 'POST', + controller: controller.publishEstate + }, + { + url: '/state', + method: 'GET', + controller: controller.getState + }, +]; + +module.exports = services.map(service => { + // 可以在此处对要导出的服务map进行统一处理 + service.url = indexRoute + service.url; + return service; +}); \ No newline at end of file diff --git a/src/modules/estateOrder/estateOrder.controller.js b/src/modules/estateOrder/estateOrder.controller.js new file mode 100644 index 0000000..bc1d06d --- /dev/null +++ b/src/modules/estateOrder/estateOrder.controller.js @@ -0,0 +1,60 @@ +'use strict'; + +const EstateOrder = require('./estateOrder.model'); +const { purchase } = require('./estateOrder.handler'); + +module.exports = { + purchase: async function (req, res) { + const actorId = res.locals.user.userId; + const { estate } = req.body; + const exec = await purchase(actorId, estate); + if (exec.code === 0 && exec.res) { + return res.json({ + code: 0, + msg: 'success', + data: exec.res + }); + } + return res.json({ + code: exec.code, + msg: exec.msg + }); + }, + getUserEstateList (req, res) { + const { userId } = res.locals.user; + const { pageSize, pageIndex, type } = req.query; + // 只返回detail内容 + EstateOrder.find({ type, userId }, 'detail').skip((+pageIndex - 1) * +pageSize).limit(+pageSize) + .then(list => { + res.json({ + code: 0, + msg: '', + data: list + }); + }) + .catch(err => { + res.json({ + code: 1, + msg: err + }); + }); + }, + getUserEstateDetail (req, res) { + const { userId } = res.locals.user; + const option = req.query; + EstateOrder.find({ userId, ...option }, 'detail') + .then(list => { + res.json({ + code: 0, + msg: '', + data: list + }); + }) + .catch(err => { + res.json({ + code: 1, + msg: err + }); + }); + } +}; \ No newline at end of file diff --git a/src/modules/estateOrder/estateOrder.handler.js b/src/modules/estateOrder/estateOrder.handler.js new file mode 100644 index 0000000..5a0588e --- /dev/null +++ b/src/modules/estateOrder/estateOrder.handler.js @@ -0,0 +1,73 @@ +'use strict'; +const mongoose = require('mongoose'); +const User = require('../user/user.model'); +const Estate = require('../estate/estate.model'); +const EstateOrder = require('./estateOrder.model'); +const { createAction } = require('../action/action.handler'); + +module.exports = { + purchase: async function (actorId, estateId) { + /* + * 1. 生成对应的action + * 2. 将目标estate打上用户印记 + * 3. 在estateOrder内记录用户买入情况 + * 4. 对用户扣款 + */ + try { + const [actor, estate] = await Promise.all([ + User.findById(actorId).exec(), + Estate.findById(estateId).exec() + ]); + if (!actor) return { res: null, code: 1, msg: 'unknow user' }; + if (!estate || estate.status !== 1) return { res: null, code: 1, msg: 'estate unavailable' }; + const action = createAction(2, { actor: actorId, estate, amount: estate.price, remain: actor.asset }); + const order = new EstateOrder({ userId: actorId, estateId, type: '0', status: 'holding', amount: estate.price , detail: { ...estate } }); + if (!Number.isFinite(+estate.price)) throw new Error('非法的地产价格!'); + let [ estateSession, orderSession, actorSession ] = await Promise.all([ + mongoose.startSession(), + mongoose.startSession(), + mongoose.startSession() + ]); + await Promise.all([ + estateSession.startTransaction(), + orderSession.startTransaction(), + actorSession.startTransaction() + ]); + const estateOpts = { session: estateSession, returnOriginal: false }; + const orderOpts = { session: orderSession, returnOriginal: false }; + const actorOpts = { session: actorSession, returnOriginal: false }; + try { + await Promise.all([ + estate.purchase(actorId, estateOpts), + order.save(orderOpts), + actor.decrease('log', +estate.price, actorOpts), + action.save() + ]); + await Promise.all([ + estateSession.commitTransaction(), + orderSession.commitTransaction(), + actorSession.commitTransaction() + ]); + return { res : action.toJSON(), code: 0 }; + } catch (error) { + // mylog.error(error); + await Promise.all([ + action.remove(), + estateSession.abortTransaction(), + orderSession.abortTransaction(), + actorSession.abortTransaction() + ]); + return { res: null, code: 3, msg: 'Insufficient funds' }; + } finally { + await Promise.all([ + estateSession.endSession(), + orderSession.endSession(), + actorSession.endSession() + ]); + } + } catch (error) { + mylog.error(error); + return { res: null, code: 500, msg: JSON.stringify(error) }; + } + } +}; diff --git a/src/modules/estateOrder/estateOrder.model.js b/src/modules/estateOrder/estateOrder.model.js new file mode 100644 index 0000000..b6df880 --- /dev/null +++ b/src/modules/estateOrder/estateOrder.model.js @@ -0,0 +1,58 @@ +'use strict'; + +const mongoose = require('mongoose'); +const EstateOrderSchema = new mongoose.Schema({ + userId: { + // 订单所属用户的objId + type: String, + required: true + }, + estateId: { + type: String, + required: true + }, + type: { + // 订单类型 + type: String, + required: true + }, + amount: { + type: Number, + required: true + }, + status: { + // 订单状态 + /** + * holding: 持有 + * sold: 卖出 + */ + type: String, + }, + detail: { + // 订单详情/快照 + type: {}, + required: true + }, + option: { + type: {} + } +}, { + timestamps: { + createdAt: 'createdAt', + updatedAt: 'updatedAt' + } +}); + +const EstateOrderModel = mongoose.model('estateOrder', EstateOrderSchema); + +EstateOrderModel.prototype.finish = async function (option) { + return this.updateOne({ + $set: { + status: 'sold' + } + }, option).exec().catch(err => { + throw new Error('订单完成状态转换失败', JSON.stringify(err)); + }); +}; + +module.exports = EstateOrderModel; \ No newline at end of file diff --git a/src/modules/estateOrder/estateOrder.service.js b/src/modules/estateOrder/estateOrder.service.js new file mode 100644 index 0000000..92527f7 --- /dev/null +++ b/src/modules/estateOrder/estateOrder.service.js @@ -0,0 +1,25 @@ +const indexRoute = '/estateorder'; +const controller = require('./estateOrder.controller'); +const services = [ + { + url: '/list', + method: 'GET', + controller: controller.getUserEstateList + }, + { + url: '/detail', + method: 'GET', + controller: controller.getUserEstateDetail + }, + { + url: '/purchase', + method: 'POST', + controller: controller.purchase + }, +]; + +module.exports = services.map(service => { + // 可以在此处对要导出的服务map进行统一处理 + service.url = indexRoute + service.url; + return service; +}); \ No newline at end of file diff --git a/src/modules/fund/fund.controller.js b/src/modules/fund/fund.controller.js new file mode 100644 index 0000000..046210b --- /dev/null +++ b/src/modules/fund/fund.controller.js @@ -0,0 +1,141 @@ +'use strict'; + +const User = require('../user/user.model'); +const Action = require('../action/action.model'); +const { createAction } = require('../action/action.handler'); +const { createTicket } = require('../ticket/ticket.handler'); +const { deriveNewAccount } = require('../../utils/account'); +const actionFilter = 'id type amount detail op createdAt'; +const { inject } = require('../../common/Provider'); + +module.exports = { + async getFundDetail (req, res) { + const { userId } = res.locals.user; + const user = await User.findById(userId, 'asset').exec().catch(err => { + mylog.error("[getFundDetail]", err); + return null; + }); + if (user && user.asset) { + return res.json({ + code: 0, + msg: 'success', + data: user.asset + }); + } + return res.json({ + code: 1, + error: 'unknow user' + }); + }, + async getActions (req, res) { + const { userId } = res.locals.user; + const { pageIndex, pageSize } = req.query; + return await Action.find({ + actor: userId + }, actionFilter).skip((pageIndex - 1) * pageSize).limit(pageSize) + .exec().then(data => { + return res.json({ + code: 0, + data + }); + }) + .catch(err => { + return res.json({ + code: 1, + error: JSON.stringify(err) + }); + }); + }, + async getActionDetail (req, res) { + const { id } = req.query; + return await Action.findById(id, actionFilter).exec().then(data => { + res.json({ + code: 0, + data + }); + }).catch(err => res.json({ + code: 1, + error: JSON.stringify(err) + })); + }, + async getSupportCoinsList (req, res) { + // eslint-disable-next-line no-undef + const coinList = await Provider.getModule('System').getValue('supportCoins'); + return res.json({ + code: 0, + data: coinList + }); + }, + async withdraw (req, res) { + /** + * 1.创建工单 + * 2.创建用户action + * 3.返回状态 + */ + const { userId } = res.locals.user; + const { amount } = req.body; + const user = await User.findById(userId).exec(); + if (user.asset && user.asset.log < amount) return res.json({ + code: 1, + error: 'insufficient fund' + }); + const action = createAction(1, { actor: userId, amount, remain: user.asset, op: 0 }); + const ticket = createTicket('withdraw', { userId, amount, action }); + try { + await user.decrease('log', +amount); + } catch (error) { + mylog.error('[Fund提现] ', error); + return res.json({ + code: 2, + msg: '扣款失败', + error: JSON.stringify(error) + }); + } + Promise.all([ticket.save(), action.save()]).then(() => { + return res.json({ + code: 0, + msg: '工单已创建' + }); + }).catch(err => { + return res.json({ + code: 3, + msg: '工单创建失败', + error: JSON.stringify(err) + }); + }); + }, + getDepositAddress: inject(function (SysConfig) { + const { ROOTSECWORD } = SysConfig; + if (!ROOTSECWORD) throw new Error('ROOTSECWORD is required'); + return async (req, res) => { + const { userId } = res.locals.user; + const user = await User.findById(userId).exec(); + let data = user.tokenAddress; + if (!data.usdtBTC || !data.usdtETH) { + const seed = parseInt(userId.substring(0, 8), 16).toString().slice(4); //前4位与Date.now前4位重复; + const btc = deriveNewAccount(ROOTSECWORD, { coin: 'BTC', seed }); + const eth = deriveNewAccount(ROOTSECWORD, { coin: 'ETH', seed }); + const user = await User.findByIdAndUpdate(userId, { + $set: { + tokenAddress: { + usdtBTC: btc, + usdtETH: eth + } + } + }, { returnOriginal: false }).exec().catch(err => { + mylog.error(err); + return null; + }); + data = user && user.tokenAddress; + } + return data ? res.json({ + code: 0, + data + }) : res.json({ + code: 1, + error: res + }); + }; + }) + +}; \ No newline at end of file diff --git a/src/modules/fund/fund.service.js b/src/modules/fund/fund.service.js new file mode 100644 index 0000000..737b789 --- /dev/null +++ b/src/modules/fund/fund.service.js @@ -0,0 +1,36 @@ +'use strict'; +const indexRoute = '/fund'; +const controller = require('./fund.controller'); +const services = [ + { + url: '/detail', + method: 'GET', + controller: controller.getFundDetail + }, + { + url: '/actions', + method: 'GET', + controller: controller.getActions + }, + { + url: '/actionDetail', + method: 'GET', + controller: controller.getActionDetail + }, + { + url: '/coins', + method: 'GET', + controller: controller.getSupportCoinsList + }, + { + url: '/queryAddress', + method: 'GET', + controller: controller.getDepositAddress + }, +]; + +module.exports = services.map(service => { + // 可以在此处对要导出的服务map进行统一处理 + service.url = indexRoute + service.url; + return service; +}); \ No newline at end of file diff --git a/src/modules/market/market.controller.js b/src/modules/market/market.controller.js new file mode 100644 index 0000000..59eaaff --- /dev/null +++ b/src/modules/market/market.controller.js @@ -0,0 +1,5 @@ +const { inject } = require('../../common/Provider'); + +module.exports = { + +}; \ No newline at end of file diff --git a/src/modules/market/market.model.js b/src/modules/market/market.model.js new file mode 100644 index 0000000..663acb7 --- /dev/null +++ b/src/modules/market/market.model.js @@ -0,0 +1,22 @@ +'use strict'; + +const mongoose = require('mongoose'); +const MarketSchema = new mongoose.Schema({ + name: { + type: String, + required: true, + unique: true + }, + type: { + type: String, + required: true + }, + price: { + type: String, + required: true + }, +}); + +const marketSchema = mongoose.model('Market', MarketSchema); + +module.exports = marketSchema; \ No newline at end of file diff --git a/src/modules/market/market.service.js b/src/modules/market/market.service.js new file mode 100644 index 0000000..94a223e --- /dev/null +++ b/src/modules/market/market.service.js @@ -0,0 +1,11 @@ +const indexRoute = '/market'; +const controller = require('./market.controller'); +const services = [ + +]; + +module.exports = services.map(service => { + // 可以在此处对要导出的服务map进行统一处理 + service.url = indexRoute + service.url; + return service; +}); \ No newline at end of file diff --git a/src/modules/msgCode/msgCode.controller.js b/src/modules/msgCode/msgCode.controller.js new file mode 100644 index 0000000..be142f1 --- /dev/null +++ b/src/modules/msgCode/msgCode.controller.js @@ -0,0 +1,89 @@ +'use strict'; +const Code = require('./msgCode.model'); +const Crypto = require('../../utils/crypto'); +const { inject } = require('../../common/Provider'); +const sendSMS = inject(require('../../utils/messenger').sendSms); +const CODE_TYPE = { + msgSignIn: -1, + signUp: 1, + forgetPasswd: 2, + changePasswd: 3 +}; +/** + * code type: + * -1:免密登录 + * 0: 注册 + * 1: 重置密码 + * 2: 更换手机等信息 + * 3: 重置财务信息(资金密码,etc...) + */ +module.exports = { + CODE_TYPE, + async sendMsgCode (phone, type) { + const validCodeType = CODE_TYPE[type]; + if (phone && type && validCodeType) { + const code = Crypto.randomNumber({length: 6}); + const [ sms, save ] = await Promise.all([ + sendSMS(phone, { + msgParam: { code }, + templateCode: 'SMS_142465215', + signName: 'TIC钱包管家' + }), + this.saveCode({ + id: phone, + code, + type: CODE_TYPE[type] + }) + ]).catch(err => { + mylog.error(err); + return [null, null]; + }); + if (sms && save) return { + code: 0, + res: code + }; + } + return { + code: 1, + error: 'Invalid param' + }; + }, + async saveCode (data) { + const res = await (new Code({ ...data })).save(); + if(!res) { + return false; + } else { + return true; + } + }, + async verifyCode (data = {}) { + // think: 是直接查是否存在正确的{ id. code }组合来验证呢,还是先拿到id,再对比code正确? + const { id, code, type } = data; + const res = await Code.findOneAndDelete({ id, code, type }).exec(); + // Code.findOneAndDelete + if (!res) { + return false; + } else { + return true; + } + }, + async changeCode (data) { + // code means: newCode + const { id, code, type, option } = data; + if (id && code && type) { + const query = { id, type }; + const update = { code }; + const res = await Code.findOneAndUpdate(query, update, { new: true, ...option }).exec(); + if (res) return true; + } + return false; + }, + async deleteCode (data) { + const { id, code, type } = data; + if (id && code && type) { + const res = await Code.deleteOne({ id, code, type }).exec(); + if (res) return true; + } + return false; + }, +}; \ No newline at end of file diff --git a/src/modules/msgCode/msgCode.model.js b/src/modules/msgCode/msgCode.model.js new file mode 100644 index 0000000..afd518f --- /dev/null +++ b/src/modules/msgCode/msgCode.model.js @@ -0,0 +1,33 @@ +'use strict'; + +const mongoose = require('mongoose'); +const CodeSchema = new mongoose.Schema({ + id: { + type: String, + required: true, + unique: true + }, + code: { + type: String, + required: true, + unique: true + }, + type: { + type:String, + required: true, + }, + expiresAt: { + type: Date, + default: Date.now, + expires: 3600 + } +}, { + timestamps: { + createdAt: 'createdAt', + updatedAt: 'updatedAt' + } +}); + +const CodeModel = mongoose.model('MsgCode', CodeSchema); + +module.exports = CodeModel; \ No newline at end of file diff --git a/src/modules/otcAds/otcAds.controller.js b/src/modules/otcAds/otcAds.controller.js new file mode 100644 index 0000000..84f0b27 --- /dev/null +++ b/src/modules/otcAds/otcAds.controller.js @@ -0,0 +1,108 @@ +'use strict'; +const mongoose = require('mongoose'); +const OtcAds = require('./otcAds.model'); +const User = require('../user/user.model'); + +module.exports = { + publishAd: async function (req, res) { + const { userId } = res.locals.user; + const { type, totalAmount, option } = req.body; + if (!Number.isFinite(+totalAmount)) return res.json({ + code: 4, + msg: 'illegal amount' + }); + const user = await User.findById(userId).exec(); + if (!user) return res.json({ + code: 1, + msg: 'unknow user' + }); + const balance = +user.asset.profit - +totalAmount; + if (!Number.isFinite(balance) || balance < 0) return res.json({ + code: 2, + msg: 'insufficient fund' + }); + const ads = new OtcAds({ userId, type, totalAmount: +totalAmount, restAmount: +totalAmount, option }); + try { + await Promise.all([ + user.decrease('profit', +totalAmount), + ads.save() + ]); + res.json({ + code: 0, + msg: 'success', + data: ads + }); + } catch (error) { + mylog.error(error); + res.json({ + code: 3, + error: JSON.stringify(error) + }); + } + + }, + revokeAd: async function (req, res) { + const { adId } = req.body; + const userId = res.locals.user ? res.locals.user.userId : ''; + const [ad, user] = await Promise.all([ + OtcAds.findById(adId).exec(), + User.findById(userId).exec() + ]); + if (!ad || !user) return res.json({ + code: 1, + msg: 'unknow user or advertisement' + }); + const restAmount = +(ad.restAmount); + await ad.updateOne({ status: 'canceled' }).exec(); + if (ad.type === 'sell') { + // 卖单回收冻结的币,而且只能回到profit + const session = await mongoose.startSession(); + await session.startTransaction(); + const option = { session }; + try { + await user.increase('profit', restAmount, option); + await session.commitTransaction(); + session.endSession(); + } catch (error) { + await session.abortTransaction(); + session.endSession(); + return res.json({ + code: 500, + msg: '撤销出错,用户余额未能恢复' + }); + } + } + return res.json({ + code: 0, + msg: 'advertisement revoked' + }); + }, + getAdList: async function (req, res) { + const { pageSize, pageIndex, type, status } = req.query; + OtcAds.find({ type, status }, 'type totalAmount restAmount status orders').limit(+pageSize).skip((+pageIndex - 1) * +pageSize).then(data => { + res.json({ + code: 0, + data + }); + }).catch(error => { + res.json({ + code: 500, + error + }); + }); + }, + getAdDetail: async function (req, res) { + const { adId } = req.query; + OtcAds.findById(adId, 'type totalAmount restAmount status orders').then(data => { + res.json({ + code: 0, + data + }); + }).catch(error => { + res.json({ + code: 500, + error + }); + }); + } +}; \ No newline at end of file diff --git a/src/modules/otcAds/otcAds.model.js b/src/modules/otcAds/otcAds.model.js new file mode 100644 index 0000000..25d5008 --- /dev/null +++ b/src/modules/otcAds/otcAds.model.js @@ -0,0 +1,55 @@ +'use strict'; + +const mongoose = require('mongoose'); +const OtcAdsSchema = new mongoose.Schema({ + userId: { + // 所属用户的objId + type: String, + required: true + }, + type: { + // 订单类型, 买单'buy' 卖单'sell' + type: String, + required: true, + enum: ['buy', 'sell'] + }, + totalAmount: { + //挂单总额 + type: Number, + required: true + }, + restAmount: { + //当前剩余额度 + type: Number, + required: true + }, + status: { + // 订单状态 + /** + * available: 挂单中 + * locked: 锁定 + * finished: 结束 + * canceled: 中止/取消 + */ + type: String, + default: 'available', + enum: ['available', 'locked', 'finished', 'canceled'] + }, + orders: { + // 涉及到的所有订单详情/快照 + type: {}, + default: {} + }, + option: { + type: {} + } +}, { + timestamps: { + createdAt: 'createdAt', + updatedAt: 'updatedAt' + } +}); + +const otcAdsSchema = mongoose.model('otcads', OtcAdsSchema); + +module.exports = otcAdsSchema; \ No newline at end of file diff --git a/src/modules/otcAds/otcAds.service.js b/src/modules/otcAds/otcAds.service.js new file mode 100644 index 0000000..ae78d32 --- /dev/null +++ b/src/modules/otcAds/otcAds.service.js @@ -0,0 +1,30 @@ +const indexRoute = '/otcads'; +const controller = require('./otcAds.controller'); +const services = [ + { + url: '/publish', + method: 'POST', + controller: controller.publishAd + }, + { + url: '/revoke', + method: 'POST', + controller: controller.revokeAd + }, + { + url: '/getAdList', + method: 'GET', + controller: controller.getAdList + }, + { + url: '/getAdDetail', + method: 'GET', + controller: controller.getAdDetail + } +]; + +module.exports = services.map(service => { + // 可以在此处对要导出的服务map进行统一处理 + service.url = indexRoute + service.url; + return service; +}); \ No newline at end of file diff --git a/src/modules/otcOrder/otcOrder.controller.js b/src/modules/otcOrder/otcOrder.controller.js new file mode 100644 index 0000000..4ec7ec7 --- /dev/null +++ b/src/modules/otcOrder/otcOrder.controller.js @@ -0,0 +1,87 @@ +'use strict'; + +const otcOrder = require('./otcOrder.model'); +const { buy, sell } = require('./otcOrder.handler'); + +module.exports = { + getOtcOrderList (req, res) { + const { userId } = res.locals.user; + const { pageSize, pageIndex, type } = req.query; + otcOrder.find({ type, userId }).skip((+pageIndex - 1) * +pageSize).limit(+pageSize) + .then(list => { + res.json({ + code: 0, + msg: '', + data: list + }); + }) + .catch(err => { + res.json({ + code: 1, + msg: err + }); + }); + }, + getOtcOrder (req, res) { + const { userId } = res.locals.user; + const option = req.query; + otcOrder.find({ userId, ...option }, 'detail') + .then(list => { + res.json({ + code: 0, + msg: '', + data: list + }); + }) + .catch(err => { + res.json({ + code: 1, + msg: err + }); + }); + }, + async buy (req, res) { + let { adId, amount } = req.body; + amount = +amount; + const { userId } = res.locals.user; + if (!userId || !adId || !Number.isFinite(amount)) return res.json({ + code: 1, + msg: 'invalid parameers' + }); + const exec = await buy(userId, adId, amount); + if (exec && exec.res) { + return res.json({ + code: 0, + msg: exec.msg, + data: exec.res + }); + } else { + return res.json({ + code: exec.code, + msg: exec.msg, + }); + } + }, + async sell (req, res) { + let { adId, amount } = req.body; + amount = +amount; + const { userId } = res.locals.user; + if (!userId || !adId || !Number.isFinite(amount)) return res.json({ + code: 1, + msg: 'invalid parameers' + }); + const exec = await sell(userId, adId, amount); + if (exec && exec.res) { + return res.json({ + code: 0, + msg: exec.msg, + data: exec.res + }); + } else { + return res.json({ + code: exec.code, + msg: exec.msg, + }); + } + } +}; \ No newline at end of file diff --git a/src/modules/otcOrder/otcOrder.handler.js b/src/modules/otcOrder/otcOrder.handler.js new file mode 100644 index 0000000..50a642b --- /dev/null +++ b/src/modules/otcOrder/otcOrder.handler.js @@ -0,0 +1,100 @@ +'use strict'; +const mongoose = require('mongoose'); +const User = require('../user/user.model'); +const OtcAds = require('../otcAds/otcAds.model'); +const OtcOrder = require('../otcOrder/otcOrder.model'); +const { createAction } = require('../action/action.handler'); +/** + + * 1. 生成对应的action + * 2. 生成订单 + * 3. 冻结卖家资金 + * 4. + * + * @param {String} actorId + * @param {String} adId + * @param {Number} amount + * @returns + */ +async function otcBuy (actorId, adId, amount) { + try { + const actor = await User.findById(actorId).exec(); + if (!actor) return { res: null, code: 1, msg: 'unknow user' }; + const adv = await OtcAds.findById(adId).exec(); + const order = new OtcOrder({ }); + const action = createAction(4, { actor: actorId, adv, }); + let orderSession = await mongoose.startSession(); + let actorSession = await mongoose.startSession(); + orderSession.startTransaction(); + actorSession.startTransaction(); + const actorOpts = { session: actorSession, returnOriginal: false }; + const orderOpts = { session: orderSession, returnOriginal: false }; + try { + await action.save(); + await orderSession.commitTransaction(); + await actorSession.commitTransaction(); + return { res : { }, code: 0, msg: 'success' }; + } catch (error) { + await orderSession.abortTransaction(); + await actorSession.abortTransaction(); + return { res: null, code: 3, msg: 'Insufficient funds' }; + } finally { + orderSession.endSession(); + actorSession.endSession(); + } + } catch (error) { + return { res: null, code: 500, msg: JSON.stringify(error) }; + } +} + +async function otcSell (actorId, adId, amount) { + try { + const actor = await User.findById(actorId).exec(); + if (!actor) return { res: null, code: 1, msg: 'unknow user' }; + const adv = await OtcAds.findById(adId).exec(); + const order = new OtcOrder({ }); + const action = createAction(5, { actor: actorId, adv, }); + let orderSession = await mongoose.startSession(); + let actorSession = await mongoose.startSession(); + orderSession.startTransaction(); + actorSession.startTransaction(); + const actorOpts = { session: actorSession, returnOriginal: false }; + const orderOpts = { session: orderSession, returnOriginal: false }; + try { + await action.save(); + await orderSession.commitTransaction(); + await actorSession.commitTransaction(); + return { res : { }, code: 0, msg: 'success' }; + } catch (error) { + await action.remove(); + await orderSession.abortTransaction(); + await actorSession.abortTransaction(); + return { res: null, code: 3, msg: 'Insufficient funds' }; + } finally { + orderSession.endSession(); + actorSession.endSession(); + } + } catch (error) { + return { res: null, code: 500, msg: JSON.stringify(error) }; + } +} + + +module.exports = { + otcBuy, + otcSell +}; + +/** + * 卖NEW的卖家广告 --- 场外买家 + * 1.在广告页面,输入想要购买的数量amount,选择支付方式,选择支付币种,点击购买,发送请求。 + * 2. + * 2.1 后台生成订单otcOrder,包含上面的参数。 + * 2.2 冻结卖家广告中的amount数量的资金。 + * 2.3 分别通知广告主和买家 + * 2.4 买家付款,点击我已付款按钮,订单状态变为 买家已付款 + * 2.5 卖家收到款,点击确认收款,订单状态变为,卖家已放币。 + * 2.6 订单状态变更为finished + * 2.7 广告状态变更,若该广告涉及金额已空,则改为finished,否则回到available + * 3. + */ \ No newline at end of file diff --git a/src/modules/otcOrder/otcOrder.model.js b/src/modules/otcOrder/otcOrder.model.js new file mode 100644 index 0000000..f8b1644 --- /dev/null +++ b/src/modules/otcOrder/otcOrder.model.js @@ -0,0 +1,56 @@ +'use strict'; + +const mongoose = require('mongoose'); +const OtcOrderSchema = new mongoose.Schema({ + type: { + // 订单类型,买单[buy]or卖单[sell] + type: String, + required: true + }, + adId: { + // 对应广告id + type: String, + required: true + }, + from: { + // 转账发起人,卖家 + type: String, + }, + to: { + // 收款人, 买家 + type: String, + }, + coinType: { + type: String, + }, + payMethod: { + type: String, + enum : ['alipay', 'wechatpay', 'bankcard'], + }, + amount: { + type: Number, + required: true + }, + status: { + // 订单状态 + /* + * hanging: 有效 + * locked: 锁定 + * finished: 结束 + */ + type: String, + enum : ['alipay', 'wechatpay', 'bankcard'], + }, + option: { + type: {} + } +}, { + timestamps: { + createdAt: 'createdAt', + updatedAt: 'updatedAt' + } +}); + +const otcOrderSchema = mongoose.model('otcOrder', OtcOrderSchema); + +module.exports = otcOrderSchema; \ No newline at end of file diff --git a/src/modules/otcOrder/otcOrder.service.js b/src/modules/otcOrder/otcOrder.service.js new file mode 100644 index 0000000..30d740e --- /dev/null +++ b/src/modules/otcOrder/otcOrder.service.js @@ -0,0 +1,30 @@ +const indexRoute = '/otcorder'; +const controller = require('./otcOrder.controller'); +const services = [ + { + url: '/list', + method: 'GET', + controller: controller.getOtcOrderList + }, + { + url: '/detail', + method: 'GET', + controller: controller.getOtcOrder + }, + { + url: '/buy', + method: 'POST', + controller: controller.buy + }, + { + url: '/sell', + method: 'POST', + controller: controller.sell + }, +]; + +module.exports = services.map(service => { + // 可以在此处对要导出的服务map进行统一处理 + service.url = indexRoute + service.url; + return service; +}); \ No newline at end of file diff --git a/src/modules/sign/sign.controller.js b/src/modules/sign/sign.controller.js new file mode 100644 index 0000000..e0391d4 --- /dev/null +++ b/src/modules/sign/sign.controller.js @@ -0,0 +1,204 @@ +const Crypto = require('../../utils/crypto'); +const { createToken } = require('../../utils/jwt'); +const { inject } = require('../../common/Provider'); +const User = require('../user/user.model'); +const Code = require('../msgCode/msgCode.controller'); +const { createNewUser } = require('../user/user.controller'); +const CODE_TYPE = Code.CODE_TYPE; + +const msgCodeSignIn = async function (phone, code) { + const validCode = await Code.verifyCode({ id: phone, code, type: CODE_TYPE['msgSignIn'] }); + return validCode ? await User.findOne({ + phone, + }).exec() : null; +}; + +module.exports = { + checkExist: async function (req, res) { + // method GET + const { phone }= req.query; + const isExist = await User.exists({ phone }); + if (isExist) res.json({ + code: 0, + msg: '' + }); + else res.json({ + code: 1, + msg: '' + }); + }, + signIn: inject(function(SysConfig) { + const { JWT_SECRET, PWD_HASH_SALT } = SysConfig; + return async (req, res) => { + let { phone, password, code } = req.body; + let user; + if (code) { + user = await msgCodeSignIn(phone, code); + } else { + password = Crypto.hash(password, { salt: PWD_HASH_SALT }); + user = await User.findOne({ + phone, + password + }).exec(); + } + if (user) { + const token = createToken({ + userId: user.id + }, JWT_SECRET, {}); + res.json({ + code: 0, + msg: 'success', + data: { + id: user.id, + nickname: user.nickname, + phone: user.phone, + email: user.email, + token + } + }); + } else { + res.json({ + code: 1, + error: 'unknow user' + }); + } + }; + }), + signUp: inject(function(SysConfig) { + const { JWT_SECRET, PWD_HASH_SALT } = SysConfig; + return async (req, res) => { + let { phone, code, inviter, password } = req.body; + if (!phone || !code || !password) return res.json({ + code: 1, + msg: 'Missing required parameters' + }); + const isCodeValid = await Code.verifyCode({ id: phone, code, type: CODE_TYPE['signUp'] }); + password = Crypto.hash(password, { salt: PWD_HASH_SALT }); + if (isCodeValid) { + createNewUser({ + phone, + inviter, + password + }).then(user => { + const token = createToken({ + userId: user.id + }, JWT_SECRET, {}); + res.json({ + code: 0, + msg: 'success', + data: { + //刚注册,需要返回的应该只有token,其他的没意义 + token + } + }); + }) + .catch(err => { + res.json({ + code: 3, + msg: err, + }); + }); + } else { + res.json({ + code: 2, + msg: 'Invalid code', + }); + } + }; + }), + forgetPasswd: inject(function(SysConfig) { + const { PWD_HASH_SALT } = SysConfig; + return async (req, res) => { + const { phone, password, code } = req.body; + const validCode = await Code.verifyCode({ + id: phone, + code, + type: CODE_TYPE['forgetPasswd'] + }); + if (!validCode) { + res.json({ + code: 1, + msg: 'Incorrect code' + }); + } + const newPasswd = Crypto.hash(password, PWD_HASH_SALT); + const query = { phone }; + const update = { password: newPasswd }; + // eslint-disable-next-line no-unused-vars + const success = await User.findOneAndUpdate(query, update, { new: true }).exec().catch(err => null); + if (success) { + res.json({ + code: 0, + msg: 'Reseted passwd' + }); + } else { + res.json({ + code: 500, + msg: 'unknow error' + }); + } + }; + }), + changePasswd: inject(function (SysConfig) { + const { PWD_HASH_SALT } = SysConfig; + return async (req, res) => { + const { phone, password, code } = req.body; + const validCode = await Code.verifyCode({ + id: phone, + code, + type: CODE_TYPE['changePasswd'] + }); + if (!validCode) { + res.json({ + code: 1, + msg: 'Incorrect code' + }); + } + const newPasswd = Crypto.hash(password, PWD_HASH_SALT); + const query = { phone }; + const update = { password: newPasswd }; + // eslint-disable-next-line no-unused-vars + const success = await User.findOneAndUpdate(query, update, { new: true }).exec().catch(err => null); + if (success) { + res.json({ + code: 0, + msg: 'Reseted passwd' + }); + } else { + res.json({ + code: 500, + msg: 'unknow error' + }); + } + }; + }), + sendMsgCode: async (req, res) => { + // 可作为通用的发送验证码接口 + let { phone, type } = req.body; + // 注意此处的type是字面值不是数值,应该是例如'signIn'等 + const exec = await Code.sendMsgCode(phone, type); + if (exec && exec.code === 0) { + res.json({ + code: 0, + msg: 'Code sended' + }); + } else { + res.json({ + code: exec.code ? exec.code : 1, + error: exec.error ? exec.error : 'failed' + }); + } + }, + getLocaleSupport: async (req, res) => { + const System = global.Provider.getModule('System'); + const locale = await System.getValue('locale'); + if (!locale) return res.json({ + code: 1, + error: "does't exists" + }); + return res.json({ + code: 0, + data: locale + }); + } +}; \ No newline at end of file diff --git a/src/modules/sign/sign.service.js b/src/modules/sign/sign.service.js new file mode 100644 index 0000000..13a4eca --- /dev/null +++ b/src/modules/sign/sign.service.js @@ -0,0 +1,40 @@ +const indexRoute = '/sign'; +const controller = require('./sign.controller'); +const services = [ + { + url: '/check', + method: 'GET', + controller: controller.checkExist + }, + { + url: '/in', + method: 'POST', + controller: controller.signIn + }, + { + url: '/up', + method: 'POST', + controller: controller.signUp + }, + { + url: '/forget', + method: 'POST', + controller: controller.forgetPasswd + }, + { + url: '/send', + method: 'POST', + controller: controller.sendMsgCode + }, + { + url: '/code', + method: 'GET', + controller: controller.getLocaleSupport + }, +]; + +module.exports = services.map(service => { + // 可以在此处对要导出的服务map进行统一处理 + service.url = indexRoute + service.url; + return service; +}); \ No newline at end of file diff --git a/src/modules/sysBoard/sysBoard.controller.js b/src/modules/sysBoard/sysBoard.controller.js new file mode 100644 index 0000000..ef7c24e --- /dev/null +++ b/src/modules/sysBoard/sysBoard.controller.js @@ -0,0 +1,81 @@ +"use strict"; + +const Board = require('./sysBoard.model'); + +module.exports = { + async publish (req, res) { + const { data = {} } = req.body; + new Board(data).save().then(anno => { + return res.json({ + code: 0, + data: anno + }); + }).catch(err => { + mylog.error(err); + return res.json({ + code: 1, + error: JSON.stringify(err) + }); + }); + }, + async getList (req, res) { + const { config } = req.query; + return Board.find(config, 'id title short url option createdAt').then(data => { + return res.json({ + code: 0, + data + }); + }).catch(err => { + mylog.error(err); + return res.json({ + code: 1, + error: JSON.stringify(err) + }); + }); + }, + async getOne (req, res) { + const { id } = req.query; + return Board.findById(id, 'id title url option body author').then(data => { + return res.json({ + code: 0, + data + }).catch(err => { + mylog.error(err); + return res.json({ + code: 1, + error: JSON.stringify(err) + }); + }); + }); + }, + async updateOne (req, res) { + const { id, data } = req.body; + return Board.findByIdAndUpdate(id, data).then(data => { + return res.json({ + code: 0, + data + }).catch(err => { + mylog.error(err); + return res.json({ + code: 1, + error: JSON.stringify(err) + }); + }); + }); + }, + async deleteOne (req, res) { + const { id, data } = req.body; + return Board.findByIdAndDelete(id).then(data => { + return res.json({ + code: 0, + data + }).catch(err => { + mylog.error(err); + return res.json({ + code: 1, + error: JSON.stringify(err) + }); + }); + }); + }, +}; \ No newline at end of file diff --git a/src/modules/sysBoard/sysBoard.model.js b/src/modules/sysBoard/sysBoard.model.js new file mode 100644 index 0000000..f94d5a8 --- /dev/null +++ b/src/modules/sysBoard/sysBoard.model.js @@ -0,0 +1,31 @@ +'use strict'; + +const mongoose = require('mongoose'); + +const SysBoardSchema = new mongoose.Schema({ + title: { + type: String, + required: true, + }, + author: { + type: String, + }, + short: { + type: String, + }, + url: { + type: String, + }, + body: { + type: {} + }, + option: { + type: {} + } +},{ + timestamps: {} +}); + +const SysBoardModel = mongoose.model('SysBoard', SysBoardSchema); + +module.exports = SysBoardModel; \ No newline at end of file diff --git a/src/modules/sysBoard/sysBoard.service.js b/src/modules/sysBoard/sysBoard.service.js new file mode 100644 index 0000000..fb5fbfb --- /dev/null +++ b/src/modules/sysBoard/sysBoard.service.js @@ -0,0 +1,37 @@ +"use strict"; + +const indexRoute = '/sysboard'; +const controller = require('./sysBoard.controller'); +const services = [ + { + url: '/publish', + method: 'POST', + controller: controller.publish + }, + { + url: '/update', + method: 'POST', + controller: controller.updateOne + }, + { + url: '/list', + method: 'GET', + controller: controller.getList + }, + { + url: '/detail', + method: 'GET', + controller: controller.getOne + }, + { + url: '/delete', + method: 'POST', + controller: controller.deleteOne + }, +]; + +module.exports = services.map(service => { +// 可以在此处对要导出的服务map进行统一处理 + service.url = indexRoute + service.url; + return service; +}); \ No newline at end of file diff --git a/src/modules/system/system.controller.js b/src/modules/system/system.controller.js new file mode 100644 index 0000000..6dcc7c0 --- /dev/null +++ b/src/modules/system/system.controller.js @@ -0,0 +1,90 @@ +const System = require('./system.model'); +const _cache = {}; +const provider = { + getValue: async function (key) { + if (!_cache[key]) { + await System.findOne({ key }).then(data => { + _cache[key] = data.value; + }).catch(() => { + mylog.error('unexpected key', key); + return null; + }); + } + return _cache[key]; + } +}; +module.exports = { + provider, + getConfig: function (req, res) { + const { key } = req.query; + if (!_cache[key]) { + System.findOne({ key }).then(data => { + _cache[key] = data.value; + res.json({ + code: 0, + data + }); + }).catch(err => { + res.json({ + code: 1, + err + }); + }); + } else res.json({ + code: 0, + data: _cache[key] + }); + }, + publishConfig: async function (req, res) { + const { key, value, option } = req.body; + const isExist = await System.exists({ key }); + if (!isExist) { + new System({ key, value, option }).save() + .then(data => { + _cache[key] = data.value; + res.json({ + code: 0, + msg: 'success', + data + }); + }) + .catch(err => { + res.json({ + code: 2, + err + }); + }); + } else { + res.json({ + code: 1, + msg: 'key already existed' + }); + } + }, + updateConfig: async function (req, res) { + const { key, value, option } = req.body; + const isExist = await System.exists({ key }); + if (isExist) { + System.findOneAndUpdate({ key }, { value, option }, { new: true }) + .then(data => { + _cache[key] = data.value; + res.json({ + code: 0, + msg: 'success', + data + }); + }) + .catch(err => { + res.json({ + code: 2, + err + }); + }); + } else { + res.json({ + code: 1, + err: "key doesn't existed" + }); + } + } +}; \ No newline at end of file diff --git a/src/modules/system/system.model.js b/src/modules/system/system.model.js new file mode 100644 index 0000000..4be1738 --- /dev/null +++ b/src/modules/system/system.model.js @@ -0,0 +1,26 @@ +'use strict'; + +const mongoose = require('mongoose'); +const SystemSchema = new mongoose.Schema({ + key: { + type: String, + required: true, + unique: true + }, + value: { + type: {}, + required: true + }, + option: { + type: {} + }, +}, { + timestamps: { + createdAt: 'createdAt', + updatedAt: 'updatedAt' + } +}); + +const systemSchema = mongoose.model('System', SystemSchema); + +module.exports = systemSchema; \ No newline at end of file diff --git a/src/modules/system/system.service.js b/src/modules/system/system.service.js new file mode 100644 index 0000000..2ac0ad8 --- /dev/null +++ b/src/modules/system/system.service.js @@ -0,0 +1,25 @@ +const indexRoute = '/system'; +const controller = require('./system.controller'); +const services = [ + { + url: '/get', + method: 'GET', + controller: controller.getConfig + }, + { + url: '/publish', + method: 'POST', + controller: controller.publishConfig + }, + { + url: '/update', + method: 'POST', + controller: controller.updateConfig + }, +]; + +module.exports = services.map(service => { + // 可以在此处对要导出的服务map进行统一处理 + service.url = indexRoute + service.url; + return service; +}); \ No newline at end of file diff --git a/src/modules/ticket/ticket.controller.js b/src/modules/ticket/ticket.controller.js new file mode 100644 index 0000000..c55ef0d --- /dev/null +++ b/src/modules/ticket/ticket.controller.js @@ -0,0 +1,58 @@ +'use strict'; + +const Ticket = require('./ticket.model'); +const handleTicket = require('./ticket.handler'); + +module.exports = { + async reqHandleTicket (req, res) { + const { handler } = res.local.handler; + const { id } = req.body; + const ticket = await Ticket.findById(id).exec().catch(err => { + mylog.error(err); + return null; + }); + if (!ticket) return res.json({ + code: 1, + msg: 'unknow ticket' + }); + handleTicket(ticket, handler).then(data => { + return res.json({ + code: 0, + msg: 'success' + }); + }).catch(err => { + return res.json({ + code: 1, + error: JSON.stringify(err) + }); + }); + }, + async getTicketList (req, res) { + const { pageSize, pageIndex } = req.query; + return Ticket.find().skip((pageIndex - 1) * pageSize).limit(pageSize).then(data => { + return res.json({ + code: 0, + data + }); + }).catch(err => { + return res.json({ + code: 1, + error: JSON.stringify(err) + }); + }); + }, + async getTicketDetail (req, res) { + const { ticketId } = req.query; + return Ticket.findById(ticketId).then(data => { + return res.json({ + code: 0, + data + }); + }).catch(err => { + return res.json({ + code: 1, + error: JSON.stringify(err) + }); + }); + } +}; \ No newline at end of file diff --git a/src/modules/ticket/ticket.handler.js b/src/modules/ticket/ticket.handler.js new file mode 100644 index 0000000..586c543 --- /dev/null +++ b/src/modules/ticket/ticket.handler.js @@ -0,0 +1,43 @@ +'use strict'; +const Ticket = require('./ticket.model'); +const User = require('../user/user.model'); +const name2type = { + withdraw: 0 +}; +async function handleTicket (ticket, handler) { + // 所有工单在此处理,只要状态未完结(status !== 'finished'),就可以多次调用改变状态。 + const { userId, status } = ticket; + const user = await User.findById(userId).exec(); + if (!user) return null; + ticket = await ticket.next(handler); + if (status === 'processing') { + await dealTicket(ticket, user); + await ticket.next(handler); + return ticket; + } + return null; +} +async function dealTicket (ticket, user) { + if (ticket.type === 0) { + // 提现 + dealWithdraw(ticket, user); + } +} + +async function dealWithdraw (ticket, user) { + +} + +function createTicket (name, data) { + const type = name2type[name]; + const status = 'created'; + const { userId, handler, action, option } = data; + return new Ticket({ + status, type, userId, handler, action, option + }); +} + +module.exports = { + createTicket, + handleTicket +}; \ No newline at end of file diff --git a/src/modules/ticket/ticket.model.js b/src/modules/ticket/ticket.model.js new file mode 100644 index 0000000..7ccdf01 --- /dev/null +++ b/src/modules/ticket/ticket.model.js @@ -0,0 +1,58 @@ +'use strict'; + +const mongoose = require('mongoose'); +// 用户提现专用工单!!! +const TicketSchema = new mongoose.Schema({ + userId: { + // 订单所属用户的objId + type: String, + }, + type: { + // 订单类型 + type: String, + }, + amount: { + type: Number + }, + status: { + type: String + }, + action: { + type: {} + }, + handler: { + type: String + }, + option: { + type: {} + } +},{ + timestamps: {} +}); + +const TicketModel = mongoose.model('Ticket', TicketSchema); +/** + * status: created -> processing -> finished + */ +TicketModel.prototype.next = async function (handler = '') { + // handler 表示处理人 + if (this.status === 'finished') return this; + if (this.type === 0) { + // deposite + if (this.status === 'created') { + await this.update({ + status: 'processing', + handler + }); + } + if (this.status === 'processing') { + await this.update({ + status: 'finished', + handler + }); + } + } + return this; +}; + +module.exports = TicketModel; \ No newline at end of file diff --git a/src/modules/ticket/ticket.service.js b/src/modules/ticket/ticket.service.js new file mode 100644 index 0000000..754dbbf --- /dev/null +++ b/src/modules/ticket/ticket.service.js @@ -0,0 +1,27 @@ +'use strict'; + +const indexRoute = '/ticket'; +const controller = require('./ticket.controller'); +const services = [ + { + url: '/handle', + method: 'POST', + controller: controller.reqHandleTicket + }, + { + url: '/list', + method: 'GET', + controller: controller.getTicketList + }, + { + url: '/detail', + method: 'GET', + controller: controller.getTicketDetail + }, +]; + +module.exports = services.map(service => { + // 可以在此处对要导出的服务map进行统一处理 + service.url = indexRoute + service.url; + return service; +}); \ No newline at end of file diff --git a/src/modules/user/user.controller.js b/src/modules/user/user.controller.js new file mode 100644 index 0000000..25df5e0 --- /dev/null +++ b/src/modules/user/user.controller.js @@ -0,0 +1,71 @@ +const User = require('./user.model'); +const MsgCode = require('../msgCode/msgCode.controller'); +const changePwd = require('../sign/sign.controller').changePasswd; +const mongoose = require('mongoose'); + +module.exports = { + changePwd, + async createNewUser (data) { + let { phone, inviter, password } = data; + if (inviter) inviter = new mongoose.Types.ObjectId(inviter); + await (new User({ phone, inviter, password })).save().then(user => user).catch(err => { + mylog.error('创建新用户失败', err); + return null; + }); + }, + async getUserInfo (req, res) { + const { userId } = res.locals.user; + User.findById(userId, 'id nickname phone').then(data => { + res.json({ + code: 0, + msg: 'success', + data + }); + }).catch(err => { + res.json({ + code: 1, + error: JSON.stringify(err) + }); + }); + }, + async changePhone (req, res) { + const { userId } = res.locals.user; + const { newPhone, code } = req.body; + if (!newPhone || !code) return res.json({ + code: 1, + error: 'Missing required param' + }); + const isValidCode = MsgCode.verifyCode({ id: userId, code, type: 2 }); + if (!isValidCode) return res.json({ + code: 2, + error: 'Incorrect Code' + }); + return User.findByIdAndUpdate(userId, { + phone: newPhone + }).then(data => { + res.json({ + code: 0, + data + }); + }).catch(err => res.json({ + code: 500, + error: JSON.stringify(err) + })); + }, + async getGroup (req, res) { + const { userId } = res.locals.user; + User.find({ inviter: userId }).then(data => { + res.json({ + code: 0, + msg: 'success', + data + }); + }).catch(err => { + res.json({ + code: 1, + msg: 'failed', + error: JSON.stringify(err) + }); + }); + } +}; \ No newline at end of file diff --git a/src/modules/user/user.model.js b/src/modules/user/user.model.js new file mode 100644 index 0000000..67eee32 --- /dev/null +++ b/src/modules/user/user.model.js @@ -0,0 +1,88 @@ +'use strict'; + +const mongoose = require('mongoose'); +const ASSETTYPES = ['log', 'profit', 'usdtBTC', 'usdtETH', 'usdtTRX']; + +const UserSchema = new mongoose.Schema({ + phone: { + type: String, + required: true, + unique: true + }, + password: { + type: String, + required: true + }, + nickname: { + type: String, + required: false + }, + inviter: { + type: String, + }, + gender: { + type: String + }, + asset: { + type: {}, + default: { + log: 0, //充值的log + profit: 0 // 买卖地皮赚到的 + } + }, + tokenAddress: { + type: {}, + default: { + 'usdtBTC': '', + 'usdtETH': '', + 'usdtTRX': '' + } + }, + avatar: { + type: String + }, + option: { + type: {} + }, + group: [{ + type: mongoose.Schema.Types.ObjectId, + ref: 'User' + }] +}, { + timestamps: { + createdAt: 'createdAt', + updatedAt: 'updatedAt' + } +}); + + +const UserModel = mongoose.model('User', UserSchema); + +UserModel.prototype.increase = async function (assetName, amount, option = {}) { + if (!assetName || !amount || amount <= 0 || !Number.isFinite(amount) || !ASSETTYPES.includes(assetName)) return null; + const asset = 'asset.' + assetName; + const res = await UserModel.findByIdAndUpdate(this.id, { + $inc: { + [asset]: amount + } + }, { ...option, returnOriginal: false }).exec().catch(err => null); + if (!res || !res.asset) throw new Error('用户资产修改[increase]失败!'); + if (res.asset[assetName] < 0) throw new Error('Insufficient funds'); + return res; +}; +UserModel.prototype.decrease = async function (assetName, amount, option) { + // 传入的amount应该是大于0的正数 + if (!assetName || !amount || amount <= 0 || !Number.isFinite(amount) || !ASSETTYPES.includes(assetName)) return null; + amount = 0 - amount; + const asset = 'asset.' + assetName; + const res = await UserModel.findByIdAndUpdate(this.id, { + $inc: { + [asset]: amount + } + }, { ...option, returnOriginal: false }).exec().catch(err => null); + if (!res || !res.asset) throw new Error('用户资产修改[decrease]失败!'); + if (res.asset[assetName] < 0) throw new Error('Insufficient funds'); + return res; +}; + +module.exports = UserModel; \ No newline at end of file diff --git a/src/modules/user/user.service.js b/src/modules/user/user.service.js new file mode 100644 index 0000000..6434563 --- /dev/null +++ b/src/modules/user/user.service.js @@ -0,0 +1,30 @@ +const indexRoute = '/user'; +const controller = require('./user.controller'); +const services = [ + { + url: '/info', + method: 'GET', + controller: controller.getUserInfo + }, + { + url: '/chpasswd', + method: 'POST', + controller: controller.changePwd + }, + { + url: '/chphone', + method: 'POST', + controller: controller.changePhone + }, + { + url: '/group', + method: 'GET', + controller: controller.getGroup + }, +]; + +module.exports = services.map(service => { + // 可以在此处对要导出的服务map进行统一处理 + service.url = indexRoute + service.url; + return service; +}); \ No newline at end of file diff --git a/src/routes/index.js b/src/routes/index.js new file mode 100644 index 0000000..0f0c3b7 --- /dev/null +++ b/src/routes/index.js @@ -0,0 +1,12 @@ +'use strict'; +const express = require('express'); +const ServiceManager = require('../service'); +const versionList = ['v1']; + +module.exports = app => { + versionList.map(version => { + const path = '/' + version.replace('/', ''); + app.use(path, ServiceManager.mountService(express.Router(), version)); + }); + return app; +}; diff --git a/src/server.js b/src/server.js new file mode 100644 index 0000000..669932f --- /dev/null +++ b/src/server.js @@ -0,0 +1,71 @@ +'use strict'; +/** + 1.导入express组件app + 2.创建数据库连接 + 3.注册路由 + 4.启动node服务 + */ + +const fs = require('fs'); +const path = require('path'); +const app = require('./app.js'); +const Socket = require('socket.io'); +const inject = require('./common/Provider').inject; +// const cluster = require('cluster') + +module.exports = () => inject(function runServer (SysConfig) { // 配置并启动 Web 服务 + mylog.info('★★★★★★★★ Starting Server ★★★★★★★★'); + + const greenlock = SysConfig && SysConfig.sslType==='greenlock' ? require('greenlock-express').create({ + version: 'draft-11', + server: SysConfig.netType==='devnet' // for test: acme-staging-v02 + ? 'https://acme-staging-v02.api.letsencrypt.org/directory' + : 'https://acme-v02.api.letsencrypt.org/directory', + agreeTos: true, + communityMember: false, + store: require('greenlock-store-fs'), + email: 'ssl@faronear.org', + approvedDomains: SysConfig.sslDomainList, + configDir: path.resolve(__dirname, 'ssl'), + app, + }) : null; + + /** * 启动 Web 服务 ***/ + let webServer; + if (SysConfig.protocol === 'http') { + webServer = require('http').createServer(app).listen(SysConfig.port, function (err) { + if (err) mylog.info(err); + else mylog.info(`Server listening on ${SysConfig.protocol}://${SysConfig.host}:${SysConfig.port} with for ${app.settings.env} environment`); + }); + } else if (SysConfig.protocol === 'https') { + webServer = require('https').createServer(SysConfig.sslType === 'greenlock' ? greenlock.httpsOptions : { + key: fs.readFileSync(SysConfig.sslKey), + cert: fs.readFileSync(SysConfig.sslCert), + }, app).listen(SysConfig.port, function (err) { + if (err) mylog.error(err); + else console.log(`Server listening on ${SysConfig.protocol}://${SysConfig.host}:${SysConfig.port} for ${app.settings.env} environment`); + }); + } else if ('httpall' === SysConfig.protocol) { + let portHttp = SysConfig.port ? SysConfig.port : 80; + // let portHttps = (SysConfig.port && SysConfig.port !== 80) ? SysConfig.port + 443 : 443 + let portHttps = 443; + if (SysConfig.sslType === 'greenlock') { + webServer = greenlock.listen(portHttp, portHttps, function (err) { + if (err) console.log(err); + else console.log(`Server listening on [${SysConfig.protocol}] http=>https://${SysConfig.host}:${portHttp}=>${portHttps} for ${app.settings.env} environment`); + }); + } else { + webServer = require('https').createServer({ + key: fs.readFileSync(SysConfig.sslKey), + cert: fs.readFileSync(SysConfig.sslCert), + // ca: [ fs.readFileSync(SysConfig.sslCA) ] // only for self-signed certificate: https://nodejs.org/api/tls.html#tls_tls_createserver_options_secureconnectionlistener + }, app).listen(portHttps, function (err) { + if (err) console.log(err); + else console.log(`Server listening on [${SysConfig.protocol}] https://${SysConfig.host}:${portHttps} for ${app.settings.env} environment`); + }); + } + } + + // const socket = Socket(webServer); + return webServer; +}); diff --git a/src/service/index.js b/src/service/index.js new file mode 100644 index 0000000..5275266 --- /dev/null +++ b/src/service/index.js @@ -0,0 +1,53 @@ +const event = require('events'); +const serviceMap = require('./serviceMap'); +const { auth } = require('./middleware'); +// const Provider = require('../common/Provider') +class ServiceManager extends event { + constructor(props = {}) { + mylog.info('初始化服务控制器...'); + super(props); + this._map = serviceMap; + this.middleWares = [ + auth + ]; + } + static getInstance() { + if(!ServiceManager.instance) { + ServiceManager.instance = new ServiceManager(); + } + return ServiceManager.instance; + } + + get ServiceMap() { + return this._map; + } + /** + * + * @desc 注册一个服务 + * @param {*} [service={}] + * @param {String} path // 路由地址 + * @param {String} type // HTTP请求类型['GET','POST','PUT'...] + * @param {Function} handler //处理函数 + * @memberof ServiceManager + */ + regist(service = {}) { + const { version, ...main } = service; + this._map[version].push(main); + } + + mountService(route, version) { + mylog.info('开始挂载中间价...'); + this.middleWares.map(fn => { + route.use(fn); + }); + mylog.info('开始挂载服务...'); + if (this.ServiceMap[version] && this.ServiceMap[version].length !== 0) { + this.ServiceMap[version].map(service => { + route[service.method.toLowerCase()](service.url, service.controller); + }); + } + return route; + } +} + +module.exports = ServiceManager.getInstance(); \ No newline at end of file diff --git a/src/service/middleware.js b/src/service/middleware.js new file mode 100644 index 0000000..4887ac7 --- /dev/null +++ b/src/service/middleware.js @@ -0,0 +1,27 @@ +const { inject } = require('../common/Provider'); +const { verifyToken, createToken } = require('../utils/jwt'); +const ignoreToken = ['/sign/check', '/sign/in', '/sign/up', '/sign/forget', '/sign/send', '/sign/changePwd', '/sign/code']; +module.exports = { + auth: inject(function (SysConfig) { + const { JWT_SECRET } = SysConfig; + return async (req, res, next) => { + const token = req.headers.token; + if(!ignoreToken.includes(req.path)) { + if (token) { + const deToken = await verifyToken(token, JWT_SECRET); + if(deToken) { + res.locals.user = deToken.data; + // 换发新token,存在问题:如果上一个token没过期,其他人截获了的话,会不会被盗用? + res.set('token', createToken({ + userId: deToken.userId + }, JWT_SECRET, {})); + return next(); + } + } + return res.status(403).send('unauthorized'); + } else { + next(); + } + }; + }) +}; \ No newline at end of file diff --git a/src/service/serviceMap.js b/src/service/serviceMap.js new file mode 100644 index 0000000..81af818 --- /dev/null +++ b/src/service/serviceMap.js @@ -0,0 +1,20 @@ +// 根服务map,读取所有模块导出的服务map,按照版本进行切分后提供给ServiceManager +const fs = require('fs'); + +fs.readdirSync('./src/modules').map(m => { + if (fs.existsSync(`./src/modules/${m}/${m}.service.js`)) { + require(`../modules/${m}/${m}.service.js`); + } +}); + +const modules = module.children +.reduce((acc, c) => { + if (c && c.exports && c.exports.length) { + return acc.concat(c.exports); + } + return acc; +}, []).filter(m => m.controller !== undefined); + +module.exports = { + v1: modules +}; \ No newline at end of file diff --git a/src/utils/account.js b/src/utils/account.js new file mode 100644 index 0000000..daaa27d --- /dev/null +++ b/src/utils/account.js @@ -0,0 +1,41 @@ +'use strict'; + +const tools = require('./crypto'); + +const getPath = uid => { + const pre = Date.now() + ''; + let seed = pre + uid; + if (seed.length < 19) seed += (Date.now() + '').slice(seed.length - 19); + const frag1 = seed.slice(0, 6); + const frag2 = seed.slice(6, 12); + const frag3 = seed.slice(-8); + const append = `/${frag1}'/${frag2}/${frag3}`; + return { + BTC: `m/44'/0'${append}`, + ETH: `m/44'/60'${append}` + }; +}; + +module.exports = { + createAccount (type = 'BTC') { + const keypair = tools.randomKeypair(); + return { + type, + ...keypair, + address: tools.pubkey2address(keypair.pubkey, { + coin: type + }) + }; + }, + deriveNewAccount (root, option) { + const { coin, seed } = option; + if (!coin || !seed) throw new Error('Invalid Params'); + const path = getPath(seed)[coin]; + const keypair = tools.secword2keypair(root, { coin, path }); + return { + pubkey: keypair.pubkey, + address: tools.pubkey2address(keypair.pubkey, { coin }), + path + }; + } +}; \ No newline at end of file diff --git a/src/utils/crypto.js b/src/utils/crypto.js new file mode 100644 index 0000000..170f3c9 --- /dev/null +++ b/src/utils/crypto.js @@ -0,0 +1,323 @@ +const crypto = require('crypto'); +const nacl = require('tweetnacl'); +const bs58check = require('bs58check'); +const { keccak256 } = require('js-sha3'); +const Secword = require('bitcore-mnemonic'); // https://bitcore.io/api/mnemonic/ https://github.com/bitpay/bitcore-mnemonic +// const bip39 = require('bip39') // https://github.com/bitcoinjs/bip39 // 有更多语言,但不方便选择语言,也不能使用 pass +// const HDKey = require('hdkey') // https://github.com/cryptocoinjs/hdkey // 或者用 bitcore-mnemonic 或者 ethers 里的相同功能 + +// 全部以hex为默认输入输出格式,方便人的阅读,以及方便函数之间统一接口 + +const my = {}; +my.HASHER = 'sha256'; // 默认的哈希算法。could be md5, sha1, sha256, sha512, ripemd160。 可用 Crypto.getHashes/Ciphers/Curves() 查看支持的种类。 +my.HASHER_LIST = crypto.getHashes(); +my.CIPHER = 'aes-256-cfb'; // 默认的加解密算法 +my.CIPHER_LIST = crypto.getCiphers(); +my.CURVE = 'secp256k1'; // 默认的ECDH曲线,用于把私钥转成公钥。 +my.CURVE_LIST = ['secp256k1']; // crypto.getCurves() 引入到浏览器里后出错,不支持 getCurves. +my.OUTPUT = 'hex'; // 默认的哈希或加密的输入格式 +my.OUTPUT_LIST = ['hex', 'latin1', 'base64']; // or 'buf' to Buffer explicitly +my.INPUT = 'utf8'; // 默认的加密方法的明文格式。utf8 能够兼容 latin1, ascii 的情形 +my.INPUT_LIST = ['utf8', 'ascii', 'latin1']; // ignored for Buffer/TypedArray/DataView +my.COIN = 'BTC'; // 默认的币种 +my.COIN_LIST = ['TIC', 'BTC', 'ETH']; +my.CHAINNET = 'mainnet'; // 默认的链网 + +module.exports = { + hash: function (data, option) { // data can be anything, but converts to string or remains be Buffer/TypedArray/DataView + if (typeof(data) !== 'boolean' && data !== Infinity) { + option = option || {}; + if (typeof (data) !== 'string' && !(data instanceof Buffer) && !(data instanceof DataView)) + data = JSON.stringify(data); + if (option.salt && typeof (option.salt) === 'string') + data = data + this.hash(option.salt); + let hasher = my.HASHER_LIST.indexOf(option.hasher) >= 0 ? option.hasher : my.HASHER; // 默认为 sha256. + let inputEncoding = my.INPUT_LIST.indexOf(option.input) >= 0 ? option.input : my.INPUT; // 'utf8', 'ascii' or 'latin1' for string data, default to utf8 if not specified; ignored for Buffer, TypedArray, or DataView. + let outputEncoding = (option.output === 'buf') ? undefined : (my.OUTPUT_LIST.indexOf(option.output) >= 0 ? option.output : my.OUTPUT); // option.output: 留空=》默认输出hex格式;或者手动指定 'buf', hex', 'latin1' or 'base64' + return crypto.createHash(hasher).update(data, inputEncoding).digest(outputEncoding); + } + return null; + } + , + isHashable: function (data, option) { + option = option || {}; + if (option.strict) { + return data && typeof (data) !== 'boolean' && data !== Infinity; // 允许大多数数据,除了空值、布尔值、无限数 + } + return typeof (data) !== 'undefined'; // 允许一切数据,除非 undefined + } + , + isHash: function (hash, option) { + option = option || {}; + option.hasher = my.HASHER_LIST.indexOf(option.hasher) >= 0 ? option.hasher : my.HASHER; + switch (option.hasher) { + case 'sha256': return /^[a-fA-F0-9]{64}$/.test(hash); + case 'md5': return /^[a-fA-F0-9]{32}$/.test(hash); + case 'ripemd160': case 'sha1': return /^[a-fA-F0-9]{40}$/.test(hash); + case 'sha512': return /^[a-fA-F0-9]{128}$/.test(hash); + } + return false; + } + , + encrypt: function (data, pwd, option) { + if (this.isHashable(data) && typeof (pwd) === 'string') { + option = option || {}; + let inputEncoding = my.INPUT_LIST.indexOf(option.input) >= 0 ? option.input : my.INPUT; // 'utf8' by default, 'ascii', 'latin1' for string or ignored for Buffer/TypedArray/DataView + let outputEncoding = (option.output === 'buf') ? undefined : (my.OUTPUT_LIST.indexOf(option.output) >= 0 ? option.output : my.OUTPUT); // 'latin1', 'base64', 'hex' by default or 'buf' to Buffer explicitly + let cipher = crypto.createCipher( + my.CIPHER_LIST.indexOf(option.cipher) >= 0 ? option.cipher : my.CIPHER, + this.hash(pwd)); + if (typeof (data) !== 'string' && !(data instanceof Buffer) && !(data instanceof DataView)) + data = JSON.stringify(data); + let encrypted = cipher.update(data, inputEncoding, outputEncoding); + encrypted += cipher.final(outputEncoding); // 但是 Buffer + Buffer 还是会变成string + return encrypted; + } + return null; + } + , + decrypt: function (data, pwd, option) { // data 应当是 encrypt 输出的数据类型 + if (data && (typeof (data) === 'string' || data instanceof Buffer) && typeof (pwd) === 'string') { + option = option || {}; + let inputEncoding = my.OUTPUT_LIST.indexOf(option.input) >= 0 ? option.input : my.OUTPUT; // input (=output of encrypt) could be 'latin1', 'base64', 'hex' by default for string or ignored for Buffer + let outputEncoding = (option.output === 'buf') ? undefined : (my.INPUT_LIST.indexOf(option.output) >= 0 ? option.output : my.INPUT); // output (=input of encrypt) could be 'latin1', 'ascii', 'utf8' by default or 'buf' to Buffer explicitly + let decipher = crypto.createDecipher( + my.CIPHER_LIST.indexOf(option.cipher) >= 0 ? option.cipher : my.CIPHER, + this.hash(pwd)); + let decrypted = decipher.update(data, inputEncoding, outputEncoding); + decrypted += decipher.final(outputEncoding); // 但是 Buffer + Buffer 还是会变成string + if (option.format === 'json') { // 如果用户输入错误密码,deciper也能返回结果。为了判断是否正确结果,对应当是 json 格式的原文做解析来验证。 + try { + JSON.parse(decrypted); + } catch (exception) { + return null; + } + } + return decrypted; + } + return null; + } + , + sign: function (data, seckey, option) { // data can be string or buffer or object, results are the same + if (this.isHashable(data) && this.isSeckey(seckey)) { + option = option || {}; + + // 使用nacl的签名算法。注意,nacl.sign需要的seckey是64字节=512位,而比特币/以太坊的seckey是32字节。因此本方法只能用于 TIC 币的 keypair。 + option.output = 'buf'; // 哈希必须输出为 buffer + var hashBuf = this.hash(data, option); + var signature = nacl.sign.detached(hashBuf, Buffer.from(seckey, 'hex')); + return Buffer.from(signature).toString('hex'); // 返回128个hex字符,64字节 + + // 方案2:尚未彻底实现。 + // let hasher=my.HASHER_LIST.indexOf(option.hasher)>=0?option.hasher:my.HASHER + // let inputEncoding=my.INPUT_LIST.indexOf(option.input)>=0?option.input:my.INPUT // 'utf8', 'ascii' or 'latin1' for string data, default to utf8 if not specified; ignored for Buffer, TypedArray, or DataView. + // let outputEncoding=(option.output==='buf')?undefined:(my.OUTPUT_LIST.indexOf(option.output)>=0?option.output:my.OUTPUT) + // let signer=crypto.createSign(hasher) + // return signer.update(data, inputEncoding).sign(seckey, outputEncoding) // todo: crypto的sign要求的seckey必须是PEM格式,因此这样写是不能用的。 + } + return null; + } + , + isSignature: function (signature) { + return /^[a-fA-F0-9]{128}$/.test(signature); + } + , + verify: function (data, signature, pubkey, option) { // data could be anything, but converts to string or remains be Buffer/TypedArray/DataView + if (this.isHashable(data) && this.isSignature(signature) && this.isPubkey(pubkey)) { + option = option || {}; + option.output = 'buf'; // 哈希必须输出为 buffer + var bufHash = this.hash(data, option); + var bufSignature = Buffer.from(signature, 'hex'); + var bufPubkey = Buffer.from(pubkey, 'hex'); + var res = nacl.sign.detached.verify(bufHash, bufSignature, bufPubkey); + return res; + } + return null; + } + , + pass2keypair: function (pass, option) { // 如果使用其他机制,例如密码、随机数,不使用secword,也可生成keypair + if (this.isHashable(pass)) { + option = option || {}; + option.hasher = my.HASHER_LIST.indexOf(option.hasher) >= 0 ? option.hasher : my.HASHER; + var hashBuf = crypto.createHash(option.hasher).update(pass).digest(); + var keypair = nacl.sign.keyPair.fromSeed(hashBuf); + return { + hash: hashBuf.toString('hex'), + pubkey: Buffer.from(keypair.publicKey).toString('hex'), // 测试过 不能直接keypair.publicKey.toString('hex'),不是buffer类型 + seckey: Buffer.from(keypair.secretKey).toString('hex') + }; + } + return null; + } + , + secword2keypair: function (secword, option = {coin: my.COIN, path: 'master'}) { // option.coin 币种;option.passphase 密码,默认为空;option.path==='master' 生成 HD master key,不定义则默认为相应币种的第一对公私钥。 + if (Secword.isValid(secword)) { + const { coin = my.COIN, path = 'master', pass = '' } = option; + // 用 bip39 算法从 secword 到种子,再用 bip32 算法从种子到根私钥。这是比特币、以太坊的标准方式,结果一致。 + // let hdmaster=HDKey.fromMasterSeed(new Buffer(this.secword2seed(secword, option.pass), 'hex')) // 和 new Secword(secword).toHDPrivateKey 求出的公私钥一样! + let hdmaster = new Secword(secword).toHDPrivateKey(pass); // 和 ethers.HDNode.fromMnemonic(secword)的公私钥一样。而 ethers.HDNode.fromMnemonic(secword).derivePath("m/44'/60'/0'/0/0")的公私钥===ethers.Wallet.fromMnemonic(secword [,"m/44'/60'/0'/0/0"]) + let key = hdmaster; + if (path === 'master') { + key = hdmaster; + } else if (!path) { + switch (coin) { + case 'BTC': key = hdmaster.derive("m/44'/0'/0'/0/0"); break; + case 'ETH': key = hdmaster.derive("m/44'/60'/0'/0/0"); break; + default: key = hdmaster.derive("m/44'/99'/0'/0/0"); break; + } + } else { // 指定了路径 option.path,例如 "m/44'/0'/0'/0/6" 或 "m/0/2147483647'/1" + key = hdmaster.derive(path); + } + return { + coin, + path, + seckey: key.privateKey.toString('hex'), // 或者 key.toJSON().privateKey。或者 key.privateKey.slice(2) 删除开头的'0x'如果是ethers.HDNode.fromMnemonic(secword)的结果 + pubkey: key.publicKey.toString('hex') + }; + + } + return null; + } + , + seckey2pubkey: function (seckey, option = {}) { + if (this.isSeckey(seckey) && seckey.length === 64) { // 只能用于32字节的私钥(BTC, ETH)。也就是不能用于 TIC 的私钥。 + const {curve = my.CURVE, compress = 'compressed'} = option; + return new crypto.ECDH(curve).setPrivateKey(seckey, 'hex').getPublicKey('hex', compress).toString('hex'); // ecdh.getPublicKey(不加参数) 默认为 'uncompressed' + // 从 nodejs 10.0 开始,还有 crypto.ECDH.convertKey 方法,更直接。 + // 或者 require('secp256k1').publicKeyCreate(Buffer.from(seckey, 'hex'),compress).toString('hex') + // 或者 require('bitcore-lib').PublicKey.fromPrivateKey(new Btc.PrivateKey(seckey)).toString('hex') + // 注意,Buffer.from(nacl.box.keyPair.fromSecretKey(Buffer.from(seckey,'hex')).publicKey).toString('hex') 得到的公钥与上面的不同 + } + return null; + } + , + secword2account: function (secword, option) { // account 比 keypair 多了 address 字段。 + option = option || {}; + option.coin = my.COIN_LIST.indexOf(option.coin) >= 0 ? option.coin : my.COIN; + let kp = this.secword2keypair(secword, option); + if (kp) { + kp.address = this.pubkey2address(kp.pubkey, option); + return kp; + } + return null; + } + , + secword2address: function (secword, option) { + option = option || {}; + option.coin = my.COIN_LIST.indexOf(option.coin) >= 0 ? option.coin : my.COIN; + let kp = this.secword2keypair(secword, option); + if (kp) { + return this.pubkey2address(kp.pubkey, option); + } + return null; + } + , + isSecword: function (secword) { + return Secword.isValid(secword); + } + , + isSeckey: function (seckey) { + // 比特币、以太坊的私钥:64 hex + // nacl.sign 的私钥 128 hex, nacl.box 的私钥 64 hex + return /^([a-fA-F0-9]{128}|[a-fA-F0-9]{64})$/.test(seckey); + } + , + isPubkey: function (pubkey) { + // 比特币的公钥:压缩型 '02|03' + 64 hex 或 无压缩型 '04' + 128 hex + // 以太坊的公钥:'02|03' + 64 hex + // nacl.sign 的公钥:64 hex + return /^((02|03)?[a-fA-F0-9]{64}|04[a-fA-F0-9]{128})$/.test(pubkey); // "d2f186a630f5558ba3ede10a4dd0549da5854eab3ed28ee8534350c2535d38b0" + } + , + isAddress: function (address) { + return /^[m|t|d|T][123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{33}$/.test(address); // && address.length>25 && bs58check.decode(address.slice(1)) && ['A'].indexOf(address[0]>=0)) { + } + , + pubkey2address: function (pubkey, option = {}) { // pubkey 应当是string类型 + if (this.isPubkey(pubkey)) { + const { coin = 'BTC', netType = 'mainnet', curve = my.CURVE } = option; + let h256 = crypto.createHash('sha256').update(Buffer.from(pubkey, 'hex')).digest(); + let h160 = crypto.createHash('ripemd160').update(h256).digest('hex'); + let prefix; + if (coin === 'BTC') { + switch (netType) { + case 'mainnet': prefix = '00'; break; // 1 + case 'testnet': prefix = '6f'; break; // m or n + case 'p2sh': prefix = '05'; break; // 3 + default: prefix = '00'; + } + return bs58check.encode(Buffer.from(prefix + h160, 'hex')); // wallet import format + } else if (coin === 'ETH') { // 目前不支持 ETH或其他币种 地址转换,因为这会大量增加前端打包的js。 + const uncompressedPubkey = crypto.ECDH.convertKey(pubkey, curve, 'hex', 'buffer', 'uncompressed'); + return '0x' + keccak256(uncompressedPubkey.slice(1)).slice(24); + } + } + return null; + } + , + secword2seed: function (secword, pass) { // 遵循bip39的算法。和 ether.HDNode.mnemonic2Seed 结果一样,是64字节的种子。 + if (Secword.isValid(secword)) { // bip39.validateMnemonic(secword)) { + return new Secword(secword).toSeed(pass).toString('hex'); // 结果一致于 bip39.mnemonicToSeedHex(secword) 或 ethers.HDNode.mnemonic2Seed(secword) + } + return null; + } + , + randomSecword: function (lang) { // Object.keys(Secword.Words) => [ 'CHINESE', 'ENGLISH', 'FRENCH', 'ITALIAN', 'JAPANESE', 'SPANISH' ] + lang = (lang && Secword.Words.hasOwnProperty(lang.toUpperCase())) ? lang.toUpperCase() : 'ENGLISH'; + return new Secword(Secword.Words[lang]).phrase; + } + , + randomSeckey: function () { + return Buffer.from(nacl.box.keyPair().secretKey).toString('hex'); // 32字节 + } + , + randomKeypair: function () { + // 此函数有错!! + let kp = nacl.box.keyPair(); + const seckey = Buffer.from(kp.secretKey).toString('hex'); + const pubkey = this.seckey2pubkey(seckey); + return { + seckey, pubkey + }; + } + , + randomString: function (length = 6, alphabet) { // 长度为 length,字母表为 alphabet 的随机字符串 + alphabet = alphabet || "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789#$%^&*@"; + var text = ''; + for (var i = 0; i < length; i++) { + text += alphabet.charAt(Math.floor(Math.random() * alphabet.length)); + } + return text; + } + , + randomNumber: function (option) { // 长度为 option.length 的随机数字,或者 (option.min||0) <= num < option.max + option = option || {}; + let num = 0; + if (option.length > 0) { + num = parseInt(Math.random() * Math.pow(10, option.length)); + let l = new String(num).length; + while (l < option.length) { + num = '0' + num; // 注意,这时返回的是字符串! + l++; + } + } else if (option.max > 0) { + option.min = (option.min >= 0) ? option.min : 0; + num = parseInt(Math.random() * (option.max - option.min)) + option.min; + } else { // 如果 option 为空 + num = Math.random(); + } + return num; + } + , + rsaSign: function (string2Sign, prikey, signType) { + signType = signType || 'RSA-SHA1'; // could be RSA-SHA256, RSA-SHA1 or more + let signer = crypto.createSign(signType); + return encodeURIComponent(signer.update(string2Sign).sign(prikey, 'base64')); + } + , + rsaVerify: function (string2Verify, sign, pubkey, signType) { + signType = signType || 'RSA-SHA1'; // could be RSA-SHA256, RSA-SHA1 or more + let verifier = crypto.createVerify(signType); + return verifier.update(string2Verify).verify(pubkey, sign, 'base64'); + } +}; \ No newline at end of file diff --git a/src/utils/index.js b/src/utils/index.js new file mode 100644 index 0000000..ad88cf2 --- /dev/null +++ b/src/utils/index.js @@ -0,0 +1,38 @@ +/* eslint-disable no-useless-escape */ +const PHONE_VALID_REG = /^\+[0-9]+-[0-9]+\b/; +const URL_PROTOCOL_REG = /^\w+(?=\:\/\/)/; +const URL_HOST_REG = /(?<=\:\/\/)\S+(?=\:)|(?<=\:\/\/)\S+(?=\b)/; +const URL_PORT_REG = /(?<=\:)\d+/; + +const getFuncProps = fn => { + const reg = /^(async function|function){1}\s*[^\(]*\(\s*([^\)]*)\)/m; + return Object.toString.call(fn).match(reg)[2].replace(/ /g, '').split(','); +}; +const isPhoneNumber = n => { + return PHONE_VALID_REG.test(n); +}; +const parseURL = (url, type) => { + return type === 'protocol' ? getProtocol(url) : + type === 'host' ? getHost(url) : + type === 'port' ? getPort(url) : ''; +}; +const getProtocol = url => { + const res = URL_PROTOCOL_REG.exec(url); + return Object.prototype.toString.call(res) === '[object Array]' ? res[0] : null; +}; +const getHost = url => { + const res = URL_HOST_REG.exec(url); + return Object.prototype.toString.call(res) === '[object Array]' ? res[0] : null; +}; +const getPort = url => { + const res = URL_PORT_REG.exec(url); + return Object.prototype.toString.call(res) === '[object Array]' ? res[0] : null; +}; +module.exports = { + getFuncProps, + isPhoneNumber, + parseURL, + getPort, + getHost, + getProtocol +}; \ No newline at end of file diff --git a/src/utils/jwt.js b/src/utils/jwt.js new file mode 100644 index 0000000..c4086a3 --- /dev/null +++ b/src/utils/jwt.js @@ -0,0 +1,30 @@ +const jwt = require('jsonwebtoken'); + +module.exports = { + createToken: (data = {}, JWT_SECRET, config = {}) => { + if (typeof config !== 'object') { + config = {}; + } + if (!config.maxAge || typeof config.maxAge !== 'number') { + config.maxAge = 3600; + } + + let token = jwt.sign({ data }, JWT_SECRET, { + expiresIn: config.maxAge, + algorithm: 'HS256' + }); + + return token; + }, + verifyToken: async (token = '', JWT_SECRET) => { + return await new Promise(resolve => { + if (!token) return null; + jwt.verify(token, JWT_SECRET, (err, res) => { + if (err) { + resolve(null); + } + resolve(res); + }); + }); + } +}; \ No newline at end of file diff --git a/src/utils/locale.js b/src/utils/locale.js new file mode 100755 index 0000000..b72304b --- /dev/null +++ b/src/utils/locale.js @@ -0,0 +1,262 @@ + +module.exports = { + LangSet: [ + { code: 'zh_CN', name: '中文(简体)' }, + { code: 'en_US', name: 'English' }, + { code: 'jp_JP', name: '日本語' }, + { code: 'kr_KR', name: '한국어' }, + { code: 'ru_RU', name: 'русский' }, + { code: 'es_ES', name: 'Español' } + ], + NationSet: [ + // {pode:"N004", iso2:"AF", name_en:"Afghanistan", name_zh_CN:"阿富汗", name_native:"‫افغانستان‬‎", itc:"+93-"}, + // {pode:"N008", iso2:"AL", name_en:"Albania", name_zh_CN:"阿尔巴尼亚", name_native:"Shqipëri", itc:"+355-"}, + // {pode:"N010", iso2:"AQ", name_en:"Antarctica", name_zh_CN:"南极洲", itc:"+672-"}, + // {pode:"N012", iso2:"DZ", name_en:"Algeria", name_zh_CN:"阿尔及利亚", name_native:"‫الجزائر‬‎", itc:"+213-"}, + // {pode:"N016", iso2:"AS", name_en:"American Samoa", name_zh_CN:"美属萨摩亚", itc:"+1-"}, + // {pode:"N020", iso2:"AD", name_en:"Andorra", name_zh_CN:"安道尔", itc:"+376-"}, + // {pode:"N024", iso2:"AO", name_en:"Angola", name_zh_CN:"安哥拉", itc:"+244-"}, + // {pode:"N028", iso2:"AG", name_en:"Antigua and Barbuda", name_zh_CN:"安提瓜和巴布达", itc:"+1-"}, + // {pode:"N031", iso2:"AZ", name_en:"Azerbaijan", name_zh_CN:"阿塞拜疆", name_native:"Azərbaycan", itc:"+994-"}, + // {pode:"N032", iso2:"AR", name_en:"Argentina", name_zh_CN:"阿根廷", itc:"+54-"}, + // {pode:"N036", iso2:"AU", name_en:"Australia", name_zh_CN:"澳大利亚", itc:"+61-"}, + // {pode:"N040", iso2:"AT", name_en:"Austria", name_zh_CN:"奥地利", name_native:"Österreich", itc:"+43-"}, + // {pode:"N044", iso2:"BS", name_en:"Bahamas", name_zh_CN:"巴哈马", itc:"+1-"}, + // {pode:"N048", iso2:"BH", name_en:"Bahrain", name_zh_CN:"巴林", name_native:"‫البحرين‬‎", itc:"+973-"}, + // {pode:"N050", iso2:"BD", name_en:"Bangladesh", name_zh_CN:"孟加拉", name_native:"বাংলাদেশ", itc:"+880-"}, + // {pode:"N051", iso2:"AM", name_en:"Armenia", name_zh_CN:"亚美尼亚", name_native:"Հայաստան", itc:"+374-"}, + // {pode:"N052", iso2:"BB", name_en:"Barbados", name_zh_CN:"巴巴多斯", itc:"+1-"}, + // {pode:"N056", iso2:"BE", name_en:"Belgium", name_zh_CN:"比利时", name_native:"België", itc:"+32-"}, + // {pode:"N060", iso2:"BM", name_en:"Bermuda", name_zh_CN:"百慕大", itc:"+1-"}, + // {pode:"N064", iso2:"BT", name_en:"Bhutan", name_zh_CN:"不丹", name_native:"འབྲུག", itc:"+975-"}, + // {pode:"N068", iso2:"BO", name_en:"Bolivia", name_zh_CN:"玻利维亚", itc:"+591-"}, + // {pode:"N070", iso2:"BA", name_en:"Bosnia and Herzegovina", name_zh_CN:"波黑", name_native:"Босна и Херцеговина", itc:"+387-"}, + // {pode:"N072", iso2:"BW", name_en:"Botswana", name_zh_CN:"博茨瓦纳", itc:"+267-"}, + // {pode:"N074", iso2:"BV", name_en:"Bouvet Island", name_zh_CN:"布韦岛", itc:"+47-"}, + // {pode:"N076", iso2:"BR", name_en:"Brazil", name_zh_CN:"巴西", name_native:"Brasil", itc:"+55-"}, + // {pode:"N084", iso2:"BZ", name_en:"Belize", name_zh_CN:"伯利兹", itc:"+501-"}, + // {pode:"N086", iso2:"IO", name_en:"British Indian Ocean Territory", name_zh_CN:"英属印度洋领地", itc:"+246-"}, + // {pode:"N090", iso2:"SB", name_en:"Solomon Islands", name_zh_CN:"所罗门群岛", itc:"+677-"}, + // {pode:"N092", iso2:"VG", name_en:"British Virgin Islands", name_zh_CN:"英属维尔京群岛", itc:"+1-"}, + // {pode:"N096", iso2:"BN", name_en:"Brunei", name_zh_CN:"文莱", itc:"+673-"}, + // {pode:"N100", iso2:"BG", name_en:"Bulgaria", name_zh_CN:"保加利亚", name_native:"България", itc:"+359-"}, + // {pode:"N104", iso2:"MM", name_en:"Myanmar (Burma)", name_zh_CN:"缅甸", name_native:"မြန်မာ", itc:"+95-"}, + // {pode:"N108", iso2:"BI", name_en:"Burundi", name_zh_CN:"布隆迪", name_native:"Uburundi", itc:"+257-"}, + // {pode:"N112", iso2:"BY", name_en:"Belarus", name_zh_CN:"白俄罗斯", name_native:"Беларусь", itc:"+375-"}, + // {pode:"N116", iso2:"KH", name_en:"Cambodia", name_zh_CN:"柬埔寨", name_native:"កម្ពុជា", itc:"+855-"}, + // {pode:"N120", iso2:"CM", name_en:"Cameroon", name_zh_CN:"喀麦隆", name_native:"Cameroun", itc:"+237-"}, + // {pode:"N124", iso2:"CA", name_en:"Canada", name_zh_CN:"加拿大", itc:"+1-"}, + // {pode:"N132", iso2:"CV", name_en:"Cape Verde", name_zh_CN:"佛得角", name_native:"Kabu Verdi", itc:"+238-"}, + // {pode:"N136", iso2:"KY", name_en:"Cayman Islands", name_zh_CN:"开曼群岛", itc:"+1-"}, + // {pode:"N140", iso2:"CF", name_en:"Central African Republic", name_zh_CN:"中非", name_native:"République centrafricaine", itc:"+236-"}, + // {pode:"N144", iso2:"LK", name_en:"Sri Lanka", name_zh_CN:"斯里兰卡", name_native:"ශ්‍රී ලංකාව", itc:"+94-"}, + // {pode:"N148", iso2:"TD", name_en:"Chad", name_zh_CN:"乍得", name_native:"Tchad", itc:"+235-"}, + // {pode:"N152", iso2:"CL", name_en:"Chile", name_zh_CN:"智利", itc:"+56-"}, + { pode: "N156", iso2: "CN", name_en: "China", name_zh_CN: "中国", name_native: "中国", itc: "+86-" }, + { pode: "N158", iso2: "TW", name_en: "Taiwan", name_zh_CN: "台湾", name_native: "台灣", itc: "+886-" }, + // {pode:"N162", iso2:"CX", name_en:"Christmas Island", name_zh_CN:"圣诞岛", itc:"+61-"}, + // {pode:"N166", iso2:"CC", name_en:"Cocos (Keeling) Islands", name_zh_CN:"科科斯群岛", itc:"+61-"}, + // {pode:"N170", iso2:"CO", name_en:"Colombia", name_zh_CN:"哥伦比亚", itc:"+57-"}, + // {pode:"N174", iso2:"KM", name_en:"Comoros", name_zh_CN:"科摩罗", name_native:"‫جزر القمر‬‎", itc:"+269-"}, + // {pode:"N175", iso2:"YT", name_en:"Mayotte", name_zh_CN:"马约特", itc:"+262-"}, + // {pode:"N178", iso2:"CG", name_en:"Congo (Republic)", name_zh_CN:"刚果(布)", name_native:"Congo-Brazzaville", itc:"+242-"}, + // {pode:"N180", iso2:"CD", name_en:"Congo (DRC)", name_zh_CN:"刚果(金)", name_native:"Jamhuri ya Kidemokrasia ya Kongo", itc:"+243-"}, + // {pode:"N184", iso2:"CK", name_en:"Cook Islands", name_zh_CN:"库克群岛", itc:"+682-"}, + // {pode:"N188", iso2:"CR", name_en:"Costa Rica", name_zh_CN:"哥斯达黎加", itc:"+506-"}, + // {pode:"N191", iso2:"HR", name_en:"Croatia", name_zh_CN:"克罗地亚", name_native:"Hrvatska", itc:"+385-"}, + // {pode:"N192", iso2:"CU", name_en:"Cuba", name_zh_CN:"古巴", itc:"+53-"}, + // {pode:"N196", iso2:"CY", name_en:"Cyprus", name_zh_CN:"塞浦路斯", name_native:"Κύπρος", itc:"+357-"}, + // {pode:"N203", iso2:"CZ", name_en:"Czech Republic", name_zh_CN:"捷克", name_native:"Česká republika", itc:"+420-"}, + // {pode:"N204", iso2:"BJ", name_en:"Benin", name_zh_CN:"贝宁", name_native:"Bénin", itc:"+229-"}, + // {pode:"N208", iso2:"DK", name_en:"Denmark", name_zh_CN:"丹麦", name_native:"Danmark", itc:"+45-"}, + // {pode:"N212", iso2:"DM", name_en:"Dominica", name_zh_CN:"多米尼克", itc:"+1-"}, + // {pode:"N214", iso2:"DO", name_en:"Dominican Republic", name_zh_CN:"多米尼加", name_native:"República Dominicana", itc:"+1-"}, + // {pode:"N218", iso2:"EC", name_en:"Ecuador", name_zh_CN:"厄瓜多尔", itc:"+593-"}, + // {pode:"N222", iso2:"SV", name_en:"El Salvador", name_zh_CN:"萨尔瓦多", itc:"+503-"}, + // {pode:"N226", iso2:"GQ", name_en:"Equatorial Guinea", name_zh_CN:"赤道几内亚", name_native:"Guinea Ecuatorial", itc:"+240-"}, + // {pode:"N231", iso2:"ET", name_en:"Ethiopia", name_zh_CN:"埃塞俄比亚", itc:"+251-"}, + // {pode:"N232", iso2:"ER", name_en:"Eritrea", name_zh_CN:"厄立特里亚", itc:"+291-"}, + // {pode:"N233", iso2:"EE", name_en:"Estonia", name_zh_CN:"爱沙尼亚", name_native:"Eesti", itc:"+372-"}, + // {pode:"N234", iso2:"FO", name_en:"Faroe Islands", name_zh_CN:"法罗群岛", name_native:"Føroyar", itc:"+298-"}, + // {pode:"N238", iso2:"FK", name_en:"Falkland Islands", name_zh_CN:"马尔维纳斯群岛(福克兰)", name_native:"Islas Malvinas", itc:"+500-"}, + // {pode:"N239", iso2:"GS", name_en:"South Georgia and the South Sandwich Islands", name_zh_CN:"南乔治亚岛和南桑威奇群岛", itc:"+500-"}, + // {pode:"N242", iso2:"FJ", name_en:"Fiji", name_zh_CN:"斐济群岛", itc:"+679-"}, + // {pode:"N246", iso2:"FI", name_en:"Finland", name_zh_CN:"芬兰", name_native:"Suomi", itc:"+358-"}, + // {pode:"N248", iso2:"AX", name_en:"Åland Islands", name_zh_CN:"奥兰群岛", itc:"+358-"}, + {pode:"N250", iso2:"FR", name_en:"France", name_zh_CN:"法国", itc:"+33-"}, + // {pode:"N254", iso2:"GF", name_en:"French Guiana", name_zh_CN:"法属圭亚那", name_native:"Guyane française", itc:"+594-"}, + // {pode:"N258", iso2:"PF", name_en:"French Polynesia", name_zh_CN:"法属波利尼西亚", name_native:"Polynésie française", itc:"+689-"}, + // {pode:"N260", iso2:"TF", name_en:"French Southern Territories", name_zh_CN:"法属南部领地", itc:"+262-"}, + // {pode:"N262", iso2:"DJ", name_en:"Djibouti", name_zh_CN:"吉布提", itc:"+253-"}, + // {pode:"N266", iso2:"GA", name_en:"Gabon", name_zh_CN:"加蓬", itc:"+241-"}, + // {pode:"N268", iso2:"GE", name_en:"Georgia", name_zh_CN:"格鲁吉亚", name_native:"საქართველო", itc:"+995-"}, + // {pode:"N270", iso2:"GM", name_en:"Gambia", name_zh_CN:"冈比亚", itc:"+220-"}, + // {pode:"N275", iso2:"PS", name_en:"Palestine", name_zh_CN:"巴勒斯坦", name_native:"‫فلسطين‬‎", itc:"+970-"}, + // {pode:"N276", iso2:"DE", name_en:"Germany", name_zh_CN:"德国", name_native:"Deutschland", itc:"+49-"}, + // {pode:"N288", iso2:"GH", name_en:"Ghana", name_zh_CN:"加纳", name_native:"Gaana", itc:"+233-"}, + // {pode:"N292", iso2:"GI", name_en:"Gibraltar", name_zh_CN:"直布罗陀", itc:"+350-"}, + // {pode:"N296", iso2:"KI", name_en:"Kiribati", name_zh_CN:"基里巴斯", itc:"+686-"}, + // {pode:"N300", iso2:"GR", name_en:"Greece", name_zh_CN:"希腊", name_native:"Ελλάδα", itc:"+30-"}, + // {pode:"N304", iso2:"GL", name_en:"Greenland", name_zh_CN:"格陵兰", name_native:"Kalaallit Nunaat", itc:"+299-"}, + // {pode:"N308", iso2:"GD", name_en:"Grenada", name_zh_CN:"格林纳达", itc:"+1-"}, + // {pode:"N312", iso2:"GP", name_en:"Guadeloupe", name_zh_CN:"瓜德罗普", itc:"+590-"}, + {pode:"N316", iso2:"GU", name_en:"Guam", name_zh_CN:"关岛", itc:"+1-"}, + // {pode:"N320", iso2:"GT", name_en:"Guatemala", name_zh_CN:"危地马拉", itc:"+502-"}, + // {pode:"N324", iso2:"GN", name_en:"Guinea", name_zh_CN:"几内亚", name_native:"Guinée", itc:"+224-"}, + // {pode:"N328", iso2:"GY", name_en:"Guyana", name_zh_CN:"圭亚那", itc:"+592-"}, + // {pode:"N332", iso2:"HT", name_en:"Haiti", name_zh_CN:"海地", itc:"+509-"}, + // {pode:"N334", iso2:"HM", name_en:"Heard Island and McDonald Islands", name_zh_CN:"赫德岛和麦克唐纳群岛", itc:""}, + // {pode:"N336", iso2:"VA", name_en:"Vatican City", name_zh_CN:"梵蒂冈", name_native:"Città del Vaticano", itc:"+39-"}, + // {pode:"N340", iso2:"HN", name_en:"Honduras", name_zh_CN:"洪都拉斯", itc:"+504-"}, + { pode: "N344", iso2: "HK", name_en: "Hong Kong", name_zh_CN: "中国香港", name_native: "中国香港", itc: "+852-" }, + // {pode:"N348", iso2:"HU", name_en:"Hungary", name_zh_CN:"匈牙利", name_native:"Magyarország", itc:"+36-"}, + // {pode:"N352", iso2:"IS", name_en:"Iceland", name_zh_CN:"冰岛", name_native:"Ísland", itc:"+354-"}, + // {pode:"N356", iso2:"IN", name_en:"India", name_zh_CN:"印度", name_native:"भारत", itc:"+91-"}, + // {pode:"N360", iso2:"ID", name_en:"Indonesia", name_zh_CN:"印尼", itc:"+62-"}, + // {pode:"N364", iso2:"IR", name_en:"Iran", name_zh_CN:"伊朗", name_native:"‫ایران‬‎", itc:"+98-"}, + // {pode:"N368", iso2:"IQ", name_en:"Iraq", name_zh_CN:"伊拉克", name_native:"‫العراق‬‎", itc:"+964-"}, + // {pode:"N372", iso2:"IE", name_en:"Ireland", name_zh_CN:"爱尔兰", itc:"+353-"}, + // {pode:"N376", iso2:"IL", name_en:"Israel", name_zh_CN:"以色列", name_native:"‫ישראל‬‎", itc:"+972-"}, + // {pode:"N380", iso2:"IT", name_en:"Italy", name_zh_CN:"意大利", name_native:"Italia", itc:"+39-"}, + // {pode:"N384", iso2:"CI", name_en:"Côte D'Ivoire", name_zh_CN:"科特迪瓦", itc:"+225-"}, + // {pode:"N388", iso2:"JM", name_en:"Jamaica", name_zh_CN:"牙买加", itc:"+1-"}, + {pode:"N392", iso2:"JP", name_en:"Japan", name_zh_CN:"日本", name_native:"日本", itc:"+81-"}, + // {pode:"N398", iso2:"KZ", name_en:"Kazakhstan", name_zh_CN:"哈萨克斯坦", name_native:"Казахстан", itc:"+7-"}, + // {pode:"N400", iso2:"JO", name_en:"Jordan", name_zh_CN:"约旦", name_native:"‫الأردن‬‎", itc:"+962-"}, + // {pode:"N404", iso2:"KE", name_en:"Kenya", name_zh_CN:"肯尼亚", itc:"+254-"}, + // {pode:"N408", iso2:"KP", name_en:"North Korea", name_zh_CN:"朝鲜", name_native:"조선 민주주의 인민 공화국", itc:"+850-"}, + {pode:"N410", iso2:"KR", name_en:"South Korea", name_zh_CN:"韩国", name_native:"대한민국", itc:"+82-"}, + // {pode:"N414", iso2:"KW", name_en:"Kuwait", name_zh_CN:"科威特", name_native:"‫الكويت‬‎", itc:"+965-"}, + // {pode:"N417", iso2:"KG", name_en:"Kyrgyzstan", name_zh_CN:"吉尔吉斯斯坦", name_native:"Кыргызстан", itc:"+996-"}, + // {pode:"N418", iso2:"LA", name_en:"Laos", name_zh_CN:"老挝", name_native:"ລາວ", itc:"+856-"}, + // {pode:"N422", iso2:"LB", name_en:"Lebanon", name_zh_CN:"黎巴嫩", name_native:"‫لبنان‬‎", itc:"+961-"}, + // {pode:"N426", iso2:"LS", name_en:"Lesotho", name_zh_CN:"莱索托", itc:"+266-"}, + // {pode:"N428", iso2:"LV", name_en:"Latvia", name_zh_CN:"拉脱维亚", name_native:"Latvija", itc:"+371-"}, + // {pode:"N430", iso2:"LR", name_en:"Liberia", name_zh_CN:"利比里亚", itc:"+231-"}, + // {pode:"N434", iso2:"LY", name_en:"Libya", name_zh_CN:"利比亚", name_native:"‫ليبيا‬‎", itc:"+218-"}, + // {pode:"N438", iso2:"LI", name_en:"Liechtenstein", name_zh_CN:"列支敦士登", itc:"+423-"}, + // {pode:"N440", iso2:"LT", name_en:"Lithuania", name_zh_CN:"立陶宛", name_native:"Lietuva", itc:"+370-"}, + // {pode:"N442", iso2:"LU", name_en:"Luxembourg", name_zh_CN:"卢森堡", itc:"+352-"}, + { pode: "N446", iso2: "MO", name_en: "Macao", name_zh_CN: "中国澳门", name_native: "中国澳门", itc: "+853-" }, + // {pode:"N450", iso2:"MG", name_en:"Madagascar", name_zh_CN:"马达加斯加", name_native:"Madagasikara", itc:"+261-"}, + // {pode:"N454", iso2:"MW", name_en:"Malawi", name_zh_CN:"马拉维", itc:"+265-"}, + {pode:"N458", iso2:"MY", name_en:"Malaysia", name_zh_CN:"马来西亚", itc:"+60-"}, + // {pode:"N462", iso2:"MV", name_en:"Maldives", name_zh_CN:"马尔代夫", itc:"+960-"}, + // {pode:"N466", iso2:"ML", name_en:"Mali", name_zh_CN:"马里", itc:"+223-"}, + // {pode:"N470", iso2:"MT", name_en:"Malta", name_zh_CN:"马耳他", itc:"+356-"}, + // {pode:"N474", iso2:"MQ", name_en:"Martinique", name_zh_CN:"马提尼克", itc:"+596-"}, + // {pode:"N478", iso2:"MR", name_en:"Mauritania", name_zh_CN:"毛里塔尼亚", name_native:"‫موريتانيا‬‎", itc:"+222-"}, + // {pode:"N480", iso2:"MU", name_en:"Mauritius", name_zh_CN:"毛里求斯", name_native:"Moris", itc:"+230-"}, + {pode:"N484", iso2:"MX", name_en:"Mexico", name_zh_CN:"墨西哥", name_native:"México", itc:"+52-"}, + // {pode:"N492", iso2:"MC", name_en:"Monaco", name_zh_CN:"摩纳哥", itc:"+377-"}, + // {pode:"N496", iso2:"MN", name_en:"Mongolia", name_zh_CN:"蒙古", name_native:"Монгол", itc:"+976-"}, + // {pode:"N498", iso2:"MD", name_en:"Moldova", name_zh_CN:"摩尔多瓦", name_native:"Republica Moldova", itc:"+373-"}, + // {pode:"N499", iso2:"ME", name_en:"Montenegro", name_zh_CN:"黑山", name_native:"Crna Gora", itc:"+382-"}, + // {pode:"N500", iso2:"MS", name_en:"Montserrat", name_zh_CN:"蒙塞拉特岛", itc:"+1-"}, + // {pode:"N504", iso2:"MA", name_en:"Morocco", name_zh_CN:"摩洛哥", name_native:"‫المغرب‬‎", itc:"+212-"}, + // {pode:"N508", iso2:"MZ", name_en:"Mozambique", name_zh_CN:"莫桑比克", name_native:"Moçambique", itc:"+258-"}, + // {pode:"N512", iso2:"OM", name_en:"Oman", name_zh_CN:"阿曼", name_native:"‫عُمان‬‎", itc:"+968-"}, + // {pode:"N516", iso2:"NA", name_en:"Namibia", name_zh_CN:"纳米比亚", name_native:"Namibië", itc:"+264-"}, + // {pode:"N520", iso2:"NR", name_en:"Nauru", name_zh_CN:"瑙鲁", itc:"+674-"}, + // {pode:"N524", iso2:"NP", name_en:"Nepal", name_zh_CN:"尼泊尔", name_native:"नेपाल", itc:"+977-"}, + // {pode:"N528", iso2:"NL", name_en:"Netherlands", name_zh_CN:"荷兰", name_native:"Nederland", itc:"+31-"}, + // {pode:"N531", iso2:"CW", name_en:"Curaçao", name_zh_CN:"库拉索", itc:"+599-"}, + // {pode:"N533", iso2:"AW", name_en:"Aruba", name_zh_CN:"阿鲁巴", itc:"+297-"}, + // {pode:"N534", iso2:"SX", name_en:"Sint Maarten", name_zh_CN:"荷属圣马丁", itc:"+1-"}, + // {pode:"N535", iso2:"BQ", name_en:"Caribbean Netherlands", name_zh_CN:"荷兰加勒比区", itc:"+599-"}, + // {pode:"N540", iso2:"NC", name_en:"New Caledonia", name_zh_CN:"新喀里多尼亚", name_native:"Nouvelle-Calédonie", itc:"+687-"}, + // {pode:"N548", iso2:"VU", name_en:"Vanuatu", name_zh_CN:"瓦努阿图", itc:"+678-"}, + // {pode:"N554", iso2:"NZ", name_en:"New Zealand", name_zh_CN:"新西兰", itc:"+64-"}, + // {pode:"N558", iso2:"NI", name_en:"Nicaragua", name_zh_CN:"尼加拉瓜", itc:"+505-"}, + // {pode:"N562", iso2:"NE", name_en:"Niger", name_zh_CN:"尼日尔", name_native:"Nijar", itc:"+227-"}, + // {pode:"N566", iso2:"NG", name_en:"Nigeria", name_zh_CN:"尼日利亚", itc:"+234-"}, + // {pode:"N570", iso2:"NU", name_en:"Niue", name_zh_CN:"纽埃", itc:"+683-"}, + // {pode:"N574", iso2:"NF", name_en:"Norfolk Island", name_zh_CN:"诺福克岛", itc:"+672-"}, + // {pode:"N578", iso2:"NO", name_en:"Norway", name_zh_CN:"挪威", name_native:"Norge", itc:"+47-"}, + // {pode:"N580", iso2:"MP", name_en:"Northern Mariana Islands", name_zh_CN:"北马里亚纳群岛", itc:"+1-"}, + // {pode:"N581", iso2:"UM", name_en:"U.S. Minor Outlying Islands", name_zh_CN:"美国本土外小岛屿", itc:"+1-"}, + // {pode:"N583", iso2:"FM", name_en:"Micronesia", name_zh_CN:"密克罗尼西亚联邦", itc:"+691-"}, + // {pode:"N584", iso2:"MH", name_en:"Marshall Islands", name_zh_CN:"马绍尔群岛", itc:"+692-"}, + // {pode:"N585", iso2:"PW", name_en:"Palau", name_zh_CN:"帕劳", itc:"+680-"}, + // {pode:"N586", iso2:"PK", name_en:"Pakistan", name_zh_CN:"巴基斯坦", name_native:"‫پاکستان‬‎", itc:"+92-"}, + // {pode:"N591", iso2:"PA", name_en:"Panama", name_zh_CN:"巴拿马", name_native:"Panamá", itc:"+507-"}, + // {pode:"N598", iso2:"PG", name_en:"Papua New Guinea", name_zh_CN:"巴布亚新几内亚", itc:"+675-"}, + // {pode:"N600", iso2:"PY", name_en:"Paraguay", name_zh_CN:"巴拉圭", itc:"+595-"}, + // {pode:"N604", iso2:"PE", name_en:"Peru", name_zh_CN:"秘鲁", name_native:"Perú", itc:"+51-"}, + // {pode:"N608", iso2:"PH", name_en:"Philippines", name_zh_CN:"菲律宾", itc:"+63-"}, + // {pode:"N612", iso2:"PN", name_en:"Pitcairn Islands", name_zh_CN:"皮特凯恩群岛", itc:"+64-"}, + // {pode:"N616", iso2:"PL", name_en:"Poland", name_zh_CN:"波兰", name_native:"Polska", itc:"+48-"}, + // {pode:"N620", iso2:"PT", name_en:"Portugal", name_zh_CN:"葡萄牙", itc:"+351-"}, + // {pode:"N624", iso2:"GW", name_en:"Guinea-Bissau", name_zh_CN:"几内亚比绍", name_native:"Guiné Bissau", itc:"+245-"}, + // {pode:"N626", iso2:"TL", name_en:"Timor-Leste", name_zh_CN:"东帝汶", itc:"+670-"}, + // {pode:"N630", iso2:"PR", name_en:"Puerto Rico", name_zh_CN:"波多黎各", itc:"+1-"}, + // {pode:"N634", iso2:"QA", name_en:"Qatar", name_zh_CN:"卡塔尔", name_native:"‫قطر‬‎", itc:"+974-"}, + // {pode:"N638", iso2:"RE", name_en:"Réunion", name_zh_CN:"留尼汪", name_native:"La Réunion", itc:"+262-"}, + // {pode:"N642", iso2:"RO", name_en:"Romania", name_zh_CN:"罗马尼亚", name_native:"România", itc:"+40-"}, + // {pode:"N643", iso2:"RU", name_en:"Russia", name_zh_CN:"俄罗斯", name_native:"Россия", itc:"+7-"}, + // {pode:"N646", iso2:"RW", name_en:"Rwanda", name_zh_CN:"卢旺达", itc:"+250-"}, + // {pode:"N652", iso2:"BL", name_en:"Saint Barthélemy", name_zh_CN:"圣巴泰勒米岛", itc:"+590-"}, + // {pode:"N654", iso2:"SH", name_en:"Saint Helena", name_zh_CN:"圣赫勒拿", itc:"+290-"}, + // {pode:"N659", iso2:"KN", name_en:"Saint Kitts and Nevis", name_zh_CN:"圣基茨和尼维斯", itc:"+1-"}, + // {pode:"N660", iso2:"AI", name_en:"Anguilla", name_zh_CN:"安圭拉", itc:"+1-"}, + // {pode:"N662", iso2:"LC", name_en:"Saint Lucia", name_zh_CN:"圣卢西亚", itc:"+1-"}, + // {pode:"N663", iso2:"MF", name_en:"Saint Martin", name_zh_CN:"法属圣马丁", name_native:"Saint-Martin (partie française)", itc:"+590-"}, + // {pode:"N666", iso2:"PM", name_en:"Saint Pierre and Miquelon", name_zh_CN:"圣皮埃尔和密克隆", name_native:"Saint-Pierre-et-Miquelon", itc:"+508-"}, + // {pode:"N670", iso2:"VC", name_en:"Saint Vincent and the Grenadines", name_zh_CN:"圣文森特和格林纳丁斯", itc:"+1-"}, + // {pode:"N674", iso2:"SM", name_en:"San Marino", name_zh_CN:"圣马力诺", itc:"+378-"}, + // {pode:"N678", iso2:"ST", name_en:"São Tomé and Príncipe", name_zh_CN:"圣多美和普林西比", name_native:"São Tomé e Príncipe", itc:"+239-"}, + // {pode:"N682", iso2:"SA", name_en:"Saudi Arabia", name_zh_CN:"沙特阿拉伯", name_native:"‫المملكة العربية السعودية‬‎", itc:"+966-"}, + // {pode:"N686", iso2:"SN", name_en:"Senegal", name_zh_CN:"塞内加尔", name_native:"Sénégal", itc:"+221-"}, + // {pode:"N688", iso2:"RS", name_en:"Serbia", name_zh_CN:"塞尔维亚", name_native:"Србија", itc:"+381-"}, + // {pode:"N690", iso2:"SC", name_en:"Seychelles", name_zh_CN:"塞舌尔", itc:"+248-"}, + // {pode:"N694", iso2:"SL", name_en:"Sierra Leone", name_zh_CN:"塞拉利昂", itc:"+232-"}, + {pode:"N702", iso2:"SG", name_en:"Singapore", name_zh_CN:"新加坡", itc:"+65-"}, + // {pode:"N703", iso2:"SK", name_en:"Slovakia", name_zh_CN:"斯洛伐克", name_native:"Slovensko", itc:"+421-"}, + // {pode:"N704", iso2:"VN", name_en:"Vietnam", name_zh_CN:"越南", name_native:"Việt Nam", itc:"+84-"}, + // {pode:"N705", iso2:"SI", name_en:"Slovenia", name_zh_CN:"斯洛文尼亚", name_native:"Slovenija", itc:"+386-"}, + // {pode:"N706", iso2:"SO", name_en:"Somalia", name_zh_CN:"索马里", name_native:"Soomaaliya", itc:"+252-"}, + // {pode:"N710", iso2:"ZA", name_en:"South Africa", name_zh_CN:"南非", itc:"+27-"}, + // {pode:"N716", iso2:"ZW", name_en:"Zimbabwe", name_zh_CN:"津巴布韦", itc:"+263-"}, + // {pode:"N724", iso2:"ES", name_en:"Spain", name_zh_CN:"西班牙", name_native:"España", itc:"+34-"}, + // {pode:"N728", iso2:"SS", name_en:"South Sudan", name_zh_CN:"南苏丹", name_native:"‫جنوب السودان‬‎", itc:"+211-"}, + // {pode:"N729", iso2:"SD", name_en:"Sudan", name_zh_CN:"苏丹", name_native:"‫السودان‬‎", itc:"+249-"}, + // {pode:"N732", iso2:"EH", name_en:"Western Sahara", name_zh_CN:"西撒哈拉", name_native:"‫الصحراء الغربية‬‎", itc:"+212-"}, + // {pode:"N740", iso2:"SR", name_en:"Suriname", name_zh_CN:"苏里南", itc:"+597-"}, + // {pode:"N744", iso2:"SJ", name_en:"Svalbard and Jan Mayen", name_zh_CN:"斯瓦尔巴群岛和扬马延岛", itc:"+47-"}, + // {pode:"N748", iso2:"SZ", name_en:"Swaziland", name_zh_CN:"斯威士兰", itc:"+268-"}, + {pode:"N752", iso2:"SE", name_en:"Sweden", name_zh_CN:"瑞典", name_native:"Sverige", itc:"+46-"}, + {pode:"N756", iso2:"CH", name_en:"Switzerland", name_zh_CN:"瑞士", name_native:"Schweiz", itc:"+41-"}, + // {pode:"N760", iso2:"SY", name_en:"Syria", name_zh_CN:"叙利亚", name_native:"‫سوريا‬‎", itc:"+963-"}, + // {pode:"N762", iso2:"TJ", name_en:"Tajikistan", name_zh_CN:"塔吉克斯坦", itc:"+992-"}, + // {pode:"N764", iso2:"TH", name_en:"Thailand", name_zh_CN:"泰国", name_native:"ไทย", itc:"+66-"}, + // {pode:"N768", iso2:"TG", name_en:"Togo", name_zh_CN:"多哥", itc:"+228-"}, + // {pode:"N772", iso2:"TK", name_en:"Tokelau", name_zh_CN:"托克劳", itc:"+690-"}, + // {pode:"N776", iso2:"TO", name_en:"Tonga", name_zh_CN:"汤加", itc:"+676-"}, + // {pode:"N780", iso2:"TT", name_en:"Trinidad and Tobago", name_zh_CN:"特立尼达和多巴哥", itc:"+1-"}, + // {pode:"N784", iso2:"AE", name_en:"United Arab Emirates", name_zh_CN:"阿联酋", name_native:"‫الإمارات العربية المتحدة‬‎", itc:"+971-"}, + // {pode:"N788", iso2:"TN", name_en:"Tunisia", name_zh_CN:"突尼斯", name_native:"‫تونس‬‎", itc:"+216-"}, + // {pode:"N792", iso2:"TR", name_en:"Turkey", name_zh_CN:"土耳其", name_native:"Türkiye", itc:"+90-"}, + // {pode:"N795", iso2:"TM", name_en:"Turkmenistan", name_zh_CN:"土库曼斯坦", itc:"+993-"}, + // {pode:"N796", iso2:"TC", name_en:"Turks and Caicos Islands", name_zh_CN:"特克斯和凯科斯群岛", itc:"+1-"}, + // {pode:"N798", iso2:"TV", name_en:"Tuvalu", name_zh_CN:"图瓦卢", itc:"+688-"}, + // {pode:"N800", iso2:"UG", name_en:"Uganda", name_zh_CN:"乌干达", itc:"+256-"}, + // {pode:"N804", iso2:"UA", name_en:"Ukraine", name_zh_CN:"乌克兰", name_native:"Україна", itc:"+380-"}, + // {pode:"N807", iso2:"MK", name_en:"Macedonia", name_zh_CN:"马其顿", name_native:"Македонија", itc:"+389-"}, + // {pode:"N818", iso2:"EG", name_en:"Egypt", name_zh_CN:"埃及", name_native:"‫مصر‬‎", itc:"+20-"}, + {pode:"N826", iso2:"GB", name_en:"United Kingdom", name_zh_CN:"英国", itc:"+44-"}, + // {pode:"N831", iso2:"GG", name_en:"Guernsey", name_zh_CN:"根西岛", itc:"+44-"}, + // {pode:"N832", iso2:"JE", name_en:"Jersey", name_zh_CN:"泽西岛", itc:"+44-"}, + // {pode:"N833", iso2:"IM", name_en:"Isle of Man", name_zh_CN:"马恩岛", itc:"+44-"}, + // {pode:"N834", iso2:"TZ", name_en:"Tanzania", name_zh_CN:"坦桑尼亚", itc:"+255-"}, + {pode:"N840", iso2:"US", name_en:"United States", name_zh_CN:"美国", itc:"+1-"}, + // {pode:"N850", iso2:"VI", name_en:"U.S. Virgin Islands", name_zh_CN:"美属维尔京群岛", itc:"+1-"}, + // {pode:"N854", iso2:"BF", name_en:"Burkina Faso", name_zh_CN:"布基纳法索", itc:"+226-"}, + // {pode:"N858", iso2:"UY", name_en:"Uruguay", name_zh_CN:"乌拉圭", itc:"+598-"}, + // {pode:"N860", iso2:"UZ", name_en:"Uzbekistan", name_zh_CN:"乌兹别克斯坦", name_native:"Oʻzbekiston", itc:"+998-"}, + // {pode:"N862", iso2:"VE", name_en:"Venezuela", name_zh_CN:"委内瑞拉", itc:"+58-"}, + // {pode:"N876", iso2:"WF", name_en:"Wallis and Futuna", name_zh_CN:"瓦利斯和富图纳", name_native:"Wallis-et-Futuna", itc:"+681-"}, + // {pode:"N882", iso2:"WS", name_en:"Samoa", name_zh_CN:"萨摩亚", itc:"+685-"}, + // {pode:"N887", iso2:"YE", name_en:"Yemen", name_zh_CN:"也门", name_native:"‫اليمن‬‎", itc:"+967-"}, + // {pode:"N894", iso2:"ZM", name_en:"Zambia", name_zh_CN:"赞比亚", itc:"+260-"} + ] +}; \ No newline at end of file diff --git a/src/utils/messenger.js b/src/utils/messenger.js new file mode 100644 index 0000000..bdb15c0 --- /dev/null +++ b/src/utils/messenger.js @@ -0,0 +1,50 @@ +'use strict'; + +async function sendSmsAliyun (smsClient, phone, msgParam, templateCode, signName) { // msgParam 是消息模板参数对象,例如 { code: "890353" } + let matches = phone.match(/\d+/g); + let smsNumber = matches[0] === '86' ? matches[1] : '00' + matches[0] + matches[1]; + const res = await smsClient.sendSMS({ + PhoneNumbers: smsNumber,//必填:待发送手机号。支持以逗号分隔的形式进行批量调用,批量上限为1000个手机号码,批量调用相对于单条调用及时性稍有延迟,验证码类型的短信推荐使用单条调用的方式;发送国际/港澳台消息时,接收号码格式为00+国际区号+号码,如“0085200000000” + SignName: signName,//必填:短信签名-可在短信控制台中找到 + TemplateCode: templateCode,//必填:短信模板-可在短信控制台中找到,发送国际/港澳台消息时,请使用国际/港澳台短信模版 + TemplateParam: JSON.stringify(msgParam) //可选:模板中的变量替换JSON串,如模板内容为"亲爱的${name},您的验证码为${code}"时。 + }); + if (res && res.Code && res.Code === 'ok') { + return { state: 'DONE' }; + } else { + return { state: 'FAILED', error: res }; + } +} + +module.exports = { + sendMail: function (SysConfig) { + const smtpTransporter = require('nodemailer').createTransport(SysConfig.SMTP); + return async function (option) { // 或者如果smtp参数已经确定,就可以直接定义 sendMail: Bluebird.promisify(Smtp.sendMail).bind(Smtp) + /* + { + from: '"Fred Foo 👻" ', // sender address + to: "bar@example.com, baz@example.com", // list of receivers + subject: "Hello ✔", // Subject line + text: "Hello world?", // plain text body + html: "Hello world?" // html body + } + */ + return await smtpTransporter.sendMail(option); + }; + } + , + sendSms: function (SysConfig) { + const smsClient = new (require('@alicloud/sms-sdk'))({ // 在调用时,才创建 smsClient,防止 SysConfig 还没有建立好。 + accessKeyId: SysConfig.SMS.aliyun.accessKeyId, + secretAccessKey: SysConfig.SMS.aliyun.secretAccessKey + }); + return async function (phone, option = {}) { // 通过option对象,对外提供统一的调用参数格式 + if (/^\+\d+-\d+$/.test(phone)) { + if (option.msgParam && option.templateCode && option.signName) { + return await sendSmsAliyun(smsClient, phone, option.msgParam, option.templateCode, option.signName); + } + } + return null; // 手机号格式错误,或者 option.vendor 错误。 + }; + } +}; \ No newline at end of file