init
This commit is contained in:
19
.eslintrc.json
Normal file
19
.eslintrc.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
node_modules
|
||||||
|
log
|
||||||
|
package-lock.json
|
||||||
|
.vscode
|
||||||
|
configSec.js
|
||||||
|
*test.js
|
||||||
|
build
|
||||||
70
CODE.md
Normal file
70
CODE.md
Normal file
@@ -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
|
||||||
|
|
||||||
5
config.js
Normal file
5
config.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
port: 80,
|
||||||
|
protocol: 'http',
|
||||||
|
host: 'localhost',
|
||||||
|
};
|
||||||
23
index.js
Normal file
23
index.js
Normal file
@@ -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]);
|
||||||
42
logger/index.js
Normal file
42
logger/index.js
Normal file
@@ -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
|
||||||
43
package.json
Normal file
43
package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
30
script.js
Normal file
30
script.js
Normal file
@@ -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 <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} 创建完毕`); });
|
||||||
|
});
|
||||||
|
|
||||||
25
src/app.js
Normal file
25
src/app.js
Normal file
@@ -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;
|
||||||
0
src/common/Auth.js
Normal file
0
src/common/Auth.js
Normal file
64
src/common/Config.js
Normal file
64
src/common/Config.js
Normal file
@@ -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 <type>', 'Database type: mysql|sqlite. ')
|
||||||
|
.option('--dbName <name>', 'Database name')
|
||||||
|
.option('-H, --host <host>', 'Host ip or domain name. ')
|
||||||
|
.option('-P, --protocol <protocol>', 'Server protocol: http|https|httpall. ')
|
||||||
|
.option('-p, --port <port>', 'Server port number.')
|
||||||
|
.option('--sslType <type>', `SSL provider type: file|greenlock`)
|
||||||
|
.option('--sslCert <cert>', 'SSL certificate file. ')
|
||||||
|
.option('--sslKey <key>', 'SSL private key file. ')
|
||||||
|
.option('--sslCA <ca>', '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();
|
||||||
7
src/common/DepMap.js
Normal file
7
src/common/DepMap.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
'use strict';
|
||||||
|
// 在此注册要放在服务容器Provider里的模块
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
SysConfig: require('./Config').config,
|
||||||
|
System: require('../modules/system/system.controller').provider
|
||||||
|
};
|
||||||
56
src/common/Provider.js
Normal file
56
src/common/Provider.js
Normal file
@@ -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),
|
||||||
|
};
|
||||||
31
src/common/dbConnect.js
Normal file
31
src/common/dbConnect.js
Normal file
@@ -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;
|
||||||
|
});
|
||||||
19
src/jobs/index.js
Normal file
19
src/jobs/index.js
Normal file
@@ -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('任务队列启动...');
|
||||||
|
});
|
||||||
126
src/jobs/jobs.js
Normal file
126
src/jobs/jobs.js
Normal file
@@ -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);
|
||||||
106
src/modules/action/action.handler.js
Normal file
106
src/modules/action/action.handler.js
Normal file
@@ -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,
|
||||||
|
};
|
||||||
48
src/modules/action/action.model.js
Normal file
48
src/modules/action/action.model.js
Normal file
@@ -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;
|
||||||
104
src/modules/estate/estate.controller.js
Normal file
104
src/modules/estate/estate.controller.js
Normal file
@@ -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()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
102
src/modules/estate/estate.model.js
Normal file
102
src/modules/estate/estate.model.js
Normal file
@@ -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;
|
||||||
31
src/modules/estate/estate.service.js
Normal file
31
src/modules/estate/estate.service.js
Normal file
@@ -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;
|
||||||
|
});
|
||||||
60
src/modules/estateOrder/estateOrder.controller.js
Normal file
60
src/modules/estateOrder/estateOrder.controller.js
Normal file
@@ -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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
73
src/modules/estateOrder/estateOrder.handler.js
Normal file
73
src/modules/estateOrder/estateOrder.handler.js
Normal file
@@ -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) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
58
src/modules/estateOrder/estateOrder.model.js
Normal file
58
src/modules/estateOrder/estateOrder.model.js
Normal file
@@ -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;
|
||||||
25
src/modules/estateOrder/estateOrder.service.js
Normal file
25
src/modules/estateOrder/estateOrder.service.js
Normal file
@@ -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;
|
||||||
|
});
|
||||||
141
src/modules/fund/fund.controller.js
Normal file
141
src/modules/fund/fund.controller.js
Normal file
@@ -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
|
||||||
|
});
|
||||||
|
};
|
||||||
|
})
|
||||||
|
|
||||||
|
};
|
||||||
36
src/modules/fund/fund.service.js
Normal file
36
src/modules/fund/fund.service.js
Normal file
@@ -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;
|
||||||
|
});
|
||||||
5
src/modules/market/market.controller.js
Normal file
5
src/modules/market/market.controller.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
const { inject } = require('../../common/Provider');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
|
||||||
|
};
|
||||||
22
src/modules/market/market.model.js
Normal file
22
src/modules/market/market.model.js
Normal file
@@ -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;
|
||||||
11
src/modules/market/market.service.js
Normal file
11
src/modules/market/market.service.js
Normal file
@@ -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;
|
||||||
|
});
|
||||||
89
src/modules/msgCode/msgCode.controller.js
Normal file
89
src/modules/msgCode/msgCode.controller.js
Normal file
@@ -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;
|
||||||
|
},
|
||||||
|
};
|
||||||
33
src/modules/msgCode/msgCode.model.js
Normal file
33
src/modules/msgCode/msgCode.model.js
Normal file
@@ -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;
|
||||||
108
src/modules/otcAds/otcAds.controller.js
Normal file
108
src/modules/otcAds/otcAds.controller.js
Normal file
@@ -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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
55
src/modules/otcAds/otcAds.model.js
Normal file
55
src/modules/otcAds/otcAds.model.js
Normal file
@@ -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;
|
||||||
30
src/modules/otcAds/otcAds.service.js
Normal file
30
src/modules/otcAds/otcAds.service.js
Normal file
@@ -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;
|
||||||
|
});
|
||||||
87
src/modules/otcOrder/otcOrder.controller.js
Normal file
87
src/modules/otcOrder/otcOrder.controller.js
Normal file
@@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
100
src/modules/otcOrder/otcOrder.handler.js
Normal file
100
src/modules/otcOrder/otcOrder.handler.js
Normal file
@@ -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.
|
||||||
|
*/
|
||||||
56
src/modules/otcOrder/otcOrder.model.js
Normal file
56
src/modules/otcOrder/otcOrder.model.js
Normal file
@@ -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;
|
||||||
30
src/modules/otcOrder/otcOrder.service.js
Normal file
30
src/modules/otcOrder/otcOrder.service.js
Normal file
@@ -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;
|
||||||
|
});
|
||||||
204
src/modules/sign/sign.controller.js
Normal file
204
src/modules/sign/sign.controller.js
Normal file
@@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
40
src/modules/sign/sign.service.js
Normal file
40
src/modules/sign/sign.service.js
Normal file
@@ -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;
|
||||||
|
});
|
||||||
81
src/modules/sysBoard/sysBoard.controller.js
Normal file
81
src/modules/sysBoard/sysBoard.controller.js
Normal file
@@ -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)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
31
src/modules/sysBoard/sysBoard.model.js
Normal file
31
src/modules/sysBoard/sysBoard.model.js
Normal file
@@ -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;
|
||||||
37
src/modules/sysBoard/sysBoard.service.js
Normal file
37
src/modules/sysBoard/sysBoard.service.js
Normal file
@@ -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;
|
||||||
|
});
|
||||||
90
src/modules/system/system.controller.js
Normal file
90
src/modules/system/system.controller.js
Normal file
@@ -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"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
26
src/modules/system/system.model.js
Normal file
26
src/modules/system/system.model.js
Normal file
@@ -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;
|
||||||
25
src/modules/system/system.service.js
Normal file
25
src/modules/system/system.service.js
Normal file
@@ -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;
|
||||||
|
});
|
||||||
58
src/modules/ticket/ticket.controller.js
Normal file
58
src/modules/ticket/ticket.controller.js
Normal file
@@ -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)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
43
src/modules/ticket/ticket.handler.js
Normal file
43
src/modules/ticket/ticket.handler.js
Normal file
@@ -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
|
||||||
|
};
|
||||||
58
src/modules/ticket/ticket.model.js
Normal file
58
src/modules/ticket/ticket.model.js
Normal file
@@ -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;
|
||||||
27
src/modules/ticket/ticket.service.js
Normal file
27
src/modules/ticket/ticket.service.js
Normal file
@@ -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;
|
||||||
|
});
|
||||||
71
src/modules/user/user.controller.js
Normal file
71
src/modules/user/user.controller.js
Normal file
@@ -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)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
88
src/modules/user/user.model.js
Normal file
88
src/modules/user/user.model.js
Normal file
@@ -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;
|
||||||
30
src/modules/user/user.service.js
Normal file
30
src/modules/user/user.service.js
Normal file
@@ -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;
|
||||||
|
});
|
||||||
12
src/routes/index.js
Normal file
12
src/routes/index.js
Normal file
@@ -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;
|
||||||
|
};
|
||||||
71
src/server.js
Normal file
71
src/server.js
Normal file
@@ -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;
|
||||||
|
});
|
||||||
53
src/service/index.js
Normal file
53
src/service/index.js
Normal file
@@ -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();
|
||||||
27
src/service/middleware.js
Normal file
27
src/service/middleware.js
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
};
|
||||||
20
src/service/serviceMap.js
Normal file
20
src/service/serviceMap.js
Normal file
@@ -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
|
||||||
|
};
|
||||||
41
src/utils/account.js
Normal file
41
src/utils/account.js
Normal file
@@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
323
src/utils/crypto.js
Normal file
323
src/utils/crypto.js
Normal file
@@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
38
src/utils/index.js
Normal file
38
src/utils/index.js
Normal file
@@ -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
|
||||||
|
};
|
||||||
30
src/utils/jwt.js
Normal file
30
src/utils/jwt.js
Normal file
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
262
src/utils/locale.js
Executable file
262
src/utils/locale.js
Executable file
@@ -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-"}
|
||||||
|
]
|
||||||
|
};
|
||||||
50
src/utils/messenger.js
Normal file
50
src/utils/messenger.js
Normal file
@@ -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 👻" <foo@example.com>', // sender address
|
||||||
|
to: "bar@example.com, baz@example.com", // list of receivers
|
||||||
|
subject: "Hello ✔", // Subject line
|
||||||
|
text: "Hello world?", // plain text body
|
||||||
|
html: "<b>Hello world?</b>" // 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 错误。
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user