UNPKG

@bxjs/base

Version:

1,662 lines (1,398 loc) 132 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); require('reflect-metadata'); var BCRYPTJS = require('bcryptjs'); var __F__ = require('lodash'); var uuid = require('uuid'); var typeorm = require('typeorm'); var PATH = require('path'); var FS = require('fs-extra'); var QS = require('querystring'); var VIEW = require('nunjucks'); var FS$1 = require('fs'); var classValidator = require('class-validator'); var CRON_PARSER = require('cron-parser'); var _FC2 = require('@alicloud/fc2'); var classTransformer = require('class-transformer'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } function _interopNamespace(e) { if (e && e.__esModule) return e; var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n['default'] = e; return Object.freeze(n); } var __F____namespace = /*#__PURE__*/_interopNamespace(__F__); var _FC2__default = /*#__PURE__*/_interopDefaultLegacy(_FC2); // 框架依赖的环境变量前缀条件的各种判断统一处理拦截 =》TODO 移到web层进行封装处理 if (!process.env.NODE_ENV) { process.env.NODE_ENV = 'localhost'; } switch (process.env.NODE_ENV) { // 遵循NodeJS的开发约定习惯保持一致性 case 'localhost': case 'development': case 'prerelease': case 'production': case 'test': break; default: throw new Error(`【ERROR】运行态环境变量NODE_ENV[${process.env.NODE_ENV}]配置错误`); } global['__BXJS_CONFIG__'] = undefined; global['xconfig'] = (key, defaultValue) => { if (!global['__BXJS_CONFIG__']) { let target = require(`config/config.${process.env.NODE_ENV}.js`); let base = require('config/config.js'); global['__BXJS_CONFIG__'] = xassign({}, base, target); // FIXME: 线下兼容微应用从环境变量中获取APPID而不是从配置中获取APPID这样支持过渡版本的appid获取。 if (process.env.APPID && !F.get(global['__BXJS_CONFIG__'], 'framework.appid')) { F.set(global['__BXJS_CONFIG__'], 'framework.appid', process.env.APPID); } } return F.get(global['__BXJS_CONFIG__'], key); }; const CRYPTO = require('crypto'); const algorithm = 'aes-256-ctr'; function aes256_encrypt(key, data) { let sha256 = CRYPTO.createHash('sha256'); sha256.update(key); let iv = CRYPTO.randomBytes(16), plaintext = Buffer.from(data), cipher = CRYPTO.createCipheriv(algorithm, sha256.digest(), iv), ciphertext = cipher.update(plaintext); ciphertext = Buffer.concat([iv, ciphertext, cipher.final()]); return ciphertext.toString('base64'); } function aes256_decrypt(key, data) { const sha256 = CRYPTO.createHash('sha256'); sha256.update(key); let input = Buffer.from(data, 'base64'), iv = input.slice(0, 16), ciphertext = input.slice(16), decipher = CRYPTO.createDecipheriv(algorithm, sha256.digest(), iv), plaintext = decipher.update(ciphertext); plaintext += decipher.final(); return plaintext; } // 密码管理模块(基于bcrypt库封装,更高级的HASH算法用于保存密码信息,加密算法是不可逆的) function password_encrypt(content) { return BCRYPTJS.hashSync(content, BCRYPTJS.genSaltSync(10)); } // 比较输入的密码内容与数据库中加密的内容是否一致 function password_compare(content, encrypted_content) { return BCRYPTJS.compareSync(content, encrypted_content); } // https://www.liaoxuefeng.com/wiki/1022910821149312/1023025778520640 // nodejs 加解密的各种实现 const CRYPTO$1 = require('crypto'); // 私钥加密 function rsa_private_encrypt(private_key, content) { let out = CRYPTO$1.privateEncrypt(private_key, Buffer.from(content, 'utf8')); return out.toString('base64'); } // 公钥解密 function rsa_public_decrypt(public_key, content) { let out = CRYPTO$1.publicDecrypt(public_key, Buffer.from(content, 'base64')); return out; } //////////////////////////////////// // 公钥加密 function rsa_public_encrypt(public_key, content) { let out = CRYPTO$1.publicEncrypt(public_key, Buffer.from(content, 'utf8')); return out.toString('base64'); } // 私钥解密 function rsa_private_decrypt(private_key, content) { let out = CRYPTO$1.privateDecrypt(private_key, Buffer.from(content, 'base64')); return out; } // let key = xconfig(`application.app.${appid}.private_key`) // xassert(key) // // 从文件加载key: // function loadKey(file) { // // key实际上就是PEM编码的字符串: // return FS.readFileSync(file, 'utf8'); // } // let // prvKey = loadKey('./rsa-prv.pem'), // pubKey = loadKey('./rsa-pub.pem'), // message = 'Hello, world!'; // // 使用私钥加密: // let enc_by_prv = CRYPTO.privateEncrypt(prvKey, Buffer.from(message, 'utf8')); // console.log('encrypted by private key: ' + enc_by_prv.toString('hex')); // let dec_by_pub = CRYPTO.publicDecrypt(pubKey, enc_by_prv); // console.log('decrypted by public key: ' + dec_by_pub.toString('utf8')); // // 使用公钥加密: // let enc_by_pub = CRYPTO.publicEncrypt(pubKey, Buffer.from(message, 'utf8')); // console.log('encrypted by public key: ' + enc_by_pub.toString('hex')); // // 使用私钥解密: // let dec_by_prv = CRYPTO.privateDecrypt(prvKey, enc_by_pub); // console.log('decrypted by private key: ' + dec_by_prv.toString('utf8')); global['xcrypto'] = { // TODO: MD5、SHA256,更多常见签名都集成进来方便业务上开发使用各种API对接使用。 aes256_encrypt, aes256_decrypt, password_encrypt, password_compare, rsa_public_encrypt, rsa_public_decrypt, rsa_private_encrypt, rsa_private_decrypt }; global['F'] = __F__.cloneDeep(__F____namespace); // 避免篡改内容的时候出现第三方库依赖的冲突问题 // ==》》 解决lodash库中定义非常有歧义的地方,改进为更加直观自然的用法理解实现。 global['F']['isEmpty'] = expr => { if (!expr) { return true; } else if (global['F'].isArray(expr) && expr.length === 0) { return true; } else if (global['F'].isPlainObject(expr) && __F__.isEmpty(expr)) { return true; } else { return false; } }; // 将对象数组中的指定字段返回到前端,用于将服务端暴露必要字段到前端,减少安全隐患问题。 global['F']['pluck'] = function (items, keys) { if (!items || !keys) { return []; } return __F__.map(items !== null && items !== void 0 ? items : [], v => __F__.pick(v, keys !== null && keys !== void 0 ? keys : [])); }; global['F']['pick'] = function (items, keys) { if (!items || !keys) { return {}; } return __F__.pick(items, keys !== null && keys !== void 0 ? keys : []); }; const moment = require('moment-timezone'); // import * as _moment from 'moment-timezone' // 设置默认时区为北京时间 moment.tz.setDefault('Asia/Shanghai'); global['xdate'] = input => { return moment(input); }; function xuuid$1() { // 还是用uuid4的随机数进行实现比较合适 return uuid.v4().replace(/\-/g, ''); // 改进npm包中的uuid v1时间序列算法确保唯一性和随机性可以稳定可靠的应用于serverless分布式高并发应用场景下 // 改进基于时间序列的UUID算法确保serverless分布式并发场景下的全局唯一性以及使用密码机随机性改进不可预测性 // return require('uuid/v1')({ nsecs: Math.floor(Math.random() * 10000) }).replace(/\-/g, '') // 优化算法中的100纳秒时间为随机数,进一步降低冲突概率。 // 冲突概率分析: // 1秒支撑1000并发(毫秒时间戳精度保证), // 1000并发中相同毫秒时间的并发,再通过10000百纳秒随机数进行区分避免冲突, // 不同机器上的并发时间戳可能会有偏移导致重复偏差时间(通过6个字节的node id随机数区分机器,2字节0x3fff随机数区分clockseq) // 冲突概率基本上可以保证忽略不计避免shortid算法高并发冲突的缺陷 // return require('uuid/v1')({ nsecs: Math.floor(Math.random() * 10000) }).replace(/\-/g, '') ////////////////////////////////////////////////////////////////////////////////////// // 考虑到serverless实际环境并不是合适node id使用mac地址,直接使用默认的8字节随机数替代更模拟更合适(node id + clockseq)。 // // 封装正确的uuid实现算法确保唯一性(使用6个字节的机器mac地址,确保分布式机器算法执行的唯一性) // const mac = await __xgetmac__() // // 将字符串转换为buffer二进制处理 // let buf = [] // // MAC地址格式 ##:##:##:##:##:## // const values = mac.split(':') // xassert(values && values.length == 6) // for (let i = 0; i < 6; ++i) { // const tmpByte = parseInt(values[i], 16); // buf.push(tmpByte) // } // // 以mac地址作为机器唯一标识确保正确性 // const uuid = require('uuid/v1')({node: buf}) // return uuid; } const SLEEP_ASYNC = require('sleep-async')(); async function xsleep$1(ms = -1) { const MAX = 50 * 365 * 24 * 3600 * 1000; if (ms < 0) { ms = MAX; // 50年最大数视为永久阻塞方便断点单步调试问题 } else if (ms >= MAX) { ms = MAX; } return new Promise((resolve, reject) => { try { SLEEP_ASYNC.sleep(ms, () => { resolve(); }); } catch (err) { resolve(); } }); } // 深度拷贝功能封装(解决配置参数合并类似问题),规避Object.assign不支持深度拷贝缺陷。 // 最后一个参数如果为boolean表示是否数组合并,默认值为false数组不合并直接覆盖。 function xassign$1(target, source, ...args) { // console.log('统一处理开始:', args) // console.log('统一处理开始1111:', args[args.length - 1]) const is_concat = !args || !__F__.isBoolean(args[args.length - 1]) ? false : args[args.length - 1]; if (args && __F__.isBoolean(args[args.length - 1])) { args.pop(); } function customize(objValue, srcValue) { if (__F__.isArray(objValue)) { if (is_concat) { return objValue.concat(srcValue); } else { return srcValue; } } } const param = [source, ...args]; let out = target; for (let i = 0; i < param.length; i++) { out = __F__.mergeWith(out, param[i], customize); } // console.log('参数合并返回结果:', out) return out; } /// <reference path="../../typings/index.d.ts" /> global['xuuid'] = xuuid$1; global['xsleep'] = xsleep$1; global['xassign'] = xassign$1; // import { xos } from './xos' // global['xos'] = xos as any // TODO 需要改进为i18n类似的错误码的翻译处理成为配置数据的一部分 // 从前端视角约定的各种错误类型抽象(不用考虑具体的错误子类型, // 子类型由xthrow的param参数进行具体的表达, // 可以覆盖msg和code的提示信息扩展子类型进行约定即可 param:{[code,msg要求多语言配置,]other....}。) // 在框架层面上将各种错误类型进行统一的抽象并在前端进行正确的错误信息的详细展示以及逻辑判定处理。 // 0无数据(null or undefined,期望接口中获取到的数据结果却不存在导致后续的逻辑无法继续下去), // 1无权限(503 无访问权限), // 2空页面 (404网址不存在类型错误), // 4搜索无结果(searchKeyWord by opensearch、database、cache、other data container), // 5未知错误(500 服务器内部错误请稍后重试,未被定义的错误类型的错误被拦截) // 3网页请求错误(对应于PARAM类型的错误), // --------------------------------- // 页面配置数据获取失败导致错误页面无法正常加载需要出现空白页的错误提示纯粹的html处理显示,使用框架自定义的默认页面进行提示 // 对应到前端的错误页面显示模板内容提示信息 // - globals = { // errorType: 0, // errorMessage: 'XXXXXXXXX', // errorStack: 'XXXXXXXXX', // i18n: ... // }; // errorType: 使用的图标不同而已,0无数据,1无权限,2空页面,3网页请求错误,4搜索无结果,5未知错误, // errorMessage:显示给用户的错误信息 // errorStack:debug用的错误信息,快速点击页面7次显示出来 // 错误值的多语言翻译注册表 global['ERRORS'] = {}; // 错误码定义为ascii的utf8字符串作为唯一标识前端定义(前端code为int是0表示成功过,非0的int或string表示错误码) global['ERR$TODO'] = 'ERR$TODO'; global['ERRORS']['ERR$TODO'] = { 'zh': '暂未实现错误', 'en': 'todo error' }; global['ERR$UNKNOWN'] = 'ERR$UNKNOWN'; global['ERRORS']['ERR$UNKNOWN'] = { 'zh': '未知错误', 'en': 'unknown error' }; global['ERR$ASSERT'] = 'ERR$ASSERT'; global['ERRORS']['ERR$ASSERT'] = { 'zh': '断言错误', 'en': 'assert error' }; global['ERR$PARAM'] = 'ERR$PARAM'; global['ERRORS']['ERR$PARAM'] = { 'zh': '参数错误', 'en': 'parameters error' }; // 各个系统内部模块启动初始化的时候检查用户传入的配置参数如果出错会报此错误 global['ERR$CONFIG'] = 'ERR$CONFIG'; global['ERRORS']['ERR$CONFIG'] = { 'zh': '配置信息错误', 'en': 'config info error' }; // 需要登录用户才能使用的功能,游客无法请求的控制器接口访问,一般在框架上会自动拦截掉此类错误,只需要正确配置即可。 global['ERR$UNAUTHORIZED'] = 'ERR$UNAUTHORIZED'; global['ERRORS']['ERR$UNAUTHORIZED'] = { 'zh': '未授权请重新登录', 'en': 'unauthorized, please login again' }; // 登录的用户需要访问的资源缺少授权权限(菜单权限、数据权限、路由权限,主要是这三种权限数据) global['ERR$FORBIDDEN'] = 'ERR$FORBIDDEN'; global['ERRORS']['ERR$FORBIDDEN'] = { 'zh': '无权限错误', 'en': 'no right error' }; const ErrorStackParser = require('error-stack-parser'); const circular_json = require('circular-json'); // // 准确定位错误码位置,间接得到函数调用位置地址信息,结合符号报表的正确解析处理完美得到错误定位信息,准确代码调试。 // export function __get_base_func_caller_source_position(position: number = 3) { // try { // throw new Error() // } catch (err) { // const out = ErrorStackParser.parse(err) // let idx = 0 // // 找到第二个TS文件的执行位置 // let find_ts_suffix_file_count = 0 // for (; idx < out.length; idx++) { // if (/\.ts$/.test(out[idx].fileName)) { // find_ts_suffix_file_count += 1 // } // if (find_ts_suffix_file_count === position) { // break // } // } // if (find_ts_suffix_file_count === position) { // return '[' + out[idx]['fileName'] + ':' + out[idx]['lineNumber'] + ']' // } else { // // TODO 需要定位为什么调用栈无法找到对应的位置出现越界?? // // console.error(err) // console.error(circular_json.stringify(out, null, 4) // .replace(/\r/g, '').replace(/\n/g, '')) // return '#' // } // } // } // 得到断言发生位置的所有ts源码文件的调用栈(忽略掉非源码部分webpack自动生成的并非业务中的信息,这部分信息需要在调试后台日志中可以拦截处理) function __get_base_func_caller_source_stack() { try { throw new Error(); } catch (err) { let stack = ErrorStackParser.parse(err); stack.shift(); stack.shift(); // 解析错误栈信息并且仅仅解析出纯粹的业务代码错误栈信息方便业务上准确查看错误栈信息(框架暂时不稳定全部透传出来方便业务代码调试) // stack = stack.filter(v => /\.ts$/.test(v.fileName) && !(/\/\@bxjs\//.test(v.fileName))) const out = []; for (const v of stack) { out.push(v.fileName + ':' + v.lineNumber); // out.push(v.fileName + ':' + v.lineNumber + (v.functionName ? ':' + v.functionName : '')) } return out; } } function xassert$1(expr, code = 'ERR$ASSERT', param) { if (F.isEmpty(expr)) { const stack = __get_base_func_caller_source_stack(); throw new Error(JSON.stringify({ code, msg: global['ERRORS'][code]['zh'], param, stack })); } return expr; } // 获取异常调用栈用于辅助错误提示定位 function xstack(err, compact = true) { xassert$1(err instanceof Error); // 解析错误栈信息并且仅仅解析出纯粹的业务代码错误栈信息方便业务上准确查看错误栈信息 let stack = ErrorStackParser.parse(err); if (compact) { stack = stack.filter(v => /\.ts$/.test(v.fileName) && !/\/(\@malagu|\@bxjs|bxjs_.+)\//.test(v.fileName)); } const out = []; for (const v of stack) { out.push(v.fileName + ':' + v.lineNumber); // out.push(v.fileName + ':' + v.lineNumber + (v.functionName ? ':' + v.functionName : '')) } return out; } // // 错误栈的递归嵌套格式显示数据结构定义(param嵌套找到最后一个msg的JSON解析语法错误就是错误链的原始错误发生位置) // let x = { // "code": "UNKNOWN", // "msg": "未知错误", // "param": { // "msg": "您输入的用户名或密码错误,请重新登录 (ErrorCode: 1005, url: https://login.alibaba-inc.com/authorize/login.do)" // }, // "stack": "[ \"/Users/chujinghui/Desktop/work/xjs/bxjs/framework/base.ts:110\", // \"/Users/chujinghui/Desktop/work/xjs/bxjs/app/entries/web/mobile/meeting-room-visit.ts:161\", // \"/Users/chujinghui/Desktop/work/xjs/bxjs/app/entries/web/mobile/meeting-room-visit.js:40\", // \"/Users/chujinghui/Desktop/work/xjs/bxjs/app/entries/web/mobile/meeting-room-visit.js:21\", // \"/Users/chujinghui/Desktop/work/xjs/bxjs/app/entries/web/mobile/meeting-room-visit.js:13\", // \"internal/process/next_tick.js:188\"]", // } // 对于异常内容的格式化参数解析处理成为四元组code/msg/param/stack function xerror$1(err, __param) { xassert$1(err instanceof Error || F.isString(err)); if (err instanceof Error) { try { // 标准错误的统一转换处理 const data = JSON.parse(err.message); if (data.code && data.msg && global['ERRORS'][data.code]) { if (!data.param) { data.param = __param; } return data; } } catch (err) {// 忽略非标准错误解析 } // 非标准错误的统一格式转换处理 const msg = global['ERRORS']['ERR$UNKNOWN']['zh']; // TODO 错误码多语言回传到客户端问题 const code = 'ERR$UNKNOWN'; const param = { code: undefined, msg: err.message, param: __param, stack: undefined, err: err }; // 用户自定义的错误参数信息 msg为非错误码JSON四元组就是嵌套的终止条件。 const stack = xstack(err, true); return { msg, code, param, stack }; } else { const msg = global['ERRORS'][err]['zh']; // TODO 错误码多语言回传到客户端问题 const code = err; const param = undefined; const stack = xstack(new Error(), true); return { msg, code, param, stack }; } } // 用于获取错误栈的root cause根本原因(第一个被拦截的错误发生位置) function xroot(err) { xassert$1(err instanceof Error); let { msg, param, code, stack } = xerror$1(err); // 递归遍历找到错误链的root cause for (; param && param.msg;) { try { const json = JSON.parse(param.msg); param = json.param; } catch (err) { msg = param.msg; code = param.code; stack = param.stack; param = param.param; break; } } return { msg, code, param, stack }; } // TODO 报错处理(显示问题反馈联系人信息) // 将未处理的错误上抛的异常链记录下来用于精准追踪代码的执行过程(以及准确获取到根节点的错误码) // 对于promise异步回调的统一出错处理写法实例 // export function login(username: string, password: string) { // return new Promise((resolve, reject) => { // co(function* () { // let user = yield buc.oauthclient.login(username, password) // resolve(user) // }).catch(async function (err) { // xthrow(err, reject) // }) // }) // } function xthrow$1(code = 'ERR$UNKNOWN', param = undefined) { xassert$1(code instanceof Error || F.isString(code) || F.isObject(code)); let data = {}; let is_init = true; // 是否是原始传递的根错误(非根错误需要进行错误栈的压栈处理) let is_reset_code = false; if (F.isString(code) && global['ERRORS'][code] !== undefined) { // 对于常量定义错误的统一格式化规范处理(框架规范的标准错误转义处理) const stack = __get_base_func_caller_source_stack(); data = { code, msg: global['ERRORS'][code]['zh'], param, stack }; // TODO 多语言处理(后续项目遇到了再优化) if (param instanceof Error) { is_init = false; is_reset_code = true; data = xerror$1(param); // 处理错误上抛的过程中修正错误码传递信息 } } else if (code instanceof Error) { // 对于上抛错误或者原始错误的统一格式化规范处理 try { data = JSON.parse(code.message); is_init = false; // 是标准错误被二次传递(需要进行传递路径的压栈追踪处理) } catch (err) { // 非标准错误(判断特征是message字段无法正确进行json序列化解析) data = xerror$1(code); // 将非标准错误转换为标准错误后再上抛处理 } } else { // 兼容第三方原生库抛出的非Error或非法string类型的异常错误(对第三方库的异常做框架层面统一封装) const stack = __get_base_func_caller_source_stack(); if (F.isString(code)) { param = { ...param, message: code }; } else { param = { ...param, ...code }; } code = ERR$UNKNOWN; data = { code, msg: global['ERRORS'][code]['zh'], param, stack }; // 将透传上抛的错误的路径信息和附加参数也记录下来方便提供完整应用堆栈信息辅助调试业务逻辑 } // 对于标准错误进行压栈处理(每次xthrow都是错误栈的一次压栈,在param参数中进行递归处理) // 每次xthrow转发都是一次异常处理需要记录转发路径的调用路径 if (!is_init) { const stack = __get_base_func_caller_source_stack(); const list = F.clone(stack).reverse(); xassert$1(F.isArray(data.stack)); let level = 0; for (let i = data.stack.length - 1; i >= 0; i--, level++) { if (list.indexOf(data.stack[i]) < 0) { break; } } for (; level > 0; level--) { data.stack.pop(); } // 压栈前需要过滤掉重复路径信息方便查看信息 data.param = F.cloneDeep(data); data.stack = stack; } // 重置当前被修复覆盖的错误码,用于xthrow(ERR$XXX,err)改写错误的上抛处理场景实现。 if (is_reset_code) { data.code = code; data.msg = global['ERRORS'][code]['zh']; } // 上抛错误异常传递 throw new Error(circular_json.stringify(data)); } // 对于promise原生方法的底层异步方法调用的封装实现(解决三方callback嵌套问题) function xpromise$1(callback) { // 补上对应的调用栈调试信息,解决callback中的异常信息无法对上层调用路径的透传问题,有效进行第三方异步接口的同步化封装实现在service模块中第三方集成中大量使用。 const stack = __get_base_func_caller_source_stack(); stack.shift(); // 去掉当前错误信息,避免与callback出错时候的调用栈重复,除此之外的信息Promise回调中无法正常识别调用栈关系。 return new Promise(async (resolve, reject) => { try { await callback(out => { resolve(out); }, err => { const info = xerror$1(err); info.stack = [...info.stack, ...stack]; reject(new Error(JSON.stringify(info))); }); } catch (err) { const info = xerror$1(err); info.stack = [...info.stack, ...stack]; return reject(new Error(JSON.stringify(info))); } }); } // TODO 改进调用位置为xcall执行的错误发生位置(精准定位错误发生行号) async function xcall(func, error_code_or_error_callback) { try { return await func; } catch (err) { if (!error_code_or_error_callback) { // 对错误进行转义直接上抛处理 xthrow$1(err); } else if (F.isString(error_code_or_error_callback)) { // 用户自己定义错误码转义上抛处理 xthrow$1(error_code_or_error_callback, { err }); } else if (F.isFunction(error_code_or_error_callback)) { // 出错了在回调中进一步优化处理,避免try/catch的嵌套 await error_code_or_error_callback(err); } else { xassert$1(ERR$PARAM); } } } const AXIOS = require('axios'); const CRYPTO$2 = require('crypto'); global['xalert'] = dingding_notify; // 统一的报警信息透传(添加上请求上下文信息的获取) async function dingding_notify(error) { try { var _process$env$APP_NAME; let out; if (error instanceof Error) { // 四元组解析 out = xerror(error); } else { // 报警信息透传 out = error; } const warn = { app_name: (_process$env$APP_NAME = process.env.APP_NAME) !== null && _process$env$APP_NAME !== void 0 ? _process$env$APP_NAME : undefined, // FIXME: 打印出环境变量中的应用名信息方便定位错误是哪个应用发出的线上调试错误 alert: out, timestamp: xdate().toLocaleString(), ...get_context_header() }; if (!(error instanceof Error)) { warn['stack'] = __get_base_func_caller_source_stack(); } const cfg = xconfig('framework.dingding-alert'); const SECRET = cfg.secret; const WEBHOOK = cfg.webhook; const timestamp = new Date().getTime(); const signature = timestamp + '\n' + SECRET; const sign = CRYPTO$2.createHmac('sha256', SECRET).update(signature).digest('base64'); const at = {}; at['atMobiles'] = []; // 开发环境仅仅错误消息提示,数组为手机号指定通知相关开发者 if (is_production_env()) { at['isAtAll'] = true; // 生产环境通知群上关注者及时错误响应判定是否是自己问题 @所有人 } await http_post(`${WEBHOOK}&timestamp=${timestamp}&sign=${sign}`, { msgtype: 'text', text: { content: JSON.stringify(warn, null, 4) // 具体的调试通知数据内容 }, at }); } catch (err) {// 失败就忽略消息通知无需进行错误兼容处理 } } function get_context_header() { try { return xcontext.get('__BXJS_REQUEST_BASE_INFO__', undefined); } catch (err) { return undefined; } } function is_production_env() { return process.env.NODE_ENV == 'production'; } async function http_post(url, param) { // TODO 线上测试不稳定超时暂时忽略掉通过进程最大运行时间去控制超时失败 try { let res = await AXIOS({ method: 'POST', url: url, data: param ? JSON.stringify(param) : {}, headers: { 'Content-Type': 'application/json;charset=UTF-8' }, timeout: 1000 // 默认5秒超时接口返回避免僵死 }); return res === null || res === void 0 ? void 0 : res.data; } catch (err) {// 忽略请求错误无需响应在通知场景下(发送太频繁钉钉服务器会阻塞导致超时问题需要测试响应!一分钟20秒消息最大发送量限制,仅仅用于异常报警情况。) } } /// <reference path="../../typings/index.d.ts" /> global['xassert'] = xassert$1; global['xthrow'] = xthrow$1; global['xstack'] = xstack; global['xerror'] = xerror$1; global['xroot'] = xroot; global['xpromise'] = xpromise$1; global['xcall'] = xcall; const cls = require('cls-hooked'); // MAC上使用uuidgen命令行工具生成uuid唯一的命名空间标识符定义防止冲突(模块中的定义与第三方包中的定义避免冲突) const nsid = '4C7ABFD1-2D59-4D4E-949D-4EAE709483AD'; const ns = cls.createNamespace(nsid); function is_valid_key(key) { if (/^_/.test(key)) { switch (key) { // 框架内部白名单KEY的统一定义 case '__BXJS_DATABASE_TRANSACTION_LEVEL__': case '__BXJS_DATABASE_CURRENT_CONNECTION_QUERY_RUNNER__': case '__BXJS_COOKIE__': case '__BXJS_SESSION_ID__': case '__BXJS_USER_ID__': case '__BXJS_REDIRECT_URL__': case '__BXJS_REQUEST_BASE_INFO__': case '__BXJS_REQUEST__': break; default: return false; } } return true; } let xcontext$1; (function (_xcontext) { async function run(callback) { return new Promise(ok => { ns.run(async () => { await callback(); ok(); }); }); } _xcontext.run = run; function get(key, defaultValue) { xassert(is_valid_key(key), ERR$PARAM); let out = ns.get(key); if (out === undefined) { out = defaultValue; } return out; } _xcontext.get = get; function set(key, value) { xassert(is_valid_key(key), ERR$PARAM); return ns.set(key, value); } _xcontext.set = set; })(xcontext$1 || (xcontext$1 = {})); global['xcontext'] = xcontext$1; function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _initializerDefineProperty(target, property, descriptor, context) { if (!descriptor) return; Object.defineProperty(target, property, { enumerable: descriptor.enumerable, configurable: descriptor.configurable, writable: descriptor.writable, value: descriptor.initializer ? descriptor.initializer.call(context) : void 0 }); } function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) { var desc = {}; Object.keys(descriptor).forEach(function (key) { desc[key] = descriptor[key]; }); desc.enumerable = !!desc.enumerable; desc.configurable = !!desc.configurable; if ('value' in desc || desc.initializer) { desc.writable = true; } desc = decorators.slice().reverse().reduce(function (desc, decorator) { return decorator(target, property, desc) || desc; }, desc); if (context && desc.initializer !== void 0) { desc.value = desc.initializer ? desc.initializer.call(context) : void 0; desc.initializer = undefined; } if (desc.initializer === void 0) { Object.defineProperty(target, property, desc); desc = null; } return desc; } var _dec, _dec2, _dec3, _dec4, _dec5, _dec6, _dec7, _class, _class2, _descriptor, _descriptor2, _descriptor3, _descriptor4, _descriptor5, _temp; // https://segmentfault.com/a/1190000011985100 PHP的Laravel的闭包树的参考示例 // https://blog.csdn.net/LABLENET/article/details/89422691 闭包树的用法示例 // UserGroupRepository // 7951fb57-1183-4f75-99f9-098c70732f08 // 基础模型类定义 let XBaseModel = (_dec = typeorm.Entity(), _dec2 = typeorm.PrimaryGeneratedColumn({ type: 'bigint' }), _dec3 = typeorm.Column({ type: 'char', length: 32, unique: true }), _dec4 = typeorm.CreateDateColumn(), _dec5 = typeorm.UpdateDateColumn(), _dec6 = typeorm.VersionColumn(), _dec7 = typeorm.BeforeInsert(), _dec(_class = (_class2 = (_temp = class XBaseModel { constructor() { _initializerDefineProperty(this, "id", _descriptor, this); _initializerDefineProperty(this, "uuid", _descriptor2, this); _initializerDefineProperty(this, "created_at", _descriptor3, this); _initializerDefineProperty(this, "updated_at", _descriptor4, this); _initializerDefineProperty(this, "version", _descriptor5, this); } __init_base_model__() { // 兼容业务自己产生uuid在数据插入之前生成唯一业务标识 if (!this.uuid) { this.uuid = xuuid$1(); } } }, _temp), (_descriptor = _applyDecoratedDescriptor(_class2.prototype, "id", [_dec2], { configurable: true, enumerable: true, writable: true, initializer: null }), _descriptor2 = _applyDecoratedDescriptor(_class2.prototype, "uuid", [_dec3], { configurable: true, enumerable: true, writable: true, initializer: null }), _descriptor3 = _applyDecoratedDescriptor(_class2.prototype, "created_at", [_dec4], { configurable: true, enumerable: true, writable: true, initializer: null }), _descriptor4 = _applyDecoratedDescriptor(_class2.prototype, "updated_at", [_dec5], { configurable: true, enumerable: true, writable: true, initializer: null }), _descriptor5 = _applyDecoratedDescriptor(_class2.prototype, "version", [_dec6], { configurable: true, enumerable: true, writable: true, initializer: null }), _applyDecoratedDescriptor(_class2.prototype, "__init_base_model__", [_dec7], Object.getOwnPropertyDescriptor(_class2.prototype, "__init_base_model__"), _class2.prototype)), _class2)) || _class); // 对于TYPEORM的列修饰符进行业务语义扩展 function M_Column(option, type) { if (!F.isPlainObject(option)) { // TODO 错误异常统一优化 throw new Error('M.Column Param Error!' + JSON.stringify(option)); } // 将BXJS上定义的低代码数据类型统一映射到底层MySQL数据类型中,进行严格的低代码开发语义映射强制规范约定。 // 可以使用业务上自定义的属性覆盖掉底层的自动数据类型映射实现(顶层设计上做好) // 遵循mysql的最佳实践所有字段都必须是非空值,默认值都是各种empty常量值,避免代码中出现各种NULL的判断处理。 switch (type) { case 'enum': option = xassign({ type: 'char', length: 4, default: '', is_nullable: false }, option); break; case 'json': // 缺省值的配置实现 // 'ER_BLOB_CANT_HAVE_DEFAULT: BLOB, TEXT, GEOMETRY or JSON column \'gender\' can\'t have a default value' option = xassign({ type: 'json', default: '', is_nullable: false }, option); option.default = undefined; // mysql默认禁止对JSON设置缺省值的去除处理 break; case 'string_t': // varchar 最大长度65535长度,在ascii中最大长度为21845(每个UTF8占用3个字符长度), 在mysql8.0中最大只能10901个长度字符。 // option = xassign({ type: 'varchar', length: 10901, default: '', is_nullable: false }, option) option = xassign({ type: 'text', default: '', is_nullable: false }, option); // QueryFailedError: ER_TOO_BIG_ROWSIZE: Row size too large.The maximum row size for the used table type, not counting BLOBs, is 65535. // This includes storage overhead, check the manual.You have to change some columns to TEXT or BLOBs switch (option.type) { case 'tinytext': case 'text': case 'mediumtext': case 'longtext': option.length = undefined; // 去掉length配置 option.default = undefined; // 去掉default配置 break; } break; case 'boolean_t': option = xassign({ type: 'tinyint', width: 1, default: 0, is_nullable: false }, option); break; case 'integer_t': option = xassign({ type: 'int', default: 0, is_nullable: false }, option); break; case 'bigint_t': option = xassign({ type: 'bigint', default: 0, is_nullable: false }, option); break; case 'double_t': option = xassign({ type: 'double', default: 0, is_nullable: false }, option); break; case 'float_t': option = xassign({ type: 'float', default: 0, is_nullable: false }, option); break; case 'decimal_t': // https://dev.mysql.com/doc/refman/8.0/en/precision-math-decimal-characteristics.html // /** // * The precision for a decimal (exact numeric) column (applies only for decimal column), which is the maximum // * number of digits that are stored for the values. // */ // precision ?: number | null; // /** // * The scale for a decimal (exact numeric) column (applies only for decimal column), which represents the number // * of digits to the right of the decimal point and must not be greater than precision. // */ // scale ?: number; option = xassign({ type: 'decimal', precision: 18, scale: 4, default: 0, is_nullable: false }, option); // TODO 对于资金的精度确认控制 break; case 'time_t': // Could not start lifecycle { QueryFailedError: ER_INVALID_DEFAULT: Invalid default value for 'xxx_time' // option = xassign({ type: 'datetime', default: '0000-00-00 00:00:00', is_nullable: false }, option) option = xassign({ type: 'datetime', is_nullable: false }, option); break; case 'date_t': // Could not start lifecycle { QueryFailedError: ER_INVALID_DEFAULT: Invalid default value for 'xxx_date' // option = xassign({ type: 'date', default: '0000-00-00', is_nullable: false }, option) option = xassign({ type: 'date', is_nullable: false }, option); break; case 'id_t': // option = xassign({ type: 'bigint', default: 0, is_nullable: false }, option) // 涉及到外键约束问题必须要配置为允许空赋值,否则默认值为0会导致外键检查失败。 option = xassign({ type: 'bigint', default: null, is_nullable: true }, option); break; case 'uuid_t': option = xassign({ type: 'char', length: 32, default: '', is_nullable: false }, option); break; default: // TODO 错误异常统一优化 throw new Error('M.Column Invalid Field Type Error!' + JSON.stringify(option)); } return typeorm.Column(option); } /* type string_t = string // 映射到varchar最大值长度(可以根据需要定义其他text/mediumtext/longtext等等) type boolean_t = boolean // 映射到tinyint(1) // mysql中的枚举值映射到数据库中统一定义为tinyint一个字节无符号整数10,20,30,40,xxx,之间进行扩展然后在中间插入相关值进行排序。改进为char[4]定义方便代码开发使用也易于理解。 type double_t = number // 映射到double type float_t = number // 映射到float type decimal_t = number // 精确数值的特殊处理规范的约定扩展实现,仅仅在数据库中进行各种运算处理,前端仅仅是直传映射处理。 type integer_t = number // 映射到4字节的int类型的扩展 type time_t = Date // 年-月-日 时:分:秒[.微妙] =》 映射为datetime的mysql数据类型 type date_t = Date // 年-月-日 =》 映射为date的mysql数据类型(精简数据类型以及更好的方便处理数据) // 一般不需要定义会被自动生成出来(统一定义在全局Model基类中了) type id_t = string // 主键标识(64位的bigint处理实现,在前端上统一处理为字符串类型)对于 bigint 主键字符串的统一兼容处理实现的统一配置 type uuid_t = string // 主键中的扩展uuid字段处理的规范(uuid-v4去掉中划线总计32个字符长度),用xuuid方法统一生成对应的值。 */ // 暂时只暴露业务上需要的实现(如果有需要添加需要与老储确认必要性,用多少暴露多少最小原则封装底层实现暴露到上层) global['XBaseModel'] = XBaseModel; // ORM层暴露到业务上透传到D层实现的全局修饰符绑定实现 global['M'] = { Index: typeorm.Index, Tree: typeorm.Tree, TreeChildren: typeorm.TreeChildren, TreeParent: typeorm.TreeParent, // FindTrees, // TODO 统一封装处理实现 opNot: typeorm.Not, opLessThan: typeorm.LessThan, opLessThanOrEqual: typeorm.LessThanOrEqual, opMoreThan: typeorm.MoreThan, opMoreThanOrEqual: typeorm.MoreThanOrEqual, opEqual: typeorm.Equal, opLike: typeorm.Like, opBetween: typeorm.Between, opIn: typeorm.In, opIsNull: typeorm.IsNull, // 禁止业务上出现为空值的各种判断逻辑处理 opRaw: typeorm.Raw }; // ORM层扩展的全局修饰符重定义实现(改写了TYPEORM中的Column的修饰符语义使其更加简洁使用) global['M']['Column'] = M_Column; // 自动代码生成映射到TYPEORM上的一些基础修饰符的全局注册(方便目标代码自动生成映射使用,不暴露到全局符号表中给用户使用,仅仅内部使用) global['D'] = { ...global['D'], // ORM层需要做转义处理的各种全局修饰符统一绑定实现 Entity: typeorm.Entity, BeforeInsert: typeorm.BeforeInsert, OneToOne: typeorm.OneToOne, OneToMany: typeorm.OneToMany, ManyToOne: typeorm.ManyToOne, JoinColumn: typeorm.JoinColumn, ManyToMany: typeorm.ManyToMany, JoinTable: typeorm.JoinTable }; // import { createConnection, getConnectionManager, getConnection, QueryRunner, EntityManager } from 'typeorm' let s_connection_pool_count = 0; // 防止并发冲突只能在app初始化阶段统一执行 async function BXJS_DATABASE_INIT() { const cfg = xconfig('framework.database'); // 后续改进支持多链接响应处理 xassert(!F.isEmpty(cfg) && cfg.name === undefined); // 禁止定义name字段仅仅支持一个数据库实例访问(多实例数据库同时访问暂不支持,需要额外扩展实现) // await createConnection(cfg) // FC线上到底在干啥啊??只有一种可能性typeorm已经重复加载过了当docker重启的时候线上状态没有持久化。 await init_db_connection_pool(cfg); if (cfg.synchronize && process.env.NODE_ENV == 'test') { // 删除所有外键(解决typeorm初始化之后每次自动生成问题) await drop_test_database_foreign_keys(cfg); } } async function init_db_connection_pool(cfg) { const mng = typeorm.getConnectionManager(); let connect; if (!mng.has('default')) { connect = mng.create(cfg); } else { connect = mng.get('default'); } xassert(connect); if (!connect.isConnected) { await connect.connect(); } xassert(connect.isConnected); } // 全局统一执行保持数据库的链接状态(此接口无需业务调用使用,在请求入口自动调用,如果是长时间后台运行程序,开启定时器自动定时检测链接状态) async function BXJS_DATABASE_CONNECTION_OPEN() { try { s_connection_pool_count++; // 提前处理避免并发异步数据的不一致性(FC的并发度如何协同保持一致性?) // 链接池数量的判定逻辑实现(避免因为并发限制而导致不正常的阻塞延时的异常情况出现) // 连接池并发总数量溢出错误(默认配置每个FC的运行容器最多10个并发请求,需要放大FC的并发实例与MYSQL的本地连接池数量匹配) const cfg = xconfig('framework.database'); if (!(cfg !== null && cfg !== void 0 && cfg.queueLimit)) { cfg.queueLimit = 10; } xassert(s_connection_pool_count <= cfg.queueLimit, ERR$CONFIG, { s_connection_pool_count, queueLimit: cfg.queueLimit }); // 规避非法嵌套调用问题,必须要成对匹配调用。 let r = xcontext.get('__BXJS_DATABASE_CURRENT_CONNECTION_QUERY_RUNNER__'); xassert(F.isEmpty(r)); // 连接池中创建一个新请求并保存到上下文中,确保FC容器中并发请求执行DB的事务逻辑正确性。 await init_db_connection_pool(cfg); r = typeorm.getConnection().createQueryRunner(); await r.connect(); xcontext.set('__BXJS_DATABASE_CURRENT_CONNECTION_QUERY_RUNNER__', r); } catch (err) { xthrow(err); } } async function BXJS_DATABASE_CONNECTION_CLOSE() { try { // 避免嵌套执行错误(在单入口请求执行结束后框架层面上统一销毁链接回收到底层连接池中) xassert(s_connection_pool_count > 0); const r = xcontext.get('__BXJS_DATABASE_CURRENT_CONNECTION_QUERY_RUNNER__'); xassert(!F.isEmpty(r)); // 关闭数据库连接回收到本地连接池中 s_connection_pool_count--; xcontext.set('__BXJS_DATABASE_CURRENT_CONNECTION_QUERY_RUNNER__', undefined); await r.release(); // console.log(r.isReleased, r.connection.isConnected) FIXME: typeorm的状态与预期不一致 // xassert(r.isReleased && !r.connection.isConnected) } catch (err) { xthrow(err); } } // 取不到connection表示未创建连接直接报错处理(会话上下文中统一处理)=> 事务无法正常执行 // export function _get_context_connection(): Connection { // const r = xcontext.get('__BXJS_DATABASE_CURRENT_CONNECTION_QUERY_RUNNER__') as QueryRunner // xassert(!F.isEmpty(r) && !r.isReleased && r.connection.isConnected) // return r.connection // } function _get_context_connection_entity_manager() { const r = xcontext.get('__BXJS_DATABASE_CURRENT_CONNECTION_QUERY_RUNNER__'); xassert(!F.isEmpty(r) && !r.isReleased && r.connection.isConnected); return r.manager; } // 应用初始化的时候统一执行DB底层初始化避免潜在的并发冲突 global['__BXJS_DATABASE_INIT__'] = BXJS_DATABASE_INIT; // 全局初始化方法(在每次请求来的时候自动检测链接是否中断实现DB自动重连效果) global['__BXJS_DATABASE_CONNECTION_OPEN__'] = BXJS_DATABASE_CONNECTION_OPEN; global['__BXJS_DATABASE_CONNECTION_CLOSE__'] = BXJS_DATABASE_CONNECTION_CLOSE; // 测试环境开启synchronize后自动删除对应的外键功能扩展实现 // 线上数据库的基本运维原则 // 1. 禁止外键,需要将typeorm上生成的外键,手动删除掉对应的外键约束关系。(关系查询少用join操作,用单表进行查询,方便外键扩展查询) // https://blog.csdn.net/junlovejava/java/article/details/78360253 去除外键的参考方法 // https://my.oschina.net/jimmywa/blog/3041309 外键去除的思考 // 2. 标准主键bigint设置为id字段,typeorm中同步为字符串规避掉number溢出问题,作为基础表统一关联配置简化实现。 // 3. 编码类型统一开发环境与线上环境保持一致,统一使用utf8字符集。 // 数据库sync删除表方法 async function drop_test_database_foreign_keys(cfg) { const r = typeorm.getConnection().createQueryRunner(); try { // 禁用外键约束(防止下次TYPEORM再生成规避潜在错误) await r.query('SET GLOBAL FOREIGN_KEY_CHECKS = 0'); await r.query('SET FOREIGN_KEY_CHECKS = 0'); // 删除所有外键 const out = await r.query(` SELECT CONCAT('ALTER TABLE ','\`',TABLE_SCHEMA,'\`','.','\`',\`TABLE_NAME\`,'\`',' DROP FOREIGN KEY ',CONSTRAINT_NAME,' ;') FROM information_schema.TABLE_CONSTRAINTS c WHERE c.TABLE_SCHEMA='${cfg.database}' AND c.CONSTRAINT_TYPE='FOREIGN KEY'; `); for (let row of out) { const sql = row[Object.keys(row)[0]]; // console.log(sql) await r.query(sql); } } catch (err) { console.log(err); } await r.release(); } function _get_context_isInTransaction() { const level = xcontext.get('__BXJS_DATABASE_TRANSACTION_LEVEL__'); return !!level; } // nodejs 单线程运行机制 + 上下文闭包函数的线程变量模拟特性,线程局部全局变量特性处理。 async function xtransaction$1(callback, isolation) { const manager = _get_context_connection_entity_manager(); let level = xcontext.get('__BXJS_DATABASE_TRANSACTION_LEVEL__', 0); xcontext.set('__BXJS_DATABASE_TRANSACTION_LEVEL__', level + 1); let out = undefined; if (!level) { if (isolation) { await manager.transaction(isolation, async mng => { try { out = await callback(); } catch (err) { level = xcontext.get('__BXJS_DATABASE_TRANSACTION_LEVEL__'); xcontext.set('__BXJS_DATABASE_TRANSACTION_LEVEL__', level - 1); throw err; } }); } else { await manager.transaction(async mng => { try { out = await callback(); } catch (err) { level = xcontext.get('__BXJS_DATABASE_TRANSACTION_LEVEL__'); xcontext.set('__BXJS_DATABASE_TRANSACTION_LEVEL__', level - 1); throw err; } }); } } else { try { out = await callback(); } catch (err) { level = xcontext.get('__BXJS_DATABASE_TRANSACTION_LEVEL__'); xcontext.set('__BXJS_DATABASE_TRANSACTION_LEVEL__', level - 1); throw err; } } level = xcontext.get('__BXJS_DATABASE_TRANSACTION_LEVEL__'); xcontext.set('__BXJS_DATABASE_TRANSACTION_LEVEL__', level - 1); return out; } // 数据库模块初始化逻辑处理(短链接封装处理以上下文区分) // export async function create( // ): Promise<void> { // } // 创建对象并且自动赋值前端请求的JSON数据 function xnew$1(TYPE, param) { const manager = _get_context_connection_entity_manager(); if (F.isEmpty(param)) { return manager.create(TYPE); } return manager.create(TYPE, param); // 封装含义参考示例: // const user = manager.create(User); // same as const user = new User(); // const user = manager.create(User, { // id: 1, // firstName: "Timber", // lastName: "Saw" // }); // same as const user = new User(); user.firstName = "Timber"; user.lastName = "Saw"; } /** * 查询构造器的封装逻辑 * 1. Model全局符号约定(自动代码生成的时候产生如下特殊符号约定) * $user => 'user' (模型的别名) * $user._photos => 'user._photos' (外键关系字符串) * $user.id => 'user.id' (关系属性字符串) * 2. webpack的loader中配置预处理逻辑将字符串进正确的映射处理 * 3. 改进查询构造器中的as any部分代码的表达式设计兼容自动化的辅助代码提示 */ function xquery$1(TYPE, alias) { const manager = _get_context_connection_entity_manager(); return manager.createQueryBuilder(TYPE, alias); } let xdb$1; (function (_xdb) { async function save(model) { const manager = _get_context_connection_entity_manager(); const isInTransaction = _get_context_isInTransaction(); await manager.save(model, { transaction: !isInTransaction }); } _xdb.save = save; async function remove(model) { const manager = _get_context_connection_entity_manager(); const isInTransaction = _get_context_isInTransaction(); await manager.remove(model, { transaction: !isInTransaction }); } _xdb.remove = remove; async function execute(sql, param) { const manager = _get_context_connection_entity_manager(); return await manager.query(sql, param); } _xdb.execute = execute; async function find(TYPE, callback) { if (F.isPlainObject(callback)) { if (!callback['where']) { callback = { where: callback }; } } const manager = _get_context_connection_entity_manager(); const condition = F.isFunction(callback) ? callback(undefined) : callback; return await manager.find(TYPE, condition); } _xdb.find = find; async function findAndCount(TYPE, callback) { if (F.isPlainObject(callback)) { if (!callback['where']) { callback = { where: callback }; } } const manager = _get_context_connection_entity_manager(); const condition = F.isFunction(callback) ? callback(undefined) : callback; return await manager.findAndCount(TYPE, condition); } _xdb.findAndCount = findAndCount; async function findOne(TYPE, callback) { if (F.isPlainObject(callback)) { if (!callback['where']) { callback = { where: callback }; } } const manager = _get_context_connection_entity_manager(); const condition = F.isFunction(callback) ? callback(undefined) : callback; return await manager.findOne(TYPE, condition); // 上层使用断言进行为空识别处理不适合此处返回undefined导致上层使用比较麻烦必须要加! } _xdb.findOne = findOne; async function findLastOne(TYPE) { const manager = _get_context_connection_entity_manager(); return await manager.findOne(TYPE, { order: { 'id': 'DESC' } }); // 上层使用断言进行为空识别处理不适合此处返回undefined导致上层使用比较麻烦必须要加! } _xdb.findLastOne = findLastOne; async function findByIds(TYPE, ids, callback) { const manager = _get_context_connection_entity_manager(); const condition = F.isFunction(callback) ? callback(undefined) : callback; return await manager.findByIds(TYPE, ids, condition); } _xdb.findByIds = findByIds; async function findOneById(TYPE, id, callback) { const manager = _get_context_connection_entity_manager(); const condition = F.isFunction(callback) ? callback(undefined) : callback; return await manager.findOne(TYPE, id, condition); } _xdb.findOneById = findOneById; })(xdb$1 || (xdb$1 = {})); global['xdb'] = xdb$1; // 高频使用的三个接口从xdb命名空间中剥离出来简化开发使用 global['xnew'] = xnew$1; global['xquery'] = xquery$1; global['xtransaction'] = xtransaction$1; var _dec$1, _dec2$1, _dec3$1, _dec4$1, _dec5$1, _dec6$1, _dec7$1, _dec8, _dec9, _dec10, _class$1, _class2$1, _descriptor$1, _descriptor2$1, _descriptor3$1, _descriptor4$1, _descriptor5$1, _descriptor6, _descriptor7, _descriptor8, _temp$1; let MSysDataTunnel = (_dec$1 = typeorm.Index(['appid', 'name'], { unique: true }), _dec2$1 = typeorm.Entity(), _dec3$1 = typeorm.Column({ type: 'char', length: 32 }), _dec4$1 = typeorm.Column({ type: 'char', length: 4 }), _dec5$1 = typeorm.Column({ type: 'boolean', default: true }), _dec6$1 = typeorm.Column({ type: 'boolean', default: false }), _dec7$1 = typeorm.Column({ type: 'integer', default: 50 }), _dec8 = typeorm.Column({ type: 'integer', default: 10 * 60 * 1000 }), _dec9 = typeorm.Column({ type: 'bigint', default: '0' }), _dec10 = typeorm.Column({ type: 'datetime', default: '1970-01-01 00:00:00' }), _dec$1(_class$1 = _dec2$1(_class$1 = (_class2$1 = (_temp$1 = class MSysDataTunnel extends XBaseModel { constructor(...args) { super(...args); _initializerDefineProperty(this, "appid", _descriptor$1, this); _initializerDefineProperty(this, "name", _descriptor2$1, this); _initializerDefineProperty(this, "is_order_by_id_or_updated_at", _descriptor3$1, this); _initializerDefineProperty(this, "is_auto_reset", _descriptor4$1, this); _initializerDefineProperty(this, "every_step_count", _descriptor5$1, this); _initializerDefineProperty(this, "every_step_timeout", _descriptor6, this); _initializerDefineProperty(this, "last_data_id", _descriptor7, this); _initializerDefineProperty(this, "last_data_updated_at", _descriptor8, this); } // 当前轮询的最后一条数据更新时间 }, _temp$1), (_descriptor$1 = _applyDecoratedDescriptor(_class2$1.prototype, "appid", [_dec3$1], { configurable: true, enumerable: true, writable: true, initializer: null }), _descriptor2$1 = _applyDecoratedDescriptor(_class2$1.prototype, "name", [_dec4$1], { configurable: true, enumerable: true, writable: true, initializer: null }), _descriptor3$1 = _applyDecoratedDescriptor(_class2$1.prototype, "is_order_by_id_or_updated_at", [_dec5$1], { configurable: true, enumerable: true, writable: true, initializer: null }), _descriptor4$1 = _applyDecoratedDescriptor(_class2$1.prototype, "is_auto_reset", [_dec6$1], { configurable: true, enumerable: true, writable: true, initializer: null }), _descriptor5$1 = _applyDecoratedDescriptor(_class2$1.prototype, "every_step_count", [_dec7$1], { configurable: true, enumerable: true, writable: true, initializer: null }), _descriptor6 = _applyDecoratedDescriptor(_class2$1.prototype, "every_step_timeout", [_dec8], { configurable: true, enumerable: true, writable: true, initializer: null }), _descriptor7 = _applyDecoratedDescriptor(_class2$1.prototype, "last_data_id", [_dec9], { configurable: true, enumerable: true, writable: true, initializer: null }), _descriptor8 = _applyDecoratedDescriptor(_class2$1.prototype, "last_data_updated_at", [_dec10], { configurable: true, enumerable: true, writable: true, initializer: null })), _class2$1)) || _class$1) || _class$1); // 使用说明: // 1. 首次查询数据的时候自动生成该条配置记录 // 2. 使用mysql的悲观行锁进行数据变更互斥操作 // 3. 每个工作进程不断地并发取任务进行处理即可(一旦执行失败就忽略掉也无需重置处理) // 4. 对于执行失败的任务是否需要做记录??分割的任务需要进行状态跟踪处理,任务日志需要记录下来,定期进行数据删除处理。 // 5. 改进到OTS上做任务日志的存留时长的缓存统一更新处理。 // 算法优化:每完成一轮删除所有已经执行完成的任务减少任务日志数量堆积。(全部通过定时器串行访问数据库请求) // last_delete_done_task_log_time: time_t // 上一次删除完成任务日志时间 // 为了节省数据空间一旦处理完成后会删除所有的过期数据。 // 当任务完成后由一个失败容错定时器进行定时容错监控处理将自己的失败任务进行重试处理。 // 每次检查一旦配置发生变更立刻同步过去,零配置自动生成同步进去保持设置的一致性,这样保证程序弹性自动执行。 // 每个微应用或business_modules中如果定义了自己的表,都需要统一注册唯一的标识符名字做唯一命名空间区分。 // 1个字符框架命名前缀,2-3个字符当前业务系统中定义不同的微应用唯一区分,4个字符外部组件模型名区分。 // 将一些第三方依赖的资源内置到系统上进行统一维护管理?这样是否可以?还是每一个单独管理统一的维护配置程序依赖的云资源?数据模型不可见更加易于管理。 // 封装成类似TypeORM的TreeRepository建模机制扩展SSO、TaskQueue、DataTunnel等等更多的通用基础数据模型扩展,这样业务上按需集成使用(这些算法是通用开源的实现内嵌在各个系统中也不会存在非常膨胀的运维扩张简单易用)。 ////////////////////////////////////// // 每个第三方组件购买之后只能安装到某个系统上,需要先将依赖的运维资源升级好,其次才能正确使用后端资源以及包的依赖。 // 这样管理维护会过于复杂,需要自己管理自己维护使用(OTS的缓存资源+一些数据库表+需要购买的一些其他serverless资源,进行提前初始化后才能使用,需要开发人员进行统一运维)。 ////////////////////////////////////// // system是一个独立运维概念,管理system下各种应用或者第三方包的运维资源 // Models 是实现层面上的概念,不是业务层面上的概念 // repository这一层,应该暴露一个类似Models业务层上的数据模型,业务对象层。 // graphql 定义的业务数据模型层 // 业务对象层 + 导出的API + 运维资源需求约定 =》 service module,服务契约 // service module install // @M.Column({ default: '1970-01-01 00:00:00' }) // last_data_changed_at: time_t // 当前轮询的最后一条数据的变更时间(根据实际业务情况进行定义处理) // TODO: 当循环到任务完成后再统一进行异常数据处理 var _dec$2, _dec2$2, _dec3$2, _dec4$2, _dec5$2, _dec6$2, _class$2, _class2$2, _descriptor$2, _descriptor2$2, _descriptor3$2, _descriptor4$2, _temp$2; let ESysDataTunnelTaskStatus; (function (ESysDataTunnelTaskStatus) { ESysDataTunnelTaskStatus["\u521D\u59CB\u5316"] = "init"; ESysDataTunnelTaskStatus["\u5DF2\u5B8C\u6210"] = "ok"; ESysDataTunnelTaskStatus["\u5DF2\u5931\u8D25"] = "fail"; })(ESysDataTunnelTaskStatus || (ESysDataTunnelTaskStatus = {})); let MSysDataTunnelTask = (_dec$2 = typeorm.Index(['data_tunnel_id', 'status']), _dec2$2 = typeorm.Entity(), _dec3$2 = typeorm.Column({ type: 'bigint' }), _dec4$2 = typeorm.Column({ type: 'char', length: 4 }), _dec5$2 = typeorm.Column({ type: 'bigint', default: '0' }), _dec6$2 = typeorm.Column({ type: 'bigint', default: '0' }), _dec$2(_class$2 = _dec2$2(_class$