This commit is contained in:
chaosBreaking
2019-08-31 14:59:13 +08:00
commit dc5423d356
65 changed files with 3813 additions and 0 deletions

19
.eslintrc.json Normal file
View 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
View File

@@ -0,0 +1,7 @@
node_modules
log
package-lock.json
.vscode
configSec.js
*test.js
build

70
CODE.md Normal file
View 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
View File

@@ -0,0 +1,5 @@
module.exports = {
port: 80,
protocol: 'http',
host: 'localhost',
};

23
index.js Normal file
View 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
View 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
View 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
View 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
View 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
View File

64
src/common/Config.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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);

View 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,
};

View 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;

View 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()
});
}
};

View 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;

View 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;
});

View 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
});
});
}
};

View 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) };
}
}
};

View 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;

View 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;
});

View 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
});
};
})
};

View 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;
});

View File

@@ -0,0 +1,5 @@
const { inject } = require('../../common/Provider');
module.exports = {
};

View 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;

View 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;
});

View 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;
},
};

View 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;

View 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
});
});
}
};

View 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;

View 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;
});

View 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,
});
}
}
};

View 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.
*/

View 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;

View 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;
});

View 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
});
}
};

View 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;
});

View 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)
});
});
});
},
};

View 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;

View 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;
});

View 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"
});
}
}
};

View 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;

View 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;
});

View 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)
});
});
}
};

View 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
};

View 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;

View 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;
});

View 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)
});
});
}
};

View 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;

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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 错误。
};
}
};