From db2f57b736247261d682e9371b911c541a2fea83 Mon Sep 17 00:00:00 2001 From: eifrigmn Date: Mon, 23 Apr 2018 18:43:11 +0800 Subject: [PATCH] init --- .gitignore | 7 + README.md | 797 ++++++++++++++++++++++++++++++++++++ app/controllers/business.js | 121 ++++++ app/controllers/capital.js | 359 ++++++++++++++++ app/controllers/user.js | 349 ++++++++++++++++ app/index.js | 75 ++++ app/models/business.js | 325 +++++++++++++++ app/models/capital.js | 473 +++++++++++++++++++++ app/models/user.js | 534 ++++++++++++++++++++++++ app/models/verify.js | 75 ++++ config.js.example | 33 ++ log/.gitignore | 2 + package.json | 39 ++ pm2.json | 18 + scripts/box.sql | 133 ++++++ server.js | 57 +++ static/lang/en_us.json | 57 +++ static/lang/zh_cn.json | 51 +++ static/lang/zh_hk.json | 57 +++ utils/dbhelper.js | 33 ++ utils/error.js | 61 +++ utils/logger.js | 67 +++ utils/promise.js | 48 +++ utils/rdata.js | 36 ++ utils/rpc.js | 52 +++ utils/utils.js | 75 ++++ 26 files changed, 3934 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app/controllers/business.js create mode 100644 app/controllers/capital.js create mode 100644 app/controllers/user.js create mode 100644 app/index.js create mode 100644 app/models/business.js create mode 100644 app/models/capital.js create mode 100644 app/models/user.js create mode 100644 app/models/verify.js create mode 100644 config.js.example create mode 100644 log/.gitignore create mode 100644 package.json create mode 100644 pm2.json create mode 100644 scripts/box.sql create mode 100644 server.js create mode 100644 static/lang/en_us.json create mode 100644 static/lang/zh_cn.json create mode 100644 static/lang/zh_hk.json create mode 100644 utils/dbhelper.js create mode 100644 utils/error.js create mode 100644 utils/logger.js create mode 100644 utils/promise.js create mode 100644 utils/rdata.js create mode 100644 utils/rpc.js create mode 100644 utils/utils.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e2ed027 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +node_modules/ +*.log +npm-debug.log +package-lock.json +.vscode/ +config.js \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2bd3c6c --- /dev/null +++ b/README.md @@ -0,0 +1,797 @@ +## BOX-appServer API + +> 部署前请先修改config.js.example中的配置,填写代理服务器地址 +> 同时需要对数据库进行配置 + +## 1 下级员工APP递交加密后的注册申请 + ++ router: /api/v1/registrations ++ 请求方式: POST ++ 参数: + +| 字段 | 类型 | 备注 | +| :-------------: | :----: | :-------------------: | +| msg | string | 员工APP提交的加密信息 | +| applyer_id | string | 申请者唯一识别码 | +| captain_id | string | 直属上级唯一识别码 | +| applyer_account | string | 新注册员工账号 | + ++ 返回值 + +~~~javascript +{ + "code": 0, + "message": "提交信息成功。", + "data": { + "reg_id": // 服务端申请表ID, string + } +} +~~~ + ++ 错误代码 + +| code | message | +| :--: | :----------------------------: | +| 1001 | 参数不完整。 | +| 1002 | 您已提交注册申请,请耐心等待。 | +| 1009 | 注册失败,请稍候重试。 | +| 1010 | 您的账号已经存在,请勿重复提交注册申请。 | +| 1011 | 您的账号已被停用。 | + +## 2 上级APP轮询注册申请 + +- router: /api/v1/registrations/pending +- 请求方式: GET +- 参数: + +| 字段 | 类型 | 备注 | +| :--------: | :----: | :---------------: | +| captain_id | string | 直属上级唯一识别码 | + +- 返回值 + +```javascript +{ + "code": 0, + "message": "获取注册申请信息成功。", + // 如果当前无注册申请,则data值为null + "data": { + [{ + "reg_id": // 服务端申请表ID, string + "msg": // 加密后的注册信息, string + "applyer_id": // 申请者唯一标识符, string + "applyer_account": // 申请者账号, string + "manager_id": // 直属上级唯一标识符, stirng + "consent": // 审批结果, number 0待审批 1拒绝 2同意 + "apply_at": // 申请提交时间戳, number + "applyer_account": // 申请者账号 + }] + } +} +``` + +- 错误代码 + +| code | message | +| :----: | :------------: | +| 1001 | 参数不完整。 | + +## 3 下级员工APP轮询注册审批结果 + +- router: /api/v1/registrations/approval/result +- 请求方式: GET +- 参数: + +| 字段 | 类型 | 备注 | +| :-----: | :------: | :--------------: | +| reg_id | string | 服务端申请表ID | + +- 返回值 + +```javascript +{ + "code": 0, + "message": "获取授权结果成功。", + "data": { + "reg_id": // 服务端申请表ID, string + "applyer_id": // 申请者唯一标识符,string + "captain_id": // 直属上级唯一标识符, string + "msg": // 扫码注册是提交的加密信息, string + "consent": // 审批结果 1拒绝 2同意, number + "depth": // 直属上级是否为私钥APP,0是, number + "applyer_account": // 申请者账号, string + "cipher_text" // 上级对该账号的公钥的摘要信息,string + } +} +``` + +- 错误代码 + +| code | message | +| :----: | :------------------: | +| 1001 | 参数不完整。 | +| 1003 | 未找到该注册申请。 | + +## 4 上级APP提交对注册申请的审批信息 + +- router: /api/v1/registrations/approval +- 请求方式: POST +- 参数: + +| 字段 | 类型 | 备注 | +| :---------------: | :------: | :---------------------: | +| reg_id | string | 注册申请的ID | +| consent | string | 是否同意 1拒绝,2同意 | +| applyer_pub_key | string | 新注册员工公钥 | +| cipher_text | string | 该账号对申请者公钥生成的信息摘要 | +| en_pub_key | string | 该账号对申请者公钥的签名信息 | + +- 返回值 + +```javascript +{ + "code": 0, + "message": "提交授权结果成功。" +} +``` + +- 错误代码 + +| code | message | +| :----: | :------------------: | +| 1001 | 参数不完整。 | +| 1003 | 未找到该注册申请。 | +| 1004 | 指定账号不存在。 | +| 1005 | 签名信息错误。 | +| 1014 | 直属上级账号已被停用。 | + +## 5. 提交转账申请 + +- router: /api/v1/transfer/application +- 请求方式: POST +- 参数: + +| 字段 | 类型 | 备注 | +| :--------------: | :------: | :--------------------: | +| app_account_id | string | 根节点账号唯一标识符 | +| apply_info | string | 申请理由 | +| flow_id | string | 审批流编号 | +| sign | string | 申请者的签名值 | + +- 备注 + +其中`apply_info`的结构为: + +```javascript +{ + "tx_info": // 申请理由 + "to_address": // 目的地址 + "miner": // 矿工费 + "amount": // 转账金额 + "currency": // 币种 + "timestamp": // 申请时间戳 +} +``` +- 返回值 + +```javascript +{ + "code": 0, + "message": "提交转账申请成功。", + "data": { + "order_number": // 转账记录编号, string + } +} +``` + +- 错误代码 + +| code | message | +| :----: | :---------------------------: | +| 1001 | 参数不完整。 | +| 1004 | 指定账号不存在。 | +| 1005 | 签名信息错误。 | +| 1006 | 未找到对应的业务流程。 | +| 1011 | 您的账号已被停用。 | +| 2001 | 转账信息有误,请查验后重新提交。 | +| 2002 | 未找到对应币种。 | +| 2004 | 转账申请提交失败,请稍候重试。 | + +## 6. 获取转账记录列表 + +- router: /api/v1/transfer/records/list +- 请求方式: GET +- 参数: + +| 字段 | 类型 | 备注 | +| :--------------: | :-------------: | :------------------------------------------------------------: | +| app_account_id | string | 账号唯一标识符 | +| type | string/number | 转账记录类型,0作为发起者 1作为审批者 ;默认0 | +| progress | string/number | 审批进度 -1所有记录 0待审批 1审批中 2被驳回 3审批成功;默认0 | +| page | number | 列表分页,默认1 | +| limit | number | 单页显示记录条数,默认20 | + +- 返回值 + +```javascript +{ + "code": 0, + "message": "获取转账列表成功。", + "data": { + "count": // 总数据量, number + "total_pages": // 总页码, number + "current_page": // 当前页码, number + "list": [{ + "order_number": // 转账记录编号, string + "tx_info": // 申请理由, string + "amount": // 转账金额, string + "currency": // 币种, string + "single_limit": // 单笔转账限额, string + "progress": // 审批进度 0待审批 1审批中 2被驳回 3审批成功, number + "apply_at": // 该笔转账申请时间戳, number + }] + } +} +``` + +- 错误代码 + +| code | message | +| :----: | :------------: | +| 1001 | 参数不完整。 | +| 1004 | 指定账号不存在。 | +| 1011 | 您的账号已被停用。 | + +## 7. 获取转账记录详情 + +- router: /api/v1/transfer/records +- 请求方式: GET +- 参数: + +| 字段 | 类型 | 备注 | +| :------------: | :------: | :------------: | +| order_number | string | 转账记录编号 | +| app_account_id | string | 账号唯一标识符 | + +- 返回值 + +```javascript +{ + "code": 0, + "message": "获取转账信息成功。", + "data": { + "transfer_hash": // 该笔转账对应私链的哈希值, string + "order_number": // 转账记录编号, string + "tx_info": // 申请理由, string + "applyer": // 转账申请提交者账号 + "applyer_uid": // 申请者账号唯一标识符 + "progress": // 订单审批总进度 0待审批 1审批中 2被驳回 3审批成功, number + "apply_at": // 申请提交时间戳, number + "approval_at": // 审批通过时间戳,默认null, string + "reject_at": // 审批拒绝时间戳,默认null, string + "apply_info": // 申请者提交的转账信息, string + "single_limit": // 本次转账单笔限额, string + "approvaled_info": [{ + "require": // 该层级需要审批通过的最少人数, number + "total": // 参与该层审批人员总数, number + "current_progress": // 该层当前审批进度, 0待审批 1审批中 2驳回 3同意 number + "approvers": [{ // 审批信息 + "account": // 该审批者账号, string + "app_account_id": // 该账号唯一标识符, string + "sign": // 该账号对该笔转账的签名信息, string + "progress": // 该账号对该笔转账的审批结果 0待审批 2驳回 3同意, number + }] + }, + ... + ] + } +} +``` + +- 错误代码 + +| code | message | +| :----: | :------------: | +| 1001 | 参数不完整。 | +| 1004 | 指定账号不存在。 | +| 1006 | 未找到对应的业务流程。| +| 1011 | 您的账号已被停用。 | +| 2005 | 未找到对应的转账申请。| + +## 8. 提交审批意见 + +- router: /api/v1/transfer/approval +- 请求方式: POST +- 参数: + +| 字段 | 类型 | 备注 | +| :--------------: | :-------------: | :---------------------: | +| order_number | string | 转账记录编号 | +| app_account_id | string | 账号唯一标识符 | +| progress | string/number | 审批意见 2驳回 3同意 | +| sign | string | 签名信息 | + +- 返回值 + +```javascript +{ + "code": 0, + "message": "提交审批意见成功。" +} +``` + +- 错误代码 + +| code | message | +| :----: | :------------: | +| 1001 | 参数不完整。 | +| 1004 | 指定账号不存在。 | +| 1011 | 您的账号已被停用。 | + +## 9. 获取审批流模板列表 + +- router: /api/v1/business/flows/list +- 请求方式: GET +- 参数: + +| 字段 | 类型 | 备注 | +| :--------------: | :-------------: | :---------------------: | +| app_account_id | string | 账号唯一标识符 | +| key_words | string | 搜索关键字 | +| page | string | 分页,页码 | +| limit | string | 分页,单页显示数据量 | + +- 返回值 + +```javascript +{ + "code": 0, + "message": "获取审批流模板列表成功。", + "data": { + "count": // 总数据量, number + "total_pages": // 总页码, number + "current_page": // 当前页码, number + "list": [ + { + "flow_id": // 审批流模板编号, string + "flow_name": // 审批流模板名称, string + "progress": // 审批流模板审批进度 0待审批 2审批拒绝 3审批通过, number + "single_limit": // 单笔转账上限, string + } + ] + } +} +``` + +- 错误代码 + +| code | message | +| :----: | :---------------------------: | +| 1001 | 参数不完整。 | +| 1004 | 指定账号不存在。 | +| 1011 | 您的账号已被停用。 | + +## 10. 获取审批流模板详情 + +- router: /api/v1/business/flow/info +- 请求方式: GET +- 参数: + +| 字段 | 类型 | 备注 | +| :--------------: | :-------------: | :---------------------: | +| app_account_id | string | 账号唯一标识符 | +| flow_id | string | 审批流模板编号 | + +- 返回值 + +```javascript +{ + "code": 0, + "message": "获取审批流模板详情成功。", + "data": { + "progress": // 私钥APP对该模板的审批进度 0待审批 2审批拒绝 3审批同意, number + "single_limit": // 单笔转账限额,string + "flow_name": // 审批流模板名称 + "approval_info": [ + { + "require": // 该层所需最小审批通过人数, number + "total": // 参与该层审批者总数, number + "approvers": [ + { + "account": // 审批者账号, string + "app_account_id": // 审批者账号唯一标识符, string + "pub_key": // 账号公钥 + } + ] + } + ] + } + +} +``` + +- 错误代码 + +| code | message | +| :----: |:------------------:| +| 1001 | 参数不完整。 | +| 1004 | 指定账号不存在。 | +| 1006 | 未找到对应的业务流程。 | +| 1011 | 您的账号已被停用。 | + +## 11 根节点获取非直属下属的公钥信息列表 + +- router: /api/v1/employee/pubkeys/list +- 请求方式: GET +- 参数: + +| 字段 | 类型 | 备注 | +| :--------------: | :------: | :--------------------: | +| app_account_id | string | 根节点账号唯一标识符 | + +- 返回值 + +```javascript +{ + "code": 0, + "message": "获取员工公钥信息成功。", + "data": [ + { + "applyer": // 待上传公钥的员工账号唯一标识符, string + "applyer_account": // 该员工账号,string + "pub_key": // 该员工账号的公钥, string + "captain": // 该员工账号直属上级账号唯一标识符, string + "msg": // 直属上级对其公钥的加密信息, string + "cipher_text": // 直属上级对该账号公钥生成的信息摘要 + "apply_at": // 该员工账号申请创建时间戳, number + } + ] +} +``` + +- 错误代码 + +| code | message | +| :----: | :-----------------------: | +| 1001 | 参数不完整。 | +| 1004 | 指定账号不存在。 | +| 1007 | 权限不足。 | +| 1011 | 您的账号已被停用。 | + +## 12 根节点获取指定非直属下属的公钥信息 + +- router: /api/v1/employee/pubkeys/info +- 请求方式: GET +- 参数: + +| 字段 | 类型 | 备注 | +| :------------------: | :------: | :--------------------: | +| manager_account_id | string | 根节点账号唯一标识符 | +| employee_account_id | string | 员工账号唯一标识符 | + +- 返回值 + +```javascript +{ + "code": 0, + "message": "获取员工公钥信息成功。", + "data": { + "applyer": // 待上传公钥的员工账号唯一标识符, string + "applyer_account": // 该员工账号,string + "pub_key": // 该员工账号的公钥, string + "captain": // 该员工账号直属上级账号唯一标识符, string + "msg": // 直属上级对其公钥的加密信息, string + "cipher_text": // 直属上级对该账号公钥生成的信息摘要 + "apply_at": // 该员工账号申请创建时间戳, number + } +} +``` + +- 错误代码 + +| code | message | +| :----: | :-----------------------: | +| 1001 | 参数不完整。 | +| 1004 | 指定账号不存在。 | +| 1007 | 权限不足。 | +| 1008 | 指定下级账号不存在。 | +| 1011 | 您的账号已被停用。 | +| 1013 | 指定下属账号已被停用。 | + +## 13 上级管理员获取下属账号列表 + +- router: /api/v1/accounts/list +- 请求方式: POST +- 参数: + +| 字段 | 类型 | 备注 | +| :--------------: | :-------------: | :---------------------------: | +| app_account_id | string | 上级管理员账号唯一标识符 | +| key_words | string | 搜索字段 | +| page | string | 分页,页码,默认1 | +| limit | string | 分页,单页显示数据量,默认20 | + +- 返回值 + +```javascript +{ + "code": 0, + "message": "获取下属账号列表成功。", + "data": { + "count": // 总数据量, number + "total_pages": // 总页码, number + "current_page": // 当前页码, number + "list":[ // 账号列表信息 + { + "account": // 账号,string + "app_account_id": // 账号唯一标识符,string + "manager_account_id": // 对应上级账号唯一标识符,string + "cipher_text": // 上级对该账号公钥生成的信息摘要,string + "is_uploaded": // 公钥是否上传到根节点账户, 1是 0否,number + "employee_num": // 该账号下属个数,number + } + ] + } +} +``` + +- 错误代码 + +| code | message | +| :----: | :------------: | +| 1001 | 参数不完整。 | +| 1004 | 指定账号不存在。 | +| 1011 | 您的账号已被停用。 | + +## 14. 创建审批流模板 + +- router: /api/v1/business/flow +- 请求方式: POST +- 参数: + +| 字段 | 类型 | 备注 | +| :--------------: | :-------------: | :---------------------: | +| app_account_id | string | 账号唯一标识符 | +| flow | string | 审批流模板内容 | +| sign | string |创建者对审批流模板内容的签名值| + +- 备注 + +其中`flow`的结构为: + +```javascript +{ + "flow_name": // 审批流模板名称 + "single_limit": // 单笔限额 + "approval_info":[ + { + "require": // 该层所需最小审批同意人数 + "total": // 该层审批者人数 + "approvers"[ // 审批者信息 + { + "account": // 审批者账号 + "app_account_id": // 审批者账号唯一标识符 + "pub_key": // 审批者公钥 + "itemType" + } + ] + } + ] +} +``` + +- 返回值 + +```javascript +{ + "code": 0, + "message": "创建审批流模板成功。", + "data": { + "flow_id": // 创建后的审批流编号 + } +} +``` + +- 错误代码 + +| code | message | +| :----: | :-----------------------: | +| 1001 | 参数不完整。 | +| 1004 | 指定账号不存在。 | +| 1005 | 签名信息错误。 | +| 1011 | 您的账号已被停用。 | +| 1012 | 请求代理服务器失败。 | +| 3001 | 您的账号暂无权限创建审批流模板。| +| 3002 | 指定业务流模板已存在,请勿重复提交。| +| 3004 | 创建审批流模板失败。 | + +## 15. 获取余额 + +- router: /api/v1/capital/balance +- 请求方式: GET +- 参数: + +| 字段 | 类型 | 备注 | +| :--------------: | :-------------: | :---------------------: | +| app_account_id | string | 账号唯一标识符 | +| page | string | 分页,页码 | +| limit | string | 分页,单页显示数据量 | + +- 返回值 + +```javascript +{ + "code": 0, + "message": "获取余额成功。", + "data": [{ + "currency": // 币种, string + "balance": // 余额,string + }] +} +``` + +- 错误代码 + +| code | message | +| :----: | :-----------------------: | +| 1001 | 参数不完整。 | +| 1007 | 权限不足。 | + +## 16. 获取币种列表 + +- router: /api/v1/capital/currency/list +- 请求方式: GET +- 参数: + +| 字段 | 类型 | 备注 | +| :--------------: | :-------------: | :---------------------: | +| app_account_id | string | 账号唯一标识符 | +| key_words | string | 搜索字段,币种名称,若为空则显示全部列表 | +| page | string | 分页,页码 | +| limit | string | 分页,单页显示数据量 | + +- 返回值 + +```javascript +{ + "code": 0, + "message": "获取余额成功。", + "data": { + "currency_list": [ + "currency": // 币种名称, string + "address": // 收款地址, string + ] + } +} +``` + +- 错误代码 + +| code | message | +| :----: | :-----------------------: | +| 1001 | 参数不完整。 | +| 1004 | 指定账号不存在。 | +| 1011 | 您的账号已被停用。 | + +## 17. 获取下属账号详情 + +- router: /api/v1/accounts/info +- 请求方式: GET +- 参数: + +| 字段 | 类型 | 备注 | +| :-----------------: | :-------------: | :---------------------: | +| manager_account_id | string | 上级账号唯一标识符 | +| employee_account_id | string | 下属账号唯一标识符 | + +- 返回值 + +```javascript +{ + "code": 0, + "message": "获取员工账号详情成功。", + "data": { + "app_account_id": // 下属账号唯一标识符 + "cipher_text": // 上级对该账号公钥的摘要信息 + "employee_accounts_info": [ + { + "app_account_id": // 该账号直属下级账号唯一标识符 + "account": // 账号 + "cipher_text": // 摘要信息 + } + ] + } +} +``` + +- 错误代码 + +| code | message | +| :----: | :-----------------------: | +| 1001 | 参数不完整。 | +| 1004 | 指定账号不存在。 | +| 1007 | 权限不足。 | +| 1008 | 指定下级账号不存在。 | +| 1011 | 您的账号已被停用。 | +| 1013 | 指定下属账号已被停用。 | + +## 18. 删除/替换员工账号 + +- router: /api/v1/employee/account/del +- 请求方式: POST +- 参数: + +| 字段 | 类型 | 备注 | +| :-----------------: | :-------------: | :---------------------: | +| employee_account_id | string | 下属账号唯一标识符 | +| manager_account_id | string | 上级账号唯一标识符 | +| replacer_account_id | string | 替换者账号唯一标识符 | +| cipher_texts | string | 上级对下属公钥的摘要信息 | +| sign | string | 签名值 | + +- 备注 + +其中`cipher_texts`的结构为: + +```javascript +[{ + "app_account_id": // 被删除/替换员工直属下属账号唯一标识符 + "cipher_text": // 新生成的摘要信息 +}] +``` + +- 返回值 + +```javascript +{ + "code": 0, + "message": "删除/替换下属账号成功。" +} +``` + +- 错误代码 + +| code | message | +| :----: | :-----------------------: | +| 1001 | 参数不完整。 | +| 1004 | 指定账号不存在。 | +| 1005 | 签名信息错误。 | +| 1007 | 权限不足。 | +| 1008 | 指定下级账号不存在。 | +| 1011 | 您的账号已被停用。 | +| 1013 | 指定下属账号已被停用。 | +| 1015 | 非同级用户账号无法替换。 | + +## 19.员工反馈上级审核注册结果有误 + +- router: /api/v1/registrations/approval/cancel +- 请求方式: POST +- 参数: + +| 字段 | 类型 | 备注 | +| :-----------------: | :-------------: | :---------------------: | +| reg_id | string | 审批表ID | +| app_account_id | string | 员工账号唯一标识符 | +| sign | string | 签名信息 | + + +- 返回值 + +```javascript +{ + "code": 0, + "message": "通知成功。" +} +``` + +- 错误代码 + +| code | message | +| :----: | :-----------------------: | +| 1001 | 参数不完整。 | +| 1003 | 未找到该注册申请。 | +| 1004 | 指定账号不存在。 | +| 1005 | 签名信息错误。 | +| 1007 | 权限不足。 | +| 1011 | 您的账号已被停用。 | +| 1012 | 请求代理服务器失败。 | diff --git a/app/controllers/business.js b/app/controllers/business.js new file mode 100644 index 0000000..34050c8 --- /dev/null +++ b/app/controllers/business.js @@ -0,0 +1,121 @@ +// Copyright 2018. box.la authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +'use strict'; +const eError = require(global.config.info.DIR + '/utils/error'); +const logger = require(global.config.info.DIR + '/utils/logger').logger; +const rData = require(global.config.info.DIR + '/utils/rdata'); +const User = require('../models/user'); +const Business = require('../models/business'); +const Verify = require('../models/verify'); +const crypto = require('crypto'); +const UNIVERSAL_ERROR_CODE = 1000; +const ERROR_CODE = 3000; + +/** + * @function: 创建审批流模板 + * @author: david + */ +exports.genFlow = async (ctx) => { + let {app_account_id, flow, sign} = ctx.request.body; + logger.info('创建业务流模板', {appid: app_account_id, flow: flow, sign: sign}); + if(!app_account_id || !sign || !flow ) throw new eError(ctx, UNIVERSAL_ERROR_CODE + 1); + // 是否有权限创建审批流 + let account_info = await User.getAccountInfoByAppAccountID(app_account_id); + if(!account_info) throw new eError(ctx, UNIVERSAL_ERROR_CODE + 4); + if(account_info.departured) throw new eError(ctx, UNIVERSAL_ERROR_CODE + 11); + logger.info('有权创建审批流', {depth: account_info.depth}); + if(account_info.depth != 0) throw new eError(ctx, ERROR_CODE + 1); + let flow_json = flow; + if(typeof flow_json != 'object') flow_json = JSON.parse(flow); + let flow_content = flow_json.approval_info; + let approvers = flow_content[0].approvers; + if(!flow_json.single_limit || !flow_json.flow_name || !flow_content || !flow_content[0].require || !approvers || !approvers[0].account || !approvers[0].app_account_id || !approvers[0].pub_key) throw new eError(ctx, UNIVERSAL_ERROR_CODE + 1); + // 验证签名 + let sign_pass = await Verify.signInfo(flow, account_info.pub_key, sign); + logger.info('创建业务流验证签名', sign_pass); + if(!sign_pass) throw new eError(ctx, UNIVERSAL_ERROR_CODE + 5); + // 是否创建过相同业务流模板 + let flow_hash = '0x' + crypto.createHash('sha256').update(flow).digest('hex'); + logger.info('新创建的业务流模板hash', flow_hash); + let flow_exists = await Verify.flowHashExists(flow_hash); + if(flow_exists) throw new eError(ctx, ERROR_CODE + 2); + // 获取该账号对应的注册申请信息 + let reg_info = await User.getRegistrationByID(account_info.reg_id, 1); + // 向代理服务器上报新增的审批流模板 + let pass = await Business.addFlowToServer(flow_json.flow_name, app_account_id, flow, sign, flow_hash, reg_info.captain_id); + logger.info('向代理服务器上报新增审批流申请', pass); + if(!pass) throw new eError(ctx, UNIVERSAL_ERROR_CODE + 12); + // 创建业务流模板 + let flow_insert_id = await Business.genBusinessFlow(flow_json.flow_name, flow, flow_hash, account_info.id, sign, flow_json.single_limit); + logger.info('新建审批流模板ID', flow_insert_id); + let flow_info = await Business.getBusinessFlowInfo(flow_insert_id, 0); + if(!flow_info) throw new eError(ctx, ERROR_CODE + 4); + return ctx.body = new rData(ctx, 'GEN_FLOW', {flow_id: flow_info.flow_id}); +} + +/** + * @function 获取审批流模板列表 + * @author david + */ +exports.getFlowList = async(ctx) => { + let {app_account_id, key_words} = ctx.query; + if(!app_account_id) throw new eError(ctx, UNIVERSAL_ERROR_CODE + 1); + let limit = ctx.query.limit || 20; + let page = ctx.query.page || 1; + if(typeof page == 'string') page = parseInt(page); + if(typeof limit == 'string') limit = parseInt(limit); + // 获取账号信息 + let account_info = await User.getAccountInfoByAppAccountID(app_account_id); + if(!account_info) throw new eError(ctx, UNIVERSAL_ERROR_CODE + 4); + if(account_info.departured) throw new eError(ctx, UNIVERSAL_ERROR_CODE + 11); + // 查找某账号所属的根节点账号 + let manager_account_info; + if(account_info.depth == 0) { + manager_account_info = account_info; + } else { + manager_account_info = await User.getRootAccountByUnderlingAcc(account_info.lft, account_info.rgt) + } + let manager_id = manager_account_info.id; + let data; + if(key_words) { + data = await Business.searchFlowByName(manager_id, key_words, page, limit); + }else { + data = await Business.getFlowList(manager_id, page, limit); + } + if(data) await Business.updateFlowStatus(data.list); + return ctx.body = new rData(ctx, 'FLOW_LIST', data); +} + +/** + * @function 获取审批流模板详情 + * @author david + */ +exports.getFlowInfo = async (ctx) => { + let {flow_id, app_account_id} = ctx.query; + if(!flow_id || !app_account_id) throw new eError(ctx, UNIVERSAL_ERROR_CODE + 1); + let account_info = await User.getAccountInfoByAppAccountID(app_account_id); + if(!account_info) throw new eError(ctx, UNIVERSAL_ERROR_CODE + 4); + if(account_info.departured) throw new eError(ctx, UNIVERSAL_ERROR_CODE + 11); + // 查找某账号所属的根节点账号 + let manager_account_info; + if(account_info.depth == 0) { + manager_account_info = account_info; + } else { + manager_account_info = await User.getRootAccountByUnderlingAcc(account_info.lft, account_info.rgt) + } + let manager_id = manager_account_info.id; + let flow_info = await Business.getFlowInfoByID(flow_id, manager_id); + if(!flow_info) throw new eError(ctx, UNIVERSAL_ERROR_CODE + 6); + return ctx.body = new rData(ctx, 'FLOW_INFO', flow_info); +} \ No newline at end of file diff --git a/app/controllers/capital.js b/app/controllers/capital.js new file mode 100644 index 0000000..baf4df3 --- /dev/null +++ b/app/controllers/capital.js @@ -0,0 +1,359 @@ +// Copyright 2018. box.la authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +'use strict'; +const eError = require(global.config.info.DIR + '/utils/error'); +const logger = require(global.config.info.DIR + '/utils/logger').logger; +const rData = require(global.config.info.DIR + '/utils/rdata'); +const Capital = require('../models/capital'); +const User = require('../models/user'); +const Business = require('../models/business'); +const Verify = require('../models/verify'); +const BigNumber = require('bignumber.js'); +const UUID = require('uuid/v4'); +const crypto = require('crypto'); +const UNIVERSAL_ERROR_CODE = 1000; +const ERROR_CODE = 2000; + +/** + * @function 获取转账列表 + * @author david + */ +exports.getTransferRecordsList = async (ctx) => { + let app_account_id = ctx.query.app_account_id; + if (!app_account_id) throw new eError(ctx, UNIVERSAL_ERROR_CODE + 1); + let type = ctx.query.type || 0; + let progress = ctx.query.progress || 0; + let page = ctx.query.page || 1; + let limit = ctx.query.limit || 20; + if (typeof page == 'string') page = parseInt(page); + if (typeof limit == 'string') limit = parseInt(limit); + // 获取账号信息 + let account_info = await User.getAccountInfoByAppAccountID(app_account_id); + if (!account_info) throw new eError(ctx, UNIVERSAL_ERROR_CODE + 4); + if (account_info.departured) throw new eError(ctx, UNIVERSAL_ERROR_CODE + 11); + let data = await Capital.getTransferRecordsListByAppID(account_info.id, type, progress, page, limit); + return ctx.body = new rData(ctx, 'TRANSFER_LIST', data); +} + +/** + * @function 提交转账申请 + * @author david + */ +exports.applyTransfer = async (ctx) => { + let { app_account_id, apply_info, flow_id, sign } = ctx.request.body; + logger.info('用户提交转账申请', { + app_account_id: app_account_id, + apply_info: apply_info, + flow_id: flow_id, + sign: sign + }); + if (!app_account_id || !apply_info || !flow_id || !sign) throw new eError(ctx, UNIVERSAL_ERROR_CODE + 1); + // 获取申请者账号信息 + let account_info = await User.getAccountInfoByAppAccountID(app_account_id); + if (!account_info) throw new eError(ctx, UNIVERSAL_ERROR_CODE + 4); + if (account_info.departured) throw new eError(ctx, UNIVERSAL_ERROR_CODE + 11); + // 验证签名信息 + let pub_key = account_info.pub_key; + let pass = await Verify.signInfo(apply_info, pub_key, sign); + logger.info('申请转账验签', pass); + if (!pass) throw new eError(ctx, UNIVERSAL_ERROR_CODE + 5); + // 解析转账内容 + if (typeof apply_info != 'string') throw new eError(ctx, UNIVERSAL_ERROR_CODE + 1); + let apply_info_json = JSON.parse(apply_info); + let { tx_info, to_address, miner, amount, currency, timestamp } = apply_info_json + if (!apply_info || !tx_info || !to_address || !miner || !amount || !currency || !timestamp) throw new eError(ctx, ERROR_CODE + 1); + // 获取币种信息 + let currency_info = await Capital.getCurrencyInfoByName(currency); + if (!currency_info) throw new eError(ctx, ERROR_CODE + 2); + // 获取对应的审批流 + let flow_info = await Business.getBusinessFlowInfo(flow_id, 2); + if (!flow_info) throw new eError(ctx, UNIVERSAL_ERROR_CODE + 6); + // 查询审批流上链状态 + let flow_on_chain_status = await Business.businessFlowStatus(flow_info.flow_hash); + logger.info('提交转账申请获取审批流上链状态', {hash: flow_info.flow_hash, status: flow_on_chain_status}); + // 更新审批流状态 + await Business.updateFlowStatus([flow_info]); + if (flow_on_chain_status != 3) { + throw new eError(ctx, UNIVERSAL_ERROR_CODE + 6); + } else { + // 提交转账申请 + let transfer_id = await Capital.applyTransfer(UUID(), tx_info, account_info.id, currency_info.currency_id, amount, flow_info.id, apply_info, sign, flow_info.content.approval_info[0].approvers); + logger.info('提交转账申请_生成订单号', transfer_id); + let transfer_info = await Capital.getTransferInfo(transfer_id, 0); + if (!transfer_info) throw new eError(ctx, ERROR_CODE + 4); + return ctx.body = new rData(ctx, 'APPLY_TRANSFER', { order_number: transfer_info.order_number }) + } +} + +/**# + * @function 获取指定转账记录详情 + * @author david + */ +exports.getTransInfoByOrderNumber = async (ctx) => { + let { app_account_id, order_number } = ctx.query; + if (!order_number || !app_account_id) throw new eError(ctx, UNIVERSAL_ERROR_CODE + 1); + // 获取账户信息 + let account_info = await User.getAccountInfoByAppAccountID(app_account_id); + if (!account_info) throw new eError(ctx, UNIVERSAL_ERROR_CODE + 4); + if (account_info.departured) throw new eError(ctx, UNIVERSAL_ERROR_CODE + 11); + // 获取转账信息 + let tx_info = await Capital.getTransferInfo(order_number, 1); + if (!tx_info) throw new eError(ctx, ERROR_CODE + 5); + let tx_content = JSON.parse(tx_info.apply_content); + let result = { + transfer_hash: tx_info.trans_hash, + order_number: tx_info.order_number, + tx_info: tx_content.tx_info, + applyer: tx_info.applyer_acc, + applyer_uid: tx_info.applyer_uid, + progress: tx_info.progress, + arrived: tx_info.arrived, + apply_at: tx_info.apply_at, + approval_at: tx_info.approval_at, + reject_at: tx_info.reject_at, + apply_info: tx_info.apply_content + } + // 获取审批流信息 + let flow_info = await Business.getBusinessFlowInfo(tx_info.flow_id, 0); + if (!flow_info) throw new eError(ctx, UNIVERSAL_ERROR_CODE + 6); + let flow_content = flow_info.content; + if (typeof flow_content != 'object') flow_content = JSON.parse(flow_content); + result.single_limit = flow_content.single_limit; + // 获取各级人员对该订单的审批情况 + let approval_info = await Business.getTxApprovalInfoByFlowContentTransID(flow_content, tx_info.trans_id); + result.approvaled_info = approval_info; + return ctx.body = new rData(ctx, 'TRANSFER_INFO', result); + +} + +/** + * @function 审批转账申请 + * @author david + */ +exports.approvalTransfer = async (ctx) => { + let { order_number, app_account_id, progress, sign } = ctx.request.body; + if (!order_number || !app_account_id || !progress || !sign) throw new eError(ctx, UNIVERSAL_ERROR_CODE + 1); + // 获取账户信息 + let account_info = await User.getAccountInfoByAppAccountID(app_account_id); + if (!account_info) throw new eError(ctx, UNIVERSAL_ERROR_CODE + 4); + if (account_info.departured) throw new eError(ctx, UNIVERSAL_ERROR_CODE + 11); + // 获取订单信息 + let tx_info = await Capital.getTransferInfo(order_number, 1); + if (!tx_info) throw new eError(ctx, ERROR_CODE + 5); + // 验证是否有审批权限 + let approvers_comments = await Capital.getTxInfoByApprover(app_account_id, tx_info.trans_id); + if (approvers_comments == -1) { + throw new eError(ctx, ERROR_CODE + 3); + } else if (approvers_comments != 0) { + throw new eError(ctx, ERROR_CODE + 6); + } + // 验证签名 + let sign_pass = await Verify.signInfo(tx_info.apply_content, account_info.pub_key, sign); + logger.info('审批转账验签', sign_pass); + if (!sign_pass) throw new eError(ctx, UNIVERSAL_ERROR_CODE + 5); + // 提交审批意见 + await Capital.approvalTransfer(tx_info.trans_id, account_info.id, progress, sign); + // 获取订单对应的审批流模板内容 + let tx_flow = await Business.getBusinessFlowInfo(tx_info.flow_id, 0); + if (!tx_flow) throw new eError(ctx, UNIVERSAL_ERROR_CODE + 6); + // 获取审批流上链状态 + let flow_on_chain_status = await Business.businessFlowStatus(tx_flow.flow_hash); + // 更新本地审批流状态 + logger.info('审批转账_获取审批流上链状态', flow_on_chain_status); + await Business.updateFlowStatus([tx_info]); + if (flow_on_chain_status != 3) { + // 审批流哈希未上链,转账失败 + await Capital.updateTxProgress(tx_info.trans_id, 2); + throw new eError(ctx, UNIVERSAL_ERROR_CODE + 6); + } + // 获取审批者在审批流中的位置 + let location = await Business.getManagerLocation(tx_flow.content, app_account_id); + // 获取订单审批进度 + let tx_progress = await Capital.getTxProgress(tx_flow.content, tx_info.trans_id); + if (tx_progress == 1 && location.level + 1 < tx_flow.content.approval_info.length) { + await Capital.initManagerComments(tx_flow.content, tx_info.trans_id, location); + } + // 获取订单审批最新进度 + tx_progress = await Capital.getTxProgress(tx_flow.content, tx_info.trans_id); + logger.info('审批后的订单进度', {progress: tx_progress, trans_id: tx_info.trans_id}); + // 获取各级审批人员签名信息 + let approval_info = await Capital.getTxApproversSign(tx_flow.content, tx_info.trans_id) + // 审批通过,转账 + if (tx_progress == 3) { + let apply_json = tx_info.apply_content; + if (typeof apply_json != 'object') apply_json = JSON.parse(apply_json); + // 获取币种信息 + let currency_info = await Capital.getCurrencyInfoByName(apply_json.currency); + if (!currency_info) throw new eError(ctx, ERROR_CODE + 8); + // 订单全部审批通过, 上链 + let amount = new BigNumber(apply_json.amount); + let miner = new BigNumber(apply_json.miner); + let times = new BigNumber(Math.pow(10, currency_info.factor)); + let fixed = config.info.FIED; + let wd_hash = '0x' + crypto.createHash('sha256').update(tx_info.apply_content).digest('hex'); + let obj = { + hash: tx_flow.flow_hash, + // wdhash: tx_info.trans_hash, + wdhash: wd_hash, + category: currency_info.currency_id, + amount: amount.multipliedBy(times).toFixed(fixed), + fee: miner.multipliedBy(times).toFixed(fixed), + recaddress: apply_json.to_address, + apply: tx_info.apply_content, + applysign: JSON.stringify(approval_info) + } + let upload_pass = await Capital.uploadTxChain(obj); + if (!upload_pass) { + // 提交转账失败, 更改订单状态 + tx_progress = 2; + throw new eError(ctx, ERROR_CODE + 7); + } + } + // 更新订单审批进度 + logger.info('订单最终审批进度', {trans_id: tx_info.trans_id, progress: tx_progress}); + await Capital.updateTxProgress(tx_info.trans_id, tx_progress); + return ctx.body = new rData(ctx, 'APPROVAL_TX'); +} + +// 获取余额 +exports.getBalance = async (ctx) => { + let { app_account_id, currency } = ctx.query; + if (!currency || !app_account_id) throw new eError(ctx, UNIVERSAL_ERROR_CODE + 1); + let account_info = await User.getAccountInfoByAppAccountID(app_account_id); + if (!account_info || account_info.departured) throw new eError(ctx, UNIVERSAL_ERROR_CODE + 7); + // 获取币种信息 + let currency_info = await Capital.getCurrencyInfoByName(currency); + if (!currency_info) throw new eError(ctx, ERROR_CODE + 8); + let balance = currency_info.balance; + return ctx.body = new rData(ctx, 'GET_BALANCE', { currency: currency, balance: balance }); +} + +// 获取币种列表 +exports.getCurrencyList = async (ctx) => { + let { app_account_id, key_words } = ctx.query; + if (!app_account_id) throw new eError(ctx, UNIVERSAL_ERROR_CODE + 1); + let account_info = await User.getAccountInfoByAppAccountID(app_account_id); + if (!account_info) throw new eError(ctx, UNIVERSAL_ERROR_CODE + 4); + if (account_info.departured) throw new eError(ctx, UNIVERSAL_ERROR_CODE + 11); + let currency_list = await Capital.getCurrencyList(key_words) + return ctx.body = new rData(ctx, 'CURRENCY_LIST', { currency_list: currency_list }); +} + +// 伴生程序通知最终提现结果 +exports.withdrawResult = async function (ctx, next) { + let { wd_hash, tx_id } = ctx.request.body; + logger.info('伴生程序通知最终提现结果: ', { + wd_hash: wd_hash, + tx_id: tx_id + }); + // 获取该笔转账记录详情 + let tx_info = await Capital.getTransferInfoByTxBoxHash(wd_hash); + if (tx_info) { + // 插入本地数据库 tb_transfer_history,set progress = 2 + await Capital.addTransferArrivedInfo(wd_hash, tx_id, 2); + // 更新余额 + await Capital.updateBalance(tx_info.amount, tx_info.currencyID, 1); + } + return ctx.body = new rData(ctx, 'NOTICE'); +} + +// 代理服务器通知提现结果 +exports.withdrawResultOfID = async (ctx, next) => { + let { wd_hash, tx_id } = ctx.request.body; + logger.info('代理服务器通知提现结果: ', { + wd_hash: wd_hash, + tx_id: tx_id + }); + if (!wd_hash || !tx_id) throw new eError(ctx, UNIVERSAL_ERRORCODE + 1); + // 获取该笔转账记录详情 + let tx_info = await Capital.getTransferInfoByTxBoxHash(wd_hash); + if (tx_info) { + // 插入本地数据库 tb_transfer_history,set progress = 0 + await Capital.addTransferArrivedInfo(wd_hash, tx_id, 1); + } + return ctx.body = new rData(ctx, 'NOTICE'); +} + +// 代理服务器上报充值记录 +exports.depositSuccess = async (ctx) => { + let { from, to, amount, tx_id, category } = ctx.request.body; + logger.info('代理服务器通知充值结果: ', { + fromAddr: from, + toAddr: to, + amount: amount, + tx_id: tx_id, + category: category + }); + if (!from || !to || !amount || !tx_id || (category != 0 && !category)) throw new eError(ctx, UNIVERSAL_ERRORCODE + 1); + // 获取币种单位 + let currency_id = category; + let currency_info = await Capital.getCurrencyByID(currency_id); + if (!currency_info) throw new eError(ctx, ERROR_CODE + 8); + amount = new BigNumber(amount); + let times = new BigNumber(Math.pow(10, currency_info.factor)); + amount = amount.div(times).toFixed(8); + let from_array + if (from.indexOf(',')) { + from_array = from.split(','); + } else { + from_array[0] = from; + } + logger.info('充值记录落库', { + from_array: from_array, + to: to, + currency: currency_info.id, + amount: amount, + tx_id: tx_id + }) + let order_num = UUID(); + await Capital.depositHistory(order_num, from_array, to, currency_info.id, amount, tx_id); + // 更新余额 + await Capital.updateBalance(amount, category, 0); + // 更新充值地址 + if (currency_info) { + await Capital.initContractAddress(category, to); + } + return ctx.body = new rData(ctx, 'NOTICE'); +} + +/** + * @function 代理服务器提示新增币种/代币 + * @param {string/number} type // 0币种 1代币 + * @author david + */ +exports.addCurrency = async (ctx) => { + let type = ctx.request.body.type; + if (type != 0 && type != 1) throw new eError(ctx, UNIVERSAL_ERROR_CODE + 1) + let new_currency_list = await Capital.getNewCurrencyList(type); + // 更新币种列表 + if (new_currency_list && new_currency_list.length) { + await Capital.updateCurrencyList(new_currency_list, type); + } + return ctx.body = new rData(ctx, 'NOTICE'); +} + +/** + * @function 获取资产 + * @author david + */ +exports.getBalanceList = async (ctx) => { + let { app_account_id } = ctx.query; + let page = ctx.query.page || 1; + let limit = ctx.query.limit || 20; + if (!app_account_id) throw new eError(ctx, UNIVERSAL_ERROR_CODE + 1); + let account_info = await User.getAccountInfoByAppAccountID(app_account_id); + if (!account_info || account_info.departured || account_info.depth != 0) throw new eError(ctx, UNIVERSAL_ERROR_CODE + 7); + let assets = await Capital.getAssets(page, limit); + return ctx.body = new rData(ctx, 'GET_BALANCE', assets); +} \ No newline at end of file diff --git a/app/controllers/user.js b/app/controllers/user.js new file mode 100644 index 0000000..6b415aa --- /dev/null +++ b/app/controllers/user.js @@ -0,0 +1,349 @@ +// Copyright 2018. box.la authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +'use strict'; +const eError = require(global.config.info.DIR + '/utils/error'); +const logger = require(global.config.info.DIR + '/utils/logger').logger; +const rData = require(global.config.info.DIR + '/utils/rdata'); +const User = require('../models/user'); +const Verify = require('../models/verify'); +const ERROR_CODE = 1000; + +/** + * @function 将下属提交的注册信息存入数据库 + * 如果首次提交,直接存入数据库,否则提示重复提交 + * 单次申请保留30s + * @author david + */ +exports.applyForAccount = async (ctx) => { + let { msg, applyer_id, captain_id, applyer_account} = ctx.request.body + logger.info('用户提交注册信息_param', { + msg: msg, + applyer_id: applyer_id, + captain_id: captain_id, + applyer_account: applyer_account + }); + if(!msg || !applyer_id || !captain_id || !applyer_account) throw new eError(ctx, ERROR_CODE + 1); + // 检测账号是否存在 + let employee_account_info = await User.getAccountInfoByAppAccountID(applyer_id); + if(employee_account_info && employee_account_info.departured == 0) { + // 账号已存在 + throw new eError(ctx, ERROR_CODE + 10); + } else if(employee_account_info && employee_account_info.departured == 1) { + // 已离职 + throw new eError(ctx, ERROR_CODE + 11); + } + // 检测是否提交过相同申请 + let has_applyed = await Verify.hasApplyedRegistration(applyer_id, captain_id); + if(has_applyed) throw new eError(ctx, ERROR_CODE + 2); + // 将申请记录存入tb_registration_history中 + let registration_id = await User.addRegistration(applyer_id, captain_id, msg, applyer_account); + if(!registration_id) throw new eError(ctx, ERROR_CODE + 9); + // 是否是向私钥APP申请注册 + let is_admin_account = await Verify.isAdminAccount(captain_id); + logger.info('扫描私钥APP注册', is_admin_account); + if(is_admin_account) { + // 向代理服务器提交注册信息 + let data = await User.applyTegistrationToServer(registration_id, msg, applyer_id, captain_id, applyer_account, 0); + logger.info('请求代理服务器接口', data); + if(!data) { + // 撤销已提交的申请 + await User.updateCaptainApprovalInfo(registration_id, 1); + throw new eError(ctx, ERROR_CODE + 9); + } + } + logger.info('用户提交注册信息_out', {reg_id: registration_id}); + return ctx.body = new rData(ctx, "GEN_ACCOUNT", {reg_id: registration_id}); +} + +/** + * @function 获取指定上司所涉及的注册申请 + * @author david + */ +exports.getRegistrationInfo = async (ctx) => { + let { captain_id } = ctx.query; + if(!captain_id) throw new eError(ctx, ERROR_CODE + 1); + // 获取该管理员所涉及的注册申请列表 + let registration_info = await User.getRegistration(captain_id, null); + if(registration_info && registration_info.length > 5) { + // 默认最多返回最新5条记录,其余记录删除 + let min_date_time = registration_info[registration_info.length -1].apply_at; + let max_date_time = registration_info[5].apply_at; + await User.delRegistrationInfoByDateTime(min_date_time, max_date_time); + registration_info = registration_info.slice(0, 5); + } + return ctx.body = new rData(ctx, 'GET_REGISTRATION', registration_info); +} + +/** + * @function 上级管理员审批下级员工的注册申请 + * @author david + */ +exports.approvalRegistration = async (ctx) => { + let {reg_id, consent} = ctx.request.body; + if(!reg_id || !consent) throw new eError(ctx, ERROR_CODE + 1); + logger.info('上级审批注册申请', { + reg_id: reg_id, + consent: consent + }); + // 获取注册申请信息 + let registration_info = await User.getRegistrationByRegID(reg_id, 0); + if(!registration_info) throw new eError(ctx, ERROR_CODE + 3); + // 审批通过 + if(consent == 2) { + let {applyer_pub_key, cipher_text, en_pub_key} = ctx.request.body; + logger.info('上级审批注册申请_审批通过', { + applyer_pub_key: applyer_pub_key, + cipher_text: cipher_text, + en_pub_key: en_pub_key + }); + if(!applyer_pub_key || !cipher_text || !en_pub_key) throw new eError(ctx, ERROR_CODE + 1); + let is_uploaded = 1; + // 获取直属上级账号信息 + let captain_account_info = await User.getAccountInfoByAppAccountID(registration_info.captain_id); + if(!captain_account_info) throw new eError(ctx, ERROR_CODE + 4); + if(captain_account_info.departured) throw new eError(ctx, ERROR_CODE + 14); + // 验证签名 + let sign_pass = await Verify.signInfo(applyer_pub_key, captain_account_info.pub_key, en_pub_key); + logger.info('审批注册验证签名', sign_pass); + if(!sign_pass) throw new eError(ctx, ERROR_CODE + 5); + let depth = captain_account_info.depth + 1; + let rgt = captain_account_info.rgt; + if(captain_account_info.depth > 0) { + is_uploaded = 0 + } + await User.genAccount(registration_info.applyer_account, registration_info.applyer_id, applyer_pub_key, cipher_text, en_pub_key, rgt, registration_info.id, is_uploaded, depth) + } + // 记录上级审批结果 + logger.info('上级审批注册_out', {reg_id: reg_id, consent: consent}); + await User.updateCaptainApprovalInfo(reg_id, consent); + return ctx.body = new rData(ctx, 'APPROVAL_REGISTRATION'); +} + +/** + * @function 员工APP反馈上级审批注册结果出错 + * @author david + */ +exports.cancelApprovalRegistration = async (ctx) => { + let {reg_id, applyer_id, sign} = ctx.request.body; + if(!reg_id || !applyer_id || !sign) throw new eError(ctx, ERROR_CODE + 1); + // 获取员工账号信息 + let account_info = await User.getAccountInfoByAppAccountID(applyer_id); + if(!account_info) throw new eError(ctx, ERROR_CODE + 4); + if(account_info.departured) throw new eError(ctx, ERROR_CODE + 11); + // 获取注册信息 + let reg_info = await User.getRegistrationByRegID(reg_id); + if(!reg_info) throw new eError(ctx, ERROR_CODE + 3); + if(reg_info.applyer_id != applyer_id) throw new eError(ctx, ERROR_CODE + 7); + // 验证签名 + let sign_pass = await Verify.signInfo(reg_id, account_info.pub_key, sign); + logger.info('员工反馈注册审批出错_验签', sign_pass); + if(!sign_pass) throw new eError(ctx, ERROR_CODE + 5); + // 回滚信息 + await User.changeEmployee(applyer_id, account_info.lft, account_info.rgt); + // 如果是根节点账号 + if(account_info.depth == 0) { + let data = await User.applyTegistrationToServer(reg_id, reg_info.msg, applyer_id, reg_info.captain_id, account_info.account, 1); + if(!data) throw new eError(ctx, ERROR_CODE + 12); + } + await User.updateCaptainApprovalInfo(reg_id, 1); + return ctx.body = new rData(ctx, 'NOTICE'); +} + +/** + * @function 私钥app审批注册 + * @author david + */ +exports.adminApprovalRegistration = async (ctx) => { + let {regid, status} = ctx.request.body; + if(!regid || !status) throw new eError(ctx, ERROR_CODE + 1); + // 获取注册信息 + let reg_info = await User.getRegistrationByRegID(regid, 0); + if(!reg_info) throw new eError(ctx, ERROR_CODE + 3); + // 审批通过 + if(status == 2) { + let {ciphertext, pubkey} = ctx.request.body; + if(!ciphertext) throw new eError(ctx, ERROR_CODE + 1); + await User.genAccount(reg_info.applyer_account, reg_info.applyer_id, pubkey, ciphertext, null, 0, reg_info.id, 1, 0) + } + // 记录上级审批结果 + await User.updateCaptainApprovalInfo(regid, status); + return ctx.body = new rData(ctx, 'APPROVAL_REGISTRATION'); +} + +/** + * @function 获取指定注册申请信息 + * @author david + */ +exports.getRegistrationApprovalInfo = async (ctx) => { + let {reg_id} = ctx.query; + if(!reg_id) throw new eError(ctx, ERROR_CODE + 1); + let data = await User.getRegistrationByRegID(reg_id); + if(!data) throw new eError(ctx, ERROR_CODE + 3); + return ctx.body = new rData(ctx, 'GET_REGISTRATION', data); +} + +/** + * @function 根节点获取非直属下属的公钥信息 + * @author david + */ +exports.getEmployeePubKeyInfoList = async (ctx) => { + let app_account_id = ctx.query.app_account_id; + logger.info('根节点获取下属公钥列表', app_account_id); + if(!app_account_id) throw new eError(ctx, ERROR_CODE + 1); + // 获取账号信息 + let account_info = await User.getAccountInfoByAppAccountID(app_account_id); + if(!account_info) throw new eError(ctx, ERROR_CODE + 4); + if(account_info.departured) throw new eError(ctx, ERROR_CODE + 11); + if(account_info.depth != 0) throw new eError(ctx, ERROR_CODE + 7); + // 获取未被上传的下属公钥信息列表 + let result = await User.getEmployeeEnPubKeyInfoList(app_account_id); + if(result.account_ids.length) { + await User.updateAccountsPubkeyUploadInfo(result.account_ids); + } + return ctx.body = new rData(ctx, 'EMPLOYEE_PUB_KEY', result.result); +} + +/** + * @function 获取指定下属公钥信息 + * @author dvid + */ +exports.getEmployeePubKeyInfo = async (ctx) => { + let {manager_account_id, employee_account_id} = ctx.query; + if(!manager_account_id || !employee_account_id) throw new eError(ctx, ERROR_CODE + 1); + let manager_account_info = await User.getAccountInfoByAppAccountID(manager_account_id); + if(!manager_account_info) throw new eError(ctx, ERROR_CODE + 4); + if(manager_account_info.departured) throw new eError(ctx, ERROR_CODE + 11); + if(manager_account_info.depth != 0) throw new eError(ctx, ERROR_CODE + 7); + let employee_account_info = await User.getAccountInfoByAppAccountID(employee_account_id); + if(!employee_account_info) throw new eError(ctx, ERROR_CODE + 8); + if(employee_account_info.departured) throw new eError(ctx, ERROR_CODE + 13); + let result = await User.getEmployeeEnPubKeyInfo(employee_account_id); + if(result) { + // 更新状态,标记公钥已上传根节点 + await User.updateAccountsPubkeyUploadInfo(result.applyer); + } + return ctx.body = new rData(ctx, 'EMPLOYEE_PUBKEY_INFO', result); +} + +/** + * @function 获取下属账号列表 + * @author david + */ +exports.getEmployeeAccountsList = async(ctx) => { + let {app_account_id, key_words} = ctx.query; + if(!app_account_id) throw new eError(ctx, ERROR_CODE + 1); + let page = ctx.query.page || 1; + let limit = ctx.query.limit || 20; + if(typeof page == 'string') page = parseInt(page); + if(typeof limit == 'string') limit = parseInt(limit); + let employee_accounts_list = {}; + // 获取上级账号信息 + let manager_account_info = await User.getAccountInfoByAppAccountID(app_account_id); + if(!manager_account_info) throw new eError(ctx, ERROR_CODE + 4); + if(manager_account_info.departured) throw new eError(ctx, ERROR_CODE + 11); + // 如果是搜索 + if(key_words) { + employee_accounts_list = await User.searchAccountInfoByAccount(key_words, page, limit); + } else { + let depth = manager_account_info.depth + 1; + // 获取下属账号信息 + employee_accounts_list = await User.getEmployeeAccountsByCaptainID(depth, manager_account_info.lft, manager_account_info.rgt, page, limit); + } + let result = { + count: employee_accounts_list.count, + total_pages: employee_accounts_list.total_pages, + current_page: page, + list: employee_accounts_list.data + } + return ctx.body = new rData(ctx, 'ACCOUNTS_LIST', result); +} + +/** + * @function 获取下属账号详情 + * @author david + */ +exports.getEmployeeAccountsInfo = async (ctx) => { + let {manager_account_id, employee_account_id} = ctx.query; + if(!manager_account_id || !employee_account_id) throw new eError(ctx, ERROR_CODE + 1); + // 获取上级账号信息 + let manager_account_info = await User.getAccountInfoByAppAccountID(manager_account_id); + if(!manager_account_info) throw new eError(ctx, ERROR_CODE + 4); + if(manager_account_info.departured) throw new eError(ctx, ERROR_CODE + 11); + // 下级账号信息 + let employee_account_info = await User.getAccountInfoByAppAccountID(employee_account_id); + if(!employee_account_info) throw new eError(ctx, ERROR_CODE + 8); + if(employee_account_info.departured) throw new eError(ctx, ERROR_CODE + 13); + // 是否有权获取 + if(manager_account_info.depth >= employee_account_info.depth) throw new eError(ctx, ERROR_CODE + 7); + // 获取下属账户详情 + let employee_info = await User.getUnderlingInfoByManagerAccountID(employee_account_info.depth + 1, employee_account_info.lft, employee_account_info.rgt) + let result = { + app_account_id: employee_account_info.app_account_id, + cipher_text: employee_account_info.cipher_text, + } + if(employee_info) result.employee_accounts_info = employee_info; + return ctx.body = new rData(ctx, 'EMPLOYEE_ACCOUNT_INFO', result); +} + +/** + * @function 删除员/替换工账号 + * @author david + */ +exports.changeEmployeeAccount = async (ctx) => { + let {employee_account_id, manager_account_id, sign, cipher_texts, replacer_account_id} = ctx.request.body; + let msg = 'DEL_EMPLOYEE'; + logger.info('删除/替换员工账号', { + employee_account_id: employee_account_id, + manager_account_id: manager_account_id, + sign: sign, + cipher_texts: cipher_texts + }); + if(!employee_account_id || !manager_account_id || !sign ) throw new eError(ctx, ERROR_CODE + 1); + // 获取上级账号信息 + let manager_account_info = await User.getAccountInfoByAppAccountID(manager_account_id); + if(!manager_account_info) throw new eError(ctx, ERROR_CODE + 4); + if(manager_account_info.departured) throw new eError(ctx, ERROR_CODE + 11); + // 被删除/替换者账号信息 + let employee_account_info = await User.getAccountInfoByAppAccountID(employee_account_id); + if(!employee_account_info) throw new eError(ctx, ERROR_CODE + 8); + if(employee_account_info.departured) throw new eError(ctx, ERROR_CODE + 13); + // 是否有权删除/替换 + if(manager_account_info.depth >= employee_account_info.depth) throw new eError(ctx, ERROR_CODE + 7); + // 验证签名 + let sign_pass = await Verify.signInfo(employee_account_id, manager_account_info.pub_key, sign); + if(!sign_pass) throw new eError(ctx, ERROR_CODE + 5); + // 获取被删除或被替换者直属下级账号信息 + let employee_info = await User.getUnderlingInfoByManagerAccountID(employee_account_info.depth+1, employee_account_info.lft, employee_account_info.rgt); + // 更新摘要信息 + let data = await User.changeCipherInfo(employee_info, cipher_texts); + if(data == -1) throw new eError(ctx, ERROR_CODE + 1); + // 删除,更新摘要信息 + // await User.changeEmployee(employee_account_id, employee_account_info.lft, employee_account_info.rgt, data, employee_account_info.depth); + await User.changeEmployee(employee_account_id, data); + // 更新替换后的上下级关系 + if(replacer_account_id) { + // 替换 + msg = 'REPLACE_EMPLOYEE'; + let replacer_account_info = await User.getAccountInfoByAppAccountID(replacer_account_id); + if(!replacer_account_info) throw new eError(ctx, ERROR_CODE + 8) + if(replacer_account_info.departured) throw new eError(ctx, ERROR_CODE + 13); + if(replacer_account_info.depth != employee_account_info.depth) throw new eError(ctx, ERROR_CODE + 15); + if(employee_info.length ) { + for(let r of employee_info) { + await User.replaceEmployee(r.app_account_id, replacer_account_info.id); + } + } + } + return ctx.body = new rData(ctx, msg); +} diff --git a/app/index.js b/app/index.js new file mode 100644 index 0000000..f58f425 --- /dev/null +++ b/app/index.js @@ -0,0 +1,75 @@ +// Copyright 2018. box.la authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +'use strict'; + +const Router = require('koa-router'); +const config = global.config; +const User = require('./controllers/user'); +const Capital = require('./controllers/capital'); +const Business = require('./controllers/business'); +const multer = require('../utils/utils').multer; +const router = new Router(); +router.prefix('/api/' + config.info.API_VERSION); + +router + // 下属注册提交扫码后的信息 + .post('/registrations', User.applyForAccount) + // 上级APP获取待审核的注册信息 + .get('/registrations/pending', User.getRegistrationInfo) + // 下属获取注册申请审批结果 + .get('/registrations/approval/result', User.getRegistrationApprovalInfo) + // 上级APP审批下级的注册申请 + .post('/registrations/approval', User.approvalRegistration) + // 员工APP反馈上级审批结果出错 + .post('/registrations/approval/cancel', User.cancelApprovalRegistration) + // 提交转账申请 + .post('/transfer/application', Capital.applyTransfer) + // 获取转账记录列表(待审批/已审批、作为发起者/作为审批者) + .get('/transfer/records/list', Capital.getTransferRecordsList) + // 获取指定的转账记录详情 + .get('/transfer/records', Capital.getTransInfoByOrderNumber) + // 提交审批意见 + .post('/transfer/approval', Capital.approvalTransfer) + // 获取业务流模板列表 + .get('/business/flows/list', Business.getFlowList) + // 获取业务流模板详情 + .get('/business/flow/info', Business.getFlowInfo) + // 根节点获取非直属下属的公钥信息列表 + .get('/employee/pubkeys/list', User.getEmployeePubKeyInfoList) + // 根节点获取指定非直属下属的公钥信息 + .get('/employee/pubkeys/info', User.getEmployeePubKeyInfo) + // 上级管理员获取下属员工账号列表 + .get('/accounts/list', User.getEmployeeAccountsList) + // 上级管理员获取下属员工账号详情 + .get('/accounts/info', User.getEmployeeAccountsInfo) + // 创建业务流模板 + .post('/business/flow', Business.genFlow) + // 删除/替换员工账号 + .post('/employee/account/change', User.changeEmployeeAccount) + // 获取余额 + .get('/capital/balance', Capital.getBalanceList) + // 获取币种列表 + .get('/capital/currency/list', Capital.getCurrencyList) + // 代理服务器上报转账结果 + // 最终结果 + .post('/capital/withdraw', Capital.withdrawResult) + // 临时结果 + .post('/capital/withdraw/id', Capital.withdrawResultOfID) + // 代理服务器上报充值记录 + .post('/capital/deposit', Capital.depositSuccess) + // 代理服务器上报私钥APP审批注册结果 + .post('/registrations/admin/approval', User.adminApprovalRegistration) + // 代理服务器通知新增币种,代币 + .post('/capital/curency/add', Capital.addCurrency) +module.exports = router; diff --git a/app/models/business.js b/app/models/business.js new file mode 100644 index 0000000..189d949 --- /dev/null +++ b/app/models/business.js @@ -0,0 +1,325 @@ +// Copyright 2018. box.la authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +'use strict'; +const config = global.config; +const P = require(global.config.info.DIR + '/utils/promise').P; +const dbhelper = require(global.config.info.DIR + '/utils/dbhelper'); +const pool = dbhelper.pool; +const queryFormat = dbhelper.queryFormat; +const User = require('./user'); +const RPC = require('../../utils/rpc'); + +/** + * @function 新建业务流模板 + * @param {string} flow_name: // 业务流模板名称 + * @param {string} flow_content // 业务流模板内容 + * @param {string} flow_hash // 哈希 + * @param {string} founder_id // 创建者账号ID + * @param {number} single_limit // 单笔转账限额 + * @return {number} 业务流模板ID + * @author david + */ +exports.genBusinessFlow = async (flow_name, flow_content, flow_hash, founder_id, sign, single_limit) => { + let query = queryFormat(` + insert into tb_business_flow + set flowID = uuid(), flowHash = ?, flowName = ?, founderID = ?, content = ?, founderSign = ?, singleLimit = ?`, [flow_hash, flow_name, founder_id, flow_content, sign, single_limit]); + let result = await P(pool, 'query', query); + return result.insertId +} +/** + * @function 获取审批流信息 + * @param type == 0: 根据tb_business_flow.id获取 + * type == 1: 根据tb_business_flow.flowID + * @return {obj} 审批流模板详情 + * @author david + */ +exports.getBusinessFlowInfo = async (param, type) => { + let where_str; + if (type == 0) { + // 根据tb_business_flow.id获取 + where_str = ' where id = ? '; + } else { + // 根据tb_business_flow.flowID + where_str = ' where flowID = ? '; + } + let query = queryFormat(` + select id, flowID as flow_id, flowHash as flow_hash, flowName as flow_name, progress, + UNIX_TIMESTAMP(createdAt) as created_at, content, singleLimit as single_limit, + UNIX_TIMESTAMP(updatedAt) as updated_at, null approval_at + from tb_business_flow + ` + where_str, [param]); + let rows = await P(pool, 'query', query); + if (!rows.length) return null; + let result = rows[0]; + if (result.progress == 2) { + result.approval_at = result.updated_at; + } + delete result.updated_at; + result.content = JSON.parse(result.content); + return result; +} + +/** + * @function:获取用户转账申请的审批信息 + * @param: {string} app_account_id // 用户账号唯一标识符 + * {string} trans_id // 订单号 + * @author:david + */ +exports.getTransApprovalInfoByAppAccountID = async (app_account_id, trans_id) => { + let query = queryFormat(` + select rt.sign, rt.comments as progress + from tb_accounts_info as acc + left join tb_review_transfer as rt + on rt.managerAccID = acc.id + where acc.appAccountID = ? and rt.transID = ?`, [app_account_id, trans_id]); + let data = await P(pool, 'query', query); + return data[0]; +} + +/** + * @function 获取转账申请的审批信息 + * @param {string} flow_content // 审批流模板内容 + * @param {string} trans_id // 订单号 + * @author:david + */ +exports.getTxApprovalInfoByFlowContentTransID = async (flow_content, trans_id) => { + let result = []; + let approval_info = flow_content.approval_info + for (let i = 0; i < approval_info.length; i++) { + let data = {}; + let total_approvers = 0; + let total_rejects = 0; + let the_approval_info = {}; + data.require = approval_info[i].require; + let approvers = approval_info[i].approvers; + data.total = approvers.length + for (let j = 0; j < approvers.length; j++) { + approvers[j].progress = 0; + approvers[j].sign = null; + the_approval_info = await this.getTransApprovalInfoByAppAccountID(approvers[j].app_account_id, trans_id); + if (the_approval_info) { + approvers[j].progress = the_approval_info.progress; + approvers[j].sign = the_approval_info.sign; + } + if (approvers[j].progress == 3) { + total_approvers++; + } else if (approvers[j].progress == 2) { + total_rejects++; + } + delete approvers[j].pub_key; + } + if (total_approvers >= approval_info[i].require) { + total_approvers = 3; + } else if (total_rejects > approvers.length - approval_info[i].require) { + total_approvers = 2; + } else if (total_rejects == 0 && total_approvers == 0) { + total_approvers = 0; + } else { + total_approvers = 1; + } + data.approvers = approvers; + data.current_progress = total_approvers; + result[i] = data; + } + return result; +} + +/** + * @function 获取审批流模板列表 + * @param {number} page + * @param {number} limit + * @author david + */ +exports.getFlowList = async (manager_id, page, limit) => { + let start = (page - 1) * limit; + let end = limit; + let query_count = queryFormat(`select count(*) as count from tb_business_flow where founderID = ?`, [manager_id]); + let query = queryFormat(` + select id, flowID as flow_id, flowName as flow_name, content, flowHash as flow_hash, progress + from tb_business_flow + where founderID = ? + order by createdAt desc + limit ?, ?`, [manager_id, start, end]); + let data = await P(pool, 'query', query); + let data_count = await P(pool, 'query', query_count); + if (data.length) { + for (let r of data) { + let flow_content = r.content; + if (typeof flow_content != 'object') flow_content = JSON.parse(flow_content); + r.single_limit = flow_content.single_limit; + delete r.content; + } + } + return { + count: data_count[0].count, + total_pages: Math.ceil(data_count[0].count / limit), + current_page: page, + list: data + } +} + +/** + * @function 搜索获取审批流模板列表 + * @param {string} key_words // 搜索关键字 + * @param {number} page // 分页 + * @param {number} limit + * @author david + */ +exports.searchFlowByName = async (manager_id, key_words, page, limit) => { + let start = (page - 1) * limit; + let end = limit; + let query_count = queryFormat(`select count(*) as count from tb_business_flow where founderID = ? flowName like ?`, [manager_id, '%' + key_words + '%']); + let query = queryFormat(` + select id, flowID as flow_id, flowName as flow_name, content, flowHash as flow_hash, progress + from tb_business_flow + where founderID = ? and flowName like ? + order by createdAt desc + limit ?, ?`, [manager_id, '%' + key_words + '%', start, end]); + let data = await P(pool, 'query', query); + let data_count = await P(pool, 'query', query_count); + if (data.length) { + for (let r of data) { + let flow_content = r.content; + if (typeof flow_content != 'object') flow_content = JSON.parse(flow_content); + r.single_limit = flow_content.single_limit; + delete r.content; + } + } + return { + count: data_count[0].count, + total_pages: Math.ceil(data_count[0].count / limit), + current_page: page, + list: data + } +} + +/** + * @function 获取审批流模板详情 + * @param {string} flow_id // 审批流编号 + * @param {number} manager_id // 该审批流创建者账号ID + * @author david + */ +exports.getFlowInfoByID = async (flow_id, manager_id) => { + let result = []; + let query = queryFormat(` + select flowName as flow_name, content, progress + from tb_business_flow + where flowID = ? and founderID = ?`, [flow_id, manager_id]); + let data = await P(pool, 'query', query); + if (!data) return null; + let flow_content = data[0].content; + if (typeof flow_content != 'object') flow_content = JSON.parse(flow_content); + let approval_info = flow_content.approval_info; + for (let i = 0; i < approval_info.length; i++) { + let data = {}; + data.require = approval_info[i].require; + data.total = approval_info[i].approvers.length + data.approvers = approval_info[i].approvers; + result[i] = data; + } + return { + flow_name: data[0].flow_name, + progress: data[0].progress, + single_limit: flow_content.single_limit, + approval_info: result + }; +} + +/** + * @function 获取业务流结构上链状态 + * @param {string} flow_hash // 业务流模板对应的哈希值 + * @return {number} 1审批中 2审批决绝 3审批通过 + * @author david + */ +exports.businessFlowStatus = async (flow_hash) => { + let param = { + hash: flow_hash + } + let flow_on_chain = await P(RPC, 'rpcRequest', 'GET', config.info.PROXY_HOST, config.info.SERVER_URL.FLOW_STATUS, param); + if (flow_on_chain && flow_on_chain.RspNo == 0) { + if (flow_on_chain.ApprovalInfo.Status == 7) { + return 3; + } else if (flow_on_chain.ApprovalInfo.Status == 0 || flow_on_chain.ApprovalInfo.Status == 1 || flow_on_chain.ApprovalInfo.Status == 3 || flow_on_chain.ApprovalInfo.Status == 4 || flow_on_chain.ApprovalInfo.Status == 6) { + return 1; + } + } + return 2 +} + +/** + * @function 更新审批流状态 + * @param {array} flow_list // 审批流列表 + * @author david + */ +exports.updateFlowStatus = async (flow_list) => { + for(let r of flow_list) { + if(r.progress < 2) { + let flow_on_chain_status = await this.businessFlowStatus(r.flow_hash); + if(flow_on_chain_status) { + // 更新审批流审批状态 + let query = queryFormat('update tb_business_flow set progress = ? where id = ?', [flow_on_chain_status, r.id]); + await P(pool, 'query', query); + r.progress = flow_on_chain_status; + } + } + } +} + +/** + * @function 向代理服务器上报新增的审批流模板 + * @param {string} appid // 创建者账号唯一标识符 + * {string} flow // 序列化后的审批流模板内容 + * {string} sign // 创建者对flow的签名 + * {string} hash // flow对应的哈希值 + * @author david + */ +exports.addFlowToServer = async (flow_name, appid, flow, sign, hash, captain_id) => { + let param = { + name: flow_name, + appid: appid, + flow: flow, + sign: sign, + hash: hash, + captainid: captain_id + } + let host = config.info.PROXY_HOST; + let url = config.info.SERVER_URL.ADD_FLOW; + let result = await P(RPC, 'rpcRequest', 'GET', host, url, param); + return result && result.RspNo == 0 ? true : false; +} + +/** + * @function 获取管理员在对应审批流中所处的位置 + * @param {obj} flow_content // 审批流内容 + * @param {string} app_account_id // 审批者账号唯一标识符 + * @author david + */ +exports.getManagerLocation = async (flow_content, app_account_id) => { + let location = {}; + let flow_approval_info = flow_content.approval_info; + for (let i = 0; i < flow_approval_info.length; i++) { + let approvers = flow_approval_info[i].approvers; + for (let j = 0; j < approvers.length; j++) { + if (approvers[j].app_account_id == app_account_id) { + location = { + level: i, + num: j, + require: flow_approval_info[i].require + } + break; + } + } + } + return location; +} \ No newline at end of file diff --git a/app/models/capital.js b/app/models/capital.js new file mode 100644 index 0000000..6cc0f1f --- /dev/null +++ b/app/models/capital.js @@ -0,0 +1,473 @@ +// Copyright 2018. box.la authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +'use strict'; +const config = global.config; +const P = require(global.config.info.DIR + '/utils/promise').P; +const logger = require(global.config.info.DIR + '/utils/logger').logger; +const dbhelper = require(global.config.info.DIR + '/utils/dbhelper'); +const pool = dbhelper.pool; +const queryFormat = dbhelper.queryFormat; +const crypto = require('crypto'); +const User = require('./user'); +const RPC = require('../../utils/rpc'); +const Business = require('./business'); + +/** + * @function 根据账号app端唯一标识符获取转账列表 + * @param {string} account_id // 账号唯一标识符 + * @param {number} type // 0作为申请者 1作为审批者 + * @param {number} progress // 订单审批进度 -1所有记录 0待审批 1审批中 2被驳回 3审批成功 + * @param {number} page, limit // 分页 + * @author david + */ +exports.getTransferRecordsListByAppID = async (account_id, type, progress, page, limit) => { + let query_total, query; + let start = (page - 1) * limit; + let end = limit; + let str = ''; + if (type == 0) { + query = queryFormat(` + select t.orderNum as order_number, t.txInfo as tx_info, t.progress, t.amount, c.currency, + UNIX_TIMESTAMP(t.createdAt) as apply_at, t.arrived + from tb_transfer as t + left join tb_currency as c + on c.id = t.currencyID + where applyerID = ? `, [account_id]); + query_total = queryFormat(`select count(*) as total from tb_transfer where applyerID = ?`, [account_id]); + // 申请者 + if (progress != -1) { + str = queryFormat(' and progress = ?', [progress]); + } + } else if (type == 1) { + query = queryFormat(` + select t.orderNum as order_number, t.txInfo as tx_info, + UNIX_TIMESTAMP(t.createdAt) as apply_at, t.amount, c.currency, + (case t.progress when 0 then 1 else t.progress end) as progress, t.arrived + from tb_transfer as t + left join tb_review_transfer as rt + on rt.transID = t.id + left join tb_currency as c + on c.id = t.currencyID + where rt.managerAccID = ? `, [account_id]); + query_total = queryFormat(` + select count(*) as total + from tb_transfer as t + left join tb_review_transfer as rt + on rt.transID = t.id + where rt.managerAccID = ?`, [account_id]); + // 审批者 + if (progress == -1) { + // str = queryFormat(' and rt.comments <> 0 '); + str = ''; + // 获取所有类型的转账记录 + } else if (progress == 0) { + str = queryFormat(` and rt.comments = ? and t.progress < 2 `, [progress]); + } else { + // 该审批者已审批过的转账列表 + str = queryFormat(` and t.progress = ? and rt.comments <> 0 `, [progress]); + } + query = query + str + queryFormat('order by apply_at desc limit ?, ?', [start, end]); + query_total += str; + } + let total_result = await P(pool, 'query', query_total); + let total = total_result[0].total; + let total_pages = Math.ceil(total / limit) || 1; + let list = await P(pool, 'query', query); + return { + count: total, + total_pages: total_pages, + current_page: page, + list: list + } +} + +/** + * @function:员工提交转账申请 + * @param: {string} tx_info // 申请理由 + * {string} applyer_id + * // 提交申请者账号ID + * {string} currency // 币种名称,缩写 + * {string} amount // 转账金额 + * {string} flow_id // 审批流编号 + * {string} apply_content // 转账内容 + * {string} applyer_sign // 申请者对该笔转账签名 + * {array} captain_account_ids // 第一级审批者账号唯一标识符 + * @return: transfer_info.id + * @author:david + */ +exports.applyTransfer = async (order_number, tx_info, applyer_id, currency_id, amount, flow_id, apply_content, applyer_sign, captain_account_ids) => { + let trans_hash = '0x' + crypto.createHash('sha256').update(apply_content).digest('hex'); + let query = queryFormat('insert into tb_transfer set orderNum = ?, txInfo = ?, transBoxHash = ?, applyerID = ?, currencyID = ?, amount = ?, flowID = ?, applyContent = ?, applyerSign = ?', + [order_number, tx_info, trans_hash, applyer_id, currency_id, amount, flow_id, apply_content, applyer_sign]); + let conn = await P(pool, 'getConnection'); + let tx_id; + try { + await P(conn, 'beginTransaction'); + let transfer_info = await P(conn, 'query', query); + tx_id = transfer_info.insertId; + let captain_account_info = await User.getAccountInfoByAppAccountID(captain_account_ids[0].app_account_id); + let captain_review_query = queryFormat('insert into tb_review_transfer (transID, managerAccID) values (?, ?)', [tx_id, captain_account_info.id]); + for (let i = 1; i < captain_account_ids.length; i++) { + let captain = await User.getAccountInfoByAppAccountID(captain_account_ids[i].app_account_id); + captain_review_query += queryFormat(', (?, ?)', [tx_id, captain.id]); + } + await P(conn, 'query', captain_review_query); + await P(conn, 'commit'); + } catch (err) { + await P(conn, 'rollback'); + throw err; + } finally { + conn.release(); + } + return tx_id; +} + +/** + * @function 获取转账信息 + * @param type == 0: 根据trans_id获取 + * type == 1: 根据order_number获取 + * @author david + */ +exports.getTransferInfo = async (param, type) => { + let where_str; + if (type == 0) { + // 根据tb_transfer.id获取 + where_str = ' where t.id = ? '; + } else if (type == 1) { + // 根据order_number获取 + where_str = ' where t.orderNum = ? '; + } + let query = queryFormat(` + select t.id as trans_id, t.orderNum as order_number, t.transBoxHash as trans_hash, a.account as applyer_acc, + t.progress, t.applyContent as apply_content, t.applyerSign as applyer_sign, a.appAccountID as applyer_uid, + t.flowID as flow_id, UNIX_TIMESTAMP(t.createdAt) as apply_at, null approval_at, t.arrived, + null reject_at, UNIX_TIMESTAMP(t.updatedAt) as updated_at, t.id as trans_id + from tb_transfer as t + left join tb_accounts_info as a + on a.id = t.applyerID + ` + where_str, [param]); + let data = await P(pool, 'query', query); + if (data.length) { + if (data[0].progress == 2) { + data[0].reject_at = data[0].updated_at; + } else if (data[0].progress == 3) { + data[0].approval_at = data[0].updated_at + } + delete data[0].updated_at; + } + return data.length ? data[0] : null; +} + +/** + * @function 获取员工待审批的转账信息 + * @param {string} app_account_id // 账号唯一标识符 + * @param {string} trans_id // 订单号 + */ +exports.getTxInfoByApprover = async (app_account_id, trans_id) => { + let query = queryFormat(` + select rt.comments as progress + from tb_transfer as t + left join tb_review_transfer as rt + on rt.transID = t.id + left join tb_accounts_info as acc + on acc.id = rt.managerAccID + where acc.appAccountID = ? and t.id = ?`, [app_account_id, trans_id]); + let result = await P(pool, 'query', query); + + return result.length ? result[0].progress : -1; +} + + +/** + * @function 提交审批意见 + * @param {string} trans_id // 订单号 + * @param {string} manager_account_id // 审批者账号唯一标识符 + * @param {number} progress // 审批意见 + * @param {string} sign // 审批者签名 + */ +exports.approvalTransfer = async (trans_id, manager_account_id, progress, sign) => { + let query = queryFormat('update tb_review_transfer set comments = ?, sign = ? where transID = ? and managerAccID = ?', [progress, sign, trans_id, manager_account_id]); + await P(pool, 'query', query); +} + +/** + * @function 更新订单审批进度 + * @param {string} trans_id // 订单ID + * @param {number} progress // 审批意见 1审批中 2驳回 3同意 + * @author david + */ +exports.updateTxProgress = async (trans_id, progress) => { + let query = queryFormat('update tb_transfer set progress = ? where id = ?', [progress, trans_id]); + if (progress == 3) { + query = queryFormat('update tb_transfer set progress = ?, arrived = 1 where id = ?', [progress, trans_id]); + } + await P(pool, 'query', query); +} + +/** + * @function 获取币种信息 + * @param {string} currency // 币种名称,简写 + * @author david + */ +exports.getCurrencyInfoByName = async (currency) => { + let query = queryFormat('select id as currency_id, factor, currency, balance from tb_currency where currency = ? and available = 1', [currency]); + let result = await P(pool, 'query', query); + return result.length ? result[0] : null; +} + +/** + * @function 获取币种列表 + * @param {string} key_words // 搜索关键字 + * @author david + */ +exports.getCurrencyList = async (key_words) => { + let query = queryFormat(`select currency, address from tb_currency where available = 1`); + if (key_words) { + query = queryFormat('select currency, address from tb_currency where available = 1 and currency like ?', ['%' + key_words + '%']); + } + let result = await P(pool, 'query', query); + return result.length ? result : []; +} + +// 根据id获取币种信息 +exports.getCurrencyByID = async (currency_id) => { + let query = queryFormat('select id, currency, factor from tb_currency where id = ? and available = 1', [currency_id]); + let result = await P(pool, 'query', query); + return result.length ? result[0] : null; +} + +// 根据transBoxHash获取订单详情 +exports.getTransferInfoByTxBoxHash = async function (tx_box_hash) { + let query = queryFormat('select * from tb_transfer where transBoxHash = ?', [tx_box_hash]); + let rows = await P(pool, 'query', query); + return rows.length ? rows[0] : null; +} + +// 记录提现到账信息 +exports.addTransferArrivedInfo = async function (trans_hash, tx_id, progress) { + let query = queryFormat('update tb_transfer set arrived = ?, txID = ? where transBoxHash = ?', [progress, tx_id, trans_hash]); + await P(pool, 'query', query); +} + +// 记录充值记录 +exports.depositHistory = async function (order_num, from_array, to, currency, amount, tx_id) { + let query; + if (!from_array) { + query = queryFormat('insert into tb_deposit_history (orderNum, fromAddr, toAddr, currencyID, amount, txID) values (?, ?, ?, ?, ?, ?)', [order_num, 0, to, currency, amount, tx_id]); + } else { + query = queryFormat('insert into tb_deposit_history (orderNum, fromAddr, toAddr, currencyID, amount, txID) values (?, ?, ?, ?, ?, ?)', [order_num, from_array[0], to, currency, amount, tx_id]); + for (let i = 1; i < from_array.length; i++) { + query += queryFormat(', (?, ?, ?, ?, ?, ?)', [order_num, from_array[i], to, currency, amount, tx_id]); + } + } + await P(pool, 'query', query); +} + +/** + * @function 向代理服务器提交审批通过的转账申请 + * @param {obj} obj // 转账内容 + * @author david + */ +exports.uploadTxChain = async function (obj) { + let host = config.info.PROXY_HOST; + let url = config.info.SERVER_URL.APPLY_TRANSFER; + let result = await P(RPC, 'rpcRequest', 'POST', host, url, obj); + return result && result.RspNo == 0 ? true : false; +} + +/** + * @function 新增币种/代币 + * @param {string/number} type // 类型 0币种 1代币 + * @author david + */ +exports.getNewCurrencyList = async (type) => { + let host = config.info.PROXY_HOST; + let url = ''; + if (type == 0) { + url = config.info.SERVER_URL.COINLIST; + } else if (type == 1) { + url = config.info.SERVER_URL.TOKENLIST; + } + let data = await P(RPC, 'rpcRequest', 'GET', host, url, null); + // await P(pool, 'query', query_update); + if (data.RspNo != 0) { + return null + } else { + return (type == 0) ? data.CoinStatus : data.TokenInfos + } +} + +/** + * @function 更新币种列表 + * @param {array} elder_list // 原始币种列表 + * @param {array} new_list // 最新的币种列表 + * @param {string/number} type // 0币种 1代币 + * @author david + */ +exports.updateCurrencyList = async (new_list, type) => { + let query_update = queryFormat('update tb_currency set available = 0 where isToken = ? and id <> 0', [type]); + // let query = queryFormat('select id from tb_currency where isToken = ?', [type]); + for (let r of new_list) { + let query_selc = queryFormat('select id from tb_currency where id = ?', [r.Category]); + let data = await P(pool, 'query', query_selc); + if (data.length) { + let query = queryFormat('update tb_currency set available = 1 where id = ?', [r.Category]); + await P(pool, 'query', query); + } else { + let query; + if(type == 0) { + query = queryFormat('insert into tb_currency (id, currency, factor, isToken) values (?, ?, ?, ?) ', [r.Category, r.Name, r.Decimals, type]); + } else { + query = queryFormat('insert into tb_currency (id, currency, factor, address, isToken) values (?, ?, ?, ?, ?) ', [r.Category, r.TokenName, r.Decimals, r.ContractAddr, type]); + } + await P(pool, 'query', query); + } + } +} + +/** + * @function 更新余额 + * @param {string} amount 金额 + * @param {number} currency_id 币种ID + * @param {number} type 0-充值 1-提现 + * @author david + */ +exports.updateBalance = async (amount, currency_id, type) => { + if (typeof amount != 'number') amount = Number(amount); + if (type == 1) amount = -amount; + if (typeof currency_id != 'number') currency_id = Number(currency_id); + let query = queryFormat('update tb_currency set balance = balance + ? where id = ?', [amount, currency_id]); + await P(pool, 'query', query); +} + +/** + * @function 向上级管理员通知待审批转账请求 + * @param {obj} flow_content // 审批流内容 + * @param {string} trans_id // 转账列表ID + * @param {obj} location // 审批者所在的位置 + */ +exports.initManagerComments = async (flow_content, trans_id, location) => { + let approvers_info = flow_content.approval_info[location.level]; + let pass = 0; + let reject = 0; + for (let r of approvers_info.approvers) { + let comments = await this.getTxInfoByApprover(r.app_account_id, trans_id); + if (comments == 2) reject++; + if (comments == 3) pass++; + } + if ((pass >= approvers_info.require) && (location.level + 1 < flow_content.approval_info.length)) { + // 某一层approvers审批通过 + let new_approvers = flow_content.approval_info[location.level + 1].approvers; + let manager_account_info = await User.getAccountInfoByAppAccountID(new_approvers[0].app_account_id); + let query_s = queryFormat('select transID from tb_review_transfer where transID = ? and managerAccID in ( ?', [trans_id, manager_account_info.id]); + let query = queryFormat('insert into tb_review_transfer (transID, managerAccID) values (?, ?)', [trans_id, manager_account_info.id]); + for (let i = 1; i < new_approvers.length; i++) { + manager_account_info = await User.getAccountInfoByAppAccountID(new_approvers[i].app_account_id); + query += queryFormat(', (?, ?)', [trans_id, manager_account_info.id]); + query_s += queryFormat(', ? ', [manager_account_info.id]); + } + query_s += queryFormat(')'); + let result = await P(pool, 'query', query_s); + if(result.length == 0) { + await P(pool, 'query', query); + } + } +} + +/**# + * @function 获取订单审批进度 + * @param {obj} flow_content // 审批流内容 + * @param {string} trans_id // 转账信息表ID + * @author david + */ +exports.getTxProgress = async (flow_content, trans_id) => { + let flow_approval_info = flow_content.approval_info; + for(let i=0; iapprovers.length - require) { + return 2; + } else if(appr + rej < require) { + return 1; + } + } + return 3; +} + +/** + * @function 初始化以太坊合约地址 + * @author david + */ +exports.initContractAddress = async (currency_id, address) => { + let query = queryFormat('update tb_currency set address = ? where id = ? and available = 1', [address, currency_id]); + await P(pool, 'query', query); +} + +/** + * @function 获取资产列表 + * @author david + */ +exports.getAssets = async (page, limit) => { + if (typeof page != 'number') page = Number(page); + if (typeof limit != 'number') limit = Number(limit); + let start = (page - 1) * limit; + let end = limit; + let query = queryFormat('select currency, balance from tb_currency where available = 1 limit ?, ?', [start, end]); + let data = await P(pool, 'query', query); + return data; +} + +/** + * @function 获取审批人员签名信息 + * @author david + */ +exports.getTxApproversSign = async (flow_content, trans_id) => { + let data = []; + let approvers_info = flow_content.approval_info; + for(let r of approvers_info) { + let approvers = r.approvers; + for(let a of approvers) { + let sign = await this.getApproversSignByAppID(a.app_account_id, trans_id); + if(sign && sign.length) data.push({ + appid: a.app_account_id, + sign: sign.sign + }) + } + } + return data; +} + +/** + * @function 获取指定审批人员签名信息 + * @author david + */ +exports.getApproversSignByAppID = async (app_account_id, trans_id) => { + let query = queryFormat(` + select rt.sign from tb_review_transfer as rt + left join tb_accounts_info as acc + on acc.id = rt.managerAccID + where acc.appAccountID = ? and rt.transID = ?`, [app_account_id, trans_id]); + let data = await P(pool, 'query', query); + return data.length ? data[0] : null; +} \ No newline at end of file diff --git a/app/models/user.js b/app/models/user.js new file mode 100644 index 0000000..755804f --- /dev/null +++ b/app/models/user.js @@ -0,0 +1,534 @@ +// Copyright 2018. box.la authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +'use strict'; + +const config = global.config; +const P = require(global.config.info.DIR + '/utils/promise').P; +const logger = require(global.config.info.DIR + '/utils/logger').logger; +const dbhelper = require(global.config.info.DIR + '/utils/dbhelper'); +const pool = dbhelper.pool; +const queryFormat = dbhelper.queryFormat; +const RPC = require('../../utils/rpc'); + +/** + * @function 记录注册申请 + * @param {string} msg // 员工APP提交的加密信息 + * @param {string} applyer // 申请者账号唯一标识符 + * @param {string} captain // 直属上级账号唯一标识符 + * @param {string} applyer_account // 新注册员工账号 + * @return {string} 注册申请表ID + * @author david + */ +exports.addRegistration = async (applyer, captain, msg, applyer_account) => { + let query = queryFormat('insert into tb_registration_history set regID = uuid(), applyer = ?, captain = ?, msg = ?, applyerAcc = ?', [applyer, captain, msg, applyer_account]); + let data = await P(pool, 'query', query); + let reg_id = data.insertId; + query = queryFormat('select regID from tb_registration_history where id = ?', [reg_id]); + data = await P(pool, 'query', query); + return data.length ? data[0].regID : null; +} + +/** + * @function 获取注册申请 + * @param {string} captain // 直属上级账号唯一标识符 + * @param {string} applyer // 申请者账号唯一标识符 + * @returns {array} [{ + * registration_id: // 服务端申请表ID + * applyer: // 申请者 + * captain: // 直属上级 + * msg: // 加密后的注册信息 + * consent: // 审批结果 1拒绝 2同意 + * }] + * @author david + */ +exports.getRegistration = async (captain, applyer) => { + let query = queryFormat(` + select regID as reg_id, applyer as applyer_id, captain as manager_id, + msg, consent, UNIX_TIMESTAMP(createdAt) as apply_at, applyerAcc as applyer_account + from tb_registration_history + where captain = ? and isDeleted = 0`, [captain]) + if (!applyer) { + query = query + queryFormat(` order by apply_at desc `); + } else { + // 管理员涉及的注册申请 + query = query + queryFormat(` and applyer = ? `, [applyer]); + } + let result = await P(pool, 'query', query); + return result.length ? result : null; +} + +/** + * @function 根据registration_id获取注册申请 + * @param {string} reg_id // 申请表regID + * @param {string} is_deleted // 按订单是否被删除状态获取 + * @return {obj} + * @author david + */ +exports.getRegistrationByRegID = async (reg_id, is_deleted) => { + let query = queryFormat(` + SELECT rh.id, rh.regID as reg_id, rh.applyer as applyer_id, rh.captain as captain_id, + rh.msg, rh.consent, ifnull(acc.depth, -1) as depth, rh.applyerAcc as applyer_account, acc.cipherText as cipher_text + FROM tb_registration_history rh + LEFT JOIN tb_accounts_info acc ON acc.regID = rh.id + WHERE rh.regID = ? ` , [reg_id]); + if (is_deleted == 0 || is_deleted == 1) { + query = query + queryFormat(' and isDeleted = ?', [is_deleted]); + } + let result = await P(pool, 'query', query); + return result.length ? result[0] : null; +} + +/** + * @function 根据registration_id获取注册申请 + * @param {string} reg_id // 申请表id + * @param {string} is_deleted // 按订单是否被删除状态获取 + * @return {obj} + * @author david + */ +exports.getRegistrationByID = async (id, is_deleted) => { + let query = queryFormat(` + SELECT rh.id, rh.regID as reg_id, rh.applyer as applyer_id, rh.captain as captain_id, + rh.msg, rh.consent, ifnull(acc.depth, -1) as depth, rh.applyerAcc as applyer_account, acc.cipherText as cipher_text + FROM tb_registration_history rh + LEFT JOIN tb_accounts_info acc ON acc.regID = rh.id + WHERE rh.id = ? ` , [id]); + if (is_deleted == 0 || is_deleted == 1) { + query = query + queryFormat(' and isDeleted = ?', [is_deleted]); + } + let result = await P(pool, 'query', query); + return result.length ? result[0] : null; +} + +/** + * @function 记录上级审批结果 + * @param {string} reg_id // 注册表regID + * @param {string} consent // 直属上级审批结果 1拒绝 2同意 + * @returns {bool} + * @author david + */ +exports.updateCaptainApprovalInfo = async (reg_id, consent) => { + let query = queryFormat('update tb_registration_history set consent = ?, isDeleted = ? where regID = ?', [consent, 1, reg_id]); + let result = await P(pool, 'query', query); + return result.legth ? result : null; +} + +/** + * @function 按时间戳更新注册信息的isDeleted状态为1 + * @param {string} min_date_time // 开始时间戳 + * @param {string} max_date_time // 结束时间戳 + * @author david + */ +exports.delRegistrationInfoByDateTime = async (min_date_time, max_date_time) => { + let query = queryFormat('update tb_registration_history set consent = ?, isDeleted = ? where UNIX_TIMESTAMP(createdAt) between ? and ?', [1, 1, min_date_time, max_date_time]); + await P(pool, 'query', query); +} + +/** + * @function 根据appAccountID获取用户账户信息 + * @param {string} app_account_id // 账号唯一标识符 + * @author david + */ +exports.getAccountInfoByAppAccountID = async (app_account_id) => { + let query = queryFormat(` + SELECT id, account, pubKey as pub_key, appAccountID as app_account_id, regID as reg_id, + lft, rgt, depth, cipherText as cipher_text, isDepartured as departured + FROM tb_accounts_info + WHERE appAccountID = ?`, [app_account_id]); + let result = await P(pool, 'query', query); + return result.length ? result[0] : null; +} + +/** + * @function 获取下级账号列表 + * @param {string} captain_account_id // 上级管理员账号唯一标识符 + * @param {number} depth // 下属账号所在的层级 + * @param {number} page, limit // 分页 + * @author david + */ +exports.getEmployeeAccountsByCaptainID = async (depth, lft, rgt, page, limit) => { + let start = (page - 1) * limit; + let end = limit; + let query_count = queryFormat(` + SELECT count(*) as count + FROM tb_accounts_info + WHERE depth = ? AND isDepartured = 0 AND lft BETWEEN ? AND ?`, [depth, lft, rgt]); + let query = queryFormat(` + SELECT acc.account, acc.isUploaded as is_uploaded, acc.cipherText as cipher_text, + acc.appAccountID as app_account_id, rh.captain as manager_account_id + FROM tb_accounts_info as acc + left join tb_registration_history as rh + on rh.id = acc.regID + WHERE acc.depth = ? AND acc.isDepartured = 0 AND acc.lft BETWEEN ? AND ? + ORDER BY acc.lft + limit ?, ?`, [depth, lft, rgt, start, end]); + let data_count = await P(pool, 'query', query_count); + let data = await P(pool, 'query', query); + for (let r of data) { + let account_info = await this.getAccountInfoByAppAccountID(r.app_account_id); + if (account_info) { + query_count = queryFormat(` + SELECT count(*) as count + FROM tb_accounts_info + WHERE depth = ? AND isDepartured = 0 AND lft BETWEEN ? AND ?`, [account_info.depth + 1, account_info.lft, account_info.rgt]); + let child_count = await P(pool, 'query', query_count); + r.employee_num = child_count[0].count; + } + } + return { + count: data_count[0].count, + total_pages: Math.ceil(data_count[0].count / limit), + data: data.length ? data : [] + } +} + +/** + * @function 根据account搜索用户账号信息 + * @param {string} account // 用户账号 + * @author david + */ +exports.searchAccountInfoByAccount = async (account, page, limit) => { + let start = (page - 1) * limit; + let end = limit; + let query_count = queryFormat(` + select count(*) as count from tb_accounts_info where account like ?`, ['%' + account + '%']); + let query = queryFormat(` + select acc.account, acc.isUploaded as is_uploaded, acc.appAccountID as app_account_id, + acc.cipherText as cipher_ext, rh.captain as manager_account_id + from tb_accounts_info as acc + left join tb_registration_history as rh + on rh.id = acc.regID + where acc.account like ? + limit ?, ?`, ['%' + account + '%', start, end]); + let data = await P(pool, 'query', query); + let data_count = await P(pool, 'query', query_count); + return { + count: data_count[0].count, + total_pages: Math.ceil(data_count[0].count / limit), + data: data.length ? data : [] + } +} + +/** + * @function 根据account_id获取用户账号信息 + * @param {string} account_id // 账号ID + * @returns { + * account: // 审批者账号 + * app_account_id: // 审批者账号唯一标识符 + * pub_key: // 审批者公钥 + * sign: // 审批者对该笔订单签名值 + * progress: // + * } + * @author david + */ +exports.getAccountInfoByAccountID = async (account_id) => { + let query = queryFormat(` + select acc.account, acc.appAccountID as app_account_id, acc.pubKey as pub_key, + rt.sign, ifnull(rt.comments, 0) as progress, lft, rgt, depth + from tb_accounts_info as acc + left join tb_review_transfer as rt + on rt.managerAccID = acc.id + where acc.id = ?`, [account_id]); + let data = await P(pool, 'query', query); + return data[0]; +} + +/** + * @function 获取用户账号直属下级账号信息 + * @param {number} depth + * @param {number} lft + * @param {number} rgt + * @author david + */ +exports.getUnderlingInfoByManagerAccountID = async (depth, lft, rgt) => { + let query = queryFormat(` + select appAccountID as app_account_id, account, cipherText as cipher_text + from tb_accounts_info + where depth = ? and lft between ? and ?`, [depth, lft, rgt]); + let data = await P(pool, 'query', query); + return data.length ? data : null; +} + +/** + * @function 插入新用户账户信息 + * @param {string} account // 用户账户 + * @param {string} app_account_id // 账号唯一标识符 + * @param {string} pub_key // 公钥 + * @param {string} cipher_text // 上级对该账户公钥生成的摘要信息 + * @param {string} en_pub_key // 上级对下级公钥的加密信息 + * @param {number} captain_account_rgt // 直属上级账号的rgt + * @param {number} registration_id // 注册表id + * @param {bool} is_uploaded // 该用户公钥是否上传到根节点账户 + * @param {number} depth // 该账号所在的节点深度 + * @author david + */ +exports.genAccount = async (account, app_account_id, pub_key, cipher_text, en_pub_key, captain_account_rgt, registration_id, is_uploaded, depth) => { + logger.info('生成账号', { + account: account, + app_account_id: app_account_id, + pub_key: pub_key, + cipher_text: cipher_text, + en_pub_key: en_pub_key, + captain_account_rgt: captain_account_rgt, + registration_id: registration_id, + is_uploaded: is_uploaded, + depth: depth + }) + if (captain_account_rgt == 0) { + let query = queryFormat('select ifnull(max(rgt), 0) as max_rgt from tb_accounts_info'); + let data = await P(pool, 'query', query); + captain_account_rgt = data[0].max_rgt + 1; + } + let conn = await P(pool, 'getConnection'); + try { + await P(conn, 'beginTransaction'); + let query_rgt = queryFormat('update tb_accounts_info set rgt = rgt + 2 where rgt >= ?', [captain_account_rgt]); + let query_lft = queryFormat('update tb_accounts_info set lft = lft + 2 where lft > ?', [captain_account_rgt]); + let query_add = queryFormat('insert into tb_accounts_info set account = ?, appAccountID = ?, regID = ?, pubKey = ?, enPubKey = ?, cipherText = ?, lft = ?, rgt = ?, isUploaded = ?, depth = ?', [account, app_account_id, registration_id, pub_key, en_pub_key, cipher_text, captain_account_rgt, captain_account_rgt + 1, is_uploaded, depth]); + await P(conn, 'query', query_lft); + await P(conn, 'query', query_rgt); + await P(conn, 'query', query_add); + await P(conn, 'commit'); + } catch (err) { + await P(conn, 'rollback'); + throw err; + } finally { + conn.release(); + } +} + +/** + * @function 根据下属账号获取对应的根节点账号信息 + * @param {number} lft rgt // 下属账号 lft和rgt + * @author david + */ +exports.getRootAccountByUnderlingAcc = async (lft, rgt) => { + let query = queryFormat('select id from tb_accounts_info where lft < ? and rgt > ? and depth = 0', [lft, rgt]); + let data = await P(pool, 'query', query); + return data.length ? data[0] : null; +} + +/** + * @function 根节点获取未被上传的下属的公钥信息列表 + * @param {string} app_account_id // 根节点账号唯一标识符 + * @returns [{ + * applyer: // 待上传公钥的员工账号唯一标识符 + * pub_key: // 该员工账号的公钥 + * captain: // 该员工账号直属上级账号唯一标识符 + * msg: // 直属上级对其公钥的加密信息 + * apply_at // 该员工账号申请创建时间戳 + * }] + * @author david + */ +exports.getEmployeeEnPubKeyInfoList = async (app_account_id) => { + let str = ` + SELECT t.applyer, acc.pubKey as pub_key, rh.captain as captain, + acc.enPubKey as msg, acc.cipherText as cipher_text, + UNIX_TIMESTAMP(acc.createdAt) as apply_at, acc.account as applyer_account + FROM( + SELECT node.appAccountID as applyer + FROM tb_accounts_info AS node, + tb_accounts_info AS parent + left join tb_registration_history as rh + on rh.applyer = parent.appAccountID + WHERE node.lft BETWEEN parent.lft AND parent.rgt + AND rh.captain = ? + ORDER BY node.lft) AS t + LEFT JOIN tb_accounts_info AS acc + ON acc.appAccountID = t.applyer + LEFT JOIN tb_registration_history AS rh + ON rh.id = acc.regID + WHERE acc.isUploaded = 0 AND acc.isDepartured = 0`; + let query = queryFormat(str, [app_account_id]); + let result = await P(pool, 'query', query); + let account_ids = [] + if (result.length) { + for (let r of result) { + account_ids.push(r.applyer); + } + } + return { + result: result, + account_ids: account_ids + } +} + +/** + * @function 标记指定下属的公钥已上传根节点账号 + * @param {array} account_ids // 下属账号唯一标识符 + * @author david + */ +exports.updateAccountsPubkeyUploadInfo = async (account_ids) => { + let query; + if (account_ids.length > 1) { + query = queryFormat('update tb_accounts_info set isUploaded = 1 where appAccountID in (?, ', account_ids[0]); + for (let i = 1; i < account_ids.length - 1; i++) { + query = query + queryFormat('?, ', account_ids[i]); + } + query = query + queryFormat('?)', account_ids[account_ids.length - 1]); + } else { + query = queryFormat('update tb_accounts_info set isUploaded = 1 where appAccountID = ?', account_ids); + } + await P(pool, 'query', query); +} + +/** + * @function 获取指定下属加密后的公钥信息 + * @param {string} app_account_id // 指定下属账号唯一标识符 + * @author david + */ +exports.getEmployeeEnPubKeyInfo = async (app_account_id) => { + let query = queryFormat(` + select acc.appAccountID as applyer, acc.pubKey as pub_key, rh.captain as captain, + acc.enPubKey as msg, acc.cipherText as cipher_text, + UNIX_TIMESTAMP(acc.createdAt) as apply_at, acc.account as applyer_account + from tb_accounts_info as acc + left join tb_registration_history as rh + on rh.id = acc.regID + where acc.appAccountID = ? `, [app_account_id]); + let result = await P(pool, 'query', query); + return result.length ? result[0] : null; +} + +/** + * @function 删除/替换下属账号 + * @param {string} app_account_id // 指定下属账号唯一标识符 + * @param {number} lft + * @param {string} employee_info // 下属账号信息 + */ +exports.changeEmployee = async (app_account_id, employee_info) => { + let elder_leader = await this.getAccountInfoByAppAccountID(app_account_id); + let query_del = queryFormat('update tb_accounts_info set isDepartured = ? where appAccountID = ?', [1, app_account_id]); + let query_in = queryFormat(`update tb_accounts_info set lft = lft -1, rgt = rgt - 1, depth = ? where lft between ? and ?`, [elder_leader.depth, elder_leader.lft + 1, elder_leader.rgt - 1]); + let query_out_rgt = queryFormat('update tb_accounts_info set rgt = rgt -2 where rgt > ?', [elder_leader.rgt]); + let query_out_lft = queryFormat('update tb_accounts_info set lft = lft - 2 where lft > ?', [elder_leader.rgt]); + let when_str = ''; + let query_upd, where_str; + if (employee_info.length) { + query_upd = queryFormat(` + UPDATE tb_accounts_info + SET cipherText = CASE appAccountID + WHEN ? THEN ?`, [employee_info[0].app_account_id, employee_info[0].cipher_text]); + where_str = queryFormat(`WHERE appAccountID IN (?`, [employee_info[0].app_account_id]); + for (let i = 1; i < employee_info.length; i++) { + when_str = when_str + queryFormat(` + WHEN ? THEN ? + `, [employee_info[i].app_account_id, employee_info[i].cipher_text]); + where_str = where_str + queryFormat(`, ?`, [employee_info[i].app_account_id]); + } + query_upd = query_upd + when_str + ` + END + ` + where_str + ')'; + + } + let conn = await P(pool, 'getConnection'); + let tx_id; + try { + await P(conn, 'beginTransaction'); + await P(conn, 'query', query_del); + await P(conn, 'query', query_in); + await P(conn, 'query', query_out_rgt); + await P(conn, 'query', query_out_lft); + if (query_upd) await P(conn, 'query', query_upd); + await P(conn, 'commit'); + } catch (err) { + await P(conn, 'rollback'); + throw err; + } finally { + conn.release(); + } +} + +/** + * @function 替换账号,更新上下级关系 + * @param {string} member_app_account_id // 新下属账号唯一标识符 + * @param {string} leader_id // 上级账号唯一标识符 + * @author david + */ +exports.replaceEmployee = async (member_app_account_id, leader_id) => { + let leader_info = await this.getAccountInfoByAccountID(leader_id); + let member = await this.getAccountInfoByAppAccountID(member_app_account_id); + if(leader_info.lft > member.lft) { + let query_mov_lft = queryFormat('update tb_accounts_info set lft = lft - 2 where isDepartured = 0 and lft > ?', [member.rgt]); + let query_mov_rgt = queryFormat('update tb_accounts_info set rgt = rgt - 2 where isDepartured = 0 and rgt > ?', [member.rgt]); + await P(pool, 'query', query_mov_lft); + await P(pool, 'query', query_mov_rgt); + leader_info = await this.getAccountInfoByAccountID(leader_id); + member = await this.getAccountInfoByAppAccountID(member_app_account_id); + } + let conn = await P(pool, 'getConnection'); + try { + await P(conn, 'beginTransaction'); + let query_rgt = queryFormat('update tb_accounts_info set rgt = rgt + 2 where rgt >= ? and isDepartured = 0', [leader_info.rgt]); + let query_lft = queryFormat('update tb_accounts_info set lft = lft + 2 where lft > ? and isDepartured = 0', [leader_info.rgt]); + let query_add = queryFormat('update tb_accounts_info set lft = ?, rgt = ?, depth = ? where appAccountID = ?', [leader_info.rgt, leader_info.rgt+1, leader_info.depth+1, member.app_account_id]); + await P(conn, 'query', query_rgt); + await P(conn, 'query', query_lft); + await P(conn, 'query', query_add); + await P(conn, 'commit'); + } catch (err) { + await P(conn, 'rollback'); + throw err; + } finally { + conn.release(); + } +} + +/** + * @function 向代理服务器提交注册申请信息 + * @param {string} msg // 员工APP提交的加密信息 + * @param {string} applyer_id // 申请者账号唯一标识符 + * @param {string} captain_id // 直属上级账号唯一标识符 + * @param {string} applyer_account // 新注册员工账号 + * @return void + * @author david + */ +exports.applyTegistrationToServer = async (reg_id, msg, applyer_id, captain_id, applyer_account, status) => { + let host = config.info.PROXY_HOST; + let url = config.info.SERVER_URL.REGISTRATION; + let params_obj = { + regid: reg_id, + msg: msg, + applyerid: applyer_id, + captainid: captain_id, + applyeraccount: applyer_account, + status: status + } + let result = await P(RPC, 'rpcRequest', 'POST', host, url, params_obj); + return result && result.RspNo == 0 ? true : false; +} + +/** + * @function 更新摘要信息 + * @param {array} employee_account_info // 用户信息 + * @param {array} cipher_texts // 需要更新的用户摘要信息 + * @author david + */ +exports.changeCipherInfo = async (employee_info, cipher_texts) => { + let data = []; + if (typeof cipher_texts != 'object') cipher_texts = JSON.parse(cipher_texts); + if (employee_info) { + if (!cipher_texts.length) return -1; + for (let r of cipher_texts) { + for (let c of employee_info) { + if (r.app_account_id == c.app_account_id) { + data.push({ + app_account_id: r.app_account_id, + cipher_text: r.cipher_text + }); + } + } + } + } + return data; +} \ No newline at end of file diff --git a/app/models/verify.js b/app/models/verify.js new file mode 100644 index 0000000..d0e2300 --- /dev/null +++ b/app/models/verify.js @@ -0,0 +1,75 @@ +// Copyright 2018. box.la authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +'use strict'; +const Crypto = require('crypto'); +const verify = Crypto.createVerify('SHA256'); +const P = require(global.config.info.DIR + '/utils/promise').P; +const dbhelper = require(global.config.info.DIR + '/utils/dbhelper'); +const pool = dbhelper.pool; +const queryFormat = dbhelper.queryFormat; +const Utils = require('../../utils/utils'); +const NodeRSA = require('node-rsa'); + +/** + * @function 验证客户端签名信息 + * @param {string} msg // 签名的原始信息 + * @param {string} pub_key // 公钥 + * @param {string} sign_info // 签名值 + * @returns {bool} + * @author david + */ +exports.signInfo = async (msg, pub_key, sign_info) => { + let p = await Utils.insert_str(pub_key, '\n', 64); + p = '-----BEGIN RSA PUBLIC KEY-----\n' + p + '-----END RSA PUBLIC KEY-----'; + let key = new NodeRSA(p); + let pass = key.verify(msg, sign_info, 'utf8', 'base64'); + return pass; +} + +/** + * @function 检测指定业务流模板哈希是否存在 + * @param {string} flow_hash // 审批流哈希值 + * @returns {bool} + * @author david + */ +exports.flowHashExists = async (flow_hash) => { + let query = queryFormat(`select id from tb_business_flow where flowHash = ?`, [flow_hash]); + let data = await P(pool, 'query', query); + return data.length ? true : false; +} + +/** + * @function 验证是否为私钥APP账号 + * @param {string} app_account_id // 账号唯一标识符 + * @return {bool} + * @author david + */ +exports.isAdminAccount = async (app_account_id) => { + let query = queryFormat('select id from tb_accounts_info where appAccountID = ?', [app_account_id]); + let data = await P(pool, 'query', query); + return data.length ? false : true; +} + +/** + * @function 验证是否提交过注册申请 + * @param {string} applyer_id 申请者账号唯一标识符 + * @param {string} captain_id 直属上级账号唯一标识符 + * @return {bool} + * @author david + */ +exports.hasApplyedRegistration = async (applyer_id, captain_id) => { + let query = queryFormat('select id from tb_registration_history where applyer = ? and captain = ? and isDeleted = 1', [applyer_id, captain_id]); + let data = await P(pool, 'query', query); + return data.length ? true : false; +} diff --git a/config.js.example b/config.js.example new file mode 100644 index 0000000..f16cfa2 --- /dev/null +++ b/config.js.example @@ -0,0 +1,33 @@ +'use strict'; + +exports.info = { + DIR: __dirname, + APP_NAME: 'service_user', + APP_DIR: __dirname + '/app/', + LOG_DIR: __dirname + '/log/', + ENV: "dev", + PORT: 5001, + API_VERSION: 'v1', + PROXY_HOST: // 代理服务器地址 + FIXED: 8, // 小数精度 + SERVER_URL: { // 代理服务器接口 + ADD_FLOW: '/agent/approvaladd', // 新建审批流 + APPLY_TRANSFER: '/agent/wtihdraw', // 申请转账 + REGISTRATION: '/agent/registadd', // 根节点申请注册 + COINLIST: '/agent/coinlist', // 币种列表 + TOKENLIST: '/agent/tokenlist', // 代币列表 + TOKEN_DEPOSIT_ADDRESS: '/agent/status', // 代币充值地址 + FLOW_STATUS: '/agent/approvaldetail' // flow哈希是否上链 + } +}; + +// 数据库配置 +exports.mysql = { + host: + user: + password: + database: + port: +}; + + diff --git a/log/.gitignore b/log/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/log/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..daf9cb3 --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "box_app_server", + "version": "0.1.0", + "description": "", + "main": "./server.js", + "dependencies": { + "bignumber.js": "^6.0.0", + "colors": "^1.1.2", + "json-format": "^1.0.1", + "koa": "^2.4.1", + "koa-bodyparser": "^4.2.0", + "koa-generic-session": "^2.0.0", + "koa-mount": "^3.0.0", + "koa-multer": "^1.0.2", + "koa-nunjucks-async": "^1.0.1", + "koa-router": "^7.3.0", + "koa-static": "^4.0.2", + "mini-logger": "^1.1.3", + "mysql": "^2.15.0", + "node-rsa": "^0.4.2", + "request": "^2.85.0", + "request-promise": "^4.2.2", + "secp256k1": "^3.4.0" + }, + "scripts": { + "start": "node server.js" + }, + "repository": { + "type": "git", + "url": "" + }, + "keywords": [ + "BOX" + ], + "author": "box.la", + "engines": { + "node": ">= 8.2.0" + } +} diff --git a/pm2.json b/pm2.json new file mode 100644 index 0000000..6c1a56f --- /dev/null +++ b/pm2.json @@ -0,0 +1,18 @@ +//相关配置参看这里 +//http://pm2.keymetrics.io/docs/usage/application-declaration/ +{ + "apps" : [{ + "name" : "boxv2", + "script" : "./server.js", + "merge_logs" : true, + "cwd" : "./", + "watch" : false, + "instances" : "1", + "exec_mode" : "cluster", + "out_file": "/dev/null", + "instance_var": "INSTANCE_ID", + "env": { + "NODE_ENV": "production" + } + }] +} \ No newline at end of file diff --git a/scripts/box.sql b/scripts/box.sql new file mode 100644 index 0000000..f026c8a --- /dev/null +++ b/scripts/box.sql @@ -0,0 +1,133 @@ +# **************************************************************** +# Copyright 2018. box.la authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ***************************************************************** + +#注册申请记录 +drop table if exists `tb_registration_history`; +CREATE TABLE `tb_registration_history` ( + id int(10) PRIMARY KEY AUTO_INCREMENT comment '自增ID, 主键' +, regID varchar(40) NOT NULL comment 'UUID' +, applyer varchar(20) NOT NULL comment '申请者' +, captain varchar(20) NOT NULL comment '直属上级' +, applyerAcc varchar(20) NOT NULL comment '申请者账号' +, msg varchar(1000) NOT NULL comment '注册信息' +, isDeleted tinyint(1) NOT NULL DEFAULT 0 comment '该条记录是否被删除' +, consent varchar(10) NOT NULL DEFAULT 0 comment '上级审批结果 1拒绝 2同意' +, createdAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP comment '申请创建时间' +)ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + +#用户帐号 +drop table if exists `tb_accounts_info`; +CREATE TABLE `tb_accounts_info` ( + id int(10) PRIMARY KEY AUTO_INCREMENT comment '账号ID' +, regID varchar(40) NOT NULL comment '对应的注册申请表ID' +, appAccountID varchar(20) NOT NULL comment 'app端记录的账号ID' +, account varchar(20) NOT NULL comment '帐号' +, pubKey varchar(1000) NOT NULL DEFAULT '' comment '公钥' +, enPubKey varchar(1000) NULL DEFAULT NULL comment '上级对下级公钥的加密信息' +, cipherText varchar(100) NULL DEFAULT NULL comment '上级对该账号公钥生成的信息摘要' +, isDepartured tinyint(1) NOT NULL DEFAULT 0 comment '是否离职' +, lft int(10) NOT NULL comment '左值' +, rgt int(10) NOT NULL comment '右值' +, isUploaded tinyint(1) NOT NULL DEFAULT 0 comment '该账户公钥是否已发送至根节点账户' +, depth int(10) NOT NULL comment '该账号所处的节点深度' +, createdAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP comment '帐号创建时间' +, updatedAt timestamp NULL DEFAULT NULL comment '更新时间' +)ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +#更新tb_accounts_info的 updatedAt +drop trigger if exists trg_accounts_info_update; +CREATE TRIGGER `trg_accounts_info_update` BEFORE UPDATE ON `tb_accounts_info` FOR EACH ROW set new.updatedAt=CURRENT_TIMESTAMP; + +#转账申请记录 +drop table if exists `tb_transfer`; +CREATE TABLE `tb_transfer` ( + id bigint(20) PRIMARY KEY AUTO_INCREMENT comment '自增ID' +, orderNum varchar(40) NOT NULL comment '转账记录ID' +, txInfo varchar(100) NULL DEFAULT NULL comment '订单信息' +, transBoxHash varchar(100) NULL comment 'transBox上链的哈希值' +, applyerID varchar(20) NOT NULL comment '申请员工帐号ID' +, currencyID int(10) NOT NULL comment '交易币种ID' +, amount varchar(10) NOT NULL comment '转账金额' +, flowID bigint(10) NOT NULL DEFAULT 1 comment '对应于哪个业务结构' +, progress tinyint(1) NOT NULL DEFAULT 0 comment '最终审批意见,0待审批 1审批中 2驳回 3审批同意' +, txID varchar(100) NULL DEFAULT NULL comment '对应公链的txid' +, applyContent varchar(1000) NOT NULL comment '申请者提交的转账信息' +, applyerSign varchar(1000) NOT NULL comment '申请者对该笔转账申请的签名' +, arrived tinyint(1) NOT NULL DEFAULT 0 comment '是否到账 1-打包中 2-到账' +, createdAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP comment '申请创建时间' +, updatedAt timestamp NULL DEFAULT NULL comment '更新时间' +)ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +#更新tb_transfer的 updatedAt +drop trigger if exists trg_transfer_update; +CREATE TRIGGER `trg_transfer_update` BEFORE UPDATE ON `tb_transfer` FOR EACH ROW set new.updatedAt=CURRENT_TIMESTAMP; + +#审批转账记录 +drop table if exists `tb_review_transfer`; +CREATE TABLE `tb_review_transfer` ( + transID bigint(20) NOT NULL comment '转账申请表的ID' +, managerAccID int(10) NOT NULL comment '账号ID' +, comments tinyint(1) NOT NULL DEFAULT 0 comment '审批意见,1驳回 2审批同意' +, sign varchar(1000) NULL DEFAULT NULL comment '审批者签名' +, createdAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP comment '审批创建时间' +)ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + +#币种配置列表 +drop table if exists `tb_currency`; +CREATE TABLE `tb_currency` ( + id int(10) PRIMARY KEY comment '自增ID' +, currency varchar(10) NOT NULL comment '充值地址' +, factor varchar(100) NULL DEFAULT NULL comment '货币转换因子' +, balance varchar(20) NOT NULL DEFAULT 0 comment '余额' +, isToken tinyint(1) NOT NULL DEFAULT 0 comment '是否是代币' +, address varchar(66) NULL comment '充值地址' +, available tinyint(1) NOT NULL DEFAULT 1 comment '该币种是否可用' +, updatedAt timestamp NULL DEFAULT CURRENT_TIMESTAMP comment '充值时间' +)ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +BEGIN; +INSERT INTO `tb_currency` VALUES (1, 'ETH', 18, '0', 0, null, 1, CURRENT_TIMESTAMP); +COMMIT; + +#审批业务结构 +drop table if exists `tb_business_flow`; +CREATE TABLE `tb_business_flow` ( + id bigint(20) PRIMARY KEY AUTO_INCREMENT comment '自增ID' +, flowID varchar(40) NOT NULL comment '业务流ID' +, flowHash varchar(100) NULL DEFAULT NULL comment '上链哈希值' +, flowName varchar(100) NOT NULL comment '业务结构名称' +, founderID int(10) NOT NULL comment '创建者账号ID' +, founderSign varchar(1000) NOT NULL comment '创建者对模板内容的签名' +, content text comment '业务结构内容' +, singleLimit varchar(10) NOT NULL comment '单笔转账限额' +, progress tinyint(1) NOT NULL DEFAULT 0 comment '审批流模板审批进度 0待审批 2审批拒绝 3审批通过' +, createdAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP comment '业务结构创建时间' +, updatedAt timestamp NULL DEFAULT NULL comment '审批更新时间' +-- , updatedAt timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP comment '审批更新时间' +)ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +#更新tb_business_flow的 updatedAt +drop trigger if exists trg_business_flow_update; +CREATE TRIGGER `trg_business_flow_update` BEFORE UPDATE ON `tb_business_flow` FOR EACH ROW set new.updatedAt=CURRENT_TIMESTAMP; + +#充值记录表 +drop table if exists `tb_deposit_history`; +CREATE TABLE `tb_deposit_history` ( + id bigint(20) PRIMARY KEY AUTO_INCREMENT comment '充值记录ID,主键' +, orderNum varchar(50) NOT NULL comment '订单号' +, fromAddr varchar(66) NOT NULL comment '付款方地址' +, toAddr varchar(66) NOT NULL comment '收款方地址' +, currencyID int(10) NOT NULL comment '币种ID' +, amount varchar(100) NULL DEFAULT NULL comment '充值金额' +, txID varchar(100) NULL DEFAULT NULL comment '对应公链的txid' +, updatedAt timestamp NULL DEFAULT CURRENT_TIMESTAMP comment '充值时间' +)ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; \ No newline at end of file diff --git a/server.js b/server.js new file mode 100644 index 0000000..73b9847 --- /dev/null +++ b/server.js @@ -0,0 +1,57 @@ +// Copyright 2018. box.la authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +'use strict'; +let config = require('./config'); +global.config = config; +Object.freeze(global.config); + +const koa = require('koa'); +const bodyParser = require('koa-bodyparser'); +const router = require('koa-router')(); +const httpLogger = require('./utils/logger').httpLogger; +const logger = require('./utils/logger').logger; +const Utils = require('./utils/utils'); +// const cors = require('cors'); +require('colors'); +const app = new koa(); +// http access logger +app.use(httpLogger); +// bodyParser for json and form +app.use(bodyParser({ + jsonLimit:'5mb', + textLimit:'5mb', + formLimit:'5mb' +})); +// global error handle +app.use(Utils.handleError); +// hang router +router.use("", require(config.info.APP_DIR).routes()); +app.use(router.routes()) + .use(router.allowedMethods({throw:true})); + +// 500 handler +app.on('error', function *(error){ + logger.error(error); +}); + +let port = config.info.PORT || 3000; +if(config.info.ENV === 'test'){ + module.exports = app; +}else{ + app.listen(port); + console.info('Application running in'.green,config.info.ENV.red,'environment.'.green); + console.info('You can now visit '.green + + ('http://localhost:'+config.info.PORT).underline.blue + + ' via your browser.'.green); +} diff --git a/static/lang/en_us.json b/static/lang/en_us.json new file mode 100644 index 0000000..5ed700a --- /dev/null +++ b/static/lang/en_us.json @@ -0,0 +1,57 @@ +{ + "1001": "Parameter can not be empty.", + "1002": "Account already exists.", + "1003": "Invalid signature information", + "1004": "Failed to create an account. Please try again later.", + "1005": "User not Found.", + "1006": "The registration application was not found.", + "1007": "The administrator you choose does not currently have permission to review your transfer request. Please check and try again.", + "1008": "The employee account was not found", + "1009": "Incorrect password, account activation failed.", + + "1000_USERI_INTIALIZE": "You have successfully initialized an account.", + "1000_TRANSFER_LIST": "Get transfer list successfully.", + "1000_EMPLOYEE_REGISTER": "Registration application submitted successfully.", + "1000_REGISTER_LIST": "Obtain employee registration information list successfully.", + "1000_APPROVAL_REGISTER": "Submit approval comments successfully.", + "1000_MANAGER_LIST": "Obtain the administrator account list successfully.", + "1000_EMPLOYEE_ACCOUNT_INFO": "Obtain employee account information successfully.", + "1000_EMPLOYEE_ACCOUNT_ACTIVE": "Your account has successfully actived.", + + "2001": "Your account is under review. Please try again later.", + "2002": "Your account has not been audited, please contact the administrator.", + "2003": "The employee account was not found", + "2004": "This transfer request was not found", + "2005": "No corresponding business process was found.", + "2006": "You have submitted approval comments, do not submit it again.", + "2007": "Subordinates did not complete the signature.", + "2008": "Insufficient permissions, please wait for the ", + "2009": " level administrator signs and tries again.", + "2010": "No right to approve.", + "2011": "Recipient address format error.", + "2012": "Recharge address has not been generated, please try again later.", + "2013": "The recharge address already exists.", + "2014": "Notify the recharge result failed.", + "2015": "The grade ", + "2016": "' signature information is invalid.", + "2017": "Your account has not been activated yet.", + + "2000_APPLY_TRANSFER": "Transfer application submitted successfully.", + "2000_EMPLOYEE_TRANSFER_LIST": "Get transfer record list successfully.", + "2000_TRANSFER_INFO": "Query transfer details successful.", + "2000_SIGN_TRANSFER": "Submission of audit opinions was successful.", + "2000_DEPOSIT_ADDRESS": "Get recharge address successfully.", + "2000_DEPOSIT_ADDRESS_ADD": "Added recharge address successfully.", + "2000_DEPOSIT_SUCCESS": "Notify the recharge result is successful.", + "2000_TRANSFER_SUCCESS": "Receive transfer information successfully.", + + "3001": "Business structure already exists, please do not create it again.", + "3002": "The business structure was not found.", + "3003": "The business structure hashes on the chain failed.", + "3004": "Business structure failed to create, all levels of user name can not be repeated.", + "3000_BUSINESS_FLOW": "Create business structure successfully.", + "3000_GET_BUSINESS_FLOW": "Obtain a list of existing business structures successfully.", + "3000_GET_BUSINESS_FLOW_INFO": "Details of the specified business structure to obtain success.", + "3000_GET_EMPLOYEE_LIST": "Obtain employee list successfully.", + "3000_APPROVAL_FLOW": "Submit approval comments successfully." +} \ No newline at end of file diff --git a/static/lang/zh_cn.json b/static/lang/zh_cn.json new file mode 100644 index 0000000..d57d0c3 --- /dev/null +++ b/static/lang/zh_cn.json @@ -0,0 +1,51 @@ +{ + "GEN_ACCOUNT": "提交信息成功。", + "GET_REGISTRATION": "获取注册申请信息成功。", + "APPROVAL_REGISTRATION": "提交注册审批意见成功。", + "EMPLOYEE_PUB_KEY": "获取员工公钥信息成功。", + "APPLY_TRANSFER": "提交转账申请成功。", + "TRANSFER_LIST": "获取转账列表成功。", + "TRANSFER_INFO": "获取转信息表成功。", + "FLOW_LIST": "获取审批流模板列表成功。", + "FLOW_INFO": "获取审批流模板详情成功。", + "GEN_FLOW": "创建业务流模板成功。", + "ACCOUNTS_LIST": "获取下属账号列表成功。", + "EMPLOYEE_PUBKEY_INFO": "获取员工公钥信息成功。", + "APPROVAL_TX": "提交审批意见成功。", + "GET_BALANCE": "获取余额成功。", + "CURRENCY_LIST": "获取币种列表成功。", + "EMPLOYEE_ACCOUNT_INFO": "获取员工账号详情成功。", + "DEL_EMPLOYEE": "删除下属账号成功。", + "REPLACE_EMPLOYEE": "替换成功。", + "WITHDRAW_SUCCESS": "通知提现记录成功。", + "NOTICE": "通知成功。", + + "1001": "参数不完整。", + "1002": "您已提交注册申请,请耐心等待。", + "1003": "未找到该注册申请。", + "1004": "指定账号不存在。", + "1005": "签名信息错误。", + "1006": "未找到对应的业务流程。", + "1007": "权限不足。", + "1008": "指定下级账号不存在。", + "1009": "注册失败,请稍候重试。", + "1010": "您的账号已经存在,请勿重复提交注册申请。", + "1011": "您的账号已被停用。", + "1012": "请求代理服务器失败。", + "1013": "指定下属账号已被停用。", + "1014": "直属上级账号已被停用。", + "1015": "非同级用户账号无法替换。", + + "2001": "转账信息有误,请查验后重新提交。", + "2002": "未找到对应币种。", + "2003": "无权审批该转账申请。", + "2004": "转账申请提交失败,请稍候重试。", + "2005": "未找到对应的转账申请。", + "2006": "您已提交审批意见,请勿重复提交。", + "2007": "转账信息哈希上链失败,请稍候重试。", + "2008": "未找到对应币种信息。", + + "3001": "您的账号暂无权限创建审批流模板。", + "3002": "指定业务流模板已存在,请勿重复提交。", + "3004": "创建审批流模板失败。" +} diff --git a/static/lang/zh_hk.json b/static/lang/zh_hk.json new file mode 100644 index 0000000..2282f22 --- /dev/null +++ b/static/lang/zh_hk.json @@ -0,0 +1,57 @@ +{ + "1001": "參數不能為空​​。", + "1002": "該帳號已存在。", + "1003": "簽名信息錯誤。", + "1004": "創建帳號失敗,請稍候重試。", + "1005": "該管理員帳號未註冊。", + "1006": "未找到該註冊申請。", + "1007": "您所選擇的管理員暫時沒有權限審核您的轉賬申請,請核對後重試。", + "1008": "未找到該員工帳號。", + "1009": "密碼錯誤,激活賬號失敗。", + + "1000_USERI_INTIALIZE": "賬戶初始化成功。", + "1000_TRANSFER_LIST": "獲取轉賬列表成功。", + "1000_EMPLOYEE_REGISTER": "註冊申請提交成功。", + "1000_REGISTER_LIST": "獲取員工註冊信息列表成功。", + "1000_APPROVAL_REGISTER": "提交審批意見成功。", + "1000_MANAGER_LIST": "獲取管理員帳號列表成功。", + "1000_EMPLOYEE_ACCOUNT_INFO": "獲取員工帳號信息成功。", + "1000_EMPLOYEE_ACCOUNT_ACTIVE": "激活賬號成功。", + + "2001": "您的帳號正在審核中,請稍候重試。", + "2002": "您的帳號未通過審核,請聯繫管理員。", + "2003": "未找到該員工帳號。", + "2004": "未找到該筆轉賬申請。", + "2005": "未找到對應的業務流程。", + "2006": "您已提交審批意見,請勿重複提交。", + "2007": "下級未完成簽名。", + "2008": "權限不足,請等待 ", + "2009": " 級管理員完成簽名後再試。", + "2010": "無權審批。", + "2011": "接收方地址格式錯誤。", + "2012": "充值地址尚未生成,請稍候再試。", + "2013": "該充值地址已經存在。", + "2014": "通知充值結果失敗。", + "2015": "第 ", + "2016": " 級簽名信息錯誤。", + "2017": "您的賬號尚未激活。", + + "2000_APPLY_TRANSFER": "轉賬申請提交成功。", + "2000_EMPLOYEE_TRANSFER_LIST": "獲取轉賬記錄列表成功。", + "2000_TRANSFER_INFO": "查詢轉賬詳情成功。", + "2000_SIGN_TRANSFER": "提交審核意見成功。", + "2000_DEPOSIT_ADDRESS": "獲取充值地址成功。", + "2000_DEPOSIT_ADDRESS_ADD": "新增充值地址成功。", + "2000_DEPOSIT_SUCCESS": "通知充值結果成功。", + "2000_TRANSFER_SUCCESS": "接收轉賬信息成功。", + + "3001": "業務結構已存在,請勿重複創建。", + "3002": "未找到該業務結構。", + "3003": "該業務結構哈希上鍊失敗。", + "3004": "創建業務結構失敗,各級用戶名不能重複。", + "3000_BUSINESS_FLOW": "創建業務結構成功。", + "3000_GET_BUSINESS_FLOW": "獲取已有業務結構列表成功。", + "3000_GET_BUSINESS_FLOW_INFO": "獲取指定業務結構詳情成功。", + "3000_GET_EMPLOYEE_LIST": "獲取員工列表成功。", + "3000_APPROVAL_FLOW": "提交審批意見成功。" +} diff --git a/utils/dbhelper.js b/utils/dbhelper.js new file mode 100644 index 0000000..dd18c71 --- /dev/null +++ b/utils/dbhelper.js @@ -0,0 +1,33 @@ +// Copyright 2018. box.la authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +'use strict'; + +const mysql = require('mysql'); +const config = global.config; + +const dbinfo = config.mysql; +const dbinfo_manage = config.mysql_manage; + +exports.pool = mysql.createPool({ + connectionLimit : 10, + host : dbinfo.host, + port : dbinfo.port, + user : dbinfo.user, + password : dbinfo.password, + database : dbinfo.database, + charset : 'UTF8_GENERAL_CI', + debug : false, + supportBigNumbers :true +}); +exports.queryFormat = mysql.format; diff --git a/utils/error.js b/utils/error.js new file mode 100644 index 0000000..789ebe9 --- /dev/null +++ b/utils/error.js @@ -0,0 +1,61 @@ +// Copyright 2018. box.la authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +'use strict'; +const en_us = require('../static/lang/en_us.json'); +const zh_cn = require('../static/lang/zh_cn.json'); +const zh_hk = require('../static/lang/zh_hk.json'); + +const obj = { + 'en_us' : en_us, + 'zh_cn' : zh_cn, + 'zh_hk': zh_hk +} + +class ExtendableError extends Error { + constructor(ctx, code, msg) { + let lang = ctx.header['content-language'] || 'zh_cn'; + super(obj[lang][code]); + + Object.defineProperty(this, 'code', { + enumerable : false, + value : code + }) + + Object.defineProperty(this, 'ctx', { + enumerable : false, + value : ctx + }) + + // extending Error is weird and does not propagate `message` + Object.defineProperty(this, 'message', { + enumerable : false, + value : msg?obj[lang][code]+msg+obj[lang][code+1]:obj[lang][code], + writable : true + }); + + if (Error.hasOwnProperty('captureStackTrace')) { + Error.captureStackTrace(this, this.constructor); + return; + } + + Object.defineProperty(this, 'stack', { + enumerable : false, + value : (new Error(obj[lang][code])).stack + }); + } +} + +module.exports = ExtendableError; + + diff --git a/utils/logger.js b/utils/logger.js new file mode 100644 index 0000000..b3c3557 --- /dev/null +++ b/utils/logger.js @@ -0,0 +1,67 @@ +// Copyright 2018. box.la authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +const Logger = require('mini-logger'); +const path = require('path'); +const config = global.config.info; +const jsonFormat = require("json-format"); + +var jsonOption = { + type: 'space', + size: 2 +} + +let logger = Logger({ + dir: path.join(config.DIR, 'log'), + categories: [ 'error', 'errorcode' ,'info' ], + format: '[{category}.]YYYY-MM-DD[.log]', + stdout: true, + timestamp: true +}); + +logger._options.categories.forEach(function(key){ + let fn = logger[key]; + logger[key] = function(){ + let err = new Error().stack; + let reg = new RegExp("at.*?" +global.config.info.DIR + ".*?\\:.*?\\:", "g"); + let paths = err.match(reg); + let path = paths[1].replace("("+global.config.info.DIR, '').replace(":",' ')+"\n"; + for(let j=0; j