├── stock-web ├── public │ ├── CNAME │ ├── favicon.ico │ ├── home_bg.png │ ├── icons │ │ ├── icon-128x128.png │ │ ├── icon-192x192.png │ │ └── icon-512x512.png │ └── pro_icon.svg ├── src │ ├── e2e │ │ ├── __mocks__ │ │ │ └── antd-pro-merge-less.js │ │ └── baseLayout.e2e.js │ ├── locales │ │ ├── zh-CN │ │ │ ├── component.ts │ │ │ ├── pwa.ts │ │ │ ├── globalHeader.ts │ │ │ ├── settingDrawer.ts │ │ │ ├── menu.ts │ │ │ ├── settings.ts │ │ │ └── pages.ts │ │ ├── zh-TW │ │ │ ├── component.ts │ │ │ ├── pwa.ts │ │ │ ├── globalHeader.ts │ │ │ ├── settingDrawer.ts │ │ │ ├── menu.ts │ │ │ └── settings.ts │ │ ├── ja-JP │ │ │ ├── component.ts │ │ │ ├── pwa.ts │ │ │ ├── globalHeader.ts │ │ │ ├── settingDrawer.ts │ │ │ ├── menu.ts │ │ │ ├── settings.ts │ │ │ └── pages.ts │ │ ├── en-US │ │ │ ├── component.ts │ │ │ ├── pwa.ts │ │ │ ├── globalHeader.ts │ │ │ ├── settingDrawer.ts │ │ │ ├── menu.ts │ │ │ └── settings.ts │ │ ├── id-ID │ │ │ ├── component.ts │ │ │ ├── pwa.ts │ │ │ ├── globalHeader.ts │ │ │ ├── settingDrawer.ts │ │ │ ├── menu.ts │ │ │ └── settings.ts │ │ ├── pt-BR │ │ │ ├── component.ts │ │ │ ├── pwa.ts │ │ │ ├── globalHeader.ts │ │ │ ├── settingDrawer.ts │ │ │ ├── menu.ts │ │ │ └── settings.ts │ │ ├── zh-TW.ts │ │ ├── pt-BR.ts │ │ ├── ja-JP.ts │ │ ├── zh-CN.ts │ │ ├── en-US.ts │ │ └── id-ID.ts │ ├── pages │ │ ├── Welcome.less │ │ ├── 404.tsx │ │ ├── TableList │ │ │ ├── data.d.ts │ │ │ └── service.ts │ │ ├── User │ │ │ └── login │ │ │ │ └── index.less │ │ ├── Admin.tsx │ │ └── Welcome.tsx │ ├── components │ │ ├── PageLoading │ │ │ └── index.tsx │ │ ├── Authorized │ │ │ ├── index.tsx │ │ │ ├── AuthorizedRoute.tsx │ │ │ ├── renderAuthorize.ts │ │ │ ├── Authorized.tsx │ │ │ ├── Secured.tsx │ │ │ ├── CheckPermissions.tsx │ │ │ └── PromiseRender.tsx │ │ ├── HeaderDropdown │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── NoticeIcon │ │ │ ├── index.less │ │ │ ├── NoticeList.less │ │ │ └── NoticeList.tsx │ │ ├── HeaderSearch │ │ │ ├── index.less │ │ │ └── index.tsx │ │ └── GlobalHeader │ │ │ ├── index.less │ │ │ ├── RightContent.tsx │ │ │ └── AvatarDropdown.tsx │ ├── utils │ │ ├── utils.less │ │ ├── Authorized.ts │ │ ├── utils.ts │ │ ├── authority.ts │ │ ├── request.ts │ │ └── utils.test.ts │ ├── layouts │ │ ├── BlankLayout.tsx │ │ ├── UserLayout.less │ │ ├── SecurityLayout.tsx │ │ └── UserLayout.tsx │ ├── manifest.json │ ├── services │ │ ├── user.ts │ │ ├── login.ts │ │ ├── stock.ts │ │ └── fetch.ts │ ├── models │ │ ├── connect.d.ts │ │ ├── setting.ts │ │ ├── user.ts │ │ ├── login.ts │ │ └── global.ts │ ├── global.less │ ├── typings.d.ts │ ├── service-worker.js │ ├── global.tsx │ └── assets │ │ └── logo.svg ├── .eslintignore ├── .prettierrc.js ├── .stylelintrc.js ├── mock │ ├── route.ts │ └── notices.ts ├── jsconfig.json ├── .eslintrc.js ├── jest.config.js ├── .editorconfig ├── .prettierignore ├── config │ ├── config.dev.ts │ ├── defaultSettings.ts │ ├── proxy.ts │ ├── config.ts │ └── routes.ts ├── .gitignore ├── tsconfig.json ├── README.md └── tests │ ├── PuppeteerEnvironment.js │ ├── getBrowser.js │ ├── beforeTest.js │ └── run-tests.js ├── stock-server ├── .prettierrc ├── nest-cli.json ├── src │ ├── config │ │ └── jwt-key.ts │ ├── libs │ │ ├── enums │ │ │ ├── role-enum.ts │ │ │ └── error-code-enum.ts │ │ ├── decorators │ │ │ ├── role.decorator.ts │ │ │ ├── profileInfo.ts │ │ │ └── user.decorator.ts │ │ ├── filters │ │ │ ├── busi.exception.ts │ │ │ └── http-exception.filter.ts │ │ ├── interceptors │ │ │ └── data.interceptor.ts │ │ └── pipes │ │ │ └── params-validation.pipe.ts │ ├── app.service.ts │ ├── modules │ │ ├── user │ │ │ ├── user.module.ts │ │ │ ├── dto │ │ │ │ └── user.dto.ts │ │ │ ├── user.service.ts │ │ │ └── user.controller.ts │ │ ├── auth │ │ │ ├── auth.service.spec.ts │ │ │ ├── guards │ │ │ │ └── role.guard.ts │ │ │ ├── strategies │ │ │ │ ├── jwt.strategy.ts │ │ │ │ └── local.strategy.ts │ │ │ ├── auth.module.ts │ │ │ └── auth.service.ts │ │ └── stock │ │ │ ├── stock.module.ts │ │ │ ├── dto │ │ │ └── stock.dto.ts │ │ │ ├── stock.controller.ts │ │ │ └── tasks │ │ │ └── sync-source.service.ts │ ├── main.ts │ ├── app.controller.spec.ts │ ├── utils │ │ └── common.ts │ ├── app.controller.ts │ ├── app.module.ts │ └── entities │ │ ├── StUser.ts │ │ ├── StockLog.ts │ │ ├── UserStock.ts │ │ └── Stock.ts ├── tsconfig.build.json ├── test │ ├── jest-e2e.json │ └── app.e2e-spec.ts ├── tsconfig.json ├── .gitignore ├── .eslintrc.js ├── package.json ├── README.md └── docs │ └── stock-demo.sql ├── README.md └── .gitignore /stock-web/public/CNAME: -------------------------------------------------------------------------------- 1 | preview.pro.ant.design -------------------------------------------------------------------------------- /stock-web/src/e2e/__mocks__/antd-pro-merge-less.js: -------------------------------------------------------------------------------- 1 | export default undefined; 2 | -------------------------------------------------------------------------------- /stock-server/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /stock-server/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /stock-server/src/config/jwt-key.ts: -------------------------------------------------------------------------------- 1 | export const jwtConstants = { 2 | secret: 'your jwt secret', 3 | }; 4 | -------------------------------------------------------------------------------- /stock-web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fengly0503/stock-pe/HEAD/stock-web/public/favicon.ico -------------------------------------------------------------------------------- /stock-web/public/home_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fengly0503/stock-pe/HEAD/stock-web/public/home_bg.png -------------------------------------------------------------------------------- /stock-server/src/libs/enums/role-enum.ts: -------------------------------------------------------------------------------- 1 | export enum UserRole { 2 | ADMIN = 'admin', 3 | USER = 'user', 4 | } 5 | -------------------------------------------------------------------------------- /stock-web/.eslintignore: -------------------------------------------------------------------------------- 1 | /lambda/ 2 | /scripts 3 | /config 4 | .history 5 | public 6 | dist 7 | .umi 8 | mock 9 | src -------------------------------------------------------------------------------- /stock-web/.prettierrc.js: -------------------------------------------------------------------------------- 1 | const fabric = require('@umijs/fabric'); 2 | 3 | module.exports = { 4 | ...fabric.prettier, 5 | }; 6 | -------------------------------------------------------------------------------- /stock-web/.stylelintrc.js: -------------------------------------------------------------------------------- 1 | const fabric = require('@umijs/fabric'); 2 | 3 | module.exports = { 4 | ...fabric.stylelint, 5 | }; 6 | -------------------------------------------------------------------------------- /stock-web/public/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fengly0503/stock-pe/HEAD/stock-web/public/icons/icon-128x128.png -------------------------------------------------------------------------------- /stock-web/public/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fengly0503/stock-pe/HEAD/stock-web/public/icons/icon-192x192.png -------------------------------------------------------------------------------- /stock-web/public/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fengly0503/stock-pe/HEAD/stock-web/public/icons/icon-512x512.png -------------------------------------------------------------------------------- /stock-server/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /stock-web/mock/route.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | '/api/auth_routes': { 3 | '/form/advanced-form': { authority: ['admin', 'user'] }, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /stock-server/src/libs/decorators/role.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common'; 2 | 3 | export const Roles = (...args: string[]) => SetMetadata('roles', args); 4 | -------------------------------------------------------------------------------- /stock-server/src/libs/decorators/profileInfo.ts: -------------------------------------------------------------------------------- 1 | export class ProfileInfo { 2 | mobile: string; 3 | uid: number; 4 | name: string; 5 | roles: string[]; 6 | avatar: string; 7 | } 8 | -------------------------------------------------------------------------------- /stock-web/src/locales/zh-CN/component.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.tagSelect.expand': '展开', 3 | 'component.tagSelect.collapse': '收起', 4 | 'component.tagSelect.all': '全部', 5 | }; 6 | -------------------------------------------------------------------------------- /stock-web/src/locales/zh-TW/component.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.tagSelect.expand': '展開', 3 | 'component.tagSelect.collapse': '收起', 4 | 'component.tagSelect.all': '全部', 5 | }; 6 | -------------------------------------------------------------------------------- /stock-web/src/locales/ja-JP/component.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.tagSelect.expand': '展開', 3 | 'component.tagSelect.collapse': '折りたたむ', 4 | 'component.tagSelect.all': 'すべて', 5 | }; 6 | -------------------------------------------------------------------------------- /stock-server/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /stock-web/src/locales/en-US/component.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.tagSelect.expand': 'Expand', 3 | 'component.tagSelect.collapse': 'Collapse', 4 | 'component.tagSelect.all': 'All', 5 | }; 6 | -------------------------------------------------------------------------------- /stock-web/src/locales/id-ID/component.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.tagSelect.expand': 'Perluas', 3 | 'component.tagSelect.collapse': 'Lipat', 4 | 'component.tagSelect.all': 'Semua', 5 | }; 6 | -------------------------------------------------------------------------------- /stock-web/src/locales/pt-BR/component.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.tagSelect.expand': 'Expandir', 3 | 'component.tagSelect.collapse': 'Diminuir', 4 | 'component.tagSelect.all': 'Todas', 5 | }; 6 | -------------------------------------------------------------------------------- /stock-web/src/pages/Welcome.less: -------------------------------------------------------------------------------- 1 | @import '~antd/lib/style/themes/default.less'; 2 | 3 | .pre { 4 | margin: 12px 0; 5 | padding: 12px 20px; 6 | background: @input-bg; 7 | box-shadow: @card-shadow; 8 | } 9 | -------------------------------------------------------------------------------- /stock-web/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "emitDecoratorMetadata": true, 4 | "experimentalDecorators": true, 5 | "baseUrl": ".", 6 | "paths": { 7 | "@/*": ["./src/*"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /stock-web/src/components/PageLoading/index.tsx: -------------------------------------------------------------------------------- 1 | import { PageLoading } from '@ant-design/pro-layout'; 2 | 3 | // loading components from code split 4 | // https://umijs.org/plugin/umi-plugin-react.html#dynamicimport 5 | export default PageLoading; 6 | -------------------------------------------------------------------------------- /stock-web/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [require.resolve('@umijs/fabric/dist/eslint')], 3 | globals: { 4 | ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: true, 5 | page: true, 6 | REACT_APP_ENV: true, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /stock-server/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /stock-web/src/locales/zh-CN/pwa.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.pwa.offline': '当前处于离线状态', 3 | 'app.pwa.serviceworker.updated': '有新内容', 4 | 'app.pwa.serviceworker.updated.hint': '请点击“刷新”按钮或者手动刷新页面', 5 | 'app.pwa.serviceworker.updated.ok': '刷新', 6 | }; 7 | -------------------------------------------------------------------------------- /stock-web/src/locales/zh-TW/pwa.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.pwa.offline': '當前處於離線狀態', 3 | 'app.pwa.serviceworker.updated': '有新內容', 4 | 'app.pwa.serviceworker.updated.hint': '請點擊“刷新”按鈕或者手動刷新頁面', 5 | 'app.pwa.serviceworker.updated.ok': '刷新', 6 | }; 7 | -------------------------------------------------------------------------------- /stock-server/src/libs/decorators/user.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator } from '@nestjs/common'; 2 | 3 | export const User = createParamDecorator((data, req) => { 4 | const request = req.switchToHttp().getRequest(); 5 | return request.user; 6 | }); 7 | -------------------------------------------------------------------------------- /stock-web/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testURL: 'http://localhost:8000', 3 | testEnvironment: './tests/PuppeteerEnvironment', 4 | verbose: false, 5 | globals: { 6 | ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: false, 7 | localStorage: null, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /stock-web/src/locales/ja-JP/pwa.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.pwa.offline': 'あなたは今オフラインです', 3 | 'app.pwa.serviceworker.updated': '新しいコンテンツが利用可能です', 4 | 'app.pwa.serviceworker.updated.hint': 5 | '現在のページをリロードするには、「更新」ボタンを押してください', 6 | 'app.pwa.serviceworker.updated.ok': 'リフレッシュ', 7 | }; 8 | -------------------------------------------------------------------------------- /stock-server/src/libs/enums/error-code-enum.ts: -------------------------------------------------------------------------------- 1 | export enum BusiErrorCode { 2 | TIMEOUT = -1, // 系统繁忙 3 | SUCCESS = 0, // 成功 4 | PARAM_ERROR = 10000, // 请求参数错误 5 | NOT_FOUND = 10001, // 查找的资源不存在 6 | UN_AUTHORIZED = 20000, // 用户未登录 7 | AUTH_FORBIDDEN = 30000, // 用户没有权限 8 | PWD_ERROR = 40000, // 账号或者密码错误 9 | } 10 | -------------------------------------------------------------------------------- /stock-web/src/locales/en-US/pwa.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.pwa.offline': 'You are offline now', 3 | 'app.pwa.serviceworker.updated': 'New content is available', 4 | 'app.pwa.serviceworker.updated.hint': 'Please press the "Refresh" button to reload current page', 5 | 'app.pwa.serviceworker.updated.ok': 'Refresh', 6 | }; 7 | -------------------------------------------------------------------------------- /stock-web/src/utils/utils.less: -------------------------------------------------------------------------------- 1 | // mixins for clearfix 2 | // ------------------------ 3 | .clearfix() { 4 | zoom: 1; 5 | &::before, 6 | &::after { 7 | display: table; 8 | content: ' '; 9 | } 10 | &::after { 11 | clear: both; 12 | height: 0; 13 | font-size: 0; 14 | visibility: hidden; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /stock-web/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /stock-web/src/locales/id-ID/pwa.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.pwa.offline': 'Koneksi anda terputus', 3 | 'app.pwa.serviceworker.updated': 'Konten baru sudah tersedia', 4 | 'app.pwa.serviceworker.updated.hint': 5 | 'Silahkan klik tombol "Refresh" untuk memuat ulang halaman ini', 6 | 'app.pwa.serviceworker.updated.ok': 'Memuat ulang', 7 | }; 8 | -------------------------------------------------------------------------------- /stock-web/.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.svg 2 | package.json 3 | .umi 4 | .umi-production 5 | /dist 6 | .dockerignore 7 | .DS_Store 8 | .eslintignore 9 | *.png 10 | *.toml 11 | docker 12 | .editorconfig 13 | Dockerfile* 14 | .gitignore 15 | .prettierignore 16 | LICENSE 17 | .eslintcache 18 | *.lock 19 | yarn-error.log 20 | .history 21 | CNAME 22 | /build 23 | /public -------------------------------------------------------------------------------- /stock-web/src/locales/pt-BR/pwa.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.pwa.offline': 'Você está offline agora', 3 | 'app.pwa.serviceworker.updated': 'Novo conteúdo está disponível', 4 | 'app.pwa.serviceworker.updated.hint': 5 | 'Por favor, pressione o botão "Atualizar" para recarregar a página atual', 6 | 'app.pwa.serviceworker.updated.ok': 'Atualizar', 7 | }; 8 | -------------------------------------------------------------------------------- /stock-web/src/layouts/BlankLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Inspector } from 'react-dev-inspector'; 3 | 4 | const InspectorWrapper = process.env.NODE_ENV === 'development' ? Inspector : React.Fragment; 5 | 6 | const Layout: React.FC = ({ children }) => { 7 | return {children}; 8 | }; 9 | 10 | export default Layout; 11 | -------------------------------------------------------------------------------- /stock-web/src/components/Authorized/index.tsx: -------------------------------------------------------------------------------- 1 | import Authorized from './Authorized'; 2 | import Secured from './Secured'; 3 | import check from './CheckPermissions'; 4 | import renderAuthorize from './renderAuthorize'; 5 | 6 | Authorized.Secured = Secured; 7 | Authorized.check = check; 8 | 9 | const RenderAuthorize = renderAuthorize(Authorized); 10 | 11 | export default RenderAuthorize; 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## stock-pe 2 | #### 基于NestJs + TypeOrm + Ant Design Pro 搭建股票估值查询系统 3 | 4 | 主要功能点: 5 | * 自定义返回数据格式 6 | * 自定义异常处理,业务码与http码分离 7 | * TypeOrm基本用法和Entity自动生成 8 | * MySql脚本基础用法 9 | * 请求参数服务端验证 10 | * jwt权限验证 11 | * 用户身份信息注入 12 | * 定时任务同步数据 13 | * Linux下Nginx部署 14 | * Jenkins持续集成/部署 15 | 16 | 17 | [过程文档](https://juejin.cn/post/6944181120304939045) 18 | [在线演示](https://web.loveeveryday.cn/stock-web/welcome) 19 | -------------------------------------------------------------------------------- /stock-web/src/components/HeaderDropdown/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .container > * { 4 | background-color: @popover-bg; 5 | border-radius: 4px; 6 | box-shadow: @shadow-1-down; 7 | } 8 | 9 | @media screen and (max-width: @screen-xs) { 10 | .container { 11 | width: 100% !important; 12 | } 13 | .container > * { 14 | border-radius: 0 !important; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /stock-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /stock-web/config/config.dev.ts: -------------------------------------------------------------------------------- 1 | // https://umijs.org/config/ 2 | import { defineConfig } from "umi"; 3 | 4 | export default defineConfig({ 5 | plugins: [ 6 | // https://github.com/zthxxx/react-dev-inspector 7 | "react-dev-inspector/plugins/umi/react-inspector", 8 | ], 9 | // https://github.com/zthxxx/react-dev-inspector#inspector-loader-props 10 | inspectorConfig: { 11 | exclude: [], 12 | babelPlugins: [], 13 | babelOptions: {}, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /stock-web/src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Result } from 'antd'; 2 | import React from 'react'; 3 | import { history } from 'umi'; 4 | 5 | const NoFoundPage: React.FC = () => ( 6 | history.push('/')}> 12 | Back Home 13 | 14 | } 15 | /> 16 | ); 17 | 18 | export default NoFoundPage; 19 | -------------------------------------------------------------------------------- /stock-server/src/modules/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, HttpModule } from '@nestjs/common'; 2 | import { UserController } from './user.controller'; 3 | import { UserService } from './user.service'; 4 | import { StUser } from '../../entities/StUser'; 5 | import { TypeOrmModule } from '@nestjs/typeorm'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([StUser]), HttpModule], 9 | controllers: [UserController], 10 | providers: [UserService], 11 | exports: [UserService], 12 | }) 13 | export class UserModule {} 14 | -------------------------------------------------------------------------------- /stock-server/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json -------------------------------------------------------------------------------- /stock-web/src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ant Design Pro", 3 | "short_name": "Ant Design Pro", 4 | "display": "standalone", 5 | "start_url": "./?utm_source=homescreen", 6 | "theme_color": "#002140", 7 | "background_color": "#001529", 8 | "icons": [ 9 | { 10 | "src": "icons/icon-192x192.png", 11 | "sizes": "192x192" 12 | }, 13 | { 14 | "src": "icons/icon-128x128.png", 15 | "sizes": "128x128" 16 | }, 17 | { 18 | "src": "icons/icon-512x512.png", 19 | "sizes": "512x512" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /stock-server/src/modules/auth/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AuthService } from './auth.service'; 3 | 4 | describe('AuthService', () => { 5 | let service: AuthService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [AuthService], 10 | }).compile(); 11 | 12 | service = module.get(AuthService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /stock-web/src/services/user.ts: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | import HttpClient from "./fetch"; 3 | 4 | export async function query(): Promise { 5 | return request("/api/users"); 6 | } 7 | 8 | export async function queryCurrent(): Promise { 9 | // console.log('token是 1:', (window as any).userToken); 10 | // return request(`${API_SERVER}/server/profile`); 11 | return HttpClient.request({ 12 | url: `/server/profile`, 13 | }); 14 | } 15 | 16 | export async function queryNotices(): Promise { 17 | return request("/api/notices"); 18 | } 19 | -------------------------------------------------------------------------------- /stock-web/config/defaultSettings.ts: -------------------------------------------------------------------------------- 1 | import { Settings as ProSettings } from '@ant-design/pro-layout'; 2 | 3 | type DefaultSettings = Partial & { 4 | pwa: boolean; 5 | }; 6 | 7 | const proSettings: DefaultSettings = { 8 | navTheme: 'dark', 9 | // 拂晓蓝 10 | primaryColor: '#1890ff', 11 | layout: 'side', 12 | contentWidth: 'Fluid', 13 | fixedHeader: false, 14 | fixSiderbar: true, 15 | colorWeak: false, 16 | title: '股票查询', 17 | pwa: false, 18 | iconfontUrl: '', 19 | }; 20 | 21 | export type { DefaultSettings }; 22 | 23 | export default proSettings; 24 | -------------------------------------------------------------------------------- /stock-web/src/utils/Authorized.ts: -------------------------------------------------------------------------------- 1 | import RenderAuthorize from '@/components/Authorized'; 2 | import { getAuthority } from './authority'; 3 | /* eslint-disable eslint-comments/disable-enable-pair */ 4 | /* eslint-disable import/no-mutable-exports */ 5 | let Authorized = RenderAuthorize(getAuthority()); 6 | 7 | // Reload the rights component 8 | const reloadAuthorized = (): void => { 9 | Authorized = RenderAuthorize(getAuthority()); 10 | }; 11 | 12 | /** Hard code block need it。 */ 13 | window.reloadAuthorized = reloadAuthorized; 14 | 15 | export { reloadAuthorized }; 16 | export default Authorized; 17 | -------------------------------------------------------------------------------- /stock-server/src/modules/stock/stock.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, HttpModule } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { StUser } from '../../entities/StUser'; 4 | import { Stock } from '../../entities/Stock'; 5 | import { TasksService } from './tasks/sync-source.service'; 6 | import { StockService } from './stock.service'; 7 | import { StockController } from './stock.controller'; 8 | 9 | @Module({ 10 | imports: [TypeOrmModule.forFeature([StUser, Stock]), HttpModule], 11 | providers: [TasksService, StockService], 12 | controllers: [StockController], 13 | }) 14 | export class StockModule {} 15 | -------------------------------------------------------------------------------- /stock-web/src/locales/zh-TW.ts: -------------------------------------------------------------------------------- 1 | import component from './zh-TW/component'; 2 | import globalHeader from './zh-TW/globalHeader'; 3 | import menu from './zh-TW/menu'; 4 | import pwa from './zh-TW/pwa'; 5 | import settingDrawer from './zh-TW/settingDrawer'; 6 | import settings from './zh-TW/settings'; 7 | 8 | export default { 9 | 'navBar.lang': '語言', 10 | 'layout.user.link.help': '幫助', 11 | 'layout.user.link.privacy': '隱私', 12 | 'layout.user.link.terms': '條款', 13 | 'app.preview.down.block': '下載此頁面到本地項目', 14 | ...globalHeader, 15 | ...menu, 16 | ...settingDrawer, 17 | ...settings, 18 | ...pwa, 19 | ...component, 20 | }; 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | **/node_modules 5 | # roadhog-api-doc ignore 6 | /src/utils/request-temp.js 7 | _roadhog-api-doc 8 | 9 | # production 10 | /dist 11 | /.vscode 12 | 13 | # misc 14 | .DS_Store 15 | npm-debug.log* 16 | yarn-error.log 17 | 18 | /coverage 19 | .idea 20 | yarn.lock 21 | package-lock.json 22 | *bak 23 | .vscode 24 | 25 | # visual studio code 26 | .history 27 | *.log 28 | functions/* 29 | .temp/** 30 | 31 | # umi 32 | .umi 33 | .umi-production 34 | 35 | # screenshot 36 | screenshot 37 | .firebase 38 | .eslintcache 39 | 40 | build 41 | -------------------------------------------------------------------------------- /stock-web/src/components/NoticeIcon/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .popover { 4 | position: relative; 5 | width: 336px; 6 | } 7 | 8 | .noticeButton { 9 | display: inline-block; 10 | cursor: pointer; 11 | transition: all 0.3s; 12 | } 13 | .icon { 14 | padding: 4px; 15 | vertical-align: middle; 16 | } 17 | 18 | .badge { 19 | font-size: 16px; 20 | } 21 | 22 | .tabs { 23 | :global { 24 | .ant-tabs-nav-list { 25 | margin: auto; 26 | } 27 | 28 | .ant-tabs-nav-scroll { 29 | text-align: center; 30 | } 31 | .ant-tabs-bar { 32 | margin-bottom: 0; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /stock-web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | **/node_modules 5 | # roadhog-api-doc ignore 6 | /src/utils/request-temp.js 7 | _roadhog-api-doc 8 | 9 | # production 10 | /dist 11 | /.vscode 12 | 13 | # misc 14 | .DS_Store 15 | npm-debug.log* 16 | yarn-error.log 17 | 18 | /coverage 19 | .idea 20 | yarn.lock 21 | package-lock.json 22 | *bak 23 | .vscode 24 | 25 | # visual studio code 26 | .history 27 | *.log 28 | functions/* 29 | .temp/** 30 | 31 | # umi 32 | .umi 33 | .umi-production 34 | 35 | # screenshot 36 | screenshot 37 | .firebase 38 | .eslintcache 39 | 40 | build 41 | -------------------------------------------------------------------------------- /stock-server/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { TransformInterceptor } from './libs/interceptors/data.interceptor'; 3 | import { HttpExceptionFilter } from './libs/filters/http-exception.filter'; 4 | import { ParamsValidationPipe } from './libs/pipes/params-validation.pipe'; 5 | import { AppModule } from './app.module'; 6 | 7 | async function bootstrap() { 8 | const app = await NestFactory.create(AppModule); 9 | app.useGlobalFilters(new HttpExceptionFilter()); 10 | app.useGlobalInterceptors(new TransformInterceptor()); 11 | app.useGlobalPipes(new ParamsValidationPipe()); 12 | await app.listen(3000); 13 | } 14 | bootstrap(); 15 | -------------------------------------------------------------------------------- /stock-web/public/pro_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /stock-web/src/locales/pt-BR.ts: -------------------------------------------------------------------------------- 1 | import component from './pt-BR/component'; 2 | import globalHeader from './pt-BR/globalHeader'; 3 | import menu from './pt-BR/menu'; 4 | import pwa from './pt-BR/pwa'; 5 | import settingDrawer from './pt-BR/settingDrawer'; 6 | import settings from './pt-BR/settings'; 7 | 8 | export default { 9 | 'navBar.lang': 'Idiomas', 10 | 'layout.user.link.help': 'ajuda', 11 | 'layout.user.link.privacy': 'política de privacidade', 12 | 'layout.user.link.terms': 'termos de serviços', 13 | 'app.preview.down.block': 'Download this page to your local project', 14 | ...globalHeader, 15 | ...menu, 16 | ...settingDrawer, 17 | ...settings, 18 | ...pwa, 19 | ...component, 20 | }; 21 | -------------------------------------------------------------------------------- /stock-server/src/libs/filters/busi.exception.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, HttpStatus } from '@nestjs/common'; 2 | import { BusiErrorCode } from '../enums/error-code-enum'; 3 | 4 | export class BusiException extends HttpException { 5 | private _code: BusiErrorCode; 6 | private _message: string; 7 | constructor( 8 | code: BusiErrorCode | number, 9 | message: string, 10 | statusCode: HttpStatus = HttpStatus.BAD_REQUEST, 11 | ) { 12 | super(message, statusCode); 13 | this._code = code; 14 | this._message = message; 15 | } 16 | 17 | getErrorCode(): BusiErrorCode { 18 | return this._code; 19 | } 20 | 21 | getErrorMessage(): string { 22 | return this._message; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /stock-server/src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let appController: AppController; 7 | 8 | beforeEach(async () => { 9 | const app: TestingModule = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | 14 | appController = app.get(AppController); 15 | }); 16 | 17 | describe('root', () => { 18 | it('should return "Hello World!"', () => { 19 | expect(appController.getHello()).toBe('Hello World!'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /stock-server/src/modules/auth/guards/role.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; 2 | import { Reflector } from '@nestjs/core'; 3 | 4 | @Injectable() 5 | export class RolesGuard implements CanActivate { 6 | constructor(private readonly reflector: Reflector) {} 7 | 8 | canActivate(context: ExecutionContext): boolean { 9 | const roles = this.reflector.get('roles', context.getHandler()); 10 | if (!roles) { 11 | return true; 12 | } 13 | const request = context.switchToHttp().getRequest(); 14 | const user = request.user; 15 | const hasRole = () => user.roles.some((role) => roles.includes(role)); 16 | return user && user.roles && hasRole(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /stock-web/src/components/HeaderSearch/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .headerSearch { 4 | .input { 5 | width: 0; 6 | min-width: 0; 7 | overflow: hidden; 8 | background: transparent; 9 | border-radius: 0; 10 | transition: width 0.3s, margin-left 0.3s; 11 | :global(.ant-select-selection) { 12 | background: transparent; 13 | } 14 | input { 15 | padding-right: 0; 16 | padding-left: 0; 17 | border: 0; 18 | box-shadow: none !important; 19 | } 20 | &, 21 | &:hover, 22 | &:focus { 23 | border-bottom: 1px solid @border-color-base; 24 | } 25 | &.show { 26 | width: 210px; 27 | margin-left: 8px; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /stock-server/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /stock-server/src/libs/interceptors/data.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | NestInterceptor, 4 | CallHandler, 5 | ExecutionContext, 6 | } from '@nestjs/common'; 7 | import { map } from 'rxjs/operators'; 8 | import { Observable } from 'rxjs'; 9 | interface Response { 10 | data: T; 11 | } 12 | @Injectable() 13 | export class TransformInterceptor 14 | implements NestInterceptor> { 15 | intercept( 16 | context: ExecutionContext, 17 | next: CallHandler, 18 | ): Observable> { 19 | return next.handle().pipe( 20 | map((data) => { 21 | return { 22 | data: data ?? null, 23 | code: 0, 24 | message: 'success', 25 | }; 26 | }), 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /stock-web/src/components/HeaderDropdown/index.tsx: -------------------------------------------------------------------------------- 1 | import type { DropDownProps } from 'antd/es/dropdown'; 2 | import { Dropdown } from 'antd'; 3 | import React from 'react'; 4 | import classNames from 'classnames'; 5 | import styles from './index.less'; 6 | 7 | export type HeaderDropdownProps = { 8 | overlayClassName?: string; 9 | overlay: React.ReactNode | (() => React.ReactNode) | any; 10 | placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomCenter'; 11 | } & Omit; 12 | 13 | const HeaderDropdown: React.FC = ({ overlayClassName: cls, ...restProps }) => ( 14 | 15 | ); 16 | 17 | export default HeaderDropdown; 18 | -------------------------------------------------------------------------------- /stock-server/src/utils/common.ts: -------------------------------------------------------------------------------- 1 | import * as dayjs from 'dayjs'; 2 | 3 | export const getOneDayTimeStamp = () => { 4 | const date = new Date(); 5 | const time = date.getTime(); //当前的毫秒数 6 | const oneDay = 86400000; //1000 * 60 * 60 * 24; //一天的毫秒数 7 | return time + oneDay; 8 | }; 9 | // 休眠函数 10 | export const sleepPromise = (ms) => 11 | new Promise((resolve) => setTimeout(resolve, ms)); 12 | /** 13 | * 返回当前时间 14 | */ 15 | export const currentDateTime = () => { 16 | return dayjs(new Date()).format('YYYY-MM-DD HH:mm:ss'); 17 | }; 18 | 19 | /** 20 | * 返回若干年、月、日、前的日期,默认:day 格式 20210101 21 | */ 22 | export const getDateOfBefore = ( 23 | n: number, 24 | scale: 'day' | 'month' | 'year' = 'day', 25 | ) => { 26 | return dayjs().subtract(n, scale).format('YYYYMMDD'); 27 | }; 28 | -------------------------------------------------------------------------------- /stock-server/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'prettier/@typescript-eslint', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /stock-server/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Post, UseGuards, Request, Get } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | import { AuthService } from './modules/auth/auth.service'; 4 | import { ProfileInfo } from './libs/decorators/profileInfo'; 5 | import { User } from './libs/decorators/user.decorator'; 6 | 7 | @Controller() 8 | export class AppController { 9 | constructor(private readonly authService: AuthService) {} 10 | 11 | @UseGuards(AuthGuard('jwt')) 12 | @Get('hello') 13 | async getHellow(@User() user: ProfileInfo) { 14 | return `${user.name},你好!`; 15 | } 16 | 17 | // 登录 18 | @UseGuards(AuthGuard('local')) 19 | @Post('login') 20 | async login(@Request() req) { 21 | return this.authService.login(req.user); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /stock-web/src/locales/ja-JP.ts: -------------------------------------------------------------------------------- 1 | import globalHeader from './ja-JP/globalHeader'; 2 | import menu from './ja-JP/menu'; 3 | import settingDrawer from './ja-JP/settingDrawer'; 4 | import settings from './ja-JP/settings'; 5 | import pwa from './ja-JP/pwa'; 6 | import component from './ja-JP/component'; 7 | import pages from './ja-JP/pages'; 8 | 9 | export default { 10 | 'navBar.lang': '言語', 11 | 'layout.user.link.help': 'ヘルプ', 12 | 'layout.user.link.privacy': 'プライバシー', 13 | 'layout.user.link.terms': '利用規約', 14 | 'app.preview.down.block': 'このページをローカルプロジェクトにダウンロードしてください', 15 | 'app.welcome.link.fetch-blocks': '', 16 | 'app.welcome.link.block-list': '', 17 | ...globalHeader, 18 | ...menu, 19 | ...settingDrawer, 20 | ...settings, 21 | ...pwa, 22 | ...component, 23 | ...pages, 24 | }; 25 | -------------------------------------------------------------------------------- /stock-web/src/locales/zh-CN.ts: -------------------------------------------------------------------------------- 1 | import component from './zh-CN/component'; 2 | import globalHeader from './zh-CN/globalHeader'; 3 | import menu from './zh-CN/menu'; 4 | import pwa from './zh-CN/pwa'; 5 | import settingDrawer from './zh-CN/settingDrawer'; 6 | import settings from './zh-CN/settings'; 7 | import pages from './zh-CN/pages'; 8 | 9 | export default { 10 | 'navBar.lang': '语言', 11 | 'layout.user.link.help': '帮助', 12 | 'layout.user.link.privacy': '隐私', 13 | 'layout.user.link.terms': '条款', 14 | 'app.preview.down.block': '下载此页面到本地项目', 15 | 'app.welcome.link.fetch-blocks': '获取全部区块', 16 | 'app.welcome.link.block-list': '基于 block 开发,快速构建标准页面', 17 | ...pages, 18 | ...globalHeader, 19 | ...menu, 20 | ...settingDrawer, 21 | ...settings, 22 | ...pwa, 23 | ...component, 24 | }; 25 | -------------------------------------------------------------------------------- /stock-server/src/modules/auth/strategies/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { ExtractJwt, Strategy } from 'passport-jwt'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Injectable } from '@nestjs/common'; 4 | import { jwtConstants } from '../../../config/jwt-key'; 5 | 6 | @Injectable() 7 | export class JwtStrategy extends PassportStrategy(Strategy) { 8 | constructor() { 9 | super({ 10 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 11 | ignoreExpiration: false, 12 | secretOrKey: jwtConstants.secret, 13 | }); 14 | } 15 | 16 | async validate(payload: any) { 17 | return { 18 | uid: payload.sub, 19 | mobile: payload.mobile, 20 | roles: payload.roles, 21 | name: payload.name, 22 | avatar: payload.avatar, 23 | }; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /stock-web/src/locales/zh-CN/globalHeader.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.globalHeader.search': '站内搜索', 3 | 'component.globalHeader.search.example1': '搜索提示一', 4 | 'component.globalHeader.search.example2': '搜索提示二', 5 | 'component.globalHeader.search.example3': '搜索提示三', 6 | 'component.globalHeader.help': '使用文档', 7 | 'component.globalHeader.notification': '通知', 8 | 'component.globalHeader.notification.empty': '你已查看所有通知', 9 | 'component.globalHeader.message': '消息', 10 | 'component.globalHeader.message.empty': '您已读完所有消息', 11 | 'component.globalHeader.event': '待办', 12 | 'component.globalHeader.event.empty': '你已完成所有待办', 13 | 'component.noticeIcon.clear': '清空', 14 | 'component.noticeIcon.cleared': '清空了', 15 | 'component.noticeIcon.empty': '暂无数据', 16 | 'component.noticeIcon.view-more': '查看更多', 17 | }; 18 | -------------------------------------------------------------------------------- /stock-web/src/locales/zh-TW/globalHeader.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.globalHeader.search': '站內搜索', 3 | 'component.globalHeader.search.example1': '搜索提示壹', 4 | 'component.globalHeader.search.example2': '搜索提示二', 5 | 'component.globalHeader.search.example3': '搜索提示三', 6 | 'component.globalHeader.help': '使用手冊', 7 | 'component.globalHeader.notification': '通知', 8 | 'component.globalHeader.notification.empty': '妳已查看所有通知', 9 | 'component.globalHeader.message': '消息', 10 | 'component.globalHeader.message.empty': '您已讀完所有消息', 11 | 'component.globalHeader.event': '待辦', 12 | 'component.globalHeader.event.empty': '妳已完成所有待辦', 13 | 'component.noticeIcon.clear': '清空', 14 | 'component.noticeIcon.cleared': '清空了', 15 | 'component.noticeIcon.empty': '暫無資料', 16 | 'component.noticeIcon.view-more': '查看更多', 17 | }; 18 | -------------------------------------------------------------------------------- /stock-server/src/modules/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AuthService } from './auth.service'; 3 | import { UserModule } from '../user/user.module'; 4 | import { PassportModule } from '@nestjs/passport'; 5 | import { LocalStrategy } from './strategies/local.strategy'; 6 | import { JwtModule } from '@nestjs/jwt'; 7 | import { jwtConstants } from '../../config/jwt-key'; 8 | import { JwtStrategy } from './strategies/jwt.strategy'; 9 | 10 | @Module({ 11 | providers: [AuthService, LocalStrategy, JwtStrategy], 12 | imports: [ 13 | UserModule, 14 | PassportModule, 15 | JwtModule.register({ 16 | secret: jwtConstants.secret, 17 | signOptions: { expiresIn: '86400s' }, // 一天过期时间 18 | }), 19 | ], 20 | exports: [AuthService], 21 | }) 22 | export class AuthModule {} 23 | -------------------------------------------------------------------------------- /stock-web/src/services/login.ts: -------------------------------------------------------------------------------- 1 | import HttpClient from "./fetch"; 2 | 3 | // config define 中的 API_SERVER 可直接使用 4 | 5 | export type LoginParamsType = { 6 | username: string; 7 | password: string; 8 | // mobile: string; 9 | // captcha: string; 10 | }; 11 | 12 | // export async function fakeAccountLogin(params: LoginParamsType) { 13 | // return request('/app/login/account', { 14 | // method: 'POST', 15 | // data: params, 16 | // }); 17 | // } 18 | 19 | // export async function getFakeCaptcha(mobile: string) { 20 | // return request(`/app/login/captcha?mobile=${mobile}`); 21 | // } 22 | 23 | // server 会在 proxy pathRewrite 中去除 24 | export async function mobileLogin(data: LoginParamsType) { 25 | return HttpClient.request({ 26 | url: `/server/auth/login`, 27 | method: "POST", 28 | data, 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /stock-web/src/models/connect.d.ts: -------------------------------------------------------------------------------- 1 | import type { MenuDataItem, Settings as ProSettings } from '@ant-design/pro-layout'; 2 | import { GlobalModelState } from './global'; 3 | import { UserModelState } from './user'; 4 | import type { StateType } from './login'; 5 | 6 | export { GlobalModelState, UserModelState }; 7 | 8 | export type Loading = { 9 | global: boolean; 10 | effects: Record; 11 | models: { 12 | global?: boolean; 13 | menu?: boolean; 14 | setting?: boolean; 15 | user?: boolean; 16 | login?: boolean; 17 | }; 18 | }; 19 | 20 | export type ConnectState = { 21 | global: GlobalModelState; 22 | loading: Loading; 23 | settings: ProSettings; 24 | user: UserModelState; 25 | login: StateType; 26 | }; 27 | 28 | export type Route = { 29 | routes?: Route[]; 30 | } & MenuDataItem; 31 | -------------------------------------------------------------------------------- /stock-web/src/locales/ja-JP/globalHeader.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.globalHeader.search': '検索', 3 | 'component.globalHeader.search.example1': '検索例1', 4 | 'component.globalHeader.search.example2': '検索例2', 5 | 'component.globalHeader.search.example3': '検索例3', 6 | 'component.globalHeader.help': 'ヘルプ', 7 | 'component.globalHeader.notification': '通知', 8 | 'component.globalHeader.notification.empty': 'すべての通知を表示しました。', 9 | 'component.globalHeader.message': 'メッセージ', 10 | 'component.globalHeader.message.empty': 'すべてのメッセージを表示しました。', 11 | 'component.globalHeader.event': 'イベント', 12 | 'component.globalHeader.event.empty': 'すべてのイベントを表示しました。', 13 | 'component.noticeIcon.clear': 'クリア', 14 | 'component.noticeIcon.cleared': 'クリア済み', 15 | 'component.noticeIcon.empty': '通知なし', 16 | 'component.noticeIcon.view-more': 'もっと見る', 17 | }; 18 | -------------------------------------------------------------------------------- /stock-web/src/pages/TableList/data.d.ts: -------------------------------------------------------------------------------- 1 | export type TableListItem = { 2 | key: number; 3 | disabled?: boolean; 4 | href: string; 5 | avatar: string; 6 | name: string; 7 | owner: string; 8 | desc: string; 9 | callNo: number; 10 | status: number; 11 | updatedAt: Date; 12 | createdAt: Date; 13 | progress: number; 14 | }; 15 | 16 | export type TableListPagination = { 17 | total: number; 18 | pageSize: number; 19 | current: number; 20 | }; 21 | 22 | export type TableListData = { 23 | list: TableListItem[]; 24 | pagination: Partial; 25 | }; 26 | 27 | export type TableListParams = { 28 | status?: string; 29 | name?: string; 30 | desc?: string; 31 | key?: number; 32 | pageSize?: number; 33 | currentPage?: number; 34 | filter?: Record; 35 | sorter?: Record; 36 | }; 37 | -------------------------------------------------------------------------------- /stock-server/src/modules/user/dto/user.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString, IsNotEmpty, IsMobilePhone } from 'class-validator'; 2 | import { BusiErrorCode } from '../../../libs/enums/error-code-enum'; 3 | 4 | /** 5 | * 创建用户 6 | */ 7 | export class CreateUserDto { 8 | @IsNotEmpty({ 9 | message: '姓名不能为空', 10 | context: { errorCode: BusiErrorCode.PARAM_ERROR }, 11 | }) 12 | @IsString({ 13 | message: '姓名必须是字符串', 14 | context: { errorCode: BusiErrorCode.PARAM_ERROR }, 15 | }) 16 | readonly name: string; 17 | 18 | @IsNotEmpty({ 19 | message: '手机号不能为空', 20 | context: { errorCode: BusiErrorCode.PARAM_ERROR }, 21 | }) 22 | @IsMobilePhone('zh-CN') 23 | readonly mobile: string; 24 | @IsNotEmpty({ 25 | message: '密码不能为空', 26 | context: { errorCode: BusiErrorCode.PARAM_ERROR }, 27 | }) 28 | readonly password: string; 29 | } 30 | -------------------------------------------------------------------------------- /stock-web/config/proxy.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 在生产环境 代理是无法生效的,所以这里没有生产环境的配置 3 | * The agent cannot take effect in the production environment 4 | * so there is no configuration of the production environment 5 | * For details, please see 6 | * https://pro.ant.design/docs/deploy 7 | */ 8 | export default { 9 | // 开发环境 10 | dev: { 11 | "/server/": { 12 | target: "http://localhost:3000", 13 | changeOrigin: true, 14 | pathRewrite: { "^/server": "" }, // 作用是在代理路径中将属性路径去除 15 | }, 16 | }, 17 | // 测试环境 18 | test: { 19 | "/server/": { 20 | target: "http://localhost:3000", 21 | changeOrigin: true, 22 | pathRewrite: { "^/server": "" }, 23 | }, 24 | }, 25 | // 预发环境 26 | pre: { 27 | "/server/": { 28 | target: "http://localhost:3000", 29 | changeOrigin: true, 30 | pathRewrite: { "^/server": "" }, 31 | }, 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /stock-web/src/locales/en-US.ts: -------------------------------------------------------------------------------- 1 | import component from './en-US/component'; 2 | import globalHeader from './en-US/globalHeader'; 3 | import menu from './en-US/menu'; 4 | import pwa from './en-US/pwa'; 5 | import settingDrawer from './en-US/settingDrawer'; 6 | import settings from './en-US/settings'; 7 | import pages from './en-US/pages'; 8 | 9 | export default { 10 | 'navBar.lang': 'Languages', 11 | 'layout.user.link.help': 'Help', 12 | 'layout.user.link.privacy': 'Privacy', 13 | 'layout.user.link.terms': 'Terms', 14 | 'app.preview.down.block': 'Download this page to your local project', 15 | 'app.welcome.link.fetch-blocks': 'Get all block', 16 | 'app.welcome.link.block-list': 'Quickly build standard, pages based on `block` development', 17 | ...globalHeader, 18 | ...menu, 19 | ...settingDrawer, 20 | ...settings, 21 | ...pwa, 22 | ...component, 23 | ...pages, 24 | }; 25 | -------------------------------------------------------------------------------- /stock-server/src/modules/stock/dto/stock.dto.ts: -------------------------------------------------------------------------------- 1 | import { Exclude } from 'class-transformer'; 2 | 3 | // 股票列表类型 4 | export class StockDto { 5 | readonly code: string; 6 | readonly name: string; 7 | readonly market: string; 8 | readonly price: string; 9 | readonly peTtm: string; 10 | readonly peTtmAvg: string; 11 | readonly peTtmRate: string; 12 | readonly peTtmMid: string; 13 | @Exclude() 14 | readonly sourceData: object | null; 15 | 16 | us: any; 17 | } 18 | 19 | // 列表查询参数类型 20 | export class StockQueryDto { 21 | readonly pageSize: number; 22 | readonly pageIndex: number; 23 | readonly keywords: string; 24 | readonly orderBy: number; 25 | } 26 | 27 | export class UserStockDto { 28 | readonly code: string; 29 | } 30 | 31 | // 分页数据类型 32 | export class PageListModel { 33 | totalNum: number; // 总数 34 | pageSize: number; // 页数量 35 | pageIndex: number; // 当前页码 36 | list: T[]; 37 | } 38 | -------------------------------------------------------------------------------- /stock-web/src/locales/id-ID.ts: -------------------------------------------------------------------------------- 1 | import component from './id-ID/component'; 2 | import globalHeader from './id-ID/globalHeader'; 3 | import menu from './id-ID/menu'; 4 | import pwa from './id-ID/pwa'; 5 | import settingDrawer from './id-ID/settingDrawer'; 6 | import settings from './id-ID/settings'; 7 | import pages from './id-ID/pages'; 8 | 9 | export default { 10 | 'navbar.lang': 'Bahasa', 11 | 'layout.user.link.help': 'Bantuan', 12 | 'layout.user.link.privacy': 'Privasi', 13 | 'layout.user.link.terms': 'Ketentuan', 14 | 'app.preview.down.block': 'Unduh halaman ini dalam projek lokal anda', 15 | 'app.welcome.link.fetch-blocks': 'Dapatkan semua blok', 16 | 'app.welcome.link.block-list': 17 | 'Buat standar dengan cepat, halaman-halaman berdasarkan pengembangan `block`', 18 | ...globalHeader, 19 | ...menu, 20 | ...settingDrawer, 21 | ...settings, 22 | ...pwa, 23 | ...component, 24 | ...pages, 25 | }; 26 | -------------------------------------------------------------------------------- /stock-web/src/pages/User/login/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .main { 4 | width: 328px; 5 | margin: 0 auto; 6 | @media screen and (max-width: @screen-sm) { 7 | width: 95%; 8 | max-width: 328px; 9 | } 10 | 11 | :global { 12 | .@{ant-prefix}-tabs-nav-list { 13 | margin: auto; 14 | font-size: 16px; 15 | } 16 | } 17 | 18 | .icon { 19 | margin-left: 16px; 20 | color: rgba(0, 0, 0, 0.2); 21 | font-size: 24px; 22 | vertical-align: middle; 23 | cursor: pointer; 24 | transition: color 0.3s; 25 | 26 | &:hover { 27 | color: @primary-color; 28 | } 29 | } 30 | 31 | .other { 32 | margin-top: 24px; 33 | line-height: 22px; 34 | text-align: left; 35 | .register { 36 | float: right; 37 | } 38 | } 39 | 40 | .prefixIcon { 41 | color: @primary-color; 42 | font-size: @font-size-base; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /stock-web/src/pages/TableList/service.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | import type { TableListParams, TableListItem } from './data.d'; 3 | 4 | export async function queryRule(params?: TableListParams) { 5 | return request('/api/rule', { 6 | params, 7 | }); 8 | } 9 | 10 | export async function removeRule(params: { key: number[] }) { 11 | return request('/api/rule', { 12 | method: 'POST', 13 | data: { 14 | ...params, 15 | method: 'delete', 16 | }, 17 | }); 18 | } 19 | 20 | export async function addRule(params: TableListItem) { 21 | return request('/api/rule', { 22 | method: 'POST', 23 | data: { 24 | ...params, 25 | method: 'post', 26 | }, 27 | }); 28 | } 29 | 30 | export async function updateRule(params: TableListParams) { 31 | return request('/api/rule', { 32 | method: 'POST', 33 | data: { 34 | ...params, 35 | method: 'update', 36 | }, 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /stock-web/src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import { parse } from 'querystring'; 2 | 3 | /* eslint no-useless-escape:0 import/prefer-default-export:0 */ 4 | const reg = /(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/; 5 | 6 | export const isUrl = (path: string): boolean => reg.test(path); 7 | 8 | export const isAntDesignPro = (): boolean => { 9 | if (ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site') { 10 | return true; 11 | } 12 | return window.location.hostname === 'preview.pro.ant.design'; 13 | }; 14 | 15 | // 给官方演示站点用,用于关闭真实开发环境不需要使用的特性 16 | export const isAntDesignProOrDev = (): boolean => { 17 | const { NODE_ENV } = process.env; 18 | if (NODE_ENV === 'development') { 19 | return true; 20 | } 21 | return isAntDesignPro(); 22 | }; 23 | 24 | export const getPageQuery = () => parse(window.location.href.split('?')[1]); 25 | -------------------------------------------------------------------------------- /stock-web/src/locales/en-US/globalHeader.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.globalHeader.search': 'Search', 3 | 'component.globalHeader.search.example1': 'Search example 1', 4 | 'component.globalHeader.search.example2': 'Search example 2', 5 | 'component.globalHeader.search.example3': 'Search example 3', 6 | 'component.globalHeader.help': 'Help', 7 | 'component.globalHeader.notification': 'Notification', 8 | 'component.globalHeader.notification.empty': 'You have viewed all notifications.', 9 | 'component.globalHeader.message': 'Message', 10 | 'component.globalHeader.message.empty': 'You have viewed all messsages.', 11 | 'component.globalHeader.event': 'Event', 12 | 'component.globalHeader.event.empty': 'You have viewed all events.', 13 | 'component.noticeIcon.clear': 'Clear', 14 | 'component.noticeIcon.cleared': 'Cleared', 15 | 'component.noticeIcon.empty': 'No notifications', 16 | 'component.noticeIcon.view-more': 'View more', 17 | }; 18 | -------------------------------------------------------------------------------- /stock-web/src/locales/id-ID/globalHeader.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.globalHeader.search': 'Pencarian', 3 | 'component.globalHeader.search.example1': 'Contoh 1 Pencarian', 4 | 'component.globalHeader.search.example2': 'Contoh 2 Pencarian', 5 | 'component.globalHeader.search.example3': 'Contoh 3 Pencarian', 6 | 'component.globalHeader.help': 'Bantuan', 7 | 'component.globalHeader.notification': 'Notifikasi', 8 | 'component.globalHeader.notification.empty': 'Anda telah membaca semua notifikasi', 9 | 'component.globalHeader.message': 'Pesan', 10 | 'component.globalHeader.message.empty': 'Anda telah membaca semua pesan.', 11 | 'component.globalHeader.event': 'Acara', 12 | 'component.globalHeader.event.empty': 'Anda telah melihat semua acara.', 13 | 'component.noticeIcon.clear': 'Kosongkan', 14 | 'component.noticeIcon.cleared': 'Berhasil dikosongkan', 15 | 'component.noticeIcon.empty': 'Tidak ada pemberitahuan', 16 | 'component.noticeIcon.view-more': 'Melihat lebih', 17 | }; 18 | -------------------------------------------------------------------------------- /stock-web/src/components/Authorized/AuthorizedRoute.tsx: -------------------------------------------------------------------------------- 1 | import { Redirect, Route } from 'umi'; 2 | 3 | import React from 'react'; 4 | import Authorized from './Authorized'; 5 | import type { IAuthorityType } from './CheckPermissions'; 6 | 7 | type AuthorizedRouteProps = { 8 | currentAuthority: string; 9 | component: React.ComponentClass; 10 | render: (props: any) => React.ReactNode; 11 | redirectPath: string; 12 | authority: IAuthorityType; 13 | }; 14 | 15 | const AuthorizedRoute: React.SFC = ({ 16 | component: Component, 17 | render, 18 | authority, 19 | redirectPath, 20 | ...rest 21 | }) => ( 22 | } />} 25 | > 26 | (Component ? : render(props))} 29 | /> 30 | 31 | ); 32 | 33 | export default AuthorizedRoute; 34 | -------------------------------------------------------------------------------- /stock-web/src/global.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | html, 4 | body, 5 | #root { 6 | height: 100%; 7 | } 8 | 9 | .colorWeak { 10 | filter: invert(80%); 11 | } 12 | 13 | .ant-layout { 14 | min-height: 100vh; 15 | } 16 | 17 | canvas { 18 | display: block; 19 | } 20 | 21 | body { 22 | text-rendering: optimizeLegibility; 23 | -webkit-font-smoothing: antialiased; 24 | -moz-osx-font-smoothing: grayscale; 25 | } 26 | 27 | ul, 28 | ol { 29 | list-style: none; 30 | } 31 | 32 | @media (max-width: @screen-xs) { 33 | .ant-table { 34 | width: 100%; 35 | overflow-x: auto; 36 | &-thead > tr, 37 | &-tbody > tr { 38 | > th, 39 | > td { 40 | white-space: pre; 41 | > span { 42 | display: block; 43 | } 44 | } 45 | } 46 | } 47 | } 48 | 49 | // 兼容IE11 50 | @media screen and(-ms-high-contrast: active), (-ms-high-contrast: none) { 51 | body .ant-design-pro > .ant-layout { 52 | min-height: 100vh; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /stock-web/src/services/stock.ts: -------------------------------------------------------------------------------- 1 | import HttpClient from './fetch'; 2 | 3 | export type StockListQuery = { 4 | keywords: string; 5 | pageIndex: number; 6 | pageSize: number; 7 | orderBy: number; 8 | }; 9 | 10 | export const stockList = async (data: StockListQuery) => { 11 | return HttpClient.request({ 12 | url: `/server/stock/list`, 13 | method: 'POST', 14 | data, 15 | }); 16 | }; 17 | 18 | export const userStockList = async (data: StockListQuery) => { 19 | return HttpClient.request({ 20 | url: `/server/stock/user-list`, 21 | method: 'POST', 22 | data, 23 | }); 24 | }; 25 | 26 | export const addChoice = async (code: string) => { 27 | return HttpClient.request({ 28 | url: `/server/stock/add-choice`, 29 | method: 'POST', 30 | data: { code }, 31 | }); 32 | }; 33 | 34 | export const deleteChoice = async (code: string) => { 35 | return HttpClient.request({ 36 | url: `/server/stock/delete-choice`, 37 | method: 'POST', 38 | data: { code }, 39 | }); 40 | }; 41 | -------------------------------------------------------------------------------- /stock-web/src/locales/pt-BR/globalHeader.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.globalHeader.search': 'Busca', 3 | 'component.globalHeader.search.example1': 'Exemplo de busca 1', 4 | 'component.globalHeader.search.example2': 'Exemplo de busca 2', 5 | 'component.globalHeader.search.example3': 'Exemplo de busca 3', 6 | 'component.globalHeader.help': 'Ajuda', 7 | 'component.globalHeader.notification': 'Notificação', 8 | 'component.globalHeader.notification.empty': 'Você visualizou todas as notificações.', 9 | 'component.globalHeader.message': 'Mensagem', 10 | 'component.globalHeader.message.empty': 'Você visualizou todas as mensagens.', 11 | 'component.globalHeader.event': 'Evento', 12 | 'component.globalHeader.event.empty': 'Você visualizou todos os eventos.', 13 | 'component.noticeIcon.clear': 'Limpar', 14 | 'component.noticeIcon.cleared': 'Limpo', 15 | 'component.noticeIcon.empty': 'Sem notificações', 16 | 'component.noticeIcon.loaded': 'Carregado', 17 | 'component.noticeIcon.view-more': 'Veja mais', 18 | }; 19 | -------------------------------------------------------------------------------- /stock-server/src/libs/filters/http-exception.filter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArgumentsHost, 3 | Catch, 4 | ExceptionFilter, 5 | HttpException, 6 | } from '@nestjs/common'; 7 | import { BusiException } from './busi.exception'; 8 | 9 | @Catch(HttpException) 10 | export class HttpExceptionFilter implements ExceptionFilter { 11 | catch(exception: HttpException, host: ArgumentsHost) { 12 | const ctx = host.switchToHttp(); 13 | const response = ctx.getResponse(); 14 | const request = ctx.getRequest(); 15 | const status = exception.getStatus(); 16 | 17 | let code, message; 18 | if (exception instanceof BusiException) { 19 | code = exception.getErrorCode(); 20 | message = exception.getErrorMessage(); 21 | } else { 22 | code = exception.getStatus(); 23 | message = exception.message; 24 | } 25 | response.status(status).json({ 26 | code, 27 | message, 28 | data: null, 29 | date: new Date().toLocaleDateString(), 30 | path: request.url, 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /stock-server/src/modules/auth/strategies/local.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Strategy } from 'passport-local'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Injectable, HttpStatus } from '@nestjs/common'; 4 | import { AuthService } from '../auth.service'; 5 | import { BusiException } from '../../../libs/filters/busi.exception'; 6 | import { BusiErrorCode } from '../../../libs/enums/error-code-enum'; 7 | 8 | @Injectable() 9 | export class LocalStrategy extends PassportStrategy(Strategy) { 10 | constructor(private readonly authService: AuthService) { 11 | super(); 12 | } 13 | /** 14 | * 15 | * @param username 提交的参数名必须是 username ,应该是PassportStrategy默认的 16 | * @param password 17 | */ 18 | async validate(username: string, password: string): Promise { 19 | const user = await this.authService.validateUser(username, password); 20 | if (!user) { 21 | throw new BusiException( 22 | BusiErrorCode.PWD_ERROR, 23 | '账号或者密码错误', 24 | HttpStatus.OK, 25 | ); 26 | } 27 | return user; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /stock-web/src/components/Authorized/renderAuthorize.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable eslint-comments/disable-enable-pair */ 2 | /* eslint-disable import/no-mutable-exports */ 3 | let CURRENT: string | string[] = 'NULL'; 4 | 5 | type CurrentAuthorityType = string | string[] | (() => typeof CURRENT); 6 | /** 7 | * Use authority or getAuthority 8 | * 9 | * @param {string|()=>String} currentAuthority 10 | */ 11 | const renderAuthorize = (Authorized: T): ((currentAuthority: CurrentAuthorityType) => T) => ( 12 | currentAuthority: CurrentAuthorityType, 13 | ): T => { 14 | if (currentAuthority) { 15 | if (typeof currentAuthority === 'function') { 16 | CURRENT = currentAuthority(); 17 | } 18 | if ( 19 | Object.prototype.toString.call(currentAuthority) === '[object String]' || 20 | Array.isArray(currentAuthority) 21 | ) { 22 | CURRENT = currentAuthority as string[]; 23 | } 24 | } else { 25 | CURRENT = 'NULL'; 26 | } 27 | return Authorized; 28 | }; 29 | 30 | export { CURRENT }; 31 | export default (Authorized: T) => renderAuthorize(Authorized); 32 | -------------------------------------------------------------------------------- /stock-web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "build/dist", 4 | "module": "esnext", 5 | "target": "esnext", 6 | "lib": ["esnext", "dom"], 7 | "sourceMap": true, 8 | "baseUrl": ".", 9 | "jsx": "preserve", 10 | "allowSyntheticDefaultImports": true, 11 | "moduleResolution": "node", 12 | "forceConsistentCasingInFileNames": true, 13 | "noImplicitReturns": true, 14 | "suppressImplicitAnyIndexErrors": true, 15 | "noUnusedLocals": true, 16 | "allowJs": true, 17 | "skipLibCheck": true, 18 | "experimentalDecorators": true, 19 | "strict": true, 20 | "paths": { 21 | "@/*": ["./src/*"], 22 | "@@/*": ["./src/.umi/*"] 23 | } 24 | }, 25 | "include": [ 26 | "mock/**/*", 27 | "src/**/*", 28 | "tests/**/*", 29 | "test/**/*", 30 | "__test__/**/*", 31 | "typings/**/*", 32 | "config/**/*", 33 | ".eslintrc.js", 34 | ".stylelintrc.js", 35 | ".prettierrc.js", 36 | "jest.config.js", 37 | "mock/*" 38 | ], 39 | "exclude": ["node_modules", "build", "dist", "scripts", "src/.umi/*", "webpack", "jest"] 40 | } 41 | -------------------------------------------------------------------------------- /stock-web/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'slash2'; 2 | declare module '*.css'; 3 | declare module '*.less'; 4 | declare module '*.scss'; 5 | declare module '*.sass'; 6 | declare module '*.svg'; 7 | declare module '*.png'; 8 | declare module '*.jpg'; 9 | declare module '*.jpeg'; 10 | declare module '*.gif'; 11 | declare module '*.bmp'; 12 | declare module '*.tiff'; 13 | declare module 'omit.js'; 14 | 15 | // google analytics interface 16 | type GAFieldsObject = { 17 | eventCategory: string; 18 | eventAction: string; 19 | eventLabel?: string; 20 | eventValue?: number; 21 | nonInteraction?: boolean; 22 | }; 23 | 24 | interface Window { 25 | ga: ( 26 | command: 'send', 27 | hitType: 'event' | 'pageview', 28 | fieldsObject: GAFieldsObject | string, 29 | ) => void; 30 | reloadAuthorized: () => void; 31 | } 32 | 33 | declare let ga: () => void; 34 | 35 | // preview.pro.ant.design only do not use in your production ; 36 | // preview.pro.ant.design 专用环境变量,请不要在你的项目中使用它。 37 | declare let ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: 'site' | undefined; 38 | 39 | declare const REACT_APP_ENV: 'test' | 'dev' | 'pre' | false; 40 | -------------------------------------------------------------------------------- /stock-server/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | import { UserController } from './modules/user/user.controller'; 5 | import { UserModule } from './modules/user/user.module'; 6 | import { TypeOrmModule } from '@nestjs/typeorm'; 7 | import { AuthModule } from './modules/auth/auth.module'; 8 | import { StockModule } from './modules/stock/stock.module'; 9 | import { ScheduleModule } from '@nestjs/schedule'; 10 | 11 | @Module({ 12 | imports: [ 13 | TypeOrmModule.forRoot({ 14 | name: 'default', 15 | type: 'mysql', 16 | host: 'data base host', 17 | port: 3306, 18 | username: 'data base username', 19 | password: 'data base password', 20 | database: 'stock_demo', 21 | synchronize: false, 22 | entities: [__dirname + '/entities/*.js'], 23 | }), 24 | UserModule, 25 | AuthModule, 26 | StockModule, 27 | ScheduleModule.forRoot(), 28 | ], 29 | controllers: [AppController, UserController], 30 | providers: [AppService], 31 | }) 32 | export class AppModule {} 33 | -------------------------------------------------------------------------------- /stock-web/src/components/Authorized/Authorized.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Result } from 'antd'; 3 | import check from './CheckPermissions'; 4 | import type { IAuthorityType } from './CheckPermissions'; 5 | import type AuthorizedRoute from './AuthorizedRoute'; 6 | import type Secured from './Secured'; 7 | 8 | type AuthorizedProps = { 9 | authority: IAuthorityType; 10 | noMatch?: React.ReactNode; 11 | }; 12 | 13 | type IAuthorizedType = React.FunctionComponent & { 14 | Secured: typeof Secured; 15 | check: typeof check; 16 | AuthorizedRoute: typeof AuthorizedRoute; 17 | }; 18 | 19 | const Authorized: React.FunctionComponent = ({ 20 | children, 21 | authority, 22 | noMatch = ( 23 | 28 | ), 29 | }) => { 30 | const childrenRender: React.ReactNode = typeof children === 'undefined' ? null : children; 31 | const dom = check(authority, childrenRender, noMatch); 32 | return <>{dom}; 33 | }; 34 | 35 | export default Authorized as IAuthorizedType; 36 | -------------------------------------------------------------------------------- /stock-web/README.md: -------------------------------------------------------------------------------- 1 | # Ant Design Pro 2 | 3 | This project is initialized with [Ant Design Pro](https://pro.ant.design). Follow is the quick guide for how to use. 4 | 5 | ## Environment Prepare 6 | 7 | Install `node_modules`: 8 | 9 | ```bash 10 | npm install 11 | ``` 12 | 13 | or 14 | 15 | ```bash 16 | yarn 17 | ``` 18 | 19 | ## Provided Scripts 20 | 21 | Ant Design Pro provides some useful script to help you quick start and build with web project, code style check and test. 22 | 23 | Scripts provided in `package.json`. It's safe to modify or add additional script: 24 | 25 | ### Start project 26 | 27 | ```bash 28 | npm start 29 | ``` 30 | 31 | ### Build project 32 | 33 | ```bash 34 | npm run build 35 | ``` 36 | 37 | ### Check code style 38 | 39 | ```bash 40 | npm run lint 41 | ``` 42 | 43 | You can also use script to auto fix some lint error: 44 | 45 | ```bash 46 | npm run lint:fix 47 | ``` 48 | 49 | ### Test code 50 | 51 | ```bash 52 | npm test 53 | ``` 54 | 55 | ## More 56 | 57 | You can view full document on our [official website](https://pro.ant.design). And welcome any feedback in our [github](https://github.com/ant-design/ant-design-pro). 58 | -------------------------------------------------------------------------------- /stock-web/src/models/setting.ts: -------------------------------------------------------------------------------- 1 | import type { Reducer } from 'umi'; 2 | import type { DefaultSettings } from '../../config/defaultSettings'; 3 | import defaultSettings from '../../config/defaultSettings'; 4 | 5 | export type SettingModelType = { 6 | namespace: 'settings'; 7 | state: DefaultSettings; 8 | reducers: { 9 | changeSetting: Reducer; 10 | }; 11 | }; 12 | 13 | const updateColorWeak: (colorWeak: boolean) => void = (colorWeak) => { 14 | const root = document.getElementById('root'); 15 | if (root) { 16 | root.className = colorWeak ? 'colorWeak' : ''; 17 | } 18 | }; 19 | 20 | const SettingModel: SettingModelType = { 21 | namespace: 'settings', 22 | state: defaultSettings, 23 | reducers: { 24 | changeSetting(state = defaultSettings, { payload }) { 25 | const { colorWeak, contentWidth } = payload; 26 | 27 | if (state.contentWidth !== contentWidth && window.dispatchEvent) { 28 | window.dispatchEvent(new Event('resize')); 29 | } 30 | updateColorWeak(!!colorWeak); 31 | return { 32 | ...state, 33 | ...payload, 34 | }; 35 | }, 36 | }, 37 | }; 38 | export default SettingModel; 39 | -------------------------------------------------------------------------------- /stock-web/tests/PuppeteerEnvironment.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | const NodeEnvironment = require('jest-environment-node'); 3 | const getBrowser = require('./getBrowser'); 4 | 5 | class PuppeteerEnvironment extends NodeEnvironment { 6 | // Jest is not available here, so we have to reverse engineer 7 | // the setTimeout function, see https://github.com/facebook/jest/blob/v23.1.0/packages/jest-runtime/src/index.js#L823 8 | setTimeout(timeout) { 9 | if (this.global.jasmine) { 10 | // eslint-disable-next-line no-underscore-dangle 11 | this.global.jasmine.DEFAULT_TIMEOUT_INTERVAL = timeout; 12 | } else { 13 | this.global[Symbol.for('TEST_TIMEOUT_SYMBOL')] = timeout; 14 | } 15 | } 16 | 17 | async setup() { 18 | const browser = await getBrowser(); 19 | const page = await browser.newPage(); 20 | this.global.browser = browser; 21 | this.global.page = page; 22 | } 23 | 24 | async teardown() { 25 | const { page, browser } = this.global; 26 | 27 | if (page) { 28 | await page.close(); 29 | } 30 | 31 | if (browser) { 32 | await browser.disconnect(); 33 | } 34 | 35 | if (browser) { 36 | await browser.close(); 37 | } 38 | } 39 | } 40 | 41 | module.exports = PuppeteerEnvironment; 42 | -------------------------------------------------------------------------------- /stock-web/config/config.ts: -------------------------------------------------------------------------------- 1 | // https://umijs.org/config/ 2 | import { defineConfig } from "umi"; 3 | import defaultSettings from "./defaultSettings"; 4 | import proxy from "./proxy"; 5 | import routes from "./routes"; 6 | 7 | const { REACT_APP_ENV } = process.env; 8 | 9 | export default defineConfig({ 10 | hash: true, 11 | antd: {}, 12 | dva: { 13 | hmr: true, 14 | }, 15 | history: { 16 | type: "browser", 17 | }, 18 | locale: { 19 | // default zh-CN 20 | default: "zh-CN", 21 | antd: true, 22 | // default true, when it is true, will use `navigator.language` overwrite default 23 | baseNavigator: true, 24 | }, 25 | dynamicImport: { 26 | loading: "@/components/PageLoading/index", 27 | }, 28 | targets: { 29 | ie: 11, 30 | }, 31 | // umi routes: https://umijs.org/docs/routing 32 | routes, 33 | // Theme for antd: https://ant.design/docs/react/customize-theme-cn 34 | theme: { 35 | "primary-color": defaultSettings.primaryColor, 36 | }, 37 | title: false, 38 | ignoreMomentLocale: true, 39 | // REACT_APP_ENV 在package.json 运行脚本中配置环境 40 | proxy: proxy[REACT_APP_ENV || "dev"], 41 | manifest: { 42 | basePath: "/", 43 | }, 44 | base: "/stock-web/", 45 | publicPath: "/stock-web/", 46 | esbuild: {}, 47 | // mock: false, //关闭mock 48 | }); 49 | -------------------------------------------------------------------------------- /stock-web/tests/getBrowser.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | /* eslint-disable import/no-extraneous-dependencies */ 3 | const findChrome = require('carlo/lib/find_chrome'); 4 | 5 | const getBrowser = async () => { 6 | try { 7 | // eslint-disable-next-line import/no-unresolved 8 | const puppeteer = require('puppeteer'); 9 | const browser = await puppeteer.launch({ 10 | args: [ 11 | '--disable-gpu', 12 | '--disable-dev-shm-usage', 13 | '--no-first-run', 14 | '--no-zygote', 15 | '--no-sandbox', 16 | ], 17 | }); 18 | return browser; 19 | } catch (error) { 20 | // console.log(error) 21 | } 22 | 23 | try { 24 | // eslint-disable-next-line import/no-unresolved 25 | const puppeteer = require('puppeteer-core'); 26 | const findChromePath = await findChrome({}); 27 | const { executablePath } = findChromePath; 28 | const browser = await puppeteer.launch({ 29 | executablePath, 30 | args: [ 31 | '--disable-gpu', 32 | '--disable-dev-shm-usage', 33 | '--no-first-run', 34 | '--no-zygote', 35 | '--no-sandbox', 36 | ], 37 | }); 38 | return browser; 39 | } catch (error) { 40 | console.log('🧲 no find chrome'); 41 | } 42 | throw new Error('no find puppeteer'); 43 | }; 44 | 45 | module.exports = getBrowser; 46 | -------------------------------------------------------------------------------- /stock-web/src/locales/zh-CN/settingDrawer.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.setting.pagestyle': '整体风格设置', 3 | 'app.setting.pagestyle.dark': '暗色菜单风格', 4 | 'app.setting.pagestyle.light': '亮色菜单风格', 5 | 'app.setting.content-width': '内容区域宽度', 6 | 'app.setting.content-width.fixed': '定宽', 7 | 'app.setting.content-width.fluid': '流式', 8 | 'app.setting.themecolor': '主题色', 9 | 'app.setting.themecolor.dust': '薄暮', 10 | 'app.setting.themecolor.volcano': '火山', 11 | 'app.setting.themecolor.sunset': '日暮', 12 | 'app.setting.themecolor.cyan': '明青', 13 | 'app.setting.themecolor.green': '极光绿', 14 | 'app.setting.themecolor.daybreak': '拂晓蓝(默认)', 15 | 'app.setting.themecolor.geekblue': '极客蓝', 16 | 'app.setting.themecolor.purple': '酱紫', 17 | 'app.setting.navigationmode': '导航模式', 18 | 'app.setting.sidemenu': '侧边菜单布局', 19 | 'app.setting.topmenu': '顶部菜单布局', 20 | 'app.setting.fixedheader': '固定 Header', 21 | 'app.setting.fixedsidebar': '固定侧边菜单', 22 | 'app.setting.fixedsidebar.hint': '侧边菜单布局时可配置', 23 | 'app.setting.hideheader': '下滑时隐藏 Header', 24 | 'app.setting.hideheader.hint': '固定 Header 时可配置', 25 | 'app.setting.othersettings': '其他设置', 26 | 'app.setting.weakmode': '色弱模式', 27 | 'app.setting.copy': '拷贝设置', 28 | 'app.setting.copyinfo': '拷贝成功,请到 src/defaultSettings.js 中替换默认配置', 29 | 'app.setting.production.hint': 30 | '配置栏只在开发环境用于预览,生产环境不会展现,请拷贝后手动修改配置文件', 31 | }; 32 | -------------------------------------------------------------------------------- /stock-web/src/locales/zh-TW/settingDrawer.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.setting.pagestyle': '整體風格設置', 3 | 'app.setting.pagestyle.dark': '暗色菜單風格', 4 | 'app.setting.pagestyle.light': '亮色菜單風格', 5 | 'app.setting.content-width': '內容區域寬度', 6 | 'app.setting.content-width.fixed': '定寬', 7 | 'app.setting.content-width.fluid': '流式', 8 | 'app.setting.themecolor': '主題色', 9 | 'app.setting.themecolor.dust': '薄暮', 10 | 'app.setting.themecolor.volcano': '火山', 11 | 'app.setting.themecolor.sunset': '日暮', 12 | 'app.setting.themecolor.cyan': '明青', 13 | 'app.setting.themecolor.green': '極光綠', 14 | 'app.setting.themecolor.daybreak': '拂曉藍(默認)', 15 | 'app.setting.themecolor.geekblue': '極客藍', 16 | 'app.setting.themecolor.purple': '醬紫', 17 | 'app.setting.navigationmode': '導航模式', 18 | 'app.setting.sidemenu': '側邊菜單布局', 19 | 'app.setting.topmenu': '頂部菜單布局', 20 | 'app.setting.fixedheader': '固定 Header', 21 | 'app.setting.fixedsidebar': '固定側邊菜單', 22 | 'app.setting.fixedsidebar.hint': '側邊菜單布局時可配置', 23 | 'app.setting.hideheader': '下滑時隱藏 Header', 24 | 'app.setting.hideheader.hint': '固定 Header 時可配置', 25 | 'app.setting.othersettings': '其他設置', 26 | 'app.setting.weakmode': '色弱模式', 27 | 'app.setting.copy': '拷貝設置', 28 | 'app.setting.copyinfo': '拷貝成功,請到 src/defaultSettings.js 中替換默認配置', 29 | 'app.setting.production.hint': 30 | '配置欄只在開發環境用於預覽,生產環境不會展現,請拷貝後手動修改配置文件', 31 | }; 32 | -------------------------------------------------------------------------------- /stock-server/src/modules/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { StUser } from '../../entities/StUser'; 3 | import { InjectRepository } from '@nestjs/typeorm'; 4 | import { Repository } from 'typeorm'; 5 | import { CreateUserDto } from './dto/user.dto'; 6 | // eslint-disable-next-line @typescript-eslint/no-var-requires 7 | const bcrypt = require('bcryptjs'); 8 | import { BusiException } from '../../libs/filters/busi.exception'; 9 | import { BusiErrorCode } from '../../libs/enums/error-code-enum'; 10 | 11 | @Injectable() 12 | export class UserService { 13 | constructor( 14 | @InjectRepository(StUser) 15 | private readonly stUserRepository: Repository, 16 | ) {} 17 | 18 | async create(dto: CreateUserDto) { 19 | const user = this.stUserRepository.create(dto); 20 | const salt = bcrypt.genSaltSync(10); 21 | user.salt = salt; 22 | user.password = bcrypt.hashSync(user.password, salt); 23 | return this.stUserRepository 24 | .save(user) 25 | .then((res) => { 26 | return { id: res.id }; 27 | }) 28 | .catch((err) => { 29 | throw new BusiException(BusiErrorCode.PARAM_ERROR, err.message); 30 | }); 31 | } 32 | 33 | async findByMobile(mobile: string): Promise { 34 | const result = await this.stUserRepository.findOne({ mobile }); 35 | return result; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /stock-web/tests/beforeTest.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | /* eslint-disable import/no-extraneous-dependencies */ 3 | const { execSync } = require('child_process'); 4 | const { join } = require('path'); 5 | const findChrome = require('carlo/lib/find_chrome'); 6 | const detectInstaller = require('detect-installer'); 7 | 8 | const installPuppeteer = () => { 9 | // find can use package manger 10 | const packages = detectInstaller(join(__dirname, '../../')); 11 | // get installed package manger 12 | const packageName = packages.find(detectInstaller.hasPackageCommand) || 'npm'; 13 | console.log(`🤖 will use ${packageName} install puppeteer`); 14 | const command = `${packageName} ${packageName.includes('yarn') ? 'add' : 'i'} puppeteer`; 15 | execSync(command, { 16 | stdio: 'inherit', 17 | }); 18 | }; 19 | 20 | const initPuppeteer = async () => { 21 | try { 22 | // eslint-disable-next-line import/no-unresolved 23 | const findChromePath = await findChrome({}); 24 | const { executablePath } = findChromePath; 25 | console.log(`🧲 find you browser in ${executablePath}`); 26 | return; 27 | } catch (error) { 28 | console.log('🧲 no find chrome'); 29 | } 30 | 31 | try { 32 | require.resolve('puppeteer'); 33 | } catch (error) { 34 | // need install puppeteer 35 | await installPuppeteer(); 36 | } 37 | }; 38 | 39 | initPuppeteer(); 40 | -------------------------------------------------------------------------------- /stock-server/src/modules/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Body, 5 | Post, 6 | HttpStatus, 7 | UseGuards, 8 | } from '@nestjs/common'; 9 | import { BusiException } from '../../libs/filters/busi.exception'; 10 | import { BusiErrorCode } from '../../libs/enums/error-code-enum'; 11 | import { UserService } from './user.service'; 12 | import { CreateUserDto } from './dto/user.dto'; 13 | import { AuthGuard } from '@nestjs/passport'; 14 | import { Roles } from '../../libs/decorators/role.decorator'; 15 | import { RolesGuard } from '../auth/guards/role.guard'; 16 | import { UserRole } from '../../libs/enums/role-enum'; 17 | 18 | @Controller('user') 19 | @UseGuards(AuthGuard('jwt')) 20 | export class UserController { 21 | constructor(private readonly userService: UserService) {} 22 | @Get('sayhi') 23 | async sayhi(): Promise { 24 | return '你好,世界'; 25 | } 26 | 27 | @Get('exception') 28 | async exception(): Promise { 29 | // throw new BusiException(BusiErrorCode.NOT_FOUND, '缺省状态码,默认触发http 400错误'); 30 | throw new BusiException( 31 | BusiErrorCode.NOT_FOUND, 32 | '错误:http状态正常', 33 | HttpStatus.OK, 34 | ); 35 | } 36 | 37 | @UseGuards(RolesGuard) 38 | @Roles(UserRole.ADMIN) 39 | @Post('create') 40 | async create(@Body() user: CreateUserDto) { 41 | return this.userService.create(user); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /stock-web/src/locales/ja-JP/settingDrawer.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.setting.pagestyle': 'ページスタイル設定', 3 | 'app.setting.pagestyle.dark': 'ダークスタイル', 4 | 'app.setting.pagestyle.light': 'ライトスタイル', 5 | 'app.setting.content-width': 'コンテンツの幅', 6 | 'app.setting.content-width.fixed': '固定', 7 | 'app.setting.content-width.fluid': '流体', 8 | 'app.setting.themecolor': 'テーマカラー', 9 | 'app.setting.themecolor.dust': 'ダストレッド', 10 | 'app.setting.themecolor.volcano': 'ボルケ-ノ', 11 | 'app.setting.themecolor.sunset': 'サンセットオレンジ', 12 | 'app.setting.themecolor.cyan': 'シアン', 13 | 'app.setting.themecolor.green': 'ポーラーグリーン', 14 | 'app.setting.themecolor.daybreak': '夜明けの青(デフォルト)', 15 | 'app.setting.themecolor.geekblue': 'ギーク ブルー', 16 | 'app.setting.themecolor.purple': 'ゴールデンパープル', 17 | 'app.setting.navigationmode': 'ナビゲーションモード', 18 | 'app.setting.sidemenu': 'サイドメニューのレイアウト', 19 | 'app.setting.topmenu': 'トップメニューのレイアウト', 20 | 'app.setting.fixedheader': '固定ヘッダー', 21 | 'app.setting.fixedsidebar': '固定サイドバー', 22 | 'app.setting.fixedsidebar.hint': 'サイドメニューのレイアウトで動作します', 23 | 'app.setting.hideheader': 'スクロール時の非表示ヘッダー', 24 | 'app.setting.hideheader.hint': '非表示ヘッダーが有効になっている場合に機能します', 25 | 'app.setting.othersettings': 'その他の設定', 26 | 'app.setting.weakmode': 'ウィークモード', 27 | 'app.setting.copy': 'コピー設定', 28 | 'app.setting.copyinfo': 29 | 'コピーが成功しました。src/models/setting.jsのdefaultSettingsを置き換えてください', 30 | 'app.setting.production.hint': '設定パネルは開発環境でのみ表示されます。手動で変更してください', 31 | }; 32 | -------------------------------------------------------------------------------- /stock-web/src/pages/Admin.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { HeartTwoTone, SmileTwoTone } from '@ant-design/icons'; 3 | import { Card, Typography, Alert } from 'antd'; 4 | import { PageHeaderWrapper } from '@ant-design/pro-layout'; 5 | import { useIntl } from 'umi'; 6 | 7 | export default (): React.ReactNode => { 8 | const intl = useIntl(); 9 | return ( 10 | 16 | 17 | 30 | 31 | Ant Design Pro You 32 | 33 | 34 |

35 | Want to add more pages? Please refer to{' '} 36 | 37 | use block 38 | 39 | 。 40 |

41 |
42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /stock-web/src/layouts/UserLayout.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .container { 4 | display: flex; 5 | flex-direction: column; 6 | height: 100vh; 7 | overflow: auto; 8 | background: @layout-body-background; 9 | } 10 | 11 | .lang { 12 | width: 100%; 13 | height: 40px; 14 | line-height: 44px; 15 | text-align: right; 16 | :global(.ant-dropdown-trigger) { 17 | margin-right: 24px; 18 | } 19 | } 20 | 21 | .content { 22 | flex: 1; 23 | padding: 32px 0; 24 | } 25 | 26 | @media (min-width: @screen-md-min) { 27 | .container { 28 | background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg'); 29 | background-repeat: no-repeat; 30 | background-position: center 110px; 31 | background-size: 100%; 32 | } 33 | 34 | .content { 35 | padding: 32px 0 24px; 36 | } 37 | } 38 | 39 | .top { 40 | text-align: center; 41 | } 42 | 43 | .header { 44 | height: 44px; 45 | line-height: 44px; 46 | a { 47 | text-decoration: none; 48 | } 49 | } 50 | 51 | .logo { 52 | height: 44px; 53 | margin-right: 16px; 54 | vertical-align: top; 55 | } 56 | 57 | .title { 58 | position: relative; 59 | top: 2px; 60 | color: @heading-color; 61 | font-weight: 600; 62 | font-size: 33px; 63 | font-family: Avenir, 'Helvetica Neue', Arial, Helvetica, sans-serif; 64 | } 65 | 66 | .desc { 67 | margin-top: 12px; 68 | margin-bottom: 40px; 69 | color: @text-color-secondary; 70 | font-size: @font-size-base; 71 | } 72 | -------------------------------------------------------------------------------- /stock-server/src/libs/pipes/params-validation.pipe.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArgumentMetadata, 3 | HttpStatus, 4 | Injectable, 5 | PipeTransform, 6 | } from '@nestjs/common'; 7 | import { plainToClass } from 'class-transformer'; 8 | import { BusiException } from '../filters/busi.exception'; 9 | import { BusiErrorCode } from '../enums/error-code-enum'; 10 | import { validate } from 'class-validator'; 11 | 12 | @Injectable() 13 | export class ParamsValidationPipe implements PipeTransform { 14 | async transform(value: any, metadata: ArgumentMetadata) { 15 | const { metatype } = metadata; 16 | 17 | // 如果参数不是 类 而是普通的 JavaScript 对象则不进行验证 18 | if (!metatype || !this.toValidate(metatype)) { 19 | return value; 20 | } 21 | 22 | // 通过元数据和对象实例,去构建原有类型 23 | const object = plainToClass(metatype, value); 24 | const errors = await validate(object); 25 | 26 | if (errors.length > 0) { 27 | console.log('错误提示:', JSON.stringify(errors)); 28 | // 获取到第一个没有通过验证的错误对象 29 | const error = errors.shift(); 30 | // 将未通过验证的字段的错误信息和状态码,以BusiException的形式抛给全局异常过滤器 31 | for (const key in error.constraints) { 32 | throw new BusiException( 33 | BusiErrorCode.PARAM_ERROR, 34 | error.constraints[key], 35 | HttpStatus.BAD_REQUEST, 36 | ); 37 | } 38 | } 39 | return value; 40 | } 41 | 42 | private toValidate(metatype): boolean { 43 | const types = [String, Boolean, Number, Array, Object]; 44 | return !types.find((type) => metatype === type); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /stock-server/src/modules/stock/stock.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Post, Body, UseGuards } from '@nestjs/common'; 2 | import { StockService } from './stock.service'; 3 | import { StockQueryDto } from './dto/stock.dto'; 4 | import { AuthGuard } from '@nestjs/passport'; 5 | import { RolesGuard } from '../auth/guards/role.guard'; 6 | import { User } from '../../libs/decorators/user.decorator'; 7 | import { ProfileInfo } from '../../libs/decorators/profileInfo'; 8 | import { UserStockDto } from './dto/stock.dto'; 9 | 10 | @Controller('stock') 11 | @UseGuards(AuthGuard('jwt')) 12 | export class StockController { 13 | constructor(private readonly userService: StockService) {} 14 | 15 | @UseGuards(RolesGuard) 16 | @Post('list') 17 | async getStockList(@Body() parmas: StockQueryDto, @User() user: ProfileInfo) { 18 | return this.userService.getStockList(user.uid, parmas); 19 | } 20 | 21 | @UseGuards(RolesGuard) 22 | @Post('add-choice') 23 | async addChoice(@Body() parmas: UserStockDto, @User() user: ProfileInfo) { 24 | return this.userService.addUserStock(user.uid, parmas.code); 25 | } 26 | 27 | @UseGuards(RolesGuard) 28 | @Post('user-list') 29 | async getUserStocts( 30 | @Body() parmas: StockQueryDto, 31 | @User() user: ProfileInfo, 32 | ) { 33 | return this.userService.getUserStocts(user.uid, parmas); 34 | } 35 | 36 | @UseGuards(RolesGuard) 37 | @Post('delete-choice') 38 | async deleteChoice(@Body() parmas: UserStockDto, @User() user: ProfileInfo) { 39 | return this.userService.deleteUserStock(user.uid, parmas.code); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /stock-server/src/modules/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { UserService } from '../user/user.service'; 3 | import { JwtService } from '@nestjs/jwt'; 4 | import { UserRole } from '../../libs/enums/role-enum'; 5 | // eslint-disable-next-line @typescript-eslint/no-var-requires 6 | const bcrypt = require('bcryptjs'); 7 | 8 | @Injectable() 9 | export class AuthService { 10 | constructor( 11 | private readonly usersService: UserService, 12 | private readonly jwtService: JwtService, 13 | ) {} 14 | 15 | async validateUser(mobile: string, pass: string): Promise { 16 | const user = await this.usersService.findByMobile(mobile); 17 | if (user) { 18 | const userpwd = bcrypt.hashSync(pass, user.salt); 19 | if (user.password === userpwd) { 20 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 21 | const { password, ...result } = user; 22 | return result; 23 | } 24 | } 25 | return null; 26 | } 27 | 28 | async login(user: any) { 29 | let roles = []; 30 | if (user.role === 100) { 31 | roles = [UserRole.ADMIN]; 32 | } else { 33 | roles = [UserRole.USER]; 34 | } 35 | const payload = { 36 | id: user.id, 37 | mobile: user.mobile, 38 | sub: user.id, 39 | roles, 40 | name: user.name, 41 | avatar: user.avatar, 42 | }; 43 | return { 44 | id: user.id, 45 | name: user.name, 46 | mobile: user.mobile, 47 | roles, 48 | avatar: user.avatar, 49 | accessToken: this.jwtService.sign(payload), 50 | }; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /stock-web/src/utils/authority.ts: -------------------------------------------------------------------------------- 1 | import { reloadAuthorized } from './Authorized'; 2 | 3 | // use localStorage to store the authority info, which might be sent from server in actual project. 4 | export function getAuthority(str?: string): string | string[] { 5 | const authorityString = 6 | typeof str === 'undefined' && localStorage ? localStorage.getItem('user-role') : str; 7 | // authorityString could be admin, "admin", ["admin"] 8 | let authority; 9 | try { 10 | if (authorityString) { 11 | authority = JSON.parse(authorityString); 12 | } 13 | } catch (e) { 14 | authority = authorityString; 15 | } 16 | if (typeof authority === 'string') { 17 | return [authority]; 18 | } 19 | // preview.pro.ant.design only do not use in your production. 20 | // preview.pro.ant.design 专用环境变量,请不要在你的项目中使用它。 21 | if (!authority && ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site') { 22 | return ['admin']; 23 | } 24 | return authority; 25 | } 26 | /** 27 | * 获取用户登录的token 28 | */ 29 | export const getAuthToken = () => { 30 | // console.log(typeof localStorage.getItem('user-token')); 31 | return localStorage ? localStorage.getItem('user-token') : ''; 32 | }; 33 | 34 | export function setAuthority(response: any): void { 35 | if (response.code === 0) { 36 | // const proAuthority = typeof response.data.roles === 'string' ? [response.data.roles] : response.data.roles; 37 | localStorage.setItem('user-role', JSON.stringify(response.data.roles)); 38 | localStorage.setItem('user-token', response.data.accessToken); 39 | // auto reload 40 | reloadAuthorized(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /stock-server/src/entities/StUser.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | Entity, 4 | Index, 5 | OneToMany, 6 | PrimaryGeneratedColumn, 7 | } from 'typeorm'; 8 | import { UserStock } from './UserStock'; 9 | 10 | @Index('uk_mobile', ['mobile'], { unique: true }) 11 | @Entity('st_user', { schema: 'stock_demo' }) 12 | export class StUser { 13 | @PrimaryGeneratedColumn({ type: 'int', name: 'id', comment: '主键id' }) 14 | id: number; 15 | 16 | @Column('varchar', { 17 | name: 'mobile', 18 | unique: true, 19 | comment: '手机号', 20 | length: 20, 21 | }) 22 | mobile: string; 23 | 24 | @Column('varchar', { name: 'name', comment: '昵称', length: 20 }) 25 | name: string; 26 | 27 | @Column('tinyint', { 28 | name: 'role', 29 | comment: '角色:100 超级管理员,0 普通用户', 30 | width: 1, 31 | default: () => "'0'", 32 | }) 33 | role: boolean; 34 | 35 | @Column('varchar', { name: 'password', comment: '密码', length: 100 }) 36 | password: string; 37 | 38 | @Column('varchar', { name: 'salt', comment: '密码盐', length: 100 }) 39 | salt: string; 40 | 41 | @Column('datetime', { 42 | name: 'create_dt', 43 | comment: '创建时间', 44 | default: () => 'CURRENT_TIMESTAMP', 45 | }) 46 | createDt: Date; 47 | 48 | @Column('timestamp', { 49 | name: 'update_dt', 50 | nullable: true, 51 | comment: '修改时间', 52 | }) 53 | updateDt: Date | null; 54 | 55 | @Column('tinyint', { 56 | name: 'is_delete', 57 | comment: '是否删除', 58 | width: 1, 59 | default: () => "'0'", 60 | }) 61 | isDelete: boolean; 62 | 63 | @OneToMany(() => UserStock, (userStock) => userStock.u) 64 | userStocks: UserStock[]; 65 | } 66 | -------------------------------------------------------------------------------- /stock-server/src/entities/StockLog.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'; 2 | 3 | @Index('uk_code_log_date', ['code', 'logDate'], { unique: true }) 4 | @Index('idx_code', ['code'], {}) 5 | @Entity('stock_log', { schema: 'stock_demo' }) 6 | export class StockLog { 7 | @PrimaryGeneratedColumn({ type: 'int', name: 'id', comment: '主键id' }) 8 | id: number; 9 | 10 | @Column('varchar', { name: 'code', comment: '股票代码', length: 10 }) 11 | code: string; 12 | 13 | @Column('date', { name: 'log_date', comment: '日期' }) 14 | logDate: string; 15 | 16 | @Column('decimal', { 17 | name: 'pe', 18 | comment: '当前PE(市盈率)', 19 | precision: 12, 20 | scale: 4, 21 | default: () => "'0.0000'", 22 | }) 23 | pe: string; 24 | 25 | @Column('decimal', { 26 | name: 'pe_ttm', 27 | comment: '当前PE TTM(市盈率)', 28 | precision: 12, 29 | scale: 4, 30 | default: () => "'0.0000'", 31 | }) 32 | peTtm: string; 33 | 34 | @Column('decimal', { 35 | name: 'total_mv', 36 | comment: '总市值', 37 | precision: 16, 38 | scale: 4, 39 | default: () => "'0.0000'", 40 | }) 41 | totalMv: string; 42 | 43 | @Column('datetime', { 44 | name: 'create_dt', 45 | comment: '创建时间', 46 | default: () => 'CURRENT_TIMESTAMP', 47 | }) 48 | createDt: Date; 49 | 50 | @Column('timestamp', { 51 | name: 'update_dt', 52 | nullable: true, 53 | comment: '修改时间', 54 | }) 55 | updateDt: Date | null; 56 | 57 | @Column('tinyint', { 58 | name: 'is_delete', 59 | comment: '是否删除', 60 | width: 1, 61 | default: () => "'0'", 62 | }) 63 | isDelete: boolean; 64 | } 65 | -------------------------------------------------------------------------------- /stock-server/src/entities/UserStock.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | Entity, 4 | Index, 5 | JoinColumn, 6 | ManyToOne, 7 | PrimaryGeneratedColumn, 8 | } from 'typeorm'; 9 | import { StUser } from './StUser'; 10 | import { Stock } from './Stock'; 11 | 12 | @Index('uk_uid_code', ['uid', 'code'], { unique: true }) 13 | @Index('code', ['code'], {}) 14 | @Entity('user_stock', { schema: 'stock_demo' }) 15 | export class UserStock { 16 | @PrimaryGeneratedColumn({ type: 'int', name: 'id', comment: '主键id' }) 17 | id: number; 18 | 19 | @Column('int', { name: 'uid', comment: '用户id', default: () => "'0'" }) 20 | uid: number; 21 | 22 | @Column('varchar', { name: 'code', comment: '股票代码', length: 10 }) 23 | code: string; 24 | 25 | @Column('datetime', { 26 | name: 'create_dt', 27 | comment: '创建时间', 28 | default: () => 'CURRENT_TIMESTAMP', 29 | }) 30 | createDt: Date; 31 | 32 | @Column('timestamp', { 33 | name: 'update_dt', 34 | nullable: true, 35 | comment: '修改时间', 36 | }) 37 | updateDt: Date | null; 38 | 39 | @Column('tinyint', { 40 | name: 'is_delete', 41 | comment: '是否删除', 42 | width: 1, 43 | default: () => "'0'", 44 | }) 45 | isDelete: boolean; 46 | 47 | @ManyToOne(() => StUser, (stUser) => stUser.userStocks, { 48 | onDelete: 'RESTRICT', 49 | onUpdate: 'RESTRICT', 50 | }) 51 | @JoinColumn([{ name: 'uid', referencedColumnName: 'id' }]) 52 | u: StUser; 53 | 54 | @ManyToOne(() => Stock, (stock) => stock.userStocks, { 55 | onDelete: 'RESTRICT', 56 | onUpdate: 'RESTRICT', 57 | }) 58 | @JoinColumn([{ name: 'code', referencedColumnName: 'code' }]) 59 | code2: Stock; 60 | } 61 | -------------------------------------------------------------------------------- /stock-web/src/locales/en-US/settingDrawer.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.setting.pagestyle': 'Page style setting', 3 | 'app.setting.pagestyle.dark': 'Dark style', 4 | 'app.setting.pagestyle.light': 'Light style', 5 | 'app.setting.content-width': 'Content Width', 6 | 'app.setting.content-width.fixed': 'Fixed', 7 | 'app.setting.content-width.fluid': 'Fluid', 8 | 'app.setting.themecolor': 'Theme Color', 9 | 'app.setting.themecolor.dust': 'Dust Red', 10 | 'app.setting.themecolor.volcano': 'Volcano', 11 | 'app.setting.themecolor.sunset': 'Sunset Orange', 12 | 'app.setting.themecolor.cyan': 'Cyan', 13 | 'app.setting.themecolor.green': 'Polar Green', 14 | 'app.setting.themecolor.daybreak': 'Daybreak Blue (default)', 15 | 'app.setting.themecolor.geekblue': 'Geek Glue', 16 | 'app.setting.themecolor.purple': 'Golden Purple', 17 | 'app.setting.navigationmode': 'Navigation Mode', 18 | 'app.setting.sidemenu': 'Side Menu Layout', 19 | 'app.setting.topmenu': 'Top Menu Layout', 20 | 'app.setting.fixedheader': 'Fixed Header', 21 | 'app.setting.fixedsidebar': 'Fixed Sidebar', 22 | 'app.setting.fixedsidebar.hint': 'Works on Side Menu Layout', 23 | 'app.setting.hideheader': 'Hidden Header when scrolling', 24 | 'app.setting.hideheader.hint': 'Works when Hidden Header is enabled', 25 | 'app.setting.othersettings': 'Other Settings', 26 | 'app.setting.weakmode': 'Weak Mode', 27 | 'app.setting.copy': 'Copy Setting', 28 | 'app.setting.copyinfo': 'copy success,please replace defaultSettings in src/models/setting.js', 29 | 'app.setting.production.hint': 30 | 'Setting panel shows in development environment only, please manually modify', 31 | }; 32 | -------------------------------------------------------------------------------- /stock-web/src/utils/request.ts: -------------------------------------------------------------------------------- 1 | /** Request 网络请求工具 更详细的 api 文档: https://github.com/umijs/umi-request */ 2 | import { extend } from 'umi-request'; 3 | import { notification } from 'antd'; 4 | // import { getAuthToken } from '@/utils/authority'; 5 | 6 | const codeMessage: { [status: number]: string } = { 7 | 200: '服务器成功返回请求的数据。', 8 | 201: '新建或修改数据成功。', 9 | 202: '一个请求已经进入后台排队(异步任务)。', 10 | 204: '删除数据成功。', 11 | 400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。', 12 | 401: '用户没有权限(令牌、用户名、密码错误)。', 13 | 403: '用户得到授权,但是访问是被禁止的。', 14 | 404: '发出的请求针对的是不存在的记录,服务器没有进行操作。', 15 | 406: '请求的格式不可得。', 16 | 410: '请求的资源被永久删除,且不会再得到的。', 17 | 422: '当创建一个对象时,发生一个验证错误。', 18 | 500: '服务器发生错误,请检查服务器。', 19 | 502: '网关错误。', 20 | 503: '服务不可用,服务器暂时过载或维护。', 21 | 504: '网关超时。', 22 | }; 23 | 24 | /** 异常处理程序 */ 25 | const errorHandler = (error: { response: Response }): Response => { 26 | const { response } = error; 27 | if (response && response.status) { 28 | const errorText = codeMessage[response.status] || response.statusText; 29 | const { status, url } = response; 30 | 31 | notification.error({ 32 | message: `请求错误 ${status}: ${url}`, 33 | description: errorText, 34 | }); 35 | } else if (!response) { 36 | notification.error({ 37 | description: '您的网络发生异常,无法连接服务器', 38 | message: '网络异常', 39 | }); 40 | } 41 | return response; 42 | }; 43 | 44 | /** 配置request请求时的默认参数 */ 45 | const request = extend({ 46 | errorHandler, // 默认错误处理 47 | // credentials: 'include', // 默认请求是否带上cookie 48 | // prefix: API_SERVER, 49 | headers: { 50 | Authorization: `Bearer ${(window as any).userToken}`, // `Bearer ${getAuthToken()}`, 51 | }, 52 | }); 53 | 54 | export default request; 55 | -------------------------------------------------------------------------------- /stock-web/tests/run-tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable eslint-comments/disable-enable-pair */ 2 | /* eslint-disable @typescript-eslint/no-var-requires */ 3 | /* eslint-disable eslint-comments/no-unlimited-disable */ 4 | const { spawn } = require('child_process'); 5 | // eslint-disable-next-line import/no-extraneous-dependencies 6 | const { kill } = require('cross-port-killer'); 7 | 8 | const env = Object.create(process.env); 9 | env.BROWSER = 'none'; 10 | env.TEST = true; 11 | env.UMI_UI = 'none'; 12 | env.PROGRESS = 'none'; 13 | // flag to prevent multiple test 14 | let once = false; 15 | 16 | const startServer = spawn(/^win/.test(process.platform) ? 'npm.cmd' : 'npm', ['start'], { 17 | env, 18 | }); 19 | 20 | startServer.stderr.on('data', (data) => { 21 | // eslint-disable-next-line 22 | console.log(data.toString()); 23 | }); 24 | 25 | startServer.on('exit', () => { 26 | kill(process.env.PORT || 8000); 27 | }); 28 | 29 | console.log('Starting development server for e2e tests...'); 30 | startServer.stdout.on('data', (data) => { 31 | console.log(data.toString()); 32 | // hack code , wait umi 33 | if ( 34 | (!once && data.toString().indexOf('Compiled successfully') >= 0) || 35 | data.toString().indexOf('Theme generated successfully') >= 0 36 | ) { 37 | // eslint-disable-next-line 38 | once = true; 39 | console.log('Development server is started, ready to run tests.'); 40 | const testCmd = spawn( 41 | /^win/.test(process.platform) ? 'npm.cmd' : 'npm', 42 | ['test', '--', '--maxWorkers=1', '--runInBand'], 43 | { 44 | stdio: 'inherit', 45 | }, 46 | ); 47 | testCmd.on('exit', (code) => { 48 | startServer.kill(); 49 | process.exit(code); 50 | }); 51 | } 52 | }); 53 | -------------------------------------------------------------------------------- /stock-web/src/locales/id-ID/settingDrawer.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.setting.pagestyle': 'Pengaturan style Halaman', 3 | 'app.setting.pagestyle.dark': 'Style Gelap', 4 | 'app.setting.pagestyle.light': 'Style Cerah', 5 | 'app.setting.content-width': 'Lebar Konten', 6 | 'app.setting.content-width.fixed': 'Tetap', 7 | 'app.setting.content-width.fluid': 'Fluid', 8 | 'app.setting.themecolor': 'Theme Color', 9 | 'app.setting.themecolor.dust': 'Dust Red', 10 | 'app.setting.themecolor.volcano': 'Volcano', 11 | 'app.setting.themecolor.sunset': 'Sunset Orange', 12 | 'app.setting.themecolor.cyan': 'Cyan', 13 | 'app.setting.themecolor.green': 'Polar Green', 14 | 'app.setting.themecolor.daybreak': 'Daybreak Blue (bawaan)', 15 | 'app.setting.themecolor.geekblue': 'Geek Glue', 16 | 'app.setting.themecolor.purple': 'Golden Purple', 17 | 'app.setting.navigationmode': 'Mode Navigasi', 18 | 'app.setting.sidemenu': 'Susunan Menu Samping', 19 | 'app.setting.topmenu': 'Susunan Menu Atas', 20 | 'app.setting.fixedheader': 'Header Tetap', 21 | 'app.setting.fixedsidebar': 'Sidebar Tetap', 22 | 'app.setting.fixedsidebar.hint': 'Berjalan pada Susunan Menu Samping', 23 | 'app.setting.hideheader': 'Sembunyikan Header ketika gulir ke bawah', 24 | 'app.setting.hideheader.hint': 'Bekerja ketika Header tersembunyi dimunculkan', 25 | 'app.setting.othersettings': 'Pengaturan Lainnya', 26 | 'app.setting.weakmode': 'Mode Lemah', 27 | 'app.setting.copy': 'Salin Pengaturan', 28 | 'app.setting.copyinfo': 29 | 'Berhasil disalin,tolong ubah defaultSettings pada src/models/setting.js', 30 | 'app.setting.production.hint': 31 | 'Panel pengaturan hanya muncul pada lingkungan pengembangan, silahkan modifikasi secara menual', 32 | }; 33 | -------------------------------------------------------------------------------- /stock-web/src/locales/pt-BR/settingDrawer.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.setting.pagestyle': 'Configuração de estilo da página', 3 | 'app.setting.pagestyle.dark': 'Dark style', 4 | 'app.setting.pagestyle.light': 'Light style', 5 | 'app.setting.content-width': 'Largura do conteúdo', 6 | 'app.setting.content-width.fixed': 'Fixo', 7 | 'app.setting.content-width.fluid': 'Fluido', 8 | 'app.setting.themecolor': 'Cor do Tema', 9 | 'app.setting.themecolor.dust': 'Dust Red', 10 | 'app.setting.themecolor.volcano': 'Volcano', 11 | 'app.setting.themecolor.sunset': 'Sunset Orange', 12 | 'app.setting.themecolor.cyan': 'Cyan', 13 | 'app.setting.themecolor.green': 'Polar Green', 14 | 'app.setting.themecolor.daybreak': 'Daybreak Blue (default)', 15 | 'app.setting.themecolor.geekblue': 'Geek Glue', 16 | 'app.setting.themecolor.purple': 'Golden Purple', 17 | 'app.setting.navigationmode': 'Modo de Navegação', 18 | 'app.setting.sidemenu': 'Layout do Menu Lateral', 19 | 'app.setting.topmenu': 'Layout do Menu Superior', 20 | 'app.setting.fixedheader': 'Cabeçalho fixo', 21 | 'app.setting.fixedsidebar': 'Barra lateral fixa', 22 | 'app.setting.fixedsidebar.hint': 'Funciona no layout do menu lateral', 23 | 'app.setting.hideheader': 'Esconder o cabeçalho quando rolar', 24 | 'app.setting.hideheader.hint': 'Funciona quando o esconder cabeçalho está abilitado', 25 | 'app.setting.othersettings': 'Outras configurações', 26 | 'app.setting.weakmode': 'Weak Mode', 27 | 'app.setting.copy': 'Copiar Configuração', 28 | 'app.setting.copyinfo': 29 | 'copiado com sucesso,por favor trocar o defaultSettings em src/models/setting.js', 30 | 'app.setting.production.hint': 31 | 'O painel de configuração apenas é exibido no ambiente de desenvolvimento, por favor modifique manualmente o', 32 | }; 33 | -------------------------------------------------------------------------------- /stock-web/src/utils/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { isUrl } from './utils'; 2 | 3 | describe('isUrl tests', (): void => { 4 | it('should return false for invalid and corner case inputs', (): void => { 5 | expect(isUrl([] as any)).toBeFalsy(); 6 | expect(isUrl({} as any)).toBeFalsy(); 7 | expect(isUrl(false as any)).toBeFalsy(); 8 | expect(isUrl(true as any)).toBeFalsy(); 9 | expect(isUrl(NaN as any)).toBeFalsy(); 10 | expect(isUrl(null as any)).toBeFalsy(); 11 | expect(isUrl(undefined as any)).toBeFalsy(); 12 | expect(isUrl('')).toBeFalsy(); 13 | }); 14 | 15 | it('should return false for invalid URLs', (): void => { 16 | expect(isUrl('foo')).toBeFalsy(); 17 | expect(isUrl('bar')).toBeFalsy(); 18 | expect(isUrl('bar/test')).toBeFalsy(); 19 | expect(isUrl('http:/example.com/')).toBeFalsy(); 20 | expect(isUrl('ttp://example.com/')).toBeFalsy(); 21 | }); 22 | 23 | it('should return true for valid URLs', (): void => { 24 | expect(isUrl('http://example.com/')).toBeTruthy(); 25 | expect(isUrl('https://example.com/')).toBeTruthy(); 26 | expect(isUrl('http://example.com/test/123')).toBeTruthy(); 27 | expect(isUrl('https://example.com/test/123')).toBeTruthy(); 28 | expect(isUrl('http://example.com/test/123?foo=bar')).toBeTruthy(); 29 | expect(isUrl('https://example.com/test/123?foo=bar')).toBeTruthy(); 30 | expect(isUrl('http://www.example.com/')).toBeTruthy(); 31 | expect(isUrl('https://www.example.com/')).toBeTruthy(); 32 | expect(isUrl('http://www.example.com/test/123')).toBeTruthy(); 33 | expect(isUrl('https://www.example.com/test/123')).toBeTruthy(); 34 | expect(isUrl('http://www.example.com/test/123?foo=bar')).toBeTruthy(); 35 | expect(isUrl('https://www.example.com/test/123?foo=bar')).toBeTruthy(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /stock-web/src/components/GlobalHeader/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | @pro-header-hover-bg: rgba(0, 0, 0, 0.025); 4 | 5 | .menu { 6 | :global(.anticon) { 7 | margin-right: 8px; 8 | } 9 | :global(.ant-dropdown-menu-item) { 10 | min-width: 160px; 11 | } 12 | } 13 | 14 | .right { 15 | display: flex; 16 | float: right; 17 | height: 48px; 18 | margin-left: auto; 19 | overflow: hidden; 20 | .action { 21 | display: flex; 22 | align-items: center; 23 | height: 100%; 24 | padding: 0 12px; 25 | cursor: pointer; 26 | transition: all 0.3s; 27 | > span { 28 | vertical-align: middle; 29 | } 30 | &:hover { 31 | background: @pro-header-hover-bg; 32 | } 33 | &:global(.opened) { 34 | background: @pro-header-hover-bg; 35 | } 36 | } 37 | .search { 38 | padding: 0 12px; 39 | &:hover { 40 | background: transparent; 41 | } 42 | } 43 | .account { 44 | .avatar { 45 | margin: ~'calc((@{layout-header-height} - 24px) / 2)' 0; 46 | margin-right: 8px; 47 | color: @primary-color; 48 | vertical-align: top; 49 | background: rgba(255, 255, 255, 0.85); 50 | } 51 | } 52 | } 53 | 54 | .dark { 55 | .action { 56 | color: rgba(255, 255, 255, 0.85); 57 | > span { 58 | color: rgba(255, 255, 255, 0.85); 59 | } 60 | &:hover, 61 | &:global(.opened) { 62 | background: @primary-color; 63 | } 64 | } 65 | } 66 | 67 | :global(.ant-pro-global-header) { 68 | .dark { 69 | .action { 70 | color: @text-color; 71 | > span { 72 | color: @text-color; 73 | } 74 | &:hover { 75 | color: rgba(255, 255, 255, 0.85); 76 | > span { 77 | color: rgba(255, 255, 255, 0.85); 78 | } 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /stock-web/src/layouts/SecurityLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PageLoading } from '@ant-design/pro-layout'; 3 | import type { ConnectProps } from 'umi'; 4 | import { Redirect, connect } from 'umi'; 5 | import { stringify } from 'querystring'; 6 | import type { ConnectState } from '@/models/connect'; 7 | import type { CurrentUser } from '@/models/user'; 8 | 9 | type SecurityLayoutProps = { 10 | loading?: boolean; 11 | currentUser?: CurrentUser; 12 | } & ConnectProps; 13 | 14 | type SecurityLayoutState = { 15 | isReady: boolean; 16 | }; 17 | 18 | class SecurityLayout extends React.Component { 19 | state: SecurityLayoutState = { 20 | isReady: false, 21 | }; 22 | 23 | componentDidMount() { 24 | this.setState({ 25 | isReady: true, 26 | }); 27 | const { dispatch } = this.props; 28 | if (dispatch) { 29 | dispatch({ 30 | type: 'user/fetchCurrent', 31 | }); 32 | } 33 | } 34 | 35 | render() { 36 | const { isReady } = this.state; 37 | const { children, loading, currentUser } = this.props; 38 | // You can replace it to your authentication rule (such as check token exists) 39 | // 你可以把它替换成你自己的登录认证规则(比如判断 token 是否存在) 40 | // console.log('登录用户是:', currentUser); 41 | const isLogin = currentUser && currentUser.uid; 42 | const queryString = stringify({ 43 | redirect: window.location.href, 44 | }); 45 | 46 | if ((!isLogin && loading) || !isReady) { 47 | return ; 48 | } 49 | if (!isLogin && window.location.pathname !== '/login') { 50 | return ; 51 | } 52 | return children; 53 | } 54 | } 55 | 56 | export default connect(({ user, loading }: ConnectState) => ({ 57 | currentUser: user.currentUser, 58 | loading: loading.models.user, 59 | }))(SecurityLayout); 60 | -------------------------------------------------------------------------------- /stock-web/src/locales/zh-TW/menu.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.welcome': '歡迎', 3 | 'menu.more-blocks': '更多區塊', 4 | 'menu.home': '首頁', 5 | 'menu.login': '登錄', 6 | 'menu.admin': '权限', 7 | 'menu.admin.sub-page': '二级管理页', 8 | 'menu.exception.403': '403', 9 | 'menu.exception.404': '404', 10 | 'menu.exception.500': '500', 11 | 'menu.register': '註冊', 12 | 'menu.register.result': '註冊結果', 13 | 'menu.dashboard': 'Dashboard', 14 | 'menu.dashboard.analysis': '分析頁', 15 | 'menu.dashboard.monitor': '監控頁', 16 | 'menu.dashboard.workplace': '工作臺', 17 | 'menu.form': '表單頁', 18 | 'menu.form.basic-form': '基礎表單', 19 | 'menu.form.step-form': '分步表單', 20 | 'menu.form.step-form.info': '分步表單(填寫轉賬信息)', 21 | 'menu.form.step-form.confirm': '分步表單(確認轉賬信息)', 22 | 'menu.form.step-form.result': '分步表單(完成)', 23 | 'menu.form.advanced-form': '高級表單', 24 | 'menu.list': '列表頁', 25 | 'menu.list.table-list': '查詢表格', 26 | 'menu.list.basic-list': '標淮列表', 27 | 'menu.list.card-list': '卡片列表', 28 | 'menu.list.search-list': '搜索列表', 29 | 'menu.list.search-list.articles': '搜索列表(文章)', 30 | 'menu.list.search-list.projects': '搜索列表(項目)', 31 | 'menu.list.search-list.applications': '搜索列表(應用)', 32 | 'menu.profile': '詳情頁', 33 | 'menu.profile.basic': '基礎詳情頁', 34 | 'menu.profile.advanced': '高級詳情頁', 35 | 'menu.result': '結果頁', 36 | 'menu.result.success': '成功頁', 37 | 'menu.result.fail': '失敗頁', 38 | 'menu.account': '個人頁', 39 | 'menu.account.center': '個人中心', 40 | 'menu.account.settings': '個人設置', 41 | 'menu.account.trigger': '觸發報錯', 42 | 'menu.account.logout': '退出登錄', 43 | 'menu.exception': '异常页', 44 | 'menu.exception.not-permission': '403', 45 | 'menu.exception.not-find': '404', 46 | 'menu.exception.server-error': '500', 47 | 'menu.exception.trigger': '触发错误', 48 | 'menu.editor': '圖形編輯器', 49 | 'menu.editor.flow': '流程編輯器', 50 | 'menu.editor.mind': '腦圖編輯器', 51 | 'menu.editor.koni': '拓撲編輯器', 52 | }; 53 | -------------------------------------------------------------------------------- /stock-web/src/e2e/baseLayout.e2e.js: -------------------------------------------------------------------------------- 1 | const { uniq } = require('lodash'); 2 | const RouterConfig = require('../../config/config').default.routes; 3 | 4 | const BASE_URL = `http://localhost:${process.env.PORT || 8000}`; 5 | 6 | function formatter(routes, parentPath = '') { 7 | const fixedParentPath = parentPath.replace(/\/{1,}/g, '/'); 8 | let result = []; 9 | routes.forEach((item) => { 10 | if (item.path) { 11 | result.push(`${fixedParentPath}/${item.path}`.replace(/\/{1,}/g, '/')); 12 | } 13 | if (item.routes) { 14 | result = result.concat( 15 | formatter(item.routes, item.path ? `${fixedParentPath}/${item.path}` : parentPath), 16 | ); 17 | } 18 | }); 19 | return uniq(result.filter((item) => !!item)); 20 | } 21 | 22 | beforeEach(async () => { 23 | await page.goto(`${BASE_URL}`); 24 | await page.evaluate(() => { 25 | localStorage.setItem('antd-pro-authority', '["admin"]'); 26 | }); 27 | }); 28 | 29 | describe('Ant Design Pro E2E test', () => { 30 | const testPage = (path) => async () => { 31 | await page.goto(`${BASE_URL}${path}`); 32 | await page.waitForSelector('footer', { 33 | timeout: 2000, 34 | }); 35 | const haveFooter = await page.evaluate( 36 | () => document.getElementsByTagName('footer').length > 0, 37 | ); 38 | expect(haveFooter).toBeTruthy(); 39 | }; 40 | 41 | const routers = formatter(RouterConfig); 42 | routers.forEach((route) => { 43 | it(`test pages ${route}`, testPage(route)); 44 | }); 45 | 46 | it('topmenu should have footer', async () => { 47 | const params = '?navTheme=light&layout=topmenu'; 48 | await page.goto(`${BASE_URL}${params}`); 49 | await page.waitForSelector('footer', { 50 | timeout: 2000, 51 | }); 52 | const haveFooter = await page.evaluate( 53 | () => document.getElementsByTagName('footer').length > 0, 54 | ); 55 | expect(haveFooter).toBeTruthy(); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /stock-web/src/locales/ja-JP/menu.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.welcome': 'ようこそ', 3 | 'menu.more-blocks': 'その他のブロック', 4 | 'menu.home': 'ホーム', 5 | 'menu.admin': '管理者', 6 | 'menu.admin.sub-page': 'サブページ', 7 | 'menu.login': 'ログイン', 8 | 'menu.register': '登録', 9 | 'menu.register.result': '登録結果', 10 | 'menu.dashboard': 'ダッシュボード', 11 | 'menu.dashboard.analysis': '分析', 12 | 'menu.dashboard.monitor': 'モニター', 13 | 'menu.dashboard.workplace': '職場', 14 | 'menu.exception.403': '403', 15 | 'menu.exception.404': '404', 16 | 'menu.exception.500': '500', 17 | 'menu.form': 'フォーム', 18 | 'menu.form.basic-form': '基本フォーム', 19 | 'menu.form.step-form': 'ステップフォーム', 20 | 'menu.form.step-form.info': 'ステップフォーム(転送情報の書き込み)', 21 | 'menu.form.step-form.confirm': 'ステップフォーム(転送情報の確認)', 22 | 'menu.form.step-form.result': 'ステップフォーム(完成)', 23 | 'menu.form.advanced-form': '高度なフォーム', 24 | 'menu.list': 'リスト', 25 | 'menu.list.table-list': '検索テーブル', 26 | 'menu.list.basic-list': '基本リスト', 27 | 'menu.list.card-list': 'カードリスト', 28 | 'menu.list.search-list': '検索リスト', 29 | 'menu.list.search-list.articles': '検索リスト(記事)', 30 | 'menu.list.search-list.projects': '検索リスト(プロジェクト)', 31 | 'menu.list.search-list.applications': '検索リスト(アプリ)', 32 | 'menu.profile': 'プロフィール', 33 | 'menu.profile.basic': '基本プロフィール', 34 | 'menu.profile.advanced': '高度なプロフィール', 35 | 'menu.result': '結果', 36 | 'menu.result.success': '成功', 37 | 'menu.result.fail': '失敗', 38 | 'menu.exception': '例外', 39 | 'menu.exception.not-permission': '403', 40 | 'menu.exception.not-find': '404', 41 | 'menu.exception.server-error': '500', 42 | 'menu.exception.trigger': 'トリガー', 43 | 'menu.account': 'アカウント', 44 | 'menu.account.center': 'アカウントセンター', 45 | 'menu.account.settings': 'アカウント設定', 46 | 'menu.account.trigger': 'トリガーエラー', 47 | 'menu.account.logout': 'ログアウト', 48 | 'menu.editor': 'グラフィックエディタ', 49 | 'menu.editor.flow': 'フローエディタ', 50 | 'menu.editor.mind': 'マインドエディター', 51 | 'menu.editor.koni': 'コニエディター', 52 | }; 53 | -------------------------------------------------------------------------------- /stock-web/src/locales/zh-CN/menu.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.welcome': '欢迎使用', 3 | 'menu.more-blocks': '更多区块', 4 | 'menu.home': '首页', 5 | 'menu.admin': '管理页面', 6 | 'menu.admin.sub-page': '权限管理', 7 | 'menu.login': '登录', 8 | 'menu.register': '注册', 9 | 'menu.register.result': '注册结果', 10 | 'menu.dashboard': 'Dashboard', 11 | 'menu.dashboard.analysis': '分析页', 12 | 'menu.dashboard.monitor': '监控页', 13 | 'menu.dashboard.workplace': '工作台', 14 | 'menu.exception.403': '403', 15 | 'menu.exception.404': '404', 16 | 'menu.exception.500': '500', 17 | 'menu.form': '表单页', 18 | 'menu.form.basic-form': '基础表单', 19 | 'menu.form.step-form': '分步表单', 20 | 'menu.form.step-form.info': '分步表单(填写转账信息)', 21 | 'menu.form.step-form.confirm': '分步表单(确认转账信息)', 22 | 'menu.form.step-form.result': '分步表单(完成)', 23 | 'menu.form.advanced-form': '高级表单', 24 | 'menu.list': '列表页', 25 | 'menu.list.table-list': '查询表格', 26 | 'menu.list.basic-list': '标准列表', 27 | 'menu.list.card-list': '卡片列表', 28 | 'menu.list.search-list': '搜索列表', 29 | 'menu.list.search-list.articles': '搜索列表(文章)', 30 | 'menu.list.search-list.projects': '搜索列表(项目)', 31 | 'menu.list.search-list.applications': '搜索列表(应用)', 32 | 'menu.profile': '详情页', 33 | 'menu.profile.basic': '基础详情页', 34 | 'menu.profile.advanced': '高级详情页', 35 | 'menu.result': '结果页', 36 | 'menu.result.success': '成功页', 37 | 'menu.result.fail': '失败页', 38 | 'menu.exception': '异常页', 39 | 'menu.exception.not-permission': '403', 40 | 'menu.exception.not-find': '404', 41 | 'menu.exception.server-error': '500', 42 | 'menu.exception.trigger': '触发错误', 43 | 'menu.account': '个人页', 44 | 'menu.account.center': '个人中心', 45 | 'menu.account.settings': '个人设置', 46 | 'menu.account.trigger': '触发报错', 47 | 'menu.account.logout': '退出登录', 48 | 'menu.editor': '图形编辑器', 49 | 'menu.editor.flow': '流程编辑器', 50 | 'menu.editor.mind': '脑图编辑器', 51 | 'menu.editor.koni': '拓扑编辑器', 52 | 'menu.list.stock-list': '股票列表', 53 | 'menu.list.user-stock-list': '我的自选', 54 | }; 55 | -------------------------------------------------------------------------------- /stock-web/src/services/fetch.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { message } from 'antd'; 3 | import { getAuthToken } from '@/utils/authority'; 4 | 5 | class HttpClient { 6 | static historyPush = () => {}; 7 | 8 | static setBefore(hook: any) { 9 | axios.interceptors.request.use( 10 | (config: any) => { 11 | if (typeof hook === 'function') { 12 | hook(); 13 | } 14 | return config; 15 | }, 16 | (error: any) => Promise.reject(error), 17 | ); 18 | } 19 | 20 | static setAfter(hook: any) { 21 | axios.interceptors.response.use( 22 | (response: any) => { 23 | if (typeof hook === 'function') { 24 | hook(); 25 | } 26 | return response; 27 | }, 28 | (error: any) => { 29 | if (typeof hook === 'function') { 30 | hook(); 31 | } 32 | return Promise.reject(error); 33 | }, 34 | ); 35 | } 36 | 37 | static async request(opt: any) { 38 | const token = getAuthToken(); 39 | const defaultOption = { 40 | headers: { 41 | Authorization: `Bearer ${token}`, 42 | }, 43 | }; 44 | const option = { ...opt, ...defaultOption, url: `${opt.url}` }; 45 | return axios(option) 46 | .then((response: any) => { 47 | return response.data; 48 | }) 49 | .catch((error: any) => { 50 | if (!error.response) { 51 | message.error(`没有响应:${error.message}`); 52 | } 53 | const { status } = error.response; 54 | if (status === 401) { 55 | message.error('请登陆'); 56 | } 57 | if (status === 403) { 58 | message.error(`${status}: 没有权限访问,请联系管理员`); 59 | } 60 | if (status <= 504 && status >= 500) { 61 | message.error(`${status}: 服务器内部错误`); 62 | } 63 | if (status >= 404 && status < 422) { 64 | message.error(`${status}: 未找到资源`); 65 | } 66 | if (error.response.data && error.response.data.msg) { 67 | message.error(error.response.data.msg); 68 | } 69 | return error.response; 70 | }); 71 | } 72 | } 73 | 74 | export default HttpClient; 75 | -------------------------------------------------------------------------------- /stock-web/src/models/user.ts: -------------------------------------------------------------------------------- 1 | import type { Effect, Reducer } from 'umi'; 2 | 3 | import { queryCurrent, query as queryUsers } from '@/services/user'; 4 | 5 | export type CurrentUser = { 6 | avatar?: string; 7 | name?: string; 8 | title?: string; 9 | group?: string; 10 | signature?: string; 11 | tags?: { 12 | key: string; 13 | label: string; 14 | }[]; 15 | uid?: string; 16 | unreadCount?: number; 17 | roles?: string[]; 18 | }; 19 | 20 | export type UserModelState = { 21 | currentUser?: CurrentUser; 22 | }; 23 | 24 | export type UserModelType = { 25 | namespace: 'user'; 26 | state: UserModelState; 27 | effects: { 28 | fetch: Effect; 29 | fetchCurrent: Effect; 30 | }; 31 | reducers: { 32 | saveCurrentUser: Reducer; 33 | changeNotifyCount: Reducer; 34 | }; 35 | }; 36 | 37 | const UserModel: UserModelType = { 38 | namespace: 'user', 39 | 40 | state: { 41 | currentUser: {}, 42 | }, 43 | 44 | effects: { 45 | *fetch(_, { call, put }) { 46 | const response = yield call(queryUsers); 47 | yield put({ 48 | type: 'save', 49 | payload: response, 50 | }); 51 | }, 52 | *fetchCurrent(_, { call, put }) { 53 | const response = yield call(queryCurrent); 54 | // console.log('用户数据:', response); 55 | // if (response.code === 0) { 56 | // console.log('准备存储数据', response.data); 57 | yield put({ 58 | type: 'saveCurrentUser', 59 | payload: response, 60 | }); 61 | // } 62 | }, 63 | }, 64 | 65 | reducers: { 66 | saveCurrentUser(state, action) { 67 | return { 68 | ...state, 69 | currentUser: action.payload.data || {}, 70 | }; 71 | }, 72 | changeNotifyCount( 73 | state = { 74 | currentUser: {}, 75 | }, 76 | action, 77 | ) { 78 | return { 79 | ...state, 80 | currentUser: { 81 | ...state.currentUser, 82 | notifyCount: action.payload.totalCount, 83 | unreadCount: action.payload.unreadCount, 84 | }, 85 | }; 86 | }, 87 | }, 88 | }; 89 | 90 | export default UserModel; 91 | -------------------------------------------------------------------------------- /stock-web/src/pages/Welcome.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PageContainer } from '@ant-design/pro-layout'; 3 | import { Card, Alert, Typography } from 'antd'; 4 | import { useIntl, FormattedMessage } from 'umi'; 5 | import styles from './Welcome.less'; 6 | 7 | const CodePreview: React.FC = ({ children }) => ( 8 |
 9 |     
10 |       {children}
11 |     
12 |   
13 | ); 14 | 15 | export default (): React.ReactNode => { 16 | const intl = useIntl(); 17 | return ( 18 | 19 | 20 | 33 | 34 | {' '} 35 | 40 | 41 | 42 | 43 | yarn add @ant-design/pro-table 44 | 50 | {' '} 51 | 56 | 57 | 58 | 59 | yarn add @ant-design/pro-layout 60 | 61 | 62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /stock-server/src/modules/stock/tasks/sync-source.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger, HttpService } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Repository, getManager } from 'typeorm'; 4 | import { Stock } from '../../../entities/Stock'; 5 | import { Cron } from '@nestjs/schedule'; 6 | import { getDateOfBefore } from '../../../utils/common'; 7 | 8 | @Injectable() 9 | export class TasksService { 10 | constructor( 11 | private readonly httpService: HttpService, 12 | @InjectRepository(Stock) 13 | private readonly stockRepository: Repository, 14 | ) {} 15 | private readonly logger = new Logger(TasksService.name); 16 | 17 | @Cron('30 * * * * *') 18 | async taskDemo() { 19 | console.log('每分钟第30秒执行一次'); 20 | } 21 | 22 | /** 23 | * 获取第三方pe数据略 24 | */ 25 | 26 | /** 27 | * 计算PE_TTM_AVG 28 | */ 29 | @Cron('0 30 7 * * *') 30 | async culculatePeTtmAvg() { 31 | this.logger.debug('每天凌晨7点30分--计算PE_TTM_AVG'); 32 | const manager = getManager(); 33 | const dateOfFiveYearAgo = getDateOfBefore(5, 'year'); 34 | await manager.query(`update stock st set st.pe_ttm_avg=(select IFNULL(ROUND(avg(pe_ttm),4),0) 35 | as pe_av from stock_log sl where sl.code=st.code and 36 | sl.log_date>${dateOfFiveYearAgo} and sl.pe_ttm>0) where st.id>0 and st.is_delete=0`); 37 | } 38 | 39 | /** 40 | * 计算最新PE_TTM 41 | */ 42 | @Cron('0 40 7 * * *') 43 | async culculatePeTtm() { 44 | this.logger.debug('每天凌晨7点40分--计算最新PE_TTM'); 45 | const manager = getManager(); 46 | await manager.query(`update stock st set st.pe_ttm=(select IFNULL(sl.pe_ttm,0) as pe_ttm from stock_log sl 47 | where sl.code=st.code order by sl.log_date desc limit 1) 48 | where st.id>0 and st.code in (select code from stock_log group by code) and st.is_delete=0`); 49 | } 50 | 51 | /** 52 | * 计算最新PE_TTM_RATE 53 | */ 54 | @Cron('0 50 7 * * *') 55 | async culculatePeTtmRate() { 56 | this.logger.debug('每天凌晨7点50分--计算最新PE_TTM_RATE'); 57 | const manager = getManager(); 58 | await manager.query( 59 | `update stock set pe_ttm_rate= IFNULL(ROUND(pe_ttm/pe_ttm_avg,4),0) where id>0 and pe_ttm_avg>0`, 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /stock-web/src/locales/en-US/menu.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.welcome': 'Welcome', 3 | 'menu.more-blocks': 'More Blocks', 4 | 'menu.home': 'Home', 5 | 'menu.admin': 'Admin', 6 | 'menu.admin.sub-page': 'Sub-Page', 7 | 'menu.login': 'Login', 8 | 'menu.register': 'Register', 9 | 'menu.register.result': 'Register Result', 10 | 'menu.dashboard': 'Dashboard', 11 | 'menu.dashboard.analysis': 'Analysis', 12 | 'menu.dashboard.monitor': 'Monitor', 13 | 'menu.dashboard.workplace': 'Workplace', 14 | 'menu.exception.403': '403', 15 | 'menu.exception.404': '404', 16 | 'menu.exception.500': '500', 17 | 'menu.form': 'Form', 18 | 'menu.form.basic-form': 'Basic Form', 19 | 'menu.form.step-form': 'Step Form', 20 | 'menu.form.step-form.info': 'Step Form(write transfer information)', 21 | 'menu.form.step-form.confirm': 'Step Form(confirm transfer information)', 22 | 'menu.form.step-form.result': 'Step Form(finished)', 23 | 'menu.form.advanced-form': 'Advanced Form', 24 | 'menu.list': 'List', 25 | 'menu.list.table-list': 'Search Table', 26 | 'menu.list.basic-list': 'Basic List', 27 | 'menu.list.card-list': 'Card List', 28 | 'menu.list.search-list': 'Search List', 29 | 'menu.list.search-list.articles': 'Search List(articles)', 30 | 'menu.list.search-list.projects': 'Search List(projects)', 31 | 'menu.list.search-list.applications': 'Search List(applications)', 32 | 'menu.profile': 'Profile', 33 | 'menu.profile.basic': 'Basic Profile', 34 | 'menu.profile.advanced': 'Advanced Profile', 35 | 'menu.result': 'Result', 36 | 'menu.result.success': 'Success', 37 | 'menu.result.fail': 'Fail', 38 | 'menu.exception': 'Exception', 39 | 'menu.exception.not-permission': '403', 40 | 'menu.exception.not-find': '404', 41 | 'menu.exception.server-error': '500', 42 | 'menu.exception.trigger': 'Trigger', 43 | 'menu.account': 'Account', 44 | 'menu.account.center': 'Account Center', 45 | 'menu.account.settings': 'Account Settings', 46 | 'menu.account.trigger': 'Trigger Error', 47 | 'menu.account.logout': 'Logout', 48 | 'menu.editor': 'Graphic Editor', 49 | 'menu.editor.flow': 'Flow Editor', 50 | 'menu.editor.mind': 'Mind Editor', 51 | 'menu.editor.koni': 'Koni Editor', 52 | }; 53 | -------------------------------------------------------------------------------- /stock-web/config/routes.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | path: '/', 4 | component: '../layouts/BlankLayout', 5 | routes: [ 6 | { 7 | path: '/login', 8 | component: '../layouts/UserLayout', 9 | routes: [ 10 | { 11 | name: 'login', 12 | path: '/login', 13 | component: './User/login', 14 | }, 15 | ], 16 | }, 17 | { 18 | path: '/', 19 | component: '../layouts/SecurityLayout', 20 | routes: [ 21 | { 22 | path: '/', 23 | component: '../layouts/BasicLayout', 24 | authority: ['admin', 'user'], 25 | routes: [ 26 | { 27 | path: '/', 28 | redirect: '/welcome', 29 | }, 30 | { 31 | path: '/welcome', 32 | name: 'welcome', 33 | icon: 'smile', 34 | component: './Welcome', 35 | }, 36 | { 37 | path: '/admin', 38 | name: 'admin', 39 | icon: 'crown', 40 | component: './Admin', 41 | authority: ['admin'], 42 | routes: [ 43 | { 44 | path: '/admin/sub-page', 45 | name: 'sub-page', 46 | icon: 'smile', 47 | component: './Welcome', 48 | authority: ['admin'], 49 | }, 50 | ], 51 | }, 52 | { 53 | name: 'list.stock-list', 54 | icon: 'table', 55 | path: '/stock-list', 56 | component: './Stock/StockList', 57 | }, 58 | { 59 | name: 'list.user-stock-list', 60 | icon: 'table', 61 | path: '/user-stock-list', 62 | component: './User/my', 63 | }, 64 | { 65 | component: './404', 66 | }, 67 | ], 68 | }, 69 | { 70 | component: './404', 71 | }, 72 | ], 73 | }, 74 | ], 75 | }, 76 | { 77 | component: './404', 78 | }, 79 | ]; 80 | -------------------------------------------------------------------------------- /stock-web/src/locales/id-ID/menu.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.welcome': 'Selamat Datang', 3 | 'menu.more-blocks': 'Blocks Lainnya', 4 | 'menu.home': 'Halaman Awal', 5 | 'menu.admin': 'Admin', 6 | 'menu.admin.sub-page': 'Sub-Halaman', 7 | 'menu.login': 'Masuk', 8 | 'menu.register': 'Pendaftaran', 9 | 'menu.register.result': 'Hasil Pendaftaran', 10 | 'menu.dashboard': 'Dasbor', 11 | 'menu.dashboard.analysis': 'Analisis', 12 | 'menu.dashboard.monitor': 'Monitor', 13 | 'menu.dashboard.workplace': 'Workplace', 14 | 'menu.exception.403': '403', 15 | 'menu.exception.404': '404', 16 | 'menu.exception.500': '500', 17 | 'menu.form': 'Form', 18 | 'menu.form.basic-form': 'Form Dasar', 19 | 'menu.form.step-form': 'Form Bertahap', 20 | 'menu.form.step-form.info': 'Form Bertahap(menulis informasi yang dibagikan)', 21 | 'menu.form.step-form.confirm': 'Form Bertahap(konfirmasi informasi yang dibagikan)', 22 | 'menu.form.step-form.result': 'Form Bertahap(selesai)', 23 | 'menu.form.advanced-form': 'Form Lanjutan', 24 | 'menu.list': 'Daftar', 25 | 'menu.list.table-list': 'Tabel Pencarian', 26 | 'menu.list.basic-list': 'Daftar Dasar', 27 | 'menu.list.card-list': 'Daftar Kartu', 28 | 'menu.list.search-list': 'Daftar Pencarian', 29 | 'menu.list.search-list.articles': 'Daftar Pencarian(artikel)', 30 | 'menu.list.search-list.projects': 'Daftar Pencarian(projek)', 31 | 'menu.list.search-list.applications': 'Daftar Pencarian(aplikasi)', 32 | 'menu.profile': 'Profil', 33 | 'menu.profile.basic': 'Profil Dasar', 34 | 'menu.profile.advanced': 'Profile Lanjutan', 35 | 'menu.result': 'Hasil', 36 | 'menu.result.success': 'Sukses', 37 | 'menu.result.fail': 'Gagal', 38 | 'menu.exception': 'Pengecualian', 39 | 'menu.exception.not-permission': '403', 40 | 'menu.exception.not-find': '404', 41 | 'menu.exception.server-error': '500', 42 | 'menu.exception.trigger': 'Jalankan', 43 | 'menu.account': 'Akun', 44 | 'menu.account.center': 'Detail Akun', 45 | 'menu.account.settings': 'Pengaturan Akun', 46 | 'menu.account.trigger': 'Mengaktivasi Error', 47 | 'menu.account.logout': 'Keluar', 48 | 'menu.editor': 'Penyusun Grafis', 49 | 'menu.editor.flow': 'Penyusun Alur', 50 | 'menu.editor.mind': 'Penyusun Mind', 51 | 'menu.editor.koni': 'Penyusun Koni', 52 | }; 53 | -------------------------------------------------------------------------------- /stock-web/src/service-worker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable eslint-comments/disable-enable-pair */ 2 | /* eslint-disable no-restricted-globals */ 3 | /* eslint-disable no-underscore-dangle */ 4 | /* globals workbox */ 5 | workbox.core.setCacheNameDetails({ 6 | prefix: 'antd-pro', 7 | suffix: 'v1', 8 | }); 9 | // Control all opened tabs ASAP 10 | workbox.clientsClaim(); 11 | 12 | /** 13 | * Use precaching list generated by workbox in build process. 14 | * https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.precaching 15 | */ 16 | workbox.precaching.precacheAndRoute(self.__precacheManifest || []); 17 | 18 | /** 19 | * Register a navigation route. 20 | * https://developers.google.com/web/tools/workbox/modules/workbox-routing#how_to_register_a_navigation_route 21 | */ 22 | workbox.routing.registerNavigationRoute('/index.html'); 23 | 24 | /** 25 | * Use runtime cache: 26 | * https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.routing#.registerRoute 27 | * 28 | * Workbox provides all common caching strategies including CacheFirst, NetworkFirst etc. 29 | * https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.strategies 30 | */ 31 | 32 | /** Handle API requests */ 33 | workbox.routing.registerRoute(/\/api\//, workbox.strategies.networkFirst()); 34 | 35 | /** Handle third party requests */ 36 | workbox.routing.registerRoute( 37 | /^https:\/\/gw\.alipayobjects\.com\//, 38 | workbox.strategies.networkFirst(), 39 | ); 40 | workbox.routing.registerRoute( 41 | /^https:\/\/cdnjs\.cloudflare\.com\//, 42 | workbox.strategies.networkFirst(), 43 | ); 44 | workbox.routing.registerRoute(/\/color.less/, workbox.strategies.networkFirst()); 45 | 46 | /** Response to client after skipping waiting with MessageChannel */ 47 | addEventListener('message', (event) => { 48 | const replyPort = event.ports[0]; 49 | const message = event.data; 50 | if (replyPort && message && message.type === 'skip-waiting') { 51 | event.waitUntil( 52 | self.skipWaiting().then( 53 | () => { 54 | replyPort.postMessage({ 55 | error: null, 56 | }); 57 | }, 58 | (error) => { 59 | replyPort.postMessage({ 60 | error, 61 | }); 62 | }, 63 | ), 64 | ); 65 | } 66 | }); 67 | -------------------------------------------------------------------------------- /stock-web/src/locales/pt-BR/menu.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.welcome': 'Welcome', 3 | 'menu.more-blocks': 'More Blocks', 4 | 'menu.home': 'Início', 5 | 'menu.login': 'Login', 6 | 'menu.admin': 'Admin', 7 | 'menu.admin.sub-page': 'Sub-Page', 8 | 'menu.register': 'Registro', 9 | 'menu.register.result': 'Resultado de registro', 10 | 'menu.dashboard': 'Dashboard', 11 | 'menu.dashboard.analysis': 'Análise', 12 | 'menu.dashboard.monitor': 'Monitor', 13 | 'menu.dashboard.workplace': 'Ambiente de Trabalho', 14 | 'menu.exception.403': '403', 15 | 'menu.exception.404': '404', 16 | 'menu.exception.500': '500', 17 | 'menu.form': 'Formulário', 18 | 'menu.form.basic-form': 'Formulário Básico', 19 | 'menu.form.step-form': 'Formulário Assistido', 20 | 'menu.form.step-form.info': 'Formulário Assistido(gravar informações de transferência)', 21 | 'menu.form.step-form.confirm': 'Formulário Assistido(confirmar informações de transferência)', 22 | 'menu.form.step-form.result': 'Formulário Assistido(finalizado)', 23 | 'menu.form.advanced-form': 'Formulário Avançado', 24 | 'menu.list': 'Lista', 25 | 'menu.list.table-list': 'Tabela de Busca', 26 | 'menu.list.basic-list': 'Lista Básica', 27 | 'menu.list.card-list': 'Lista de Card', 28 | 'menu.list.search-list': 'Lista de Busca', 29 | 'menu.list.search-list.articles': 'Lista de Busca(artigos)', 30 | 'menu.list.search-list.projects': 'Lista de Busca(projetos)', 31 | 'menu.list.search-list.applications': 'Lista de Busca(aplicações)', 32 | 'menu.profile': 'Perfil', 33 | 'menu.profile.basic': 'Perfil Básico', 34 | 'menu.profile.advanced': 'Perfil Avançado', 35 | 'menu.result': 'Resultado', 36 | 'menu.result.success': 'Sucesso', 37 | 'menu.result.fail': 'Falha', 38 | 'menu.exception': 'Exceção', 39 | 'menu.exception.not-permission': '403', 40 | 'menu.exception.not-find': '404', 41 | 'menu.exception.server-error': '500', 42 | 'menu.exception.trigger': 'Disparar', 43 | 'menu.account': 'Conta', 44 | 'menu.account.center': 'Central da Conta', 45 | 'menu.account.settings': 'Configurar Conta', 46 | 'menu.account.trigger': 'Disparar Erro', 47 | 'menu.account.logout': 'Sair', 48 | 'menu.editor': 'Graphic Editor', 49 | 'menu.editor.flow': 'Flow Editor', 50 | 'menu.editor.mind': 'Mind Editor', 51 | 'menu.editor.koni': 'Koni Editor', 52 | }; 53 | -------------------------------------------------------------------------------- /stock-web/src/layouts/UserLayout.tsx: -------------------------------------------------------------------------------- 1 | import type { MenuDataItem } from '@ant-design/pro-layout'; 2 | import { DefaultFooter, getMenuData, getPageTitle } from '@ant-design/pro-layout'; 3 | import { Helmet, HelmetProvider } from 'react-helmet-async'; 4 | import type { ConnectProps } from 'umi'; 5 | import { Link, useIntl, connect, FormattedMessage } from 'umi'; 6 | import React from 'react'; 7 | import type { ConnectState } from '@/models/connect'; 8 | import logo from '../assets/logo.svg'; 9 | import styles from './UserLayout.less'; 10 | 11 | export type UserLayoutProps = { 12 | breadcrumbNameMap: Record; 13 | } & Partial; 14 | 15 | const UserLayout: React.FC = (props) => { 16 | const { 17 | route = { 18 | routes: [], 19 | }, 20 | } = props; 21 | const { routes = [] } = route; 22 | const { 23 | children, 24 | location = { 25 | pathname: '', 26 | }, 27 | } = props; 28 | const { formatMessage } = useIntl(); 29 | const { breadcrumb } = getMenuData(routes); 30 | const title = getPageTitle({ 31 | pathname: location.pathname, 32 | formatMessage, 33 | breadcrumb, 34 | ...props, 35 | }); 36 | return ( 37 | 38 | 39 | {title} 40 | 41 | 42 | 43 |
44 |
{/* */}
45 |
46 |
47 |
48 | 49 | logo 50 | 股票PE记录 51 | 52 |
53 |
54 | 58 |
59 |
60 | {children} 61 |
62 | 63 |
64 |
65 | ); 66 | }; 67 | 68 | export default connect(({ settings }: ConnectState) => ({ ...settings }))(UserLayout); 69 | -------------------------------------------------------------------------------- /stock-web/src/components/NoticeIcon/NoticeList.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .list { 4 | max-height: 400px; 5 | overflow: auto; 6 | &::-webkit-scrollbar { 7 | display: none; 8 | } 9 | .item { 10 | padding-right: 24px; 11 | padding-left: 24px; 12 | overflow: hidden; 13 | cursor: pointer; 14 | transition: all 0.3s; 15 | 16 | .meta { 17 | width: 100%; 18 | } 19 | 20 | .avatar { 21 | margin-top: 4px; 22 | background: @component-background; 23 | } 24 | .iconElement { 25 | font-size: 32px; 26 | } 27 | 28 | &.read { 29 | opacity: 0.4; 30 | } 31 | &:last-child { 32 | border-bottom: 0; 33 | } 34 | &:hover { 35 | background: @primary-1; 36 | } 37 | .title { 38 | margin-bottom: 8px; 39 | font-weight: normal; 40 | } 41 | .description { 42 | font-size: 12px; 43 | line-height: @line-height-base; 44 | } 45 | .datetime { 46 | margin-top: 4px; 47 | font-size: 12px; 48 | line-height: @line-height-base; 49 | } 50 | .extra { 51 | float: right; 52 | margin-top: -1.5px; 53 | margin-right: 0; 54 | color: @text-color-secondary; 55 | font-weight: normal; 56 | } 57 | } 58 | .loadMore { 59 | padding: 8px 0; 60 | color: @primary-6; 61 | text-align: center; 62 | cursor: pointer; 63 | &.loadedAll { 64 | color: rgba(0, 0, 0, 0.25); 65 | cursor: unset; 66 | } 67 | } 68 | } 69 | 70 | .notFound { 71 | padding: 73px 0 88px; 72 | color: @text-color-secondary; 73 | text-align: center; 74 | img { 75 | display: inline-block; 76 | height: 76px; 77 | margin-bottom: 16px; 78 | } 79 | } 80 | 81 | .bottomBar { 82 | height: 46px; 83 | color: @text-color; 84 | line-height: 46px; 85 | text-align: center; 86 | border-top: 1px solid @border-color-split; 87 | border-radius: 0 0 @border-radius-base @border-radius-base; 88 | transition: all 0.3s; 89 | div { 90 | display: inline-block; 91 | width: 50%; 92 | cursor: pointer; 93 | transition: all 0.3s; 94 | user-select: none; 95 | 96 | &:only-child { 97 | width: 100%; 98 | } 99 | &:not(:only-child):last-child { 100 | border-left: 1px solid @border-color-split; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /stock-web/src/components/Authorized/Secured.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CheckPermissions from './CheckPermissions'; 3 | 4 | /** 默认不能访问任何页面 default is "NULL" */ 5 | const Exception403 = () => 403; 6 | 7 | export const isComponentClass = (component: React.ComponentClass | React.ReactNode): boolean => { 8 | if (!component) return false; 9 | const proto = Object.getPrototypeOf(component); 10 | if (proto === React.Component || proto === Function.prototype) return true; 11 | return isComponentClass(proto); 12 | }; 13 | 14 | // Determine whether the incoming component has been instantiated 15 | // AuthorizedRoute is already instantiated 16 | // Authorized render is already instantiated, children is no instantiated 17 | // Secured is not instantiated 18 | const checkIsInstantiation = (target: React.ComponentClass | React.ReactNode) => { 19 | if (isComponentClass(target)) { 20 | const Target = target as React.ComponentClass; 21 | return (props: any) => ; 22 | } 23 | if (React.isValidElement(target)) { 24 | return (props: any) => React.cloneElement(target, props); 25 | } 26 | return () => target; 27 | }; 28 | 29 | /** 30 | * 用于判断是否拥有权限访问此 view 权限 authority 支持传入 string, () => boolean | Promise e.g. 'user' 只有 user 用户能访问 31 | * e.g. 'user,admin' user 和 admin 都能访问 e.g. ()=>boolean 返回true能访问,返回false不能访问 e.g. Promise then 能访问 32 | * catch不能访问 e.g. authority support incoming string, () => boolean | Promise e.g. 'user' only user 33 | * user can access e.g. 'user, admin' user and admin can access e.g. () => boolean true to be able 34 | * to visit, return false can not be accessed e.g. Promise then can not access the visit to catch 35 | * 36 | * @param {string | function | Promise} authority 37 | * @param {ReactNode} error 非必需参数 38 | */ 39 | const authorize = (authority: string, error?: React.ReactNode) => { 40 | /** 41 | * Conversion into a class 防止传入字符串时找不到staticContext造成报错 String parameters can cause staticContext 42 | * not found error 43 | */ 44 | let classError: boolean | React.FunctionComponent = false; 45 | if (error) { 46 | classError = (() => error) as React.FunctionComponent; 47 | } 48 | if (!authority) { 49 | throw new Error('authority is required'); 50 | } 51 | return function decideAuthority(target: React.ComponentClass | React.ReactNode) { 52 | const component = CheckPermissions(authority, target, classError || Exception403); 53 | return checkIsInstantiation(component); 54 | }; 55 | }; 56 | 57 | export default authorize; 58 | -------------------------------------------------------------------------------- /stock-web/src/components/Authorized/CheckPermissions.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CURRENT } from './renderAuthorize'; 3 | // eslint-disable-next-line import/no-cycle 4 | import PromiseRender from './PromiseRender'; 5 | 6 | export type IAuthorityType = 7 | | undefined 8 | | string 9 | | string[] 10 | | Promise 11 | | ((currentAuthority: string | string[]) => IAuthorityType); 12 | 13 | /** 14 | * 通用权限检查方法 Common check permissions method 15 | * 16 | * @param { 权限判定 | Permission judgment } authority 17 | * @param { 你的权限 | Your permission description } currentAuthority 18 | * @param { 通过的组件 | Passing components } target 19 | * @param { 未通过的组件 | no pass components } Exception 20 | */ 21 | const checkPermissions = ( 22 | authority: IAuthorityType, 23 | currentAuthority: string | string[], 24 | target: T, 25 | Exception: K, 26 | ): T | K | React.ReactNode => { 27 | // 没有判定权限.默认查看所有 28 | // Retirement authority, return target; 29 | if (!authority) { 30 | return target; 31 | } 32 | // 数组处理 33 | if (Array.isArray(authority)) { 34 | if (Array.isArray(currentAuthority)) { 35 | if (currentAuthority.some((item) => authority.includes(item))) { 36 | return target; 37 | } 38 | } else if (authority.includes(currentAuthority)) { 39 | return target; 40 | } 41 | return Exception; 42 | } 43 | // string 处理 44 | if (typeof authority === 'string') { 45 | if (Array.isArray(currentAuthority)) { 46 | if (currentAuthority.some((item) => authority === item)) { 47 | return target; 48 | } 49 | } else if (authority === currentAuthority) { 50 | return target; 51 | } 52 | return Exception; 53 | } 54 | // Promise 处理 55 | if (authority instanceof Promise) { 56 | return ok={target} error={Exception} promise={authority} />; 57 | } 58 | // Function 处理 59 | if (typeof authority === 'function') { 60 | const bool = authority(currentAuthority); 61 | // 函数执行后返回值是 Promise 62 | if (bool instanceof Promise) { 63 | return ok={target} error={Exception} promise={bool} />; 64 | } 65 | if (bool) { 66 | return target; 67 | } 68 | return Exception; 69 | } 70 | throw new Error('unsupported parameters'); 71 | }; 72 | 73 | export { checkPermissions }; 74 | 75 | function check(authority: IAuthorityType, target: T, Exception: K): T | K | React.ReactNode { 76 | return checkPermissions(authority, CURRENT, target, Exception); 77 | } 78 | 79 | export default check; 80 | -------------------------------------------------------------------------------- /stock-web/src/locales/zh-CN/settings.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.settings.menuMap.basic': '基本设置', 3 | 'app.settings.menuMap.security': '安全设置', 4 | 'app.settings.menuMap.binding': '账号绑定', 5 | 'app.settings.menuMap.notification': '新消息通知', 6 | 'app.settings.basic.avatar': '头像', 7 | 'app.settings.basic.change-avatar': '更换头像', 8 | 'app.settings.basic.email': '邮箱', 9 | 'app.settings.basic.email-message': '请输入您的邮箱!', 10 | 'app.settings.basic.nickname': '昵称', 11 | 'app.settings.basic.nickname-message': '请输入您的昵称!', 12 | 'app.settings.basic.profile': '个人简介', 13 | 'app.settings.basic.profile-message': '请输入个人简介!', 14 | 'app.settings.basic.profile-placeholder': '个人简介', 15 | 'app.settings.basic.country': '国家/地区', 16 | 'app.settings.basic.country-message': '请输入您的国家或地区!', 17 | 'app.settings.basic.geographic': '所在省市', 18 | 'app.settings.basic.geographic-message': '请输入您的所在省市!', 19 | 'app.settings.basic.address': '街道地址', 20 | 'app.settings.basic.address-message': '请输入您的街道地址!', 21 | 'app.settings.basic.phone': '联系电话', 22 | 'app.settings.basic.phone-message': '请输入您的联系电话!', 23 | 'app.settings.basic.update': '更新基本信息', 24 | 'app.settings.security.strong': '强', 25 | 'app.settings.security.medium': '中', 26 | 'app.settings.security.weak': '弱', 27 | 'app.settings.security.password': '账户密码', 28 | 'app.settings.security.password-description': '当前密码强度', 29 | 'app.settings.security.phone': '密保手机', 30 | 'app.settings.security.phone-description': '已绑定手机', 31 | 'app.settings.security.question': '密保问题', 32 | 'app.settings.security.question-description': '未设置密保问题,密保问题可有效保护账户安全', 33 | 'app.settings.security.email': '备用邮箱', 34 | 'app.settings.security.email-description': '已绑定邮箱', 35 | 'app.settings.security.mfa': 'MFA 设备', 36 | 'app.settings.security.mfa-description': '未绑定 MFA 设备,绑定后,可以进行二次确认', 37 | 'app.settings.security.modify': '修改', 38 | 'app.settings.security.set': '设置', 39 | 'app.settings.security.bind': '绑定', 40 | 'app.settings.binding.taobao': '绑定淘宝', 41 | 'app.settings.binding.taobao-description': '当前未绑定淘宝账号', 42 | 'app.settings.binding.alipay': '绑定支付宝', 43 | 'app.settings.binding.alipay-description': '当前未绑定支付宝账号', 44 | 'app.settings.binding.dingding': '绑定钉钉', 45 | 'app.settings.binding.dingding-description': '当前未绑定钉钉账号', 46 | 'app.settings.binding.bind': '绑定', 47 | 'app.settings.notification.password': '账户密码', 48 | 'app.settings.notification.password-description': '其他用户的消息将以站内信的形式通知', 49 | 'app.settings.notification.messages': '系统消息', 50 | 'app.settings.notification.messages-description': '系统消息将以站内信的形式通知', 51 | 'app.settings.notification.todo': '待办任务', 52 | 'app.settings.notification.todo-description': '待办任务将以站内信的形式通知', 53 | 'app.settings.open': '开', 54 | 'app.settings.close': '关', 55 | }; 56 | -------------------------------------------------------------------------------- /stock-web/src/locales/zh-TW/settings.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.settings.menuMap.basic': '基本設置', 3 | 'app.settings.menuMap.security': '安全設置', 4 | 'app.settings.menuMap.binding': '賬號綁定', 5 | 'app.settings.menuMap.notification': '新消息通知', 6 | 'app.settings.basic.avatar': '頭像', 7 | 'app.settings.basic.change-avatar': '更換頭像', 8 | 'app.settings.basic.email': '郵箱', 9 | 'app.settings.basic.email-message': '請輸入您的郵箱!', 10 | 'app.settings.basic.nickname': '昵稱', 11 | 'app.settings.basic.nickname-message': '請輸入您的昵稱!', 12 | 'app.settings.basic.profile': '個人簡介', 13 | 'app.settings.basic.profile-message': '請輸入個人簡介!', 14 | 'app.settings.basic.profile-placeholder': '個人簡介', 15 | 'app.settings.basic.country': '國家/地區', 16 | 'app.settings.basic.country-message': '請輸入您的國家或地區!', 17 | 'app.settings.basic.geographic': '所在省市', 18 | 'app.settings.basic.geographic-message': '請輸入您的所在省市!', 19 | 'app.settings.basic.address': '街道地址', 20 | 'app.settings.basic.address-message': '請輸入您的街道地址!', 21 | 'app.settings.basic.phone': '聯系電話', 22 | 'app.settings.basic.phone-message': '請輸入您的聯系電話!', 23 | 'app.settings.basic.update': '更新基本信息', 24 | 'app.settings.security.strong': '強', 25 | 'app.settings.security.medium': '中', 26 | 'app.settings.security.weak': '弱', 27 | 'app.settings.security.password': '賬戶密碼', 28 | 'app.settings.security.password-description': '當前密碼強度', 29 | 'app.settings.security.phone': '密保手機', 30 | 'app.settings.security.phone-description': '已綁定手機', 31 | 'app.settings.security.question': '密保問題', 32 | 'app.settings.security.question-description': '未設置密保問題,密保問題可有效保護賬戶安全', 33 | 'app.settings.security.email': '備用郵箱', 34 | 'app.settings.security.email-description': '已綁定郵箱', 35 | 'app.settings.security.mfa': 'MFA 設備', 36 | 'app.settings.security.mfa-description': '未綁定 MFA 設備,綁定後,可以進行二次確認', 37 | 'app.settings.security.modify': '修改', 38 | 'app.settings.security.set': '設置', 39 | 'app.settings.security.bind': '綁定', 40 | 'app.settings.binding.taobao': '綁定淘寶', 41 | 'app.settings.binding.taobao-description': '當前未綁定淘寶賬號', 42 | 'app.settings.binding.alipay': '綁定支付寶', 43 | 'app.settings.binding.alipay-description': '當前未綁定支付寶賬號', 44 | 'app.settings.binding.dingding': '綁定釘釘', 45 | 'app.settings.binding.dingding-description': '當前未綁定釘釘賬號', 46 | 'app.settings.binding.bind': '綁定', 47 | 'app.settings.notification.password': '賬戶密碼', 48 | 'app.settings.notification.password-description': '其他用戶的消息將以站內信的形式通知', 49 | 'app.settings.notification.messages': '系統消息', 50 | 'app.settings.notification.messages-description': '系統消息將以站內信的形式通知', 51 | 'app.settings.notification.todo': '待辦任務', 52 | 'app.settings.notification.todo-description': '待辦任務將以站內信的形式通知', 53 | 'app.settings.open': '開', 54 | 'app.settings.close': '關', 55 | }; 56 | -------------------------------------------------------------------------------- /stock-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stock-demo", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nest start", 13 | "start:dev": "nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/main", 16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 17 | "test": "jest", 18 | "test:watch": "jest --watch", 19 | "test:cov": "jest --coverage", 20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 21 | "test:e2e": "jest --config ./test/jest-e2e.json" 22 | }, 23 | "dependencies": { 24 | "@nestjs/common": "^7.5.1", 25 | "@nestjs/core": "^7.5.1", 26 | "@nestjs/jwt": "^7.2.0", 27 | "@nestjs/passport": "^7.1.5", 28 | "@nestjs/platform-express": "^7.5.1", 29 | "@nestjs/schedule": "^0.4.3", 30 | "@nestjs/typeorm": "^7.1.5", 31 | "@types/webpack": "^4.41.27", 32 | "bcryptjs": "^2.4.3", 33 | "class-transformer": "^0.4.0", 34 | "class-validator": "^0.13.1", 35 | "dayjs": "^1.10.4", 36 | "mysql": "^2.18.1", 37 | "passport": "^0.4.1", 38 | "passport-jwt": "^4.0.0", 39 | "passport-local": "^1.0.0", 40 | "reflect-metadata": "^0.1.13", 41 | "rimraf": "^3.0.2", 42 | "rxjs": "^6.6.3", 43 | "tapable": "^1.1.3", 44 | "typeorm": "^0.2.31" 45 | }, 46 | "devDependencies": { 47 | "@nestjs/cli": "^7.5.1", 48 | "@nestjs/schematics": "^7.1.3", 49 | "@nestjs/testing": "^7.5.1", 50 | "@types/express": "^4.17.8", 51 | "@types/jest": "^26.0.15", 52 | "@types/node": "^14.14.6", 53 | "@types/supertest": "^2.0.10", 54 | "@typescript-eslint/eslint-plugin": "^4.6.1", 55 | "@typescript-eslint/parser": "^4.6.1", 56 | "eslint": "^7.12.1", 57 | "eslint-config-prettier": "7.2.0", 58 | "eslint-plugin-prettier": "^3.1.4", 59 | "jest": "^26.6.3", 60 | "prettier": "^2.1.2", 61 | "supertest": "^6.0.0", 62 | "ts-jest": "^26.4.3", 63 | "ts-loader": "^8.0.8", 64 | "ts-node": "^9.0.0", 65 | "tsconfig-paths": "^3.9.0", 66 | "typescript": "^4.0.5" 67 | }, 68 | "jest": { 69 | "moduleFileExtensions": [ 70 | "js", 71 | "json", 72 | "ts" 73 | ], 74 | "rootDir": "src", 75 | "testRegex": ".*\\.spec\\.ts$", 76 | "transform": { 77 | "^.+\\.(t|j)s$": "ts-jest" 78 | }, 79 | "collectCoverageFrom": [ 80 | "**/*.(t|j)s" 81 | ], 82 | "coverageDirectory": "../coverage", 83 | "testEnvironment": "node" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /stock-web/src/components/GlobalHeader/RightContent.tsx: -------------------------------------------------------------------------------- 1 | import { Tag } from 'antd'; 2 | import type { Settings as ProSettings } from '@ant-design/pro-layout'; 3 | // import { QuestionCircleOutlined } from '@ant-design/icons'; 4 | import React from 'react'; 5 | import type { ConnectProps } from 'umi'; 6 | import { connect } from 'umi'; 7 | import type { ConnectState } from '@/models/connect'; 8 | import Avatar from './AvatarDropdown'; 9 | // import HeaderSearch from '../HeaderSearch'; 10 | import styles from './index.less'; 11 | 12 | export type GlobalHeaderRightProps = { 13 | theme?: ProSettings['navTheme'] | 'realDark'; 14 | } & Partial & 15 | Partial; 16 | 17 | const ENVTagColor = { 18 | dev: 'orange', 19 | test: 'green', 20 | pre: '#87d068', 21 | }; 22 | 23 | const GlobalHeaderRight: React.SFC = (props) => { 24 | const { theme, layout } = props; 25 | let className = styles.right; 26 | 27 | if (theme === 'dark' && layout === 'top') { 28 | className = `${styles.right} ${styles.dark}`; 29 | } 30 | 31 | return ( 32 |
33 | {/* umi ui, value: 'umi ui' }, 39 | { 40 | label: Ant Design, 41 | value: 'Ant Design', 42 | }, 43 | { 44 | label: Pro Table, 45 | value: 'Pro Table', 46 | }, 47 | { 48 | label: Pro Layout, 49 | value: 'Pro Layout', 50 | }, 51 | ]} 52 | // onSearch={value => { 53 | // //console.log('input', value); 54 | // }} 55 | /> */} 56 | {/* 57 | 66 | 67 | 68 | */} 69 | 70 | {REACT_APP_ENV && ( 71 | 72 | {REACT_APP_ENV} 73 | 74 | )} 75 | {/* */} 76 |
77 | ); 78 | }; 79 | 80 | export default connect(({ settings }: ConnectState) => ({ 81 | theme: settings.navTheme, 82 | layout: settings.layout, 83 | }))(GlobalHeaderRight); 84 | -------------------------------------------------------------------------------- /stock-web/src/components/GlobalHeader/AvatarDropdown.tsx: -------------------------------------------------------------------------------- 1 | import { LogoutOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons'; 2 | import { Avatar, Menu, Spin } from 'antd'; 3 | import React from 'react'; 4 | import type { ConnectProps } from 'umi'; 5 | import { history, connect } from 'umi'; 6 | import type { ConnectState } from '@/models/connect'; 7 | import type { CurrentUser } from '@/models/user'; 8 | import HeaderDropdown from '../HeaderDropdown'; 9 | import styles from './index.less'; 10 | 11 | export type GlobalHeaderRightProps = { 12 | currentUser?: CurrentUser; 13 | menu?: boolean; 14 | } & Partial; 15 | 16 | class AvatarDropdown extends React.Component { 17 | onMenuClick = (event: { 18 | key: React.Key; 19 | keyPath: React.Key[]; 20 | item: React.ReactInstance; 21 | domEvent: React.MouseEvent; 22 | }) => { 23 | const { key } = event; 24 | 25 | if (key === 'logout') { 26 | const { dispatch } = this.props; 27 | 28 | if (dispatch) { 29 | dispatch({ 30 | type: 'login/logout', 31 | }); 32 | } 33 | 34 | return; 35 | } 36 | 37 | history.push(`/account/${key}`); 38 | }; 39 | 40 | render(): React.ReactNode { 41 | const { 42 | currentUser = { 43 | avatar: '', 44 | name: '', 45 | }, 46 | menu, 47 | } = this.props; 48 | const menuHeaderDropdown = ( 49 | 50 | {menu && ( 51 | 52 | 53 | 个人中心 54 | 55 | )} 56 | {menu && ( 57 | 58 | 59 | 个人设置 60 | 61 | )} 62 | {menu && } 63 | 64 | 65 | 66 | 退出登录 67 | 68 | 69 | ); 70 | return currentUser && currentUser.name ? ( 71 | 72 | 73 | 74 | {currentUser.name} 75 | 76 | 77 | ) : ( 78 | 79 | 86 | 87 | ); 88 | } 89 | } 90 | 91 | export default connect(({ user }: ConnectState) => ({ 92 | currentUser: user.currentUser, 93 | }))(AvatarDropdown); 94 | -------------------------------------------------------------------------------- /stock-web/src/components/Authorized/PromiseRender.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Spin } from 'antd'; 3 | import isEqual from 'lodash/isEqual'; 4 | import { isComponentClass } from './Secured'; 5 | // eslint-disable-next-line import/no-cycle 6 | 7 | type PromiseRenderProps = { 8 | ok: T; 9 | error: K; 10 | promise: Promise; 11 | }; 12 | 13 | type PromiseRenderState = { 14 | component: React.ComponentClass | React.FunctionComponent; 15 | }; 16 | 17 | export default class PromiseRender extends React.Component< 18 | PromiseRenderProps, 19 | PromiseRenderState 20 | > { 21 | state: PromiseRenderState = { 22 | component: () => null, 23 | }; 24 | 25 | componentDidMount(): void { 26 | this.setRenderComponent(this.props); 27 | } 28 | 29 | shouldComponentUpdate = ( 30 | nextProps: PromiseRenderProps, 31 | nextState: PromiseRenderState, 32 | ): boolean => { 33 | const { component } = this.state; 34 | if (!isEqual(nextProps, this.props)) { 35 | this.setRenderComponent(nextProps); 36 | } 37 | if (nextState.component !== component) return true; 38 | return false; 39 | }; 40 | 41 | // set render Component : ok or error 42 | setRenderComponent(props: PromiseRenderProps): void { 43 | const ok = this.checkIsInstantiation(props.ok); 44 | const error = this.checkIsInstantiation(props.error); 45 | props.promise 46 | .then(() => { 47 | this.setState({ 48 | component: ok, 49 | }); 50 | return true; 51 | }) 52 | .catch(() => { 53 | this.setState({ 54 | component: error, 55 | }); 56 | }); 57 | } 58 | 59 | // Determine whether the incoming component has been instantiated 60 | // AuthorizedRoute is already instantiated 61 | // Authorized render is already instantiated, children is no instantiated 62 | // Secured is not instantiated 63 | checkIsInstantiation = ( 64 | target: React.ReactNode | React.ComponentClass, 65 | ): React.FunctionComponent => { 66 | if (isComponentClass(target)) { 67 | const Target = target as React.ComponentClass; 68 | return (props: any) => ; 69 | } 70 | if (React.isValidElement(target)) { 71 | return (props: any) => React.cloneElement(target, props); 72 | } 73 | return () => target as React.ReactNode & null; 74 | }; 75 | 76 | render() { 77 | const { component: Component } = this.state; 78 | const { ok, error, promise, ...rest } = this.props; 79 | 80 | return Component ? ( 81 | 82 | ) : ( 83 |
92 | 93 |
94 | ); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /stock-web/src/global.tsx: -------------------------------------------------------------------------------- 1 | import { Button, message, notification } from 'antd'; 2 | 3 | import React from 'react'; 4 | import { useIntl } from 'umi'; 5 | import defaultSettings from '../config/defaultSettings'; 6 | 7 | const { pwa } = defaultSettings; 8 | const isHttps = document.location.protocol === 'https:'; 9 | 10 | // if pwa is true 11 | if (pwa) { 12 | // Notify user if offline now 13 | window.addEventListener('sw.offline', () => { 14 | message.warning(useIntl().formatMessage({ id: 'app.pwa.offline' })); 15 | }); 16 | 17 | // Pop up a prompt on the page asking the user if they want to use the latest version 18 | window.addEventListener('sw.updated', (event: Event) => { 19 | const e = event as CustomEvent; 20 | const reloadSW = async () => { 21 | // Check if there is sw whose state is waiting in ServiceWorkerRegistration 22 | // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration 23 | const worker = e.detail && e.detail.waiting; 24 | if (!worker) { 25 | return true; 26 | } 27 | // Send skip-waiting event to waiting SW with MessageChannel 28 | await new Promise((resolve, reject) => { 29 | const channel = new MessageChannel(); 30 | channel.port1.onmessage = (msgEvent) => { 31 | if (msgEvent.data.error) { 32 | reject(msgEvent.data.error); 33 | } else { 34 | resolve(msgEvent.data); 35 | } 36 | }; 37 | worker.postMessage({ type: 'skip-waiting' }, [channel.port2]); 38 | }); 39 | // Refresh current page to use the updated HTML and other assets after SW has skiped waiting 40 | window.location.reload(true); 41 | return true; 42 | }; 43 | const key = `open${Date.now()}`; 44 | const btn = ( 45 | 54 | ); 55 | notification.open({ 56 | message: useIntl().formatMessage({ id: 'app.pwa.serviceworker.updated' }), 57 | description: useIntl().formatMessage({ id: 'app.pwa.serviceworker.updated.hint' }), 58 | btn, 59 | key, 60 | onClose: async () => null, 61 | }); 62 | }); 63 | } else if ('serviceWorker' in navigator && isHttps) { 64 | // unregister service worker 65 | const { serviceWorker } = navigator; 66 | if (serviceWorker.getRegistrations) { 67 | serviceWorker.getRegistrations().then((sws) => { 68 | sws.forEach((sw) => { 69 | sw.unregister(); 70 | }); 71 | }); 72 | } 73 | serviceWorker.getRegistration().then((sw) => { 74 | if (sw) sw.unregister(); 75 | }); 76 | 77 | // remove all caches 78 | if (window.caches && window.caches.keys) { 79 | caches.keys().then((keys) => { 80 | keys.forEach((key) => { 81 | caches.delete(key); 82 | }); 83 | }); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /stock-web/src/locales/ja-JP/settings.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.settings.menuMap.basic': '基本設定', 3 | 'app.settings.menuMap.security': 'セキュリティ設定', 4 | 'app.settings.menuMap.binding': 'アカウントのバインド', 5 | 'app.settings.menuMap.notification': '新しいメッセージの通知', 6 | 'app.settings.basic.avatar': 'アバター', 7 | 'app.settings.basic.change-avatar': 'アバターを変更する', 8 | 'app.settings.basic.email': 'メール', 9 | 'app.settings.basic.email-message': 'メールアドレスを入力してください!', 10 | 'app.settings.basic.nickname': 'ニックネーム', 11 | 'app.settings.basic.nickname-message': 'ニックネームを入力してください!', 12 | 'app.settings.basic.profile': '個人プロフィール', 13 | 'app.settings.basic.profile-message': '個人プロフィールを入力してください!', 14 | 'app.settings.basic.profile-placeholder': '自己紹介', 15 | 'app.settings.basic.country': '国/地域', 16 | 'app.settings.basic.country-message': 'あなたの国を入力してください!', 17 | 'app.settings.basic.geographic': '州または市', 18 | 'app.settings.basic.geographic-message': '地理情報を入力してください!', 19 | 'app.settings.basic.address': '住所', 20 | 'app.settings.basic.address-message': '住所を入力してください!', 21 | 'app.settings.basic.phone': '電話番号', 22 | 'app.settings.basic.phone-message': '電話番号を入力してください!', 23 | 'app.settings.basic.update': '更新情報', 24 | 'app.settings.security.strong': '強い', 25 | 'app.settings.security.medium': 'ミディアム', 26 | 'app.settings.security.weak': '弱い', 27 | 'app.settings.security.password': 'アカウントパスワード', 28 | 'app.settings.security.password-description': '現在のパスワードの強度', 29 | 'app.settings.security.phone': 'セキュリティ電話番号', 30 | 'app.settings.security.phone-description': 'バインドされた電話番号', 31 | 'app.settings.security.question': '秘密の質問', 32 | 'app.settings.security.question-description': 33 | 'セキュリティの質問が設定されてません。セキュリティポリシーはアカウントのセキュリティを効果的に保護できます', 34 | 'app.settings.security.email': 'バックアップメール', 35 | 'app.settings.security.email-description': 'バインドされたメール', 36 | 'app.settings.security.mfa': '多要素認証デバイス', 37 | 'app.settings.security.mfa-description': 38 | 'バインドされていない多要素認証デバイスは、バインド後、2回確認できます', 39 | 'app.settings.security.modify': '変更する', 40 | 'app.settings.security.set': 'セットする', 41 | 'app.settings.security.bind': 'バインド', 42 | 'app.settings.binding.taobao': 'タオバオをバインドする', 43 | 'app.settings.binding.taobao-description': '現在バインドされていないタオバオアカウント', 44 | 'app.settings.binding.alipay': 'アリペイをバインドする', 45 | 'app.settings.binding.alipay-description': '現在バインドされていないアリペイアカウント', 46 | 'app.settings.binding.dingding': 'ディントークをバインドする', 47 | 'app.settings.binding.dingding-description': '現在バインドされていないディントークアカウント', 48 | 'app.settings.binding.bind': 'バインド', 49 | 'app.settings.notification.password': 'アカウントパスワード', 50 | 'app.settings.notification.password-description': 51 | '他のユーザーからのメッセージは、ステーションレターの形式で通知されます', 52 | 'app.settings.notification.messages': 'システムメッセージ', 53 | 'app.settings.notification.messages-description': 54 | 'システムメッセージは、ステーションレターの形式で通知されます', 55 | 'app.settings.notification.todo': 'To Do(用事) 通知', 56 | 'app.settings.notification.todo-description': 'To Doタスクは、内部レターの形式で通知されます', 57 | 'app.settings.open': '開く', 58 | 'app.settings.close': '閉じる', 59 | }; 60 | -------------------------------------------------------------------------------- /stock-web/src/components/HeaderSearch/index.tsx: -------------------------------------------------------------------------------- 1 | import { SearchOutlined } from '@ant-design/icons'; 2 | import { AutoComplete, Input } from 'antd'; 3 | import useMergedState from 'rc-util/es/hooks/useMergedState'; 4 | import type { AutoCompleteProps } from 'antd/es/auto-complete'; 5 | import React, { useRef } from 'react'; 6 | 7 | import classNames from 'classnames'; 8 | import styles from './index.less'; 9 | 10 | export type HeaderSearchProps = { 11 | onSearch?: (value?: string) => void; 12 | onChange?: (value?: string) => void; 13 | onVisibleChange?: (b: boolean) => void; 14 | className?: string; 15 | placeholder?: string; 16 | options: AutoCompleteProps['options']; 17 | defaultOpen?: boolean; 18 | open?: boolean; 19 | defaultValue?: string; 20 | value?: string; 21 | }; 22 | 23 | const HeaderSearch: React.FC = (props) => { 24 | const { 25 | className, 26 | defaultValue, 27 | onVisibleChange, 28 | placeholder, 29 | open, 30 | defaultOpen, 31 | ...restProps 32 | } = props; 33 | 34 | const inputRef = useRef(null); 35 | 36 | const [value, setValue] = useMergedState(defaultValue, { 37 | value: props.value, 38 | onChange: props.onChange, 39 | }); 40 | 41 | const [searchMode, setSearchMode] = useMergedState(defaultOpen ?? false, { 42 | value: props.open, 43 | onChange: onVisibleChange, 44 | }); 45 | 46 | const inputClass = classNames(styles.input, { 47 | [styles.show]: searchMode, 48 | }); 49 | 50 | return ( 51 |
{ 54 | setSearchMode(true); 55 | if (searchMode && inputRef.current) { 56 | inputRef.current.focus(); 57 | } 58 | }} 59 | onTransitionEnd={({ propertyName }) => { 60 | if (propertyName === 'width' && !searchMode) { 61 | if (onVisibleChange) { 62 | onVisibleChange(searchMode); 63 | } 64 | } 65 | }} 66 | > 67 | 73 | 84 | { 90 | if (e.key === 'Enter') { 91 | if (restProps.onSearch) { 92 | restProps.onSearch(value); 93 | } 94 | } 95 | }} 96 | onBlur={() => { 97 | setSearchMode(false); 98 | }} 99 | /> 100 | 101 |
102 | ); 103 | }; 104 | 105 | export default HeaderSearch; 106 | -------------------------------------------------------------------------------- /stock-server/src/entities/Stock.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | Entity, 4 | Index, 5 | OneToMany, 6 | PrimaryGeneratedColumn, 7 | } from 'typeorm'; 8 | import { UserStock } from './UserStock'; 9 | 10 | @Index('uk_stock_code', ['code'], { unique: true }) 11 | @Entity('stock', { schema: 'stock_demo' }) 12 | export class Stock { 13 | @PrimaryGeneratedColumn({ type: 'int', name: 'id', comment: '主键id' }) 14 | id: number; 15 | 16 | @Column('varchar', { 17 | name: 'code', 18 | unique: true, 19 | comment: '股票代码', 20 | length: 10, 21 | }) 22 | code: string; 23 | 24 | @Column('varchar', { name: 'name', comment: '股票名称', length: 30 }) 25 | name: string; 26 | 27 | @Column('varchar', { name: 'market', comment: '股票市场', length: 10 }) 28 | market: string; 29 | 30 | @Column('decimal', { 31 | name: 'price', 32 | comment: '当前股价', 33 | precision: 12, 34 | scale: 4, 35 | default: () => "'0.0000'", 36 | }) 37 | price: string; 38 | 39 | @Column('decimal', { 40 | name: 'pe', 41 | comment: '当前PE(市盈率)', 42 | precision: 12, 43 | scale: 4, 44 | default: () => "'0.0000'", 45 | }) 46 | pe: string; 47 | 48 | @Column('decimal', { 49 | name: 'pe_avg', 50 | comment: '平均PE(市盈率)', 51 | precision: 12, 52 | scale: 4, 53 | default: () => "'0.0000'", 54 | }) 55 | peAvg: string; 56 | 57 | @Column('decimal', { 58 | name: 'pe_ttm', 59 | comment: '当前PE TTM(市盈率)', 60 | precision: 12, 61 | scale: 4, 62 | default: () => "'0.0000'", 63 | }) 64 | peTtm: string; 65 | 66 | @Column('decimal', { 67 | name: 'pe_ttm_avg', 68 | comment: '平均PE TTM(市盈率)', 69 | precision: 12, 70 | scale: 4, 71 | default: () => "'0.0000'", 72 | }) 73 | peTtmAvg: string; 74 | 75 | @Column('decimal', { 76 | name: 'pe_ttm_rate', 77 | comment: '最新pet_tm与平均值的比例', 78 | precision: 12, 79 | scale: 4, 80 | default: () => "'0.0000'", 81 | }) 82 | peTtmRate: string; 83 | 84 | @Column('decimal', { 85 | name: 'pe_ttm_mid', 86 | comment: 'pe_ttm中位数', 87 | precision: 12, 88 | scale: 4, 89 | default: () => "'0.0000'", 90 | }) 91 | peTtmMid: string; 92 | 93 | @Column('decimal', { 94 | name: 'total_mv', 95 | comment: '总市值', 96 | precision: 12, 97 | scale: 4, 98 | default: () => "'0.0000'", 99 | }) 100 | totalMv: string; 101 | 102 | @Column('json', { name: 'source_data', nullable: true, comment: '数据源' }) 103 | sourceData: object | null; 104 | 105 | @Column('datetime', { 106 | name: 'create_dt', 107 | comment: '创建时间', 108 | default: () => 'CURRENT_TIMESTAMP', 109 | }) 110 | createDt: Date; 111 | 112 | @Column('timestamp', { 113 | name: 'update_dt', 114 | nullable: true, 115 | comment: '修改时间', 116 | }) 117 | updateDt: Date | null; 118 | 119 | @Column('tinyint', { 120 | name: 'is_delete', 121 | comment: '是否删除', 122 | width: 1, 123 | default: () => "'0'", 124 | }) 125 | isDelete: boolean; 126 | 127 | @OneToMany(() => UserStock, (userStock) => userStock.code2) 128 | userStocks: UserStock[]; 129 | } 130 | -------------------------------------------------------------------------------- /stock-web/mock/notices.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | 3 | const getNotices = (req: Request, res: Response) => { 4 | res.json([ 5 | { 6 | id: '000000001', 7 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png', 8 | title: '你收到了 14 份新周报', 9 | datetime: '2017-08-09', 10 | type: 'notification', 11 | }, 12 | { 13 | id: '000000002', 14 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png', 15 | title: '你推荐的 曲妮妮 已通过第三轮面试', 16 | datetime: '2017-08-08', 17 | type: 'notification', 18 | }, 19 | { 20 | id: '000000003', 21 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png', 22 | title: '这种模板可以区分多种通知类型', 23 | datetime: '2017-08-07', 24 | read: true, 25 | type: 'notification', 26 | }, 27 | { 28 | id: '000000004', 29 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png', 30 | title: '左侧图标用于区分不同的类型', 31 | datetime: '2017-08-07', 32 | type: 'notification', 33 | }, 34 | { 35 | id: '000000005', 36 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png', 37 | title: '内容不要超过两行字,超出时自动截断', 38 | datetime: '2017-08-07', 39 | type: 'notification', 40 | }, 41 | { 42 | id: '000000006', 43 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg', 44 | title: '曲丽丽 评论了你', 45 | description: '描述信息描述信息描述信息', 46 | datetime: '2017-08-07', 47 | type: 'message', 48 | clickClose: true, 49 | }, 50 | { 51 | id: '000000007', 52 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg', 53 | title: '朱偏右 回复了你', 54 | description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像', 55 | datetime: '2017-08-07', 56 | type: 'message', 57 | clickClose: true, 58 | }, 59 | { 60 | id: '000000008', 61 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg', 62 | title: '标题', 63 | description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像', 64 | datetime: '2017-08-07', 65 | type: 'message', 66 | clickClose: true, 67 | }, 68 | { 69 | id: '000000009', 70 | title: '任务名称', 71 | description: '任务需要在 2017-01-12 20:00 前启动', 72 | extra: '未开始', 73 | status: 'todo', 74 | type: 'event', 75 | }, 76 | { 77 | id: '000000010', 78 | title: '第三方紧急代码变更', 79 | description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务', 80 | extra: '马上到期', 81 | status: 'urgent', 82 | type: 'event', 83 | }, 84 | { 85 | id: '000000011', 86 | title: '信息安全考试', 87 | description: '指派竹尔于 2017-01-09 前完成更新并发布', 88 | extra: '已耗时 8 天', 89 | status: 'doing', 90 | type: 'event', 91 | }, 92 | { 93 | id: '000000012', 94 | title: 'ABCD 版本发布', 95 | description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务', 96 | extra: '进行中', 97 | status: 'processing', 98 | type: 'event', 99 | }, 100 | ]); 101 | }; 102 | 103 | export default { 104 | 'GET /api/notices': getNotices, 105 | }; 106 | -------------------------------------------------------------------------------- /stock-web/src/locales/zh-CN/pages.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'pages.layouts.userLayout.title': '股票数据统计-开源', 3 | 'pages.login.accountLogin.tab': '账户密码登录', 4 | 'pages.login.accountLogin.errorMessage': '账户或密码错误', 5 | 'pages.login.username.placeholder': '用户名: guest', 6 | 'pages.login.username.required': '用户名是必填项!', 7 | 'pages.login.password.placeholder': '密码: 123456', 8 | 'pages.login.password.required': '密码是必填项!', 9 | 'pages.login.phoneLogin.tab': '手机号登录', 10 | 'pages.login.phoneLogin.errorMessage': '验证码错误', 11 | 'pages.login.phoneNumber.placeholder': '请输入手机号!', 12 | 'pages.login.phoneNumber.required': '手机号是必填项!', 13 | 'pages.login.phoneNumber.invalid': '不合法的手机号!', 14 | 'pages.login.captcha.placeholder': '请输入验证码!', 15 | 'pages.login.captcha.required': '验证码是必填项!', 16 | 'pages.login.phoneLogin.getVerificationCode': '获取验证码', 17 | 'pages.getCaptchaSecondText': '秒后重新获取', 18 | 'pages.login.rememberMe': '自动登录', 19 | 'pages.login.forgotPassword': '忘记密码 ?', 20 | 'pages.login.submit': '提交', 21 | 'pages.login.loginWith': '其他登录方式 :', 22 | 'pages.login.registerAccount': '注册账户', 23 | 'pages.welcome.advancedComponent': '高级表格', 24 | 'pages.welcome.link': '欢迎使用', 25 | 'pages.welcome.advancedLayout': '高级布局', 26 | 'pages.welcome.alertMessage': '更快更强的重型组件,已经发布。', 27 | 'pages.admin.subPage.title': ' 这个页面只有 admin 权限才能查看', 28 | 'pages.admin.subPage.alertMessage': 'umi ui 现已发布,欢迎使用 npm run ui 启动体验。', 29 | 'pages.searchTable.createForm.newRule': '新建规则', 30 | 'pages.searchTable.updateForm.ruleConfig': '规则配置', 31 | 'pages.searchTable.updateForm.basicConfig': '基本信息', 32 | 'pages.searchTable.updateForm.ruleName.nameLabel': '规则名称', 33 | 'pages.searchTable.updateForm.ruleName.nameRules': '请输入规则名称!', 34 | 'pages.searchTable.updateForm.ruleDesc.descLabel': '规则描述', 35 | 'pages.searchTable.updateForm.ruleDesc.descPlaceholder': '请输入至少五个字符', 36 | 'pages.searchTable.updateForm.ruleDesc.descRules': '请输入至少五个字符的规则描述!', 37 | 'pages.searchTable.updateForm.ruleProps.title': '配置规则属性', 38 | 'pages.searchTable.updateForm.object': '监控对象', 39 | 'pages.searchTable.updateForm.ruleProps.templateLabel': '规则模板', 40 | 'pages.searchTable.updateForm.ruleProps.typeLabel': '规则类型', 41 | 'pages.searchTable.updateForm.schedulingPeriod.title': '设定调度周期', 42 | 'pages.searchTable.updateForm.schedulingPeriod.timeLabel': '开始时间', 43 | 'pages.searchTable.updateForm.schedulingPeriod.timeRules': '请选择开始时间!', 44 | 'pages.searchTable.titleDesc': '描述', 45 | 'pages.searchTable.ruleName': '规则名称为必填项', 46 | 'pages.searchTable.titleCallNo': '服务调用次数', 47 | 'pages.searchTable.titleStatus': '状态', 48 | 'pages.searchTable.nameStatus.default': '关闭', 49 | 'pages.searchTable.nameStatus.running': '运行中', 50 | 'pages.searchTable.nameStatus.online': '已上线', 51 | 'pages.searchTable.nameStatus.abnormal': '异常', 52 | 'pages.searchTable.titleUpdatedAt': '上次调度时间', 53 | 'pages.searchTable.exception': '请输入异常原因!', 54 | 'pages.searchTable.titleOption': '操作', 55 | 'pages.searchTable.config': '配置', 56 | 'pages.searchTable.subscribeAlert': '订阅警报', 57 | 'pages.searchTable.title': '查询表格', 58 | 'pages.searchTable.new': '新建', 59 | 'pages.searchTable.chosen': '已选择', 60 | 'pages.searchTable.item': '项', 61 | 'pages.searchTable.totalServiceCalls': '服务调用次数总计', 62 | 'pages.searchTable.tenThousand': '万', 63 | 'pages.searchTable.batchDeletion': '批量删除', 64 | 'pages.searchTable.batchApproval': '批量审批', 65 | }; 66 | -------------------------------------------------------------------------------- /stock-web/src/models/login.ts: -------------------------------------------------------------------------------- 1 | import { stringify } from 'querystring'; 2 | import type { Reducer, Effect } from 'umi'; 3 | import { history } from 'umi'; 4 | 5 | import { mobileLogin } from '@/services/login'; 6 | import { setAuthority } from '@/utils/authority'; 7 | import { getPageQuery } from '@/utils/utils'; 8 | import { message } from 'antd'; 9 | 10 | // export type StateType = { 11 | // status?: 'ok' | 'error'; 12 | // type?: string; 13 | // currentAuthority?: 'user' | 'guest' | 'admin'; 14 | // }; 15 | 16 | export type StateType = { 17 | code?: number; 18 | data?: any; 19 | // currentAuthority?: 'user' | 'guest' | 'admin'; 20 | }; 21 | 22 | export type LoginModelType = { 23 | namespace: string; 24 | state: StateType; 25 | effects: { 26 | login: Effect; 27 | logout: Effect; 28 | }; 29 | reducers: { 30 | changeLoginStatus: Reducer; 31 | setLoginError: Reducer; 32 | }; 33 | }; 34 | 35 | const Model: LoginModelType = { 36 | namespace: 'login', 37 | 38 | state: { 39 | code: undefined, 40 | }, 41 | 42 | effects: { 43 | *login({ payload }, { call, put }) { 44 | const response = yield call(mobileLogin, payload); 45 | if (response && response.code === 0) { 46 | yield put({ 47 | type: 'changeLoginStatus', 48 | payload: response, 49 | }); 50 | const urlParams = new URL(window.location.href); 51 | const params = getPageQuery(); 52 | message.success('🎉 🎉 🎉 登录成功!'); 53 | let { redirect } = params as { redirect: string }; 54 | if (redirect) { 55 | const redirectUrlParams = new URL(redirect); 56 | if (redirectUrlParams.origin === urlParams.origin) { 57 | // 加上 /stock-web 的长度 58 | redirect = redirect.substr(urlParams.origin.length + 10); 59 | if (redirect.match(/^\/.*#/)) { 60 | redirect = redirect.substr(redirect.indexOf('#') + 1); 61 | } 62 | } else { 63 | window.location.href = '/'; 64 | return; 65 | } 66 | } 67 | // console.log('url是:', redirect); 68 | history.replace(redirect || '/'); 69 | } else { 70 | yield put({ 71 | type: 'setLoginError', 72 | payload: response.code, 73 | }); 74 | } 75 | }, 76 | 77 | logout() { 78 | const { redirect } = getPageQuery(); 79 | // Note: There may be security issues, please note 80 | // 清除localstorage 81 | localStorage.setItem('user-role', ''); 82 | localStorage.setItem('user-token', ''); 83 | if (window.location.pathname !== '/login' && !redirect) { 84 | history.replace({ 85 | pathname: '/login', 86 | search: stringify({ 87 | redirect: window.location.href, 88 | }), 89 | }); 90 | } 91 | }, 92 | }, 93 | 94 | reducers: { 95 | changeLoginStatus(state, { payload }) { 96 | setAuthority(payload); 97 | return { 98 | ...state, 99 | code: payload.code, 100 | type: payload.type, 101 | }; 102 | }, 103 | setLoginError(state, { payload }) { 104 | return { 105 | ...state, 106 | code: payload, 107 | }; 108 | }, 109 | }, 110 | }; 111 | 112 | export default Model; 113 | -------------------------------------------------------------------------------- /stock-web/src/locales/en-US/settings.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.settings.menuMap.basic': 'Basic Settings', 3 | 'app.settings.menuMap.security': 'Security Settings', 4 | 'app.settings.menuMap.binding': 'Account Binding', 5 | 'app.settings.menuMap.notification': 'New Message Notification', 6 | 'app.settings.basic.avatar': 'Avatar', 7 | 'app.settings.basic.change-avatar': 'Change avatar', 8 | 'app.settings.basic.email': 'Email', 9 | 'app.settings.basic.email-message': 'Please input your email!', 10 | 'app.settings.basic.nickname': 'Nickname', 11 | 'app.settings.basic.nickname-message': 'Please input your Nickname!', 12 | 'app.settings.basic.profile': 'Personal profile', 13 | 'app.settings.basic.profile-message': 'Please input your personal profile!', 14 | 'app.settings.basic.profile-placeholder': 'Brief introduction to yourself', 15 | 'app.settings.basic.country': 'Country/Region', 16 | 'app.settings.basic.country-message': 'Please input your country!', 17 | 'app.settings.basic.geographic': 'Province or city', 18 | 'app.settings.basic.geographic-message': 'Please input your geographic info!', 19 | 'app.settings.basic.address': 'Street Address', 20 | 'app.settings.basic.address-message': 'Please input your address!', 21 | 'app.settings.basic.phone': 'Phone Number', 22 | 'app.settings.basic.phone-message': 'Please input your phone!', 23 | 'app.settings.basic.update': 'Update Information', 24 | 'app.settings.security.strong': 'Strong', 25 | 'app.settings.security.medium': 'Medium', 26 | 'app.settings.security.weak': 'Weak', 27 | 'app.settings.security.password': 'Account Password', 28 | 'app.settings.security.password-description': 'Current password strength', 29 | 'app.settings.security.phone': 'Security Phone', 30 | 'app.settings.security.phone-description': 'Bound phone', 31 | 'app.settings.security.question': 'Security Question', 32 | 'app.settings.security.question-description': 33 | 'The security question is not set, and the security policy can effectively protect the account security', 34 | 'app.settings.security.email': 'Backup Email', 35 | 'app.settings.security.email-description': 'Bound Email', 36 | 'app.settings.security.mfa': 'MFA Device', 37 | 'app.settings.security.mfa-description': 38 | 'Unbound MFA device, after binding, can be confirmed twice', 39 | 'app.settings.security.modify': 'Modify', 40 | 'app.settings.security.set': 'Set', 41 | 'app.settings.security.bind': 'Bind', 42 | 'app.settings.binding.taobao': 'Binding Taobao', 43 | 'app.settings.binding.taobao-description': 'Currently unbound Taobao account', 44 | 'app.settings.binding.alipay': 'Binding Alipay', 45 | 'app.settings.binding.alipay-description': 'Currently unbound Alipay account', 46 | 'app.settings.binding.dingding': 'Binding DingTalk', 47 | 'app.settings.binding.dingding-description': 'Currently unbound DingTalk account', 48 | 'app.settings.binding.bind': 'Bind', 49 | 'app.settings.notification.password': 'Account Password', 50 | 'app.settings.notification.password-description': 51 | 'Messages from other users will be notified in the form of a station letter', 52 | 'app.settings.notification.messages': 'System Messages', 53 | 'app.settings.notification.messages-description': 54 | 'System messages will be notified in the form of a station letter', 55 | 'app.settings.notification.todo': 'To-do Notification', 56 | 'app.settings.notification.todo-description': 57 | 'The to-do list will be notified in the form of a letter from the station', 58 | 'app.settings.open': 'Open', 59 | 'app.settings.close': 'Close', 60 | }; 61 | -------------------------------------------------------------------------------- /stock-web/src/locales/id-ID/settings.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.settings.menuMap.basic': 'Pengaturan Dasar', 3 | 'app.settings.menuMap.security': 'Pengaturan Keamanan', 4 | 'app.settings.menuMap.binding': 'Pengikatan Akun', 5 | 'app.settings.menuMap.notification': 'Notifikasi Pesan Baru', 6 | 'app.settings.basic.avatar': 'Avatar', 7 | 'app.settings.basic.change-avatar': 'Ubah avatar', 8 | 'app.settings.basic.email': 'Email', 9 | 'app.settings.basic.email-message': 'Tolong masukkan email!', 10 | 'app.settings.basic.nickname': 'Nickname', 11 | 'app.settings.basic.nickname-message': 'Tolong masukkan Nickname!', 12 | 'app.settings.basic.profile': 'Profil Personal', 13 | 'app.settings.basic.profile-message': 'Tolong masukkan profil personal!', 14 | 'app.settings.basic.profile-placeholder': 'Perkenalan Singkat tentang Diri Anda', 15 | 'app.settings.basic.country': 'Negara/Wilayah', 16 | 'app.settings.basic.country-message': 'Tolong masukkan negara anda!', 17 | 'app.settings.basic.geographic': 'Provinsi atau kota', 18 | 'app.settings.basic.geographic-message': 'Tolong masukkan info geografis anda!', 19 | 'app.settings.basic.address': 'Alamat Jalan', 20 | 'app.settings.basic.address-message': 'Tolong masukkan Alamat Jalan anda!', 21 | 'app.settings.basic.phone': 'Nomor Ponsel', 22 | 'app.settings.basic.phone-message': 'Tolong masukkan Nomor Ponsel anda!', 23 | 'app.settings.basic.update': 'Perbarui Informasi', 24 | 'app.settings.security.strong': 'Kuat', 25 | 'app.settings.security.medium': 'Sedang', 26 | 'app.settings.security.weak': 'Lemah', 27 | 'app.settings.security.password': 'Kata Sandi Akun', 28 | 'app.settings.security.password-description': 'Kekuatan Kata Sandi saat ini', 29 | 'app.settings.security.phone': 'Keamanan Ponsel', 30 | 'app.settings.security.phone-description': 'Mengikat Ponsel', 31 | 'app.settings.security.question': 'Pertanyaan Keamanan', 32 | 'app.settings.security.question-description': 33 | 'Pertanyaan Keamanan belum diatur, dan kebijakan keamanan dapat melindungi akun secara efektif', 34 | 'app.settings.security.email': 'Email Cadangan', 35 | 'app.settings.security.email-description': 'Mengikat Email', 36 | 'app.settings.security.mfa': 'Perangka MFA', 37 | 'app.settings.security.mfa-description': 38 | 'Tidak mengikat Perangkat MFA, setelah diikat, dapat dikonfirmasi dua kali', 39 | 'app.settings.security.modify': 'Modifikasi', 40 | 'app.settings.security.set': 'Setel', 41 | 'app.settings.security.bind': 'Ikat', 42 | 'app.settings.binding.taobao': 'Mengikat Taobao', 43 | 'app.settings.binding.taobao-description': 'Tidak mengikat akun Taobao saat ini', 44 | 'app.settings.binding.alipay': 'Mengikat Alipay', 45 | 'app.settings.binding.alipay-description': 'Tidak mengikat akun Alipay saat ini', 46 | 'app.settings.binding.dingding': 'Mengikat DingTalk', 47 | 'app.settings.binding.dingding-description': 'Tidak mengikat akun DingTalk', 48 | 'app.settings.binding.bind': 'Ikat', 49 | 'app.settings.notification.password': 'Kata Sandi Akun', 50 | 'app.settings.notification.password-description': 51 | 'Pesan dari pengguna lain akan diberitahu dalam bentuk surat', 52 | 'app.settings.notification.messages': 'Pesan Sistem', 53 | 'app.settings.notification.messages-description': 54 | 'Pesan sistem akan diberitahu dalam bentuk surat', 55 | 'app.settings.notification.todo': 'Notifikasi daftar To-do', 56 | 'app.settings.notification.todo-description': 57 | 'Daftar to-do akan diberitahukan dalam bentuk surat dari stasiun', 58 | 'app.settings.open': 'Buka', 59 | 'app.settings.close': 'Tutup', 60 | }; 61 | -------------------------------------------------------------------------------- /stock-server/README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 6 | [circleci-url]: https://circleci.com/gh/nestjs/nest 7 | 8 |

A progressive Node.js framework for building efficient and scalable server-side applications.

9 |

10 | NPM Version 11 | Package License 12 | NPM Downloads 13 | CircleCI 14 | Coverage 15 | Discord 16 | Backers on Open Collective 17 | Sponsors on Open Collective 18 | 19 | Support us 20 | 21 |

22 | 24 | 25 | ## Description 26 | 27 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 28 | 29 | ## Installation 30 | 31 | ```bash 32 | $ npm install 33 | ``` 34 | 35 | ## Running the app 36 | 37 | ```bash 38 | # development 39 | $ npm run start 40 | 41 | # watch mode 42 | $ npm run start:dev 43 | 44 | # production mode 45 | $ npm run start:prod 46 | ``` 47 | 48 | ## Test 49 | 50 | ```bash 51 | # unit tests 52 | $ npm run test 53 | 54 | # e2e tests 55 | $ npm run test:e2e 56 | 57 | # test coverage 58 | $ npm run test:cov 59 | ``` 60 | 61 | ## Support 62 | 63 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 64 | 65 | ## Stay in touch 66 | 67 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 68 | - Website - [https://nestjs.com](https://nestjs.com/) 69 | - Twitter - [@nestframework](https://twitter.com/nestframework) 70 | 71 | ## License 72 | 73 | Nest is [MIT licensed](LICENSE). 74 | -------------------------------------------------------------------------------- /stock-web/src/components/NoticeIcon/NoticeList.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar, List } from 'antd'; 2 | 3 | import React from 'react'; 4 | import classNames from 'classnames'; 5 | import type { NoticeIconData } from './index'; 6 | import styles from './NoticeList.less'; 7 | 8 | export type NoticeIconTabProps = { 9 | count?: number; 10 | name?: string; 11 | showClear?: boolean; 12 | showViewMore?: boolean; 13 | style?: React.CSSProperties; 14 | title: string; 15 | tabKey: string; 16 | data?: NoticeIconData[]; 17 | onClick?: (item: NoticeIconData) => void; 18 | onClear?: () => void; 19 | emptyText?: string; 20 | clearText?: string; 21 | viewMoreText?: string; 22 | list: NoticeIconData[]; 23 | onViewMore?: (e: any) => void; 24 | }; 25 | const NoticeList: React.SFC = ({ 26 | data = [], 27 | onClick, 28 | onClear, 29 | title, 30 | onViewMore, 31 | emptyText, 32 | showClear = true, 33 | clearText, 34 | viewMoreText, 35 | showViewMore = false, 36 | }) => { 37 | if (!data || data.length === 0) { 38 | return ( 39 |
40 | not found 44 |
{emptyText}
45 |
46 | ); 47 | } 48 | return ( 49 |
50 | 51 | className={styles.list} 52 | dataSource={data} 53 | renderItem={(item, i) => { 54 | const itemCls = classNames(styles.item, { 55 | [styles.read]: item.read, 56 | }); 57 | // eslint-disable-next-line no-nested-ternary 58 | const leftIcon = item.avatar ? ( 59 | typeof item.avatar === 'string' ? ( 60 | 61 | ) : ( 62 | {item.avatar} 63 | ) 64 | ) : null; 65 | 66 | return ( 67 | { 71 | onClick?.(item); 72 | }} 73 | > 74 | 79 | {item.title} 80 |
{item.extra}
81 |
82 | } 83 | description={ 84 |
85 |
{item.description}
86 |
{item.datetime}
87 |
88 | } 89 | /> 90 | 91 | ); 92 | }} 93 | /> 94 |
95 | {showClear ? ( 96 |
97 | {clearText} {title} 98 |
99 | ) : null} 100 | {showViewMore ? ( 101 |
{ 103 | if (onViewMore) { 104 | onViewMore(e); 105 | } 106 | }} 107 | > 108 | {viewMoreText} 109 |
110 | ) : null} 111 |
112 | 113 | ); 114 | }; 115 | 116 | export default NoticeList; 117 | -------------------------------------------------------------------------------- /stock-web/src/locales/ja-JP/pages.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'pages.layouts.userLayout.title': 'Ant Designは、西湖区で最も影響力のあるWebデザイン仕様です。', 3 | 'pages.login.accountLogin.tab': 'アカウントログイン', 4 | 'pages.login.accountLogin.errorMessage': 5 | 'ユーザー名/パスワードが正しくありません(admin/ant.design)', 6 | 'pages.login.username.placeholder': 'ユーザー名:adminまたはuser', 7 | 'pages.login.username.required': 'ユーザー名を入力してください!', 8 | 'pages.login.password.placeholder': 'パスワード:ant.design', 9 | 'pages.login.password.required': 'パスワードを入力してください!', 10 | 'pages.login.phoneLogin.tab': '電話ログイン', 11 | 'pages.login.phoneLogin.errorMessage': '検証コードエラー', 12 | 'pages.login.phoneNumber.placeholder': '電話番号', 13 | 'pages.login.phoneNumber.required': '電話番号を入力してください!', 14 | 'pages.login.phoneNumber.invalid': '電話番号が無効です!', 15 | 'pages.login.captcha.placeholder': '確認コード', 16 | 'pages.login.captcha.required': '確認コードを入力してください!', 17 | 'pages.login.phoneLogin.getVerificationCode': '確認コードを取得', 18 | 'pages.getCaptchaSecondText': '秒', 19 | 'pages.login.rememberMe': 'Remember me', 20 | 'pages.login.forgotPassword': 'パスワードをお忘れですか?', 21 | 'pages.login.submit': 'ログイン', 22 | 'pages.login.loginWith': 'その他のログイン方法:', 23 | 'pages.login.registerAccount': 'アカウント登録', 24 | 'pages.welcome.advancedComponent': '高度なコンポーネント', 25 | 'pages.welcome.link': 'ようこそ', 26 | 'pages.welcome.advancedLayout': '高度なレイアウト', 27 | 'pages.welcome.alertMessage': 'より高速で強力な頑丈なコンポーネントがリリースされました。', 28 | 'pages.admin.subPage.title': 'このページは管理者のみが表示できます', 29 | 'pages.admin.subPage.alertMessage': 30 | 'Umi uiがリリースされました。npm run uiを使用して体験してください。', 31 | 'pages.searchTable.createForm.newRule': '新しいルール', 32 | 'pages.searchTable.updateForm.ruleConfig': 'ルール構成', 33 | 'pages.searchTable.updateForm.basicConfig': '基本情報', 34 | 'pages.searchTable.updateForm.ruleName.nameLabel': 'ルール名', 35 | 'pages.searchTable.updateForm.ruleName.nameRules': 'ルール名を入力してください!', 36 | 'pages.searchTable.updateForm.ruleDesc.descLabel': 'ルールの説明', 37 | 'pages.searchTable.updateForm.ruleDesc.descPlaceholder': '5文字以上入力してください', 38 | 'pages.searchTable.updateForm.ruleDesc.descRules': '5文字以上のルールの説明を入力してください!', 39 | 'pages.searchTable.updateForm.ruleProps.title': 'プロパティの構成', 40 | 'pages.searchTable.updateForm.object': '監視対象', 41 | 'pages.searchTable.updateForm.ruleProps.templateLabel': 'ルールテンプレート', 42 | 'pages.searchTable.updateForm.ruleProps.typeLabel': 'ルールタイプ', 43 | 'pages.searchTable.updateForm.schedulingPeriod.title': 'スケジュール期間の設定', 44 | 'pages.searchTable.updateForm.schedulingPeriod.timeLabel': '開始時間', 45 | 'pages.searchTable.updateForm.schedulingPeriod.timeRules': '開始時間を選択してください!', 46 | 'pages.searchTable.titleDesc': '説明', 47 | 'pages.searchTable.ruleName': 'ルール名が必要です', 48 | 'pages.searchTable.titleCallNo': 'サービスコール数', 49 | 'pages.searchTable.titleStatus': 'ステータス', 50 | 'pages.searchTable.nameStatus.default': 'デフォルト', 51 | 'pages.searchTable.nameStatus.running': '起動中', 52 | 'pages.searchTable.nameStatus.online': 'オンライン', 53 | 'pages.searchTable.nameStatus.abnormal': '異常', 54 | 'pages.searchTable.titleUpdatedAt': '最終スケジュール', 55 | 'pages.searchTable.exception': '例外の理由を入力してください!', 56 | 'pages.searchTable.titleOption': 'オプション', 57 | 'pages.searchTable.config': '構成', 58 | 'pages.searchTable.subscribeAlert': 'アラートを購読する', 59 | 'pages.searchTable.title': 'お問い合わせフォーム', 60 | 'pages.searchTable.new': '新しい', 61 | 'pages.searchTable.chosen': '選んだ項目', 62 | 'pages.searchTable.item': '項目', 63 | 'pages.searchTable.totalServiceCalls': 'サービスコールの総数', 64 | 'pages.searchTable.tenThousand': '万', 65 | 'pages.searchTable.batchDeletion': 'バッチ削除', 66 | 'pages.searchTable.batchApproval': 'バッチ承認', 67 | }; 68 | -------------------------------------------------------------------------------- /stock-web/src/locales/pt-BR/settings.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.settings.menuMap.basic': 'Configurações Básicas', 3 | 'app.settings.menuMap.security': 'Configurações de Segurança', 4 | 'app.settings.menuMap.binding': 'Vinculação de Conta', 5 | 'app.settings.menuMap.notification': 'Mensagens de Notificação', 6 | 'app.settings.basic.avatar': 'Avatar', 7 | 'app.settings.basic.change-avatar': 'Alterar avatar', 8 | 'app.settings.basic.email': 'Email', 9 | 'app.settings.basic.email-message': 'Por favor insira seu email!', 10 | 'app.settings.basic.nickname': 'Nome de usuário', 11 | 'app.settings.basic.nickname-message': 'Por favor insira seu nome de usuário!', 12 | 'app.settings.basic.profile': 'Perfil pessoal', 13 | 'app.settings.basic.profile-message': 'Por favor insira seu perfil pessoal!', 14 | 'app.settings.basic.profile-placeholder': 'Breve introdução sua', 15 | 'app.settings.basic.country': 'País/Região', 16 | 'app.settings.basic.country-message': 'Por favor insira país!', 17 | 'app.settings.basic.geographic': 'Província, estado ou cidade', 18 | 'app.settings.basic.geographic-message': 'Por favor insira suas informações geográficas!', 19 | 'app.settings.basic.address': 'Endereço', 20 | 'app.settings.basic.address-message': 'Por favor insira seu endereço!', 21 | 'app.settings.basic.phone': 'Número de telefone', 22 | 'app.settings.basic.phone-message': 'Por favor insira seu número de telefone!', 23 | 'app.settings.basic.update': 'Atualizar Informações', 24 | 'app.settings.security.strong': 'Forte', 25 | 'app.settings.security.medium': 'Média', 26 | 'app.settings.security.weak': 'Fraca', 27 | 'app.settings.security.password': 'Senha da Conta', 28 | 'app.settings.security.password-description': 'Força da senha', 29 | 'app.settings.security.phone': 'Telefone de Seguraça', 30 | 'app.settings.security.phone-description': 'Telefone vinculado', 31 | 'app.settings.security.question': 'Pergunta de Segurança', 32 | 'app.settings.security.question-description': 33 | 'A pergunta de segurança não está definida e a política de segurança pode proteger efetivamente a segurança da conta', 34 | 'app.settings.security.email': 'Email de Backup', 35 | 'app.settings.security.email-description': 'Email vinculado', 36 | 'app.settings.security.mfa': 'Dispositivo MFA', 37 | 'app.settings.security.mfa-description': 38 | 'O dispositivo MFA não vinculado, após a vinculação, pode ser confirmado duas vezes', 39 | 'app.settings.security.modify': 'Modificar', 40 | 'app.settings.security.set': 'Atribuir', 41 | 'app.settings.security.bind': 'Vincular', 42 | 'app.settings.binding.taobao': 'Vincular Taobao', 43 | 'app.settings.binding.taobao-description': 'Atualmente não vinculado à conta Taobao', 44 | 'app.settings.binding.alipay': 'Vincular Alipay', 45 | 'app.settings.binding.alipay-description': 'Atualmente não vinculado à conta Alipay', 46 | 'app.settings.binding.dingding': 'Vincular DingTalk', 47 | 'app.settings.binding.dingding-description': 'Atualmente não vinculado à conta DingTalk', 48 | 'app.settings.binding.bind': 'Vincular', 49 | 'app.settings.notification.password': 'Senha da Conta', 50 | 'app.settings.notification.password-description': 51 | 'Mensagens de outros usuários serão notificadas na forma de uma estação de letra', 52 | 'app.settings.notification.messages': 'Mensagens de Sistema', 53 | 'app.settings.notification.messages-description': 54 | 'Mensagens de sistema serão notificadas na forma de uma estação de letra', 55 | 'app.settings.notification.todo': 'Notificação de To-do', 56 | 'app.settings.notification.todo-description': 57 | 'A lista de to-do será notificada na forma de uma estação de letra', 58 | 'app.settings.open': 'Aberto', 59 | 'app.settings.close': 'Fechado', 60 | }; 61 | -------------------------------------------------------------------------------- /stock-web/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | Group 28 Copy 5Created with Sketch. -------------------------------------------------------------------------------- /stock-web/src/models/global.ts: -------------------------------------------------------------------------------- 1 | import type { Reducer, Effect } from 'umi'; 2 | 3 | import type { NoticeIconData } from '@/components/NoticeIcon'; 4 | import { queryNotices } from '@/services/user'; 5 | import type { ConnectState } from './connect.d'; 6 | 7 | export type NoticeItem = { 8 | id: string; 9 | type: string; 10 | status: string; 11 | } & NoticeIconData; 12 | 13 | export type GlobalModelState = { 14 | collapsed: boolean; 15 | notices: NoticeItem[]; 16 | }; 17 | 18 | export type GlobalModelType = { 19 | namespace: 'global'; 20 | state: GlobalModelState; 21 | effects: { 22 | fetchNotices: Effect; 23 | clearNotices: Effect; 24 | changeNoticeReadState: Effect; 25 | }; 26 | reducers: { 27 | changeLayoutCollapsed: Reducer; 28 | saveNotices: Reducer; 29 | saveClearedNotices: Reducer; 30 | }; 31 | }; 32 | 33 | const GlobalModel: GlobalModelType = { 34 | namespace: 'global', 35 | 36 | state: { 37 | collapsed: false, 38 | notices: [], 39 | }, 40 | 41 | effects: { 42 | *fetchNotices(_, { call, put, select }) { 43 | const data = yield call(queryNotices); 44 | yield put({ 45 | type: 'saveNotices', 46 | payload: data, 47 | }); 48 | const unreadCount: number = yield select( 49 | (state: ConnectState) => state.global.notices.filter((item) => !item.read).length, 50 | ); 51 | yield put({ 52 | type: 'user/changeNotifyCount', 53 | payload: { 54 | totalCount: data.length, 55 | unreadCount, 56 | }, 57 | }); 58 | }, 59 | *clearNotices({ payload }, { put, select }) { 60 | yield put({ 61 | type: 'saveClearedNotices', 62 | payload, 63 | }); 64 | const count: number = yield select((state: ConnectState) => state.global.notices.length); 65 | const unreadCount: number = yield select( 66 | (state: ConnectState) => state.global.notices.filter((item) => !item.read).length, 67 | ); 68 | yield put({ 69 | type: 'user/changeNotifyCount', 70 | payload: { 71 | totalCount: count, 72 | unreadCount, 73 | }, 74 | }); 75 | }, 76 | *changeNoticeReadState({ payload }, { put, select }) { 77 | const notices: NoticeItem[] = yield select((state: ConnectState) => 78 | state.global.notices.map((item) => { 79 | const notice = { ...item }; 80 | if (notice.id === payload) { 81 | notice.read = true; 82 | } 83 | return notice; 84 | }), 85 | ); 86 | 87 | yield put({ 88 | type: 'saveNotices', 89 | payload: notices, 90 | }); 91 | 92 | yield put({ 93 | type: 'user/changeNotifyCount', 94 | payload: { 95 | totalCount: notices.length, 96 | unreadCount: notices.filter((item) => !item.read).length, 97 | }, 98 | }); 99 | }, 100 | }, 101 | 102 | reducers: { 103 | changeLayoutCollapsed(state = { notices: [], collapsed: true }, { payload }): GlobalModelState { 104 | return { 105 | ...state, 106 | collapsed: payload, 107 | }; 108 | }, 109 | saveNotices(state, { payload }): GlobalModelState { 110 | return { 111 | collapsed: false, 112 | ...state, 113 | notices: payload, 114 | }; 115 | }, 116 | saveClearedNotices(state = { notices: [], collapsed: true }, { payload }): GlobalModelState { 117 | return { 118 | ...state, 119 | collapsed: false, 120 | notices: state.notices.filter((item): boolean => item.type !== payload), 121 | }; 122 | }, 123 | }, 124 | }; 125 | 126 | export default GlobalModel; 127 | -------------------------------------------------------------------------------- /stock-server/docs/stock-demo.sql: -------------------------------------------------------------------------------- 1 | -- 创建数据库 2 | CREATE SCHEMA `stock_demo` DEFAULT CHARACTER SET utf8mb4 ; 3 | 4 | -- --------------------------------------- 5 | -- 用户表 简化角色 6 | -- --------------------------------------- 7 | CREATE TABLE `st_user` ( 8 | id int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id', 9 | mobile varchar(20) NOT NULL DEFAULT '' COMMENT '手机号', 10 | name varchar(20) NOT NULL DEFAULT '' COMMENT '昵称', 11 | role tinyint(1) NOT NULL DEFAULT '0' COMMENT '角色:100 超级管理员,0 普通用户', 12 | password varchar(100) NOT NULL DEFAULT '' COMMENT '密码', 13 | salt varchar(100) NOT NULL DEFAULT '' COMMENT '密码盐', 14 | create_dt datetime NOT NULL DEFAULT now() COMMENT '创建时间', 15 | update_dt timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', 16 | is_delete tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除', 17 | PRIMARY KEY (`id`), 18 | UNIQUE KEY `uk_mobile` (`mobile`) 19 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; 20 | 21 | -- --------------------------------------- 22 | -- 股票表 23 | -- --------------------------------------- 24 | CREATE TABLE `stock` ( 25 | id int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id', 26 | code varchar(10) NOT NULL DEFAULT '' COMMENT '股票代码', 27 | name varchar(30) NOT NULL DEFAULT '' COMMENT '股票名称', 28 | market varchar(10) NOT NULL DEFAULT '' COMMENT '股票市场', 29 | price decimal(12,4) NOT NULL DEFAULT '0.00' COMMENT '当前股价', 30 | pe decimal(12,4) NOT NULL DEFAULT '0.00' COMMENT '当前PE(市盈率)', 31 | pe_avg decimal(12,4) NOT NULL DEFAULT '0.00' COMMENT '平均PE(市盈率)', 32 | pe_ttm decimal(12,4) NOT NULL DEFAULT '0.00' COMMENT '当前PE TTM(市盈率)', 33 | pe_ttm_avg decimal(12,4) NOT NULL DEFAULT '0.00' COMMENT '平均PE TTM(市盈率)', 34 | pe_ttm_rate decimal(12,4) NOT NULL DEFAULT '0.00' COMMENT '最新pet_tm与平均值的比例', 35 | pe_ttm_mid decimal(12,4) NOT NULL DEFAULT '0.00' COMMENT 'pe_ttm中位数', 36 | total_mv decimal(12,4) NOT NULL DEFAULT '0.00' COMMENT '总市值', 37 | source_data json NULL COMMENT '数据源', 38 | create_dt datetime NOT NULL DEFAULT now() COMMENT '创建时间', 39 | update_dt timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', 40 | is_delete tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除', 41 | PRIMARY KEY (`id`), 42 | UNIQUE KEY `uk_stock_code` (`code`) 43 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; 44 | 45 | -- --------------------------------------- 46 | -- 股票流水账: 记录每日pe pb等值,简化为只记录pe 47 | -- --------------------------------------- 48 | CREATE TABLE `stock_log` ( 49 | id int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id', 50 | code varchar(10) NOT NULL DEFAULT '' COMMENT '股票代码', 51 | log_date date NOT NULL COMMENT '日期', 52 | pe decimal(12,4) NOT NULL DEFAULT '0.00' COMMENT '当前PE(市盈率)', 53 | pe_ttm decimal(12,4) NOT NULL DEFAULT '0.00' COMMENT '当前PE TTM(市盈率)', 54 | total_mv decimal(16,4) NOT NULL DEFAULT '0.00' COMMENT '总市值', 55 | create_dt datetime NOT NULL DEFAULT now() COMMENT '创建时间', 56 | update_dt timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', 57 | is_delete tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除', 58 | PRIMARY KEY (`id`), 59 | UNIQUE KEY `uk_code_log_date` (`code` ASC,`log_date` ASC), 60 | INDEX `idx_code` (`code` ASC) 61 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; 62 | 63 | -- --------------------------------------- 64 | -- 用户股票自选列表 65 | -- --------------------------------------- 66 | CREATE TABLE `user_stock` ( 67 | id int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id', 68 | uid int(11) NOT NULL DEFAULT '0' COMMENT '用户id', 69 | code varchar(10) NOT NULL DEFAULT '' COMMENT '股票代码', 70 | create_dt datetime NOT NULL DEFAULT now() COMMENT '创建时间', 71 | update_dt timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', 72 | is_delete tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除', 73 | PRIMARY KEY (`id`), 74 | UNIQUE KEY `uk_uid_code` (`uid` ASC,`code` ASC), 75 | FOREIGN KEY (uid) REFERENCES st_user(id), 76 | FOREIGN KEY (code) REFERENCES stock(code) 77 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; --------------------------------------------------------------------------------