diff --git a/admin-web/README-antd.md b/admin-web/README-antd.md deleted file mode 100644 index ce0725dfd..000000000 --- a/admin-web/README-antd.md +++ /dev/null @@ -1,136 +0,0 @@ -English | [简体中文](./README.zh-CN.md) | [Русский](./README.ru-RU.md) - -

Ant Design Pro

- -
- -An out-of-box UI solution for enterprise applications as a React boilerplate. - -[![Build With Umi](https://img.shields.io/badge/build%20with-umi-028fe4.svg?style=flat-square)](http://umijs.org/) -[![Build Status](https://dev.azure.com/ant-design/ant-design-pro/_apis/build/status/ant-design.ant-design-pro?branchName=master)](https://dev.azure.com/ant-design/ant-design-pro/_build/latest?definitionId=1?branchName=master) -[![Dependencies](https://img.shields.io/david/ant-design/ant-design-pro.svg)](https://david-dm.org/ant-design/ant-design-pro) -[![DevDependencies](https://img.shields.io/david/dev/ant-design/ant-design-pro.svg)](https://david-dm.org/ant-design/ant-design-pro?type=dev) -[![Gitter](https://img.shields.io/gitter/room/ant-design/pro-english.svg?style=flat-square&logoWidth=20&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgd2lkdGg9IjEyMzUiIGhlaWdodD0iNjUwIiB2aWV3Qm94PSIwIDAgNzQxMCAzOTAwIj4NCjxyZWN0IHdpZHRoPSI3NDEwIiBoZWlnaHQ9IjM5MDAiIGZpbGw9IiNiMjIyMzQiLz4NCjxwYXRoIGQ9Ik0wLDQ1MEg3NDEwbTAsNjAwSDBtMCw2MDBINzQxMG0wLDYwMEgwbTAsNjAwSDc0MTBtMCw2MDBIMCIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjMwMCIvPg0KPHJlY3Qgd2lkdGg9IjI5NjQiIGhlaWdodD0iMjEwMCIgZmlsbD0iIzNjM2I2ZSIvPg0KPGcgZmlsbD0iI2ZmZiI%2BDQo8ZyBpZD0iczE4Ij4NCjxnIGlkPSJzOSI%2BDQo8ZyBpZD0iczUiPg0KPGcgaWQ9InM0Ij4NCjxwYXRoIGlkPSJzIiBkPSJNMjQ3LDkwIDMxNy41MzQyMzAsMzA3LjA4MjAzOSAxMzIuODczMjE4LDE3Mi45MTc5NjFIMzYxLjEyNjc4MkwxNzYuNDY1NzcwLDMwNy4wODIwMzl6Ii8%2BDQo8dXNlIHhsaW5rOmhyZWY9IiNzIiB5PSI0MjAiLz4NCjx1c2UgeGxpbms6aHJlZj0iI3MiIHk9Ijg0MCIvPg0KPHVzZSB4bGluazpocmVmPSIjcyIgeT0iMTI2MCIvPg0KPC9nPg0KPHVzZSB4bGluazpocmVmPSIjcyIgeT0iMTY4MCIvPg0KPC9nPg0KPHVzZSB4bGluazpocmVmPSIjczQiIHg9IjI0NyIgeT0iMjEwIi8%2BDQo8L2c%2BDQo8dXNlIHhsaW5rOmhyZWY9IiNzOSIgeD0iNDk0Ii8%2BDQo8L2c%2BDQo8dXNlIHhsaW5rOmhyZWY9IiNzMTgiIHg9Ijk4OCIvPg0KPHVzZSB4bGluazpocmVmPSIjczkiIHg9IjE5NzYiLz4NCjx1c2UgeGxpbms6aHJlZj0iI3M1IiB4PSIyNDcwIi8%2BDQo8L2c%2BDQo8L3N2Zz4%3D)](https://gitter.im/ant-design/pro-english?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) -[![Join the chat at https://gitter.im/ant-design/ant-design-pro](https://img.shields.io/gitter/room/ant-design/ant-design-pro.svg?style=flat-square&logoWidth=20&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgd2lkdGg9IjkwMCIgaGVpZ2h0PSI2MDAiIHZpZXdCb3g9IjAgMCAzMCAyMCI%2BDQo8ZGVmcz4NCjxwYXRoIGlkPSJzIiBkPSJNMCwtMSAwLjU4Nzc4NSwwLjgwOTAxNyAtMC45NTEwNTcsLTAuMzA5MDE3SDAuOTUxMDU3TC0wLjU4Nzc4NSwwLjgwOTAxN3oiIGZpbGw9IiNmZmRlMDAiLz4NCjwvZGVmcz4NCjxyZWN0IHdpZHRoPSIzMCIgaGVpZ2h0PSIyMCIgZmlsbD0iI2RlMjkxMCIvPg0KPHVzZSB4bGluazpocmVmPSIjcyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNSw1KSBzY2FsZSgzKSIvPg0KPHVzZSB4bGluazpocmVmPSIjcyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTAsMikgcm90YXRlKDIzLjAzNjI0MykiLz4NCjx1c2UgeGxpbms6aHJlZj0iI3MiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEyLDQpIHJvdGF0ZSg0NS44Njk4OTgpIi8%2BDQo8dXNlIHhsaW5rOmhyZWY9IiNzIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxMiw3KSByb3RhdGUoNjkuOTQ1Mzk2KSIvPg0KPHVzZSB4bGluazpocmVmPSIjcyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTAsOSkgcm90YXRlKDIwLjY1OTgwOCkiLz4NCjwvc3ZnPg%3D%3D)](https://gitter.im/ant-design/ant-design-pro?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) - -![](https://user-images.githubusercontent.com/8186664/44953195-581e3d80-aec4-11e8-8dcb-54b9db38ec11.png) - -
- -- Preview: http://preview.pro.ant.design -- Home Page: http://pro.ant.design -- Documentation: http://pro.ant.design/docs/getting-started -- ChangeLog: http://pro.ant.design/docs/changelog -- FAQ: http://pro.ant.design/docs/faq -- Mirror Site in China: http://ant-design-pro.gitee.io - -## 2.0 Released Now! 🎉🎉🎉 -[Announcing Ant Design Pro 2.0.0](https://medium.com/ant-design/beautiful-and-powerful-ant-design-pro-2-0-release-51358da5af95) - -## Translation Recruitment :loudspeaker: - -We need your help: https://github.com/ant-design/ant-design-pro/issues/120 - -## Features - -- :gem: **Neat Design**: Follow [Ant Design specification](http://ant.design/) -- :triangular_ruler: **Common Templates**: Typical templates for enterprise applications -- :rocket: **State of The Art Development**: Newest development stack of React/umi/dva/antd -- :iphone: **Responsive**: Designed for variable screen sizes -- :art: **Theming**: Customizable theme with simple config -- :globe_with_meridians: **International**: Built-in i18n solution -- :gear: **Best Practices**: Solid workflow to make your code healthy -- :1234: **Mock development**: Easy to use mock development solution -- :white_check_mark: **UI Test**: Fly safely with unit and e2e tests - -## Templates - -``` -- Dashboard - - Analytic - - Monitor - - Workspace -- Form - - Basic Form - - Step Form - - Advanced From -- List - - Standard Table - - Standard List - - Card List - - Search List (Project/Applications/Article) -- Profile - - Simple Profile - - Advanced Profile -- Account - - Account Center - - Account Settings -- Result - - Success - - Failed -- Exception - - 403 - - 404 - - 500 -- User - - Login - - Register - - Register Result -``` - -## Usage - -### Use bash - -```bash -$ git clone https://github.com/ant-design/ant-design-pro.git --depth=1 -$ cd ant-design-pro -$ npm install -$ npm start # visit http://localhost:8000 -``` - -### Use by docker - -```bash -# preview -$ docker pull antdesign/ant-design-pro -$ docker run -p 80:80 antdesign/ant-design-pro -# open http://localhost - -# dev -$ npm run docker:dev - -# build -$ npm run docker:build - - -# production dev -$ npm run docker-prod:dev - -# production build -$ npm run docker-prod:build -``` - -### Use Gitpod - -Open the project in Gitpod (free online dev environment for GitHub) and start coding immediately. - -[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/ant-design/ant-design-pro) - -More instructions at [documentation](http://pro.ant.design/docs/getting-started). - -## Browsers support - -Modern browsers and IE11. - -| [IE / Edge](http://godban.github.io/browsers-support-badges/)
IE / Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | [Opera](http://godban.github.io/browsers-support-badges/)
Opera | -| --------- | --------- | --------- | --------- | --------- | -| IE11, Edge| last 2 versions| last 2 versions| last 2 versions| last 2 versions - -## Contributing - -Any type of contribution is welcome, here are some examples of how you may contribute to this project: - -- Use Ant Design Pro in your daily work. -- Submit [issues](http://github.com/ant-design/ant-design-pro/issues) to report bugs or ask questions. -- Propose [pull requests](http://github.com/ant-design/ant-design-pro/pulls) to improve our code. diff --git a/admin-web/README.md b/admin-web/README.md index 43e8a3284..b40101c02 100644 --- a/admin-web/README.md +++ b/admin-web/README.md @@ -3,3 +3,32 @@ > 采用 antd pro 快速开发 > TODO + +## 命名规范 + +#### 1.文件夹命名 + +文件夹命名全部小写,单词之间以中划线隔离 例如: node-modules + + +#### 2.文件命名 + +文件以小写开头,以驼峰格式连接单词 例如: dashBoard.js +component目录下的文件 以大写开头 +route目录下的文件以大写开头 +model目录下的文件大写开头 + + +#### 3.标点符号 + +对于字符串统一用单引号 例如: 'hello world' + +#### 4.语法规范 + +JS语法规范遵守ES6规范 +http://www.tuicool.com/articles/YrQ7j2a + +#### 5.注释 + +1.route目录下的文件需要加上文件头部注释(写清楚文件是什么功能) +2.component文件需要加上头部注释 (写清楚改控件的用处) \ No newline at end of file diff --git a/admin-web/README.ru-RU.md b/admin-web/README.ru-RU.md deleted file mode 100644 index 22d16c9ac..000000000 --- a/admin-web/README.ru-RU.md +++ /dev/null @@ -1,103 +0,0 @@ -[English](./README.md) | [简体中文](./README.zh-CN.md) | Русский - -

Ant Design Pro

- -
- -UI-решение "из коробки" для корпоративных приложений как React boilerplate - -[![Build With Umi](https://img.shields.io/badge/build%20with-umi-028fe4.svg?style=flat-square)](http://umijs.org/) -[![Build Status](https://dev.azure.com/ant-design/ant-design-pro/_apis/build/status/ant-design.ant-design-pro?branchName=master)](https://dev.azure.com/ant-design/ant-design-pro/_build/latest?definitionId=1?branchName=master) -[![Dependencies](https://img.shields.io/david/ant-design/ant-design-pro.svg)](https://david-dm.org/ant-design/ant-design-pro) -[![DevDependencies](https://img.shields.io/david/dev/ant-design/ant-design-pro.svg)](https://david-dm.org/ant-design/ant-design-pro?type=dev) -[![Gitter](https://img.shields.io/gitter/room/ant-design/pro-english.svg?style=flat-square&logoWidth=20&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgd2lkdGg9IjEyMzUiIGhlaWdodD0iNjUwIiB2aWV3Qm94PSIwIDAgNzQxMCAzOTAwIj4NCjxyZWN0IHdpZHRoPSI3NDEwIiBoZWlnaHQ9IjM5MDAiIGZpbGw9IiNiMjIyMzQiLz4NCjxwYXRoIGQ9Ik0wLDQ1MEg3NDEwbTAsNjAwSDBtMCw2MDBINzQxMG0wLDYwMEgwbTAsNjAwSDc0MTBtMCw2MDBIMCIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjMwMCIvPg0KPHJlY3Qgd2lkdGg9IjI5NjQiIGhlaWdodD0iMjEwMCIgZmlsbD0iIzNjM2I2ZSIvPg0KPGcgZmlsbD0iI2ZmZiI%2BDQo8ZyBpZD0iczE4Ij4NCjxnIGlkPSJzOSI%2BDQo8ZyBpZD0iczUiPg0KPGcgaWQ9InM0Ij4NCjxwYXRoIGlkPSJzIiBkPSJNMjQ3LDkwIDMxNy41MzQyMzAsMzA3LjA4MjAzOSAxMzIuODczMjE4LDE3Mi45MTc5NjFIMzYxLjEyNjc4MkwxNzYuNDY1NzcwLDMwNy4wODIwMzl6Ii8%2BDQo8dXNlIHhsaW5rOmhyZWY9IiNzIiB5PSI0MjAiLz4NCjx1c2UgeGxpbms6aHJlZj0iI3MiIHk9Ijg0MCIvPg0KPHVzZSB4bGluazpocmVmPSIjcyIgeT0iMTI2MCIvPg0KPC9nPg0KPHVzZSB4bGluazpocmVmPSIjcyIgeT0iMTY4MCIvPg0KPC9nPg0KPHVzZSB4bGluazpocmVmPSIjczQiIHg9IjI0NyIgeT0iMjEwIi8%2BDQo8L2c%2BDQo8dXNlIHhsaW5rOmhyZWY9IiNzOSIgeD0iNDk0Ii8%2BDQo8L2c%2BDQo8dXNlIHhsaW5rOmhyZWY9IiNzMTgiIHg9Ijk4OCIvPg0KPHVzZSB4bGluazpocmVmPSIjczkiIHg9IjE5NzYiLz4NCjx1c2UgeGxpbms6aHJlZj0iI3M1IiB4PSIyNDcwIi8%2BDQo8L2c%2BDQo8L3N2Zz4%3D)](https://gitter.im/ant-design/pro-english?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) -[![Join the chat at https://gitter.im/ant-design/ant-design-pro](https://img.shields.io/gitter/room/ant-design/ant-design-pro.svg?style=flat-square&logoWidth=20&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgd2lkdGg9IjkwMCIgaGVpZ2h0PSI2MDAiIHZpZXdCb3g9IjAgMCAzMCAyMCI%2BDQo8ZGVmcz4NCjxwYXRoIGlkPSJzIiBkPSJNMCwtMSAwLjU4Nzc4NSwwLjgwOTAxNyAtMC45NTEwNTcsLTAuMzA5MDE3SDAuOTUxMDU3TC0wLjU4Nzc4NSwwLjgwOTAxN3oiIGZpbGw9IiNmZmRlMDAiLz4NCjwvZGVmcz4NCjxyZWN0IHdpZHRoPSIzMCIgaGVpZ2h0PSIyMCIgZmlsbD0iI2RlMjkxMCIvPg0KPHVzZSB4bGluazpocmVmPSIjcyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNSw1KSBzY2FsZSgzKSIvPg0KPHVzZSB4bGluazpocmVmPSIjcyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTAsMikgcm90YXRlKDIzLjAzNjI0MykiLz4NCjx1c2UgeGxpbms6aHJlZj0iI3MiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEyLDQpIHJvdGF0ZSg0NS44Njk4OTgpIi8%2BDQo8dXNlIHhsaW5rOmhyZWY9IiNzIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxMiw3KSByb3RhdGUoNjkuOTQ1Mzk2KSIvPg0KPHVzZSB4bGluazpocmVmPSIjcyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTAsOSkgcm90YXRlKDIwLjY1OTgwOCkiLz4NCjwvc3ZnPg%3D%3D)](https://gitter.im/ant-design/ant-design-pro?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) - -![](https://user-images.githubusercontent.com/8186664/44953195-581e3d80-aec4-11e8-8dcb-54b9db38ec11.png) - -
- -- Демо: http://preview.pro.ant.design -- Домашняя страница: http://pro.ant.design -- Документация: http://pro.ant.design/docs/getting-started -- История изменений: http://pro.ant.design/docs/changelog -- FAQ: http://pro.ant.design/docs/faq -- Китайское зеркало сайта: http://ant-design-pro.gitee.io - -## Поиск переводчиков :loudspeaker: - -Нам нужна ваша помощь: https://github.com/ant-design/ant-design-pro/issues/120 - -## Возможности - -- :gem: **Аккуратный дизайн**: Посмотрите [спецификацию Ant Design](http://ant.design/) -- :triangular_ruler: **Общие шаблоны**: Стандартные шаблоны для корпоративных приложений -- :rocket: **Разработка, как искусство**: Новейший стек технологий React/umi/dva/antd -- :iphone: **Отзывчивая верстка**: Создан для экранов разных размеров -- :art: **Темизация**: Возможность изменения темы с помощью конфигурации -- :globe_with_meridians: **Мультиязычность**: Встроенное i18n решение -- :gear: **Лучшие практики**: Надежные процессы для хорошего кода -- :1234: **Разработка по шаблону**: Простое в использовании решение для разработки -- :white_check_mark: **UI тесты**: Разрабатывайте безопасно с юнит и e2e тестами - -## Шаблоны - -``` -- Dashboard - - Analytic - - Monitor - - Workspace -- Form - - Basic Form - - Step Form - - Advanced From -- List - - Standard Table - - Standard List - - Card List - - Search List (Project/Applications/Article) -- Profile - - Simple Profile - - Advanced Profile -- Account - - Account Center - - Account Settings -- Result - - Success - - Failed -- Exception - - 403 - - 404 - - 500 -- User - - Login - - Register - - Register Result -``` - -## Использование - -```bash -$ git clone https://github.com/ant-design/ant-design-pro.git --depth=1 -$ cd ant-design-pro -$ npm install -$ npm start # visit http://localhost:8000 -``` - -Больше информации в [документации](http://pro.ant.design/docs/getting-started). - -## Совместимость - -Современные браузеры и IE11. - -| [IE / Edge](http://godban.github.io/browsers-support-badges/)
IE / Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | [Opera](http://godban.github.io/browsers-support-badges/)
Opera | -| --------- | --------- | --------- | --------- | --------- | -| IE11, Edge| last 2 versions| last 2 versions| last 2 versions| last 2 versions - -## Распространение - -Любые варианты распространения приветствуются! Вот несколько примеров того, как вы можете помочь распространению проекта: - -- Использовать Ant Design Pro в ежедневной работе. -- Создавать [задачи](http://github.com/ant-design/ant-design-pro/issues) заводить баги или отвечать на вопросы. -- Делать [pull-реквесты](http://github.com/ant-design/ant-design-pro/pulls) для совершенствования нашего кода. diff --git a/admin-web/README.zh-CN.md b/admin-web/README.zh-CN.md deleted file mode 100644 index 6b1505061..000000000 --- a/admin-web/README.zh-CN.md +++ /dev/null @@ -1,121 +0,0 @@ -[English](./README.md) | 简体中文 | [Русский](./README.ru-RU.md) - -

Ant Design Pro

- -
- -开箱即用的中台前端/设计解决方案。 - -[![Build With Umi](https://img.shields.io/badge/build%20with-umi-028fe4.svg?style=flat-square)](http://umijs.org/) -[![Build Status](https://dev.azure.com/ant-design/ant-design-pro/_apis/build/status/ant-design.ant-design-pro?branchName=master)](https://dev.azure.com/ant-design/ant-design-pro/_build/latest?definitionId=1?branchName=master) -[![Dependencies](https://img.shields.io/david/ant-design/ant-design-pro.svg)](https://david-dm.org/ant-design/ant-design-pro) -[![DevDependencies](https://img.shields.io/david/dev/ant-design/ant-design-pro.svg)](https://david-dm.org/ant-design/ant-design-pro?type=dev) -[![Join the chat at https://gitter.im/ant-design/ant-design-pro](https://img.shields.io/gitter/room/ant-design/ant-design-pro.svg?style=flat-square&logoWidth=20&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgd2lkdGg9IjkwMCIgaGVpZ2h0PSI2MDAiIHZpZXdCb3g9IjAgMCAzMCAyMCI%2BDQo8ZGVmcz4NCjxwYXRoIGlkPSJzIiBkPSJNMCwtMSAwLjU4Nzc4NSwwLjgwOTAxNyAtMC45NTEwNTcsLTAuMzA5MDE3SDAuOTUxMDU3TC0wLjU4Nzc4NSwwLjgwOTAxN3oiIGZpbGw9IiNmZmRlMDAiLz4NCjwvZGVmcz4NCjxyZWN0IHdpZHRoPSIzMCIgaGVpZ2h0PSIyMCIgZmlsbD0iI2RlMjkxMCIvPg0KPHVzZSB4bGluazpocmVmPSIjcyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNSw1KSBzY2FsZSgzKSIvPg0KPHVzZSB4bGluazpocmVmPSIjcyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTAsMikgcm90YXRlKDIzLjAzNjI0MykiLz4NCjx1c2UgeGxpbms6aHJlZj0iI3MiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEyLDQpIHJvdGF0ZSg0NS44Njk4OTgpIi8%2BDQo8dXNlIHhsaW5rOmhyZWY9IiNzIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxMiw3KSByb3RhdGUoNjkuOTQ1Mzk2KSIvPg0KPHVzZSB4bGluazpocmVmPSIjcyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTAsOSkgcm90YXRlKDIwLjY1OTgwOCkiLz4NCjwvc3ZnPg%3D%3D)](https://gitter.im/ant-design/ant-design-pro?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) - -![](https://user-images.githubusercontent.com/8186664/44953195-581e3d80-aec4-11e8-8dcb-54b9db38ec11.png) - -
- -- 预览:http://preview.pro.ant.design -- 首页:http://pro.ant.design/index-cn -- 使用文档:http://pro.ant.design/docs/getting-started-cn -- 更新日志: http://pro.ant.design/docs/changelog-cn -- 常见问题:http://pro.ant.design/docs/faq-cn -- 国内镜像:http://ant-design-pro.gitee.io - -## 特性 - -- :gem: **优雅美观**:基于 Ant Design 体系精心设计 -- :triangular_ruler: **常见设计模式**:提炼自中后台应用的典型页面和场景 -- :rocket: **最新技术栈**:使用 React/umi/dva/antd 等前端前沿技术开发 -- :iphone: **响应式**:针对不同屏幕大小设计 -- :art: **主题**:可配置的主题满足多样化的品牌诉求 -- :globe_with_meridians: **国际化**:内建业界通用的国际化方案 -- :gear: **最佳实践**:良好的工程实践助您持续产出高质量代码 -- :1234: **Mock 数据**:实用的本地数据调试方案 -- :white_check_mark: **UI 测试**:自动化测试保障前端产品质量 - -## 模板 - -``` -- Dashboard - - 分析页 - - 监控页 - - 工作台 -- 表单页 - - 基础表单页 - - 分步表单页 - - 高级表单页 -- 列表页 - - 查询表格 - - 标准列表 - - 卡片列表 - - 搜索列表(项目/应用/文章) -- 详情页 - - 基础详情页 - - 高级详情页 -- 用户 - - 用户中心页 - - 用户设置页 -- 结果 - - 成功页 - - 失败页 -- 异常 - - 403 无权限 - - 404 找不到 - - 500 服务器出错 -- 帐户 - - 登录 - - 注册 - - 注册成功 -``` - -## 使用 - -### 使用命令行 -```bash -$ git clone https://github.com/ant-design/ant-design-pro.git --depth=1 -$ cd ant-design-pro -$ npm install -$ npm start # 访问 http://localhost:8000 -``` - -### 使用 docker - -```bash -# preview -$ docker pull antdesign/ant-design-pro -$ docker run -p 80:80 antdesign/ant-design-pro -# open http://localhost - -# dev -$ npm run docker:dev - -# build -$ npm run docker:build - - -# production dev -$ npm run docker-prod:dev - -// production build -$ npm run docker-prod:build -``` - -更多信息请参考 [使用文档](http://pro.ant.design/docs/getting-started)。 - -## 支持环境 - -现代浏览器及 IE11。 - -| [IE / Edge](http://godban.github.io/browsers-support-badges/)
IE / Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | [Opera](http://godban.github.io/browsers-support-badges/)
Opera | -| --------- | --------- | --------- | --------- | --------- | -| IE11, Edge| last 2 versions| last 2 versions| last 2 versions| last 2 versions - -## 参与贡献 - -我们非常欢迎你的贡献,你可以通过以下方式和我们一起共建 :smiley:: - -- 在你的公司或个人项目中使用 Ant Design Pro。 -- 通过 [Issue](http://github.com/ant-design/ant-design-pro/issues) 报告 bug 或进行咨询。 -- 提交 [Pull Request](http://github.com/ant-design/ant-design-pro/pulls) 改进 Pro 的代码。 diff --git a/admin-web/helpers/validator.js b/admin-web/helpers/validator.js new file mode 100644 index 000000000..02bc4b81c --- /dev/null +++ b/admin-web/helpers/validator.js @@ -0,0 +1,10 @@ + +// 校验必须是英文或者数字 +export function checkTypeWithEnglishAndNumbers (rule, value, callback, text) { + let char = /^[a-zA-Z0-9]+$/ + if (char.test(value)) { + callback() + } else { + callback(text) + } +} \ No newline at end of file diff --git a/admin-web/mock/admin.js b/admin-web/mock/admin.js index aa30f8d3d..be9c114b2 100644 --- a/admin-web/mock/admin.js +++ b/admin-web/mock/admin.js @@ -49,10 +49,10 @@ function getDictionaryTree(req, res) { } export default { - 'GET /admin-api/admins/admin/menu_resource_tree': getAdminMenu, + 'GET /admin-api/admins/admin/menu_resource_tree': getAdminMenuAll, 'GET /admin-api/admins/admin/url_resource_list': getAdminUrls, 'GET /admin-api/admins/resource/tree': getResourceTree, 'GET /admin-api/admins/role/page': getQueryRole, - 'GET /admin-api/admins/admin/page': getQueryRole, + // 'GET /admin-api/admins/admin/page': getQueryRole, 'GET /admin-api/admins/data_dict/tree': getDictionaryTree, }; diff --git a/admin-web/src/components/Dictionary/DictionaryText.js b/admin-web/src/components/Dictionary/DictionaryText.js index 9ee99f2df..7faf7e329 100644 --- a/admin-web/src/components/Dictionary/DictionaryText.js +++ b/admin-web/src/components/Dictionary/DictionaryText.js @@ -5,6 +5,7 @@ export default class DictionaryText extends PureComponent { componentDidMount() {} render() { + debugger; const { dicKey, dicValue } = this.props; return ( diff --git a/admin-web/src/components/Product/ProductAttrSelectFormItem.js b/admin-web/src/components/Product/ProductAttrSelectFormItem.js new file mode 100644 index 000000000..3c019d940 --- /dev/null +++ b/admin-web/src/components/Product/ProductAttrSelectFormItem.js @@ -0,0 +1,101 @@ +import React, {PureComponent} from "react"; +import {Select} from "antd"; + +const Option = Select.Option; + +export default class ProductAttrSelectFormItem extends PureComponent { + + handleSelectAttr = (value, option) => { + // console.log(value); + // console.log(option); + // debugger; + const { dispatch, index } = this.props; + // let attrIndex = option.key.substring(option.key.indexOf('option-attr-') + 'option-attr-'.length, option.key.lastIndexOf('-')); + // console.log('attrIndex: ' + attrIndex); + // debugger; + dispatch({ + type: 'productSpuAddOrUpdate/selectAttr', + payload: { + attrIndex: index, + attr: { + id: option.props.value, + name: option.props.children, + values: [] + } + }, + }); + } + + handleSelectAttrValue = (values, options) => { + let attrValues = []; + const { dispatch, index } = this.props; + // debugger; + // console.log('x' + this.children[0]); + // let firstOption = this.children[0]; + // let attrIndex = firstOption.key.substring(firstOption.key.indexOf('option-attr-value-') + 'option-attr-value-'.length, firstOption.key.lastIndexOf('-')); + for (let i in options) { + let option = options[i]; + attrValues.push({ + id: parseInt(option.props.value), + name: option.props.children, + }); + } + dispatch({ + type: 'productSpuAddOrUpdate/selectAttrValues', + payload: { + attrIndex: index, + attrValues: attrValues, + }, + }); + // debugger; + + // console.log(value); + } + + render() { + const {attr, allAttrTree, selectedAttrIds, index} = this.props; + // console.log('i: ' + i); + // 1. 规格 + let attrOptions = []; + // allAttrTree.unshift(attr); + // debugger; + for (let j in allAttrTree) { + let allAttr = allAttrTree[j]; + if (selectedAttrIds.has(allAttr.id) && allAttr.id !== attr.id) { + continue; + } + attrOptions.push(); + } + // 2. 规格值 + let attrValueOptions = []; + // debugger; + if (attr.id) { + // 2.1 先找到规格值的数组 + let attrValues = []; + for (let j in allAttrTree) { + let allAttr = allAttrTree[j]; + if (attr.id === allAttr.id) { + attrValues = allAttr.values; + break; + } + } + // 2.2 生成规格值的 HTML + for (let j in attrValues) { + let attrValue = attrValues[j]; + attrValueOptions.push(); // + '' 的原因是,多选必须是字符串 + } + } + // 3. 拼装最终,添加到 attrTreeHTML 中 + return
+ + +
; + } + +} \ No newline at end of file diff --git a/admin-web/src/components/Product/ProductSkuAddOrUpdateTable.js b/admin-web/src/components/Product/ProductSkuAddOrUpdateTable.js new file mode 100644 index 000000000..8e9452b92 --- /dev/null +++ b/admin-web/src/components/Product/ProductSkuAddOrUpdateTable.js @@ -0,0 +1,85 @@ +import React, {PureComponent} from "react"; +import {InputNumber, Select, Table} from "antd"; +import Input from "antd/es/input"; + +const Option = Select.Option; + +class SkuInputNumber extends PureComponent { + + handleChange = value => { + // debugger; + const { dispatch, index, dataIndex } = this.props; + if (dataIndex === 'price') { + dispatch({ + type: 'productSpuAddOrUpdate/inputSkuPrice', + payload: { + index: index, + price: value + }, + }); + } else if (dataIndex === 'quantity') { + dispatch({ + type: 'productSpuAddOrUpdate/inputSkuQuantity', + payload: { + index: index, + quantity: value + }, + }); + } + } + + render() { + return + } + +} + +export default class ProductSkuAddOrUpdateTable extends PureComponent { + + render() { + let that = this; + // debugger; + // console.log('ProductSkuAddOrUpdateTable'); + const {attrTree, skus, dispatch} = this.props; + let columns = []; + for (let i in attrTree) { + let attr = attrTree[i]; + columns.push({ + title: attr.name, + dataIndex: 'attrs[i]', + render(value, record) { + return record.attrs[i].name; + } + }) + } + columns.push({ + title: '价格', + dataIndex: 'price', + render(value, record, index) { + let props = { + record: record, + index: index, + dispatch: dispatch, + dataIndex: 'price' + }; + return ; + } + }); + columns.push({ + title: '库存', + dataIndex: 'quantity', + render(value, record, index) { + let props = { + record: record, + index: index, + dispatch: dispatch, + dataIndex: 'quantity' + }; + return ; + } + }); + return ; + // return
; + } + +} \ No newline at end of file diff --git a/admin-web/src/locales/zh-CN/menu.js b/admin-web/src/locales/zh-CN/menu.js index be480f4df..44bdf9a3f 100644 --- a/admin-web/src/locales/zh-CN/menu.js +++ b/admin-web/src/locales/zh-CN/menu.js @@ -44,4 +44,9 @@ export default { 'menu.account.settings': '个人设置', 'menu.account.trigger': '触发报错', 'menu.account.logout': '退出登录', + // 商品相关 + 'menu.product': '商品管理', + 'menu.product.product-spu-list': '商品管理', + 'menu.product.product-spu-add': '商品添加', + 'menu.product.product-category-list': '商品分类', }; diff --git a/admin-web/src/models/admin/adminList.js b/admin-web/src/models/admin/adminList.js index 6adf934fe..bd1455f36 100644 --- a/admin-web/src/models/admin/adminList.js +++ b/admin-web/src/models/admin/adminList.js @@ -76,13 +76,13 @@ export default { }, *query({ payload }, { call, put }) { const response = yield call(queryAdmin, payload); - message.info('查询成功!'); const { count, admins } = response.data; yield put({ type: 'querySuccess', payload: { list: admins, count, + pageNo: payload.pageNo + 1 }, }); }, diff --git a/admin-web/src/models/product/productSpuAddOrUpdate.js b/admin-web/src/models/product/productSpuAddOrUpdate.js index af7409534..6ceaa56e7 100644 --- a/admin-web/src/models/product/productSpuAddOrUpdate.js +++ b/admin-web/src/models/product/productSpuAddOrUpdate.js @@ -1,14 +1,12 @@ import { message } from 'antd'; -import { productCategoryTree, productCategoryAdd, productCategoryUpdate, productCategoryUpdateStatus, productCategoryDelete } from '../../services/product'; +import { productCategoryTree, productSpuAdd, productCategoryUpdate, productCategoryUpdateStatus, productCategoryDelete } from '../../services/product'; export default { namespace: 'productSpuAddOrUpdate', state: { list: [], - attrTree: [{ - - } + attrTree: [ // { // id: // // name: // @@ -17,6 +15,16 @@ export default { // name: // // }] // } + ], + skus: [ + // { + // attrs: [{ + // id: // 规格值编号 + // name: // 规格值名 + // }], + // price: // 价格 + // quantity: // 数量 + // } ] }, @@ -65,7 +73,7 @@ export default { *addAttr({ payload }, { call, put }) { // const { queryParams } = payload; // const response = yield call(productCategoryTree, queryParams); - message.info('调试:添加规格成功!'); + // message.info('调试:添加规格成功!'); yield put({ type: 'addAttrSuccess', payload: { @@ -73,17 +81,137 @@ export default { }, }); }, + *selectAttr({ payload }, { call, put }) { + // const { queryParams } = payload; + // const response = yield call(productCategoryTree, queryParams); + // message.info('调试:选择规格成功!'); + yield put({ + type: 'selectAttrSuccess', + payload: payload, + }); + }, + *selectAttrValues({ payload }, { call, put }) { + // const { queryParams } = payload; + // const response = yield call(productCategoryTree, queryParams); + // message.info('调试:选择规格值成功!'); + yield put({ + type: 'selectAttrValueSuccess', + payload: payload, + }); + }, + *inputSkuPrice({ payload }, { call, put }) { + // debugger; + yield put({ + type: 'inputSkuPriceSuccess', + payload: payload, + }); + }, + *inputSkuQuantity({ payload }, { call, put }) { + // debugger; + yield put({ + type: 'inputSkuQuantitySuccess', + payload: payload, + }); + }, + *add({ payload }, { call, put }) { + const { callback, body } = payload; + const response = yield call(productSpuAdd, body); + if (callback) { + callback(response); + } + // yield put({ + // type: 'tree', + // payload: {}, + // }); + alert('添加成功!后续改成跳转到手机站的详情'); + }, + *update({ payload }, { call, put }) { + const { callback, body } = payload; + const response = yield call(productSpuAdd, body); + if (callback) { + callback(response); + } + // yield put({ + // type: 'tree', + // payload: {}, + // }); + alert('修改成功!后续改成跳转到手机站的详情'); + }, }, reducers: { addAttrSuccess(state, {payload}) { // debugger; - console.log(state.attrTree); + // console.log(state.attrTree); state.attrTree.push(payload.attrAdd); return { ...state } }, + selectAttrSuccess(state, {payload}) { + // debugger; + // console.log(state.attrTree); + state.attrTree[payload.attrIndex] = payload.attr; + return { + ...state + } + }, + selectAttrValueSuccess(state, {payload}) { + // debugger; + // console.log(state); + state.attrTree[payload.attrIndex].values = payload.attrValues; + // 生成 skus 值 + let skus = []; + let skuSize = 1; + for (let i in state.attrTree) { // 先计算 sku 数量 + let attr = state.attrTree[i]; + skuSize = skuSize * attr.values.length; + } + // console.log('skuSize: ' + skuSize); + for (let i = 0; i < skuSize; i++) { // 初始化 sku 格子 + skus.push({ + attrs: [], + price: undefined, + quantity: undefined, + }); + } + for (let i = 0; i < state.attrTree.length; i++) { // 初始化 sku 格子里的 attrs + for (let j = 0; j < skuSize; j++) { + // let values = state.attrTree[i].values; + // let attr = values[j % values.length]; + // skus[i].attrs.push({ + // id: attr.id, + // name: attr.name, + // }); + let values = state.attrTree[i].values; + let attr = values[j % values.length]; + skus[j].attrs.push({ + id: attr.id, + name: attr.name, + }); + } + } + state.skus = skus; + // debugger; + // console.l og('skus: ' + skus); + return { + ...state + } + }, + inputSkuPriceSuccess(state, {payload}) { + // debugger; + state.skus[payload.index].price = payload.price; + return { + ...state + } + }, + inputSkuQuantitySuccess(state, {payload}) { + // debugger; + state.skus[payload.index].quantity = payload.quantity; + return { + ...state + } + }, treeSuccess(state, { payload }) { return { ...state, diff --git a/admin-web/src/models/product/productSpuList.js b/admin-web/src/models/product/productSpuList.js index d4492f3cb..cc9ad9607 100644 --- a/admin-web/src/models/product/productSpuList.js +++ b/admin-web/src/models/product/productSpuList.js @@ -1,5 +1,6 @@ import { message } from 'antd'; import { productSpuPage, productCategoryAdd, productCategoryUpdate, productCategoryUpdateStatus, productCategoryDelete } from '../../services/product'; +import {routerRedux} from "dva/router"; export default { namespace: 'productSpuList', @@ -9,46 +10,50 @@ export default { }, effects: { - *add({ payload }, { call, put }) { - const { callback, body } = payload; - const response = yield call(productCategoryAdd, body); - if (callback) { - callback(response); - } - yield put({ - type: 'tree', - payload: {}, - }); - }, - *update({ payload }, { call, put }) { - const { callback, body } = payload; - const response = yield call(productCategoryUpdate, body); - if (callback) { - callback(response); - } - yield put({ - type: 'tree', - payload: {}, - }); - }, - *updateStatus({ payload }, { call, put }) { - const { callback, body } = payload; - const response = yield call(productCategoryUpdateStatus, body); - if (callback) { - callback(response); - } - yield put({ - type: 'tree', - payload: {}, - }); - }, - *delete({ payload }, { call, put }) { - const response = yield call(productCategoryDelete, payload); - message.info('删除成功!'); - yield put({ - type: 'tree', - payload: {}, - }); + // *add({ payload }, { call, put }) { + // const { callback, body } = payload; + // const response = yield call(productCategoryAdd, body); + // if (callback) { + // callback(response); + // } + // yield put({ + // type: 'tree', + // payload: {}, + // }); + // }, + // *update({ payload }, { call, put }) { + // const { callback, body } = payload; + // const response = yield call(productCategoryUpdate, body); + // if (callback) { + // callback(response); + // } + // yield put({ + // type: 'tree', + // payload: {}, + // }); + // }, + // *updateStatus({ payload }, { call, put }) { + // const { callback, body } = payload; + // const response = yield call(productCategoryUpdateStatus, body); + // if (callback) { + // callback(response); + // } + // yield put({ + // type: 'tree', + // payload: {}, + // }); + // }, + // *delete({ payload }, { call, put }) { + // const response = yield call(productCategoryDelete, payload); + // message.info('删除成功!'); + // yield put({ + // type: 'tree', + // payload: {}, + // }); + // }, + *redirectToAdd({ payload }, { call, put }) { + // const { callback, body } = payload; + yield put(routerRedux.replace('/product/product-spu-add')); }, *page({ payload }, { call, put }) { const { queryParams } = payload; diff --git a/admin-web/src/pages/Admin/AdminList.js b/admin-web/src/pages/Admin/AdminList.js index d2da4e2ab..26eb689c6 100644 --- a/admin-web/src/pages/Admin/AdminList.js +++ b/admin-web/src/pages/Admin/AdminList.js @@ -2,10 +2,13 @@ import React, { PureComponent, Fragment } from 'react'; import { connect } from 'dva'; -import { Card, Form, Input, Button, Modal, message, Table, Divider, Tree, Spin } from 'antd'; +import {Card, Form, Input, Button, Modal, message, Table, Divider, Tree, Spin, Row, Col, Select, Icon} from 'antd'; +import { checkTypeWithEnglishAndNumbers } from '../../../helpers/validator' import PageHeaderWrapper from '@/components/PageHeaderWrapper'; import styles from './AdminList.less'; +import moment from "moment"; +import Pagination from "antd/es/pagination"; const FormItem = Form.Item; const { TreeNode } = Tree; @@ -31,31 +34,36 @@ const CreateForm = Form.create()(props => { width: 200, }; - const title = modalType === 'add' ? '添加一个 Resource' : '更新一个 Resource'; - const okText = modalType === 'add' ? '添加' : '更新'; + const title = modalType === 'add' ? '新建管理员' : '更新管理员'; return ( handleModalVisible()} > - + {form.getFieldDecorator('username', { - rules: [{ required: true, message: '请输入名称!', min: 2 }], + rules: [{ required: true, message: '请输入用户名!'}, + {max: 16, min:6, message: '长度为6-16位'}, + { validator: (rule, value, callback) => checkTypeWithEnglishAndNumbers(rule, value, callback, '数字以及字母')} + ], initialValue: initValues.username, })()} {form.getFieldDecorator('nickname', { - rules: [{ required: true, message: '请输入昵称!', min: 2 }], + rules: [{ required: true, message: '请输入昵称!'}, + {max: 10, message: '姓名最大长度为10'}], initialValue: initValues.nickname, })()} {form.getFieldDecorator('password', { + rules: [{ required: modalType === 'add', message: '请填写密码'}, // 添加时,必须输入密码 + {max: 16, min: 6, message: '长度为6-18位'}], initialValue: initValues.password, })()} @@ -304,9 +312,49 @@ class ResourceList extends PureComponent { }); }; + onPageChange = (page = {}) => { + const { dispatch } = this.props; + // debugger; + dispatch({ + type: 'adminList/query', + payload: { + pageNo: page - 1, + pageSize: 10, + } + }); + } + + renderSimpleForm() { // TODO 芋艿,搜索功能未完成 + const { + form: { getFieldDecorator }, + } = this.props; + return ( +
+ +
+ + {getFieldDecorator('name')()} + + + + + + + + + + + ); + } + render() { - const { list, data } = this.props; - const { roleList, roleCheckedKeys, roleAssignLoading } = data; + let that = this; + const { list, data } = this.props; + const { count, pageNo, pageSize, roleList, roleCheckedKeys, roleAssignLoading } = data; const { modalVisible, modalType, @@ -323,15 +371,9 @@ class ResourceList extends PureComponent { }; const columns = [ - { - title: 'id', - dataIndex: 'id', - render: text => {text}, - }, { title: '用户名', - dataIndex: 'username', - render: text => {text}, + dataIndex: 'username' }, { title: '昵称', @@ -341,17 +383,22 @@ class ResourceList extends PureComponent { title: '状态', dataIndex: 'status', render(val) { - return {status[val]}; + return {status[val]}; // TODO 芋艿,此处要改 }, }, + { + title: '创建时间', + dataIndex: 'createTime', + render: val => {moment(val).format('YYYY-MM-DD HH:mm')}, + }, { title: '操作', - width: 300, + width: 360, render: (text, record) => { - const statusText = record.status === 1 ? '确认禁用' : '取消禁用'; + const statusText = record.status === 1 ? '禁用' : '禁用'; return ( - this.handleModalVisible(true, 'update', record)}>更新 + this.handleModalVisible(true, 'update', record)}>编辑 this.handleRoleAssign(record)}>角色分配 @@ -369,16 +416,17 @@ class ResourceList extends PureComponent { ]; return ( - +
+
{that.renderSimpleForm()}
@@ -387,6 +435,12 @@ class ResourceList extends PureComponent { columns={columns} dataSource={list} rowKey="id" + pagination={{ + current: pageNo, + pageSize: pageSize, + total: count, + onChange: this.onPageChange + }} />
diff --git a/admin-web/src/pages/Admin/AdminList.less b/admin-web/src/pages/Admin/AdminList.less index ebb45c292..7ad3dac3f 100644 --- a/admin-web/src/pages/Admin/AdminList.less +++ b/admin-web/src/pages/Admin/AdminList.less @@ -13,3 +13,35 @@ .tableDelete { color: red; } + +.tableListForm { + :global { + .ant-form-item { + display: flex; + margin-right: 0; + margin-bottom: 24px; + > .ant-form-item-label { + width: auto; + padding-right: 8px; + line-height: 32px; + } + .ant-form-item-control { + line-height: 32px; + } + } + .ant-form-item-control-wrapper { + flex: 1; + } + } + .submitButtons { + display: block; + margin-bottom: 24px; + white-space: nowrap; + } +} + +@media screen and (max-width: @screen-lg) { + .tableListForm :global(.ant-form-item) { + margin-right: 24px; + } +} \ No newline at end of file diff --git a/admin-web/src/pages/Product/ProductSpuAddOrUpdate.js b/admin-web/src/pages/Product/ProductSpuAddOrUpdate.js index 77bf53911..2aa0d6660 100644 --- a/admin-web/src/pages/Product/ProductSpuAddOrUpdate.js +++ b/admin-web/src/pages/Product/ProductSpuAddOrUpdate.js @@ -1,12 +1,14 @@ /* eslint-disable */ -import React, { PureComponent, Fragment } from 'react'; +import React, {PureComponent, Fragment, Component} from 'react'; import { connect } from 'dva'; import moment from 'moment'; import {Card, Form, Input, Radio, Button, Table, Select} from 'antd'; import PageHeaderWrapper from '@/components/PageHeaderWrapper'; import styles from './ProductSpuAddOrUpdate.less'; +import ProductAttrSelectFormItem from "../../components/Product/ProductAttrSelectFormItem"; +import ProductSkuAddOrUpdateTable from "../../components/Product/ProductSkuAddOrUpdateTable"; const FormItem = Form.Item; const RadioGroup = Radio.Group; @@ -16,12 +18,15 @@ const Option = Select.Option; @connect(({ productSpuList, productAttrList, productSpuAddOrUpdate, loading }) => ({ // list: productSpuList.list.spus, // loading: loading.models.productSpuList, + productAttrList, + productSpuAddOrUpdate, allAttrTree: productAttrList.tree, - attrTree: productSpuAddOrUpdate.attrTree + attrTree: productSpuAddOrUpdate.attrTree, + skus: productSpuAddOrUpdate.skus, })) @Form.create() -class ProductSpuAddOrUpdate extends PureComponent { +class ProductSpuAddOrUpdate extends Component { state = { modalVisible: false, modalType: 'add', //add update @@ -42,18 +47,18 @@ class ProductSpuAddOrUpdate extends PureComponent { }); } - handleSubmit = e => { - const { dispatch, form } = this.props; - e.preventDefault(); - form.validateFieldsAndScroll((err, values) => { - if (!err) { - dispatch({ - type: 'form/submitRegularForm', - payload: values, - }); - } - }); - } + // handleSubmit = e => { + // const { dispatch, form } = this.props; + // e.preventDefault(); + // form.validateFieldsAndScroll((err, values) => { + // if (!err) { + // dispatch({ + // type: 'form/submitRegularForm', + // payload: values, + // }); + // } + // }); + // } handleAddAttr = e => { // alert('你猜'); @@ -65,50 +70,171 @@ class ProductSpuAddOrUpdate extends PureComponent { }); } + handleSubmit = e => { + debugger; + e.preventDefault(); + const { skus, dispatch } = this.props; + // 生成 skuStr 格式 + let skuStr = []; // 因为提交是字符串格式 + for (let i in skus) { + let sku = skus[i]; + if (!sku.price || !sku.quantity) { + continue; + } + let newAttr = { + attrs: [], + price: sku.price, + quantity: sku.quantity, + } + for (let j in sku.attrs) { + newAttr.attrs.push(sku.attrs[j].id); + } + skuStr.push(newAttr); + } + if (skuStr.length === 0) { + alert('请设置商品规格!'); + return; + } + this.props.form.validateFields((err, values) => { + if (!err) { + dispatch({ + type: 'productSpuAddOrUpdate/add', + payload: { + body: { + ...values, + skuStr: JSON.stringify(skuStr) + } + }, + }); + } + }); + // console.log(fields); + } + + // handleSelectAttr = (value, option) => { + // // console.log(value); + // // console.log(option); + // // debugger; + // const { dispatch } = this.props; + // let attrIndex = option.key.substring(option.key.indexOf('option-attr-') + 'option-attr-'.length, option.key.lastIndexOf('-')); + // // console.log('attrIndex: ' + attrIndex); + // // debugger; + // dispatch({ + // type: 'productSpuAddOrUpdate/selectAttr', + // payload: { + // attrIndex: attrIndex, + // attr: { + // id: option.props.value, + // name: option.props.children, + // values: [] + // } + // }, + // }); + // } + // + // handleSelectAttrValue = (values, options) => { + // let attrValues = []; + // const { dispatch } = this.props; + // debugger; + // // console.log('x' + this.children[0]); + // let firstOption = this.children[0]; + // // let attrIndex = firstOption.key.substring(firstOption.key.indexOf('option-attr-value-') + 'option-attr-value-'.length, firstOption.key.lastIndexOf('-')); + // let attrIndex = 0; + // for (let i in options) { + // let option = options[i]; + // attrValues.push({ + // id: parseInt(option.props.value), + // name: option.props.children, + // }); + // } + // dispatch({ + // type: 'productSpuAddOrUpdate/selectAttrValues', + // payload: { + // attrIndex: attrIndex, + // attrValues: attrValues, + // }, + // }); + // // debugger; + // + // // console.log(value); + // } + render() { // debugger; - const { form, data, attrTree } = this.props; - - // 规格明细 - const columns = [ - { - title: '颜色', - dataIndex: 'price' - }, - { - title: '价格', - dataIndex: 'price', - render(val) { - return {status[val]}; - }, - }, - { - title: '库存', - dataIndex: 'quantity', - } - ]; + const { form, skus, attrTree, allAttrTree, dispatch } = this.props; + // const that = this; // 添加规格 // debugger; let attrTreeHTML = []; if (attrTree && attrTree.length > 0) { + // 已选择的的规格集合 + let selectedAttrIds = new Set(); for (let i in attrTree) { let attr = attrTree[i]; - attr =
- -
; - attrTreeHTML.push(attr); - // debugger; + selectedAttrIds.add(attr.id); + } + // 创建每个规格下拉框的 HTML + for (let i in attrTree) { + let attr = attrTree[i]; + let itemProps = { + attr: attr, + allAttrTree: allAttrTree, + dispatch: dispatch, + selectedAttrIds: selectedAttrIds, + index: i // 位置。不然无法正确修改 Model 指定位置的数据 + }; + attrTreeHTML.push(); } } + // if (attrTree && attrTree.length > 0) { + // for (let i in attrTree) { + // let attr = attrTree[i]; + // // console.log('i: ' + i); + // // 1. 规格 + // let attrOptions = []; + // for (let j in allAttrTree) { + // let attr = allAttrTree[j]; + // attrOptions.push(); + // } + // // 2. 规格值 + // let attrValueOptions = []; + // // debugger; + // if (attr.id) { + // // 2.1 先招到规格值的数组 + // let attrValues = []; + // for (let j in allAttrTree) { + // let allAttr = allAttrTree[j]; + // if (attr.id === allAttr.id) { + // attrValues = allAttr.values; + // break; + // } + // } + // // 2.2 生成规格值的 HTML + // for (let j in attrValues) { + // let attrValue = attrValues[j]; + // attrValueOptions.push(); // + '' 的原因是,多选必须是字符串 + // } + // } + // // 3. 拼装最终,添加到 attrTreeHTML 中 + // attr =
+ // + // + //
; + // attrTreeHTML.push(attr); + // } + // } + // 规格明细 + let productSkuProps = { + attrTree: attrTree, + skus: skus, + dispatch: dispatch, + }; + // console.log(productSkuProps); return ( @@ -163,13 +289,12 @@ class ProductSpuAddOrUpdate extends PureComponent { )} - {/**/} - {/*{form.getFieldDecorator('visible', {*/} - {/*initialValue: 1, // TODO 修改*/} - {/*})(*/} - {/*
*/} - {/*)}*/} - {/**/} + + {/*
*/} + + + + diff --git a/admin-web/src/pages/Product/ProductSpuList.js b/admin-web/src/pages/Product/ProductSpuList.js index 6435529fc..6abb3de4a 100644 --- a/admin-web/src/pages/Product/ProductSpuList.js +++ b/admin-web/src/pages/Product/ProductSpuList.js @@ -77,52 +77,13 @@ class ProductSpuList extends PureComponent { }); } - handleModalVisible = (flag, modalType, initValues) => { - this.setState({ - modalVisible: !!flag, - initValues: initValues || {}, - modalType: modalType || 'add', + redirectToAdd = () => { + const { dispatch } = this.props; + dispatch({ + type: 'productSpuList/redirectToAdd', }); }; - handleAdd = ({ fields, modalType, initValues }) => { - const { dispatch, data } = this.props; - const queryParams = { - pageNo: data.pageNo, - pageSize: data.pageSize, - }; - if (modalType === 'add') { - dispatch({ - type: 'roleList/add', - payload: { - body: { - ...fields, - }, - queryParams, - callback: () => { - message.success('添加成功'); - this.handleModalVisible(); - }, - }, - }); - } else { - dispatch({ - type: 'roleList/update', - payload: { - body: { - ...initValues, - ...fields, - }, - queryParams, - callback: () => { - message.success('更新成功'); - this.handleModalVisible(); - }, - }, - }); - } - }; - render() { // debugger; const { list, data } = this.props; @@ -199,7 +160,7 @@ class ProductSpuList extends PureComponent { diff --git a/admin-web/src/services/product.js b/admin-web/src/services/product.js index c0a51274e..f8181bf02 100644 --- a/admin-web/src/services/product.js +++ b/admin-web/src/services/product.js @@ -44,6 +44,13 @@ export async function productSpuPage(params) { }); } +export async function productSpuAdd(params) { + return request(`/product-api/admins/spu/add?${stringify(params)}`, { + method: 'POST', + body: {}, + }); +} + // product attr + attr value export async function productAttrTree(params) { diff --git a/admin/admin-application/src/main/java/cn/iocoder/mall/admin/application/config/MVCConfiguration.java b/admin/admin-application/src/main/java/cn/iocoder/mall/admin/application/config/MVCConfiguration.java index 19806637b..802debaa8 100644 --- a/admin/admin-application/src/main/java/cn/iocoder/mall/admin/application/config/MVCConfiguration.java +++ b/admin/admin-application/src/main/java/cn/iocoder/mall/admin/application/config/MVCConfiguration.java @@ -1,6 +1,7 @@ package cn.iocoder.mall.admin.application.config; import cn.iocoder.common.framework.config.GlobalExceptionHandler; +import cn.iocoder.mall.admin.sdk.interceptor.AdminAccessLogInterceptor; import cn.iocoder.mall.admin.sdk.interceptor.AdminSecurityInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; @@ -18,16 +19,17 @@ public class MVCConfiguration implements WebMvcConfigurer { @Autowired private AdminSecurityInterceptor adminSecurityInterceptor; + @Autowired + private AdminAccessLogInterceptor adminAccessLogInterceptor; // @Override public void addInterceptors(InterceptorRegistry registry) { // registry.addInterceptor(securityInterceptor).addPathPatterns("/user/**", "/admin/**"); // 只拦截我们定义的接口 + registry.addInterceptor(adminAccessLogInterceptor).addPathPatterns("/admins/**"); registry.addInterceptor(adminSecurityInterceptor).addPathPatterns("/admins/**") .excludePathPatterns("/admins/passport/login"); // 排除登陆接口 } - - @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 解决 swagger-ui.html 的访问,参考自 https://stackoverflow.com/questions/43545540/swagger-ui-no-mapping-found-for-http-request 解决 diff --git a/admin/admin-application/src/main/java/cn/iocoder/mall/admin/application/controller/admins/AdminController.java b/admin/admin-application/src/main/java/cn/iocoder/mall/admin/application/controller/admins/AdminController.java index 041671d41..00e92bbfc 100644 --- a/admin/admin-application/src/main/java/cn/iocoder/mall/admin/application/controller/admins/AdminController.java +++ b/admin/admin-application/src/main/java/cn/iocoder/mall/admin/application/controller/admins/AdminController.java @@ -42,6 +42,8 @@ public class AdminController { // =========== 当前管理员相关的资源 API =========== + // TODO 功能:当前管理员 + @SuppressWarnings("Duplicates") @GetMapping("/menu_resource_tree") @ApiOperation(value = "获得当前登陆的管理员拥有的菜单权限", notes = "以树结构返回") @@ -120,7 +122,7 @@ public class AdminController { public CommonResult update(@RequestParam("id") Integer id, @RequestParam("username") String username, @RequestParam("nickname") String nickname, - @RequestParam("password") String password) { + @RequestParam(value = "password", required = false) String password) { AdminUpdateDTO adminUpdateDTO = new AdminUpdateDTO().setId(id).setUsername(username).setNickname(nickname).setPassword(password); return adminService.updateAdmin(AdminSecurityContextHolder.getContext().getAdminId(), adminUpdateDTO); } diff --git a/admin/admin-application/src/main/java/cn/iocoder/mall/admin/application/controller/admins/PassportController.java b/admin/admin-application/src/main/java/cn/iocoder/mall/admin/application/controller/admins/PassportController.java index d555f23eb..2c52286f9 100644 --- a/admin/admin-application/src/main/java/cn/iocoder/mall/admin/application/controller/admins/PassportController.java +++ b/admin/admin-application/src/main/java/cn/iocoder/mall/admin/application/controller/admins/PassportController.java @@ -3,17 +3,17 @@ package cn.iocoder.mall.admin.application.controller.admins; import cn.iocoder.common.framework.vo.CommonResult; import cn.iocoder.mall.admin.api.OAuth2Service; import cn.iocoder.mall.admin.api.bo.OAuth2AccessTokenBO; -import cn.iocoder.mall.admin.application.convert.AdminConvert; import cn.iocoder.mall.admin.application.convert.PassportConvert; -import cn.iocoder.mall.admin.application.vo.AdminInfoVO; import cn.iocoder.mall.admin.application.vo.PassportLoginVO; -import cn.iocoder.mall.admin.sdk.context.AdminSecurityContextHolder; import com.alibaba.dubbo.config.annotation.Reference; import io.swagger.annotations.Api; import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiImplicitParams; import io.swagger.annotations.ApiOperation; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("admins/passport") @@ -35,10 +35,8 @@ public class PassportController { return PassportConvert.INSTANCE.convert(result); } - // TODO 艿艿:后续继续完善 - @GetMapping("/info") - public CommonResult info() { - return CommonResult.success(AdminConvert.INSTANCE.convert(AdminSecurityContextHolder.getContext())); - } + // TODO 功能 logout + + // TODO 功能 refresh_token } \ No newline at end of file diff --git a/admin/admin-sdk/src/main/java/cn/iocoder/mall/admin/sdk/interceptor/AdminAccessLogInterceptor.java b/admin/admin-sdk/src/main/java/cn/iocoder/mall/admin/sdk/interceptor/AdminAccessLogInterceptor.java new file mode 100644 index 000000000..7cd08dda8 --- /dev/null +++ b/admin/admin-sdk/src/main/java/cn/iocoder/mall/admin/sdk/interceptor/AdminAccessLogInterceptor.java @@ -0,0 +1,78 @@ +package cn.iocoder.mall.admin.sdk.interceptor; + +import cn.iocoder.common.framework.util.HttpUtil; +import cn.iocoder.mall.admin.api.AdminAccessLogService; +import cn.iocoder.mall.admin.api.dto.AdminAccessLogAddDTO; +import com.alibaba.dubbo.config.annotation.Reference; +import com.alibaba.fastjson.JSON; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Date; + +/** + * 访问日志拦截器 + */ +@Component +public class AdminAccessLogInterceptor extends HandlerInterceptorAdapter { + + private Logger logger = LoggerFactory.getLogger(getClass()); + + /** + * 开始时间 + */ + private static final ThreadLocal START_TIME = new ThreadLocal<>(); + /** + * 管理员编号 + */ + private static final ThreadLocal ADMIN_ID = new ThreadLocal<>(); + + @Reference + private AdminAccessLogService adminAccessLogService; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + // 记录当前时间 + START_TIME.set(new Date()); + return true; + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { + AdminAccessLogAddDTO accessLog = new AdminAccessLogAddDTO(); + try { + accessLog.setAdminId(ADMIN_ID.get()); + if (accessLog.getAdminId() == null) { + accessLog.setAdminId(AdminAccessLogAddDTO.ADMIN_ID_NULL); + } + accessLog.setUri(request.getRequestURI()); // TODO 提升:如果想要优化,可以使用 Swagger 的 @ApiOperation 注解。 + accessLog.setQueryString(HttpUtil.buildQueryString(request)); + accessLog.setMethod(request.getMethod()); + accessLog.setUserAgent(HttpUtil.getUserAgent(request)); + accessLog.setIp(HttpUtil.getIp(request)); + accessLog.setStartTime(START_TIME.get()); + accessLog.setResponseTime((int) (System.currentTimeMillis() - accessLog.getStartTime().getTime()));// 默认响应时间设为0 + adminAccessLogService.addAdminAccessLog(accessLog); + // TODO 提升:暂时不考虑 ELK 的方案。而是基于 MySQL 存储。如果访问日志比较多,需要定期归档。 + } catch (Throwable th) { + logger.error("[afterCompletion][插入管理员访问日志({}) 发生异常({})", JSON.toJSONString(accessLog), ExceptionUtils.getRootCauseMessage(th)); + } finally { + clear(); + } + } + + public static void setAdminId(Integer adminId) { + ADMIN_ID.set(adminId); + } + + public static void clear() { + START_TIME.remove(); + ADMIN_ID.remove(); + } + +} diff --git a/admin/admin-sdk/src/main/java/cn/iocoder/mall/admin/sdk/interceptor/AdminSecurityInterceptor.java b/admin/admin-sdk/src/main/java/cn/iocoder/mall/admin/sdk/interceptor/AdminSecurityInterceptor.java index bfc580613..876071167 100644 --- a/admin/admin-sdk/src/main/java/cn/iocoder/mall/admin/sdk/interceptor/AdminSecurityInterceptor.java +++ b/admin/admin-sdk/src/main/java/cn/iocoder/mall/admin/sdk/interceptor/AdminSecurityInterceptor.java @@ -39,6 +39,13 @@ public class AdminSecurityInterceptor extends HandlerInterceptorAdapter { // 添加到 AdminSecurityContext AdminSecurityContext context = new AdminSecurityContext(authentication.getAdminId(), authentication.getRoleIds()); AdminSecurityContextHolder.setContext(context); + // 同时也记录管理员编号到 AdminAccessLogInterceptor 中。因为: + // AdminAccessLogInterceptor 需要在 AdminSecurityInterceptor 之前执行,这样记录的访问日志才健全 + // AdminSecurityInterceptor 执行后,会移除 AdminSecurityContext 信息,这就导致 AdminAccessLogInterceptor 无法获得管理员编号 + // 因此,这里需要进行记录 + if (authentication.getAdminId() != null) { + AdminAccessLogInterceptor.setAdminId(authentication.getAdminId()); + } } else { String url = request.getRequestURI(); if (!url.equals("/admin/passport/login")) { // TODO 临时写死。非登陆接口,必须已经认证身份,不允许匿名访问 diff --git a/admin/admin-service-api/src/main/java/cn/iocoder/mall/admin/api/AdminAccessLogService.java b/admin/admin-service-api/src/main/java/cn/iocoder/mall/admin/api/AdminAccessLogService.java new file mode 100644 index 000000000..0b22c1819 --- /dev/null +++ b/admin/admin-service-api/src/main/java/cn/iocoder/mall/admin/api/AdminAccessLogService.java @@ -0,0 +1,13 @@ +package cn.iocoder.mall.admin.api; + +import cn.iocoder.common.framework.vo.CommonResult; +import cn.iocoder.mall.admin.api.dto.AdminAccessLogAddDTO; + +/** + * 管理员访问日志 Service 接口 + */ +public interface AdminAccessLogService { + + CommonResult addAdminAccessLog(AdminAccessLogAddDTO adminAccessLogAddDTO); + +} diff --git a/admin/admin-service-api/src/main/java/cn/iocoder/mall/admin/api/AdminService.java b/admin/admin-service-api/src/main/java/cn/iocoder/mall/admin/api/AdminService.java index 78e5e6752..76903ffb6 100644 --- a/admin/admin-service-api/src/main/java/cn/iocoder/mall/admin/api/AdminService.java +++ b/admin/admin-service-api/src/main/java/cn/iocoder/mall/admin/api/AdminService.java @@ -9,6 +9,9 @@ import cn.iocoder.mall.admin.api.dto.AdminUpdateDTO; import java.util.Set; +/** + * 管理员 Service 接口 + */ public interface AdminService { CommonResult getAdminPage(AdminPageDTO adminPageDTO); diff --git a/admin/admin-service-api/src/main/java/cn/iocoder/mall/admin/api/dto/AdminAccessLogAddDTO.java b/admin/admin-service-api/src/main/java/cn/iocoder/mall/admin/api/dto/AdminAccessLogAddDTO.java new file mode 100644 index 000000000..fe259c718 --- /dev/null +++ b/admin/admin-service-api/src/main/java/cn/iocoder/mall/admin/api/dto/AdminAccessLogAddDTO.java @@ -0,0 +1,132 @@ +package cn.iocoder.mall.admin.api.dto; + + +import javax.validation.constraints.NotNull; +import java.util.Date; + +/** + * 管理员访问日志添加 DTO + */ +public class AdminAccessLogAddDTO { + + /** + * 管理员编号 - 空 + */ + public static final Integer ADMIN_ID_NULL = 0; + + /** + * 管理员编号. + * + * 当管理员为空时,该值为0 + */ + @NotNull(message = "管理员编号不能为空") + private Integer adminId; + /** + * 访问地址 + */ + @NotNull(message = "访问地址不能为空") + private String uri; + /** + * 参数 + */ + @NotNull(message = "请求参数不能为空") + private String queryString; + /** + * http 方法 + */ + @NotNull(message = "http 请求方法不能为空") + private String method; + /** + * User Agent + */ + @NotNull(message = "User-Agent 不能为空") + private String userAgent; + /** + * ip + */ + @NotNull(message = "ip 不能为空") + private String ip; + /** + * 请求时间 + */ + @NotNull(message = "请求时间不能为空") + private Date startTime; + /** + * 响应时长 -- 毫秒级 + */ + @NotNull(message = "响应时长不能为空") + private Integer responseTime; + + public Integer getAdminId() { + return adminId; + } + + public AdminAccessLogAddDTO setAdminId(Integer adminId) { + this.adminId = adminId; + return this; + } + + public String getUri() { + return uri; + } + + public AdminAccessLogAddDTO setUri(String uri) { + this.uri = uri; + return this; + } + + public String getQueryString() { + return queryString; + } + + public AdminAccessLogAddDTO setQueryString(String queryString) { + this.queryString = queryString; + return this; + } + + public String getMethod() { + return method; + } + + public AdminAccessLogAddDTO setMethod(String method) { + this.method = method; + return this; + } + + public String getUserAgent() { + return userAgent; + } + + public AdminAccessLogAddDTO setUserAgent(String userAgent) { + this.userAgent = userAgent; + return this; + } + + public String getIp() { + return ip; + } + + public AdminAccessLogAddDTO setIp(String ip) { + this.ip = ip; + return this; + } + + public Date getStartTime() { + return startTime; + } + + public AdminAccessLogAddDTO setStartTime(Date startTime) { + this.startTime = startTime; + return this; + } + + public Integer getResponseTime() { + return responseTime; + } + + public AdminAccessLogAddDTO setResponseTime(Integer responseTime) { + this.responseTime = responseTime; + return this; + } + +} \ No newline at end of file diff --git a/admin/admin-service-impl/src/main/java/cn/iocoder/mall/admin/convert/AdminAccessLogConvert.java b/admin/admin-service-impl/src/main/java/cn/iocoder/mall/admin/convert/AdminAccessLogConvert.java new file mode 100644 index 000000000..a7816e6c7 --- /dev/null +++ b/admin/admin-service-impl/src/main/java/cn/iocoder/mall/admin/convert/AdminAccessLogConvert.java @@ -0,0 +1,17 @@ +package cn.iocoder.mall.admin.convert; + +import cn.iocoder.mall.admin.api.dto.AdminAccessLogAddDTO; +import cn.iocoder.mall.admin.dataobject.AdminAccessLogDO; +import org.mapstruct.Mapper; +import org.mapstruct.Mappings; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface AdminAccessLogConvert { + + AdminAccessLogConvert INSTANCE = Mappers.getMapper(AdminAccessLogConvert.class); + + @Mappings({}) + AdminAccessLogDO convert(AdminAccessLogAddDTO adminAccessLogAddDTO); + +} \ No newline at end of file diff --git a/admin/admin-service-impl/src/main/java/cn/iocoder/mall/admin/dao/AdminAccessLogMapper.java b/admin/admin-service-impl/src/main/java/cn/iocoder/mall/admin/dao/AdminAccessLogMapper.java new file mode 100644 index 000000000..58cd8f525 --- /dev/null +++ b/admin/admin-service-impl/src/main/java/cn/iocoder/mall/admin/dao/AdminAccessLogMapper.java @@ -0,0 +1,11 @@ +package cn.iocoder.mall.admin.dao; + +import cn.iocoder.mall.admin.dataobject.AdminAccessLogDO; +import org.springframework.stereotype.Repository; + +@Repository +public interface AdminAccessLogMapper { + + void insert(AdminAccessLogDO entity); + +} diff --git a/admin/admin-service-impl/src/main/java/cn/iocoder/mall/admin/dataobject/AdminAccessLogDO.java b/admin/admin-service-impl/src/main/java/cn/iocoder/mall/admin/dataobject/AdminAccessLogDO.java new file mode 100644 index 000000000..93221788c --- /dev/null +++ b/admin/admin-service-impl/src/main/java/cn/iocoder/mall/admin/dataobject/AdminAccessLogDO.java @@ -0,0 +1,132 @@ +package cn.iocoder.mall.admin.dataobject; + +import cn.iocoder.common.framework.dataobject.BaseDO; + +import java.util.Date; + +/** + * 管理员访问日志 DO + */ +public class AdminAccessLogDO extends BaseDO { + + /** + * 编号 + */ + private Integer id; + /** + * 管理员编号. + * + * 当管理员为空时,该值为0 + */ + private Integer adminId; + /** + * 访问地址 + */ + private String uri; + /** + * 参数 + */ + private String queryString; + /** + * http 方法 + */ + private String method; + /** + * userAgent + */ + private String userAgent; + /** + * ip + */ + private String ip; + /** + * 请求时间 + */ + private Date startTime; + /** + * 响应时长 -- 毫秒级 + */ + private Integer responseTime; + + public Integer getId() { + return id; + } + + public AdminAccessLogDO setId(Integer id) { + this.id = id; + return this; + } + + public Integer getAdminId() { + return adminId; + } + + public AdminAccessLogDO setAdminId(Integer adminId) { + this.adminId = adminId; + return this; + } + + public String getUri() { + return uri; + } + + public AdminAccessLogDO setUri(String uri) { + this.uri = uri; + return this; + } + + public String getQueryString() { + return queryString; + } + + public AdminAccessLogDO setQueryString(String queryString) { + this.queryString = queryString; + return this; + } + + public String getMethod() { + return method; + } + + public AdminAccessLogDO setMethod(String method) { + this.method = method; + return this; + } + + public String getUserAgent() { + return userAgent; + } + + public AdminAccessLogDO setUserAgent(String userAgent) { + this.userAgent = userAgent; + return this; + } + + public String getIp() { + return ip; + } + + public AdminAccessLogDO setIp(String ip) { + this.ip = ip; + return this; + } + + public Date getStartTime() { + return startTime; + } + + public AdminAccessLogDO setStartTime(Date startTime) { + this.startTime = startTime; + return this; + } + + public Integer getResponseTime() { + return responseTime; + } + + public AdminAccessLogDO setResponseTime(Integer responseTime) { + this.responseTime = responseTime; + return this; + } + +} \ No newline at end of file diff --git a/admin/admin-service-impl/src/main/java/cn/iocoder/mall/admin/service/AdminAccessLogServiceImpl.java b/admin/admin-service-impl/src/main/java/cn/iocoder/mall/admin/service/AdminAccessLogServiceImpl.java new file mode 100644 index 000000000..c2f5d7160 --- /dev/null +++ b/admin/admin-service-impl/src/main/java/cn/iocoder/mall/admin/service/AdminAccessLogServiceImpl.java @@ -0,0 +1,56 @@ +package cn.iocoder.mall.admin.service; + +import cn.iocoder.common.framework.util.StringUtil; +import cn.iocoder.common.framework.vo.CommonResult; +import cn.iocoder.mall.admin.api.AdminAccessLogService; +import cn.iocoder.mall.admin.api.dto.AdminAccessLogAddDTO; +import cn.iocoder.mall.admin.convert.AdminAccessLogConvert; +import cn.iocoder.mall.admin.dao.AdminAccessLogMapper; +import cn.iocoder.mall.admin.dataobject.AdminAccessLogDO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Date; + +@Service +@com.alibaba.dubbo.config.annotation.Service(validation = "true") +public class AdminAccessLogServiceImpl implements AdminAccessLogService { + + /** + * 请求参数最大长度。 + */ + private static final Integer QUERY_STRING_MAX_LENGTH = 4096; + /** + * 请求地址最大长度。 + */ + private static final Integer URI_MAX_LENGTH = 4096; + /** + * User-Agent 最大长度。 + */ + private static final Integer USER_AGENT_MAX_LENGTH = 1024; + + @Autowired + private AdminAccessLogMapper adminAccessLogMapper; + + @Override + public CommonResult addAdminAccessLog(AdminAccessLogAddDTO adminAccessLogAddDTO) { + // 创建 AdminAccessLogDO + AdminAccessLogDO accessLog = AdminAccessLogConvert.INSTANCE.convert(adminAccessLogAddDTO); + accessLog.setCreateTime(new Date()); + // 截取最大长度 + if (accessLog.getUri().length() > URI_MAX_LENGTH) { + accessLog.setUri(StringUtil.substring(accessLog.getUri(), URI_MAX_LENGTH)); + } + if (accessLog.getQueryString().length() > QUERY_STRING_MAX_LENGTH) { + accessLog.setQueryString(StringUtil.substring(accessLog.getQueryString(), QUERY_STRING_MAX_LENGTH)); + } + if (accessLog.getUserAgent().length() > USER_AGENT_MAX_LENGTH) { + accessLog.setUserAgent(StringUtil.substring(accessLog.getUserAgent(), USER_AGENT_MAX_LENGTH)); + } + // 插入 + adminAccessLogMapper.insert(accessLog); + // 返回成功 + return CommonResult.success(true); + } + +} \ No newline at end of file diff --git a/admin/admin-service-impl/src/main/java/cn/iocoder/mall/admin/service/OAuth2ServiceImpl.java b/admin/admin-service-impl/src/main/java/cn/iocoder/mall/admin/service/OAuth2ServiceImpl.java index ca88f7da4..3215002dc 100644 --- a/admin/admin-service-impl/src/main/java/cn/iocoder/mall/admin/service/OAuth2ServiceImpl.java +++ b/admin/admin-service-impl/src/main/java/cn/iocoder/mall/admin/service/OAuth2ServiceImpl.java @@ -77,7 +77,6 @@ public class OAuth2ServiceImpl implements OAuth2Service { } // 获得管理员拥有的角色 List adminRoleDOs = adminService.getAdminRoles(accessTokenDO.getAdminId()); - // TODO 芋艿,有个 bug ,要排除掉已经失效的角色 return CommonResult.success(OAuth2Convert.INSTANCE.convertToAuthentication(accessTokenDO, adminRoleDOs)); } diff --git a/admin/admin-service-impl/src/main/resources/mapper/AdminAccessLogMapper.xml b/admin/admin-service-impl/src/main/resources/mapper/AdminAccessLogMapper.xml new file mode 100644 index 000000000..4afa93f7c --- /dev/null +++ b/admin/admin-service-impl/src/main/resources/mapper/AdminAccessLogMapper.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + INSERT INTO admin_access_log ( + admin_id, uri, query_string, method, user_agent, + ip, start_time, response_time, create_time + ) VALUES ( + #{adminId}, #{uri}, #{queryString}, #{method}, #{userAgent}, + #{ip}, #{startTime}, #{responseTime}, #{createTime} + ) + + + \ No newline at end of file diff --git a/common/common-framework/src/main/java/cn/iocoder/common/framework/util/HttpUtil.java b/common/common-framework/src/main/java/cn/iocoder/common/framework/util/HttpUtil.java index 418db24aa..c71bb9323 100644 --- a/common/common-framework/src/main/java/cn/iocoder/common/framework/util/HttpUtil.java +++ b/common/common-framework/src/main/java/cn/iocoder/common/framework/util/HttpUtil.java @@ -1,11 +1,49 @@ package cn.iocoder.common.framework.util; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.util.StringUtils; +import javax.servlet.ServletRequest; import javax.servlet.http.HttpServletRequest; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.Enumeration; public class HttpUtil { + private static final Logger logger = LoggerFactory.getLogger(HttpUtil.class); + + /** + * Standard Servlet 2.3+ spec request attributes for include URI and paths. + *

If included via a RequestDispatcher, the current resource will see the + * originating request. Its own URI and paths are exposed as request attributes. + */ + public static final String INCLUDE_REQUEST_URI_ATTRIBUTE = "javax.servlet.include.request_uri"; + public static final String INCLUDE_CONTEXT_PATH_ATTRIBUTE = "javax.servlet.include.context_path"; +// public static final String INCLUDE_SERVLET_PATH_ATTRIBUTE = "javax.servlet.include.servlet_path"; +// public static final String INCLUDE_PATH_INFO_ATTRIBUTE = "javax.servlet.include.path_info"; +// public static final String INCLUDE_QUERY_STRING_ATTRIBUTE = "javax.servlet.include.query_string"; +// +// /** +// * Standard Servlet 2.4+ spec request attributes for forward URI and paths. +// *

If forwarded to via a RequestDispatcher, the current resource will see its +// * own URI and paths. The originating URI and paths are exposed as request attributes. +// */ +// public static final String FORWARD_REQUEST_URI_ATTRIBUTE = "javax.servlet.forward.request_uri"; +// public static final String FORWARD_CONTEXT_PATH_ATTRIBUTE = "javax.servlet.forward.context_path"; +// public static final String FORWARD_SERVLET_PATH_ATTRIBUTE = "javax.servlet.forward.servlet_path"; +// public static final String FORWARD_PATH_INFO_ATTRIBUTE = "javax.servlet.forward.path_info"; +// public static final String FORWARD_QUERY_STRING_ATTRIBUTE = "javax.servlet.forward.query_string"; + + /** + * Default character encoding to use when request.getCharacterEncoding + * returns null, according to the Servlet spec. + * + * @see javax.servlet.ServletRequest#getCharacterEncoding + */ + public static final String DEFAULT_CHARACTER_ENCODING = "ISO-8859-1"; + public static String obtainAccess(HttpServletRequest request) { String authorization = request.getHeader("Authorization"); if (!StringUtils.hasText(authorization)) { @@ -39,4 +77,243 @@ public class HttpUtil { return request.getRemoteAddr(); } + /** + * @param request 请求 + * @return ua + */ + public static String getUserAgent(HttpServletRequest request) { + String ua = request.getHeader("User-Agent"); + return ua != null ? ua : ""; + } + + /** + * 根据request拼接queryString + * + * @return queryString + */ + @SuppressWarnings("unchecked") + public static String buildQueryString(HttpServletRequest request) { + Enumeration es = request.getParameterNames(); + if (!es.hasMoreElements()) { + return ""; + } + String parameterName, parameterValue; + StringBuilder params = new StringBuilder(); + while (es.hasMoreElements()) { + parameterName = es.nextElement(); + parameterValue = request.getParameter(parameterName); + params.append(parameterName).append("=").append(parameterValue).append("&"); + } + return params.deleteCharAt(params.length() - 1).toString(); + } + + /** + * Return the path within the web application for the given request. + * Detects include request URL if called within a RequestDispatcher include. + *

+ * For example, for a request to URL + *

+ * http://www.somehost.com/myapp/my/url.jsp, + *

+ * for an application deployed to /mayapp (the application's context path), this method would return + *

+ * /my/url.jsp. + * + * 该方法,是从 Shiro 源码中,扣出来。add by 芋艿 + * + * @param request current HTTP request + * @return the path within the web application + */ + public static String getPathWithinApplication(HttpServletRequest request) { + String contextPath = getContextPath(request); + String requestUri = getRequestUri(request); + if (StringUtils.startsWithIgnoreCase(requestUri, contextPath)) { + // Normal case: URI contains context path. + String path = requestUri.substring(contextPath.length()); + return (StringUtils.hasText(path) ? path : "/"); + } else { + // Special case: rather unusual. + return requestUri; + } + } + + /** + * Return the request URI for the given request, detecting an include request + * URL if called within a RequestDispatcher include. + *

As the value returned by request.getRequestURI() is not + * decoded by the servlet container, this method will decode it. + *

The URI that the web container resolves should be correct, but some + * containers like JBoss/Jetty incorrectly include ";" strings like ";jsessionid" + * in the URI. This method cuts off such incorrect appendices. + * + * @param request current HTTP request + * @return the request URI + */ + public static String getRequestUri(HttpServletRequest request) { + String uri = (String) request.getAttribute(INCLUDE_REQUEST_URI_ATTRIBUTE); + if (uri == null) { + uri = request.getRequestURI(); + } + return normalize(decodeAndCleanUriString(request, uri)); + } + + /** + * Normalize a relative URI path that may have relative values ("/./", + * "/../", and so on ) it it. WARNING - This method is + * useful only for normalizing application-generated paths. It does not + * try to perform security checks for malicious input. + * Normalize operations were was happily taken from org.apache.catalina.util.RequestUtil in + * Tomcat trunk, r939305 + * + * @param path Relative path to be normalized + * @return normalized path + */ + public static String normalize(String path) { + return normalize(path, true); + } + + /** + * Normalize a relative URI path that may have relative values ("/./", + * "/../", and so on ) it it. WARNING - This method is + * useful only for normalizing application-generated paths. It does not + * try to perform security checks for malicious input. + * Normalize operations were was happily taken from org.apache.catalina.util.RequestUtil in + * Tomcat trunk, r939305 + * + * @param path Relative path to be normalized + * @param replaceBackSlash Should '\\' be replaced with '/' + * @return normalized path + */ + private static String normalize(String path, boolean replaceBackSlash) { + + if (path == null) + return null; + + // Create a place for the normalized path + String normalized = path; + + if (replaceBackSlash && normalized.indexOf('\\') >= 0) + normalized = normalized.replace('\\', '/'); + + if (normalized.equals("/.")) + return "/"; + + // Add a leading "/" if necessary + if (!normalized.startsWith("/")) + normalized = "/" + normalized; + + // Resolve occurrences of "//" in the normalized path + while (true) { + int index = normalized.indexOf("//"); + if (index < 0) + break; + normalized = normalized.substring(0, index) + + normalized.substring(index + 1); + } + + // Resolve occurrences of "/./" in the normalized path + while (true) { + int index = normalized.indexOf("/./"); + if (index < 0) + break; + normalized = normalized.substring(0, index) + + normalized.substring(index + 2); + } + + // Resolve occurrences of "/../" in the normalized path + while (true) { + int index = normalized.indexOf("/../"); + if (index < 0) + break; + if (index == 0) + return (null); // Trying to go outside our context + int index2 = normalized.lastIndexOf('/', index - 1); + normalized = normalized.substring(0, index2) + + normalized.substring(index + 3); + } + + // Return the normalized path that we have completed + return (normalized); + } + + /** + * Decode the supplied URI string and strips any extraneous portion after a ';'. + * + * @param request the incoming HttpServletRequest + * @param uri the application's URI string + * @return the supplied URI string stripped of any extraneous portion after a ';'. + */ + private static String decodeAndCleanUriString(HttpServletRequest request, String uri) { + uri = decodeRequestString(request, uri); + int semicolonIndex = uri.indexOf(';'); + return (semicolonIndex != -1 ? uri.substring(0, semicolonIndex) : uri); + } + + /** + * Return the context path for the given request, detecting an include request + * URL if called within a RequestDispatcher include. + *

As the value returned by request.getContextPath() is not + * decoded by the servlet container, this method will decode it. + * + * @param request current HTTP request + * @return the context path + */ + public static String getContextPath(HttpServletRequest request) { + String contextPath = (String) request.getAttribute(INCLUDE_CONTEXT_PATH_ATTRIBUTE); + if (contextPath == null) { + contextPath = request.getContextPath(); + } + if ("/".equals(contextPath)) { + // Invalid case, but happens for includes on Jetty: silently adapt it. + contextPath = ""; + } + return decodeRequestString(request, contextPath); + } + + /** + * Decode the given source string with a URLDecoder. The encoding will be taken + * from the request, falling back to the default "ISO-8859-1". + *

The default implementation uses URLDecoder.decode(input, enc). + * + * @param request current HTTP request + * @param source the String to decode + * @return the decoded String + * @see #DEFAULT_CHARACTER_ENCODING + * @see javax.servlet.ServletRequest#getCharacterEncoding + * @see java.net.URLDecoder#decode(String, String) + * @see java.net.URLDecoder#decode(String) + */ + @SuppressWarnings({"deprecation"}) + public static String decodeRequestString(HttpServletRequest request, String source) { + String enc = determineEncoding(request); + try { + return URLDecoder.decode(source, enc); + } catch (UnsupportedEncodingException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Could not decode request string [" + source + "] with encoding '" + enc + + "': falling back to platform default encoding; exception message: " + ex.getMessage()); + } + return URLDecoder.decode(source); + } + } + + /** + * Determine the encoding for the given request. + * Can be overridden in subclasses. + *

The default implementation checks the request's + * {@link ServletRequest#getCharacterEncoding() character encoding}, and if that + * null, falls back to the {@link #DEFAULT_CHARACTER_ENCODING}. + * + * @param request current HTTP request + * @return the encoding for the request (never null) + * @see javax.servlet.ServletRequest#getCharacterEncoding() + */ + protected static String determineEncoding(HttpServletRequest request) { + String enc = request.getCharacterEncoding(); + if (enc == null) { + enc = DEFAULT_CHARACTER_ENCODING; + } + return enc; + } + } \ No newline at end of file diff --git a/common/common-framework/src/main/java/cn/iocoder/common/framework/util/StringUtil.java b/common/common-framework/src/main/java/cn/iocoder/common/framework/util/StringUtil.java index 8e98169ed..c81be713f 100644 --- a/common/common-framework/src/main/java/cn/iocoder/common/framework/util/StringUtil.java +++ b/common/common-framework/src/main/java/cn/iocoder/common/framework/util/StringUtil.java @@ -27,4 +27,8 @@ public class StringUtil { return array; } + public static String substring(String str, int start) { + return org.apache.commons.lang3.StringUtils.substring(str, start); + } + } \ No newline at end of file diff --git a/product/product-application/src/main/java/cn/iocoder/mall/product/application/controller/admins/AdminsProductSpuController.java b/product/product-application/src/main/java/cn/iocoder/mall/product/application/controller/admins/AdminsProductSpuController.java index 0c64f6dab..d598b03dd 100644 --- a/product/product-application/src/main/java/cn/iocoder/mall/product/application/controller/admins/AdminsProductSpuController.java +++ b/product/product-application/src/main/java/cn/iocoder/mall/product/application/controller/admins/AdminsProductSpuController.java @@ -52,7 +52,7 @@ public class AdminsProductSpuController { @RequestParam("sellPoint") String sellPoint, @RequestParam("description") String description, @RequestParam("cid") Integer cid, - @RequestParam("picURLs") List picUrls, + @RequestParam("picUrls") List picUrls, @RequestParam("visible") Boolean visible, @RequestParam("skuStr") String skuStr) { // TODO 芋艿,因为考虑不使用 json 接受参数,所以这里手动转。 // 创建 ProductSpuAddDTO 对象 diff --git a/user/user-application/src/main/java/cn/iocoder/mall/user/application/config/MVCConfiguration.java b/user/user-application/src/main/java/cn/iocoder/mall/user/application/config/MVCConfiguration.java index 3fd525fa5..23e561992 100644 --- a/user/user-application/src/main/java/cn/iocoder/mall/user/application/config/MVCConfiguration.java +++ b/user/user-application/src/main/java/cn/iocoder/mall/user/application/config/MVCConfiguration.java @@ -2,6 +2,7 @@ package cn.iocoder.mall.user.application.config; import cn.iocoder.common.framework.config.GlobalExceptionHandler; import cn.iocoder.mall.admin.sdk.interceptor.AdminSecurityInterceptor; +import cn.iocoder.mall.user.sdk.interceptor.UserAccessLogInterceptor; import cn.iocoder.mall.user.sdk.interceptor.UserSecurityInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; @@ -18,13 +19,18 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; public class MVCConfiguration implements WebMvcConfigurer { @Autowired - private UserSecurityInterceptor securityInterceptor; + private UserSecurityInterceptor userSecurityInterceptor; + @Autowired + private UserAccessLogInterceptor userAccessLogInterceptor; @Autowired private AdminSecurityInterceptor adminSecurityInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(securityInterceptor).addPathPatterns("/users/**"); // 只拦截我们定义的接口 + // 用户 + registry.addInterceptor(userAccessLogInterceptor).addPathPatterns("/users/**"); + registry.addInterceptor(userSecurityInterceptor).addPathPatterns("/users/**"); // 只拦截我们定义的接口 + // 管理员 registry.addInterceptor(adminSecurityInterceptor).addPathPatterns("/admins/**"); // 只拦截我们定义的接口 } diff --git a/user/user-sdk/src/main/java/cn/iocoder/mall/user/sdk/interceptor/UserAccessLogInterceptor.java b/user/user-sdk/src/main/java/cn/iocoder/mall/user/sdk/interceptor/UserAccessLogInterceptor.java new file mode 100644 index 000000000..c52646fcf --- /dev/null +++ b/user/user-sdk/src/main/java/cn/iocoder/mall/user/sdk/interceptor/UserAccessLogInterceptor.java @@ -0,0 +1,78 @@ +package cn.iocoder.mall.user.sdk.interceptor; + +import cn.iocoder.common.framework.util.HttpUtil; +import cn.iocoder.mall.user.service.api.UserAccessLogService; +import cn.iocoder.mall.user.service.api.dto.UserAccessLogAddDTO; +import com.alibaba.dubbo.config.annotation.Reference; +import com.alibaba.fastjson.JSON; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Date; + +/** + * 访问日志拦截器 + */ +@Component +public class UserAccessLogInterceptor extends HandlerInterceptorAdapter { + + private Logger logger = LoggerFactory.getLogger(getClass()); + + /** + * 开始时间 + */ + private static final ThreadLocal START_TIME = new ThreadLocal<>(); + /** + * 管理员编号 + */ + private static final ThreadLocal USER_ID = new ThreadLocal<>(); + + @Reference + private UserAccessLogService userAccessLogService; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + // 记录当前时间 + START_TIME.set(new Date()); + return true; + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { + UserAccessLogAddDTO accessLog = new UserAccessLogAddDTO(); + try { + accessLog.setUserId(USER_ID.get()); + if (accessLog.getUserId() == null) { + accessLog.setUserId(UserAccessLogAddDTO.USER_ID_NULL); + } + accessLog.setUri(request.getRequestURI()); // TODO 提升:如果想要优化,可以使用 Swagger 的 @ApiOperation 注解。 + accessLog.setQueryString(HttpUtil.buildQueryString(request)); + accessLog.setMethod(request.getMethod()); + accessLog.setUserAgent(HttpUtil.getUserAgent(request)); + accessLog.setIp(HttpUtil.getIp(request)); + accessLog.setStartTime(START_TIME.get()); + accessLog.setResponseTime((int) (System.currentTimeMillis() - accessLog.getStartTime().getTime()));// 默认响应时间设为0 + userAccessLogService.addUserAccessLog(accessLog); + // TODO 提升:暂时不考虑 ELK 的方案。而是基于 MySQL 存储。如果访问日志比较多,需要定期归档。 + } catch (Throwable th) { + logger.error("[afterCompletion][插入管理员访问日志({}) 发生异常({})", JSON.toJSONString(accessLog), ExceptionUtils.getRootCauseMessage(th)); + } finally { + clear(); + } + } + + public static void setUserId(Integer userId) { + USER_ID.set(userId); + } + + public static void clear() { + START_TIME.remove(); + USER_ID.remove(); + } + +} diff --git a/user/user-sdk/src/main/java/cn/iocoder/mall/user/sdk/interceptor/UserSecurityInterceptor.java b/user/user-sdk/src/main/java/cn/iocoder/mall/user/sdk/interceptor/UserSecurityInterceptor.java index b11c643b8..73605789a 100644 --- a/user/user-sdk/src/main/java/cn/iocoder/mall/user/sdk/interceptor/UserSecurityInterceptor.java +++ b/user/user-sdk/src/main/java/cn/iocoder/mall/user/sdk/interceptor/UserSecurityInterceptor.java @@ -40,6 +40,13 @@ public class UserSecurityInterceptor extends HandlerInterceptorAdapter { // 添加到 SecurityContext UserSecurityContext context = new UserSecurityContext(authentication.getUserId()); UserSecurityContextHolder.setContext(context); + // 同时也记录管理员编号到 AdminAccessLogInterceptor 中。因为: + // AdminAccessLogInterceptor 需要在 AdminSecurityInterceptor 之前执行,这样记录的访问日志才健全 + // AdminSecurityInterceptor 执行后,会移除 AdminSecurityContext 信息,这就导致 AdminAccessLogInterceptor 无法获得管理员编号 + // 因此,这里需要进行记录 + if (authentication.getUserId() != null) { + UserAccessLogInterceptor.setUserId(authentication.getUserId()); + } } // 校验是否需要已授权 HandlerMethod method = (HandlerMethod) handler; diff --git a/user/user-service-api/src/main/java/cn/iocoder/mall/user/service/api/UserAccessLogService.java b/user/user-service-api/src/main/java/cn/iocoder/mall/user/service/api/UserAccessLogService.java new file mode 100644 index 000000000..c840b8ffc --- /dev/null +++ b/user/user-service-api/src/main/java/cn/iocoder/mall/user/service/api/UserAccessLogService.java @@ -0,0 +1,10 @@ +package cn.iocoder.mall.user.service.api; + +import cn.iocoder.common.framework.vo.CommonResult; +import cn.iocoder.mall.user.service.api.dto.UserAccessLogAddDTO; + +public interface UserAccessLogService { + + CommonResult addUserAccessLog(UserAccessLogAddDTO userAccessLogAddDTO); + +} \ No newline at end of file diff --git a/user/user-service-api/src/main/java/cn/iocoder/mall/user/service/api/dto/UserAccessLogAddDTO.java b/user/user-service-api/src/main/java/cn/iocoder/mall/user/service/api/dto/UserAccessLogAddDTO.java new file mode 100644 index 000000000..bccb98895 --- /dev/null +++ b/user/user-service-api/src/main/java/cn/iocoder/mall/user/service/api/dto/UserAccessLogAddDTO.java @@ -0,0 +1,131 @@ +package cn.iocoder.mall.user.service.api.dto; + +import javax.validation.constraints.NotNull; +import java.util.Date; + +/** + * 用户访问日志添加 DTO + */ +public class UserAccessLogAddDTO { + + /** + * 用户编号 - 空 + */ + public static final Integer USER_ID_NULL = 0; + + /** + * 用户编号. + * + * 当用户为空时,该值为0 + */ + @NotNull(message = "用户编号不能为空") + private Integer userId; + /** + * 访问地址 + */ + @NotNull(message = "访问地址不能为空") + private String uri; + /** + * 参数 + */ + @NotNull(message = "请求参数不能为空") + private String queryString; + /** + * http 方法 + */ + @NotNull(message = "http 请求方法不能为空") + private String method; + /** + * User Agent + */ + @NotNull(message = "User-Agent 不能为空") + private String userAgent; + /** + * ip + */ + @NotNull(message = "ip 不能为空") + private String ip; + /** + * 请求时间 + */ + @NotNull(message = "请求时间不能为空") + private Date startTime; + /** + * 响应时长 -- 毫秒级 + */ + @NotNull(message = "响应时长不能为空") + private Integer responseTime; + + public Integer getUserId() { + return userId; + } + + public UserAccessLogAddDTO setUserId(Integer userId) { + this.userId = userId; + return this; + } + + public String getUri() { + return uri; + } + + public UserAccessLogAddDTO setUri(String uri) { + this.uri = uri; + return this; + } + + public String getQueryString() { + return queryString; + } + + public UserAccessLogAddDTO setQueryString(String queryString) { + this.queryString = queryString; + return this; + } + + public String getMethod() { + return method; + } + + public UserAccessLogAddDTO setMethod(String method) { + this.method = method; + return this; + } + + public String getUserAgent() { + return userAgent; + } + + public UserAccessLogAddDTO setUserAgent(String userAgent) { + this.userAgent = userAgent; + return this; + } + + public String getIp() { + return ip; + } + + public UserAccessLogAddDTO setIp(String ip) { + this.ip = ip; + return this; + } + + public Date getStartTime() { + return startTime; + } + + public UserAccessLogAddDTO setStartTime(Date startTime) { + this.startTime = startTime; + return this; + } + + public Integer getResponseTime() { + return responseTime; + } + + public UserAccessLogAddDTO setResponseTime(Integer responseTime) { + this.responseTime = responseTime; + return this; + } + +} \ No newline at end of file diff --git a/user/user-service-impl/src/main/java/cn/iocoder/mall/user/convert/UserAccessLogConvert.java b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/convert/UserAccessLogConvert.java new file mode 100644 index 000000000..a6e7ddb5e --- /dev/null +++ b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/convert/UserAccessLogConvert.java @@ -0,0 +1,17 @@ +package cn.iocoder.mall.user.convert; + +import cn.iocoder.mall.user.dataobject.UserAccessLogDO; +import cn.iocoder.mall.user.service.api.dto.UserAccessLogAddDTO; +import org.mapstruct.Mapper; +import org.mapstruct.Mappings; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface UserAccessLogConvert { + + UserAccessLogConvert INSTANCE = Mappers.getMapper(UserAccessLogConvert.class); + + @Mappings({}) + UserAccessLogDO convert(UserAccessLogAddDTO adminAccessLogAddDTO); + +} \ No newline at end of file diff --git a/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dao/UserAccessLogMapper.java b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dao/UserAccessLogMapper.java new file mode 100644 index 000000000..c86c58b40 --- /dev/null +++ b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dao/UserAccessLogMapper.java @@ -0,0 +1,11 @@ +package cn.iocoder.mall.user.dao; + +import cn.iocoder.mall.user.dataobject.UserAccessLogDO; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserAccessLogMapper { + + void insert(UserAccessLogDO entity); + +} diff --git a/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dataobject/UserAccessLogDO.java b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dataobject/UserAccessLogDO.java new file mode 100644 index 000000000..4c10fa920 --- /dev/null +++ b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dataobject/UserAccessLogDO.java @@ -0,0 +1,132 @@ +package cn.iocoder.mall.user.dataobject; + +import cn.iocoder.common.framework.dataobject.BaseDO; + +import java.util.Date; + +/** + * 用户访问日志 DO + */ +public class UserAccessLogDO extends BaseDO { + + /** + * 编号 + */ + private Integer id; + /** + * 用户编号. + * + * 当用户编号为空时,该值为0 + */ + private Integer userId; + /** + * 访问地址 + */ + private String uri; + /** + * 参数 + */ + private String queryString; + /** + * http 方法 + */ + private String method; + /** + * userAgent + */ + private String userAgent; + /** + * ip + */ + private String ip; + /** + * 请求时间 + */ + private Date startTime; + /** + * 响应时长 -- 毫秒级 + */ + private Integer responseTime; + + public Integer getId() { + return id; + } + + public UserAccessLogDO setId(Integer id) { + this.id = id; + return this; + } + + public Integer getUserId() { + return userId; + } + + public UserAccessLogDO setUserId(Integer userId) { + this.userId = userId; + return this; + } + + public String getUri() { + return uri; + } + + public UserAccessLogDO setUri(String uri) { + this.uri = uri; + return this; + } + + public String getQueryString() { + return queryString; + } + + public UserAccessLogDO setQueryString(String queryString) { + this.queryString = queryString; + return this; + } + + public String getMethod() { + return method; + } + + public UserAccessLogDO setMethod(String method) { + this.method = method; + return this; + } + + public String getUserAgent() { + return userAgent; + } + + public UserAccessLogDO setUserAgent(String userAgent) { + this.userAgent = userAgent; + return this; + } + + public String getIp() { + return ip; + } + + public UserAccessLogDO setIp(String ip) { + this.ip = ip; + return this; + } + + public Date getStartTime() { + return startTime; + } + + public UserAccessLogDO setStartTime(Date startTime) { + this.startTime = startTime; + return this; + } + + public Integer getResponseTime() { + return responseTime; + } + + public UserAccessLogDO setResponseTime(Integer responseTime) { + this.responseTime = responseTime; + return this; + } + +} \ No newline at end of file diff --git a/user/user-service-impl/src/main/java/cn/iocoder/mall/user/service/UserAccessLogServiceImpl.java b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/service/UserAccessLogServiceImpl.java new file mode 100644 index 000000000..7c409586e --- /dev/null +++ b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/service/UserAccessLogServiceImpl.java @@ -0,0 +1,56 @@ +package cn.iocoder.mall.user.service; + +import cn.iocoder.common.framework.util.StringUtil; +import cn.iocoder.common.framework.vo.CommonResult; +import cn.iocoder.mall.user.convert.UserAccessLogConvert; +import cn.iocoder.mall.user.dao.UserAccessLogMapper; +import cn.iocoder.mall.user.dataobject.UserAccessLogDO; +import cn.iocoder.mall.user.service.api.UserAccessLogService; +import cn.iocoder.mall.user.service.api.dto.UserAccessLogAddDTO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Date; + +@Service +@com.alibaba.dubbo.config.annotation.Service(validation = "true") +public class UserAccessLogServiceImpl implements UserAccessLogService { + + /** + * 请求参数最大长度。 + */ + private static final Integer QUERY_STRING_MAX_LENGTH = 4096; + /** + * 请求地址最大长度。 + */ + private static final Integer URI_MAX_LENGTH = 4096; + /** + * User-Agent 最大长度。 + */ + private static final Integer USER_AGENT_MAX_LENGTH = 1024; + + @Autowired + private UserAccessLogMapper userAccessLogMapper; + + @Override + public CommonResult addUserAccessLog(UserAccessLogAddDTO userAccessLogAddDTO) { + // 创建 UserAccessLogDO + UserAccessLogDO accessLog = UserAccessLogConvert.INSTANCE.convert(userAccessLogAddDTO); + accessLog.setCreateTime(new Date()); + // 截取最大长度 + if (accessLog.getUri().length() > URI_MAX_LENGTH) { + accessLog.setUri(StringUtil.substring(accessLog.getUri(), URI_MAX_LENGTH)); + } + if (accessLog.getQueryString().length() > QUERY_STRING_MAX_LENGTH) { + accessLog.setQueryString(StringUtil.substring(accessLog.getQueryString(), QUERY_STRING_MAX_LENGTH)); + } + if (accessLog.getUserAgent().length() > USER_AGENT_MAX_LENGTH) { + accessLog.setUserAgent(StringUtil.substring(accessLog.getUserAgent(), USER_AGENT_MAX_LENGTH)); + } + // 插入 + userAccessLogMapper.insert(accessLog); + // 返回成功 + return CommonResult.success(true); + } + +} \ No newline at end of file diff --git a/user/user-service-impl/src/main/resources/mapper/UserAccessLogMapper.xml b/user/user-service-impl/src/main/resources/mapper/UserAccessLogMapper.xml new file mode 100644 index 000000000..4168673ac --- /dev/null +++ b/user/user-service-impl/src/main/resources/mapper/UserAccessLogMapper.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + INSERT INTO user_access_log ( + user_id, uri, query_string, method, user_agent, + ip, start_time, response_time, create_time + ) VALUES ( + #{userId}, #{uri}, #{queryString}, #{method}, #{userAgent}, + #{ip}, #{startTime}, #{responseTime}, #{createTime} + ) + + + \ No newline at end of file