xiongzhu 1 سال پیش
کامیت
1d59b9d962
100فایلهای تغییر یافته به همراه6757 افزوده شده و 0 حذف شده
  1. 49 0
      .env
  2. 49 0
      .env.production
  3. 24 0
      .eslintrc.js
  4. 41 0
      .gitignore
  5. 17 0
      .prettierrc
  6. 12 0
      .vscode/launch.json
  7. 21 0
      LICENSE
  8. 223 0
      README.md
  9. 2 0
      build.sh
  10. 20 0
      docker-compose.yml
  11. 6 0
      fix.js
  12. 0 0
      graph.json
  13. 18 0
      nest-cli.json
  14. 147 0
      package.json
  15. 2866 0
      src/aliyun/afs.ts
  16. 12 0
      src/aliyun/aliyun.module.ts
  17. 104 0
      src/aliyun/aliyun.service.ts
  18. 19 0
      src/aliyun/config/aliyun.config.ts
  19. 90 0
      src/app.module.ts
  20. 45 0
      src/auth/auth.controller.ts
  21. 30 0
      src/auth/auth.module.ts
  22. 69 0
      src/auth/auth.service.ts
  23. 11 0
      src/auth/dto/login.dto.ts
  24. 22 0
      src/auth/jwt-auth.guard.ts
  25. 13 0
      src/auth/jwt.config.ts
  26. 43 0
      src/auth/jwt.strategy.ts
  27. 4 0
      src/auth/public.decorator.ts
  28. 6 0
      src/auth/roles.decorator.ts
  29. 49 0
      src/auth/roles.guard.ts
  30. 32 0
      src/balance/balance.controller.ts
  31. 20 0
      src/balance/balance.module.ts
  32. 165 0
      src/balance/balance.service.ts
  33. 36 0
      src/balance/entities/balance-record.entities.ts
  34. 34 0
      src/balance/entities/balance.entities.ts
  35. 28 0
      src/channel/channel.controller.ts
  36. 18 0
      src/channel/channel.module.ts
  37. 29 0
      src/channel/channel.service.ts
  38. 26 0
      src/channel/entities/channel.entities.ts
  39. 10 0
      src/common/dto/page-request.ts
  40. 5 0
      src/common/enums/stream-platform.enum.ts
  41. 19 0
      src/device/device.controller.ts
  42. 13 0
      src/device/device.module.ts
  43. 66 0
      src/device/device.service.ts
  44. 28 0
      src/device/entities/device.entity.ts
  45. 87 0
      src/events/events.gateway.ts
  46. 10 0
      src/events/events.module.ts
  47. 19 0
      src/file/file.controller.ts
  48. 12 0
      src/file/file.module.ts
  49. 37 0
      src/file/file.service.ts
  50. 35 0
      src/filters/all-exceptions-filter.filter.ts
  51. 30 0
      src/helpers/configure-swagger-docs.helper.ts
  52. 7 0
      src/libs/danmaku-websocket.min.js
  53. 46 0
      src/main.ts
  54. 5 0
      src/model/role.enum.ts
  55. 22 0
      src/phone-list/entities/phone-list.entity.ts
  56. 16 0
      src/phone-list/entities/phone.entity.ts
  57. 74 0
      src/phone-list/phone-list.controller.ts
  58. 14 0
      src/phone-list/phone-list.module.ts
  59. 80 0
      src/phone-list/phone-list.service.ts
  60. 57 0
      src/rcs-number/entities/rcs-number.entity.ts
  61. 33 0
      src/rcs-number/helpers.ts
  62. 5 0
      src/rcs-number/impl/get-number-service.ts
  63. 163 0
      src/rcs-number/impl/mwze167.service.ts
  64. 31 0
      src/rcs-number/rcs-number.controller.ts
  65. 17 0
      src/rcs-number/rcs-number.module.ts
  66. 37 0
      src/rcs-number/rcs-number.service.ts
  67. 81 0
      src/rcs-number/usacode-api-service.ts
  68. 7 0
      src/repl.ts
  69. 26 0
      src/shared/hashing/bcrypt.service.spec.ts
  70. 15 0
      src/shared/hashing/bcrypt.service.ts
  71. 24 0
      src/shared/hashing/hashing.service.spec.ts
  72. 7 0
      src/shared/hashing/hashing.service.ts
  73. 10 0
      src/shared/mailer/mailer.module.ts
  74. 19 0
      src/shared/mailer/mailer.service.spec.ts
  75. 39 0
      src/shared/mailer/mailer.service.ts
  76. 8 0
      src/shared/utils/utils.module.ts
  77. 18 0
      src/shared/utils/utils.service.spec.ts
  78. 9 0
      src/shared/utils/utils.service.ts
  79. 24 0
      src/sms/dto/sms.dto.ts
  80. 19 0
      src/sms/entities/sms.entity.ts
  81. 25 0
      src/sms/sms.controller.ts
  82. 14 0
      src/sms/sms.module.ts
  83. 38 0
      src/sms/sms.service.ts
  84. 23 0
      src/statistics/statistics.controller.ts
  85. 29 0
      src/statistics/statistics.module.ts
  86. 100 0
      src/statistics/statistics.service.ts
  87. 31 0
      src/sys-config/entities/sys-config.entity.ts
  88. 18 0
      src/sys-config/sys-config.admin.controller.ts
  89. 27 0
      src/sys-config/sys-config.controller.ts
  90. 14 0
      src/sys-config/sys-config.module.ts
  91. 96 0
      src/sys-config/sys-config.service.ts
  92. 33 0
      src/task/entities/task-item.entity.ts
  93. 61 0
      src/task/entities/task.entity.ts
  94. 60 0
      src/task/task.controller.ts
  95. 30 0
      src/task/task.module.ts
  96. 407 0
      src/task/task.service.ts
  97. 13 0
      src/transformers/array.transformer.ts
  98. 11 0
      src/transformers/decimal.transformer.ts
  99. 10 0
      src/transformers/json.transformer.ts
  100. 33 0
      src/users/dto/user-create.dto.ts

+ 49 - 0
.env

@@ -0,0 +1,49 @@
+NODE_ENV=dev
+NODE_API_PORT=
+
+ENDPOINT_CORS=
+
+SWAGGER_USER=admin
+SWAGGER_PASSWORD=123456
+
+EMAIL_HOST="smtp.mailtrap.io"
+EMAIL_PORT=2525
+EMAIL_AUTH_USER="1"
+EMAIL_AUTH_PASSWORD="1"
+EMAIL_DEBUG=true
+EMAIL_LOGGER=true
+EMAIL_LAYOUT_DIR='/templates/emails/'
+EMAIL_PARTIAL_DIR='/templates/emails/'
+EMAIL_VIEW_PATH='/templates/emails/'
+EMAIL_DEFAULT_LAYOUT='index'
+
+JWT_SECRET_KEY="9n^z27*5%S7H%^TA"
+JWT_TOKEN_AUDIENCE="localhost:3000"
+JWT_TOKEN_ISSUER="localhost:3000"
+JWT_ACCESS_TOKEN_TTL=3600
+
+THROTTLE_TTL=60
+THROTTLE_LIMIT=20
+
+TYPEORM_CONNECTION="mysql"
+TYPEORM_HOST="rdsave1o67m1ido6gwp6public.mysql.rds.aliyuncs.com"
+TYPEORM_PORT=3306
+TYPEORM_USERNAME=zouma
+TYPEORM_PASSWORD="2wsx@WSX#EDC"
+TYPEORM_DATABASE=rcs_submit
+TYPEORM_AUTO_SCHEMA_SYNC=true
+TYPEORM_ENTITIES="dist/**/*.entity.js"
+TYPEORM_SUBSCRIBERS="dist/subscriber/**/*.js"
+TYPEORM_MIGRATIONS="dist/migrations/**/*.js"
+TYPEORM_ENTITIES_DIR="src/entity"
+TYPEORM_MIGRATIONS_DIR="src/migrations"
+TYPEORM_SUBSCRIBERS_DIR="src/subscriber"
+
+ALIYUN_ACCESS_KEY_ID=PXzJyah5rZfWHIIH
+ALIYUN_ACCESS_KEY_SECRET=e1MS6j0wypXJrw8CM0hObZu8qKbfah
+ALIYUN_OSS_ENDPOINT=oss-cn-hangzhou.aliyuncs.com
+ALIYUN_OSS_BUCKET=nebuai
+ALIYUN_OSS_REGION=oss-cn-hangzhou
+ALIYUN_OSS_CDN=https://cdn.raex.vip
+ALIYUN_SMS_SIGN=走马信息
+ALIYUN_SMS_TEMPLATE_CODE=SMS_175485688

+ 49 - 0
.env.production

@@ -0,0 +1,49 @@
+NODE_ENV=production
+NODE_API_PORT=3003
+
+ENDPOINT_CORS=
+
+SWAGGER_USER=admin
+SWAGGER_PASSWORD=123456
+
+EMAIL_HOST="smtp.mailtrap.io"
+EMAIL_PORT=2525
+EMAIL_AUTH_USER="1"
+EMAIL_AUTH_PASSWORD="1"
+EMAIL_DEBUG=true
+EMAIL_LOGGER=true
+EMAIL_LAYOUT_DIR='/templates/emails/'
+EMAIL_PARTIAL_DIR='/templates/emails/'
+EMAIL_VIEW_PATH='/templates/emails/'
+EMAIL_DEFAULT_LAYOUT='index'
+
+JWT_SECRET_KEY="&udNgnwba%4UspeC"
+JWT_TOKEN_AUDIENCE="localhost:3000"
+JWT_TOKEN_ISSUER="localhost:3000"
+JWT_ACCESS_TOKEN_TTL=3600
+
+THROTTLE_TTL=60
+THROTTLE_LIMIT=20
+
+TYPEORM_CONNECTION="mysql"
+TYPEORM_HOST="rdsave1o67m1ido6gwp6public.mysql.rds.aliyuncs.com"
+TYPEORM_PORT=3306
+TYPEORM_USERNAME=zouma
+TYPEORM_PASSWORD="2wsx@WSX#EDC"
+TYPEORM_DATABASE=rcs_submit
+TYPEORM_AUTO_SCHEMA_SYNC=true
+TYPEORM_ENTITIES="dist/**/*.entity.js"
+TYPEORM_SUBSCRIBERS="dist/subscriber/**/*.js"
+TYPEORM_MIGRATIONS="dist/migrations/**/*.js"
+TYPEORM_ENTITIES_DIR="src/entity"
+TYPEORM_MIGRATIONS_DIR="src/migrations"
+TYPEORM_SUBSCRIBERS_DIR="src/subscriber"
+
+ALIYUN_ACCESS_KEY_ID=PXzJyah5rZfWHIIH
+ALIYUN_ACCESS_KEY_SECRET=e1MS6j0wypXJrw8CM0hObZu8qKbfah
+ALIYUN_OSS_ENDPOINT=oss-cn-hangzhou.aliyuncs.com
+ALIYUN_OSS_BUCKET=nebuai
+ALIYUN_OSS_REGION=oss-cn-hangzhou
+ALIYUN_OSS_CDN=https://cdn.raex.vip
+ALIYUN_SMS_SIGN=走马信息
+ALIYUN_SMS_TEMPLATE_CODE=SMS_175485688

+ 24 - 0
.eslintrc.js

@@ -0,0 +1,24 @@
+module.exports = {
+  parser: '@typescript-eslint/parser',
+  parserOptions: {
+    project: 'tsconfig.json',
+    sourceType: 'module',
+  },
+  plugins: ['@typescript-eslint/eslint-plugin'],
+  extends: [
+    'plugin:@typescript-eslint/eslint-recommended',
+    'plugin:@typescript-eslint/recommended',
+    'prettier',
+    'prettier/@typescript-eslint',
+  ],
+  root: true,
+  env: {
+    node: true,
+    jest: true,
+  },
+  rules: {
+    '@typescript-eslint/interface-name-prefix': 'off',
+    '@typescript-eslint/explicit-function-return-type': 'off',
+    '@typescript-eslint/no-explicit-any': 'off',
+  },
+};

+ 41 - 0
.gitignore

@@ -0,0 +1,41 @@
+# compiled output
+/dist
+/node_modules
+
+# Enviroment
+# .env
+# .env.dev
+# .env.stage
+# .env.prod
+
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+
+# OS
+.DS_Store
+
+# Tests
+/coverage
+/.nyc_output
+
+# IDEs and editors
+/.idea
+.project
+.classpath
+.c9/
+*.launch
+.settings/
+*.sublime-workspace
+tags
+
+# IDE - VSCode
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json

+ 17 - 0
.prettierrc

@@ -0,0 +1,17 @@
+{
+    "singleQuote": true,
+    "trailingComma": "none",
+    "semi": false,
+    "printWidth": 120,
+    "tabWidth": 4,
+    "overrides": [
+        {
+            "files": "*.hbs",
+            "options": {
+                "singleQuote": false,
+                "semi": true,
+                "parser": "html"
+            }
+        }
+    ]
+}

+ 12 - 0
.vscode/launch.json

@@ -0,0 +1,12 @@
+{
+    "version": "0.2.0",
+    "configurations": [
+        {
+            "type": "node-terminal",
+            "name": "运行脚本: start:debug",
+            "request": "launch",
+            "command": "npm run start:debug",
+            "cwd": "${workspaceFolder}"
+        }
+    ]
+}

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019-2023 Tony133
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 223 - 0
README.md

@@ -0,0 +1,223 @@
+# NestJSApiBoilerplateJWT
+
+An API Boilerplate to create a ready-to-use REST API in seconds with NestJS 9.x and JWT Auth System :heart_eyes_cat:
+
+## Installation
+
+```bash
+   $ npm install
+```
+
+## Set Enviroment for secret key JWT and other configurations
+
+```bash
+   $ cp .env.example .env
+```
+
+To set up on multiple environments, such as dev, stage or prod, we do as follows:
+
+```bash
+   $ cp .env.example .env.dev # or .env.stage, etc
+```
+
+## Config settings .env for send notification when a user registers, forgot password or change password
+
+```
+   EMAIL_HOST=smtp.mailtrap.io
+   EMAIL_PORT=2525
+   EMAIL_AUTH_USER=[:user]
+   EMAIL_AUTH_PASSWORD=[:password]
+   EMAIL_DEBUG=true
+   EMAIL_LOGGER=true
+   EMAIL_LAYOUT_DIR='templates/emails/'
+   EMAIL_PARTIAL_DIR='templates/emails/'
+   EMAIL_VIEW_PATH='/templates/emails/'
+   EMAIL_DEFAULT_LAYOUT='index'
+```
+
+## Config settings .env for connect MySQL
+
+Once the database has been configured, start the Nest App via `npm run start:dev` it automatically synchronizes the entities so ready to use. :heart_eyes_cat:
+
+```
+   TYPEORM_CONNECTION = "mysql"
+   TYPEORM_HOST = "localhost"
+   TYPEORM_PORT = 3306
+   TYPEORM_USERNAME = [:user]
+   TYPEORM_PASSWORD = [:password]
+   TYPEORM_DATABASE = [:database]
+   TYPEORM_AUTO_SCHEMA_SYNC = true
+   TYPEORM_ENTITIES = "dist/**/*.entity.js"
+   TYPEORM_SUBSCRIBERS = "dist/subscriber/**/*.js"
+   TYPEORM_MIGRATIONS = "dist/migrations/**/*.js"
+   TYPEORM_ENTITIES_DIR = "src/entity"
+   TYPEORM_MIGRATIONS_DIR = "src/migration"
+   TYPEORM_SUBSCRIBERS_DIR = "src/subscriber"
+```
+
+## Install TypeScript Node
+
+```bash
+   $ npm install -g ts-node
+```
+
+## Running migrations with typeorm
+
+```bash
+   $ ts-node node_modules/.bin/typeorm migration:run -d dist/typeorm-cli.config
+```
+
+or
+
+```bash
+   $ node_modules/.bin/typeorm migration:run -d dist/typeorm-cli.config
+```
+
+## Running the app
+
+```bash
+    # development
+    $ npm run start
+
+    # watch mode
+    $ npm run start:dev
+
+    # production mode
+    $ npm run start:prod
+```
+
+## Runnig the app in REPL mode
+
+```bash
+   $ npm run start -- --entryFile repl
+```
+
+or
+
+```bash
+   $ npm run start:repl
+```
+
+## Docker
+
+There is a `docker-compose.yml` file for starting MySQL with Docker.
+
+`$ docker-compose up db`
+
+After running, you can stop the Docker container with
+
+`$ docker-compose down`
+
+
+## Url Swagger for Api Documentation
+
+```
+
+http://127.0.0.1:3000/docs
+
+```
+or
+
+```
+
+http://127.0.0.1:3000/docs-json
+
+```
+or
+
+```
+
+http://127.0.0.1:3000/docs-yaml
+
+```
+
+Configure `SWAGGER_USER` and `SWAGGER_PASSWORD` in the .env file and set `NODE_ENV` to `local` or `dev`or `staging` to access the SWAGGER(Open Api) documentation with basic authentication.
+
+```
+
+NODE_ENV=[:enviroments]
+SWAGGER_USER=[:user]
+SWAGGER_PASSWORD=[:password]
+
+````
+
+If you want to add more environments, include them in the `SWAGGER_ENVS` array in `main.ts`, see the following:
+
+```typescript
+const SWAGGER_ENVS = ['local', 'dev', 'staging'];
+````
+
+## Configuring the NODE_API_PORT environment variable as the default port if you don't want to use the default
+
+```
+   NODE_API_PORT=3333
+```
+
+## Configuring the ENDPOINT_CORS environment variable for app frontend
+
+```
+   ENDPOINT_CORS='http://127.0.0.1:4200'
+```
+
+## Getting secure resource with Curl
+
+```bash
+    $ curl -H 'content-type: application/json' -v -X GET http://127.0.0.1:3000/api/secure  -H 'Authorization: Bearer [:token]'
+```
+
+## Generate Token JWT Authentication with Curl
+
+```bash
+   $ curl -H 'content-type: application/json' -v -X POST -d '{"email": "tony_admin@nest.it", "password": "secret"}' http://127.0.0.1:3000/api/auth/login
+
+```
+
+## Registration user with Curl
+
+```bash
+   $ curl -H 'content-type: application/json' -v -X POST -d '{"name": "tony", "email": "tony_admin@nest.it", "username":"tony_admin", "password": "secret"}' http://127.0.0.1:3000/api/auth/register
+
+```
+
+## Forgot password with curl
+
+```bash
+   $ curl -H 'content-type: application/json' -v -X POST -d '{"email": "tony_admin@nest.it"}' http://127.0.0.1:3000/api/auth/forgot-password
+```
+
+## Change password User with curl
+
+```bash
+   $ curl -H 'content-type: application/json' -v -X POST -d '{"email": "tony_admin@nest.it", "password": "secret123"}' http://127.0.0.1:3000/api/auth/change-password  -H 'Authorization: Bearer [:token]'
+```
+
+## Update profile User with curl
+
+```bash
+   $ curl -H 'content-type: application/json' -v -X PUT -d '{"name": "tony", "email": "tony_admin@nest.it", "username": "tony_admin"}' http://127.0.0.1:3000/api/users/:id/profile  -H 'Authorization: Bearer [:token]'
+```
+
+## Users list with Curl
+
+```bash
+   $ curl -H 'content-type: application/json' -H 'Accept: application/json' -v -X GET http://127.0.0.1:3000/api/users  -H 'Authorization: Bearer [:token]'
+```
+
+## User by Id with Curl
+
+```bash
+   $ curl -H 'content-type: application/json' -H 'Accept: application/json' -v -X GET http://127.0.0.1:3000/api/users/:id  -H 'Authorization: Bearer [:token]'
+```
+
+## Update User with Curl
+
+```bash
+   $ curl -H 'content-type: application/json' -v -X PATCH -d '{"name": "tony", "email": "tony_admin@nest.it", "username": "tony_admin", "password":"secret"}' http://127.0.0.1:3000/api/users/:id  -H 'Authorization: Bearer [:token]'
+```
+
+## Delete User by Id with Curl
+
+```bash
+   $ curl -H 'content-type: application/json' -H 'Accept: application/json' -v -X DELETE http://127.0.0.1:3000/api/users/:id  -H 'Authorization: Bearer [:token]'
+```
+## test submit

+ 2 - 0
build.sh

@@ -0,0 +1,2 @@
+yarn build
+cp .env.production dist/.env

+ 20 - 0
docker-compose.yml

@@ -0,0 +1,20 @@
+version: "3"
+
+services:
+  db:
+    image: mysql:8.0
+    restart: always
+    environment:
+      MYSQL_ROOT_PASSWORD: root
+      MYSQL_DATABASE: nest
+    ports:
+      - "3306:3306"
+
+  db-test:
+    image: mysql:8.0
+    restart: always
+    environment:
+      MYSQL_ROOT_PASSWORD: root
+      MYSQL_DATABASE: test
+    ports:
+      - "3307:3306"

+ 6 - 0
fix.js

@@ -0,0 +1,6 @@
+const fs = require('fs')
+
+const package = JSON.parse(fs.readFileSync('./node_modules/bignumber.js/package.json', 'utf8'))
+package.exports['./bignumber'] = './bignumber.js'
+fs.writeFileSync('./node_modules/bignumber.js/package.json', JSON.stringify(package, null, 2))
+console.log('Fixed bignumber.js package.json')

+ 0 - 0
graph.json


+ 18 - 0
nest-cli.json

@@ -0,0 +1,18 @@
+{
+    "collection": "@nestjs/schematics",
+    "sourceRoot": "src",
+    "compilerOptions": {
+        "deleteOutDir": true,
+        "plugins": [
+            "@nestjs/swagger"
+        ],
+        "assets": [
+            {
+                "include": "../views",
+                "outDir": "dist/views",
+                "watchAssets": true
+            }
+        ],
+        "watchAssets": true
+    }
+}

+ 147 - 0
package.json

@@ -0,0 +1,147 @@
+{
+  "name": "nestjs-api-boilerplate-jwt",
+  "version": "1.0.0",
+  "description": "An API Boilerplate to create a ready-to-use REST API in seconds with NestJS 9.x and Auth JWT System",
+  "author": "Tony133",
+  "license": "MIT",
+  "scripts": {
+    "prebuild": "rimraf dist",
+    "build": "nest build",
+    "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
+    "start": "nest start",
+    "start:dev": "nest start --watch",
+    "start:debug": "nest start --debug --watch",
+    "start:prod": "node dist/main",
+    "start:repl": "nest start --entryFile repl",
+    "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
+    "test": "jest",
+    "test:watch": "jest --watch",
+    "test:cov": "jest --coverage",
+    "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
+    "test:e2e": "jest --config ./test/jest-e2e.json",
+    "postinstall": "node fix.js"
+  },
+  "dependencies": {
+    "@alicloud/dysmsapi20170525": "2.0.23",
+    "@dqbd/tiktoken": "^1.0.6",
+    "@esm2cjs/p-timeout": "^6.0.0",
+    "@fidm/x509": "^1.2.1",
+    "@keyv/mysql": "^1.6.3",
+    "@keyv/redis": "^2.5.7",
+    "@nestjs/axios": "^2.0.0",
+    "@nestjs/common": "^9.3.3",
+    "@nestjs/config": "^2.3.1",
+    "@nestjs/core": "^9.3.3",
+    "@nestjs/devtools-integration": "^0.1.4",
+    "@nestjs/jwt": "^10.0.1",
+    "@nestjs/mapped-types": "1.2.2",
+    "@nestjs/mongoose": "^9.2.2",
+    "@nestjs/passport": "^9.0.3",
+    "@nestjs/platform-express": "^9.3.3",
+    "@nestjs/platform-socket.io": "^10.3.8",
+    "@nestjs/schedule": "^3.0.4",
+    "@nestjs/swagger": "^6.2.1",
+    "@nestjs/throttler": "^4.0.0",
+    "@nestjs/typeorm": "^9.0.1",
+    "@nestjs/websockets": "^10.3.8",
+    "@types/ws": "^8.5.10",
+    "ali-oss": "^6.17.1",
+    "axios": "^1.3.6",
+    "bcrypt": "^5.1.0",
+    "big.js": "^6.2.1",
+    "bignumber.js": "^9.1.1",
+    "class-transformer": "^0.5.1",
+    "class-validator": "^0.13.0",
+    "crypto": "^1.0.1",
+    "date-fns": "^2.29.3",
+    "decimal.js": "^10.4.3",
+    "dedent": "^1.5.1",
+    "douyudm": "^2.1.1",
+    "eventsource-parser": "^1.0.0",
+    "exceljs": "^4.4.0",
+    "express-basic-auth": "^1.2.1",
+    "express-handlebars": "^7.0.6",
+    "handlebars": "^4.7.7",
+    "handlebars-helpers": "^0.10.0",
+    "hbs": "^4.2.0",
+    "http-proxy-agent": "^7.0.0",
+    "https-proxy-agent": "^7.0.2",
+    "ioredis": "^5.3.2",
+    "isomorphic-fetch": "^3.0.0",
+    "keyv": "^4.5.2",
+    "moment": "^2.30.1",
+    "mysql2": "^3.1.2",
+    "nestjs-typeorm-paginate": "^4.0.3",
+    "nodemailer": "^6.9.1",
+    "p-timeout": "^6.1.1",
+    "passport": "^0.6.0",
+    "passport-http-bearer": "^1.0.1",
+    "passport-jwt": "^4.0.1",
+    "pem": "^1.14.7",
+    "quick-lru": "^5.0.0",
+    "randomstring": "^1.2.3",
+    "reflect-metadata": "^0.1.13",
+    "rimraf": "^4.1.2",
+    "rxjs": "^7.8.0",
+    "typeorm": "^0.3.12",
+    "uuid": "^9.0.0",
+    "ws": "^8.14.2",
+    "xlsx": "^0.18.5",
+    "yup": "^1.0.0"
+  },
+  "devDependencies": {
+    "@inquirer/prompts": "^1.0.0",
+    "@nestjs/cli": "^9.2.0",
+    "@nestjs/schematics": "^9.0.4",
+    "@nestjs/testing": "^9.3.3",
+    "@types/express": "^4.17.17",
+    "@types/jest": "^29.4.0",
+    "@types/multer": "^1.4.7",
+    "@types/node": "^18.14.1",
+    "@types/nodemailer": "^6.4.7",
+    "@types/nodemailer-express-handlebars": "^4.0.2",
+    "@types/supertest": "^2.0.12",
+    "@types/uuid": "^9.0.1",
+    "@typescript-eslint/eslint-plugin": "^5.53.0",
+    "@typescript-eslint/parser": "^5.53.0",
+    "eslint": "^8.34.0",
+    "eslint-config-prettier": "^8.6.0",
+    "eslint-plugin-prettier": "^4.2.1",
+    "inquirer": "^9.2.0",
+    "jest": "^29.4.1",
+    "nodemailer-express-handlebars": "^6.0.0",
+    "prettier": "^2.8.7",
+    "supertest": "^6.3.3",
+    "ts-jest": "^29.0.5",
+    "ts-loader": "^9.4.2",
+    "ts-node": "^10.9.1",
+    "tsconfig-paths": "^4.1.2",
+    "typescript": "^4.9.5"
+  },
+  "jest": {
+    "moduleFileExtensions": [
+      "js",
+      "json",
+      "ts"
+    ],
+    "rootDir": "src",
+    "testRegex": ".spec.ts$",
+    "transform": {
+      "^.+\\.(t|j)s$": "ts-jest"
+    },
+    "collectCoverageFrom": [
+      "**/*.{!(module),}.(t|j)s"
+    ],
+    "coveragePathIgnorePatterns": [
+      "/src/migrations",
+      "/src/helpers",
+      "/src/main.ts",
+      "/src/repl.ts",
+      ".constants.ts",
+      ".guard.ts",
+      ".config.ts"
+    ],
+    "coverageDirectory": "../coverage",
+    "testEnvironment": "node"
+  }
+}

+ 2866 - 0
src/aliyun/afs.ts

@@ -0,0 +1,2866 @@
+// This file is auto-generated, don't edit it
+/**
+ *
+ */
+import Util, * as $Util from '@alicloud/tea-util'
+import OpenApi, * as $OpenApi from '@alicloud/openapi-client'
+import EndpointUtil from '@alicloud/endpoint-util'
+import * as $tea from '@alicloud/tea-typescript'
+
+export class AnalyzeNvcRequest extends $tea.Model {
+    sourceIp?: string
+    scoreJsonStr?: string
+    data?: string
+    static names(): { [key: string]: string } {
+        return {
+            sourceIp: 'SourceIp',
+            scoreJsonStr: 'ScoreJsonStr',
+            data: 'Data'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            sourceIp: 'string',
+            scoreJsonStr: 'string',
+            data: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class AnalyzeNvcResponseBody extends $tea.Model {
+    requestId?: string
+    bizCode?: string
+    static names(): { [key: string]: string } {
+        return {
+            requestId: 'RequestId',
+            bizCode: 'BizCode'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            requestId: 'string',
+            bizCode: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class AnalyzeNvcResponse extends $tea.Model {
+    headers: { [key: string]: string }
+    body: AnalyzeNvcResponseBody
+    static names(): { [key: string]: string } {
+        return {
+            headers: 'headers',
+            body: 'body'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            headers: { type: 'map', keyType: 'string', valueType: 'string' },
+            body: AnalyzeNvcResponseBody
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class AuthenticateSigRequest extends $tea.Model {
+    sourceIp?: string
+    sessionId?: string
+    appKey?: string
+    sig?: string
+    token?: string
+    scene?: string
+    remoteIp?: string
+    static names(): { [key: string]: string } {
+        return {
+            sourceIp: 'SourceIp',
+            sessionId: 'SessionId',
+            appKey: 'AppKey',
+            sig: 'Sig',
+            token: 'Token',
+            scene: 'Scene',
+            remoteIp: 'RemoteIp'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            sourceIp: 'string',
+            sessionId: 'string',
+            appKey: 'string',
+            sig: 'string',
+            token: 'string',
+            scene: 'string',
+            remoteIp: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class AuthenticateSigResponseBody extends $tea.Model {
+    msg?: string
+    requestId?: string
+    riskLevel?: string
+    code?: number
+    detail?: string
+    static names(): { [key: string]: string } {
+        return {
+            msg: 'Msg',
+            requestId: 'RequestId',
+            riskLevel: 'RiskLevel',
+            code: 'Code',
+            detail: 'Detail'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            msg: 'string',
+            requestId: 'string',
+            riskLevel: 'string',
+            code: 'number',
+            detail: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class AuthenticateSigResponse extends $tea.Model {
+    headers: { [key: string]: string }
+    body: AuthenticateSigResponseBody
+    static names(): { [key: string]: string } {
+        return {
+            headers: 'headers',
+            body: 'body'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            headers: { type: 'map', keyType: 'string', valueType: 'string' },
+            body: AuthenticateSigResponseBody
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class ConfigurationStyleRequest extends $tea.Model {
+    sourceIp?: string
+    applyType?: string
+    scene?: string
+    configurationMethod?: string
+    refExtId?: string
+    static names(): { [key: string]: string } {
+        return {
+            sourceIp: 'SourceIp',
+            applyType: 'ApplyType',
+            scene: 'Scene',
+            configurationMethod: 'ConfigurationMethod',
+            refExtId: 'RefExtId'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            sourceIp: 'string',
+            applyType: 'string',
+            scene: 'string',
+            configurationMethod: 'string',
+            refExtId: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class ConfigurationStyleResponseBody extends $tea.Model {
+    codeData?: ConfigurationStyleResponseBodyCodeData
+    requestId?: string
+    bizCode?: string
+    static names(): { [key: string]: string } {
+        return {
+            codeData: 'CodeData',
+            requestId: 'RequestId',
+            bizCode: 'BizCode'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            codeData: ConfigurationStyleResponseBodyCodeData,
+            requestId: 'string',
+            bizCode: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class ConfigurationStyleResponse extends $tea.Model {
+    headers: { [key: string]: string }
+    body: ConfigurationStyleResponseBody
+    static names(): { [key: string]: string } {
+        return {
+            headers: 'headers',
+            body: 'body'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            headers: { type: 'map', keyType: 'string', valueType: 'string' },
+            body: ConfigurationStyleResponseBody
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class CreateConfigurationRequest extends $tea.Model {
+    sourceIp?: string
+    configurationName?: string
+    applyType?: string
+    scene?: string
+    maxPV?: string
+    configurationMethod?: string
+    static names(): { [key: string]: string } {
+        return {
+            sourceIp: 'SourceIp',
+            configurationName: 'ConfigurationName',
+            applyType: 'ApplyType',
+            scene: 'Scene',
+            maxPV: 'MaxPV',
+            configurationMethod: 'ConfigurationMethod'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            sourceIp: 'string',
+            configurationName: 'string',
+            applyType: 'string',
+            scene: 'string',
+            maxPV: 'string',
+            configurationMethod: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class CreateConfigurationResponseBody extends $tea.Model {
+    refExtId?: string
+    requestId?: string
+    bizCode?: string
+    static names(): { [key: string]: string } {
+        return {
+            refExtId: 'RefExtId',
+            requestId: 'RequestId',
+            bizCode: 'BizCode'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            refExtId: 'string',
+            requestId: 'string',
+            bizCode: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class CreateConfigurationResponse extends $tea.Model {
+    headers: { [key: string]: string }
+    body: CreateConfigurationResponseBody
+    static names(): { [key: string]: string } {
+        return {
+            headers: 'headers',
+            body: 'body'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            headers: { type: 'map', keyType: 'string', valueType: 'string' },
+            body: CreateConfigurationResponseBody
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeAfsConfigNameRequest extends $tea.Model {
+    sourceIp?: string
+    productName?: string
+    static names(): { [key: string]: string } {
+        return {
+            sourceIp: 'SourceIp',
+            productName: 'ProductName'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            sourceIp: 'string',
+            productName: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeAfsConfigNameResponseBody extends $tea.Model {
+    requestId?: string
+    configNames?: DescribeAfsConfigNameResponseBodyConfigNames[]
+    bizCode?: string
+    hasData?: boolean
+    static names(): { [key: string]: string } {
+        return {
+            requestId: 'RequestId',
+            configNames: 'ConfigNames',
+            bizCode: 'BizCode',
+            hasData: 'HasData'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            requestId: 'string',
+            configNames: { type: 'array', itemType: DescribeAfsConfigNameResponseBodyConfigNames },
+            bizCode: 'string',
+            hasData: 'boolean'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeAfsConfigNameResponse extends $tea.Model {
+    headers: { [key: string]: string }
+    body: DescribeAfsConfigNameResponseBody
+    static names(): { [key: string]: string } {
+        return {
+            headers: 'headers',
+            body: 'body'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            headers: { type: 'map', keyType: 'string', valueType: 'string' },
+            body: DescribeAfsConfigNameResponseBody
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeAfsOneConfDataRequest extends $tea.Model {
+    sourceIp?: string
+    appKey?: string
+    scene?: string
+    productName?: string
+    static names(): { [key: string]: string } {
+        return {
+            sourceIp: 'SourceIp',
+            appKey: 'AppKey',
+            scene: 'Scene',
+            productName: 'ProductName'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            sourceIp: 'string',
+            appKey: 'string',
+            scene: 'string',
+            productName: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeAfsOneConfDataResponseBody extends $tea.Model {
+    requestId?: string
+    icOneConfDatas?: DescribeAfsOneConfDataResponseBodyIcOneConfDatas[]
+    ncOneConfDatas?: DescribeAfsOneConfDataResponseBodyNcOneConfDatas[]
+    nvcOneConfDatas?: DescribeAfsOneConfDataResponseBodyNvcOneConfDatas[]
+    bizCode?: string
+    hasData?: boolean
+    static names(): { [key: string]: string } {
+        return {
+            requestId: 'RequestId',
+            icOneConfDatas: 'IcOneConfDatas',
+            ncOneConfDatas: 'NcOneConfDatas',
+            nvcOneConfDatas: 'NvcOneConfDatas',
+            bizCode: 'BizCode',
+            hasData: 'HasData'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            requestId: 'string',
+            icOneConfDatas: { type: 'array', itemType: DescribeAfsOneConfDataResponseBodyIcOneConfDatas },
+            ncOneConfDatas: { type: 'array', itemType: DescribeAfsOneConfDataResponseBodyNcOneConfDatas },
+            nvcOneConfDatas: { type: 'array', itemType: DescribeAfsOneConfDataResponseBodyNvcOneConfDatas },
+            bizCode: 'string',
+            hasData: 'boolean'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeAfsOneConfDataResponse extends $tea.Model {
+    headers: { [key: string]: string }
+    body: DescribeAfsOneConfDataResponseBody
+    static names(): { [key: string]: string } {
+        return {
+            headers: 'headers',
+            body: 'body'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            headers: { type: 'map', keyType: 'string', valueType: 'string' },
+            body: DescribeAfsOneConfDataResponseBody
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeAfsTotalConfDataRequest extends $tea.Model {
+    sourceIp?: string
+    productName?: string
+    static names(): { [key: string]: string } {
+        return {
+            sourceIp: 'SourceIp',
+            productName: 'ProductName'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            sourceIp: 'string',
+            productName: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeAfsTotalConfDataResponseBody extends $tea.Model {
+    requestId?: string
+    icTotalConfSigDatas?: DescribeAfsTotalConfDataResponseBodyIcTotalConfSigDatas[]
+    nvcTotalConfSecVerifyDatas?: DescribeAfsTotalConfDataResponseBodyNvcTotalConfSecVerifyDatas[]
+    icTotalConfVerifyDatas?: DescribeAfsTotalConfDataResponseBodyIcTotalConfVerifyDatas[]
+    nvcTotalConfVerifyDatas?: DescribeAfsTotalConfDataResponseBodyNvcTotalConfVerifyDatas[]
+    icTotalConfSecVerifyDatas?: DescribeAfsTotalConfDataResponseBodyIcTotalConfSecVerifyDatas[]
+    ncTotalConfBlockDatas?: DescribeAfsTotalConfDataResponseBodyNcTotalConfBlockDatas[]
+    icTotalConfBlockDatas?: DescribeAfsTotalConfDataResponseBodyIcTotalConfBlockDatas[]
+    ncTotalConfSigDatas?: DescribeAfsTotalConfDataResponseBodyNcTotalConfSigDatas[]
+    bizCode?: string
+    hasData?: boolean
+    ncTotalConfVerifyDatas?: DescribeAfsTotalConfDataResponseBodyNcTotalConfVerifyDatas[]
+    static names(): { [key: string]: string } {
+        return {
+            requestId: 'RequestId',
+            icTotalConfSigDatas: 'IcTotalConfSigDatas',
+            nvcTotalConfSecVerifyDatas: 'NvcTotalConfSecVerifyDatas',
+            icTotalConfVerifyDatas: 'IcTotalConfVerifyDatas',
+            nvcTotalConfVerifyDatas: 'NvcTotalConfVerifyDatas',
+            icTotalConfSecVerifyDatas: 'IcTotalConfSecVerifyDatas',
+            ncTotalConfBlockDatas: 'NcTotalConfBlockDatas',
+            icTotalConfBlockDatas: 'IcTotalConfBlockDatas',
+            ncTotalConfSigDatas: 'NcTotalConfSigDatas',
+            bizCode: 'BizCode',
+            hasData: 'HasData',
+            ncTotalConfVerifyDatas: 'NcTotalConfVerifyDatas'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            requestId: 'string',
+            icTotalConfSigDatas: { type: 'array', itemType: DescribeAfsTotalConfDataResponseBodyIcTotalConfSigDatas },
+            nvcTotalConfSecVerifyDatas: {
+                type: 'array',
+                itemType: DescribeAfsTotalConfDataResponseBodyNvcTotalConfSecVerifyDatas
+            },
+            icTotalConfVerifyDatas: {
+                type: 'array',
+                itemType: DescribeAfsTotalConfDataResponseBodyIcTotalConfVerifyDatas
+            },
+            nvcTotalConfVerifyDatas: {
+                type: 'array',
+                itemType: DescribeAfsTotalConfDataResponseBodyNvcTotalConfVerifyDatas
+            },
+            icTotalConfSecVerifyDatas: {
+                type: 'array',
+                itemType: DescribeAfsTotalConfDataResponseBodyIcTotalConfSecVerifyDatas
+            },
+            ncTotalConfBlockDatas: {
+                type: 'array',
+                itemType: DescribeAfsTotalConfDataResponseBodyNcTotalConfBlockDatas
+            },
+            icTotalConfBlockDatas: {
+                type: 'array',
+                itemType: DescribeAfsTotalConfDataResponseBodyIcTotalConfBlockDatas
+            },
+            ncTotalConfSigDatas: { type: 'array', itemType: DescribeAfsTotalConfDataResponseBodyNcTotalConfSigDatas },
+            bizCode: 'string',
+            hasData: 'boolean',
+            ncTotalConfVerifyDatas: {
+                type: 'array',
+                itemType: DescribeAfsTotalConfDataResponseBodyNcTotalConfVerifyDatas
+            }
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeAfsTotalConfDataResponse extends $tea.Model {
+    headers: { [key: string]: string }
+    body: DescribeAfsTotalConfDataResponseBody
+    static names(): { [key: string]: string } {
+        return {
+            headers: 'headers',
+            body: 'body'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            headers: { type: 'map', keyType: 'string', valueType: 'string' },
+            body: DescribeAfsTotalConfDataResponseBody
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeAfsVerifySigDataRequest extends $tea.Model {
+    sourceIp?: string
+    appKey?: string
+    scene?: string
+    productName?: string
+    static names(): { [key: string]: string } {
+        return {
+            sourceIp: 'SourceIp',
+            appKey: 'AppKey',
+            scene: 'Scene',
+            productName: 'ProductName'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            sourceIp: 'string',
+            appKey: 'string',
+            scene: 'string',
+            productName: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeAfsVerifySigDataResponseBody extends $tea.Model {
+    nvcCodeDatas?: DescribeAfsVerifySigDataResponseBodyNvcCodeDatas[]
+    nvcSecDatas?: DescribeAfsVerifySigDataResponseBodyNvcSecDatas[]
+    icVerifyDatas?: DescribeAfsVerifySigDataResponseBodyIcVerifyDatas[]
+    requestId?: string
+    ncVerifyDatas?: DescribeAfsVerifySigDataResponseBodyNcVerifyDatas[]
+    nvcVerifyDatas?: DescribeAfsVerifySigDataResponseBodyNvcVerifyDatas[]
+    icSecVerifyDatas?: DescribeAfsVerifySigDataResponseBodyIcSecVerifyDatas[]
+    ncSigDatas?: DescribeAfsVerifySigDataResponseBodyNcSigDatas[]
+    bizCode?: string
+    hasData?: boolean
+    static names(): { [key: string]: string } {
+        return {
+            nvcCodeDatas: 'NvcCodeDatas',
+            nvcSecDatas: 'NvcSecDatas',
+            icVerifyDatas: 'IcVerifyDatas',
+            requestId: 'RequestId',
+            ncVerifyDatas: 'NcVerifyDatas',
+            nvcVerifyDatas: 'NvcVerifyDatas',
+            icSecVerifyDatas: 'IcSecVerifyDatas',
+            ncSigDatas: 'NcSigDatas',
+            bizCode: 'BizCode',
+            hasData: 'HasData'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            nvcCodeDatas: { type: 'array', itemType: DescribeAfsVerifySigDataResponseBodyNvcCodeDatas },
+            nvcSecDatas: { type: 'array', itemType: DescribeAfsVerifySigDataResponseBodyNvcSecDatas },
+            icVerifyDatas: { type: 'array', itemType: DescribeAfsVerifySigDataResponseBodyIcVerifyDatas },
+            requestId: 'string',
+            ncVerifyDatas: { type: 'array', itemType: DescribeAfsVerifySigDataResponseBodyNcVerifyDatas },
+            nvcVerifyDatas: { type: 'array', itemType: DescribeAfsVerifySigDataResponseBodyNvcVerifyDatas },
+            icSecVerifyDatas: { type: 'array', itemType: DescribeAfsVerifySigDataResponseBodyIcSecVerifyDatas },
+            ncSigDatas: { type: 'array', itemType: DescribeAfsVerifySigDataResponseBodyNcSigDatas },
+            bizCode: 'string',
+            hasData: 'boolean'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeAfsVerifySigDataResponse extends $tea.Model {
+    headers: { [key: string]: string }
+    body: DescribeAfsVerifySigDataResponseBody
+    static names(): { [key: string]: string } {
+        return {
+            headers: 'headers',
+            body: 'body'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            headers: { type: 'map', keyType: 'string', valueType: 'string' },
+            body: DescribeAfsVerifySigDataResponseBody
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeCaptchaDayRequest extends $tea.Model {
+    sourceIp?: string
+    configName?: string
+    type?: string
+    time?: string
+    refExtId?: string
+    static names(): { [key: string]: string } {
+        return {
+            sourceIp: 'SourceIp',
+            configName: 'ConfigName',
+            type: 'Type',
+            time: 'Time',
+            refExtId: 'RefExtId'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            sourceIp: 'string',
+            configName: 'string',
+            type: 'string',
+            time: 'string',
+            refExtId: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeCaptchaDayResponseBody extends $tea.Model {
+    captchaDay?: DescribeCaptchaDayResponseBodyCaptchaDay
+    requestId?: string
+    bizCode?: string
+    hasData?: boolean
+    static names(): { [key: string]: string } {
+        return {
+            captchaDay: 'CaptchaDay',
+            requestId: 'RequestId',
+            bizCode: 'BizCode',
+            hasData: 'HasData'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            captchaDay: DescribeCaptchaDayResponseBodyCaptchaDay,
+            requestId: 'string',
+            bizCode: 'string',
+            hasData: 'boolean'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeCaptchaDayResponse extends $tea.Model {
+    headers: { [key: string]: string }
+    body: DescribeCaptchaDayResponseBody
+    static names(): { [key: string]: string } {
+        return {
+            headers: 'headers',
+            body: 'body'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            headers: { type: 'map', keyType: 'string', valueType: 'string' },
+            body: DescribeCaptchaDayResponseBody
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeCaptchaIpCityRequest extends $tea.Model {
+    sourceIp?: string
+    configName?: string
+    type?: string
+    time?: string
+    refExtId?: string
+    static names(): { [key: string]: string } {
+        return {
+            sourceIp: 'SourceIp',
+            configName: 'ConfigName',
+            type: 'Type',
+            time: 'Time',
+            refExtId: 'RefExtId'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            sourceIp: 'string',
+            configName: 'string',
+            type: 'string',
+            time: 'string',
+            refExtId: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeCaptchaIpCityResponseBody extends $tea.Model {
+    captchaIps?: DescribeCaptchaIpCityResponseBodyCaptchaIps[]
+    captchaCities?: DescribeCaptchaIpCityResponseBodyCaptchaCities[]
+    requestId?: string
+    bizCode?: string
+    hasData?: boolean
+    static names(): { [key: string]: string } {
+        return {
+            captchaIps: 'CaptchaIps',
+            captchaCities: 'CaptchaCities',
+            requestId: 'RequestId',
+            bizCode: 'BizCode',
+            hasData: 'HasData'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            captchaIps: { type: 'array', itemType: DescribeCaptchaIpCityResponseBodyCaptchaIps },
+            captchaCities: { type: 'array', itemType: DescribeCaptchaIpCityResponseBodyCaptchaCities },
+            requestId: 'string',
+            bizCode: 'string',
+            hasData: 'boolean'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeCaptchaIpCityResponse extends $tea.Model {
+    headers: { [key: string]: string }
+    body: DescribeCaptchaIpCityResponseBody
+    static names(): { [key: string]: string } {
+        return {
+            headers: 'headers',
+            body: 'body'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            headers: { type: 'map', keyType: 'string', valueType: 'string' },
+            body: DescribeCaptchaIpCityResponseBody
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeCaptchaMinRequest extends $tea.Model {
+    sourceIp?: string
+    configName?: string
+    type?: string
+    time?: string
+    refExtId?: string
+    static names(): { [key: string]: string } {
+        return {
+            sourceIp: 'SourceIp',
+            configName: 'ConfigName',
+            type: 'Type',
+            time: 'Time',
+            refExtId: 'RefExtId'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            sourceIp: 'string',
+            configName: 'string',
+            type: 'string',
+            time: 'string',
+            refExtId: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeCaptchaMinResponseBody extends $tea.Model {
+    requestId?: string
+    captchaMins?: DescribeCaptchaMinResponseBodyCaptchaMins[]
+    bizCode?: string
+    hasData?: boolean
+    static names(): { [key: string]: string } {
+        return {
+            requestId: 'RequestId',
+            captchaMins: 'CaptchaMins',
+            bizCode: 'BizCode',
+            hasData: 'HasData'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            requestId: 'string',
+            captchaMins: { type: 'array', itemType: DescribeCaptchaMinResponseBodyCaptchaMins },
+            bizCode: 'string',
+            hasData: 'boolean'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeCaptchaMinResponse extends $tea.Model {
+    headers: { [key: string]: string }
+    body: DescribeCaptchaMinResponseBody
+    static names(): { [key: string]: string } {
+        return {
+            headers: 'headers',
+            body: 'body'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            headers: { type: 'map', keyType: 'string', valueType: 'string' },
+            body: DescribeCaptchaMinResponseBody
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeCaptchaOrderRequest extends $tea.Model {
+    sourceIp?: string
+    lang?: string
+    static names(): { [key: string]: string } {
+        return {
+            sourceIp: 'SourceIp',
+            lang: 'Lang'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            sourceIp: 'string',
+            lang: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeCaptchaOrderResponseBody extends $tea.Model {
+    requestId?: string
+    bizCode?: string
+    static names(): { [key: string]: string } {
+        return {
+            requestId: 'RequestId',
+            bizCode: 'BizCode'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            requestId: 'string',
+            bizCode: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeCaptchaOrderResponse extends $tea.Model {
+    headers: { [key: string]: string }
+    body: DescribeCaptchaOrderResponseBody
+    static names(): { [key: string]: string } {
+        return {
+            headers: 'headers',
+            body: 'body'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            headers: { type: 'map', keyType: 'string', valueType: 'string' },
+            body: DescribeCaptchaOrderResponseBody
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeCaptchaRiskRequest extends $tea.Model {
+    sourceIp?: string
+    configName?: string
+    time?: string
+    refExtId?: string
+    static names(): { [key: string]: string } {
+        return {
+            sourceIp: 'SourceIp',
+            configName: 'ConfigName',
+            time: 'Time',
+            refExtId: 'RefExtId'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            sourceIp: 'string',
+            configName: 'string',
+            time: 'string',
+            refExtId: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeCaptchaRiskResponseBody extends $tea.Model {
+    requestId?: string
+    numOfLastMonth?: number
+    riskLevel?: string
+    numOfThisMonth?: number
+    bizCode?: string
+    static names(): { [key: string]: string } {
+        return {
+            requestId: 'RequestId',
+            numOfLastMonth: 'NumOfLastMonth',
+            riskLevel: 'RiskLevel',
+            numOfThisMonth: 'NumOfThisMonth',
+            bizCode: 'BizCode'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            requestId: 'string',
+            numOfLastMonth: 'number',
+            riskLevel: 'string',
+            numOfThisMonth: 'number',
+            bizCode: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeCaptchaRiskResponse extends $tea.Model {
+    headers: { [key: string]: string }
+    body: DescribeCaptchaRiskResponseBody
+    static names(): { [key: string]: string } {
+        return {
+            headers: 'headers',
+            body: 'body'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            headers: { type: 'map', keyType: 'string', valueType: 'string' },
+            body: DescribeCaptchaRiskResponseBody
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeConfigNameRequest extends $tea.Model {
+    sourceIp?: string
+    static names(): { [key: string]: string } {
+        return {
+            sourceIp: 'SourceIp'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            sourceIp: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeConfigNameResponseBody extends $tea.Model {
+    requestId?: string
+    configNames?: DescribeConfigNameResponseBodyConfigNames[]
+    hasConfig?: boolean
+    bizCode?: string
+    static names(): { [key: string]: string } {
+        return {
+            requestId: 'RequestId',
+            configNames: 'ConfigNames',
+            hasConfig: 'HasConfig',
+            bizCode: 'BizCode'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            requestId: 'string',
+            configNames: { type: 'array', itemType: DescribeConfigNameResponseBodyConfigNames },
+            hasConfig: 'boolean',
+            bizCode: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeConfigNameResponse extends $tea.Model {
+    headers: { [key: string]: string }
+    body: DescribeConfigNameResponseBody
+    static names(): { [key: string]: string } {
+        return {
+            headers: 'headers',
+            body: 'body'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            headers: { type: 'map', keyType: 'string', valueType: 'string' },
+            body: DescribeConfigNameResponseBody
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeEarlyWarningRequest extends $tea.Model {
+    sourceIp?: string
+    static names(): { [key: string]: string } {
+        return {
+            sourceIp: 'SourceIp'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            sourceIp: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeEarlyWarningResponseBody extends $tea.Model {
+    requestId?: string
+    hasWarning?: boolean
+    earlyWarnings?: DescribeEarlyWarningResponseBodyEarlyWarnings[]
+    bizCode?: string
+    static names(): { [key: string]: string } {
+        return {
+            requestId: 'RequestId',
+            hasWarning: 'HasWarning',
+            earlyWarnings: 'EarlyWarnings',
+            bizCode: 'BizCode'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            requestId: 'string',
+            hasWarning: 'boolean',
+            earlyWarnings: { type: 'array', itemType: DescribeEarlyWarningResponseBodyEarlyWarnings },
+            bizCode: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeEarlyWarningResponse extends $tea.Model {
+    headers: { [key: string]: string }
+    body: DescribeEarlyWarningResponseBody
+    static names(): { [key: string]: string } {
+        return {
+            headers: 'headers',
+            body: 'body'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            headers: { type: 'map', keyType: 'string', valueType: 'string' },
+            body: DescribeEarlyWarningResponseBody
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeOrderInfoRequest extends $tea.Model {
+    sourceIp?: string
+    static names(): { [key: string]: string } {
+        return {
+            sourceIp: 'SourceIp'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            sourceIp: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeOrderInfoResponseBody extends $tea.Model {
+    orderLevel?: string
+    requestId?: string
+    num?: string
+    endDate?: string
+    bizCode?: string
+    beginDate?: string
+    static names(): { [key: string]: string } {
+        return {
+            orderLevel: 'OrderLevel',
+            requestId: 'RequestId',
+            num: 'Num',
+            endDate: 'EndDate',
+            bizCode: 'BizCode',
+            beginDate: 'BeginDate'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            orderLevel: 'string',
+            requestId: 'string',
+            num: 'string',
+            endDate: 'string',
+            bizCode: 'string',
+            beginDate: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeOrderInfoResponse extends $tea.Model {
+    headers: { [key: string]: string }
+    body: DescribeOrderInfoResponseBody
+    static names(): { [key: string]: string } {
+        return {
+            headers: 'headers',
+            body: 'body'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            headers: { type: 'map', keyType: 'string', valueType: 'string' },
+            body: DescribeOrderInfoResponseBody
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribePersonMachineListRequest extends $tea.Model {
+    sourceIp?: string
+    static names(): { [key: string]: string } {
+        return {
+            sourceIp: 'SourceIp'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            sourceIp: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribePersonMachineListResponseBody extends $tea.Model {
+    personMachineRes?: DescribePersonMachineListResponseBodyPersonMachineRes
+    requestId?: string
+    bizCode?: string
+    static names(): { [key: string]: string } {
+        return {
+            personMachineRes: 'PersonMachineRes',
+            requestId: 'RequestId',
+            bizCode: 'BizCode'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            personMachineRes: DescribePersonMachineListResponseBodyPersonMachineRes,
+            requestId: 'string',
+            bizCode: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribePersonMachineListResponse extends $tea.Model {
+    headers: { [key: string]: string }
+    body: DescribePersonMachineListResponseBody
+    static names(): { [key: string]: string } {
+        return {
+            headers: 'headers',
+            body: 'body'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            headers: { type: 'map', keyType: 'string', valueType: 'string' },
+            body: DescribePersonMachineListResponseBody
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class SetEarlyWarningRequest extends $tea.Model {
+    sourceIp?: string
+    warnOpen?: boolean
+    channel?: string
+    frequency?: string
+    timeOpen?: boolean
+    timeBegin?: string
+    timeEnd?: string
+    title?: string
+    static names(): { [key: string]: string } {
+        return {
+            sourceIp: 'SourceIp',
+            warnOpen: 'WarnOpen',
+            channel: 'Channel',
+            frequency: 'Frequency',
+            timeOpen: 'TimeOpen',
+            timeBegin: 'TimeBegin',
+            timeEnd: 'TimeEnd',
+            title: 'Title'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            sourceIp: 'string',
+            warnOpen: 'boolean',
+            channel: 'string',
+            frequency: 'string',
+            timeOpen: 'boolean',
+            timeBegin: 'string',
+            timeEnd: 'string',
+            title: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class SetEarlyWarningResponseBody extends $tea.Model {
+    requestId?: string
+    bizCode?: string
+    static names(): { [key: string]: string } {
+        return {
+            requestId: 'RequestId',
+            bizCode: 'BizCode'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            requestId: 'string',
+            bizCode: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class SetEarlyWarningResponse extends $tea.Model {
+    headers: { [key: string]: string }
+    body: SetEarlyWarningResponseBody
+    static names(): { [key: string]: string } {
+        return {
+            headers: 'headers',
+            body: 'body'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            headers: { type: 'map', keyType: 'string', valueType: 'string' },
+            body: SetEarlyWarningResponseBody
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class UpdateConfigNameRequest extends $tea.Model {
+    sourceIp?: string
+    lang?: string
+    refExtId?: string
+    configName?: string
+    static names(): { [key: string]: string } {
+        return {
+            sourceIp: 'SourceIp',
+            lang: 'Lang',
+            refExtId: 'RefExtId',
+            configName: 'ConfigName'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            sourceIp: 'string',
+            lang: 'string',
+            refExtId: 'string',
+            configName: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class UpdateConfigNameResponseBody extends $tea.Model {
+    requestId?: string
+    bizCode?: string
+    static names(): { [key: string]: string } {
+        return {
+            requestId: 'RequestId',
+            bizCode: 'BizCode'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            requestId: 'string',
+            bizCode: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class UpdateConfigNameResponse extends $tea.Model {
+    headers: { [key: string]: string }
+    body: UpdateConfigNameResponseBody
+    static names(): { [key: string]: string } {
+        return {
+            headers: 'headers',
+            body: 'body'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            headers: { type: 'map', keyType: 'string', valueType: 'string' },
+            body: UpdateConfigNameResponseBody
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class ConfigurationStyleResponseBodyCodeData extends $tea.Model {
+    nodeJs?: string
+    javaUrl?: string
+    python?: string
+    java?: string
+    nodeJsUrl?: string
+    pythonUrl?: string
+    html?: string
+    phpUrl?: string
+    netUrl?: string
+    php?: string
+    net?: string
+    static names(): { [key: string]: string } {
+        return {
+            nodeJs: 'NodeJs',
+            javaUrl: 'JavaUrl',
+            python: 'Python',
+            java: 'Java',
+            nodeJsUrl: 'NodeJsUrl',
+            pythonUrl: 'PythonUrl',
+            html: 'Html',
+            phpUrl: 'PhpUrl',
+            netUrl: 'NetUrl',
+            php: 'Php',
+            net: 'Net'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            nodeJs: 'string',
+            javaUrl: 'string',
+            python: 'string',
+            java: 'string',
+            nodeJsUrl: 'string',
+            pythonUrl: 'string',
+            html: 'string',
+            phpUrl: 'string',
+            netUrl: 'string',
+            php: 'string',
+            net: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeAfsConfigNameResponseBodyConfigNames extends $tea.Model {
+    configName?: string
+    appKey?: string
+    refExtId?: string
+    aliUid?: string
+    scene?: string
+    static names(): { [key: string]: string } {
+        return {
+            configName: 'ConfigName',
+            appKey: 'AppKey',
+            refExtId: 'RefExtId',
+            aliUid: 'AliUid',
+            scene: 'Scene'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            configName: 'string',
+            appKey: 'string',
+            refExtId: 'string',
+            aliUid: 'string',
+            scene: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeAfsOneConfDataResponseBodyIcOneConfDatas extends $tea.Model {
+    icSigCnt?: number
+    icBlockCnt?: number
+    tableDate?: string
+    icVerifyCnt?: number
+    icSecVerifyCnt?: number
+    icInitCnt?: number
+    icNoActionCnt?: number
+    static names(): { [key: string]: string } {
+        return {
+            icSigCnt: 'IcSigCnt',
+            icBlockCnt: 'IcBlockCnt',
+            tableDate: 'TableDate',
+            icVerifyCnt: 'IcVerifyCnt',
+            icSecVerifyCnt: 'IcSecVerifyCnt',
+            icInitCnt: 'IcInitCnt',
+            icNoActionCnt: 'IcNoActionCnt'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            icSigCnt: 'number',
+            icBlockCnt: 'number',
+            tableDate: 'string',
+            icVerifyCnt: 'number',
+            icSecVerifyCnt: 'number',
+            icInitCnt: 'number',
+            icNoActionCnt: 'number'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeAfsOneConfDataResponseBodyNcOneConfDatas extends $tea.Model {
+    tableDate?: string
+    ncSigCnt?: number
+    ncVerifyCnt?: number
+    ncNoActionCnt?: number
+    ncVerifyBlockCnt?: number
+    ncInitCnt?: number
+    ncSigBlockCnt?: number
+    static names(): { [key: string]: string } {
+        return {
+            tableDate: 'TableDate',
+            ncSigCnt: 'NcSigCnt',
+            ncVerifyCnt: 'NcVerifyCnt',
+            ncNoActionCnt: 'NcNoActionCnt',
+            ncVerifyBlockCnt: 'NcVerifyBlockCnt',
+            ncInitCnt: 'NcInitCnt',
+            ncSigBlockCnt: 'NcSigBlockCnt'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            tableDate: 'string',
+            ncSigCnt: 'number',
+            ncVerifyCnt: 'number',
+            ncNoActionCnt: 'number',
+            ncVerifyBlockCnt: 'number',
+            ncInitCnt: 'number',
+            ncSigBlockCnt: 'number'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeAfsOneConfDataResponseBodyNvcOneConfDatas extends $tea.Model {
+    nvcNoActionCnt?: number
+    nvcSecVerifyCnt?: number
+    tableDate?: string
+    nvcVerifyCnt?: number
+    nvcBlockCnt?: number
+    nvcInitCnt?: number
+    static names(): { [key: string]: string } {
+        return {
+            nvcNoActionCnt: 'NvcNoActionCnt',
+            nvcSecVerifyCnt: 'NvcSecVerifyCnt',
+            tableDate: 'TableDate',
+            nvcVerifyCnt: 'NvcVerifyCnt',
+            nvcBlockCnt: 'NvcBlockCnt',
+            nvcInitCnt: 'NvcInitCnt'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            nvcNoActionCnt: 'number',
+            nvcSecVerifyCnt: 'number',
+            tableDate: 'string',
+            nvcVerifyCnt: 'number',
+            nvcBlockCnt: 'number',
+            nvcInitCnt: 'number'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeAfsTotalConfDataResponseBodyIcTotalConfSigDatas extends $tea.Model {
+    time?: string
+    value?: number
+    category?: string
+    static names(): { [key: string]: string } {
+        return {
+            time: 'Time',
+            value: 'Value',
+            category: 'Category'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            time: 'string',
+            value: 'number',
+            category: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeAfsTotalConfDataResponseBodyNvcTotalConfSecVerifyDatas extends $tea.Model {
+    time?: string
+    value?: number
+    category?: string
+    static names(): { [key: string]: string } {
+        return {
+            time: 'Time',
+            value: 'Value',
+            category: 'Category'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            time: 'string',
+            value: 'number',
+            category: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeAfsTotalConfDataResponseBodyIcTotalConfVerifyDatas extends $tea.Model {
+    time?: string
+    value?: number
+    category?: string
+    static names(): { [key: string]: string } {
+        return {
+            time: 'Time',
+            value: 'Value',
+            category: 'Category'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            time: 'string',
+            value: 'number',
+            category: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeAfsTotalConfDataResponseBodyNvcTotalConfVerifyDatas extends $tea.Model {
+    time?: string
+    value?: number
+    category?: string
+    static names(): { [key: string]: string } {
+        return {
+            time: 'Time',
+            value: 'Value',
+            category: 'Category'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            time: 'string',
+            value: 'number',
+            category: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeAfsTotalConfDataResponseBodyIcTotalConfSecVerifyDatas extends $tea.Model {
+    time?: string
+    value?: number
+    category?: string
+    static names(): { [key: string]: string } {
+        return {
+            time: 'Time',
+            value: 'Value',
+            category: 'Category'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            time: 'string',
+            value: 'number',
+            category: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeAfsTotalConfDataResponseBodyNcTotalConfBlockDatas extends $tea.Model {
+    time?: string
+    value?: number
+    category?: string
+    static names(): { [key: string]: string } {
+        return {
+            time: 'Time',
+            value: 'Value',
+            category: 'Category'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            time: 'string',
+            value: 'number',
+            category: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeAfsTotalConfDataResponseBodyIcTotalConfBlockDatas extends $tea.Model {
+    time?: string
+    value?: number
+    category?: string
+    static names(): { [key: string]: string } {
+        return {
+            time: 'Time',
+            value: 'Value',
+            category: 'Category'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            time: 'string',
+            value: 'number',
+            category: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeAfsTotalConfDataResponseBodyNcTotalConfSigDatas extends $tea.Model {
+    time?: string
+    value?: number
+    category?: string
+    static names(): { [key: string]: string } {
+        return {
+            time: 'Time',
+            value: 'Value',
+            category: 'Category'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            time: 'string',
+            value: 'number',
+            category: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeAfsTotalConfDataResponseBodyNcTotalConfVerifyDatas extends $tea.Model {
+    time?: string
+    value?: number
+    category?: string
+    static names(): { [key: string]: string } {
+        return {
+            time: 'Time',
+            value: 'Value',
+            category: 'Category'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            time: 'string',
+            value: 'number',
+            category: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeAfsVerifySigDataResponseBodyNvcCodeDatas extends $tea.Model {
+    time?: string
+    nvcCode400?: number
+    nvcCode200?: number
+    nvcCode800?: number
+    nvcCode600?: number
+    static names(): { [key: string]: string } {
+        return {
+            time: 'Time',
+            nvcCode400: 'NvcCode400',
+            nvcCode200: 'NvcCode200',
+            nvcCode800: 'NvcCode800',
+            nvcCode600: 'NvcCode600'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            time: 'string',
+            nvcCode400: 'number',
+            nvcCode200: 'number',
+            nvcCode800: 'number',
+            nvcCode600: 'number'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeAfsVerifySigDataResponseBodyNvcSecDatas extends $tea.Model {
+    time?: string
+    nvcSecBlock?: number
+    nvcSecPass?: number
+    static names(): { [key: string]: string } {
+        return {
+            time: 'Time',
+            nvcSecBlock: 'NvcSecBlock',
+            nvcSecPass: 'NvcSecPass'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            time: 'string',
+            nvcSecBlock: 'number',
+            nvcSecPass: 'number'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeAfsVerifySigDataResponseBodyIcVerifyDatas extends $tea.Model {
+    icSigCnt?: number
+    time?: string
+    icBlockCnt?: number
+    icSecVerifyCnt?: number
+    icVerifyCnt?: number
+    static names(): { [key: string]: string } {
+        return {
+            icSigCnt: 'IcSigCnt',
+            time: 'Time',
+            icBlockCnt: 'IcBlockCnt',
+            icSecVerifyCnt: 'IcSecVerifyCnt',
+            icVerifyCnt: 'IcVerifyCnt'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            icSigCnt: 'number',
+            time: 'string',
+            icBlockCnt: 'number',
+            icSecVerifyCnt: 'number',
+            icVerifyCnt: 'number'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeAfsVerifySigDataResponseBodyNcVerifyDatas extends $tea.Model {
+    time?: string
+    ncVerifyPass?: number
+    ncVerifyBlock?: number
+    static names(): { [key: string]: string } {
+        return {
+            time: 'Time',
+            ncVerifyPass: 'NcVerifyPass',
+            ncVerifyBlock: 'NcVerifyBlock'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            time: 'string',
+            ncVerifyPass: 'number',
+            ncVerifyBlock: 'number'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeAfsVerifySigDataResponseBodyNvcVerifyDatas extends $tea.Model {
+    time?: string
+    nvcSecVerifyCnt?: number
+    nvcVerifyCnt?: number
+    static names(): { [key: string]: string } {
+        return {
+            time: 'Time',
+            nvcSecVerifyCnt: 'NvcSecVerifyCnt',
+            nvcVerifyCnt: 'NvcVerifyCnt'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            time: 'string',
+            nvcSecVerifyCnt: 'number',
+            nvcVerifyCnt: 'number'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeAfsVerifySigDataResponseBodyIcSecVerifyDatas extends $tea.Model {
+    icSecBlock?: number
+    time?: string
+    icSecPass?: number
+    static names(): { [key: string]: string } {
+        return {
+            icSecBlock: 'IcSecBlock',
+            time: 'Time',
+            icSecPass: 'IcSecPass'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            icSecBlock: 'number',
+            time: 'string',
+            icSecPass: 'number'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeAfsVerifySigDataResponseBodyNcSigDatas extends $tea.Model {
+    time?: string
+    ncSigBlock?: number
+    ncSigPass?: number
+    static names(): { [key: string]: string } {
+        return {
+            time: 'Time',
+            ncSigBlock: 'NcSigBlock',
+            ncSigPass: 'NcSigPass'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            time: 'string',
+            ncSigBlock: 'number',
+            ncSigPass: 'number'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeCaptchaDayResponseBodyCaptchaDay extends $tea.Model {
+    checkTested?: number
+    direcetStrategyInterception?: number
+    maliciousFlow?: number
+    pass?: number
+    legalSign?: number
+    uncheckTested?: number
+    askForVerify?: number
+    init?: number
+    twiceVerify?: number
+    static names(): { [key: string]: string } {
+        return {
+            checkTested: 'CheckTested',
+            direcetStrategyInterception: 'DirecetStrategyInterception',
+            maliciousFlow: 'MaliciousFlow',
+            pass: 'Pass',
+            legalSign: 'LegalSign',
+            uncheckTested: 'UncheckTested',
+            askForVerify: 'AskForVerify',
+            init: 'Init',
+            twiceVerify: 'TwiceVerify'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            checkTested: 'number',
+            direcetStrategyInterception: 'number',
+            maliciousFlow: 'number',
+            pass: 'number',
+            legalSign: 'number',
+            uncheckTested: 'number',
+            askForVerify: 'number',
+            init: 'number',
+            twiceVerify: 'number'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeCaptchaIpCityResponseBodyCaptchaIps extends $tea.Model {
+    value?: number
+    ip?: string
+    static names(): { [key: string]: string } {
+        return {
+            value: 'Value',
+            ip: 'Ip'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            value: 'number',
+            ip: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeCaptchaIpCityResponseBodyCaptchaCities extends $tea.Model {
+    pv?: number
+    lng?: string
+    lat?: string
+    location?: string
+    static names(): { [key: string]: string } {
+        return {
+            pv: 'Pv',
+            lng: 'Lng',
+            lat: 'Lat',
+            location: 'Location'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            pv: 'number',
+            lng: 'string',
+            lat: 'string',
+            location: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeCaptchaMinResponseBodyCaptchaMins extends $tea.Model {
+    time?: string
+    pass?: string
+    interception?: string
+    static names(): { [key: string]: string } {
+        return {
+            time: 'Time',
+            pass: 'Pass',
+            interception: 'Interception'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            time: 'string',
+            pass: 'string',
+            interception: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeConfigNameResponseBodyConfigNames extends $tea.Model {
+    configName?: string
+    refExtId?: string
+    aliUid?: string
+    static names(): { [key: string]: string } {
+        return {
+            configName: 'ConfigName',
+            refExtId: 'RefExtId',
+            aliUid: 'AliUid'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            configName: 'string',
+            refExtId: 'string',
+            aliUid: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribeEarlyWarningResponseBodyEarlyWarnings extends $tea.Model {
+    frequency?: string
+    timeBegin?: string
+    timeEnd?: string
+    channel?: string
+    warnOpen?: boolean
+    title?: string
+    content?: string
+    timeOpen?: boolean
+    static names(): { [key: string]: string } {
+        return {
+            frequency: 'Frequency',
+            timeBegin: 'TimeBegin',
+            timeEnd: 'TimeEnd',
+            channel: 'Channel',
+            warnOpen: 'WarnOpen',
+            title: 'Title',
+            content: 'Content',
+            timeOpen: 'TimeOpen'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            frequency: 'string',
+            timeBegin: 'string',
+            timeEnd: 'string',
+            channel: 'string',
+            warnOpen: 'boolean',
+            title: 'string',
+            content: 'string',
+            timeOpen: 'boolean'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribePersonMachineListResponseBodyPersonMachineResPersonMachines extends $tea.Model {
+    configurationName?: string
+    configurationMethod?: string
+    extId?: string
+    applyType?: string
+    lastUpdate?: string
+    scene?: string
+    sceneOriginal?: string
+    appkey?: string
+    static names(): { [key: string]: string } {
+        return {
+            configurationName: 'ConfigurationName',
+            configurationMethod: 'ConfigurationMethod',
+            extId: 'ExtId',
+            applyType: 'ApplyType',
+            lastUpdate: 'LastUpdate',
+            scene: 'Scene',
+            sceneOriginal: 'SceneOriginal',
+            appkey: 'Appkey'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            configurationName: 'string',
+            configurationMethod: 'string',
+            extId: 'string',
+            applyType: 'string',
+            lastUpdate: 'string',
+            scene: 'string',
+            sceneOriginal: 'string',
+            appkey: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class DescribePersonMachineListResponseBodyPersonMachineRes extends $tea.Model {
+    personMachines?: DescribePersonMachineListResponseBodyPersonMachineResPersonMachines[]
+    hasConfiguration?: string
+    static names(): { [key: string]: string } {
+        return {
+            personMachines: 'PersonMachines',
+            hasConfiguration: 'HasConfiguration'
+        }
+    }
+
+    static types(): { [key: string]: any } {
+        return {
+            personMachines: {
+                type: 'array',
+                itemType: DescribePersonMachineListResponseBodyPersonMachineResPersonMachines
+            },
+            hasConfiguration: 'string'
+        }
+    }
+
+    constructor(map?: { [key: string]: any }) {
+        super(map)
+    }
+}
+
+export class AfsClient extends OpenApi {
+    constructor(config: $OpenApi.Config) {
+        super(config)
+        this._endpointRule = 'regional'
+        this._endpointMap = {
+            'ap-northeast-1': 'afs.aliyuncs.com',
+            'ap-northeast-2-pop': 'afs.aliyuncs.com',
+            'ap-south-1': 'afs.aliyuncs.com',
+            'ap-southeast-1': 'afs.aliyuncs.com',
+            'ap-southeast-2': 'afs.aliyuncs.com',
+            'ap-southeast-3': 'afs.aliyuncs.com',
+            'ap-southeast-5': 'afs.aliyuncs.com',
+            'cn-beijing': 'afs.aliyuncs.com',
+            'cn-beijing-finance-1': 'afs.aliyuncs.com',
+            'cn-beijing-finance-pop': 'afs.aliyuncs.com',
+            'cn-beijing-gov-1': 'afs.aliyuncs.com',
+            'cn-beijing-nu16-b01': 'afs.aliyuncs.com',
+            'cn-chengdu': 'afs.aliyuncs.com',
+            'cn-edge-1': 'afs.aliyuncs.com',
+            'cn-fujian': 'afs.aliyuncs.com',
+            'cn-haidian-cm12-c01': 'afs.aliyuncs.com',
+            'cn-hangzhou-bj-b01': 'afs.aliyuncs.com',
+            'cn-hangzhou-finance': 'afs.aliyuncs.com',
+            'cn-hangzhou-internal-prod-1': 'afs.aliyuncs.com',
+            'cn-hangzhou-internal-test-1': 'afs.aliyuncs.com',
+            'cn-hangzhou-internal-test-2': 'afs.aliyuncs.com',
+            'cn-hangzhou-internal-test-3': 'afs.aliyuncs.com',
+            'cn-hangzhou-test-306': 'afs.aliyuncs.com',
+            'cn-hongkong': 'afs.aliyuncs.com',
+            'cn-hongkong-finance-pop': 'afs.aliyuncs.com',
+            'cn-huhehaote': 'afs.aliyuncs.com',
+            'cn-north-2-gov-1': 'afs.aliyuncs.com',
+            'cn-qingdao': 'afs.aliyuncs.com',
+            'cn-qingdao-nebula': 'afs.aliyuncs.com',
+            'cn-shanghai': 'afs.aliyuncs.com',
+            'cn-shanghai-et15-b01': 'afs.aliyuncs.com',
+            'cn-shanghai-et2-b01': 'afs.aliyuncs.com',
+            'cn-shanghai-finance-1': 'afs.aliyuncs.com',
+            'cn-shanghai-inner': 'afs.aliyuncs.com',
+            'cn-shanghai-internal-test-1': 'afs.aliyuncs.com',
+            'cn-shenzhen': 'afs.aliyuncs.com',
+            'cn-shenzhen-finance-1': 'afs.aliyuncs.com',
+            'cn-shenzhen-inner': 'afs.aliyuncs.com',
+            'cn-shenzhen-st4-d01': 'afs.aliyuncs.com',
+            'cn-shenzhen-su18-b01': 'afs.aliyuncs.com',
+            'cn-wuhan': 'afs.aliyuncs.com',
+            'cn-yushanfang': 'afs.aliyuncs.com',
+            'cn-zhangbei-na61-b01': 'afs.aliyuncs.com',
+            'cn-zhangjiakou': 'afs.aliyuncs.com',
+            'cn-zhangjiakou-na62-a01': 'afs.aliyuncs.com',
+            'cn-zhengzhou-nebula-1': 'afs.aliyuncs.com',
+            'eu-central-1': 'afs.aliyuncs.com',
+            'eu-west-1': 'afs.aliyuncs.com',
+            'eu-west-1-oxs': 'afs.aliyuncs.com',
+            'me-east-1': 'afs.aliyuncs.com',
+            'rus-west-1-pop': 'afs.aliyuncs.com',
+            'us-east-1': 'afs.aliyuncs.com',
+            'us-west-1': 'afs.aliyuncs.com'
+        }
+        this.checkConfig(config)
+        this._endpoint = this.getEndpoint(
+            'afs',
+            this._regionId,
+            this._endpointRule,
+            this._network,
+            this._suffix,
+            this._endpointMap,
+            this._endpoint
+        )
+    }
+
+    getEndpoint(
+        productId: string,
+        regionId: string,
+        endpointRule: string,
+        network: string,
+        suffix: string,
+        endpointMap: { [key: string]: string },
+        endpoint: string
+    ): string {
+        if (!Util.empty(endpoint)) {
+            return endpoint
+        }
+
+        if (!Util.isUnset(endpointMap) && !Util.empty(endpointMap[regionId])) {
+            return endpointMap[regionId]
+        }
+
+        return EndpointUtil.getEndpointRules(productId, regionId, endpointRule, network, suffix)
+    }
+
+    async analyzeNvcWithOptions(
+        request: AnalyzeNvcRequest,
+        runtime: $Util.RuntimeOptions
+    ): Promise<AnalyzeNvcResponse> {
+        Util.validateModel(request)
+        let req = new $OpenApi.OpenApiRequest({
+            body: Util.toMap(request)
+        })
+        return $tea.cast<AnalyzeNvcResponse>(
+            await this.doRPCRequest('AnalyzeNvc', '2018-01-12', 'HTTPS', 'POST', 'AK', 'json', req, runtime),
+            new AnalyzeNvcResponse({})
+        )
+    }
+
+    async analyzeNvc(request: AnalyzeNvcRequest): Promise<AnalyzeNvcResponse> {
+        let runtime = new $Util.RuntimeOptions({})
+        return await this.analyzeNvcWithOptions(request, runtime)
+    }
+
+    async authenticateSigWithOptions(
+        request: AuthenticateSigRequest,
+        runtime: $Util.RuntimeOptions
+    ): Promise<AuthenticateSigResponse> {
+        Util.validateModel(request)
+        let req = new $OpenApi.OpenApiRequest({
+            body: Util.toMap(request)
+        })
+        return $tea.cast<AuthenticateSigResponse>(
+            await this.doRPCRequest('AuthenticateSig', '2018-01-12', 'HTTPS', 'POST', 'AK', 'json', req, runtime),
+            new AuthenticateSigResponse({})
+        )
+    }
+
+    async authenticateSig(request: AuthenticateSigRequest): Promise<AuthenticateSigResponse> {
+        let runtime = new $Util.RuntimeOptions({})
+        return await this.authenticateSigWithOptions(request, runtime)
+    }
+
+    async configurationStyleWithOptions(
+        request: ConfigurationStyleRequest,
+        runtime: $Util.RuntimeOptions
+    ): Promise<ConfigurationStyleResponse> {
+        Util.validateModel(request)
+        let req = new $OpenApi.OpenApiRequest({
+            body: Util.toMap(request)
+        })
+        return $tea.cast<ConfigurationStyleResponse>(
+            await this.doRPCRequest('ConfigurationStyle', '2018-01-12', 'HTTPS', 'POST', 'AK', 'json', req, runtime),
+            new ConfigurationStyleResponse({})
+        )
+    }
+
+    async configurationStyle(request: ConfigurationStyleRequest): Promise<ConfigurationStyleResponse> {
+        let runtime = new $Util.RuntimeOptions({})
+        return await this.configurationStyleWithOptions(request, runtime)
+    }
+
+    async createConfigurationWithOptions(
+        request: CreateConfigurationRequest,
+        runtime: $Util.RuntimeOptions
+    ): Promise<CreateConfigurationResponse> {
+        Util.validateModel(request)
+        let req = new $OpenApi.OpenApiRequest({
+            body: Util.toMap(request)
+        })
+        return $tea.cast<CreateConfigurationResponse>(
+            await this.doRPCRequest('CreateConfiguration', '2018-01-12', 'HTTPS', 'POST', 'AK', 'json', req, runtime),
+            new CreateConfigurationResponse({})
+        )
+    }
+
+    async createConfiguration(request: CreateConfigurationRequest): Promise<CreateConfigurationResponse> {
+        let runtime = new $Util.RuntimeOptions({})
+        return await this.createConfigurationWithOptions(request, runtime)
+    }
+
+    async describeAfsConfigNameWithOptions(
+        request: DescribeAfsConfigNameRequest,
+        runtime: $Util.RuntimeOptions
+    ): Promise<DescribeAfsConfigNameResponse> {
+        Util.validateModel(request)
+        let req = new $OpenApi.OpenApiRequest({
+            body: Util.toMap(request)
+        })
+        return $tea.cast<DescribeAfsConfigNameResponse>(
+            await this.doRPCRequest('DescribeAfsConfigName', '2018-01-12', 'HTTPS', 'POST', 'AK', 'json', req, runtime),
+            new DescribeAfsConfigNameResponse({})
+        )
+    }
+
+    async describeAfsConfigName(request: DescribeAfsConfigNameRequest): Promise<DescribeAfsConfigNameResponse> {
+        let runtime = new $Util.RuntimeOptions({})
+        return await this.describeAfsConfigNameWithOptions(request, runtime)
+    }
+
+    async describeAfsOneConfDataWithOptions(
+        request: DescribeAfsOneConfDataRequest,
+        runtime: $Util.RuntimeOptions
+    ): Promise<DescribeAfsOneConfDataResponse> {
+        Util.validateModel(request)
+        let req = new $OpenApi.OpenApiRequest({
+            body: Util.toMap(request)
+        })
+        return $tea.cast<DescribeAfsOneConfDataResponse>(
+            await this.doRPCRequest(
+                'DescribeAfsOneConfData',
+                '2018-01-12',
+                'HTTPS',
+                'POST',
+                'AK',
+                'json',
+                req,
+                runtime
+            ),
+            new DescribeAfsOneConfDataResponse({})
+        )
+    }
+
+    async describeAfsOneConfData(request: DescribeAfsOneConfDataRequest): Promise<DescribeAfsOneConfDataResponse> {
+        let runtime = new $Util.RuntimeOptions({})
+        return await this.describeAfsOneConfDataWithOptions(request, runtime)
+    }
+
+    async describeAfsTotalConfDataWithOptions(
+        request: DescribeAfsTotalConfDataRequest,
+        runtime: $Util.RuntimeOptions
+    ): Promise<DescribeAfsTotalConfDataResponse> {
+        Util.validateModel(request)
+        let req = new $OpenApi.OpenApiRequest({
+            body: Util.toMap(request)
+        })
+        return $tea.cast<DescribeAfsTotalConfDataResponse>(
+            await this.doRPCRequest(
+                'DescribeAfsTotalConfData',
+                '2018-01-12',
+                'HTTPS',
+                'POST',
+                'AK',
+                'json',
+                req,
+                runtime
+            ),
+            new DescribeAfsTotalConfDataResponse({})
+        )
+    }
+
+    async describeAfsTotalConfData(
+        request: DescribeAfsTotalConfDataRequest
+    ): Promise<DescribeAfsTotalConfDataResponse> {
+        let runtime = new $Util.RuntimeOptions({})
+        return await this.describeAfsTotalConfDataWithOptions(request, runtime)
+    }
+
+    async describeAfsVerifySigDataWithOptions(
+        request: DescribeAfsVerifySigDataRequest,
+        runtime: $Util.RuntimeOptions
+    ): Promise<DescribeAfsVerifySigDataResponse> {
+        Util.validateModel(request)
+        let req = new $OpenApi.OpenApiRequest({
+            body: Util.toMap(request)
+        })
+        return $tea.cast<DescribeAfsVerifySigDataResponse>(
+            await this.doRPCRequest(
+                'DescribeAfsVerifySigData',
+                '2018-01-12',
+                'HTTPS',
+                'POST',
+                'AK',
+                'json',
+                req,
+                runtime
+            ),
+            new DescribeAfsVerifySigDataResponse({})
+        )
+    }
+
+    async describeAfsVerifySigData(
+        request: DescribeAfsVerifySigDataRequest
+    ): Promise<DescribeAfsVerifySigDataResponse> {
+        let runtime = new $Util.RuntimeOptions({})
+        return await this.describeAfsVerifySigDataWithOptions(request, runtime)
+    }
+
+    async describeCaptchaDayWithOptions(
+        request: DescribeCaptchaDayRequest,
+        runtime: $Util.RuntimeOptions
+    ): Promise<DescribeCaptchaDayResponse> {
+        Util.validateModel(request)
+        let req = new $OpenApi.OpenApiRequest({
+            body: Util.toMap(request)
+        })
+        return $tea.cast<DescribeCaptchaDayResponse>(
+            await this.doRPCRequest('DescribeCaptchaDay', '2018-01-12', 'HTTPS', 'POST', 'AK', 'json', req, runtime),
+            new DescribeCaptchaDayResponse({})
+        )
+    }
+
+    async describeCaptchaDay(request: DescribeCaptchaDayRequest): Promise<DescribeCaptchaDayResponse> {
+        let runtime = new $Util.RuntimeOptions({})
+        return await this.describeCaptchaDayWithOptions(request, runtime)
+    }
+
+    async describeCaptchaIpCityWithOptions(
+        request: DescribeCaptchaIpCityRequest,
+        runtime: $Util.RuntimeOptions
+    ): Promise<DescribeCaptchaIpCityResponse> {
+        Util.validateModel(request)
+        let req = new $OpenApi.OpenApiRequest({
+            body: Util.toMap(request)
+        })
+        return $tea.cast<DescribeCaptchaIpCityResponse>(
+            await this.doRPCRequest('DescribeCaptchaIpCity', '2018-01-12', 'HTTPS', 'POST', 'AK', 'json', req, runtime),
+            new DescribeCaptchaIpCityResponse({})
+        )
+    }
+
+    async describeCaptchaIpCity(request: DescribeCaptchaIpCityRequest): Promise<DescribeCaptchaIpCityResponse> {
+        let runtime = new $Util.RuntimeOptions({})
+        return await this.describeCaptchaIpCityWithOptions(request, runtime)
+    }
+
+    async describeCaptchaMinWithOptions(
+        request: DescribeCaptchaMinRequest,
+        runtime: $Util.RuntimeOptions
+    ): Promise<DescribeCaptchaMinResponse> {
+        Util.validateModel(request)
+        let req = new $OpenApi.OpenApiRequest({
+            body: Util.toMap(request)
+        })
+        return $tea.cast<DescribeCaptchaMinResponse>(
+            await this.doRPCRequest('DescribeCaptchaMin', '2018-01-12', 'HTTPS', 'POST', 'AK', 'json', req, runtime),
+            new DescribeCaptchaMinResponse({})
+        )
+    }
+
+    async describeCaptchaMin(request: DescribeCaptchaMinRequest): Promise<DescribeCaptchaMinResponse> {
+        let runtime = new $Util.RuntimeOptions({})
+        return await this.describeCaptchaMinWithOptions(request, runtime)
+    }
+
+    async describeCaptchaOrderWithOptions(
+        request: DescribeCaptchaOrderRequest,
+        runtime: $Util.RuntimeOptions
+    ): Promise<DescribeCaptchaOrderResponse> {
+        Util.validateModel(request)
+        let req = new $OpenApi.OpenApiRequest({
+            body: Util.toMap(request)
+        })
+        return $tea.cast<DescribeCaptchaOrderResponse>(
+            await this.doRPCRequest('DescribeCaptchaOrder', '2018-01-12', 'HTTPS', 'POST', 'AK', 'json', req, runtime),
+            new DescribeCaptchaOrderResponse({})
+        )
+    }
+
+    async describeCaptchaOrder(request: DescribeCaptchaOrderRequest): Promise<DescribeCaptchaOrderResponse> {
+        let runtime = new $Util.RuntimeOptions({})
+        return await this.describeCaptchaOrderWithOptions(request, runtime)
+    }
+
+    async describeCaptchaRiskWithOptions(
+        request: DescribeCaptchaRiskRequest,
+        runtime: $Util.RuntimeOptions
+    ): Promise<DescribeCaptchaRiskResponse> {
+        Util.validateModel(request)
+        let req = new $OpenApi.OpenApiRequest({
+            body: Util.toMap(request)
+        })
+        return $tea.cast<DescribeCaptchaRiskResponse>(
+            await this.doRPCRequest('DescribeCaptchaRisk', '2018-01-12', 'HTTPS', 'POST', 'AK', 'json', req, runtime),
+            new DescribeCaptchaRiskResponse({})
+        )
+    }
+
+    async describeCaptchaRisk(request: DescribeCaptchaRiskRequest): Promise<DescribeCaptchaRiskResponse> {
+        let runtime = new $Util.RuntimeOptions({})
+        return await this.describeCaptchaRiskWithOptions(request, runtime)
+    }
+
+    async describeConfigNameWithOptions(
+        request: DescribeConfigNameRequest,
+        runtime: $Util.RuntimeOptions
+    ): Promise<DescribeConfigNameResponse> {
+        Util.validateModel(request)
+        let req = new $OpenApi.OpenApiRequest({
+            body: Util.toMap(request)
+        })
+        return $tea.cast<DescribeConfigNameResponse>(
+            await this.doRPCRequest('DescribeConfigName', '2018-01-12', 'HTTPS', 'POST', 'AK', 'json', req, runtime),
+            new DescribeConfigNameResponse({})
+        )
+    }
+
+    async describeConfigName(request: DescribeConfigNameRequest): Promise<DescribeConfigNameResponse> {
+        let runtime = new $Util.RuntimeOptions({})
+        return await this.describeConfigNameWithOptions(request, runtime)
+    }
+
+    async describeEarlyWarningWithOptions(
+        request: DescribeEarlyWarningRequest,
+        runtime: $Util.RuntimeOptions
+    ): Promise<DescribeEarlyWarningResponse> {
+        Util.validateModel(request)
+        let req = new $OpenApi.OpenApiRequest({
+            body: Util.toMap(request)
+        })
+        return $tea.cast<DescribeEarlyWarningResponse>(
+            await this.doRPCRequest('DescribeEarlyWarning', '2018-01-12', 'HTTPS', 'POST', 'AK', 'json', req, runtime),
+            new DescribeEarlyWarningResponse({})
+        )
+    }
+
+    async describeEarlyWarning(request: DescribeEarlyWarningRequest): Promise<DescribeEarlyWarningResponse> {
+        let runtime = new $Util.RuntimeOptions({})
+        return await this.describeEarlyWarningWithOptions(request, runtime)
+    }
+
+    async describeOrderInfoWithOptions(
+        request: DescribeOrderInfoRequest,
+        runtime: $Util.RuntimeOptions
+    ): Promise<DescribeOrderInfoResponse> {
+        Util.validateModel(request)
+        let req = new $OpenApi.OpenApiRequest({
+            body: Util.toMap(request)
+        })
+        return $tea.cast<DescribeOrderInfoResponse>(
+            await this.doRPCRequest('DescribeOrderInfo', '2018-01-12', 'HTTPS', 'POST', 'AK', 'json', req, runtime),
+            new DescribeOrderInfoResponse({})
+        )
+    }
+
+    async describeOrderInfo(request: DescribeOrderInfoRequest): Promise<DescribeOrderInfoResponse> {
+        let runtime = new $Util.RuntimeOptions({})
+        return await this.describeOrderInfoWithOptions(request, runtime)
+    }
+
+    async describePersonMachineListWithOptions(
+        request: DescribePersonMachineListRequest,
+        runtime: $Util.RuntimeOptions
+    ): Promise<DescribePersonMachineListResponse> {
+        Util.validateModel(request)
+        let req = new $OpenApi.OpenApiRequest({
+            body: Util.toMap(request)
+        })
+        return $tea.cast<DescribePersonMachineListResponse>(
+            await this.doRPCRequest(
+                'DescribePersonMachineList',
+                '2018-01-12',
+                'HTTPS',
+                'POST',
+                'AK',
+                'json',
+                req,
+                runtime
+            ),
+            new DescribePersonMachineListResponse({})
+        )
+    }
+
+    async describePersonMachineList(
+        request: DescribePersonMachineListRequest
+    ): Promise<DescribePersonMachineListResponse> {
+        let runtime = new $Util.RuntimeOptions({})
+        return await this.describePersonMachineListWithOptions(request, runtime)
+    }
+
+    async setEarlyWarningWithOptions(
+        request: SetEarlyWarningRequest,
+        runtime: $Util.RuntimeOptions
+    ): Promise<SetEarlyWarningResponse> {
+        Util.validateModel(request)
+        let req = new $OpenApi.OpenApiRequest({
+            body: Util.toMap(request)
+        })
+        return $tea.cast<SetEarlyWarningResponse>(
+            await this.doRPCRequest('SetEarlyWarning', '2018-01-12', 'HTTPS', 'POST', 'AK', 'json', req, runtime),
+            new SetEarlyWarningResponse({})
+        )
+    }
+
+    async setEarlyWarning(request: SetEarlyWarningRequest): Promise<SetEarlyWarningResponse> {
+        let runtime = new $Util.RuntimeOptions({})
+        return await this.setEarlyWarningWithOptions(request, runtime)
+    }
+
+    async updateConfigNameWithOptions(
+        request: UpdateConfigNameRequest,
+        runtime: $Util.RuntimeOptions
+    ): Promise<UpdateConfigNameResponse> {
+        Util.validateModel(request)
+        let req = new $OpenApi.OpenApiRequest({
+            body: Util.toMap(request)
+        })
+        return $tea.cast<UpdateConfigNameResponse>(
+            await this.doRPCRequest('UpdateConfigName', '2018-01-12', 'HTTPS', 'POST', 'AK', 'json', req, runtime),
+            new UpdateConfigNameResponse({})
+        )
+    }
+
+    async updateConfigName(request: UpdateConfigNameRequest): Promise<UpdateConfigNameResponse> {
+        let runtime = new $Util.RuntimeOptions({})
+        return await this.updateConfigNameWithOptions(request, runtime)
+    }
+}

+ 12 - 0
src/aliyun/aliyun.module.ts

@@ -0,0 +1,12 @@
+import { Module } from '@nestjs/common'
+import { AliyunService } from './aliyun.service'
+import { ConfigModule } from '@nestjs/config'
+import aliyunConfig from './config/aliyun.config'
+
+@Module({
+    imports: [ConfigModule.forFeature(aliyunConfig)],
+    controllers: [],
+    providers: [AliyunService],
+    exports: [AliyunService]
+})
+export class AliyunModule {}

+ 104 - 0
src/aliyun/aliyun.service.ts

@@ -0,0 +1,104 @@
+import { SendCodeDto } from './../sms/dto/sms.dto'
+import { Inject, Injectable, InternalServerErrorException, Logger, NotAcceptableException } from '@nestjs/common'
+import aliyunConfig from './config/aliyun.config'
+import { ConfigType } from '@nestjs/config'
+import Dysmsapi20170525, * as $Dysmsapi20170525 from '@alicloud/dysmsapi20170525'
+import OpenApi, * as $OpenApi from '@alicloud/openapi-client'
+import Util, * as $Util from '@alicloud/tea-util'
+import * as $tea from '@alicloud/tea-typescript'
+import { SendSmsResponse } from '@alicloud/dysmsapi20170525'
+import * as randomstring from 'randomstring'
+import * as OSS from 'ali-oss'
+import { buffer } from 'stream/consumers'
+import { AfsClient, AuthenticateSigRequest, AuthenticateSigResponse } from './afs'
+
+@Injectable()
+export class AliyunService {
+    constructor(
+        @Inject(aliyunConfig.KEY)
+        private readonly aliyunConfiguration: ConfigType<typeof aliyunConfig>
+    ) {}
+
+    public async sendCode(data: SendCodeDto) {
+        await this.authSig(data.sessionId, data.sig, data.token)
+        if (!/^1[3-9]\d{9}$/.test(data.phone)) {
+            throw new InternalServerErrorException('手机号码格式不正确')
+        }
+        let config = new $OpenApi.Config({
+            accessKeyId: this.aliyunConfiguration.accessKeyId,
+            accessKeySecret: this.aliyunConfiguration.accessKeySecret
+        })
+        config.endpoint = `dysmsapi.aliyuncs.com`
+        let client = new Dysmsapi20170525(config)
+        const code = randomstring.generate({ length: 4, charset: 'numeric' })
+        let sendSmsRequest = new $Dysmsapi20170525.SendSmsRequest({
+            phoneNumbers: data.phone,
+            signName: this.aliyunConfiguration.smsSign,
+            templateCode: this.aliyunConfiguration.smsTemplateCode,
+            templateParam: JSON.stringify({ code })
+        })
+        try {
+            const res: SendSmsResponse = await client.sendSmsWithOptions(sendSmsRequest, new $Util.RuntimeOptions({}))
+            if (res.statusCode === 200 && res.body.code == 'OK') {
+                return { phone: data.phone, code }
+            } else {
+                throw new InternalServerErrorException(res.body.message)
+            }
+        } catch (error) {
+            Logger.error(error.message)
+            throw new InternalServerErrorException(error.message)
+        }
+    }
+
+    public async uploadFile(path: string, data: Buffer) {
+        let client = new OSS({
+            endpoint: this.aliyunConfiguration.ossEndPoint,
+            accessKeyId: this.aliyunConfiguration.accessKeyId,
+            accessKeySecret: this.aliyunConfiguration.accessKeySecret,
+            bucket: this.aliyunConfiguration.ossBucket
+        })
+        try {
+            const result = await client.put(path, data)
+            Logger.log(result)
+            if (result.res.status === 200) {
+                return { url: result.url.replace('http:', 'https:') }
+            } else {
+                throw new InternalServerErrorException(result.res.statusMessage)
+            }
+        } catch (e) {
+            Logger.error(e)
+            throw new InternalServerErrorException(e.message)
+        }
+    }
+
+    public async authSig(sessionId: string, sig: string, token: string) {
+        var client = new AfsClient({
+            accessKeyId: this.aliyunConfiguration.accessKeyId,
+            accessKeySecret: this.aliyunConfiguration.accessKeySecret,
+            endpoint: 'afs.aliyuncs.com',
+            regionId: 'cn-hangzhou',
+            toMap: null
+        })
+        // AuthenticateSigRequest
+        var data = new AuthenticateSigRequest({
+            scene: 'nc_activity_h5',
+            sessionId: sessionId,
+            sig: sig,
+            appKey: 'FFFF0N0000000000B53B',
+            token: token,
+            remoteIp: '192.168.0.11'
+        })
+        try {
+            const res: AuthenticateSigResponse = await client.authenticateSig(data)
+            if (res.body.code === 100) {
+                return
+            } else {
+                Logger.error(res.body.msg)
+                throw new NotAcceptableException('验证失败')
+            }
+        } catch (e) {
+            Logger.error(e)
+            throw new InternalServerErrorException(e.message)
+        }
+    }
+}

+ 19 - 0
src/aliyun/config/aliyun.config.ts

@@ -0,0 +1,19 @@
+import { ConfigService, registerAs } from '@nestjs/config'
+import { config } from 'dotenv'
+
+config()
+
+const configService = new ConfigService()
+
+export default registerAs('aliyun', () => {
+    return {
+        accessKeyId: configService.get<string>('ALIYUN_ACCESS_KEY_ID'),
+        accessKeySecret: configService.get<string>('ALIYUN_ACCESS_KEY_SECRET'),
+        ossEndPoint: configService.get<string>('ALIYUN_OSS_ENDPOINT'),
+        ossBucket: configService.get<string>('ALIYUN_OSS_BUCKET'),
+        ossRegion: configService.get<string>('ALIYUN_OSS_REGION'),
+        ossCdn: configService.get<string>('ALIYUN_OSS_CDN'),
+        smsSign: configService.get<string>('ALIYUN_SMS_SIGN'),
+        smsTemplateCode: configService.get<string>('ALIYUN_SMS_TEMPLATE_CODE')
+    }
+})

+ 90 - 0
src/app.module.ts

@@ -0,0 +1,90 @@
+import { AliyunModule } from './aliyun/aliyun.module'
+import { Module } from '@nestjs/common'
+import { ConfigModule, ConfigService } from '@nestjs/config'
+import { TypeOrmModule } from '@nestjs/typeorm'
+import { ThrottlerModule } from '@nestjs/throttler'
+import * as Yup from 'yup'
+import { DevtoolsModule } from '@nestjs/devtools-integration'
+import { APP_FILTER } from '@nestjs/core'
+import { UsersModule } from './users/users.module'
+import { SmsModule } from './sms/sms.module'
+import { AuthModule } from './auth/auth.module'
+import { FileModule } from './file/file.module'
+import { SysConfigModule } from './sys-config/sys-config.module'
+import { AllExceptionsFilter } from './filters/all-exceptions-filter.filter'
+import { ScheduleModule } from '@nestjs/schedule'
+import { RcsNumberModule } from './rcs-number/rcs-number.module'
+import { TaskModule } from './task/task.module'
+import { PhoneListModule } from './phone-list/phone-list.module'
+import { DeviceModule } from './device/device.module'
+import { BalanceModule } from './balance/balance.module'
+import { ChannelModule } from './channel/channel.module'
+
+@Module({
+    imports: [
+        DevtoolsModule.register({
+            http: process.env.NODE_ENV !== 'production',
+            port: 8000
+        }),
+        ConfigModule.forRoot({
+            isGlobal: true,
+            validationSchema: Yup.object({
+                TYPEORM_HOST: Yup.string().required(),
+                TYPEORM_PORT: Yup.number().default(3306),
+                TYPEORM_USERNAME: Yup.string().required(),
+                TYPEORM_PASSWORD: Yup.string().required(),
+                TYPEORM_DATABASE: Yup.string().required()
+            })
+        }),
+        ThrottlerModule.forRootAsync({
+            imports: [ConfigModule],
+            inject: [ConfigService],
+            useFactory: (config: ConfigService) => ({
+                ttl: config.get<number>('THROTTLE_TTL'),
+                limit: config.get<number>('THROTTLE_LIMIT')
+            })
+        }),
+        TypeOrmModule.forRootAsync({
+            imports: [ConfigModule],
+            inject: [ConfigService],
+            useFactory: (config: ConfigService) => ({
+                type: 'mysql',
+                host: config.get<string>('TYPEORM_HOST'),
+                port: config.get<number>('TYPEORM_PORT'),
+                username: config.get<string>('TYPEORM_USERNAME'),
+                password: config.get<string>('TYPEORM_PASSWORD'),
+                database: config.get<string>('TYPEORM_DATABASE'),
+                synchronize: true,
+                entities: [__dirname + '/**/*.entity.{ts,js}'],
+                migrations: ['dist/migrations/**/*.js'],
+                subscribers: ['dist/subscriber/**/*.js'],
+                cli: {
+                    migrationsDir: config.get<string>('TYPEORM_MIGRATIONS_DIR'),
+                    subscribersDir: config.get<string>('TYPEORM_SUBSCRIBERS_DIR')
+                },
+                autoLoadEntities: true
+            })
+        }),
+        ScheduleModule.forRoot(),
+        AliyunModule,
+        SmsModule,
+        UsersModule,
+        AuthModule,
+        FileModule,
+        SysConfigModule,
+        RcsNumberModule,
+        TaskModule,
+        PhoneListModule,
+        DeviceModule,
+        BalanceModule,
+        ChannelModule
+    ],
+    controllers: [],
+    providers: [
+        {
+            provide: APP_FILTER,
+            useClass: AllExceptionsFilter
+        }
+    ]
+})
+export class AppModule {}

+ 45 - 0
src/auth/auth.controller.ts

@@ -0,0 +1,45 @@
+import { PhoneLoginDto } from './dto/login.dto'
+import { Body, Controller, Get, Param, Post } from '@nestjs/common'
+import { AuthService } from './auth.service'
+import { ApiTags } from '@nestjs/swagger'
+import { Public } from './public.decorator'
+import { HasRoles } from './roles.decorator'
+import { Role } from '../model/role.enum'
+import { UserRegisterDto } from 'src/users/dto/user-register.dto'
+
+@ApiTags('auth')
+@Controller('/auth')
+export class AuthController {
+    constructor(private readonly authService: AuthService) {}
+
+    @Public()
+    @Post('/phoneLogin')
+    async phoneLogin(@Body() loginDto: PhoneLoginDto) {
+        return await this.authService.loginByPhone(loginDto)
+    }
+
+    @Public()
+    @Post('/login')
+    async login(@Body() { username, password }) {
+        return await this.authService.login(username, password)
+    }
+
+    @Public()
+    @Post('/admin/login')
+    async loginAdmin(@Body() { username, password }) {
+        return await this.authService.loginAdmin(username, password)
+    }
+
+    @Get('/admin/user/:userId/token')
+    @HasRoles(Role.Admin)
+    async getToken(@Param('userId') userId: string) {
+        return await this.authService.getToken(Number(userId))
+    }
+
+    @Public()
+    @Post('/register')
+    async register(@Body() register: UserRegisterDto) {
+        return await this.authService.register(register)
+    }
+
+}

+ 30 - 0
src/auth/auth.module.ts

@@ -0,0 +1,30 @@
+import { Module } from '@nestjs/common'
+import { AuthService } from './auth.service'
+import { ConfigModule } from '@nestjs/config'
+import jwtConfig from './jwt.config'
+import { JwtModule } from '@nestjs/jwt'
+import { JwtStrategy } from './jwt.strategy'
+import { UsersModule } from '../users/users.module'
+import { AuthController } from './auth.controller'
+import { APP_GUARD } from '@nestjs/core'
+import { JwtAuthGuard } from './jwt-auth.guard'
+import { RolesGuard } from './roles.guard'
+
+@Module({
+    imports: [ConfigModule.forFeature(jwtConfig), JwtModule.registerAsync(jwtConfig.asProvider()), UsersModule],
+    providers: [
+        AuthService,
+        JwtStrategy,
+        {
+            provide: APP_GUARD,
+            useClass: JwtAuthGuard
+        },
+        {
+            provide: APP_GUARD,
+            useClass: RolesGuard
+        }
+    ],
+    controllers: [AuthController],
+    exports: [AuthService]
+})
+export class AuthModule {}

+ 69 - 0
src/auth/auth.service.ts

@@ -0,0 +1,69 @@
+import { PhoneLoginDto } from './dto/login.dto'
+import { Injectable, UnauthorizedException } from '@nestjs/common'
+import { JwtService } from '@nestjs/jwt'
+import { UsersService } from '../users/users.service'
+import { Role } from 'src/model/role.enum'
+import { UserRegisterDto } from 'src/users/dto/user-register.dto'
+
+@Injectable()
+export class AuthService {
+    constructor(private readonly usersService: UsersService, private readonly jwtService: JwtService) {}
+
+    async loginByPhone(loginDto: PhoneLoginDto) {
+        let user = await this.usersService.loginByPhone(loginDto.phone, loginDto.code, loginDto.invitor)
+        const payload = {
+            username: user.username,
+            sub: user.id,
+            roles: user.roles
+        }
+        this.usersService.updateIat(user)
+        return {
+            access_token: this.jwtService.sign(payload)
+        }
+    }
+
+    async login(username: string, password: string) {
+        let user = await this.usersService.login(username, password)
+        const payload = {
+            username: user.username,
+            sub: user.id,
+            roles: user.roles
+        }
+        this.usersService.updateIat(user)
+        return {
+            access_token: this.jwtService.sign(payload)
+        }
+    }
+
+    async loginAdmin(username: string, password: string) {
+        let user = await this.usersService.login(username, password)
+        /*if (!user.roles.includes(Role.Admin)) {
+            throw new UnauthorizedException('Permission denied')
+        }*/
+        const payload = {
+            username: user.username,
+            sub: user.id,
+            roles: user.roles
+        }
+        this.usersService.updateIat(user)
+        return {
+            access_token: this.jwtService.sign(payload)
+        }
+    }
+
+    async getToken(id: number) {
+        let user = await this.usersService.findById(id)
+        const payload = {
+            username: user.username,
+            sub: user.id,
+            roles: user.roles
+        }
+        return {
+            access_token: this.jwtService.sign(payload)
+        }
+    }
+
+    async register(register: UserRegisterDto) {
+        return await this.usersService.register(register)
+    }
+}

+ 11 - 0
src/auth/dto/login.dto.ts

@@ -0,0 +1,11 @@
+import { IsString } from 'class-validator'
+
+export class PhoneLoginDto {
+    @IsString()
+    readonly phone: string
+
+    @IsString()
+    readonly code: string
+
+    readonly invitor: number | null
+}

+ 22 - 0
src/auth/jwt-auth.guard.ts

@@ -0,0 +1,22 @@
+import { ExecutionContext, Injectable } from '@nestjs/common'
+import { Reflector } from '@nestjs/core'
+import { AuthGuard } from '@nestjs/passport'
+import { IS_PUBLIC_KEY } from './public.decorator'
+
+@Injectable()
+export class JwtAuthGuard extends AuthGuard('jwt') {
+    constructor(private reflector: Reflector) {
+        super()
+    }
+
+    canActivate(context: ExecutionContext) {
+        const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
+            context.getHandler(),
+            context.getClass()
+        ])
+        if (isPublic) {
+            return true
+        }
+        return super.canActivate(context)
+    }
+}

+ 13 - 0
src/auth/jwt.config.ts

@@ -0,0 +1,13 @@
+import { ConfigService, registerAs } from '@nestjs/config'
+import { config } from 'dotenv'
+
+config()
+
+const configService = new ConfigService()
+
+export default registerAs('jwt', () => {
+    return {
+        secret: configService.get<string>('JWT_SECRET_KEY'),
+        accessTokenTtl: parseInt(configService.get<string>('JWT_ACCESS_TOKEN_TTL') ?? '3600', 10)
+    }
+})

+ 43 - 0
src/auth/jwt.strategy.ts

@@ -0,0 +1,43 @@
+import { ExtractJwt, Strategy } from 'passport-jwt'
+import { PassportStrategy } from '@nestjs/passport'
+import { Inject, Injectable, Logger, UnauthorizedException } from '@nestjs/common'
+import jwtconfig from './jwt.config'
+import { ConfigType } from '@nestjs/config'
+import { UsersService } from 'src/users/users.service'
+import { Role } from 'src/model/role.enum'
+
+@Injectable()
+export class JwtStrategy extends PassportStrategy(Strategy) {
+    constructor(
+        @Inject(jwtconfig.KEY)
+        private readonly jwtConfiguration: ConfigType<typeof jwtconfig>,
+        private readonly userService: UsersService
+    ) {
+        super({
+            jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
+            ignoreExpiration: false,
+            secretOrKey: jwtConfiguration.secret
+        })
+    }
+
+    async validate(payload: any) {
+        const user = await this.userService.findById(payload.sub)
+        if (!user) {
+            throw new UnauthorizedException('User not found')
+        }
+        // if (!(payload.roles.includes(Role.Admin) || payload.roles.includes(Role.Api))) {
+        //     if (!user.iat) {
+        //         throw new UnauthorizedException('用户身份已过期,请重新登录')
+        //     }
+        //     if (payload.iat < user.iat) {
+        //         throw new UnauthorizedException('用户身份已过期,请重新登录')
+        //     }
+        // }
+        return {
+            id: payload.sub,
+            userId: payload.sub,
+            username: payload.username,
+            roles: payload.roles
+        }
+    }
+}

+ 4 - 0
src/auth/public.decorator.ts

@@ -0,0 +1,4 @@
+import { SetMetadata } from '@nestjs/common';
+
+export const IS_PUBLIC_KEY = 'isPublic';
+export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

+ 6 - 0
src/auth/roles.decorator.ts

@@ -0,0 +1,6 @@
+import { SetMetadata } from '@nestjs/common';
+
+export const HAS_ROLES_KEY = 'hasRoles';
+export const HAS_ANY_ROLES_KEY = 'hasAnyRoles';
+export const HasRoles = (...roles: string[]) => SetMetadata(HAS_ROLES_KEY, roles);
+export const HasAnyRoles = (...roles: string[]) => SetMetadata(HAS_ANY_ROLES_KEY, roles);

+ 49 - 0
src/auth/roles.guard.ts

@@ -0,0 +1,49 @@
+import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'
+import { Reflector } from '@nestjs/core'
+import { HAS_ANY_ROLES_KEY, HAS_ROLES_KEY } from './roles.decorator'
+import { Role } from '../model/role.enum'
+
+@Injectable()
+export class RolesGuard implements CanActivate {
+    constructor(private reflector: Reflector) {}
+
+    canActivate(context: ExecutionContext): boolean {
+        const classRoles = this.reflector.get<Role[]>(HAS_ROLES_KEY, context.getClass())
+        const classAnyRoles = this.reflector.get<Role[]>(HAS_ANY_ROLES_KEY, context.getClass())
+
+        const roles = this.reflector.get<Role[]>(HAS_ROLES_KEY, context.getHandler())
+        const anyRoles = this.reflector.get<Role[]>(HAS_ANY_ROLES_KEY, context.getHandler())
+        if (!classRoles && !classAnyRoles && !roles && !anyRoles) {
+            return true
+        }
+
+        let result = true
+        const request = context.switchToHttp().getRequest()
+        const userRoles = request.user?.roles || []
+
+        if (classRoles) {
+            result = result && matchAll(classRoles, userRoles)
+        }
+
+        if (classAnyRoles) {
+            result = result && matchAny(classAnyRoles, userRoles)
+        }
+
+        if (roles) {
+            result = result && matchAll(roles, userRoles)
+        }
+
+        if (anyRoles) {
+            result = result && matchAny(anyRoles, userRoles)
+        }
+        return result
+    }
+}
+
+function matchAny(roles: Role[], userRoles: Role[]) {
+    return roles.some((role) => userRoles.includes(role))
+}
+
+function matchAll(roles: Role[], userRoles: Role[]) {
+    return roles.every((role) => userRoles.includes(role))
+}

+ 32 - 0
src/balance/balance.controller.ts

@@ -0,0 +1,32 @@
+import { Body, Controller, Get, Param, Post } from '@nestjs/common'
+import { BalanceService } from './balance.service'
+import { PageRequest } from '../common/dto/page-request'
+import { BalanceRecord } from './entities/balance-record.entities'
+
+@Controller('balance')
+export class BalanceController {
+    constructor(private readonly balanceService: BalanceService) {
+    }
+
+    @Get('/:id')
+    async getBalance(@Param('id') id: string) {
+        return await this.balanceService.findBalanceByUserId(parseInt(id))
+    }
+
+    @Get('/recharge/:id/:amount')
+    async recharge(@Param('id') id: string, @Param('amount') amount: number) {
+        return await this.balanceService.rechargeBalance(parseInt(id), amount)
+    }
+
+    @Post('/records')
+    async findRecordsByUserId(@Body() page: PageRequest<BalanceRecord>) {
+        console.log('search:', page.search)
+        return await this.balanceService.findAllRecordsPage(page)
+    }
+
+    @Get('/updateRate/:id/:rate')
+    async updateRate(@Param('id') id: string, @Param('rate') rate: number) {
+        return await this.balanceService.updateRate(parseInt(id), rate)
+    }
+
+}

+ 20 - 0
src/balance/balance.module.ts

@@ -0,0 +1,20 @@
+import { Module } from '@nestjs/common'
+import { TypeOrmModule } from '@nestjs/typeorm'
+import { Balance } from './entities/balance.entities'
+import { BalanceRecord } from './entities/balance-record.entities'
+import { BalanceController } from './balance.controller'
+import { BalanceService } from './balance.service'
+import { UsersModule } from '../users/users.module'
+import { Users } from '../users/entities/users.entity'
+
+@Module({
+    imports: [
+        TypeOrmModule.forFeature([Balance, BalanceRecord, Users]),
+        UsersModule
+    ],
+    controllers: [BalanceController],
+    providers: [BalanceService],
+    exports: [BalanceService]
+})
+export class BalanceModule {
+}

+ 165 - 0
src/balance/balance.service.ts

@@ -0,0 +1,165 @@
+import { Injectable, Logger } from '@nestjs/common'
+import { Repository } from 'typeorm'
+import { Balance } from './entities/balance.entities'
+import { InjectRepository } from '@nestjs/typeorm'
+import { BalanceRecord, BalanceType } from './entities/balance-record.entities'
+import { UsersService } from '../users/users.service'
+import { Users } from '../users/entities/users.entity'
+import { PageRequest } from '../common/dto/page-request'
+import { paginate, Pagination } from 'nestjs-typeorm-paginate'
+import Decimal from 'decimal.js';
+
+@Injectable()
+export class BalanceService {
+
+    constructor(
+        @InjectRepository(Balance)
+        private balanceRepository: Repository<Balance>,
+        @InjectRepository(BalanceRecord)
+        private recordRepository: Repository<BalanceRecord>,
+        @InjectRepository(Users)
+        private userRepository: Repository<Users>,
+        private readonly usersService: UsersService
+    ) {
+    }
+
+    async findBalanceByUserId(userId: number): Promise<Balance> {
+        return await this.balanceRepository.findOne({
+            select: {
+                userId: true,
+                currentBalance: true
+            },
+            where: {
+                userId: userId
+            }
+        })
+    }
+
+    async rechargeBalance(userId: number, amount: number): Promise<string> {
+        try {
+            // 获取余额
+            let balance = await this.balanceRepository.findOne({
+                where: {
+                    userId: userId
+                }
+            })
+
+            // 如果余额不存在
+            if (!balance) {
+                balance = new Balance()
+                balance.userId = userId
+                balance.currentBalance = 0.00
+                balance.totalBalance = 0.00
+            }
+
+            const currentBalanceDecimal = new Decimal(String(balance.currentBalance));
+            const amountDecimal = new Decimal(amount);
+            const totalBalanceDecimal = new Decimal(String(balance.totalBalance));
+            const newBalanceDecimal = currentBalanceDecimal.plus(amountDecimal);
+            const newTotalBalanceDecimal = totalBalanceDecimal.plus(amountDecimal);
+
+            balance.currentBalance = newBalanceDecimal.toNumber()
+            balance.totalBalance = newTotalBalanceDecimal.toNumber()
+
+            await this.balanceRepository.save(balance)
+
+            // 充值记录
+            const record = new BalanceRecord()
+            record.userId = userId
+            record.balanceId = balance.id
+            record.amount = amount
+            record.type = BalanceType.RECHARGE
+            await this.recordRepository.save(record)
+
+            const users = await this.usersService.findById(userId)
+            users.balance = balance.currentBalance
+            await this.userRepository.save(users)
+
+            return 'Recharge success!'
+        } catch (e) {
+            Logger.error('Error recharge ', e, 'BalanceService')
+            return 'Recharge failure!'
+        }
+    }
+
+    async feeDeduction(userId: number, cost: number, users: Users) {
+        try {
+            // 获取余额
+            let balance = await this.balanceRepository.findOne({
+                where: {
+                    userId: userId
+                }
+            })
+
+            const currentBalanceDecimal = new Decimal(String(balance.currentBalance));
+            const costDecimal = new Decimal(cost);
+            const newBalanceDecimal = currentBalanceDecimal.minus(costDecimal);
+
+            balance.currentBalance = newBalanceDecimal.toNumber()
+
+            await this.balanceRepository.save(balance)
+
+            // 扣款记录
+            const record = new BalanceRecord()
+            record.userId = userId
+            record.balanceId = balance.id
+            record.amount = cost
+            record.type = BalanceType.CONSUMPTION
+            await this.recordRepository.save(record)
+
+            users.balance = balance.currentBalance
+            await this.userRepository.save(users)
+
+            return 'Deduction success!'
+        } catch (e) {
+            Logger.error('Error deduction ', e, 'BalanceService')
+            throw new Error('Deduction failure!')
+        }
+    }
+
+    async findAllRecords(): Promise<BalanceRecord[]> {
+        return await this.recordRepository.find()
+    }
+
+    async findRecordsByUserId(userId: number): Promise<BalanceRecord[]> {
+        return await this.recordRepository.find({
+            where: {
+                userId: userId
+            }
+        })
+    }
+
+    async findAllRecordsPage(req: PageRequest<BalanceRecord>): Promise<Pagination<BalanceRecord>> {
+        return await paginate<BalanceRecord>(this.recordRepository, req.page, req.search)
+    }
+
+    async updateRate(userId: number, rate: number): Promise<string> {
+        try {
+            let balance = await this.balanceRepository.findOne({
+                where: {
+                    userId: userId
+                }
+            })
+
+            // 如果余额不存在
+            if (!balance) {
+                balance = new Balance()
+                balance.userId = userId
+            }
+
+            balance.rate = rate
+            await this.balanceRepository.save(balance)
+
+            const users = await this.usersService.findById(userId)
+            users.rate = rate
+            await this.userRepository.save(users)
+
+            return 'update rate success!'
+        } catch (e) {
+            Logger.error('Error rate ', e, 'RcsService')
+            return 'update rate fail!'
+        }
+    }
+
+
+}

+ 36 - 0
src/balance/entities/balance-record.entities.ts

@@ -0,0 +1,36 @@
+import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'
+import { number } from 'yup'
+
+export enum BalanceType {
+    RECHARGE = 'recharge',
+    CONSUMPTION = 'consumption',
+    REFUND = 'refund',
+    OTHER = 'other'
+}
+
+@Entity()
+export class BalanceRecord {
+
+    @PrimaryGeneratedColumn()
+    id: number
+
+    @Column()
+    userId: number
+
+    @Column({ nullable: true })
+    balanceId: number
+
+    @Column({ type: 'enum', enum: BalanceType, nullable: false, default: BalanceType.RECHARGE })
+    type: BalanceType
+
+    @Column('decimal', {
+        precision: 10,
+        scale: 2,
+        default: 0.00
+    })
+    amount: number
+
+    @CreateDateColumn()
+    createDate: Date
+
+}

+ 34 - 0
src/balance/entities/balance.entities.ts

@@ -0,0 +1,34 @@
+import { Column, Entity, OneToOne, PrimaryGeneratedColumn } from 'typeorm'
+import { Users } from '../../users/entities/users.entity'
+
+@Entity()
+export class Balance {
+
+    @PrimaryGeneratedColumn()
+    id: number
+
+    @Column()
+    userId: number
+
+    @Column('decimal', {
+        precision: 10,
+        scale: 2,
+        default: 0.00
+    })
+    currentBalance: number
+
+    @Column('decimal', {
+        precision: 10,
+        scale: 2,
+        default: 0.00
+    })
+    totalBalance: number
+
+    @Column('decimal', {
+        precision: 2,
+        scale: 2,
+        default: 0.00
+    })
+    rate: number
+
+}

+ 28 - 0
src/channel/channel.controller.ts

@@ -0,0 +1,28 @@
+import { Public } from '../auth/public.decorator'
+import { Body, Controller, Post, Put } from '@nestjs/common'
+import { ChannelService } from './channel.service'
+import { PageRequest } from '../common/dto/page-request'
+import { Channel } from './entities/channel.entities'
+
+@Controller('channel')
+@Public()
+export class ChannelController {
+    constructor(private readonly channelService: ChannelService) {
+    }
+
+    @Post('/')
+    async findAll(@Body() page: PageRequest<Channel>) {
+        return await this.channelService.findAll(page)
+    }
+
+    @Put('/')
+    async create(@Body() body: any) {
+        return await this.channelService.create(body)
+    }
+
+    @Post('/delete')
+    async delete(@Body() id: number) {
+        return await this.channelService.delete(id)
+    }
+
+}

+ 18 - 0
src/channel/channel.module.ts

@@ -0,0 +1,18 @@
+import { Module } from '@nestjs/common'
+import { TypeOrmModule } from '@nestjs/typeorm'
+import { Channel } from './entities/channel.entities'
+import { TaskModule } from '../task/task.module'
+import { ChannelController } from './channel.controller'
+import { ChannelService } from './channel.service'
+
+@Module({
+    imports: [
+        TypeOrmModule.forFeature([Channel]),
+        TaskModule
+    ],
+    controllers: [ChannelController],
+    providers: [ChannelService],
+    exports: [ChannelService]
+})
+export class ChannelModule {
+}

+ 29 - 0
src/channel/channel.service.ts

@@ -0,0 +1,29 @@
+import { Injectable } from '@nestjs/common'
+import { Repository } from 'typeorm'
+import { Channel } from './entities/channel.entities'
+import { InjectRepository } from '@nestjs/typeorm'
+import { PageRequest } from '../common/dto/page-request'
+import { paginate, Pagination } from 'nestjs-typeorm-paginate'
+
+@Injectable()
+export class ChannelService {
+
+    constructor(
+        @InjectRepository(Channel)
+        private channelRepository: Repository<Channel>
+    ) {
+    }
+
+    async findAll(req: PageRequest<Channel>): Promise<Pagination<Channel>> {
+        return await paginate<Channel>(this.channelRepository, req.page, req.search)
+    }
+
+    async create(channel: Channel): Promise<Channel> {
+        return await this.channelRepository.save(channel)
+    }
+
+    async delete(id: number): Promise<void> {
+        await this.channelRepository.delete(id)
+    }
+
+}

+ 26 - 0
src/channel/entities/channel.entities.ts

@@ -0,0 +1,26 @@
+import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'
+
+@Entity()
+export class Channel {
+    @PrimaryGeneratedColumn()
+    id: number
+
+    @Column()
+    mcc: string
+
+    @Column()
+    mnc: string
+
+    @Column()
+    country: string
+
+    @Column()
+    operator: string
+
+    @CreateDateColumn()
+    createdAt: Date
+
+    @Column({ nullable: true })
+    remark: string
+
+}

+ 10 - 0
src/common/dto/page-request.ts

@@ -0,0 +1,10 @@
+import { IsObject } from 'class-validator'
+import { IPaginationOptions } from 'nestjs-typeorm-paginate'
+import { FindManyOptions, FindOptionsWhere } from 'typeorm'
+
+export class PageRequest<T> {
+    @IsObject()
+    page: IPaginationOptions = { page: 1, limit: 20 }
+
+    search?: FindOptionsWhere<T> | FindManyOptions<T>
+}

+ 5 - 0
src/common/enums/stream-platform.enum.ts

@@ -0,0 +1,5 @@
+export enum StreamPlatform {
+    Twitch = 'twitch',
+    Bilibili = 'bilibili',
+    Douyu = 'douyu'
+}

+ 19 - 0
src/device/device.controller.ts

@@ -0,0 +1,19 @@
+import { Body, Controller, Delete, Param, Post } from '@nestjs/common'
+import { DeviceService } from './device.service'
+import { PageRequest } from 'src/common/dto/page-request'
+import { Device } from './entities/device.entity'
+
+@Controller('device')
+export class DeviceController {
+    constructor(private readonly deviceService: DeviceService) {}
+
+    @Post('/')
+    async findAllDevice(@Body() page: PageRequest<Device>) {
+        return await this.deviceService.findAllDevice(page)
+    }
+
+    @Delete('/:id')
+    async deviceDisconnect(@Param('id') id: string) {
+        return await this.deviceService.deleteDevice(id)
+    }
+}

+ 13 - 0
src/device/device.module.ts

@@ -0,0 +1,13 @@
+import { Module } from '@nestjs/common'
+import { DeviceService } from './device.service'
+import { DeviceController } from './device.controller'
+import { TypeOrmModule } from '@nestjs/typeorm'
+import { Device } from './entities/device.entity'
+
+@Module({
+    imports: [TypeOrmModule.forFeature([Device])],
+    providers: [DeviceService],
+    controllers: [DeviceController],
+    exports: [DeviceService]
+})
+export class DeviceModule {}

+ 66 - 0
src/device/device.service.ts

@@ -0,0 +1,66 @@
+import { Injectable } from '@nestjs/common'
+import { PageRequest } from '../common/dto/page-request'
+import { Device } from './entities/device.entity'
+import { Pagination, paginate } from 'nestjs-typeorm-paginate'
+import { InjectRepository } from '@nestjs/typeorm'
+import { Repository } from 'typeorm'
+
+@Injectable()
+export class DeviceService {
+    constructor(
+        @InjectRepository(Device)
+        private deviceRepository: Repository<Device>
+    ) {}
+
+    async findAllDevice(req: PageRequest<Device>): Promise<Pagination<Device>> {
+        return await paginate<Device>(this.deviceRepository, req.page, req.search)
+    }
+
+    async deleteDevice(id: string) {
+        await this.deviceRepository.delete(id)
+    }
+
+    async deviceConnect(id: string, socketId: string, model: string, name: string) {
+        let device = await this.deviceRepository.findOneBy({ id })
+        if (!device) {
+            device = new Device()
+        }
+        device.id = id
+        device.socketId = socketId
+        device.model = model
+        device.name = name
+        device.online = true
+        device.canSend = false
+        await this.deviceRepository.save(device)
+    }
+
+    async deviceDisconnect(id: string) {
+        const device = await this.deviceRepository.findOneBy({ id })
+        if (device) {
+            device.online = false
+            device.busy = false
+            device.canSend = false
+            await this.deviceRepository.save(device)
+        }
+    }
+
+    async updateDevice(socketId: string, data: any) {
+        const device = await this.deviceRepository.findOneBy({ socketId })
+        if (device) {
+            Object.assign(device, data)
+            await this.deviceRepository.save(device)
+        }
+    }
+
+    async findAvailableDevice() {
+        return await this.deviceRepository.findOneBy({ online: true, busy: false, canSend: true })
+    }
+
+    async setBusy(id: string, busy: boolean) {
+        const device = await this.deviceRepository.findOneBy({ id })
+        if (device) {
+            device.busy = busy
+            await this.deviceRepository.save(device)
+        }
+    }
+}

+ 28 - 0
src/device/entities/device.entity.ts

@@ -0,0 +1,28 @@
+import { Column, CreateDateColumn, Entity, PrimaryColumn, PrimaryGeneratedColumn } from 'typeorm'
+
+@Entity()
+export class Device {
+    @PrimaryColumn({ length: 50 })
+    id: string
+
+    @CreateDateColumn()
+    createdAt: Date
+
+    @Column()
+    model: string
+
+    @Column()
+    name: string
+
+    @Column()
+    online: boolean
+
+    @Column()
+    canSend: boolean
+
+    @Column()
+    socketId: string
+
+    @Column({ default: false })
+    busy: boolean
+}

+ 87 - 0
src/events/events.gateway.ts

@@ -0,0 +1,87 @@
+import { Inject, Logger, forwardRef } from '@nestjs/common'
+import {
+    MessageBody,
+    OnGatewayConnection,
+    OnGatewayDisconnect,
+    OnGatewayInit,
+    SubscribeMessage,
+    WebSocketGateway,
+    WebSocketServer
+} from '@nestjs/websockets'
+import { Server, Socket } from 'socket.io'
+import { DeviceService } from '../device/device.service'
+
+export interface Message {
+    id: string
+    action: string
+    data?: any
+}
+
+@WebSocketGateway({
+    cors: {
+        origin: true,
+        methods: 'GET,POST,PUT,PATCH,DELETE',
+        credentials: true
+    }
+})
+export class EventsGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
+    @WebSocketServer()
+    server: Server
+
+    private callbacks: { [key: string]: any } = {}
+
+    constructor(private deviceService: DeviceService) {}
+
+    afterInit(server: any) {
+        Logger.log('Initialized!', 'EventsGateway')
+    }
+
+    handleConnection(client: Socket, ...args: any[]) {
+        const id = client.handshake.query.id as string
+        const model = client.handshake.query.model as string
+        const name = client.handshake.query.name as string
+        Logger.log(`Client connected: id=${id}, model=${model}, name=${name}`, 'EventsGateway')
+        this.deviceService.deviceConnect(id, client.id, model, name)
+    }
+
+    handleDisconnect(client: any) {
+        const id = client.handshake.query.id
+        const model = client.handshake.query.model
+        const name = client.handshake.query.name
+        Logger.log(`Client disconnected: id=${id}, model=${model}, name=${name}`, 'EventsGateway')
+        this.deviceService.deviceDisconnect(client.handshake.query.id as string)
+    }
+
+    @SubscribeMessage('message')
+    handleEvent(client: Socket, data: any) {
+        Logger.log(`Client ${client.id} sent: ${JSON.stringify(data)}`, 'EventsGateway')
+        if ('updateDevice' === data.action) {
+            this.deviceService.updateDevice(client.id, data.data)
+        }
+    }
+
+    @SubscribeMessage('callback')
+    handleCallback(client: Socket, data: any) {
+        Logger.log(`Received callback: ${JSON.stringify(data)}`, 'EventsGateway')
+        if (this.callbacks[data.id]) {
+            const success = data.status === 0
+            if (success) {
+                this.callbacks[data.id][0](data.data)
+            } else {
+                this.callbacks[data.id][1](data.error)
+            }
+            delete this.callbacks[data.id]
+        }
+    }
+
+    public emitEvent(event: string, data: any) {
+        this.server.emit(event, data)
+    }
+
+    public sendForResult(message: Message, to: string) {
+        return new Promise((resolve, reject) => {
+            this.callbacks[message.id] = [resolve, reject]
+            this.server.to(to).emit('message', message)
+        })
+    }
+}

+ 10 - 0
src/events/events.module.ts

@@ -0,0 +1,10 @@
+import { Module } from '@nestjs/common'
+import { EventsGateway } from './events.gateway'
+import { DeviceModule } from 'src/device/device.module'
+
+@Module({
+    imports: [DeviceModule],
+    providers: [EventsGateway],
+    exports: [EventsGateway]
+})
+export class EventsModule {}

+ 19 - 0
src/file/file.controller.ts

@@ -0,0 +1,19 @@
+import { FileService } from './file.service'
+import { Controller, UploadedFile, UseInterceptors } from '@nestjs/common'
+import { ApiTags } from '@nestjs/swagger'
+import { Public } from '../auth/public.decorator'
+import { Post } from '@nestjs/common'
+import { FileInterceptor } from '@nestjs/platform-express'
+
+@ApiTags('file')
+@Controller('file')
+export class FileController {
+    constructor(private readonly fileService: FileService) {}
+
+    @Public()
+    @Post('upload')
+    @UseInterceptors(FileInterceptor('file'))
+    public async uploadFile(@UploadedFile() file: Express.Multer.File) {
+        return await this.fileService.upload(file)
+    }
+}

+ 12 - 0
src/file/file.module.ts

@@ -0,0 +1,12 @@
+import { Module } from '@nestjs/common'
+import { FileService } from './file.service'
+import { FileController } from './file.controller'
+import { AliyunModule } from '../aliyun/aliyun.module'
+
+@Module({
+    imports: [AliyunModule],
+    providers: [FileService],
+    controllers: [FileController],
+    exports: [FileService]
+})
+export class FileModule {}

+ 37 - 0
src/file/file.service.ts

@@ -0,0 +1,37 @@
+import { Injectable, Logger } from '@nestjs/common'
+import { AliyunService } from '../aliyun/aliyun.service'
+import * as randomstring from 'randomstring'
+import { format } from 'date-fns'
+
+@Injectable()
+export class FileService {
+    constructor(private readonly aliyunService: AliyunService) {}
+
+    public async upload(file: Express.Multer.File) {
+        const { originalname, buffer, mimetype } = file
+        let path = 'file/'
+        if (mimetype) {
+            path = mimetype.split('/')[0] + '/'
+        }
+        path +=
+            format(new Date(), 'yyyyMMdd/') +
+            randomstring.generate({ length: 8, charset: 'alphanumeric' }).toLowerCase() +
+            this.getFileExt(originalname)
+        Logger.log(`upload file name=${originalname} type=${mimetype}`)
+        const result = await this.aliyunService.uploadFile(path, buffer)
+        return result
+    }
+
+    public getFileExt(filename: string) {
+        const index = filename.lastIndexOf('.')
+        if (index === -1) {
+            return ''
+        }
+        return filename.substring(index)
+    }
+
+    public async uploadBuffer(buffer: Buffer, path) {
+        const result = await this.aliyunService.uploadFile(path, buffer)
+        return result
+    }
+}

+ 35 - 0
src/filters/all-exceptions-filter.filter.ts

@@ -0,0 +1,35 @@
+import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, Logger } from '@nestjs/common'
+import { HttpAdapterHost } from '@nestjs/core'
+
+@Catch()
+export class AllExceptionsFilter implements ExceptionFilter {
+    constructor(private readonly httpAdapterHost: HttpAdapterHost) {}
+
+    catch(exception: unknown, host: ArgumentsHost): void {
+        // In certain situations `httpAdapter` might not be available in the
+        // constructor method, thus we should resolve it here.
+        const { httpAdapter } = this.httpAdapterHost
+
+        const ctx = host.switchToHttp()
+
+        const httpStatus = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR
+        const path = httpAdapter.getRequestUrl(ctx.getRequest())
+        let res: any = {
+            status: httpStatus,
+            path: httpAdapter.getRequestUrl(ctx.getRequest()),
+            message: (exception as any).message
+        }
+        if (exception instanceof HttpException) {
+            res = exception.getResponse()
+            if (res.message instanceof Array) {
+                res.message = res.message.join()
+            }
+        }
+        Logger.error(`status=${httpStatus}, path=${path}, msg=${res.message}`, 'GlobalExceptionFilter')
+        if (!(exception instanceof HttpException)) {
+            console.trace(exception)
+        }
+
+        httpAdapter.reply(ctx.getResponse(), res, httpStatus)
+    }
+}

+ 30 - 0
src/helpers/configure-swagger-docs.helper.ts

@@ -0,0 +1,30 @@
+import { INestApplication } from '@nestjs/common'
+import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'
+import { ConfigService } from '@nestjs/config'
+import * as basicAuth from 'express-basic-auth'
+
+const SWAGGER_ENVS = ['local', 'dev', 'staging']
+
+export function configureSwaggerDocs(app: INestApplication, configService: ConfigService) {
+    if (SWAGGER_ENVS.includes(configService.get<string>('NODE_ENV'))) {
+        app.use(
+            ['/docs', '/docs-json', '/docs-yaml'],
+            basicAuth({
+                challenge: true,
+                users: {
+                    [configService.get<string>('SWAGGER_USER')]: configService.get<string>('SWAGGER_PASSWORD')
+                }
+            })
+        )
+
+        const config = new DocumentBuilder()
+            .setTitle('API')
+            .setDescription('The API description')
+            .setVersion('1.0')
+            .addTag('auth')
+            .addBearerAuth()
+            .build()
+        const document = SwaggerModule.createDocument(app, config)
+        SwaggerModule.setup('/docs', app, document)
+    }
+}

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 7 - 0
src/libs/danmaku-websocket.min.js


+ 46 - 0
src/main.ts

@@ -0,0 +1,46 @@
+import { NestFactory } from '@nestjs/core'
+import { AppModule } from './app.module'
+import { Logger, ValidationPipe } from '@nestjs/common'
+import { ConfigService } from '@nestjs/config'
+import { configureSwaggerDocs } from './helpers/configure-swagger-docs.helper'
+import { NestExpressApplication } from '@nestjs/platform-express'
+import { join } from 'path'
+
+async function bootstrap() {
+    const app = await NestFactory.create<NestExpressApplication>(AppModule, {
+        snapshot: true,
+        abortOnError: true
+    })
+    const configService = app.get<ConfigService>(ConfigService)
+
+    app.setGlobalPrefix('api')
+    app.useGlobalPipes(
+        new ValidationPipe({
+            whitelist: false,
+            transform: true,
+            forbidNonWhitelisted: false,
+            transformOptions: {
+                enableImplicitConversion: true
+            }
+        })
+    )
+
+    configureSwaggerDocs(app, configService)
+
+    app.enableCors({
+        origin: true,
+        methods: 'GET,POST,PUT,PATCH,DELETE',
+        credentials: true
+    })
+    app.useStaticAssets(join(__dirname, '..', 'public'))
+    app.setBaseViewsDir(join(__dirname, '..', 'views'))
+    app.setViewEngine('hbs')
+
+    const port = configService.get<number>('NODE_API_PORT') || 3000
+    await app.listen(port)
+    Logger.log(`Url for OpenApi: ${await app.getUrl()}/docs`, 'Swagger')
+}
+bootstrap().catch((err) => {
+    Logger.error(err, 'Bootstrap')
+    process.exit(1)
+})

+ 5 - 0
src/model/role.enum.ts

@@ -0,0 +1,5 @@
+export enum Role {
+    User = 'user',
+    Admin = 'admin',
+    Api = 'api'
+}

+ 22 - 0
src/phone-list/entities/phone-list.entity.ts

@@ -0,0 +1,22 @@
+import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'
+
+@Entity()
+export class PhoneList {
+    @PrimaryGeneratedColumn()
+    id: number
+
+    @CreateDateColumn()
+    createdAt: Date
+
+    @Column()
+    userId: number
+
+    @Column()
+    name: string
+
+    @Column()
+    count: number
+
+    @Column()
+    remark: string
+}

+ 16 - 0
src/phone-list/entities/phone.entity.ts

@@ -0,0 +1,16 @@
+import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'
+
+@Entity()
+export class Phone {
+    @PrimaryGeneratedColumn()
+    id: number
+
+    @CreateDateColumn()
+    createdAt: Date
+
+    @Column({ nullable: false })
+    number: string
+
+    @Column({ nullable: false })
+    listId: number
+}

+ 74 - 0
src/phone-list/phone-list.controller.ts

@@ -0,0 +1,74 @@
+import {
+    Body,
+    Controller,
+    Delete,
+    Get,
+    Param,
+    Post,
+    Put,
+    Req,
+    Res,
+    UploadedFile,
+    UseInterceptors
+} from '@nestjs/common'
+import { PageRequest } from 'src/common/dto/page-request'
+import { PhoneList } from './entities/phone-list.entity'
+import { Phone } from './entities/phone.entity'
+import { PhoneListService } from './phone-list.service'
+import { FileInterceptor } from '@nestjs/platform-express'
+
+@Controller('phone-list')
+export class PhoneListController {
+    constructor(private readonly phoneListService: PhoneListService) {}
+
+    @Post('/')
+    async findAllPhoneList(@Req() req, @Body() page: PageRequest<PhoneList>) {
+        if (!req.user.roles.includes('admin')) {
+            ;(page.search as any).where.userId = req.user.id
+        }
+        return await this.phoneListService.findAllPhoneList(page)
+    }
+
+    @Put('/')
+    async createPhoneList(@Req() req) {
+        req.body.userId = req.user.id
+        return await this.phoneListService.createPhoneList(req.body)
+    }
+
+    @Post('/phone')
+    async findAllPhone(@Body() page: PageRequest<Phone>) {
+        return await this.phoneListService.findAllPhone(page)
+    }
+
+    @Put('/phone')
+    async createPhone(@Req() req) {
+        return await this.phoneListService.createPhone(req.body)
+    }
+
+    @Delete('/phone/:id')
+    async delPhone(@Param('id') id: string) {
+        return await this.phoneListService.delPhone(parseInt(id))
+    }
+
+    @Delete('/:id')
+    async delPhoneList(@Param('id') id: string) {
+        return await this.phoneListService.delPhoneList(parseInt(id))
+    }
+
+    @Post('/:id/import')
+    @UseInterceptors(FileInterceptor('file'))
+    async importPhoneList(@Param('id') id: string, @UploadedFile() file: Express.Multer.File) {
+        const { buffer } = file
+        const phones = buffer
+            .toString()
+            .replaceAll('\r\n', '\n')
+            .split('\n')
+            .filter((phone) => phone.length > 0)
+        await this.phoneListService.importList(parseInt(id), phones)
+    }
+
+    @Get('/:id/download')
+    async download(@Param('id') id: string, @Res() res) {
+        await this.phoneListService.downloadPhoneList(parseInt(id), res)
+    }
+}

+ 14 - 0
src/phone-list/phone-list.module.ts

@@ -0,0 +1,14 @@
+import { Module } from '@nestjs/common'
+import { PhoneListController } from './phone-list.controller'
+import { PhoneListService } from './phone-list.service'
+import { TypeOrmModule } from '@nestjs/typeorm'
+import { PhoneList } from './entities/phone-list.entity'
+import { Phone } from './entities/phone.entity'
+
+@Module({
+    imports: [TypeOrmModule.forFeature([PhoneList, Phone])],
+    controllers: [PhoneListController],
+    providers: [PhoneListService],
+    exports: [PhoneListService]
+})
+export class PhoneListModule {}

+ 80 - 0
src/phone-list/phone-list.service.ts

@@ -0,0 +1,80 @@
+import { Injectable } from '@nestjs/common'
+import { InjectRepository } from '@nestjs/typeorm'
+import { PhoneList } from './entities/phone-list.entity'
+import { DataSource, Repository } from 'typeorm'
+import { Phone } from './entities/phone.entity'
+import { PageRequest } from '../common/dto/page-request'
+import { Pagination, paginate } from 'nestjs-typeorm-paginate'
+import { Response } from 'express'
+
+@Injectable()
+export class PhoneListService {
+    constructor(
+        @InjectRepository(PhoneList)
+        private phoneListRepository: Repository<PhoneList>,
+        @InjectRepository(Phone)
+        private phoneRepository: Repository<Phone>,
+        private readonly dataSource: DataSource
+    ) {}
+
+    async findAllPhoneList(req: PageRequest<PhoneList>): Promise<Pagination<PhoneList>> {
+        return await paginate<PhoneList>(this.phoneListRepository, req.page, req.search)
+    }
+
+    async findAllPhone(req: PageRequest<Phone>): Promise<Pagination<Phone>> {
+        return await paginate<Phone>(this.phoneRepository, req.page, req.search)
+    }
+
+    async createPhoneList(phoneList: PhoneList): Promise<PhoneList> {
+        return await this.phoneListRepository.save(phoneList)
+    }
+
+    async importList(listId: number, phones: string[]) {
+        await this.dataSource.manager.insert(
+            Phone,
+            this.phoneRepository.create(
+                phones.map((number) => {
+                    const phone = new Phone()
+                    phone.listId = listId
+                    // 如果number不带+号,添加+号
+                    if (!number.startsWith('+')) {
+                        number = '+' + number
+                    }
+                    phone.number = number
+                    return phone
+                })
+            )
+        )
+    }
+
+    async createPhone(phone: Phone): Promise<Phone> {
+        return await this.phoneRepository.save(phone)
+    }
+
+    async delPhoneList(id: number): Promise<void> {
+        await this.phoneListRepository.delete(id)
+    }
+
+    async delPhone(id: number): Promise<void> {
+        await this.phoneRepository.delete(id)
+    }
+
+    async findPhoneByListId(listId: number): Promise<Phone[]> {
+        return await this.phoneRepository.findBy({ listId })
+    }
+
+    async findCountByListId(listId: number): Promise<number> {
+        return await this.phoneRepository.countBy({ listId })
+    }
+
+    async downloadPhoneList(listId: number, res: Response) {
+        const phones = await this.findPhoneByListId(listId)
+        const phoneList = await this.phoneListRepository.findOneBy({ id: listId })
+        const fileName = `${phoneList.name}.txt`
+        const fileContent = phones.map((phone) => phone.number).join('\n')
+        res.setHeader('Content-disposition', `attachment; filename=${fileName}`)
+        res.setHeader('Content-type', 'text/plain')
+        res.write(fileContent)
+        res.end()
+    }
+}

+ 57 - 0
src/rcs-number/entities/rcs-number.entity.ts

@@ -0,0 +1,57 @@
+import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'
+import { JsonTransformer } from '../../transformers/json.transformer'
+
+export enum RcsNumberStatus {
+    PENDING = 'pending',
+    SUCCESS = 'success',
+    EXPIRED = 'expired',
+    ERROR = 'error'
+}
+
+export enum RcsNumberSource {
+    usacode = 'usacode',
+    mwze167 = 'mwze167'
+}
+
+@Entity()
+export class RcsNumber {
+    @PrimaryGeneratedColumn()
+    id: number
+
+    @CreateDateColumn()
+    createdAt: Date
+
+    @Column()
+    expiryTime: Date
+
+    @Column({ type: 'enum', enum: RcsNumberSource, nullable: false })
+    from: string
+
+    @Column()
+    mcc: string
+
+    @Column()
+    mnc: string
+
+    @Column()
+    country: string
+
+    @Column({ nullable: false })
+    number: string
+
+    @Column({ type: 'text' })
+    message: string
+
+    @Column({ type: 'enum', enum: RcsNumberStatus, nullable: false, default: RcsNumberStatus.PENDING })
+    status: RcsNumberStatus
+
+    @Column({ type: 'text', transformer: new JsonTransformer() })
+    extra: any
+
+    @Column({ nullable: true })
+    deviceId: string
+
+    @Column()
+    taskId: string
+
+}

+ 33 - 0
src/rcs-number/helpers.ts

@@ -0,0 +1,33 @@
+export function checkAndFormatNumber(country: string, number: string) {
+    switch (country) {
+        case 'us':
+            number = number.replace(/^\+1/, '').replace(/^1(\d{10})$/, '$1')
+            if (!/^\d{10}$/.test(number)) {
+                throw new Error('Invalid US number')
+            }
+            break
+        case 'br':
+            // 巴西,11位,12位去掉前导0
+            if (number.length === 12 && number.charAt(0) === '0') {
+                number = number.substring(1)
+            }
+            if (!/^\d{11}$/.test(number)) {
+                throw new Error('Invalid Brazilian number')
+            }
+            break
+        case 'vn':
+            // 越南,号码为9位
+            if (!/^\d{9}$/.test(number)) {
+                throw new Error('Invalid Vietnamese number')
+            }
+            break
+        case 'in':
+            // 印度,号码为10位
+            if (!/^\d{10}$/.test(number)) {
+                throw new Error('Invalid Indian number')
+            }
+            break
+        default:
+    }
+    return number
+}

+ 5 - 0
src/rcs-number/impl/get-number-service.ts

@@ -0,0 +1,5 @@
+import { RcsNumber } from '../entities/rcs-number.entity'
+
+export abstract class GetNumberService {
+    abstract getNumber(deviceId?: string): Promise<RcsNumber>
+}

+ 163 - 0
src/rcs-number/impl/mwze167.service.ts

@@ -0,0 +1,163 @@
+import axios, { AxiosError } from 'axios'
+import { GetNumberService } from './get-number-service'
+import { RcsNumber, RcsNumberSource, RcsNumberStatus } from '../entities/rcs-number.entity'
+import { HttpException, InternalServerErrorException, Logger, OnModuleInit, forwardRef } from '@nestjs/common'
+import { InjectRepository } from '@nestjs/typeorm'
+import { In, MoreThan, Not, Repository } from 'typeorm'
+import { Cron } from '@nestjs/schedule'
+import { Task } from '../../task/entities/task.entity'
+import { Channel } from '../../channel/entities/channel.entities'
+import { el } from 'date-fns/locale'
+import { addMinutes } from 'date-fns'
+import { channel } from 'diagnostics_channel'
+import { checkAndFormatNumber } from '../helpers'
+
+const uid = '1715242407'
+const sign = 'da1a847ef8f0df331884ce45d97e7e42'
+const pid = '133'
+
+const axiosInstance = axios.create({
+    baseURL: 'http://api.mwze167.com/registerApi/'
+})
+
+export class mwze167 extends GetNumberService {
+    constructor(
+        @InjectRepository(RcsNumber)
+        private rcsNumberRepository: Repository<RcsNumber>,
+        @InjectRepository(Task)
+        private taskRepository: Repository<Task>,
+        @InjectRepository(Channel)
+        private channelRepository: Repository<Channel>
+    ) {
+        super()
+    }
+
+    async getNumber(deviceId?: string, taskId?: string, channelId?: string): Promise<RcsNumber> {
+        let channel: Channel = null
+        let number: string = null
+        let orderId: string = null
+        let operatorName: string = null
+        try {
+            if (channelId) {
+                channel = await this.channelRepository.findOneBy({ id: parseInt(channelId) })
+            } else if (taskId) {
+                const task: Task = await this.taskRepository.findOneBy({ id: parseInt(taskId) })
+                if (task.channelId) {
+                    const channelArray = task.channelId.split(',').map(Number)
+                    const randomIndex = Math.floor(Math.random() * channelArray.length)
+                    channel = await this.channelRepository.findOneBy({ id: channelArray[randomIndex] })
+                }
+            }
+            if (!channel) {
+                // 从数据库中获取随机的channel
+                channel = await this.channelRepository.createQueryBuilder('channel').orderBy('RAND()').limit(1).getOne()
+            }
+            Logger.log(
+                `channelId=${channel.id}, country=${channel.country}, mcc=${channel.mcc}, mnc=${channel.mnc}`,
+                'mwze167'
+            )
+            const res = await axiosInstance.get('getMobile', {
+                params: {
+                    uid,
+                    sign,
+                    pid,
+                    cuy: channel.country.toUpperCase(),
+                    size: 1,
+                    ctype: 3
+                }
+            })
+            const data = res.data
+            Logger.log(data, 'mwze167')
+            number = data.data[0]?.number
+            orderId = data.orderId
+
+            const jsonString: string = data.data[0]?.mcc
+            const parsedObject = JSON.parse(jsonString)
+
+            operatorName = parsedObject?.operatorName
+        } catch (e: any) {
+            if (e.response) {
+                Logger.error(e.response.data, 'mwze167')
+                throw new InternalServerErrorException(e.response.data.message)
+            } else {
+                throw new InternalServerErrorException(e.message)
+            }
+        }
+
+        if (!number) {
+            throw new InternalServerErrorException('No number available')
+        }
+        number = checkAndFormatNumber(channel.country, number)
+        if (operatorName !== channel.operator) {
+            throw new InternalServerErrorException('Operator name not matches.')
+        }
+        const rcsNumber = new RcsNumber()
+        rcsNumber.mcc = channel.mcc
+        rcsNumber.mnc = channel.mnc
+        rcsNumber.country = channel.country
+        rcsNumber.number = number
+        rcsNumber.extra = { orderId }
+        rcsNumber.expiryTime = new Date(Date.now() + 5 * 60 * 1000)
+        rcsNumber.from = RcsNumberSource.mwze167
+        rcsNumber.status = RcsNumberStatus.PENDING
+        rcsNumber.deviceId = deviceId
+        rcsNumber.taskId = taskId
+        return await this.rcsNumberRepository.save(rcsNumber)
+    }
+
+    @Cron('0/5 * * * * *')
+    async getMessage() {
+        const numbers = await this.rcsNumberRepository.findBy({
+            expiryTime: MoreThan(addMinutes(new Date(), -1)),
+            status: Not(RcsNumberStatus.EXPIRED),
+            from: RcsNumberSource.mwze167
+        })
+        numbers.forEach(async (number) => {
+            if (number.expiryTime < new Date()) {
+                if (number.status === RcsNumberStatus.PENDING) {
+                    number.status = RcsNumberStatus.EXPIRED
+                    await this.rcsNumberRepository.save(number)
+                }
+                return
+            }
+
+            try {
+                const { data } = await axiosInstance.get('getMsg', {
+                    params: {
+                        uid,
+                        sign,
+                        orderId: number.extra.orderId
+                    }
+                })
+                if (data.data) {
+                    const msg = data.data.map((i) => i.txt).join('\n')
+                    number.status = RcsNumberStatus.SUCCESS
+                    number.message = msg
+                    await this.rcsNumberRepository.save(number)
+                }
+            } catch (e) {
+            }
+        })
+    }
+
+
+    async getNumberOld(deviceId?: string): Promise<RcsNumber> {
+        const rcsNumber = new RcsNumber()
+        rcsNumber.mcc = '631'
+        rcsNumber.mnc = '02'
+        rcsNumber.country = 'ao'
+        rcsNumber.number = '944119781'
+        let orderId = '1247929266406256640'
+        rcsNumber.extra = { orderId }
+        rcsNumber.expiryTime = new Date(Date.now() + 5 * 60 * 1000)
+        rcsNumber.from = RcsNumberSource.mwze167
+        rcsNumber.status = RcsNumberStatus.PENDING
+        rcsNumber.deviceId = deviceId
+        return await this.rcsNumberRepository.save(rcsNumber)
+    }
+
+    async sleep(ms: number): Promise<void> {
+        return new Promise(resolve => setTimeout(resolve, ms))
+    }
+
+}

+ 31 - 0
src/rcs-number/rcs-number.controller.ts

@@ -0,0 +1,31 @@
+import { Body, Controller, Get, Param, Post, Put } from '@nestjs/common'
+import { RcsNumberService } from './rcs-number.service'
+import { PageRequest } from '../common/dto/page-request'
+import { RcsNumber } from './entities/rcs-number.entity'
+import { Public } from '../auth/public.decorator'
+
+@Controller('rcs-number')
+@Public()
+export class RcsNumberController {
+    constructor(private readonly rcsNumberService: RcsNumberService) {}
+
+    @Post('/')
+    async findAll(@Body() page: PageRequest<RcsNumber>) {
+        return await this.rcsNumberService.findAll(page)
+    }
+
+    @Put('/')
+    async create(@Body() body: any) {
+        return await this.rcsNumberService.create(body?.deviceId, body?.taskId, body?.channelId)
+    }
+
+    @Get('/:id')
+    async get(@Param('id') id: string) {
+        return await this.rcsNumberService.get(Number(id))
+    }
+
+    @Post('/delete')
+    async delete(@Body() id: number) {
+        return await this.rcsNumberService.delete(id)
+    }
+}

+ 17 - 0
src/rcs-number/rcs-number.module.ts

@@ -0,0 +1,17 @@
+import { Module } from '@nestjs/common'
+import { RcsNumberService } from './rcs-number.service'
+import { RcsNumberController } from './rcs-number.controller'
+import { TypeOrmModule } from '@nestjs/typeorm'
+import { RcsNumber } from './entities/rcs-number.entity'
+import { mwze167 } from './impl/mwze167.service'
+import { Task } from '../task/entities/task.entity'
+import { Channel } from '../channel/entities/channel.entities'
+
+@Module({
+    imports: [TypeOrmModule.forFeature([RcsNumber, Task, Channel])],
+    providers: [RcsNumberService, mwze167],
+    controllers: [RcsNumberController],
+    exports: [RcsNumberService]
+})
+export class RcsNumberModule {
+}

+ 37 - 0
src/rcs-number/rcs-number.service.ts

@@ -0,0 +1,37 @@
+import { mwze167 } from './impl/mwze167.service'
+import { Injectable } from '@nestjs/common'
+import { InjectRepository } from '@nestjs/typeorm'
+import { RcsNumber } from './entities/rcs-number.entity'
+import { PageRequest } from '../common/dto/page-request'
+import { Pagination, paginate } from 'nestjs-typeorm-paginate'
+import { Repository } from 'typeorm'
+import { USACodeApiService } from './usacode-api-service'
+
+@Injectable()
+export class RcsNumberService {
+    constructor(
+        @InjectRepository(RcsNumber)
+        private rcsNumberRepository: Repository<RcsNumber>,
+        private mwze167: mwze167
+    ) {
+    }
+
+    async findAll(req: PageRequest<RcsNumber>): Promise<Pagination<RcsNumber>> {
+        return await paginate<RcsNumber>(this.rcsNumberRepository, req.page, req.search)
+    }
+
+    async create(deviceId?: string, taskId?: string, channelId?: string): Promise<RcsNumber> {
+        console.log('taskId', taskId)
+        console.log('channelId', channelId)
+        return await this.mwze167.getNumber(deviceId, taskId, channelId)
+    }
+
+    async delete(id: number): Promise<void> {
+        await this.rcsNumberRepository.delete(id)
+    }
+
+    async get(id: number): Promise<RcsNumber> {
+        return await this.rcsNumberRepository.findOneBy({ id })
+    }
+
+}

+ 81 - 0
src/rcs-number/usacode-api-service.ts

@@ -0,0 +1,81 @@
+import { Inject, Logger, OnModuleInit, forwardRef } from '@nestjs/common'
+import axios from 'axios'
+import * as WebSocket from 'ws'
+import { Subject, takeUntil } from 'rxjs'
+import { randomUUID } from 'crypto'
+
+const axiosInstance = axios.create({
+    baseURL: 'https://panel.hellomeetyou.com/',
+    headers: {
+        'X-API-KEY': 'Z0ik3OMblJznryPSWTCOmSOm'
+    }
+})
+
+export class USACodeApiService implements OnModuleInit {
+    private readonly messageSubject = new Subject<any>()
+
+    constructor(
+   
+    ) {}
+
+    onModuleInit() {
+        const ws = new WebSocket('wss://panel.hellomeetyou.com/api/ws')
+
+        ws.on('open', () => {
+            Logger.log('Connected WS Server', 'USACodeApiService')
+            ws.send(
+                JSON.stringify([
+                    {
+                        jsonrpc: '2.0',
+                        method: 'subscribe',
+                        params: {
+                            apiKey: 'Z0ik3OMblJznryPSWTCOmSOm'
+                        }
+                    },
+                    {
+                        jsonrpc: '2.0',
+                        method: 'getSubscribedUserIds',
+                        id: randomUUID()
+                    }
+                ])
+            )
+        })
+
+        ws.on('message', (data) => {
+            Logger.log('Received message ' + data, 'USACodeApiService')
+            const message = JSON.parse(data.toString())
+            if (message instanceof Array) {
+                message.forEach((msg) => {
+                    this.messageSubject.next(msg)
+                })
+            } else {
+                this.messageSubject.next(message)
+            }
+        })
+
+        // setInterval(() => {
+        //     ws.send(
+        //         JSON.stringify({
+        //             jsonrpc: '2.0',
+        //             method: 'ping'
+        //         })
+        //     )
+        // }, 50000)
+    }
+
+    public async requestNumber() {
+        const { data: service } = await axiosInstance.post('/api/checkService', {
+            services: ['SERVICE_NOT_LISTED']
+        })
+        if (service.available && service.creditsCost <= 1) {
+            const { data: response } = await axiosInstance.post(
+                'https://panel.hellomeetyou.com/api/line/changeService',
+                {
+                    services: ['SERVICE_NOT_LISTED']
+                }
+            )
+            return response.phoneNumber
+        }
+        throw new Error(service.available ? 'Price too high' : 'Service not available')
+    }
+}

+ 7 - 0
src/repl.ts

@@ -0,0 +1,7 @@
+import { repl } from '@nestjs/core'
+import { AppModule } from './app.module'
+
+async function bootstrap() {
+    await repl(AppModule)
+}
+bootstrap()

+ 26 - 0
src/shared/hashing/bcrypt.service.spec.ts

@@ -0,0 +1,26 @@
+import { Test, TestingModule } from '@nestjs/testing'
+import { BcryptService } from './bcrypt.service'
+
+describe('BcryptService', () => {
+    let service: BcryptService
+
+    beforeEach(async () => {
+        const module: TestingModule = await Test.createTestingModule({
+            providers: [
+                {
+                    provide: BcryptService,
+                    useValue: {
+                        hash: jest.fn(() => 'pass123'),
+                        compare: jest.fn(() => true)
+                    }
+                }
+            ]
+        }).compile()
+
+        service = module.get<BcryptService>(BcryptService)
+    })
+
+    it('should be defined', () => {
+        expect(service).toBeDefined()
+    })
+})

+ 15 - 0
src/shared/hashing/bcrypt.service.ts

@@ -0,0 +1,15 @@
+import { Injectable } from '@nestjs/common'
+import { compare, genSalt, hash } from 'bcrypt'
+import { HashingService } from './hashing.service'
+
+@Injectable()
+export class BcryptService implements HashingService {
+    async hash(data: string | Buffer): Promise<string> {
+        const salt = await genSalt()
+        return hash(data, salt)
+    }
+
+    compare(data: string | Buffer, encrypted: string): Promise<boolean> {
+        return compare(data, encrypted)
+    }
+}

+ 24 - 0
src/shared/hashing/hashing.service.spec.ts

@@ -0,0 +1,24 @@
+import { Test, TestingModule } from '@nestjs/testing'
+import { BcryptService } from './bcrypt.service'
+import { HashingService } from './hashing.service'
+
+describe('HashingService', () => {
+    let service: HashingService
+
+    beforeEach(async () => {
+        const module: TestingModule = await Test.createTestingModule({
+            providers: [
+                {
+                    provide: HashingService,
+                    useClass: BcryptService
+                }
+            ]
+        }).compile()
+
+        service = module.get<HashingService>(HashingService)
+    })
+
+    it('should be defined', () => {
+        expect(service).toBeDefined()
+    })
+})

+ 7 - 0
src/shared/hashing/hashing.service.ts

@@ -0,0 +1,7 @@
+import { Injectable } from '@nestjs/common'
+
+@Injectable()
+export abstract class HashingService {
+    abstract hash(data: string | Buffer): Promise<string>
+    abstract compare(data: string | Buffer, encrypted: string): Promise<boolean>
+}

+ 10 - 0
src/shared/mailer/mailer.module.ts

@@ -0,0 +1,10 @@
+import { Module } from '@nestjs/common'
+import { MailerService } from './mailer.service'
+import { ConfigModule } from '@nestjs/config'
+
+@Module({
+    imports: [ConfigModule.forRoot({ isGlobal: true })],
+    providers: [MailerService],
+    exports: [MailerService]
+})
+export class MailerModule {}

+ 19 - 0
src/shared/mailer/mailer.service.spec.ts

@@ -0,0 +1,19 @@
+import { ConfigService } from '@nestjs/config'
+import { Test, TestingModule } from '@nestjs/testing'
+import { MailerService } from './mailer.service'
+
+describe('MailerService', () => {
+    let service: MailerService
+
+    beforeEach(async () => {
+        const module: TestingModule = await Test.createTestingModule({
+            providers: [ConfigService, MailerService]
+        }).compile()
+
+        service = module.get<MailerService>(MailerService)
+    })
+
+    it('should be defined', () => {
+        expect(service).toBeDefined()
+    })
+})

+ 39 - 0
src/shared/mailer/mailer.service.ts

@@ -0,0 +1,39 @@
+import { Injectable } from '@nestjs/common'
+import { createTransport } from 'nodemailer'
+import * as Mail from 'nodemailer/lib/mailer'
+import { ConfigService } from '@nestjs/config'
+import * as hbs from 'nodemailer-express-handlebars'
+
+@Injectable()
+export class MailerService {
+    private nodemailerTransport: Mail
+
+    constructor(private readonly configService: ConfigService) {
+        this.nodemailerTransport = createTransport({
+            host: this.configService.get<string>('EMAIL_HOST'),
+            port: this.configService.get<number>('EMAIL_PORT'),
+            auth: {
+                user: this.configService.get<string>('EMAIL_AUTH_USER'),
+                pass: this.configService.get<string>('EMAIL_AUTH_PASSWORD')
+            },
+            debug: this.configService.get<boolean>('EMAIL_DEBUG'),
+            logger: false
+        })
+
+        const options = {
+            viewEngine: {
+                extname: '.hbs', // handlebars extension
+                layoutsDir: process.cwd() + `${this.configService.get<string>('EMAIL_LAYOUT_DIR')}`, // location of handlebars templates
+                defaultLayout: `${this.configService.get<string>('EMAIL_DEFAULT_LAYOUT')}`, // name of main template
+                partialsDir: process.cwd() + `${this.configService.get<string>('EMAIL_PARTIAL_DIR')}` // location of your subtemplates aka. header, footer etc
+            },
+            viewPath: process.cwd() + `${this.configService.get<string>('EMAIL_VIEW_PATH')}`,
+            extName: '.hbs'
+        }
+        this.nodemailerTransport.use('compile', hbs(options))
+    }
+
+    sendMail(options: any) {
+        return this.nodemailerTransport.sendMail(options)
+    }
+}

+ 8 - 0
src/shared/utils/utils.module.ts

@@ -0,0 +1,8 @@
+import { Module } from '@nestjs/common'
+import { UtilsService } from './utils.service'
+
+@Module({
+    providers: [UtilsService],
+    exports: [UtilsService]
+})
+export class UtilsModule {}

+ 18 - 0
src/shared/utils/utils.service.spec.ts

@@ -0,0 +1,18 @@
+import { Test, TestingModule } from '@nestjs/testing'
+import { UtilsService } from './utils.service'
+
+describe('UtilsService', () => {
+    let service: UtilsService
+
+    beforeEach(async () => {
+        const module: TestingModule = await Test.createTestingModule({
+            providers: [UtilsService]
+        }).compile()
+
+        service = module.get<UtilsService>(UtilsService)
+    })
+
+    it('should be defined', () => {
+        expect(service).toBeDefined()
+    })
+})

+ 9 - 0
src/shared/utils/utils.service.ts

@@ -0,0 +1,9 @@
+import { Injectable } from '@nestjs/common'
+import * as crypto from 'crypto'
+
+@Injectable()
+export class UtilsService {
+    public generatePassword(): string {
+        return crypto.randomUUID()
+    }
+}

+ 24 - 0
src/sms/dto/sms.dto.ts

@@ -0,0 +1,24 @@
+import { IsString, MaxLength } from 'class-validator'
+
+export class SendCodeDto {
+    @IsString()
+    @MaxLength(30)
+    readonly phone: string
+
+    @IsString()
+    readonly sessionId: string
+
+    @IsString()
+    readonly sig: string
+
+    @IsString()
+    readonly token: string
+}
+
+export class VerifyCodeDto {
+    @IsString()
+    readonly phone: string
+
+    @IsString()
+    readonly code: string
+}

+ 19 - 0
src/sms/entities/sms.entity.ts

@@ -0,0 +1,19 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'
+
+@Entity()
+export class SmsRecord {
+    @PrimaryGeneratedColumn()
+    id: number
+
+    @Column()
+    phone: string
+
+    @Column({ length: 10 })
+    code: string
+
+    @Column()
+    expireAt: Date
+
+    @CreateDateColumn()
+    createdAt: Date
+}

+ 25 - 0
src/sms/sms.controller.ts

@@ -0,0 +1,25 @@
+import { Body, Controller, Post } from '@nestjs/common'
+import { ApiTags } from '@nestjs/swagger'
+import { SendCodeDto, VerifyCodeDto } from './dto/sms.dto'
+import { SmsService } from './sms.service'
+import { Public } from '../auth/public.decorator'
+
+@ApiTags('sms')
+@Controller('/sms')
+export class SmsController {
+    constructor(private readonly smsService: SmsService) {}
+
+    @Public()
+    @Post('/sendVerify')
+    public async sendVerify(@Body() body: SendCodeDto) {
+        return await this.smsService.sendVerify(body)
+    }
+
+    @Public()
+    @Post('/verify')
+    public async verify(@Body() body: VerifyCodeDto) {
+        return {
+            result: await this.smsService.verify(body.phone, body.code)
+        }
+    }
+}

+ 14 - 0
src/sms/sms.module.ts

@@ -0,0 +1,14 @@
+import { Module } from '@nestjs/common'
+import { TypeOrmModule } from '@nestjs/typeorm'
+import { AliyunModule } from '../aliyun/aliyun.module'
+import { SmsController } from './sms.controller'
+import { SmsService } from './sms.service'
+import { SmsRecord } from './entities/sms.entity'
+
+@Module({
+    imports: [TypeOrmModule.forFeature([SmsRecord]), AliyunModule],
+    controllers: [SmsController],
+    providers: [SmsService],
+    exports: [SmsService]
+})
+export class SmsModule {}

+ 38 - 0
src/sms/sms.service.ts

@@ -0,0 +1,38 @@
+import { Injectable } from '@nestjs/common'
+import { InjectRepository } from '@nestjs/typeorm'
+import { AliyunService } from '../aliyun/aliyun.service'
+import { SmsRecord } from './entities/sms.entity'
+import { Repository } from 'typeorm'
+import { SendCodeDto } from './dto/sms.dto'
+
+@Injectable()
+export class SmsService {
+    constructor(
+        private readonly aliyunService: AliyunService,
+        @InjectRepository(SmsRecord)
+        private readonly smsRecordRepo: Repository<SmsRecord>
+    ) {}
+
+    public async sendVerify(data: SendCodeDto) {
+        let res = await this.aliyunService.sendCode(data)
+        const smsRecord = new SmsRecord()
+        smsRecord.phone = data.phone
+        smsRecord.code = res.code
+        smsRecord.expireAt = new Date(Date.now() + 5 * 60 * 1000)
+        await this.smsRecordRepo.save(smsRecord)
+    }
+    
+    public async verify(phone: string, code: string) {
+        const smsRecord = await this.smsRecordRepo.findOneBy({
+            phone: phone,
+            code: code
+        })
+        if (!smsRecord) {
+            return false
+        }
+        if (smsRecord.expireAt < new Date()) {
+            return false
+        }
+        return true
+    }
+}

+ 23 - 0
src/statistics/statistics.controller.ts

@@ -0,0 +1,23 @@
+import {
+    Controller,
+    Put,
+    Get,
+    Body,
+    Param,
+    HttpStatus,
+    NotFoundException,
+    Delete,
+    BadRequestException,
+    Req,
+    Post
+} from '@nestjs/common'
+import { StatisticsService } from './statistics.service'
+
+@Controller('statistics')
+export class StatisticsController {
+
+    constructor(private readonly statisticsService: StatisticsService) {}
+
+
+
+}

+ 29 - 0
src/statistics/statistics.module.ts

@@ -0,0 +1,29 @@
+import { forwardRef, Module } from '@nestjs/common'
+import { StatisticsController } from './statistics.controller'
+import { StatisticsService } from './statistics.service'
+import { TypeOrmModule } from '@nestjs/typeorm'
+import { Task } from '../task/entities/task.entity'
+import { TaskItem } from '../task/entities/task-item.entity'
+import { EventsModule } from '../events/events.module'
+import { PhoneListModule } from '../phone-list/phone-list.module'
+import { DeviceModule } from '../device/device.module'
+import { BalanceModule } from '../balance/balance.module'
+import { UsersModule } from '../users/users.module'
+import { Balance } from '../balance/entities/balance.entities'
+import { Users } from '../users/entities/users.entity'
+
+@Module({
+    imports: [
+        TypeOrmModule.forFeature([TaskItem, Balance, Users]),
+        forwardRef(() => EventsModule),
+        PhoneListModule,
+        DeviceModule,
+        BalanceModule,
+        UsersModule
+    ],
+    controllers: [StatisticsController],
+    providers: [StatisticsService],
+    exports: [StatisticsService]
+})
+export class StatisticsModule {
+}

+ 100 - 0
src/statistics/statistics.service.ts

@@ -0,0 +1,100 @@
+import { Injectable } from '@nestjs/common'
+import { InjectRepository } from '@nestjs/typeorm'
+import { TaskItem } from '../task/entities/task-item.entity'
+import { Repository } from 'typeorm'
+import { Task } from '../task/entities/task.entity'
+import { Balance } from '../balance/entities/balance.entities'
+import { Users } from '../users/entities/users.entity'
+
+@Injectable()
+export class StatisticsService {
+    constructor(
+        @InjectRepository(Task)
+        private taskRepository: Repository<Task>,
+        @InjectRepository(TaskItem)
+        private taskItemRepository: Repository<TaskItem>,
+        @InjectRepository(Balance)
+        private balanceRepository: Repository<Balance>,
+        @InjectRepository(Users)
+        private usersRepository: Repository<Users>
+    ) {
+    }
+
+    async getTotalStatistics(): Promise<{ totalTopUp: number, totalSend: number, totalSendSuccess: number }> {
+        // 获取所有总充值金额
+        const topUp = await this.balanceRepository.createQueryBuilder('balance')
+            .select('SUM(totalBalance)', 'topUp')
+            .getRawOne()
+
+        // 获取总发送条数
+        const send = await this.taskItemRepository.createQueryBuilder('taskItem')
+            .select('count(1)', 'send')
+            .getRawOne()
+        // 获取总发送成功条数 status = success
+        const sendSuccess = await this.taskItemRepository.createQueryBuilder('taskItem')
+            .select('count(1)', 'sendSuccess')
+            .where('status = :status', { status: 'success' })
+            .getRawOne()
+
+        return {
+            totalTopUp: topUp.totalTopUp,
+            totalSend: send.send,
+            totalSendSuccess: sendSuccess.sendSuccess
+        }
+    }
+
+    async getTotalStatisticsByMonth(): Promise<{ totalTopUp: number, totalSend: number, totalSendSuccess: number }> {
+        // 查询当月充值
+        const topUp = await this.taskItemRepository.createQueryBuilder('taskItem')
+            .select('SUM(amount)', 'totalTopUp')
+            .where('MONTH(createDate) = MONTH(current_date)')
+            .getRawOne()
+
+        // 获取当月总发送条数
+        const send = await this.taskItemRepository.createQueryBuilder('taskItem')
+            .select('count(1)', 'send')
+            .where('MONTH(createdAt) = MONTH(current_date)')
+            .getRawOne()
+        // 获取当月总发送成功条数 status = success
+        const sendSuccess = await this.taskItemRepository.createQueryBuilder('taskItem')
+            .select('count(1)', 'sendSuccess')
+            .where('status = :status', { status: 'success' })
+            .where('MONTH(createdAt) = MONTH(current_date)')
+            .getRawOne()
+
+        return {
+            totalTopUp: topUp.totalTopUp,
+            totalSend: send.send,
+            totalSendSuccess: sendSuccess.sendSuccess
+        }
+    }
+
+
+    async getTotalStatisticsByDay(): Promise<{ totalTopUp: number, totalSend: number, totalSendSuccess: number }> {
+        // 查询当日充值
+        const topUp = await this.taskItemRepository.createQueryBuilder('taskItem')
+            .select('SUM(amount)', 'totalTopUp')
+            .where('DATE(createDate) = DATE(current_date)')
+            .getRawOne()
+
+        // 获取当日总发送条数
+        const send = await this.taskItemRepository.createQueryBuilder('taskItem')
+            .select('count(1)', 'send')
+            .where('DATE(createdAt) = DATE(current_date)')
+            .getRawOne()
+        // 获取当日总发送成功条数 status = success
+        const sendSuccess = await this.taskItemRepository.createQueryBuilder('taskItem')
+            .select('count(1)', 'sendSuccess')
+            .where('status = :status', { status: 'success' })
+            .where('DATE(createdAt) = DATE(current_date)')
+            .getRawOne()
+
+        return {
+            totalTopUp: topUp.totalTopUp,
+            totalSend: send.send,
+            totalSendSuccess: sendSuccess.sendSuccess
+        }
+
+    }
+
+}

+ 31 - 0
src/sys-config/entities/sys-config.entity.ts

@@ -0,0 +1,31 @@
+import { Column, CreateDateColumn, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'
+
+export enum SysConfigType {
+    String = 'string',
+    Date = 'date',
+    Number = 'number',
+    Boolean = 'boolean',
+    Object = 'object'
+}
+
+@Entity()
+@Index('name', ['name'], { unique: true })
+export class SysConfig {
+    @PrimaryGeneratedColumn()
+    id: number
+
+    @Column({ unique: true, length: 50 })
+    name: string
+
+    @Column({ type: 'enum', enum: SysConfigType })
+    type: SysConfigType
+
+    @Column({ type: 'longtext' })
+    value: string
+
+    @Column({ length: 255, nullable: true })
+    remark: string
+
+    @CreateDateColumn()
+    createdAt: Date
+}

+ 18 - 0
src/sys-config/sys-config.admin.controller.ts

@@ -0,0 +1,18 @@
+import { Body, Controller, Get, Param, Post, Put } from '@nestjs/common'
+import { ApiTags } from '@nestjs/swagger'
+import { Public } from '../auth/public.decorator'
+import { SysConfigService } from './sys-config.service'
+import { PageRequest } from 'src/common/dto/page-request'
+import { SysConfig } from './entities/sys-config.entity'
+
+@ApiTags('sys-config.admin')
+@Controller('/admin/sys-config')
+@Public()
+export class SysConfigAdminController {
+    constructor(private readonly sysConfigService: SysConfigService) {}
+
+    @Put()
+    public async create(@Body() sysConfig: Partial<SysConfig>) {
+        return await this.sysConfigService.save(sysConfig)
+    }
+}

+ 27 - 0
src/sys-config/sys-config.controller.ts

@@ -0,0 +1,27 @@
+import { Body, Controller, Get, Param, Post } from '@nestjs/common'
+import { ApiTags } from '@nestjs/swagger'
+import { Public } from '../auth/public.decorator'
+import { SysConfigService } from './sys-config.service'
+import { SysConfig, SysConfigType } from './entities/sys-config.entity'
+import { PageRequest } from 'src/common/dto/page-request'
+
+@ApiTags('sys-config')
+@Controller('/sys-config')
+@Public()
+export class SysConfigController {
+    constructor(private readonly sysConfigService: SysConfigService) {}
+
+    @Post()
+    public async list(@Body() page: PageRequest<SysConfig>) {
+        return await this.sysConfigService.findAll(page)
+    }
+
+    @Get('/:name')
+    async getSysConfigByName(@Param('name') name: string) {
+        const res = await this.sysConfigService.findByName(name)
+        if (res.type === SysConfigType.Object) {
+            res.value = JSON.parse(res.value)
+        }
+        return res
+    }
+}

+ 14 - 0
src/sys-config/sys-config.module.ts

@@ -0,0 +1,14 @@
+import { Module } from '@nestjs/common'
+import { SysConfigController } from './sys-config.controller'
+import { SysConfigService } from './sys-config.service'
+import { TypeOrmModule } from '@nestjs/typeorm'
+import { SysConfig } from './entities/sys-config.entity'
+import { SysConfigAdminController } from './sys-config.admin.controller'
+
+@Module({
+    imports: [TypeOrmModule.forFeature([SysConfig])],
+    controllers: [SysConfigController, SysConfigAdminController],
+    providers: [SysConfigService],
+    exports: [SysConfigService]
+})
+export class SysConfigModule {}

+ 96 - 0
src/sys-config/sys-config.service.ts

@@ -0,0 +1,96 @@
+import { Injectable, InternalServerErrorException, NotFoundException, OnModuleInit } from '@nestjs/common'
+import { InjectRepository } from '@nestjs/typeorm'
+import { Repository } from 'typeorm'
+import { SysConfig, SysConfigType } from './entities/sys-config.entity'
+import { PageRequest } from '../common/dto/page-request'
+import { Pagination, paginate } from 'nestjs-typeorm-paginate'
+
+@Injectable()
+export class SysConfigService implements OnModuleInit {
+    constructor(
+        @InjectRepository(SysConfig)
+        private readonly sysConfigRepository: Repository<SysConfig>
+    ) {
+        ;(async function init() {})()
+    }
+    async onModuleInit() {
+        if (!(await this.sysConfigRepository.findOneBy({ name: 'rcs_wait' }))) {
+            await this.sysConfigRepository.save({
+                name: 'rcs_wait',
+                value: '3000',
+                type: SysConfigType.Number,
+                remark: 'RCS等待时间'
+            })
+        }
+        if (!(await this.sysConfigRepository.findOneBy({ name: 'rcs_interval' }))) {
+            await this.sysConfigRepository.save({
+                name: 'rcs_interval',
+                value: '3000',
+                type: SysConfigType.Number,
+                remark: 'RCS发送间隔'
+            })
+        }
+        if (!(await this.sysConfigRepository.findOneBy({ name: 'clean_count' }))) {
+            await this.sysConfigRepository.save({
+                name: 'clean_count',
+                value: '20',
+                type: SysConfigType.Number,
+                remark: '清理数量'
+            })
+        }
+        if (!(await this.sysConfigRepository.findOneBy({ name: 'request_number_interval' }))) {
+            await this.sysConfigRepository.save({
+                name: 'request_number_interval',
+                value: '100',
+                type: SysConfigType.Number,
+                remark: '请求新号码间隔'
+            })
+        }
+        if (!(await this.sysConfigRepository.findOneBy({ name: 'check_availability_numbers' }))) {
+            await this.sysConfigRepository.save({
+                name: 'check_availability_numbers',
+                value: '+18583199738',
+                type: SysConfigType.String,
+                remark: 'RCS检查号码'
+            })
+        }
+    }
+
+    async findAll(req: PageRequest<SysConfig>) {
+        try {
+            return await paginate<SysConfig>(this.sysConfigRepository, req.page, req.search)
+        } catch (error) {
+            //throw new InternalServerErrorException(error.message)
+            console.log(123)
+        }
+    }
+
+    async findByName(name: string): Promise<SysConfig> {
+        const conf = await this.sysConfigRepository.findOneBy({ name })
+        if (!conf) {
+            throw new NotFoundException('配置不存在')
+        }
+        return conf
+    }
+
+    async save(sysConfig: Partial<SysConfig>) {
+        try {
+            if (typeof sysConfig.value === 'object') {
+                sysConfig.value = JSON.stringify(sysConfig.value)
+            }
+            return await this.sysConfigRepository.save(sysConfig)
+        } catch (error) {
+            throw new InternalServerErrorException(error.message)
+        }
+    }
+
+    async getNumber(name: string, defaultValue: number): Promise<number> {
+        const conf = await this.sysConfigRepository.findOneBy({ name })
+        return parseInt(conf.value) || defaultValue
+    }
+
+    async getString(name: string, defaultValue: string): Promise<string> {
+        const conf = await this.sysConfigRepository.findOneBy({ name })
+        return conf.value || defaultValue
+    }
+}

+ 33 - 0
src/task/entities/task-item.entity.ts

@@ -0,0 +1,33 @@
+import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'
+
+export enum TaskItemStatus {
+    IDLE = 'idle',
+    PENDING = 'pending',
+    SUCCESS = 'success',
+    FAIL = 'fail'
+}
+
+@Entity()
+export class TaskItem {
+    @PrimaryGeneratedColumn()
+    id: number
+
+    @CreateDateColumn()
+    createdAt: Date
+
+    @Column()
+    taskId: number
+
+    @Column()
+    number: string
+
+    @Column({ type: 'text' })
+    message: string
+
+    @Column({ type: 'enum', enum: TaskItemStatus, nullable: false, default: TaskItemStatus.IDLE })
+    status: string
+
+    @CreateDateColumn()
+    sendAt: Date
+
+}

+ 61 - 0
src/task/entities/task.entity.ts

@@ -0,0 +1,61 @@
+import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'
+
+export enum TaskStatus {
+    IDLE = 'idle',
+    PENDING = 'pending',
+    PAUSE = 'pause',
+    COMPLETED = 'completed',
+    QUEUED = 'queued',
+    ERROR = 'error'
+}
+
+@Entity()
+export class Task {
+    @PrimaryGeneratedColumn()
+    id: number
+
+    @CreateDateColumn()
+    createdAt: Date
+
+    @Column()
+    userId: number
+
+    @Column()
+    name: string
+
+    @Column({ nullable: false })
+    listId: number
+
+    @Column({ type: 'text', nullable: false })
+    message: string
+
+    @Column({ type: 'enum', enum: TaskStatus, nullable: false, default: TaskStatus.IDLE })
+    status: TaskStatus
+
+    @Column()
+    error: string
+
+    @Column({ default: 0 })
+    rcsWait: number
+
+    @Column({ default: 0 })
+    rcsInterval: number
+
+    @Column({ default: 0 })
+    cleanCount: number
+
+    @Column()
+    successRate: string
+
+    @Column({ default: 0 })
+    sent: number
+
+    @Column({ default: 0 })
+    requestNumberInterval: number
+
+    @Column({ nullable: true })
+    channelId: string
+
+    @Column({ nullable: true, default: null })
+    timed: Date
+}

+ 60 - 0
src/task/task.controller.ts

@@ -0,0 +1,60 @@
+import { Body, Controller, Delete, Get, HttpException, HttpStatus, Param, Post, Put, Req, Res } from '@nestjs/common'
+import { PageRequest } from 'src/common/dto/page-request'
+import { Task, TaskStatus } from './entities/task.entity'
+import { TaskService } from './task.service'
+import { TaskItem, TaskItemStatus } from './entities/task-item.entity'
+import * as ExcelJS from 'exceljs'
+import { Response } from 'express'
+
+@Controller('task')
+export class TaskController {
+    constructor(private readonly taskService: TaskService) {}
+
+    @Post('/')
+    async findAllTask(@Req() req, @Body() page: PageRequest<Task>) {
+        if (!req.user.roles.includes('admin')) {
+            ;(page.search as any).where.userId = req.user.id
+        }
+        return await this.taskService.findAllTask(page)
+    }
+
+    @Put('/')
+    async createTask(@Req() req) {
+        req.body.userId = req.user.id
+        return await this.taskService.createTask(req.body)
+    }
+
+    @Get('/verification/:id')
+    async balanceVerification(@Param('id') id: string) {
+        return await this.taskService.balanceVerification(parseInt(id))
+    }
+
+    @Delete('/:id')
+    async delTask(@Param('id') id: string) {
+        return await this.taskService.delTask(parseInt(id))
+    }
+
+    @Post('/:id/start')
+    async startTask(@Param('id') id: string) {
+        return await this.taskService.startTask(parseInt(id))
+    }
+
+    @Post('/:id/pause')
+    async pauseTask(@Param('id') id: string) {
+        return await this.taskService.pauseTask(parseInt(id))
+    }
+
+    @Post('/item')
+    async findAllTaskItem(@Body() page: PageRequest<TaskItem>) {
+        return await this.taskService.findAllTaskItem(page)
+    }
+
+    @Post('/item/:taskId/receipt')
+    async exportTaskItem(@Param('taskId') taskId: number) {
+        try {
+            return await this.taskService.exportTaskItem(taskId)
+        } catch (error) {
+            console.error(error)
+        }
+    }
+}

+ 30 - 0
src/task/task.module.ts

@@ -0,0 +1,30 @@
+import { Module, forwardRef } from '@nestjs/common'
+import { TaskController } from './task.controller'
+import { TaskService } from './task.service'
+import { TypeOrmModule } from '@nestjs/typeorm'
+import { Task } from './entities/task.entity'
+import { TaskItem } from './entities/task-item.entity'
+import { EventsModule } from '../events/events.module'
+import { DeviceModule } from '../device/device.module'
+import { PhoneListModule } from '../phone-list/phone-list.module'
+import { SysConfigModule } from 'src/sys-config/sys-config.module'
+import { Users } from '../users/entities/users.entity'
+import { UsersModule } from '../users/users.module'
+import { Balance } from '../balance/entities/balance.entities'
+import { BalanceModule } from '../balance/balance.module'
+
+@Module({
+    imports: [
+        TypeOrmModule.forFeature([Task, TaskItem,Users,Balance]),
+        forwardRef(() => EventsModule),
+        PhoneListModule,
+        DeviceModule,
+        SysConfigModule,
+        UsersModule,
+        BalanceModule
+    ],
+    controllers: [TaskController],
+    providers: [TaskService],
+    exports: [TaskService]
+})
+export class TaskModule {}

+ 407 - 0
src/task/task.service.ts

@@ -0,0 +1,407 @@
+import { PhoneListService } from './../phone-list/phone-list.service'
+import { forwardRef, Inject, Injectable, Logger, OnModuleInit } from '@nestjs/common'
+import { InjectRepository } from '@nestjs/typeorm'
+import { Task, TaskStatus } from './entities/task.entity'
+import { In, Repository } from 'typeorm'
+import { TaskItem, TaskItemStatus } from './entities/task-item.entity'
+import { PageRequest } from '../common/dto/page-request'
+import { paginate, Pagination } from 'nestjs-typeorm-paginate'
+import { EventsGateway } from '../events/events.gateway'
+import { randomUUID } from 'crypto'
+import { setTimeout } from 'timers/promises'
+import { DeviceService } from '../device/device.service'
+import { SysConfigService } from '../sys-config/sys-config.service'
+import { Users } from '../users/entities/users.entity'
+import { Balance } from '../balance/entities/balance.entities'
+import * as ExcelJS from 'exceljs'
+import * as moment from 'moment'
+import { BalanceService } from '../balance/balance.service'
+import Decimal from 'decimal.js'
+import { Role } from '../model/role.enum'
+
+@Injectable()
+export class TaskService implements OnModuleInit {
+    constructor(
+        @InjectRepository(Task)
+        private taskRepository: Repository<Task>,
+        @InjectRepository(TaskItem)
+        private taskItemRepository: Repository<TaskItem>,
+        @InjectRepository(Users)
+        private userRepository: Repository<Users>,
+        @InjectRepository(Balance)
+        private balanceRepository: Repository<Balance>,
+        @Inject(forwardRef(() => EventsGateway))
+        private readonly eventsGateway: EventsGateway,
+        private readonly phoneListService: PhoneListService,
+        private readonly deviceService: DeviceService,
+        private readonly sysConfigService: SysConfigService,
+        private readonly balanceService: BalanceService
+    ) {
+    }
+
+    onModuleInit() {
+        this.taskRepository.update({ status: TaskStatus.PENDING }, { status: TaskStatus.IDLE })
+    }
+
+    private taskControllers: { [key: number]: AbortController } = {}
+
+    async findAllTask(req: PageRequest<Task>): Promise<Pagination<Task>> {
+        const result = await paginate<Task>(this.taskRepository, req.page, req.search)
+
+        const taskItems = result.items
+        if (taskItems.length > 0) {
+            for (const task of taskItems) {
+                if (task.status === TaskStatus.PENDING || (task.status === TaskStatus.COMPLETED && moment(task.createdAt).isSameOrAfter(moment().subtract(12, 'hours')))) {
+                    const id = task.id
+                    const successCount = await this.taskItemRepository.countBy({
+                        taskId: id,
+                        status: 'success'
+                    })
+                    const totalCount = await this.taskItemRepository.countBy({
+                        taskId: id
+                    })
+                    if (totalCount === 0) {
+                        throw new Error('No tasks found for the given taskId.')
+                    }
+                    // 计算成功率
+                    const successRate = ((successCount / totalCount) * 100).toFixed(1) + '%'
+                    task.successRate = String(successRate)
+                    const sendCount = await this.taskItemRepository.countBy({
+                        taskId: id,
+                        status: In(['success', 'fail'])
+                    })
+                    task.sent = sendCount
+                    await this.taskRepository.save(task)
+                }
+            }
+        }
+        return result
+    }
+
+    async findAllTaskItem(req: PageRequest<TaskItem>): Promise<Pagination<TaskItem>> {
+        return await paginate<TaskItem>(this.taskItemRepository, req.page, req.search)
+    }
+
+    async createTask(task: Task): Promise<Task> {
+        return await this.taskRepository.save(task)
+    }
+
+    async balanceVerification(id: number) {
+        const task = await this.taskRepository.findOneBy({ id })
+        // 获取用户信息
+        const user = await this.userRepository.findOneBy({
+            id: task.userId
+        })
+        if (user.roles.includes(Role.Admin)) {
+            return 0
+        }
+        const cost: number = await this.getCost(task, user)
+        // 验证余额
+        if (cost > (user.balance || 0)) {
+            return -1
+        } else {
+            return cost
+        }
+    }
+
+    async delTask(id: number): Promise<void> {
+        await this.taskRepository.delete(id)
+    }
+
+    async startTask(id: number): Promise<void> {
+        const task = await this.taskRepository.findOneBy({ id })
+
+        // 查询当前是否有任务执行
+        const num = await this.taskRepository.count(
+            {
+                where: {
+                    status: TaskStatus.PENDING
+                }
+            }
+        )
+        // 当前有任务
+        if (num > 0 && task.status === TaskStatus.IDLE) {
+            // 当前任务进入队列
+            task.status = TaskStatus.QUEUED
+            await this.taskRepository.save(task)
+            return
+        }
+
+        // 队列没有任务或队列中下一个待执行任务,启动此任务
+        if ((task && task.status === TaskStatus.IDLE) || task.status === TaskStatus.PAUSE || task.status === TaskStatus.QUEUED) {
+            if (task.status !== TaskStatus.PAUSE) {
+                const user = await this.userRepository.findOneBy({
+                    id: task.userId
+                })
+                if (!user.roles.includes(Role.Admin)) {
+                    // 开始任务前扣费
+                    const cost = await this.getCost(task, user)
+                    if (cost > (user.balance || 0)) {
+                        throw new Error('Insufficient balance!')
+                    }
+                    await this.balanceService.feeDeduction(task.userId, cost, user)
+                }
+            }
+
+            task.status = TaskStatus.PENDING
+            await this.taskRepository.save(task)
+            await this.runTask(task)
+
+            const newTask = await this.taskRepository.findOneBy({ id })
+            if ([TaskStatus.COMPLETED, TaskStatus.PAUSE].includes(newTask.status)) {
+                try {
+                    const successCount = await this.taskItemRepository.countBy({
+                        taskId: id,
+                        status: 'success'
+                    })
+                    const totalCount = await this.taskItemRepository.countBy({
+                        taskId: id
+                    })
+                    if (totalCount === 0) {
+                        throw new Error('No tasks found for the given taskId.')
+                    }
+                    // 计算成功率
+                    const successRate = ((successCount / totalCount) * 100).toFixed(1) + '%'
+                    newTask.successRate = String(successRate)
+                    const sendCount = await this.taskItemRepository.countBy({
+                        taskId: id,
+                        status: In(['success', 'fail'])
+                    })
+                    newTask.sent = sendCount
+                    await this.taskRepository.save(newTask)
+
+                } catch (e) {
+                    Logger.error('Error startTask ', e, 'RcsService')
+                }
+
+            }
+        }
+
+    }
+
+    async pauseTask(id: number): Promise<void> {
+        const task = await this.taskRepository.findOneBy({ id })
+        const successCount = await this.taskItemRepository.countBy({
+            taskId: id,
+            status: In(['success', 'fail'])
+        })
+        if (task && task.status === TaskStatus.PENDING) {
+            task.status = TaskStatus.PAUSE
+            task.sent = successCount
+            await this.taskRepository.save(task)
+            const controller = this.taskControllers[task.id]
+            if (controller) {
+                controller.abort()
+            }
+        }
+    }
+
+    async exportTaskItem(taskId: number) {
+        const workbook = new ExcelJS.Workbook()
+        const worksheet = workbook.addWorksheet('Sheet1')
+
+        const taskItems = await this.taskItemRepository.find({
+            where: {
+                taskId: taskId
+            }
+        })
+
+        // 设置列头
+        worksheet.columns = [
+            { header: '手机号', key: 'number', width: 30, style: { alignment: { horizontal: 'center' } } },
+            { header: '是否有效', key: 'isValid', width: 15, style: { alignment: { horizontal: 'center' } } },
+            { header: '发送成功', key: 'status', width: 15, style: { alignment: { horizontal: 'center' } } },
+            {
+                header: '发送时间',
+                key: 'sendAt',
+                width: 30,
+                style: { alignment: { horizontal: 'center' }, numFmt: 'YYYY-MM-DD HH:mm:ss' }
+            }
+        ]
+
+        taskItems.forEach(item => {
+            let valid = '有效'
+            let status = '发送成功'
+            const sendAt: Date = item.sendAt
+            const formattedSendAt = moment(sendAt).format('YYYY-MM-DD HH:mm:ss')
+            if (item.status === TaskItemStatus.FAIL) {
+                valid = '无效'
+                status = ''
+            }
+            worksheet.addRow({
+                number: item.number,
+                isValid: valid,
+                status: status,
+                sendAt: formattedSendAt
+            })
+        })
+
+        return await workbook.xlsx.writeBuffer()
+    }
+
+    async runTask(task: Task) {
+        let controller = new AbortController()
+        this.taskControllers[task.id] = controller
+        try {
+            let finish = false
+            while (!finish) {
+                if (controller.signal.aborted) {
+                    Logger.log('Task aborted', 'RcsService')
+                    return
+                }
+
+                task = await this.taskRepository.findOneBy({ id: task.id })
+                let rcsWait = 2000
+                let rcsInterval = 3000
+                let cleanCount = 20
+                let requestNumberInterval = 100
+                if (task.rcsWait > 0) {
+                    rcsWait = task.rcsWait
+                } else {
+                    try {
+                        rcsWait = await this.sysConfigService.getNumber('rcs_wait', 2000)
+                    } catch (error) {
+                        Logger.error('Error getting rcs wait time', error, 'RcsService')
+                    }
+                }
+                if (task.rcsInterval > 0) {
+                    rcsInterval = task.rcsInterval
+                } else {
+                    try {
+                        rcsInterval = await this.sysConfigService.getNumber('rcs_interval', 3000)
+                    } catch (error) {
+                        Logger.error('Error getting rcs interval time', error, 'RcsService')
+                    }
+                }
+                if (task.cleanCount > 0) {
+                    cleanCount = task.cleanCount
+                } else {
+                    try {
+                        cleanCount = await this.sysConfigService.getNumber('clean_count', 20)
+                    } catch (error) {
+                        Logger.error('Error getting clean count', error, 'RcsService')
+                    }
+                }
+                if (task.requestNumberInterval > 0) {
+                    requestNumberInterval = task.requestNumberInterval
+                } else {
+                    try {
+                        requestNumberInterval = await this.sysConfigService.getNumber('request_number_interval', 100)
+                    } catch (error) {
+                        Logger.error('Error getting request number interval', error, 'RcsService')
+                    }
+                }
+
+                let taskItems = await this.taskItemRepository.find({
+                    where: { taskId: task.id, status: TaskItemStatus.IDLE },
+                    take: 5
+                })
+
+                if (taskItems.length === 0) {
+                    finish = true
+                    task.status = TaskStatus.COMPLETED
+                    await this.taskRepository.save(task)
+                    // 从队列中获取下一个任务
+                    const tasks = await this.taskRepository.find(
+                        {
+                            where: {
+                                status: TaskStatus.QUEUED
+                            },
+                            order: {
+                                createdAt: 'ASC'
+                            }
+                        })
+                    // 异步执行startTask方法
+                    if (tasks.length > 0) {
+                        console.log('异步执行队列下一个任务')
+                        this.startTask(tasks[0].id)
+                    }
+                    console.log('当前任务结束')
+                    break
+                }
+
+                let device = null
+                while (device === null) {
+                    if (controller.signal.aborted) {
+                        Logger.log('Task aborted', 'RcsService')
+                        return
+                    }
+                    device = await this.deviceService.findAvailableDevice()
+                    if (device === null) {
+                        Logger.log('No device available, waiting...', 'RcsService')
+                        await setTimeout(2000)
+                    }
+                }
+                taskItems.forEach((taskItem) => {
+                    taskItem.status = TaskItemStatus.PENDING
+                })
+                await this.deviceService.setBusy(device.id, true)
+                await this.taskItemRepository.save(taskItems)
+                Logger.log(`Send task to device ${device.id}(${device.model})`, 'RcsService')
+
+                Promise.race([
+                    this.eventsGateway.sendForResult(
+                        {
+                            id: randomUUID(),
+                            action: 'task',
+                            data: {
+                                config: { rcsWait, rcsInterval, cleanCount, requestNumberInterval },
+                                tasks: taskItems,
+                                taskId: task.id
+                            }
+                        },
+                        device.socketId
+                    ),
+                    setTimeout(60000).then(() => {
+                        return Promise.resolve({ error: 'Timeout' })
+                    })
+                ]).then(async (res: any) => {
+                    if (res.error) {
+                        Logger.error('Task timeout', 'RcsService')
+                        await this.taskItemRepository.update(
+                            taskItems.map((i) => i.id),
+                            {
+                                status: TaskItemStatus.FAIL,
+                                sendAt: new Date()
+                            }
+                        )
+                    } else {
+                        Logger.log(
+                            `Task completed: ${res.success.length} success, ${res.fail.length} fail`,
+                            'RcsService'
+                        )
+                        if (res.success?.length > 0) {
+                            await this.taskItemRepository.update(res.success, {
+                                status: TaskItemStatus.SUCCESS,
+                                sendAt: new Date()
+                            })
+                        }
+                        if (res.fail?.length > 0) {
+                            await this.taskItemRepository.update(res.fail, {
+                                status: TaskItemStatus.FAIL,
+                                sendAt: new Date()
+                            })
+                        }
+                    }
+                })
+            }
+        } catch (e) {
+            Logger.error('Error running task', e, 'RcsService')
+            task.status = TaskStatus.ERROR
+            task.error = e.message
+            await this.taskRepository.save(task)
+        }
+
+        this.taskControllers[task.id] = null
+        return task
+    }
+
+    async getCost(task: Task, user: Users) {
+        const number: number = await this.phoneListService.findCountByListId(task.listId)
+        // 费用 = 费率*需要发送量
+        const rate = new Decimal(String(user.rate))
+        const num = new Decimal(String(number))
+        const cost = rate.mul(num)
+        return cost.toNumber()
+    }
+}

+ 13 - 0
src/transformers/array.transformer.ts

@@ -0,0 +1,13 @@
+import { ValueTransformer } from 'typeorm'
+
+export class ArrayTransformer implements ValueTransformer {
+    to(value: string[]): string {
+        return value?.join()
+    }
+    from(value: string): string[] {
+        if (value) {
+            return value.split(',')
+        }
+        return []
+    }
+}

+ 11 - 0
src/transformers/decimal.transformer.ts

@@ -0,0 +1,11 @@
+import { ValueTransformer } from 'typeorm'
+import BigNumber from "bignumber.js";
+
+export class DecimalTransformer implements ValueTransformer {
+    to(value: BigNumber): string {
+        return value?.toString()
+    }
+    from(value: string): BigNumber {
+        return new BigNumber(value)
+    }
+}

+ 10 - 0
src/transformers/json.transformer.ts

@@ -0,0 +1,10 @@
+import { ValueTransformer } from 'typeorm'
+
+export class JsonTransformer implements ValueTransformer {
+    to(value: any): string {
+        return value ? JSON.stringify(value) : null
+    }
+    from(value: string): string[] {
+        return value ? JSON.parse(value) : null
+    }
+}

+ 33 - 0
src/users/dto/user-create.dto.ts

@@ -0,0 +1,33 @@
+import { MaxLength, IsNotEmpty, IsEmail, IsString, isString, IsArray, IsOptional, Matches } from 'class-validator'
+import { Role } from '../../model/role.enum'
+
+export class UserCreateDto {
+    @IsString()
+    @MaxLength(30)
+    readonly name: string
+
+    @IsString()
+    @MaxLength(40)
+    readonly username: string
+
+    @IsString()
+    @IsOptional()
+    readonly avatar?: string
+
+    @IsEmail()
+    @IsString()
+    @IsOptional()
+    readonly email?: string
+
+    @IsString()
+    @Matches(/^1[3-9]\d{9}$/, { message: '手机号格式不正确' })
+    readonly phone: string
+
+    @IsNotEmpty()
+    @IsString()
+    @MaxLength(60)
+    password: string
+
+    @IsArray()
+    readonly roles: Role[]
+}

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است