Merge remote-tracking branch 'origin/master'

This commit is contained in:
sin 2019-03-20 13:19:11 +08:00
commit b384179610
45 changed files with 1948 additions and 546 deletions

View File

@ -1,136 +0,0 @@
English | [简体中文](./README.zh-CN.md) | [Русский](./README.ru-RU.md)
<h1 align="center">Ant Design Pro</h1>
<div align="center">
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)
</div>
- 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.
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/opera/opera_48x48.png" alt="Opera" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>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.

View File

@ -3,3 +3,32 @@
> 采用 antd pro 快速开发 > 采用 antd pro 快速开发
> TODO > 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文件需要加上头部注释 (写清楚改控件的用处)

View File

@ -1,103 +0,0 @@
[English](./README.md) | [简体中文](./README.zh-CN.md) | Русский
<h1 align="center">Ant Design Pro</h1>
<div align="center">
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)
</div>
- Демо: 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.
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/opera/opera_48x48.png" alt="Opera" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>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) для совершенствования нашего кода.

View File

@ -1,121 +0,0 @@
[English](./README.md) | 简体中文 | [Русский](./README.ru-RU.md)
<h1 align="center">Ant Design Pro</h1>
<div align="center">
开箱即用的中台前端/设计解决方案。
[![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)
</div>
- 预览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。
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/opera/opera_48x48.png" alt="Opera" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>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 的代码。

View File

@ -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)
}
}

View File

@ -49,10 +49,10 @@ function getDictionaryTree(req, res) {
} }
export default { 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/admin/url_resource_list': getAdminUrls,
'GET /admin-api/admins/resource/tree': getResourceTree, 'GET /admin-api/admins/resource/tree': getResourceTree,
'GET /admin-api/admins/role/page': getQueryRole, '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, 'GET /admin-api/admins/data_dict/tree': getDictionaryTree,
}; };

View File

@ -5,6 +5,7 @@ export default class DictionaryText extends PureComponent {
componentDidMount() {} componentDidMount() {}
render() { render() {
debugger;
const { dicKey, dicValue } = this.props; const { dicKey, dicValue } = this.props;
return ( return (
<DictionaryContext.Consumer> <DictionaryContext.Consumer>

View File

@ -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(<Option key={`option-attr-${index}-${allAttr.id}`} value={allAttr.id}>{allAttr.name}</Option>);
}
// 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(<Option key={`option-attr-value-${index}-${attrValue.id}`}
value={attrValue.id + ''}>{attrValue.name}</Option>); // + '' 的原因是多选必须是字符串
}
}
// 3. 拼装最终,添加到 attrTreeHTML 中
return <div key={`div-attr-${index}`}>
<Select key={`select-attr-${index}`} style={{width: 120}} placeholder='请选择规格' onChange={this.handleSelectAttr}>
{attrOptions}
</Select>
<Select key={`select-attr-value-${index}`} mode={"tags"} style={{width: 260}} placeholder='请选择规格值'
onChange={this.handleSelectAttrValue}>
{attrValueOptions}
</Select>
</div>;
}
}

View File

@ -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 <InputNumber placeholder="请输入" onChange={this.handleChange} />
}
}
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 <SkuInputNumber {...props} />;
}
});
columns.push({
title: '库存',
dataIndex: 'quantity',
render(value, record, index) {
let props = {
record: record,
index: index,
dispatch: dispatch,
dataIndex: 'quantity'
};
return <SkuInputNumber {...props} />;
}
});
return <Table columns={columns} dataSource={skus} rowKey="index" />;
// return <div />;
}
}

View File

@ -44,4 +44,9 @@ export default {
'menu.account.settings': '个人设置', 'menu.account.settings': '个人设置',
'menu.account.trigger': '触发报错', 'menu.account.trigger': '触发报错',
'menu.account.logout': '退出登录', 'menu.account.logout': '退出登录',
// 商品相关
'menu.product': '商品管理',
'menu.product.product-spu-list': '商品管理',
'menu.product.product-spu-add': '商品添加',
'menu.product.product-category-list': '商品分类',
}; };

View File

@ -76,13 +76,13 @@ export default {
}, },
*query({ payload }, { call, put }) { *query({ payload }, { call, put }) {
const response = yield call(queryAdmin, payload); const response = yield call(queryAdmin, payload);
message.info('查询成功!');
const { count, admins } = response.data; const { count, admins } = response.data;
yield put({ yield put({
type: 'querySuccess', type: 'querySuccess',
payload: { payload: {
list: admins, list: admins,
count, count,
pageNo: payload.pageNo + 1
}, },
}); });
}, },

View File

@ -1,14 +1,12 @@
import { message } from 'antd'; import { message } from 'antd';
import { productCategoryTree, productCategoryAdd, productCategoryUpdate, productCategoryUpdateStatus, productCategoryDelete } from '../../services/product'; import { productCategoryTree, productSpuAdd, productCategoryUpdate, productCategoryUpdateStatus, productCategoryDelete } from '../../services/product';
export default { export default {
namespace: 'productSpuAddOrUpdate', namespace: 'productSpuAddOrUpdate',
state: { state: {
list: [], list: [],
attrTree: [{ attrTree: [
}
// { // {
// id: // // id: //
// name: // // name: //
@ -17,6 +15,16 @@ export default {
// name: // // name: //
// }] // }]
// } // }
],
skus: [
// {
// attrs: [{
// id: // 规格值编号
// name: // 规格值名
// }],
// price: // 价格
// quantity: // 数量
// }
] ]
}, },
@ -65,7 +73,7 @@ export default {
*addAttr({ payload }, { call, put }) { *addAttr({ payload }, { call, put }) {
// const { queryParams } = payload; // const { queryParams } = payload;
// const response = yield call(productCategoryTree, queryParams); // const response = yield call(productCategoryTree, queryParams);
message.info('调试:添加规格成功!'); // message.info('调试:添加规格成功!');
yield put({ yield put({
type: 'addAttrSuccess', type: 'addAttrSuccess',
payload: { 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: { reducers: {
addAttrSuccess(state, {payload}) { addAttrSuccess(state, {payload}) {
// debugger; // debugger;
console.log(state.attrTree); // console.log(state.attrTree);
state.attrTree.push(payload.attrAdd); state.attrTree.push(payload.attrAdd);
return { return {
...state ...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 }) { treeSuccess(state, { payload }) {
return { return {
...state, ...state,

View File

@ -1,5 +1,6 @@
import { message } from 'antd'; import { message } from 'antd';
import { productSpuPage, productCategoryAdd, productCategoryUpdate, productCategoryUpdateStatus, productCategoryDelete } from '../../services/product'; import { productSpuPage, productCategoryAdd, productCategoryUpdate, productCategoryUpdateStatus, productCategoryDelete } from '../../services/product';
import {routerRedux} from "dva/router";
export default { export default {
namespace: 'productSpuList', namespace: 'productSpuList',
@ -9,46 +10,50 @@ export default {
}, },
effects: { effects: {
*add({ payload }, { call, put }) { // *add({ payload }, { call, put }) {
const { callback, body } = payload; // const { callback, body } = payload;
const response = yield call(productCategoryAdd, body); // const response = yield call(productCategoryAdd, body);
if (callback) { // if (callback) {
callback(response); // callback(response);
} // }
yield put({ // yield put({
type: 'tree', // type: 'tree',
payload: {}, // payload: {},
}); // });
}, // },
*update({ payload }, { call, put }) { // *update({ payload }, { call, put }) {
const { callback, body } = payload; // const { callback, body } = payload;
const response = yield call(productCategoryUpdate, body); // const response = yield call(productCategoryUpdate, body);
if (callback) { // if (callback) {
callback(response); // callback(response);
} // }
yield put({ // yield put({
type: 'tree', // type: 'tree',
payload: {}, // payload: {},
}); // });
}, // },
*updateStatus({ payload }, { call, put }) { // *updateStatus({ payload }, { call, put }) {
const { callback, body } = payload; // const { callback, body } = payload;
const response = yield call(productCategoryUpdateStatus, body); // const response = yield call(productCategoryUpdateStatus, body);
if (callback) { // if (callback) {
callback(response); // callback(response);
} // }
yield put({ // yield put({
type: 'tree', // type: 'tree',
payload: {}, // payload: {},
}); // });
}, // },
*delete({ payload }, { call, put }) { // *delete({ payload }, { call, put }) {
const response = yield call(productCategoryDelete, payload); // const response = yield call(productCategoryDelete, payload);
message.info('删除成功!'); // message.info('删除成功!');
yield put({ // yield put({
type: 'tree', // type: 'tree',
payload: {}, // payload: {},
}); // });
// },
*redirectToAdd({ payload }, { call, put }) {
// const { callback, body } = payload;
yield put(routerRedux.replace('/product/product-spu-add'));
}, },
*page({ payload }, { call, put }) { *page({ payload }, { call, put }) {
const { queryParams } = payload; const { queryParams } = payload;

View File

@ -2,10 +2,13 @@
import React, { PureComponent, Fragment } from 'react'; import React, { PureComponent, Fragment } from 'react';
import { connect } from 'dva'; 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 PageHeaderWrapper from '@/components/PageHeaderWrapper';
import styles from './AdminList.less'; import styles from './AdminList.less';
import moment from "moment";
import Pagination from "antd/es/pagination";
const FormItem = Form.Item; const FormItem = Form.Item;
const { TreeNode } = Tree; const { TreeNode } = Tree;
@ -31,31 +34,36 @@ const CreateForm = Form.create()(props => {
width: 200, width: 200,
}; };
const title = modalType === 'add' ? '添加一个 Resource' : '更新一个 Resource'; const title = modalType === 'add' ? '新建管理员' : '更新管理员';
const okText = modalType === 'add' ? '添加' : '更新';
return ( return (
<Modal <Modal
destroyOnClose destroyOnClose
title={title} title={title}
visible={modalVisible} visible={modalVisible}
onOk={okHandle} onOk={okHandle}
okText={okText} okText='保存'
onCancel={() => handleModalVisible()} onCancel={() => handleModalVisible()}
> >
<FormItem labelCol={{ span: 5 }} wrapperCol={{ span: 15 }} label=""> <FormItem labelCol={{ span: 5 }} wrapperCol={{ span: 15 }} label="用户名">
{form.getFieldDecorator('username', { {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, initialValue: initValues.username,
})(<Input placeholder="请输入" />)} })(<Input placeholder="请输入" />)}
</FormItem> </FormItem>
<FormItem labelCol={{ span: 5 }} wrapperCol={{ span: 15 }} label="昵称"> <FormItem labelCol={{ span: 5 }} wrapperCol={{ span: 15 }} label="昵称">
{form.getFieldDecorator('nickname', { {form.getFieldDecorator('nickname', {
rules: [{ required: true, message: '请输入昵称!', min: 2 }], rules: [{ required: true, message: '请输入昵称!'},
{max: 10, message: '姓名最大长度为10'}],
initialValue: initValues.nickname, initialValue: initValues.nickname,
})(<Input placeholder="请输入" />)} })(<Input placeholder="请输入" />)}
</FormItem> </FormItem>
<FormItem labelCol={{ span: 5 }} wrapperCol={{ span: 15 }} label="密码"> <FormItem labelCol={{ span: 5 }} wrapperCol={{ span: 15 }} label="密码">
{form.getFieldDecorator('password', { {form.getFieldDecorator('password', {
rules: [{ required: modalType === 'add', message: '请填写密码'}, // 添加时,必须输入密码
{max: 16, min: 6, message: '长度为6-18位'}],
initialValue: initValues.password, initialValue: initValues.password,
})(<Input placeholder="请输入" type="password" />)} })(<Input placeholder="请输入" type="password" />)}
</FormItem> </FormItem>
@ -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 (
<Form onSubmit={this.handleSearch} layout="inline">
<Row gutter={{ md: 8, lg: 24, xl: 48 }}>
<Col md={8} sm={24}>
<FormItem label="昵称">
{getFieldDecorator('name')(<Input placeholder="请输入" />)}
</FormItem>
</Col>
<Col md={8} sm={24}>
<span className={styles.submitButtons}>
<Button type="primary" htmlType="submit">
查询
</Button>
<Button style={{ marginLeft: 8 }} onClick={this.handleFormReset}>
重置
</Button>
</span>
</Col>
</Row>
</Form>
);
}
render() { render() {
const { list, data } = this.props; let that = this;
const { roleList, roleCheckedKeys, roleAssignLoading } = data; const { list, data } = this.props;
const { count, pageNo, pageSize, roleList, roleCheckedKeys, roleAssignLoading } = data;
const { const {
modalVisible, modalVisible,
modalType, modalType,
@ -323,15 +371,9 @@ class ResourceList extends PureComponent {
}; };
const columns = [ const columns = [
{
title: 'id',
dataIndex: 'id',
render: text => <strong>{text}</strong>,
},
{ {
title: '用户名', title: '用户名',
dataIndex: 'username', dataIndex: 'username'
render: text => <a>{text}</a>,
}, },
{ {
title: '昵称', title: '昵称',
@ -341,17 +383,22 @@ class ResourceList extends PureComponent {
title: '状态', title: '状态',
dataIndex: 'status', dataIndex: 'status',
render(val) { render(val) {
return <span>{status[val]}</span>; return <span>{status[val]}</span>; // TODO 芋艿此处要改
}, },
}, },
{
title: '创建时间',
dataIndex: 'createTime',
render: val => <span>{moment(val).format('YYYY-MM-DD HH:mm')}</span>,
},
{ {
title: '操作', title: '操作',
width: 300, width: 360,
render: (text, record) => { render: (text, record) => {
const statusText = record.status === 1 ? '确认禁用' : '取消禁用'; const statusText = record.status === 1 ? '禁用' : '禁用';
return ( return (
<Fragment> <Fragment>
<a onClick={() => this.handleModalVisible(true, 'update', record)}>更新</a> <a onClick={() => this.handleModalVisible(true, 'update', record)}>编辑</a>
<Divider type="vertical" /> <Divider type="vertical" />
<a onClick={() => this.handleRoleAssign(record)}>角色分配</a> <a onClick={() => this.handleRoleAssign(record)}>角色分配</a>
<Divider type="vertical" /> <Divider type="vertical" />
@ -369,16 +416,17 @@ class ResourceList extends PureComponent {
]; ];
return ( return (
<PageHeaderWrapper title="查询表格"> <PageHeaderWrapper>
<Card bordered={false}> <Card bordered={false}>
<div className={styles.tableList}> <div className={styles.tableList}>
<div className={styles.tableListForm}>{that.renderSimpleForm()}</div>
<div className={styles.tableListOperator}> <div className={styles.tableListOperator}>
<Button <Button
icon="plus" icon="plus"
type="primary" type="primary"
onClick={() => this.handleModalVisible(true, 'add', {})} onClick={() => this.handleModalVisible(true, 'add', {})}
> >
新建 新建管理员
</Button> </Button>
</div> </div>
</div> </div>
@ -387,6 +435,12 @@ class ResourceList extends PureComponent {
columns={columns} columns={columns}
dataSource={list} dataSource={list}
rowKey="id" rowKey="id"
pagination={{
current: pageNo,
pageSize: pageSize,
total: count,
onChange: this.onPageChange
}}
/> />
</Card> </Card>
<CreateForm {...parentMethods} modalVisible={modalVisible} /> <CreateForm {...parentMethods} modalVisible={modalVisible} />

View File

@ -13,3 +13,35 @@
.tableDelete { .tableDelete {
color: red; 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;
}
}

View File

@ -1,12 +1,14 @@
/* eslint-disable */ /* eslint-disable */
import React, { PureComponent, Fragment } from 'react'; import React, {PureComponent, Fragment, Component} from 'react';
import { connect } from 'dva'; import { connect } from 'dva';
import moment from 'moment'; import moment from 'moment';
import {Card, Form, Input, Radio, Button, Table, Select} from 'antd'; import {Card, Form, Input, Radio, Button, Table, Select} from 'antd';
import PageHeaderWrapper from '@/components/PageHeaderWrapper'; import PageHeaderWrapper from '@/components/PageHeaderWrapper';
import styles from './ProductSpuAddOrUpdate.less'; import styles from './ProductSpuAddOrUpdate.less';
import ProductAttrSelectFormItem from "../../components/Product/ProductAttrSelectFormItem";
import ProductSkuAddOrUpdateTable from "../../components/Product/ProductSkuAddOrUpdateTable";
const FormItem = Form.Item; const FormItem = Form.Item;
const RadioGroup = Radio.Group; const RadioGroup = Radio.Group;
@ -16,12 +18,15 @@ const Option = Select.Option;
@connect(({ productSpuList, productAttrList, productSpuAddOrUpdate, loading }) => ({ @connect(({ productSpuList, productAttrList, productSpuAddOrUpdate, loading }) => ({
// list: productSpuList.list.spus, // list: productSpuList.list.spus,
// loading: loading.models.productSpuList, // loading: loading.models.productSpuList,
productAttrList,
productSpuAddOrUpdate,
allAttrTree: productAttrList.tree, allAttrTree: productAttrList.tree,
attrTree: productSpuAddOrUpdate.attrTree attrTree: productSpuAddOrUpdate.attrTree,
skus: productSpuAddOrUpdate.skus,
})) }))
@Form.create() @Form.create()
class ProductSpuAddOrUpdate extends PureComponent { class ProductSpuAddOrUpdate extends Component {
state = { state = {
modalVisible: false, modalVisible: false,
modalType: 'add', //add update modalType: 'add', //add update
@ -42,18 +47,18 @@ class ProductSpuAddOrUpdate extends PureComponent {
}); });
} }
handleSubmit = e => { // handleSubmit = e => {
const { dispatch, form } = this.props; // const { dispatch, form } = this.props;
e.preventDefault(); // e.preventDefault();
form.validateFieldsAndScroll((err, values) => { // form.validateFieldsAndScroll((err, values) => {
if (!err) { // if (!err) {
dispatch({ // dispatch({
type: 'form/submitRegularForm', // type: 'form/submitRegularForm',
payload: values, // payload: values,
}); // });
} // }
}); // });
} // }
handleAddAttr = e => { handleAddAttr = e => {
// alert('你猜'); // 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() { render() {
// debugger; // debugger;
const { form, data, attrTree } = this.props; const { form, skus, attrTree, allAttrTree, dispatch } = this.props;
// const that = this;
// 规格明细
const columns = [
{
title: '颜色',
dataIndex: 'price'
},
{
title: '价格',
dataIndex: 'price',
render(val) {
return <span>{status[val]}</span>;
},
},
{
title: '库存',
dataIndex: 'quantity',
}
];
// 添加规格 // 添加规格
// debugger; // debugger;
let attrTreeHTML = []; let attrTreeHTML = [];
if (attrTree && attrTree.length > 0) { if (attrTree && attrTree.length > 0) {
// 已选择的的规格集合
let selectedAttrIds = new Set();
for (let i in attrTree) { for (let i in attrTree) {
let attr = attrTree[i]; let attr = attrTree[i];
attr = <div> selectedAttrIds.add(attr.id);
<Select defaultValue="lucy" style={{ width: 120 }}> }
{ // 创建每个规格下拉框的 HTML
for (let i in attrTree) {
} let attr = attrTree[i];
<Option value="jack">Jack</Option> let itemProps = {
<Option value="lucy">Lucy</Option> attr: attr,
<Option value="disabled" disabled>Disabled</Option> allAttrTree: allAttrTree,
<Option value="Yiminghe">yiminghe</Option> dispatch: dispatch,
</Select> selectedAttrIds: selectedAttrIds,
</div>; index: i // 位置。不然无法正确修改 Model 指定位置的数据
attrTreeHTML.push(attr); };
// debugger; attrTreeHTML.push(<ProductAttrSelectFormItem {...itemProps} />);
} }
} }
// 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(<Option key={`option-attr-${i}-${attr.id}`} value={attr.id}>{attr.name}</Option>);
// }
// // 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(<Option key={`option-attr-value-${i}-${attrValue.id}`} value={attrValue.id + ''}>{attrValue.name}</Option>); // + '' 的原因是,多选必须是字符串
// }
// }
// // 3. 拼装最终,添加到 attrTreeHTML 中
// attr = <div key={`div-attr-${i}`}>
// <Select key={`select-attr-${i}`} style={{ width: 120 }} placeholder='请选择规格' onChange={that.handleSelectAttr}>
// {attrOptions}
// </Select>
// <Select key={`select-attr-value-${i}`} mode={"tags"} style={{ width: 260 }} placeholder='请选择规格值' onChange={that.handleSelectAttrValue}>
// {attrValueOptions}
// </Select>
// </div>;
// attrTreeHTML.push(attr);
// }
// }
// 规格明细
let productSkuProps = {
attrTree: attrTree,
skus: skus,
dispatch: dispatch,
};
// console.log(productSkuProps);
return ( return (
<PageHeaderWrapper title=""> <PageHeaderWrapper title="">
@ -163,13 +289,12 @@ class ProductSpuAddOrUpdate extends PureComponent {
</div> </div>
)} )}
</FormItem> </FormItem>
{/*<FormItem labelCol={{ span: 5 }} wrapperCol={{ span: 15 }} label="规格明细">*/} <FormItem labelCol={{ span: 5 }} wrapperCol={{ span: 15 }} label="规格明细">
{/*{form.getFieldDecorator('visible', {*/} {/*<Table defaultExpandAllRows={true} columns={columns} rowKey="id" />*/}
{/*initialValue: 1, // TODO 修改*/} <ProductSkuAddOrUpdateTable {...productSkuProps} />
{/*})(*/}
{/*<Table defaultExpandAllRows={true} columns={columns} rowKey="id" />*/} <Button type="primary" htmlType="submit" style={{ marginLeft: 8 }} onSubmit={this.handleSubmit}>保存</Button>
{/*)}*/} </FormItem>
{/*</FormItem>*/}
</Form> </Form>
</Card> </Card>
</PageHeaderWrapper> </PageHeaderWrapper>

View File

@ -77,52 +77,13 @@ class ProductSpuList extends PureComponent {
}); });
} }
handleModalVisible = (flag, modalType, initValues) => { redirectToAdd = () => {
this.setState({ const { dispatch } = this.props;
modalVisible: !!flag, dispatch({
initValues: initValues || {}, type: 'productSpuList/redirectToAdd',
modalType: modalType || 'add',
}); });
}; };
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() { render() {
// debugger; // debugger;
const { list, data } = this.props; const { list, data } = this.props;
@ -199,7 +160,7 @@ class ProductSpuList extends PureComponent {
<Button <Button
icon="plus" icon="plus"
type="primary" type="primary"
onClick={() => this.handleModalVisible(true, 'add', {})} onClick={this.redirectToAdd}
> >
发布商品 发布商品
</Button> </Button>

View File

@ -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 // product attr + attr value
export async function productAttrTree(params) { export async function productAttrTree(params) {

View File

@ -1,6 +1,7 @@
package cn.iocoder.mall.admin.application.config; package cn.iocoder.mall.admin.application.config;
import cn.iocoder.common.framework.config.GlobalExceptionHandler; import cn.iocoder.common.framework.config.GlobalExceptionHandler;
import cn.iocoder.mall.admin.sdk.interceptor.AdminAccessLogInterceptor;
import cn.iocoder.mall.admin.sdk.interceptor.AdminSecurityInterceptor; import cn.iocoder.mall.admin.sdk.interceptor.AdminSecurityInterceptor;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@ -18,16 +19,17 @@ public class MVCConfiguration implements WebMvcConfigurer {
@Autowired @Autowired
private AdminSecurityInterceptor adminSecurityInterceptor; private AdminSecurityInterceptor adminSecurityInterceptor;
@Autowired
private AdminAccessLogInterceptor adminAccessLogInterceptor;
// //
@Override @Override
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {
// registry.addInterceptor(securityInterceptor).addPathPatterns("/user/**", "/admin/**"); // 只拦截我们定义的接口 // registry.addInterceptor(securityInterceptor).addPathPatterns("/user/**", "/admin/**"); // 只拦截我们定义的接口
registry.addInterceptor(adminAccessLogInterceptor).addPathPatterns("/admins/**");
registry.addInterceptor(adminSecurityInterceptor).addPathPatterns("/admins/**") registry.addInterceptor(adminSecurityInterceptor).addPathPatterns("/admins/**")
.excludePathPatterns("/admins/passport/login"); // 排除登陆接口 .excludePathPatterns("/admins/passport/login"); // 排除登陆接口
} }
@Override @Override
public void addResourceHandlers(ResourceHandlerRegistry registry) { public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 解决 swagger-ui.html 的访问参考自 https://stackoverflow.com/questions/43545540/swagger-ui-no-mapping-found-for-http-request 解决 // 解决 swagger-ui.html 的访问参考自 https://stackoverflow.com/questions/43545540/swagger-ui-no-mapping-found-for-http-request 解决

View File

@ -42,6 +42,8 @@ public class AdminController {
// =========== 当前管理员相关的资源 API =========== // =========== 当前管理员相关的资源 API ===========
// TODO 功能当前管理员
@SuppressWarnings("Duplicates") @SuppressWarnings("Duplicates")
@GetMapping("/menu_resource_tree") @GetMapping("/menu_resource_tree")
@ApiOperation(value = "获得当前登陆的管理员拥有的菜单权限", notes = "以树结构返回") @ApiOperation(value = "获得当前登陆的管理员拥有的菜单权限", notes = "以树结构返回")
@ -120,7 +122,7 @@ public class AdminController {
public CommonResult<Boolean> update(@RequestParam("id") Integer id, public CommonResult<Boolean> update(@RequestParam("id") Integer id,
@RequestParam("username") String username, @RequestParam("username") String username,
@RequestParam("nickname") String nickname, @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); AdminUpdateDTO adminUpdateDTO = new AdminUpdateDTO().setId(id).setUsername(username).setNickname(nickname).setPassword(password);
return adminService.updateAdmin(AdminSecurityContextHolder.getContext().getAdminId(), adminUpdateDTO); return adminService.updateAdmin(AdminSecurityContextHolder.getContext().getAdminId(), adminUpdateDTO);
} }

View File

@ -3,17 +3,17 @@ package cn.iocoder.mall.admin.application.controller.admins;
import cn.iocoder.common.framework.vo.CommonResult; import cn.iocoder.common.framework.vo.CommonResult;
import cn.iocoder.mall.admin.api.OAuth2Service; import cn.iocoder.mall.admin.api.OAuth2Service;
import cn.iocoder.mall.admin.api.bo.OAuth2AccessTokenBO; 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.convert.PassportConvert;
import cn.iocoder.mall.admin.application.vo.AdminInfoVO;
import cn.iocoder.mall.admin.application.vo.PassportLoginVO; import cn.iocoder.mall.admin.application.vo.PassportLoginVO;
import cn.iocoder.mall.admin.sdk.context.AdminSecurityContextHolder;
import com.alibaba.dubbo.config.annotation.Reference; import com.alibaba.dubbo.config.annotation.Reference;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams; import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation; 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 @RestController
@RequestMapping("admins/passport") @RequestMapping("admins/passport")
@ -35,10 +35,8 @@ public class PassportController {
return PassportConvert.INSTANCE.convert(result); return PassportConvert.INSTANCE.convert(result);
} }
// TODO 艿艿后续继续完善 // TODO 功能 logout
@GetMapping("/info")
public CommonResult<AdminInfoVO> info() { // TODO 功能 refresh_token
return CommonResult.success(AdminConvert.INSTANCE.convert(AdminSecurityContextHolder.getContext()));
}
} }

View File

@ -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<Date> START_TIME = new ThreadLocal<>();
/**
* 管理员编号
*/
private static final ThreadLocal<Integer> 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();
}
}

View File

@ -39,6 +39,13 @@ public class AdminSecurityInterceptor extends HandlerInterceptorAdapter {
// 添加到 AdminSecurityContext // 添加到 AdminSecurityContext
AdminSecurityContext context = new AdminSecurityContext(authentication.getAdminId(), authentication.getRoleIds()); AdminSecurityContext context = new AdminSecurityContext(authentication.getAdminId(), authentication.getRoleIds());
AdminSecurityContextHolder.setContext(context); AdminSecurityContextHolder.setContext(context);
// 同时也记录管理员编号到 AdminAccessLogInterceptor 因为
// AdminAccessLogInterceptor 需要在 AdminSecurityInterceptor 之前执行这样记录的访问日志才健全
// AdminSecurityInterceptor 执行后会移除 AdminSecurityContext 信息这就导致 AdminAccessLogInterceptor 无法获得管理员编号
// 因此这里需要进行记录
if (authentication.getAdminId() != null) {
AdminAccessLogInterceptor.setAdminId(authentication.getAdminId());
}
} else { } else {
String url = request.getRequestURI(); String url = request.getRequestURI();
if (!url.equals("/admin/passport/login")) { // TODO 临时写死非登陆接口必须已经认证身份不允许匿名访问 if (!url.equals("/admin/passport/login")) { // TODO 临时写死非登陆接口必须已经认证身份不允许匿名访问

View File

@ -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<Boolean> addAdminAccessLog(AdminAccessLogAddDTO adminAccessLogAddDTO);
}

View File

@ -9,6 +9,9 @@ import cn.iocoder.mall.admin.api.dto.AdminUpdateDTO;
import java.util.Set; import java.util.Set;
/**
* 管理员 Service 接口
*/
public interface AdminService { public interface AdminService {
CommonResult<AdminPageBO> getAdminPage(AdminPageDTO adminPageDTO); CommonResult<AdminPageBO> getAdminPage(AdminPageDTO adminPageDTO);

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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<Boolean> 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);
}
}

View File

@ -77,7 +77,6 @@ public class OAuth2ServiceImpl implements OAuth2Service {
} }
// 获得管理员拥有的角色 // 获得管理员拥有的角色
List<AdminRoleDO> adminRoleDOs = adminService.getAdminRoles(accessTokenDO.getAdminId()); List<AdminRoleDO> adminRoleDOs = adminService.getAdminRoles(accessTokenDO.getAdminId());
// TODO 芋艿有个 bug 要排除掉已经失效的角色
return CommonResult.success(OAuth2Convert.INSTANCE.convertToAuthentication(accessTokenDO, adminRoleDOs)); return CommonResult.success(OAuth2Convert.INSTANCE.convertToAuthentication(accessTokenDO, adminRoleDOs));
} }

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.iocoder.mall.admin.dao.AdminAccessLogMapper">
<!--<sql id="FIELDS">-->
<!--id, username, nickname, password, status,-->
<!--create_time-->
<!--</sql>-->
<insert id="insert" parameterType="AdminAccessLogDO" useGeneratedKeys="true" keyColumn="id" keyProperty="id">
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}
)
</insert>
</mapper>

View File

@ -1,11 +1,49 @@
package cn.iocoder.common.framework.util; package cn.iocoder.common.framework.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Enumeration;
public class HttpUtil { public class HttpUtil {
private static final Logger logger = LoggerFactory.getLogger(HttpUtil.class);
/**
* Standard Servlet 2.3+ spec request attributes for include URI and paths.
* <p>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.
// * <p>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 <code>request.getCharacterEncoding</code>
* returns <code>null</code>, 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) { public static String obtainAccess(HttpServletRequest request) {
String authorization = request.getHeader("Authorization"); String authorization = request.getHeader("Authorization");
if (!StringUtils.hasText(authorization)) { if (!StringUtils.hasText(authorization)) {
@ -39,4 +77,243 @@ public class HttpUtil {
return request.getRemoteAddr(); 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<String> 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.
* <p/>
* For example, for a request to URL
* <p/>
* <code>http://www.somehost.com/myapp/my/url.jsp</code>,
* <p/>
* for an application deployed to <code>/mayapp</code> (the application's context path), this method would return
* <p/>
* <code>/my/url.jsp</code>.
*
* 该方法是从 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.
* <p>As the value returned by <code>request.getRequestURI()</code> is <i>not</i>
* decoded by the servlet container, this method will decode it.
* <p>The URI that the web container resolves <i>should</i> 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. <strong>WARNING</strong> - 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. <strong>WARNING</strong> - 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.
* <p>As the value returned by <code>request.getContextPath()</code> is <i>not</i>
* 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".
* <p>The default implementation uses <code>URLDecoder.decode(input, enc)</code>.
*
* @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.
* <p>The default implementation checks the request's
* {@link ServletRequest#getCharacterEncoding() character encoding}, and if that
* <code>null</code>, falls back to the {@link #DEFAULT_CHARACTER_ENCODING}.
*
* @param request current HTTP request
* @return the encoding for the request (never <code>null</code>)
* @see javax.servlet.ServletRequest#getCharacterEncoding()
*/
protected static String determineEncoding(HttpServletRequest request) {
String enc = request.getCharacterEncoding();
if (enc == null) {
enc = DEFAULT_CHARACTER_ENCODING;
}
return enc;
}
} }

View File

@ -27,4 +27,8 @@ public class StringUtil {
return array; return array;
} }
public static String substring(String str, int start) {
return org.apache.commons.lang3.StringUtils.substring(str, start);
}
} }

View File

@ -52,7 +52,7 @@ public class AdminsProductSpuController {
@RequestParam("sellPoint") String sellPoint, @RequestParam("sellPoint") String sellPoint,
@RequestParam("description") String description, @RequestParam("description") String description,
@RequestParam("cid") Integer cid, @RequestParam("cid") Integer cid,
@RequestParam("picURLs") List<String> picUrls, @RequestParam("picUrls") List<String> picUrls,
@RequestParam("visible") Boolean visible, @RequestParam("visible") Boolean visible,
@RequestParam("skuStr") String skuStr) { // TODO 芋艿因为考虑不使用 json 接受参数所以这里手动转 @RequestParam("skuStr") String skuStr) { // TODO 芋艿因为考虑不使用 json 接受参数所以这里手动转
// 创建 ProductSpuAddDTO 对象 // 创建 ProductSpuAddDTO 对象

View File

@ -2,6 +2,7 @@ package cn.iocoder.mall.user.application.config;
import cn.iocoder.common.framework.config.GlobalExceptionHandler; import cn.iocoder.common.framework.config.GlobalExceptionHandler;
import cn.iocoder.mall.admin.sdk.interceptor.AdminSecurityInterceptor; 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 cn.iocoder.mall.user.sdk.interceptor.UserSecurityInterceptor;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@ -18,13 +19,18 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
public class MVCConfiguration implements WebMvcConfigurer { public class MVCConfiguration implements WebMvcConfigurer {
@Autowired @Autowired
private UserSecurityInterceptor securityInterceptor; private UserSecurityInterceptor userSecurityInterceptor;
@Autowired
private UserAccessLogInterceptor userAccessLogInterceptor;
@Autowired @Autowired
private AdminSecurityInterceptor adminSecurityInterceptor; private AdminSecurityInterceptor adminSecurityInterceptor;
@Override @Override
public void addInterceptors(InterceptorRegistry registry) { 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/**"); // 只拦截我们定义的接口 registry.addInterceptor(adminSecurityInterceptor).addPathPatterns("/admins/**"); // 只拦截我们定义的接口
} }

View File

@ -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<Date> START_TIME = new ThreadLocal<>();
/**
* 管理员编号
*/
private static final ThreadLocal<Integer> 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();
}
}

View File

@ -40,6 +40,13 @@ public class UserSecurityInterceptor extends HandlerInterceptorAdapter {
// 添加到 SecurityContext // 添加到 SecurityContext
UserSecurityContext context = new UserSecurityContext(authentication.getUserId()); UserSecurityContext context = new UserSecurityContext(authentication.getUserId());
UserSecurityContextHolder.setContext(context); UserSecurityContextHolder.setContext(context);
// 同时也记录管理员编号到 AdminAccessLogInterceptor 因为
// AdminAccessLogInterceptor 需要在 AdminSecurityInterceptor 之前执行这样记录的访问日志才健全
// AdminSecurityInterceptor 执行后会移除 AdminSecurityContext 信息这就导致 AdminAccessLogInterceptor 无法获得管理员编号
// 因此这里需要进行记录
if (authentication.getUserId() != null) {
UserAccessLogInterceptor.setUserId(authentication.getUserId());
}
} }
// 校验是否需要已授权 // 校验是否需要已授权
HandlerMethod method = (HandlerMethod) handler; HandlerMethod method = (HandlerMethod) handler;

View File

@ -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<Boolean> addUserAccessLog(UserAccessLogAddDTO userAccessLogAddDTO);
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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<Boolean> 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);
}
}

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.iocoder.mall.user.dao.UserAccessLogMapper">
<!--<sql id="FIELDS">-->
<!--id, username, nickname, password, status,-->
<!--create_time-->
<!--</sql>-->
<insert id="insert" parameterType="UserAccessLogDO" useGeneratedKeys="true" keyColumn="id" keyProperty="id">
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}
)
</insert>
</mapper>