Repository: moshuying/project-3-crm Branch: main Commit: ae731cc0378a Files: 383 Total size: 13.9 MB Directory structure: gitextract_6d5po4kp/ ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── dependabot.yml │ └── workflows/ │ ├── blank.yml │ └── codeql-analysis.yml ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── back/ │ ├── .gitignore │ ├── LICENSE │ ├── README-zh.md │ ├── README.md │ ├── pom.xml │ ├── resetDB.sh │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── msy/ │ │ │ └── plus/ │ │ │ ├── Application.java │ │ │ ├── aspect/ │ │ │ │ └── ControllerLogAspect.java │ │ │ ├── controller/ │ │ │ │ ├── AccountController.java │ │ │ │ ├── AnalysisController.java │ │ │ │ ├── CustomerFollowUpHistoryController.java │ │ │ │ ├── CustomerHandoverController.java │ │ │ │ ├── CustomerManagerController.java │ │ │ │ ├── DepartmentController.java │ │ │ │ ├── DictionaryContentsController.java │ │ │ │ ├── DictionaryDetailsController.java │ │ │ │ ├── EmployeeController.java │ │ │ │ ├── PermissionController.java │ │ │ │ └── RoleController.java │ │ │ ├── core/ │ │ │ │ ├── cache/ │ │ │ │ │ ├── CacheExpire.java │ │ │ │ │ └── MyRedisCacheManager.java │ │ │ │ ├── config/ │ │ │ │ │ ├── JasyptConfig.java │ │ │ │ │ ├── RedisCacheConfig.java │ │ │ │ │ ├── RedisConfig.java │ │ │ │ │ ├── Swagger3Config.java │ │ │ │ │ ├── ValidatorConfig.java │ │ │ │ │ ├── WebMvcConfig.java │ │ │ │ │ ├── WebSecurityConfig.java │ │ │ │ │ └── YamlPropertySourceFactory.java │ │ │ │ ├── constant/ │ │ │ │ │ └── ProjectConstant.java │ │ │ │ ├── dto/ │ │ │ │ │ └── AbstractConverter.java │ │ │ │ ├── exception/ │ │ │ │ │ ├── ExceptionResolver.java │ │ │ │ │ ├── ResourcesNotFoundException.java │ │ │ │ │ ├── RsaException.java │ │ │ │ │ ├── ServiceException.java │ │ │ │ │ ├── UsernameNotFoundException2.java │ │ │ │ │ └── YamlNotFoundException.java │ │ │ │ ├── jasypt/ │ │ │ │ │ └── MyEncryptablePropertyDetector.java │ │ │ │ ├── jwt/ │ │ │ │ │ ├── JwtConfigurationProperties.java │ │ │ │ │ └── JwtUtil.java │ │ │ │ ├── mapper/ │ │ │ │ │ └── MyMapper.java │ │ │ │ ├── response/ │ │ │ │ │ ├── Result.java │ │ │ │ │ ├── ResultCode.java │ │ │ │ │ └── ResultGenerator.java │ │ │ │ ├── rsa/ │ │ │ │ │ ├── RsaConfigurationProperties.java │ │ │ │ │ └── RsaUtils.java │ │ │ │ ├── service/ │ │ │ │ │ ├── AbstractService.java │ │ │ │ │ └── Service.java │ │ │ │ └── upload/ │ │ │ │ └── UploadConfigurationProperties.java │ │ │ ├── dto/ │ │ │ │ ├── AccountDTO.java │ │ │ │ ├── AccountLoginDTO.java │ │ │ │ ├── AnalysisQuery.java │ │ │ │ ├── CustomerHandoverList.java │ │ │ │ ├── CustomerManagerList.java │ │ │ │ ├── LoginResultDTO.java │ │ │ │ ├── RoleDTO.java │ │ │ │ └── RoleWithPermissionDTO.java │ │ │ ├── entity/ │ │ │ │ ├── AccountDO.java │ │ │ │ ├── AccountWithRoleDO.java │ │ │ │ ├── Analysis.java │ │ │ │ ├── CFUHSearch.java │ │ │ │ ├── CustomerFollowUpHistory.java │ │ │ │ ├── CustomerHandover.java │ │ │ │ ├── CustomerManager.java │ │ │ │ ├── Department.java │ │ │ │ ├── DictionaryContents.java │ │ │ │ ├── DictionaryDetails.java │ │ │ │ ├── Employee.java │ │ │ │ ├── EmployeeDetail.java │ │ │ │ ├── EmployeeWithRoleDO.java │ │ │ │ ├── LoginResultDO.java │ │ │ │ ├── Permission.java │ │ │ │ ├── RoleDO.java │ │ │ │ ├── RolePermissionDO.java │ │ │ │ ├── RoleWithPermissionDO.java │ │ │ │ └── Test.java │ │ │ ├── filter/ │ │ │ │ ├── AuthenticationFilter.java │ │ │ │ ├── CorsFilter.java │ │ │ │ ├── MyAuthenticationEntryPoint.java │ │ │ │ └── RequestWrapper.java │ │ │ ├── mapper/ │ │ │ │ ├── AccountMapper.java │ │ │ │ ├── CustomerFollowUpHistoryMapper.java │ │ │ │ ├── CustomerHandoverMapper.java │ │ │ │ ├── CustomerManagerMapper.java │ │ │ │ ├── DepartmentMapper.java │ │ │ │ ├── DictionaryContentsMapper.java │ │ │ │ ├── DictionaryDetailsMapper.java │ │ │ │ ├── EmployeeMapper.java │ │ │ │ ├── PermissionMapper.java │ │ │ │ └── RoleMapper.java │ │ │ ├── query/ │ │ │ │ └── AccountQuery.java │ │ │ ├── service/ │ │ │ │ ├── AccountService.java │ │ │ │ ├── CustomerFollowUpHistoryService.java │ │ │ │ ├── CustomerHandoverService.java │ │ │ │ ├── CustomerManagerService.java │ │ │ │ ├── DepartmentService.java │ │ │ │ ├── DictionaryContentsService.java │ │ │ │ ├── DictionaryDetailsService.java │ │ │ │ ├── EmployeeService.java │ │ │ │ ├── PermissionService.java │ │ │ │ ├── RoleService.java │ │ │ │ └── impl/ │ │ │ │ ├── AccountServiceImpl.java │ │ │ │ ├── CustomerFollowUpHistoryServiceImpl.java │ │ │ │ ├── CustomerHandoverServiceImpl.java │ │ │ │ ├── CustomerManagerServiceImpl.java │ │ │ │ ├── DepartmentServiceImpl.java │ │ │ │ ├── DictionaryContentsServiceImpl.java │ │ │ │ ├── DictionaryDetailsServiceImpl.java │ │ │ │ ├── EmployeeServiceImpl.java │ │ │ │ ├── PermissionServiceImpl.java │ │ │ │ ├── RoleServiceImpl.java │ │ │ │ └── UserDetailsServiceImpl.java │ │ │ └── util/ │ │ │ ├── AssertUtils.java │ │ │ ├── ContextUtils.java │ │ │ ├── DateUtils.java │ │ │ ├── FileUtils.java │ │ │ ├── IdCardUtils.java │ │ │ ├── IdUtils.java │ │ │ ├── IpUtils.java │ │ │ ├── JsonUtils.java │ │ │ ├── RedisUtils.java │ │ │ └── UrlUtils.java │ │ └── resources/ │ │ ├── META-INF/ │ │ │ ├── spring-devtools.yml │ │ │ └── swagger3.yml │ │ ├── application-dev.yml │ │ ├── application-test.yml │ │ ├── application.yml │ │ ├── banner.txt │ │ ├── mapper/ │ │ │ ├── AccountMapper.xml │ │ │ ├── CustomerFollowUpHistoryMapper.xml │ │ │ ├── CustomerHandoverMapper.xml │ │ │ ├── CustomerManagerMapper.xml │ │ │ ├── DepartmentMapper.xml │ │ │ ├── DictionaryContentsMapper.xml │ │ │ ├── DictionaryDetailsMapper.xml │ │ │ ├── EmployeeMapper.xml │ │ │ ├── PermissionMapper.xml │ │ │ └── RoleMapper.xml │ │ └── rsa/ │ │ ├── private-key.pem │ │ └── public-key.pem │ └── test/ │ ├── java/ │ │ ├── CodeGenerator.java │ │ ├── JasyptStringEncryptor.java │ │ ├── PasswordEncryptor.java │ │ ├── RsaEncryptor.java │ │ └── com/ │ │ └── msy/ │ │ └── plus/ │ │ ├── AccountControllerTest.java │ │ ├── BaseControllerTest.java │ │ ├── WithCustomSecurityContextFactory.java │ │ ├── WithCustomUser.java │ │ └── util/ │ │ └── JsonUtilsTest.java │ ├── resources/ │ │ ├── generator/ │ │ │ └── template/ │ │ │ ├── controller-restful.ftl │ │ │ ├── controller.ftl │ │ │ ├── service-impl.ftl │ │ │ └── service.ftl │ │ └── sql/ │ │ └── dev/ │ │ ├── account.sql │ │ ├── account_role.sql │ │ └── role.sql │ └── rest-test/ │ └── upload.http ├── docs/ │ ├── CRM需求模拟.docx │ ├── JAVA实训方案-CRM(10天)-高级.docx │ ├── crm商业计划书.pptx │ ├── 员工信息模板.xlsx │ └── 项目需求文档.docx ├── front/ │ ├── .github/ │ │ └── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── .gitignore │ ├── LICENSE │ ├── README.en-US.md │ ├── README.md │ ├── babel.config.js │ ├── docs/ │ │ ├── .vuepress/ │ │ │ ├── components/ │ │ │ │ ├── Alert.vue │ │ │ │ ├── Color.vue │ │ │ │ └── ColorList.vue │ │ │ ├── config.js │ │ │ ├── plugins/ │ │ │ │ └── alert/ │ │ │ │ ├── Alert.vue │ │ │ │ ├── alertMixin.js │ │ │ │ ├── clientRootMixin.js │ │ │ │ ├── enhanceApp.js │ │ │ │ └── index.js │ │ │ └── styles/ │ │ │ ├── index.styl │ │ │ └── palette.styl │ │ ├── README.md │ │ ├── advance/ │ │ │ ├── README.md │ │ │ ├── api.md │ │ │ ├── async.md │ │ │ ├── authority.md │ │ │ ├── chart.md │ │ │ ├── error.md │ │ │ ├── guard.md │ │ │ ├── i18n.md │ │ │ ├── interceptors.md │ │ │ ├── login.md │ │ │ ├── skill.md │ │ │ └── theme.md │ │ ├── develop/ │ │ │ ├── README.md │ │ │ ├── layout.md │ │ │ ├── mock.md │ │ │ ├── page.md │ │ │ ├── router.md │ │ │ ├── service.md │ │ │ └── theme.md │ │ ├── other/ │ │ │ ├── README.md │ │ │ ├── community.md │ │ │ └── upgrade.md │ │ └── start/ │ │ ├── README.md │ │ ├── faq.md │ │ └── use.md │ ├── package.json │ ├── public/ │ │ └── index.html │ ├── src/ │ │ ├── App.vue │ │ ├── bootstrap.js │ │ ├── components/ │ │ │ ├── cache/ │ │ │ │ └── AKeepAlive.js │ │ │ ├── card/ │ │ │ │ └── ChartCard.vue │ │ │ ├── chart/ │ │ │ │ ├── Bar.vue │ │ │ │ ├── MiniArea.vue │ │ │ │ ├── MiniBar.vue │ │ │ │ ├── MiniProgress.vue │ │ │ │ ├── Radar.vue │ │ │ │ ├── RankingList.vue │ │ │ │ ├── Trend.vue │ │ │ │ └── index.less │ │ │ ├── checkbox/ │ │ │ │ ├── ColorCheckbox.vue │ │ │ │ ├── ImgCheckbox.vue │ │ │ │ └── index.js │ │ │ ├── exception/ │ │ │ │ ├── ExceptionPage.vue │ │ │ │ └── typeConfig.js │ │ │ ├── form/ │ │ │ │ └── FormRow.vue │ │ │ ├── input/ │ │ │ │ └── IInput.vue │ │ │ ├── menu/ │ │ │ │ ├── Contextmenu.vue │ │ │ │ ├── SideMenu.vue │ │ │ │ ├── index.less │ │ │ │ └── menu.js │ │ │ ├── page/ │ │ │ │ └── header/ │ │ │ │ ├── PageHeader.vue │ │ │ │ └── index.less │ │ │ ├── result/ │ │ │ │ └── Result.vue │ │ │ ├── setting/ │ │ │ │ ├── Setting.vue │ │ │ │ ├── SettingItem.vue │ │ │ │ └── i18n.js │ │ │ ├── table/ │ │ │ │ ├── StandardTable.vue │ │ │ │ ├── advance/ │ │ │ │ │ ├── ActionColumns.vue │ │ │ │ │ ├── ActionSize.vue │ │ │ │ │ ├── AdvanceTable.vue │ │ │ │ │ ├── SearchArea.vue │ │ │ │ │ └── index.js │ │ │ │ └── api/ │ │ │ │ └── ApiTable.vue │ │ │ ├── task/ │ │ │ │ ├── TaskGroup.vue │ │ │ │ └── TaskItem.vue │ │ │ ├── tool/ │ │ │ │ ├── AStepItem.vue │ │ │ │ ├── AvatarList.vue │ │ │ │ ├── DetailList.vue │ │ │ │ ├── Drawer.vue │ │ │ │ ├── FooterToolBar.vue │ │ │ │ ├── HeadInfo.vue │ │ │ │ ├── TagSelect.vue │ │ │ │ └── TagSelectOption.vue │ │ │ └── transition/ │ │ │ └── PageToggleTransition.vue │ │ ├── config/ │ │ │ ├── config.js │ │ │ ├── default/ │ │ │ │ ├── admin.config.js │ │ │ │ ├── animate.config.js │ │ │ │ ├── antd.config.js │ │ │ │ ├── index.js │ │ │ │ └── setting.config.js │ │ │ ├── index.js │ │ │ └── replacer/ │ │ │ ├── index.js │ │ │ └── resolve.config.js │ │ ├── layouts/ │ │ │ ├── AdminLayout.vue │ │ │ ├── BlankView.vue │ │ │ ├── CommonLayout.vue │ │ │ ├── PageLayout.vue │ │ │ ├── PageView.vue │ │ │ ├── footer/ │ │ │ │ └── PageFooter.vue │ │ │ ├── header/ │ │ │ │ ├── AdminHeader.vue │ │ │ │ ├── HeaderAvatar.vue │ │ │ │ ├── HeaderNotice.vue │ │ │ │ ├── HeaderSearch.vue │ │ │ │ └── index.less │ │ │ └── tabs/ │ │ │ ├── TabsHead.vue │ │ │ ├── TabsView.vue │ │ │ ├── i18n.js │ │ │ └── index.js │ │ ├── main.js │ │ ├── mock/ │ │ │ ├── common/ │ │ │ │ ├── activityData.js │ │ │ │ ├── index.js │ │ │ │ └── tableData.js │ │ │ ├── goods/ │ │ │ │ └── index.js │ │ │ ├── index.js │ │ │ ├── user/ │ │ │ │ ├── login.js │ │ │ │ └── routes.js │ │ │ └── workplace/ │ │ │ └── index.js │ │ ├── pages/ │ │ │ ├── analysis/ │ │ │ │ └── index.vue │ │ │ ├── components/ │ │ │ │ ├── Palette.vue │ │ │ │ ├── TaskCard.vue │ │ │ │ └── table/ │ │ │ │ ├── Api.vue │ │ │ │ ├── Table.vue │ │ │ │ └── index.js │ │ │ ├── customer/ │ │ │ │ ├── followHistory.vue │ │ │ │ ├── handoverHistory.vue │ │ │ │ ├── manager.vue │ │ │ │ ├── official.vue │ │ │ │ └── resource.vue │ │ │ ├── dashboard/ │ │ │ │ └── workplace/ │ │ │ │ ├── WorkPlace.vue │ │ │ │ ├── i18n.js │ │ │ │ ├── index.js │ │ │ │ └── index.less │ │ │ ├── department/ │ │ │ │ └── index.vue │ │ │ ├── dictionary/ │ │ │ │ ├── contents.vue │ │ │ │ └── details.vue │ │ │ ├── employee/ │ │ │ │ └── index.vue │ │ │ ├── exception/ │ │ │ │ ├── 403.vue │ │ │ │ ├── 404.vue │ │ │ │ └── 500.vue │ │ │ ├── login/ │ │ │ │ ├── Login.vue │ │ │ │ └── index.js │ │ │ ├── permission/ │ │ │ │ └── index.vue │ │ │ ├── result/ │ │ │ │ ├── Error.vue │ │ │ │ └── Success.vue │ │ │ └── role/ │ │ │ └── index.vue │ │ ├── plugins/ │ │ │ ├── authority-plugin.js │ │ │ ├── i18n-extend.js │ │ │ ├── index.js │ │ │ └── tabs-page-plugin.js │ │ ├── router/ │ │ │ ├── async/ │ │ │ │ ├── config.async.js │ │ │ │ └── router.map.js │ │ │ ├── config.js │ │ │ ├── guards.js │ │ │ ├── i18n.js │ │ │ └── index.js │ │ ├── services/ │ │ │ ├── analysis.js │ │ │ ├── api.js │ │ │ ├── customerFollowUpHistory.js │ │ │ ├── customerHandover.js │ │ │ ├── customerManager.js │ │ │ ├── dataSource.js │ │ │ ├── department.js │ │ │ ├── dictionaryContents.js │ │ │ ├── dictionaryDetails.js │ │ │ ├── employee.js │ │ │ ├── index.js │ │ │ ├── permission.js │ │ │ ├── role.js │ │ │ └── user.js │ │ ├── store/ │ │ │ ├── index.js │ │ │ └── modules/ │ │ │ ├── account.js │ │ │ ├── index.js │ │ │ └── setting.js │ │ ├── theme/ │ │ │ ├── antd/ │ │ │ │ ├── ant-menu.less │ │ │ │ ├── ant-message.less │ │ │ │ ├── ant-table.less │ │ │ │ ├── ant-time-picker.less │ │ │ │ └── index.less │ │ │ ├── default/ │ │ │ │ ├── color.less │ │ │ │ ├── index.less │ │ │ │ ├── nprogress.less │ │ │ │ └── style.less │ │ │ ├── index.less │ │ │ └── theme.less │ │ └── utils/ │ │ ├── Objects.js │ │ ├── authority-utils.js │ │ ├── axios-interceptors.js │ │ ├── colors.js │ │ ├── formatter.js │ │ ├── i18n.js │ │ ├── request.js │ │ ├── routerUtil.js │ │ ├── theme-color-replacer-extend.js │ │ ├── themeUtil.js │ │ ├── util.js │ │ └── validators.js │ └── vue.config.js └── mysql/ ├── dev.sql └── prod.sql ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: moshuying # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] - Browser [e.g. chrome, safari] - Version [e.g. 22] **Smartphone (please complete the following information):** - Device: [e.g. iPhone6] - OS: [e.g. iOS8.1] - Browser [e.g. stock browser, safari] - Version [e.g. 22] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/dependabot.yml ================================================ # To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "daily" ================================================ FILE: .github/workflows/blank.yml ================================================ # This is a basic workflow to help you get started with Actions name: CI # Controls when the action will run. on: # Triggers the workflow on push or pull request events but only for the main branch push: branches: [ main ] pull_request: branches: [ main ] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: # This workflow contains a single job called "build" build: # The type of runner that the job will run on runs-on: ubuntu-latest # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v2 # Runs a single command using the runners shell - name: Run a one-line script run: echo Hello, world! # Runs a set of commands using the runners shell - name: Run a multi-line script run: | echo Add other actions to build, echo test, and deploy your project. ================================================ FILE: .github/workflows/codeql-analysis.yml ================================================ # For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL" on: push: branches: [ main ] pull_request: # The branches below must be a subset of the branches above branches: [ main ] schedule: - cron: '45 1 * * 2' jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'java', 'javascript' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] # Learn more: # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed steps: - name: Checkout repository uses: actions/checkout@v2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v1 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v1 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines # and modify them (or add more) to build your code if your project # uses a compiled language #- run: | # make bootstrap # make release - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v1 ================================================ FILE: .gitignore ================================================ # Compiled class file *.class # Log file *.log # BlueJ files *.ctxt # Mobile Tools for Java (J2ME) .mtj.tmp/ # Package Files # *.jar *.war *.nar *.ear *.zip *.tar.gz *.rar # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* idea ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - "14" install: - cd ./front - npm i script: - npm run build notifications: email: - 1460083332@qq.com cache: directories: - node_modules #缓存依赖 #after_script前5句是把部署分支的.git文件夹保护起来,用于保留历史部署的commit日志,否则部署分支永远只有一条commit记录。 #命令里面的变量都是在Travis CI里配置过的。 # after_script: # - git clone https://${GH_REF} .temp # - cd .temp # - git checkout gh-pages # - cd ../ # - mv .temp/.git dist # - cd dist # - git config user.name "${U_NAME}" # - git config user.email "${U_EMAIL}" # - git add . # - git commit -m ":construction_worker:- Build & Deploy by Travis CI" # - git push --force --quiet "https://${Travis_Token}@${GH_REF}" gh-pages:${D_BRANCH} # E: Build LifeCycle # 只有指定的分支提交时才会运行脚本 # branches: # only: # - master ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at . All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ================================================ FILE: CONTRIBUTING.md ================================================ 感谢贡献者们 墨抒颖 MoShuYing 刘九江 LiuJiuJiang ================================================ FILE: LICENSE ================================================ GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Remote Network Interaction; Use with the GNU General Public License. Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) <2021> <刘九江> This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . ================================================ FILE: README.md ================================================
# project-3 CRM 客户资源管理系统 [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) [![Join the chat at https://gitter.im/墨抒颖/project-3-crm](https://badges.gitter.im/墨抒颖/project-3-crm.svg)](https://gitter.im/墨抒颖/project-3-crm?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ![GitHub language count](https://img.shields.io/github/languages/count/moshuying/project-3-crm) ![GitHub search hit counter](https://img.shields.io/github/search/moshuying/project-3-crm/1) ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/moshuying/project-3-crm) ![GitHub repo size](https://img.shields.io/github/repo-size/moshuying/project-3-crm) ![GitHub closed issues](https://img.shields.io/github/issues-closed/moshuying/project-3-crm) ![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed/moshuying/project-3-crm) ![GitHub](https://img.shields.io/github/license/moshuying/project-3-crm) ### 国内用户请访问[同步仓库](https://gitee.com/moshuying/project-3-crm) # 简述 [sql文件包含在/mysql文件夹内](https://github.com/moshuying/project-3-crm/blob/main/mysql) 在线演示(向下翻页就有)
- 前端使用 [vue-antd-admin](https://github.com/iczer/vue-antd-admin) - [项目文档地址](https://iczer.gitee.io/vue-antd-admin-docs/advance/authority.html#%E9%A1%B5%E9%9D%A2%E6%9D%83%E9%99%90) - 后台使用 [spring-boot-api-seedling](https://github.com/Zoctan/spring-boot-api-seedling)

把项目拷贝下来后,导入`mysql/dev.sql`到`crm`数据库

数据库配置的在`back/src/main/resources/application-dev.yml`下,默认账户密码都是`root`。使用`crm`数据库

![image](https://user-images.githubusercontent.com/37231523/157181254-38b38973-522e-4fdb-803f-3e374caca5f4.png) 系统包括:系统设置、客户管理、营销管理、服务管理、合同管理和统计分析六个功能模块。可满足管理人员日常对客户的资源维护、销售数据分析、潜在和有价值客户分析等需求。 甲方需求文档和演讲ppt位于/docs目录下。较为详细的描述了甲方的功能需求。 - [腾讯文档在线查看甲方需求](https://docs.qq.com/doc/DR0JVbFpmdXNEU1NM) - [ppt商业计划书在线查看](https://docs.qq.com/slide/DR2dIaXB1b3hVZkdw) - [商业计划书参考](https://max.book118.com/html/2017/0508/105355794.shtm) - [sourceforge](https://sourceforge.net/projects/project-3-crm/) 系统经过github工作流,travis集成测试。尽可能多的测试了系统中的功能。 客户关系管理系统用于管理与客户相关的信息与活动,包括企业与顾客间在销售、营销和服务上的交互。从而提升其管理方式,向客户提供创新式的个性化的客户交互和服务。CRM不仅仅是一个软件,它还是方法论、软件和IT能力综合,是一种商业策略。其最终目标是吸引新客户、保留老客户以及将已有客户转为忠实客户。为企业一系列的客户关系管理解决方案。 # contributors [![](https://opencollective.com/project-3-crm/contributors.svg?width=890)](https://github.com/moshuying/project-3-crm/graphs/contributors) 部分页面截图 ![](/images/Snipaste_2021-05-24_17-26-55.png) ![](/images/Snipaste_2021-05-24_17-27-16.png) ![](/images/Snipaste_2021-05-24_17-27-48.png) ![](/images/Snipaste_2021-05-24_17-28-09.png) ![](/images/Snipaste_2021-05-24_17-28-20.png) ![](/images/Snipaste_2021-05-24_17-28-29.png) ![](/images/Snipaste_2021-05-24_17-28-40.png) ![](/images/Snipaste_2021-05-24_17-28-46.png) ![](/images/Snipaste_2021-05-24_17-28-54.png) ![](/images/Snipaste_2021-05-24_17-29-11.png) ![](/images/Snipaste_2021-05-24_17-29-16.png) ![](/images/Snipaste_2021-05-24_17-29-24.png) ![](/images/Snipaste_2021-05-24_17-29-29.png) ![](/images/Snipaste_2021-05-24_17-29-37.png) ![](/images/Snipaste_2021-05-24_17-29-48.png) ![](/images/Snipaste_2021-05-24_17-29-55.png) ![](/images/Snipaste_2021-05-24_17-30-06.png) ![](/images/Snipaste_2021-05-24_17-30-18.png) ![](/images/Snipaste_2021-05-24_17-30-39.png) ![](/images/Snipaste_2021-05-24_17-30-49.png) ![](/images/Snipaste_2021-05-24_17-30-56.png) ![](/images/Snipaste_2021-05-24_17-31-03.png) ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Supported Versions Use this section to tell people about which versions of your project are currently being supported with security updates. | Version | Supported | | ------- | ------------------ | | 5.1.x | :white_check_mark: | | 5.0.x | :x: | | 4.0.x | :white_check_mark: | | < 4.0 | :x: | ## Reporting a Vulnerability Use this section to tell people how to report a vulnerability. Tell them where to go, how often they can expect to get an update on a reported vulnerability, what to expect if the vulnerability is accepted or declined, etc. ================================================ FILE: back/.gitignore ================================================ # Compiled class file *.class # Log file *.log # BlueJ files *.ctxt # Mobile Tools for Java (J2ME) .mtj.tmp/ # Package Files # *.jar *.war *.ear *.zip *.tar.gz *.rar # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* .idea/ target/ *.iml application-prod.yml ================================================ FILE: back/LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: back/README-zh.md ================================================ # Spring Boot API Seedling ![stars](https://img.shields.io/github/stars/Zoctan/spring-boot-api-seedling.svg?style=flat-square&label=Stars) ![license](https://img.shields.io/github/license/Zoctan/spring-boot-api-seedling.svg?style=flat-square) [English](./README.md) | 简体中文 ## 简介 本项目修改自:[spring-boot-api-project-seed](https://github.com/lihengming/spring-boot-api-project-seed) 原项目本身很简洁,已经能满足很多基本需求,在此感谢种子作者。 我根据需求继续添加了一些小功能,比如 API 的签名认证、调用文档、一些小工具等,所以就有了该 Seedling 项目。 添加的内容包括: - Spring Cache:缓存 - Redis:缓存中间件 - Swagger3:API 文档展示 - Spring Security + JWT:对调用方签名认证 - Jasypt:加密配置 - 其他略 代码规范参考阿里巴巴 Java 开发手册,安装 Alibaba Java Coding Guidelines 插件。 风格规范使用 Google,安装 google-java-format 插件。 注解工具:Lombok,安装同名 Idea 插件。 ## 版本 | 依赖 | 版本 | |:-----------:|--------:| | Java | 1.8 | | SpringBoot | 2.3.5 | ## 快速开始 \# 克隆项目 git clone https://github.com/Zoctan/spring-boot-api-seedling.git \# 配置代码生成器 对 test/java 包内的代码生成器 CodeGenerator 进行配置 导入 test/resources/sql 目录下的开发环境 dev 的数据库文件 *.sql \# 根据表名生成代码 输入表名,运行 CodeGenerator.main() 方法,生成基础代码(观看[种子项目的快速演示视频](http://v.youku.com/v_show/id_XMjg1NjYwNDgxNg==.html?spm=a2h3j.8428770.3416059.1)) \# last 对开发环境配置文件 application-dev.properties 进行配置,启动项目,Have Fun Too:) ## 技术选型&文档 1. Spring Boot([种子项目作者的学习&使用指南](https://www.jianshu.com/p/1a9fd8936bd8) | [基础教程](http://blog.didispace.com/Spring-Boot%E5%9F%BA%E7%A1%80%E6%95%99%E7%A8%8B/)) 2. MyBatis([官方中文文档](http://www.mybatis.org/mybatis-3/zh/index.html)) 3. MyBatis通用Mapper插件([官方中文文档](https://mapperhelper.github.io/docs/)) 4. MyBatis PageHelper分页插件([官方中文文档](https://pagehelper.github.io/)) 5. Druid Spring Boot Starter([官方中文文档](https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter/)) 6. FastJson([官方中文文档](https://github.com/alibaba/fastjson/wiki/Quick-Start-CN) | [W3CSchool使用指南](https://www.w3cschool.cn/fastjson/fastjson-quickstart.html)) ## 相关项目 - [前端 Vue + 后端 Spring Boot 完全分离的用户角色管理模板](https://github.com/Zoctan/spring-boot-vue-admin) ## 更新记录 2020-11-09 更新 Swagger2 至 Swagger3,更新其他依赖版本。 2019-08-13 更换 Tomcat 容器为 Jetty,修复 RSA 密钥文件无法读取问题,添加文件上传控制器,更新其他依赖版本。 2018-11-29 配置改为 yml ,完善单元测试,更新其他依赖版本。 2018-07-21 增加 Jasypt 自定义配置和配置密码加密,Tomcat 打包,修改 RSA 工具和添加相应配置。 2018-07-15 增加 DTO 层,避免 DO 层被污染。 2018-07-11 添加了可自定义缓存过期时间的注解,修改了数据表 user 为 account。 ================================================ FILE: back/README.md ================================================ # Spring Boot API Seedling ![stars](https://img.shields.io/github/stars/Zoctan/spring-boot-api-seedling.svg?style=flat-square&label=Stars) ![license](https://img.shields.io/github/license/Zoctan/spring-boot-api-seedling.svg?style=flat-square) English | [简体中文](./README-zh.md) ## Introduction Modified from: [spring-boot-api-project-seed](https://github.com/lihengming/spring-boot-api-project-seed) The original project is very well and has been able to meet many basic needs. Thanks the seed author! Seedling project: I continued to add some small functions according to my needs, such as API signature authentication, API documents, some tools, etc. The added content includes: - Spring Cache: To cache - Redis: Cache middleware - Swagger3:API Doc - Spring Security + JWT:Sign the caller authentication - Jasypt:Encryption configuration - etc. The code specification refers to the《Alibaba Java Development》 and install the Alibaba Java Coding Guidelines plugin. The style specification refers to Google and install google-java-format plugin. Annotation tool: Lombok, install the Idea plugin of the same name. ## Version | Dependencies | Version | |:------------:|--------:| | Java | 1.8 | | SpringBoot | 2.3.5 | ## Start \# Clone project git clone https://github.com/Zoctan/spring-boot-api-seedling.git \# Configure code generator configure package test/java/.../CodeGenerator, import directory test/resources/sql/dev/*.sql file \# Generate code from database schema input table name, run CodeGenerator.main() method to generate basic code (watch [demo video](http://v.youku.com/v_show/id_XMjg1NjYwNDgxNg==.html?spm=a2h3j.8428770.3416059.1)) \# Last configure the development environment configuration file application-dev.properties and start the project. Have Fun Too:) ## Related project - [前端 Vue + 后端 Spring Boot 完全分离的用户角色管理模板](https://github.com/Zoctan/spring-boot-vue-admin) ## Update log 2020-11-09 Update Swagger2 to Swagger3, update other dependencies version. 2019-08-13 Modify Tomcat to Jetty, read RSA file error have been fixed, add file upload controller, update dependencies version. 2018-11-29 Modify setting file format to yml, improve unit testing, update dependencies version. 2018-07-21 Add Jasypt custom setting and password encryption, add Tomcat pack, modify RSA tool. 2018-07-15 Add DTO to prevent DO pollution. 2018-07-11 Add annotation for customizable cache expiration time, modify the data table user to account. ================================================ FILE: back/pom.xml ================================================ 4.0.0 com.github.zoctan spring-boot-api-seeding 1.1 war org.springframework.boot spring-boot-starter-parent 2.3.5.RELEASE 1.8 UTF-8 UTF-8 5.3.5.RELEASE 3.0.0 0.9.1 3.3.0 1.9.4 1.15 3.11 32.0.0-jre 2.1.3 1.3.7 4.1.5 2.1.5 1.3.0 1.2.83 1.2.2 3.0.3 2.3.30 1.18.16 6.1.6.Final io.springfox springfox-boot-starter ${swagger3.version} org.springframework.boot spring-boot-starter-security io.jsonwebtoken jjwt ${jjwt.version} mysql mysql-connector-java org.springframework.boot spring-boot-starter-aop org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-tomcat org.springframework.boot spring-boot-starter-jetty org.springframework.security spring-security-test ${spring-security-test.version} test org.springframework.boot spring-boot-devtools true org.springframework.boot spring-boot-starter-cache org.springframework.boot spring-boot-starter-data-redis io.lettuce lettuce-core redis.clients jedis ${jedis.version} commons-beanutils commons-beanutils ${commons-beanutils.version} commons-codec commons-codec ${commons-codec.version} org.apache.commons commons-lang3 ${commons-lang3.version} com.google.guava guava ${guava.version} com.alibaba fastjson ${fastjson.version} com.alibaba druid-spring-boot-starter ${druid.version} com.github.ulisesbocchio jasypt-spring-boot-starter ${jasypt.version} com.github.pagehelper pagehelper-spring-boot-starter ${pagehelper.version} org.mybatis.spring.boot mybatis-spring-boot-starter ${mybatis.version} tk.mybatis mapper ${mapper.version} tk.mybatis mapper-spring-boot-starter ${mapper-starter.version} org.freemarker freemarker ${freemarker.version} org.mybatis.generator mybatis-generator-core ${mybatis-generator.version} org.projectlombok lombok ${lombok.version} provided org.hibernate.validator hibernate-validator ${hibernate.version} ${project.artifactId} org.springframework.boot spring-boot-maven-plugin repackage com.msy.plus.Application maven-compiler-plugin ${java.version} ${java.version} aliyun-snapshots https://maven.aliyun.com/repository/snapshots aliyun-repo https://maven.aliyun.com/repository/central aliyun-plugin https://maven.aliyun.com/repository/central ================================================ FILE: back/resetDB.sh ================================================ #!/bin/bash db="seedling_dev" while IFS= read -r -d '' sql; do echo "$sql"" -> "$db mysql -uroot -proot $db <"$sql" done < <(find src/test/resources/sql/dev/ -name '*.sql' -print0) echo "finished" echo "import $db done" ================================================ FILE: back/src/main/java/com/msy/plus/Application.java ================================================ package com.msy.plus; import com.ulisesbocchio.jasyptspringboot.annotation.EnableEncryptableProperties; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.web.servlet.ServletComponentScan; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; import org.springframework.cache.annotation.EnableCaching; import org.springframework.transaction.annotation.EnableTransactionManagement; import tk.mybatis.spring.annotation.MapperScan; import static com.msy.plus.core.constant.ProjectConstant.FILTER_PACKAGE; import static com.msy.plus.core.constant.ProjectConstant.MAPPER_PACKAGE; /** * 主程序 * * @author MoShuying * @date 2018/05/27 */ @EnableCaching @SpringBootApplication @EnableEncryptableProperties @EnableTransactionManagement @MapperScan(basePackages = MAPPER_PACKAGE) @ServletComponentScan(basePackages = FILTER_PACKAGE) public class Application extends SpringBootServletInitializer { public static void main(final String[] args) { SpringApplication.run(Application.class, args); } /** 容器启动配置 */ @Override protected SpringApplicationBuilder configure(final SpringApplicationBuilder builder) { return builder.sources(Application.class); } } ================================================ FILE: back/src/main/java/com/msy/plus/aspect/ControllerLogAspect.java ================================================ package com.msy.plus.aspect; import com.msy.plus.util.IpUtils; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.Optional; import static com.msy.plus.core.constant.ProjectConstant.CONTROLLER_PACKAGE; /** * Controller log aspect * * @author MoShuying * @date 2018/07/13 */ @Aspect @Slf4j @Component public class ControllerLogAspect { private LocalDateTime startTime; @Pointcut("execution(* " + CONTROLLER_PACKAGE + "..*.*(..))") public void controllers() {} /** * before controller handling, log something * * @param joinPoint controller join point */ @Before("controllers()") public void doBefore(final JoinPoint joinPoint) { log.debug("==========================================================="); log.debug("================ Controller Log Start ==================="); log.debug("==========================================================="); this.startTime = LocalDateTime.now(); final ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (Optional.ofNullable(attributes).isPresent()) { final HttpServletRequest request = attributes.getRequest(); log.debug("==> Request: [{}]{}", request.getMethod(), request.getRequestURL()); log.debug("==> From IP: {}", IpUtils.getIpAddress()); } log.debug( "==> Method: {}", joinPoint.getSignature().getDeclaringTypeName() + "#" + joinPoint.getSignature().getName()); log.debug("==> Args: {}", Arrays.toString(joinPoint.getArgs())); } /** * after controller handling, return result * * @param result origin result */ @AfterReturning(pointcut = "controllers()", returning = "result") public void doAfterReturning(final Object result) { // 处理请求的时间差 final long difference = ChronoUnit.MILLIS.between(this.startTime, LocalDateTime.now()); log.debug("==> Spend: {}s", difference / 1000.0); log.debug("==> Return: {}", result); log.debug("================ Controller Log End ====================="); } /** * log when throwing error * * @param e error */ @AfterThrowing(pointcut = "controllers()", throwing = "e") public static void doAfterThrowing(final Throwable e) { log.debug("==> Exception: {}", e.toString()); e.printStackTrace(); log.debug("================ Controller Log End ====================="); } } ================================================ FILE: back/src/main/java/com/msy/plus/controller/AccountController.java ================================================ package com.msy.plus.controller; import com.msy.plus.core.jwt.JwtUtil; import com.msy.plus.core.response.Result; import com.msy.plus.core.response.ResultGenerator; import com.msy.plus.dto.AccountDTO; import com.msy.plus.dto.AccountLoginDTO; import com.msy.plus.dto.LoginResultDTO; import com.msy.plus.service.AccountService; import com.msy.plus.service.impl.UserDetailsServiceImpl; import io.swagger.annotations.Api; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import lombok.extern.slf4j.Slf4j; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.validation.BindingResult; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.validation.Valid; import java.util.*; /** * @author MoShuying * @date 2018/07/15 */ @Slf4j @Api(tags={"账户操作接口(登录)"}) @Validated @RestController @RequestMapping("/account") public class AccountController { @Resource private AccountService accountService; @Resource private UserDetailsServiceImpl userDetailsService; @Resource private JwtUtil jwtUtil; @Operation(summary = "账户注册", description = "注册账户,签发token") @ApiResponses({ @ApiResponse(responseCode = "200", description = "OK"), @ApiResponse(responseCode = "2004", description = "账户名重复") }) @PostMapping public Result register( @Parameter(required = true) @RequestBody @Valid final AccountDTO accountDTO, final BindingResult bindingResult) { // 账户持久化 this.accountService.save(accountDTO); // 签发 token final UserDetails userDetails = this.userDetailsService.loadUserByUsername(accountDTO.getName()); final String token = this.jwtUtil.sign( accountDTO.getName(), userDetails.getAuthorities(), accountService.getByNameWithRole(userDetails.getUsername()).getId()); return ResultGenerator.genOkResult(token); } @Operation(summary = "账户登录", description = "账户登录,签发token") @ApiResponses({ @ApiResponse(responseCode = "200", description = "OK"), @ApiResponse(responseCode = "1000", description = "密码错误") }) @PostMapping("/token") public Result login( @Parameter(required = true) @RequestBody @Valid final AccountLoginDTO accountLoginDTO, final BindingResult bindingResult) { // {"name":"admin","password":"admin"} final String name = accountLoginDTO.getName(); final String password = accountLoginDTO.getPassword(); // 验证账户 final UserDetails userDetails = this.userDetailsService.loadUserByUsername(name); if (!this.accountService.verifyPassword(password, userDetails.getPassword())) { return ResultGenerator.genFailedResult("密码错误"); } // 更新登录时间 this.accountService.updateLoginTimeByName(name); final String token = this.jwtUtil.sign(name, userDetails.getAuthorities(),accountService.getByNameWithRole(name).getId()); // 返回Ant Design Admin提供的登录返回格式 LoginResultDTO loginResultDTO = new LoginResultDTO(); // 设置过期时间,和application-*.yml文件中的过期时间设定一致 final long expireTime = this.jwtUtil.getJwtProperties().getExpireTime().toMillis(); loginResultDTO.setExpireAt(new Date(new Date().getTime()+expireTime)); loginResultDTO.setToken(token); loginResultDTO.setUserName(name); Map roles = new HashMap(); roles.put("id",name); roles.put("operation",new String[]{"add","edit","delete"}); loginResultDTO.getRoles().add(roles); loginResultDTO.setMessage("欢迎回来 "+name); return ResultGenerator.genOkResult(loginResultDTO); } @Operation(summary = "账户注销", description = "账户注销,使token失效") @ApiResponses({@ApiResponse(responseCode = "200", description = "OK")}) @DeleteMapping("/token") public Result logout(@RequestHeader Map headers) { String header = jwtUtil.getJwtProperties().getHeader(); jwtUtil.invalidRedisToken(jwtUtil.getName(headers.get(header)).get()); return ResultGenerator.genOkResult(); } @PreAuthorize("#accountDTO.name == authentication.name or hasAuthority('ADMIN')") @Operation(summary = "更新账户", description = "更新账户信息") @ApiResponses({@ApiResponse(responseCode = "200", description = "OK")}) @PatchMapping public Result update(@Parameter(required = true) @RequestBody final AccountDTO accountDTO) { this.accountService.updateByName(accountDTO); return ResultGenerator.genOkResult(); } @PreAuthorize("hasAuthority('ADMIN')" + "or hasAuthority('主席')"+ "or hasAuthority('高级主席')"+ "or hasAuthority('副主席')"+ "or hasAuthority('总裁')") @Operation(summary = "删除账户", description = "删除账户信息") @ApiResponses({@ApiResponse(responseCode = "200", description = "OK")}) @Parameter( name = "id", description = "账户Id", required = true, in = ParameterIn.QUERY, example = "1") @DeleteMapping("/{id}") public Result delete(@PathVariable final Long id) { this.accountService.deleteById(id); return ResultGenerator.genOkResult(); } // // @Operation(summary = "获取单个账户", description = "获取单个账户信息") // @ApiResponses({@ApiResponse(responseCode = "200", description = "OK")}) // @Parameter( // name = "id", // description = "账户Id", // required = true, // in = ParameterIn.PATH, // example = "1") // @GetMapping("/{id}") // public Result detail(@PathVariable final Long id) { // final AccountWithRoleDO account = this.accountService.getByIdWithRole(id); // return ResultGenerator.genOkResult(account); // } // // @Operation(summary = "获取账户列表", description = "获取多个账户信息") // @ApiResponses({@ApiResponse(responseCode = "200", description = "OK")}) // @Parameters({ // @Parameter(name = "page", description = "页号", in = ParameterIn.QUERY, example = "1"), // @Parameter(name = "size", description = "页大小", in = ParameterIn.QUERY, example = "10") // }) // @Cacheable(value = "account.list", unless = "#result == null or #result.code != 200") // @CacheExpire(expire = 60) // @GetMapping // public Result list( // @RequestParam(defaultValue = "0") final Integer page, // @RequestParam(defaultValue = "0") final Integer size) { // AccountController.log.debug("==> No cache, find database"); // PageHelper.startPage(page, size); // final List list = this.accountService.listAll(); // final PageInfo pageInfo = PageInfo.of(list); // // 不显示 password 字段 // final PageInfo objectPageInfo = JsonUtils.deleteFields(pageInfo, PageInfo.class, "password"); // return ResultGenerator.genOkResult(objectPageInfo); // } } ================================================ FILE: back/src/main/java/com/msy/plus/controller/AnalysisController.java ================================================ package com.msy.plus.controller; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import com.msy.plus.core.jwt.JwtUtil; import com.msy.plus.core.response.Result; import com.msy.plus.core.response.ResultGenerator; import com.msy.plus.dto.AnalysisQuery; import com.msy.plus.entity.*; import com.msy.plus.service.CustomerManagerService; import com.msy.plus.service.EmployeeService; import com.msy.plus.service.RoleService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.annotations.Api; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import java.util.List; import java.util.Map; /** * @author MoShuYing * @date 2021/05/15 */ @PreAuthorize( "hasAuthority('ADMIN')"+ "or hasAuthority('董事长')"+ "or hasAuthority('主席')"+ "or hasAuthority('高级主席')"+ "or hasAuthority('副主席')"+ "or hasAuthority('总裁')"+ "or hasAuthority('会长')"+ "or hasAuthority('高级总裁')"+ "or hasAuthority('高级副总裁')"+ "or hasAuthority('副总裁')"+ "or hasAuthority('总经理')"+ "or hasAuthority('副总经理')"+ "or hasAuthority('总监')"+ "or hasAuthority('经理')"+ "or hasAuthority('高级经理')"+ "or hasAuthority('副经理')"+ "or hasAuthority('主任')"+ "or hasAuthority('高级主任')"+ "or hasAuthority('副主任')"+ "or hasAuthority('组长')"+ "or hasAuthority('副组长')"+ "or hasAuthority('普通员工')"+ "or hasAuthority('人事专员')"+ "or hasAuthority('市场专员')"+ "or hasAuthority('市场主管')"+ "or hasAuthority('销售主管')" ) @Api(tags={"统计分析接口"}) @RestController @RequestMapping("/analysis") public class AnalysisController { @Resource CustomerManagerService customerManagerService; @Resource EmployeeService employeeService; @Resource RoleService roleService; @Resource private JwtUtil jwtUtil; @Operation(description = "统计分析") @PostMapping public Result listAndSearch(@RequestBody AnalysisQuery analysisQuery,@RequestHeader Map headers) { String header = jwtUtil.getJwtProperties().getHeader(); String id= jwtUtil.getId(headers.get(header)).get(); List roleIds = employeeService.getDetailById(Integer.valueOf(id).longValue()).getRoleIds(); for(Long roleId:roleIds){ RoleWithPermissionDO roleWithPermissionDO = roleService.getDetailById(roleId); if(roleWithPermissionDO==null) { continue; } String roleName = roleWithPermissionDO.getName(); if(roleName==null || roleName.isEmpty()){ continue; } if(roleName.equals("董事长")){ PageHelper.startPage(analysisQuery.getPage(),analysisQuery.getSize()); PageInfo pageInfo = PageInfo.of(customerManagerService.queryAnalysis(analysisQuery)); return ResultGenerator.genOkResult(pageInfo); } } // 除了董事长 其他人都只能查看自己的 analysisQuery.setName(jwtUtil.getName(headers.get(header)).get()); PageHelper.startPage(analysisQuery.getPage(),analysisQuery.getSize()); PageInfo pageInfo = PageInfo.of(customerManagerService.queryAnalysis(analysisQuery)); return ResultGenerator.genOkResult(pageInfo); } } ================================================ FILE: back/src/main/java/com/msy/plus/controller/CustomerFollowUpHistoryController.java ================================================ package com.msy.plus.controller; import com.msy.plus.core.jwt.JwtUtil; import com.msy.plus.core.response.Result; import com.msy.plus.core.response.ResultGenerator; import com.msy.plus.entity.CFUHSearch; import com.msy.plus.entity.CustomerFollowUpHistory; import com.msy.plus.service.CustomerFollowUpHistoryService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.annotations.Api; import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiImplicitParams; import io.swagger.annotations.ApiOperation; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.security.access.prepost.PreAuthorize; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import java.util.Date; import java.util.List; import java.util.Map; /** * @author MoShuYing * @date 2021/05/21 */ @PreAuthorize("hasAuthority('ADMIN')") @Api(tags={"客户跟进记录接口"}) @RestController @RequestMapping("/customer/follow/up/history") public class CustomerFollowUpHistoryController { @Resource private CustomerFollowUpHistoryService customerFollowUpHistoryService; @Resource private JwtUtil jwtUtil; @Operation(description = "客户跟进记录添加") @PostMapping public Result add(@RequestBody CustomerFollowUpHistory customerFollowUpHistory,@RequestHeader Map headers) { if(customerFollowUpHistory.getId()!=null){ customerFollowUpHistory.setId(null); } String header = jwtUtil.getJwtProperties().getHeader(); String id= jwtUtil.getId(headers.get(header)).get(); customerFollowUpHistory.setInputuser(Integer.valueOf(id)); customerFollowUpHistoryService.save(customerFollowUpHistory); return ResultGenerator.genOkResult(); } // @Operation(description = "客户跟进记录删除") // @DeleteMapping("/{id}") // public Result delete(@PathVariable Long id) { // customerFollowUpHistoryService.deleteById(id); // return ResultGenerator.genOkResult(); // } @Operation(description = "客户跟进记录更新") @PutMapping public Result update(@RequestBody CustomerFollowUpHistory customerFollowUpHistory) { customerFollowUpHistoryService.update(customerFollowUpHistory); return ResultGenerator.genOkResult(); } @Operation(description = "客户跟进记录获取详细信息") @GetMapping("/{id}") public Result detail(@PathVariable Long id) { CustomerFollowUpHistory customerFollowUpHistory = customerFollowUpHistoryService.getById(id); return ResultGenerator.genOkResult(customerFollowUpHistory); } @Operation(description = "客户跟进记录分页查询") @GetMapping @ApiOperation(value="分页查询客户跟进记录", notes="分页查询 ") @ApiImplicitParams({ @ApiImplicitParam(name = "page", value = "第几页", required = true, dataType = "Integer", paramType="query"), @ApiImplicitParam(name = "size", value = "一页有几条", required = true, dataType = "Integer", paramType="query") }) public Result list( @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size, @RequestParam(defaultValue = "") String keyword, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Date startTime, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Date endTime, @RequestParam(required = false) Integer type) { PageHelper.startPage(page, size); List list = customerFollowUpHistoryService.listAndSearch(keyword,startTime,endTime,type); PageInfo pageInfo = PageInfo.of(list); return ResultGenerator.genOkResult(pageInfo); } } ================================================ FILE: back/src/main/java/com/msy/plus/controller/CustomerHandoverController.java ================================================ package com.msy.plus.controller; import com.msy.plus.core.jwt.JwtUtil; import com.msy.plus.core.response.Result; import com.msy.plus.core.response.ResultGenerator; import com.msy.plus.dto.CustomerHandoverList; import com.msy.plus.entity.CustomerHandover; import com.msy.plus.service.CustomerHandoverService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.annotations.Api; import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiImplicitParams; import io.swagger.annotations.ApiOperation; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.security.access.prepost.PreAuthorize; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import java.util.Date; import java.util.List; import java.util.Map; /** * @author MoShuYing * @date 2021/05/21 */ @PreAuthorize( "hasAuthority('ADMIN')"+ "or hasAuthority('董事长')"+ "or hasAuthority('主席')"+ "or hasAuthority('高级主席')"+ "or hasAuthority('副主席')"+ "or hasAuthority('总裁')"+ "or hasAuthority('会长')"+ "or hasAuthority('高级总裁')"+ "or hasAuthority('高级副总裁')"+ "or hasAuthority('副总裁')"+ "or hasAuthority('总经理')"+ "or hasAuthority('副总经理')"+ "or hasAuthority('总监')"+ "or hasAuthority('经理')"+ "or hasAuthority('高级经理')"+ "or hasAuthority('副经理')"+ "or hasAuthority('主任')"+ "or hasAuthority('高级主任')"+ "or hasAuthority('副主任')"+ "or hasAuthority('组长')"+ "or hasAuthority('副组长')"+ "or hasAuthority('普通员工')"+ "or hasAuthority('人事专员')"+ "or hasAuthority('市场专员')"+ "or hasAuthority('市场主管')"+ "or hasAuthority('销售主管')" ) @Api(tags={"移交历史接口"}) @RestController @RequestMapping("/customer/handover") public class CustomerHandoverController { @Resource private CustomerHandoverService customerHandoverService; @Resource private JwtUtil jwtUtil; @Operation(description = "移交历史添加") @PostMapping public Result add(@RequestBody CustomerHandover customerHandover,@RequestHeader Map headers) { if(customerHandover.getId()!=null){ customerHandover.setId(null); } String header = jwtUtil.getJwtProperties().getHeader(); String id= jwtUtil.getId(headers.get(header)).get(); customerHandover.setTransuser(Integer.valueOf(id)); customerHandoverService.save(customerHandover); return ResultGenerator.genOkResult(); } // @Operation(description = "移交历史删除") // @DeleteMapping("/{id}") // public Result delete(@PathVariable Long id) { // customerHandoverService.deleteById(id); // return ResultGenerator.genOkResult(); // } // // @Operation(description = "移交历史更新") // @PutMapping // public Result update(@RequestBody CustomerHandover customerHandover) { // customerHandoverService.update(customerHandover); // return ResultGenerator.genOkResult(); // } // // @Operation(description = "移交历史获取详细信息") // @GetMapping("/{id}") // public Result detail(@PathVariable Long id) { // CustomerHandover customerHandover = customerHandoverService.getById(id); // return ResultGenerator.genOkResult(customerHandover); // } @Operation(description = "移交历史分页查询") @GetMapping @ApiOperation(value="分页查询移交历史", notes="分页查询 ") @ApiImplicitParams({ @ApiImplicitParam(name = "page", value = "第几页", required = true, dataType = "Integer", paramType="query"), @ApiImplicitParam(name = "size", value = "一页有几条", required = true, dataType = "Integer", paramType="query") }) public Result list(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size, @RequestParam(defaultValue = "") String keyword, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Date startTime, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Date endTime) { PageHelper.startPage(page, size); List list = customerHandoverService.listAndSearch(keyword,startTime,endTime); PageInfo pageInfo = PageInfo.of(list); return ResultGenerator.genOkResult(pageInfo); } } ================================================ FILE: back/src/main/java/com/msy/plus/controller/CustomerManagerController.java ================================================ package com.msy.plus.controller; import com.msy.plus.core.jwt.JwtUtil; import com.msy.plus.core.response.Result; import com.msy.plus.core.response.ResultGenerator; import com.msy.plus.dto.CustomerManagerList; import com.msy.plus.entity.CustomerManager; import com.msy.plus.service.CustomerManagerService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.annotations.Api; import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiImplicitParams; import io.swagger.annotations.ApiOperation; import org.springframework.security.access.prepost.PreAuthorize; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import java.util.List; import java.util.Map; /** * @author MoShuYing * @date 2021/05/20 */ @PreAuthorize( "hasAuthority('ADMIN')"+ "or hasAuthority('董事长')"+ "or hasAuthority('主席')"+ "or hasAuthority('高级主席')"+ "or hasAuthority('副主席')"+ "or hasAuthority('总裁')"+ "or hasAuthority('会长')"+ "or hasAuthority('高级总裁')"+ "or hasAuthority('高级副总裁')"+ "or hasAuthority('副总裁')"+ "or hasAuthority('总经理')"+ "or hasAuthority('副总经理')"+ "or hasAuthority('总监')"+ "or hasAuthority('经理')"+ "or hasAuthority('高级经理')"+ "or hasAuthority('副经理')"+ "or hasAuthority('主任')"+ "or hasAuthority('高级主任')"+ "or hasAuthority('副主任')"+ "or hasAuthority('组长')"+ "or hasAuthority('副组长')"+ "or hasAuthority('普通员工')"+ "or hasAuthority('人事专员')"+ "or hasAuthority('市场专员')"+ "or hasAuthority('市场主管')"+ "or hasAuthority('销售主管')" ) @Api(tags={"客户管理接口"}) @RestController @RequestMapping("/customer/manager") public class CustomerManagerController { @Resource private CustomerManagerService customerManagerService; @Resource private JwtUtil jwtUtil; @Operation(description = "客户管理添加") @PostMapping public Result add(@RequestBody CustomerManager customerManager,@RequestHeader Map headers) { if(customerManager.getId()!=null){ customerManager.setId(null); } String header = jwtUtil.getJwtProperties().getHeader(); String id= jwtUtil.getId(headers.get(header)).get(); customerManager.setInputuser(Integer.valueOf(id)); customerManager.setSeller(Integer.valueOf(id)); customerManagerService.save(customerManager); return ResultGenerator.genOkResult(); } // @Operation(description = "客户管理删除") // @DeleteMapping("/{id}") // public Result delete(@PathVariable Long id) { // customerManagerService.deleteById(id); // return ResultGenerator.genOkResult(); // } @Operation(description = "客户管理更新") @PutMapping public Result update(@RequestBody CustomerManager customerManager) { customerManagerService.update(customerManager); return ResultGenerator.genOkResult(); } @Operation(description = "客户管理获取详细信息") @GetMapping("/{id}") public Result detail(@PathVariable Long id) { CustomerManager customerManager = customerManagerService.getById(id); return ResultGenerator.genOkResult(customerManager); } @Operation(description = "客户管理分页查询") @GetMapping @ApiOperation(value="分页查询客户管理", notes="分页查询 ") @ApiImplicitParams({ @ApiImplicitParam(name = "page", value = "第几页", required = true, dataType = "Integer", paramType="query"), @ApiImplicitParam(name = "size", value = "一页有几条", required = true, dataType = "Integer", paramType="query") }) public Result list(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size, @RequestParam(defaultValue = "",required = false) String keyword, @RequestParam(required = false) Integer status) { PageHelper.startPage(page, size); List list = customerManagerService.listAllWithDictionary(keyword,status); PageInfo pageInfo = PageInfo.of(list); return ResultGenerator.genOkResult(pageInfo); } } ================================================ FILE: back/src/main/java/com/msy/plus/controller/DepartmentController.java ================================================ package com.msy.plus.controller; import com.msy.plus.core.response.Result; import com.msy.plus.core.response.ResultGenerator; import com.msy.plus.entity.Department; import com.msy.plus.service.DepartmentService; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import io.swagger.annotations.Api; import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiImplicitParams; import io.swagger.annotations.ApiOperation; import io.swagger.v3.oas.annotations.Operation; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import java.util.List; /** * @author MoShuYing * @date 2021/05/12 */ @PreAuthorize( "hasAuthority('ADMIN') " + "or hasAuthority('董事长') " + "or hasAuthority('主席') " + "or hasAuthority('高级主席') " + "or hasAuthority('副主席') " + "or hasAuthority('总裁') " + "or hasAuthority('会长') " + "or hasAuthority('高级总裁') " + "or hasAuthority('高级副总裁')") @Api(tags={"部门接口"}) @RestController @RequestMapping("/department") public class DepartmentController { @Resource private DepartmentService departmentService; @Operation(description = "部门添加") @PostMapping public Result add(@RequestBody Department department) { departmentService.save(department); return ResultGenerator.genOkResult(); } @Operation(description = "部门删除") @DeleteMapping("/{id}") public Result delete(@PathVariable Long id) { departmentService.deleteById(id); return ResultGenerator.genOkResult(); } @Operation(description = "部门更新") @PatchMapping public Result update(@RequestBody Department department) { departmentService.update(department); return ResultGenerator.genOkResult(); } @Operation(description = "获取部门详细信息") @GetMapping("/{id}") public Result detail(@PathVariable Long id) { Department department = departmentService.getById(id); return ResultGenerator.genOkResult(department); } @Operation(description = "分页查询部门") @GetMapping @ApiOperation(value="分页查询部门", notes="分页查询") @ApiImplicitParams({ @ApiImplicitParam(name = "page", value = "第几页", required = true, dataType = "Integer", paramType="query"), @ApiImplicitParam(name = "size", value = "一页有几条", required = true, dataType = "Integer", paramType="query") }) public Result list(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size) { PageHelper.startPage(page, size); List list = departmentService.listAll(); PageInfo pageInfo = PageInfo.of(list); return ResultGenerator.genOkResult(pageInfo); } } ================================================ FILE: back/src/main/java/com/msy/plus/controller/DictionaryContentsController.java ================================================ package com.msy.plus.controller; import com.msy.plus.core.response.Result; import com.msy.plus.core.response.ResultGenerator; import com.msy.plus.entity.DictionaryContents; import com.msy.plus.service.DictionaryContentsService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.annotations.Api; import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiImplicitParams; import io.swagger.annotations.ApiOperation; import org.springframework.security.access.prepost.PreAuthorize; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import java.util.List; /** * @author MoShuYing * @date 2021/05/18 */ @PreAuthorize("hasAuthority('ADMIN')") @Api(tags={"数据字典接口"}) @RestController @RequestMapping("/dictionary/contents") public class DictionaryContentsController { @Resource private DictionaryContentsService dictionaryContentsService; @Operation(description = "数据字典添加") @PostMapping public Result add(@RequestBody DictionaryContents dictionaryContents) { dictionaryContentsService.save(dictionaryContents); return ResultGenerator.genOkResult(); } // @Operation(description = "数据字典删除") // @DeleteMapping("/{id}") // public Result delete(@PathVariable Long id) { // dictionaryContentsService.deleteById(id); // return ResultGenerator.genOkResult(); // } @Operation(description = "数据字典更新") @PutMapping public Result update(@RequestBody DictionaryContents dictionaryContents) { dictionaryContentsService.update(dictionaryContents); return ResultGenerator.genOkResult(); } @Operation(description = "数据字典获取详细信息") @GetMapping("/{id}") public Result detail(@PathVariable Long id) { DictionaryContents dictionaryContents = dictionaryContentsService.getById(id); return ResultGenerator.genOkResult(dictionaryContents); } @Operation(description = "数据字典分页查询") @GetMapping @ApiOperation(value="分页查询数据字典", notes="分页查询 ") @ApiImplicitParams({ @ApiImplicitParam(name = "page", value = "第几页", required = true, dataType = "Integer", paramType="query"), @ApiImplicitParam(name = "size", value = "一页有几条", required = true, dataType = "Integer", paramType="query") }) public Result list(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size, @RequestParam(defaultValue = "null") String keyword) { String inKeyword = null; if (!(keyword == null || keyword.equals("null"))) { inKeyword = keyword; } PageHelper.startPage(page, size); List list = dictionaryContentsService.listWithKeyword(inKeyword); PageInfo pageInfo = PageInfo.of(list); return ResultGenerator.genOkResult(pageInfo); } } ================================================ FILE: back/src/main/java/com/msy/plus/controller/DictionaryDetailsController.java ================================================ package com.msy.plus.controller; import com.msy.plus.core.response.Result; import com.msy.plus.core.response.ResultGenerator; import com.msy.plus.entity.DictionaryContents; import com.msy.plus.entity.DictionaryDetails; import com.msy.plus.service.DictionaryDetailsService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.annotations.Api; import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiImplicitParams; import io.swagger.annotations.ApiOperation; import org.springframework.security.access.prepost.PreAuthorize; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import java.util.List; /** * @author MoShuYing * @date 2021/05/18 */ @PreAuthorize("hasAuthority('ADMIN')") @Api(tags={"数据字典明细接口"}) @RestController @RequestMapping("/dictionary/details") public class DictionaryDetailsController { @Resource private DictionaryDetailsService dictionaryDetailsService; @Operation(description = "数据字典明细添加") @PostMapping public Result add(@RequestBody DictionaryDetails dictionaryDetails) { dictionaryDetailsService.save(dictionaryDetails); return ResultGenerator.genOkResult(); } // @Operation(description = "数据字典明细删除") // @DeleteMapping("/{id}") // public Result delete(@PathVariable Long id) { // dictionaryDetailsService.deleteById(id); // return ResultGenerator.genOkResult(); // } @Operation(description = "数据字典明细更新") @PutMapping public Result update(@RequestBody DictionaryDetails dictionaryDetails) { dictionaryDetailsService.update(dictionaryDetails); return ResultGenerator.genOkResult(); } @Operation(description = "数据字典明细获取详细信息") @GetMapping("/{id}") public Result detail(@PathVariable Long id) { DictionaryDetails dictionaryDetails = dictionaryDetailsService.getById(id); return ResultGenerator.genOkResult(dictionaryDetails); } @Operation(description = "数据字典明细分页查询") @GetMapping @ApiOperation(value="分页查询数据字典明细", notes="分页查询 ") @ApiImplicitParams({ @ApiImplicitParam(name = "page", value = "第几页", required = true, dataType = "Integer", paramType="query"), @ApiImplicitParam(name = "size", value = "一页有几条", required = true, dataType = "Integer", paramType="query") }) public Result list(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size, @RequestParam(defaultValue = "1") Integer id, @RequestParam(defaultValue = "null") String keyword) { String inKeyword = null; if (!(keyword == null || keyword.equals("null"))) { inKeyword = keyword; } Integer inId = Integer.valueOf(id); if(inId==null){ inId = dictionaryDetailsService.listAll().get(0).getId(); } PageHelper.startPage(page, size); List list = dictionaryDetailsService.listWithKeyword(inId.intValue(),inKeyword); PageInfo pageInfo = PageInfo.of(list); return ResultGenerator.genOkResult(pageInfo); } } ================================================ FILE: back/src/main/java/com/msy/plus/controller/EmployeeController.java ================================================ package com.msy.plus.controller; import com.alibaba.fastjson.JSONObject; import com.msy.plus.core.jwt.JwtUtil; import com.msy.plus.core.response.Result; import com.msy.plus.core.response.ResultGenerator; import com.msy.plus.entity.Employee; import com.msy.plus.entity.EmployeeDetail; import com.msy.plus.entity.EmployeeWithRoleDO; import com.msy.plus.service.EmployeeService; import com.msy.plus.util.JsonUtils; import com.msy.plus.util.RedisUtils; import io.swagger.v3.oas.annotations.Operation; import io.swagger.annotations.Api; import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiImplicitParams; import io.swagger.annotations.ApiOperation; import org.springframework.security.access.prepost.PreAuthorize; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * @author MoShuYing * @date 2021/05/15 */ @PreAuthorize( "hasAuthority('ADMIN')"+ "or hasAuthority('董事长')"+ "or hasAuthority('主席')"+ "or hasAuthority('高级主席')"+ "or hasAuthority('副主席')"+ "or hasAuthority('总裁')"+ "or hasAuthority('会长')"+ "or hasAuthority('高级总裁')"+ "or hasAuthority('高级副总裁')"+ "or hasAuthority('副总裁')"+ "or hasAuthority('总经理')"+ "or hasAuthority('副总经理')"+ "or hasAuthority('总监')"+ "or hasAuthority('经理')"+ "or hasAuthority('高级经理')"+ "or hasAuthority('副经理')"+ "or hasAuthority('主任')"+ "or hasAuthority('高级主任')"+ "or hasAuthority('副主任')"+ "or hasAuthority('组长')"+ "or hasAuthority('副组长')"+ "or hasAuthority('人事专员')"+ "or hasAuthority('市场专员')"+ "or hasAuthority('市场主管')"+ "or hasAuthority('销售主管')" ) @Api(tags={"员工接口"}) @RestController @RequestMapping("/employee") public class EmployeeController { @Resource private EmployeeService employeeService; @Resource private PasswordEncoder passwordEncoder; @Resource private JwtUtil jwtUtil; @Operation(description = "员工添加") @PostMapping public Result add(@RequestBody EmployeeDetail employee) { if (employee.getId()!=null){ employee.setId(null); } if(employee.getDept() ==null){ return ResultGenerator.genFailedResult("请填写员工部门信息"); } if(employee.getPassword()!=null && employee.getPassword().length()<=5){ return ResultGenerator.genFailedResult("密码长度不能少于或等于五位"); } employee.setPassword(this.passwordEncoder.encode(employee.getPassword().trim())); try{ employeeService.save(employee); }catch (Exception e){ e.printStackTrace(); String msg = "信息有误"; if(e.toString().contains("for key 'employee.employee_name_uindex'")){ msg = "已有同名员工,请检查员工名称"; }else if(e.toString().contains("for key 'employee.employee_email_uindex'")){ msg = "已有同名邮箱,请检查员工邮箱"; } return ResultGenerator.genFailedResult(msg); } if(!(employee.getRoleIds() ==null || employee.getRoleIds().size()<1)){ employeeService.saveRoles(employee.getId(),employee.getRoleIds()); } return ResultGenerator.genOkResult(); } @Operation(description = "员工删除") @DeleteMapping("/{id}") public Result delete(@PathVariable Long id) { employeeService.deleteById(id); employeeService.deleteEmployeeWithRole(id); return ResultGenerator.genOkResult(); } @Operation(description = "员工更新") @PutMapping public Result update(@RequestBody EmployeeDetail employee,@RequestHeader Map headers) { if(employee.getName().equals("admin")){ return ResultGenerator.genFailedResult("禁止修改管理员角色!"); } // 更新员工基本信息 if(employee.getDept() ==null){ return ResultGenerator.genFailedResult("请填写员工部门信息"); } if(employee.getPassword()!=null){ if(employee.getPassword().length()<=5){ return ResultGenerator.genFailedResult("密码长度不能少于或等于五位"); } employee.setPassword(this.passwordEncoder.encode(employee.getPassword().trim())); } try{ employeeService.update((Employee) employee); }catch (Exception e){ e.printStackTrace(); String msg = "信息有误"; if(e.toString().contains("for key 'employee.employee_name_uindex'")){ msg = "已有同名员工,请检查员工名称"; }else if(e.toString().contains("for key 'employee.employee_email_uindex'")){ msg = "已有同名邮箱,请检查员工邮箱"; } return ResultGenerator.genFailedResult(msg); } List now= employee.getRoleIds(); if(now==null) { return ResultGenerator.genOkResult(); } List raw = this.employeeService.getAllEmployeeRoleTableRow(employee.getId()); // diff运算 List adds = new ArrayList<>(); List removes = new ArrayList<>(); for(Long i:now){ if(!raw.contains(i)){ adds.add(i); } } for(Long i:raw){ if(!now.contains(i)){ removes.add(i); } } // 更新权限即注销对应用户登录 if(!adds.isEmpty() || !removes.isEmpty()){ jwtUtil.invalidRedisToken(employee.getName()); } if(!adds.isEmpty()){ this.employeeService.saveRoles(employee.getId(),adds); } if(!removes.isEmpty()){ for(Long i :removes){ this.employeeService.deleteEmployeeWithRoleItem(employee.getId(),i); } } return ResultGenerator.genOkResult(); } @Operation(description = "员工获取详细信息") @GetMapping("/{id}") public Result detail(@PathVariable Long id) { EmployeeDetail employee = employeeService.getDetailById(id); final EmployeeDetail object = JsonUtils.deleteFields(employee, EmployeeDetail.class, "password"); return ResultGenerator.genOkResult(object); } @Operation(description = "员工分页查询") @GetMapping @ApiOperation(value="分页查询员工", notes="分页查询 ") @ApiImplicitParams({ @ApiImplicitParam(name = "page", value = "第几页", required = true, dataType = "Integer", paramType="query"), @ApiImplicitParam(name = "size", value = "一页有几条", required = true, dataType = "Integer", paramType="query") }) public Result list( @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size, @RequestParam(required = false) Integer dept, @RequestParam(defaultValue = "") String keyword) { PageHelper.startPage(page, size); List list = employeeService.listEmployeeWithRole(keyword, dept); PageInfo pageInfo = PageInfo.of(list); // 不显示 password 字段 final PageInfo objectPageInfo = JsonUtils.deleteFields(pageInfo, PageInfo.class, "password"); return ResultGenerator.genOkResult(objectPageInfo); } } ================================================ FILE: back/src/main/java/com/msy/plus/controller/PermissionController.java ================================================ package com.msy.plus.controller; import com.msy.plus.core.response.Result; import com.msy.plus.core.response.ResultGenerator; import com.msy.plus.entity.Permission; import com.msy.plus.service.PermissionService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.annotations.Api; import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiImplicitParams; import io.swagger.annotations.ApiOperation; import org.springframework.security.access.prepost.PreAuthorize; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import java.util.List; /** * @author MoShuYing * @date 2021/05/14 */ @PreAuthorize("hasAuthority('ADMIN')") @Api(tags={"权限接口"}) @RestController @RequestMapping("/permission") public class PermissionController { @Resource private PermissionService permissionService; // // @Operation(description = "权限添加") // @PostMapping // public Result add(@RequestBody Permission permission) { // permissionService.save(permission); // return ResultGenerator.genOkResult(); // } @Operation(description = "权限删除") @DeleteMapping("/{id}") public Result delete(@PathVariable Long id) { permissionService.deleteById(id); return ResultGenerator.genOkResult(); } // @Operation(description = "权限更新") // @PutMapping // public Result update(@RequestBody Permission permission) { // permissionService.update(permission); // return ResultGenerator.genOkResult(); // } // @Operation(description = "权限获取详细信息") // @GetMapping("/{id}") // public Result detail(@PathVariable Long id) { // Permission permission = permissionService.getById(id); // return ResultGenerator.genOkResult(permission); // } @Operation(description = "权限分页查询") @GetMapping @ApiOperation(value="分页查询权限", notes="分页查询 ") @ApiImplicitParams({ @ApiImplicitParam(name = "page", value = "第几页", required = true, dataType = "Integer", paramType="query"), @ApiImplicitParam(name = "size", value = "一页有几条", required = true, dataType = "Integer", paramType="query") }) public Result list(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size) { PageHelper.startPage(page, size); List list = permissionService.listAll(); PageInfo pageInfo = PageInfo.of(list); return ResultGenerator.genOkResult(pageInfo); } } ================================================ FILE: back/src/main/java/com/msy/plus/controller/RoleController.java ================================================ package com.msy.plus.controller; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import com.msy.plus.core.response.Result; import com.msy.plus.core.response.ResultGenerator; import com.msy.plus.dto.RoleWithPermissionDTO; import com.msy.plus.entity.RoleDO; import com.msy.plus.entity.RolePermissionDO; import com.msy.plus.service.RoleService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiImplicitParams; import io.swagger.annotations.ApiOperation; import io.swagger.v3.oas.annotations.Operation; import org.springframework.dao.DuplicateKeyException; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import java.util.*; /** * 角色控制器 * * @author MoShuying * @date 2018/05/27 */ @PreAuthorize("hasAuthority('ADMIN')") @Api(tags={"角色接口"}) @RestController @RequestMapping("/role") public class RoleController { @Resource private RoleService roleService; @Operation(description = "角色添加") @PostMapping public Result add(@RequestBody final RoleWithPermissionDTO roleDTO) { if(roleDTO.getPermissions()==null){ return ResultGenerator.genFailedResult("尚未添加角色权限"); } try{ this.roleService.save(roleDTO); }catch (DuplicateKeyException e){ return ResultGenerator.genFailedResult("提交的信息中包含已存在的字段"); } List temp = new ArrayList<>(); roleDTO.getPermissions().forEach(e->{ temp.add(e.getId()); }); this.roleService.savePermissions(roleDTO.getId(),temp); return ResultGenerator.genOkResult(); } @Operation(description = "角色删除") @DeleteMapping("/{id}") public Result delete(@PathVariable final Long id) { List raw = this.roleService.getAllRolePermissionTableRow(id); for(RolePermissionDO e :raw){ this.roleService.deleteRolePermissionItem(id,e.getPermission_id()); } this.roleService.deleteById(id); return ResultGenerator.genOkResult(); } @Operation(description = "角色更新") @PutMapping public Result update(@RequestBody final RoleWithPermissionDTO roleWithPermissionDTO) { // 更新用户基本信息 this.roleService.update(roleWithPermissionDTO); List nowPermissions = new ArrayList<>(); if(roleWithPermissionDTO.getPermissions()==null){ return ResultGenerator.genOkResult(); } List rawPer = this.roleService.getAllRolePermissionTableRow(roleWithPermissionDTO.getId()); // 表中权限信息去重 Set raw = new HashSet<>(); for(RolePermissionDO e: rawPer){ raw.add(e.getPermission_id()); } roleWithPermissionDTO.getPermissions().forEach(e->{ nowPermissions.add(e.getId()); }); // diff运算 Set adds = new HashSet<>(); Set removes = new HashSet<>(); // 如果修改后的不包含原来的 那么为新增元素 for(Long i:nowPermissions){ if(!raw.contains(i)){ adds.add(i); } } // 如果原来的不包含修改后的 那么是删除元素 for(Long i:raw){ if(!nowPermissions.contains(i)){ removes.add(i); } } if(!adds.isEmpty()){ this.roleService.savePermissions(roleWithPermissionDTO.getId(),new ArrayList<>(adds)); } if(!removes.isEmpty()){ removes.forEach(e->{ this.roleService.deleteRolePermissionItem(roleWithPermissionDTO.getId(),e); }); } return ResultGenerator.genOkResult(); } @Operation(description = "角色详情") @GetMapping("/{id}") public Result detail(@PathVariable final Long id) { final RoleDO role = this.roleService.getDetailById(id); return ResultGenerator.genOkResult(role); } @Operation(description = "角色列表") @GetMapping @ApiOperation(value="分页查询角色", notes="分页查询角色列表") @ApiImplicitParams({ @ApiImplicitParam(name = "page", value = "第几页", required = true, dataType = "Integer", paramType="query"), @ApiImplicitParam(name = "size", value = "一页有几条", required = true, dataType = "Integer", paramType="query") }) public Result list( @RequestParam(defaultValue = "1") final Integer page, @RequestParam(defaultValue = "10") final Integer size) { PageHelper.startPage(page, size); final List list = this.roleService.listAll(); final PageInfo pageInfo = new PageInfo<>(list); return ResultGenerator.genOkResult(pageInfo); } } ================================================ FILE: back/src/main/java/com/msy/plus/core/cache/CacheExpire.java ================================================ package com.msy.plus.core.cache; import org.springframework.core.annotation.AliasFor; import java.lang.annotation.*; /** * 缓存过期注解 * * @author MoShuying * @date 2018/07/11 */ @Inherited @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.TYPE}) public @interface CacheExpire { /** 过期时间,默认 60s */ @AliasFor("expire") long value() default 60L; /** 过期时间,默认 60s */ @AliasFor("value") long expire() default 60L; } ================================================ FILE: back/src/main/java/com/msy/plus/core/cache/MyRedisCacheManager.java ================================================ package com.msy.plus.core.cache; import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeansException; import org.springframework.beans.factory.InitializingBean; import org.springframework.cache.Cache; import org.springframework.cache.annotation.CacheConfig; import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Caching; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.data.redis.cache.RedisCache; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.cache.RedisCacheWriter; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.lang.NonNull; import org.springframework.util.ReflectionUtils; import java.time.Duration; import java.util.*; import java.util.concurrent.Callable; /** * Redis 容易出现缓存问题(超时、Redis 宕机等),当使用 spring cache 的注释 Cacheable、Cacheput 等处理缓存问题时, 我们无法使用 try catch * 处理出现的异常,所以最后导致结果是整个服务报错无法正常工作。 通过自定义 MyRedisCacheManager 并继承 RedisCacheManager 来处理异常可以解决这个问题 * *

http://www.spring4all.com/article/937 * * @author MoShuying * @date 2018/07/11 */ @Slf4j public class MyRedisCacheManager extends RedisCacheManager implements ApplicationContextAware, InitializingBean { /** key serializer */ public static final StringRedisSerializer STRING_SERIALIZER = new StringRedisSerializer(); /** * value serializer * *

使用 FastJsonRedisSerializer 会报错:java.lang.ClassCastException FastJsonRedisSerializer * fastSerializer = new FastJsonRedisSerializer<>(Object.class); */ public static final GenericFastJsonRedisSerializer FASTJSON_SERIALIZER = new GenericFastJsonRedisSerializer(); /** key serializer pair */ public static final RedisSerializationContext.SerializationPair STRING_PAIR = RedisSerializationContext.SerializationPair.fromSerializer(STRING_SERIALIZER); /** value serializer pair */ public static final RedisSerializationContext.SerializationPair FASTJSON_PAIR = RedisSerializationContext.SerializationPair.fromSerializer(FASTJSON_SERIALIZER); private final Map initialCacheConfiguration = new LinkedHashMap<>(); private ApplicationContext applicationContext; public MyRedisCacheManager( final RedisCacheWriter cacheWriter, final RedisCacheConfiguration defaultCacheConfiguration) { super(cacheWriter, defaultCacheConfiguration); } public MyRedisCacheManager( final RedisCacheWriter cacheWriter, final RedisCacheConfiguration defaultCacheConfiguration, final Map initialCacheConfigurations) { super(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations); } @Override public Cache getCache(@NonNull final String name) { final Cache cache = super.getCache(name); return new RedisCacheWrapper(cache); } @Override public void setApplicationContext(@NonNull final ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } @Override public void afterPropertiesSet() { final String[] beanNames = this.applicationContext.getBeanNamesForType(Object.class); for (final String beanName : beanNames) { final Class clazz = this.applicationContext.getType(beanName); this.add(clazz); } super.afterPropertiesSet(); } @NonNull @Override protected Collection loadCaches() { final List caches = new LinkedList<>(); for (final Map.Entry entry : this.initialCacheConfiguration.entrySet()) { caches.add(super.createRedisCache(entry.getKey(), entry.getValue())); } return caches; } private void add(final Class clazz) { ReflectionUtils.doWithMethods( clazz, method -> { ReflectionUtils.makeAccessible(method); final CacheExpire cacheExpire = AnnotationUtils.findAnnotation(method, CacheExpire.class); if (!Optional.ofNullable(cacheExpire).isPresent()) { return; } final Cacheable cacheable = AnnotationUtils.findAnnotation(method, Cacheable.class); if (Optional.ofNullable(cacheable).isPresent()) { this.add(cacheable.cacheNames(), cacheExpire); return; } final Caching caching = AnnotationUtils.findAnnotation(method, Caching.class); if (Optional.ofNullable(caching).isPresent()) { final Cacheable[] cs = caching.cacheable(); if (cs.length > 0) { for (final Cacheable c : cs) { if (Optional.ofNullable(c).isPresent()) { this.add(c.cacheNames(), cacheExpire); } } } } else { final CacheConfig cacheConfig = AnnotationUtils.findAnnotation(clazz, CacheConfig.class); if (Optional.ofNullable(cacheConfig).isPresent()) { this.add(cacheConfig.cacheNames(), cacheExpire); } } }, method -> null != AnnotationUtils.findAnnotation(method, CacheExpire.class)); } private void add(final String[] cacheNames, final CacheExpire cacheExpire) { for (final String cacheName : cacheNames) { if (!Optional.ofNullable(cacheName).isPresent() || "".equals(cacheName.trim())) { continue; } final long expire = cacheExpire.expire(); log.debug("cache name<{}> expire: {}", cacheName, expire); if (expire >= 0) { // 缓存配置 final RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofSeconds(expire)) .disableCachingNullValues() // .prefixKeysWith(cacheName) .serializeKeysWith(STRING_PAIR) .serializeValuesWith(FASTJSON_PAIR); this.initialCacheConfiguration.put(cacheName, config); } else { log.warn("{} use default expiration.", cacheName); } } } protected static class RedisCacheWrapper implements Cache { private final Cache cache; RedisCacheWrapper(final Cache cache) { this.cache = cache; } @Override public String getName() { MyRedisCacheManager.log.debug("get name: {}", this.cache.getName()); try { return this.cache.getName(); } catch (final Exception e) { MyRedisCacheManager.log.error("get name => {}", e.getMessage()); return null; } } @Override public Object getNativeCache() { MyRedisCacheManager.log.debug("native cache: {}", this.cache.getNativeCache()); try { return this.cache.getNativeCache(); } catch (final Exception e) { MyRedisCacheManager.log.error("get native cache => {}", e.getMessage()); return null; } } @Override public ValueWrapper get(@NonNull final Object o) { MyRedisCacheManager.log.debug("get => o: {}", o); try { return this.cache.get(o); } catch (final Exception e) { MyRedisCacheManager.log.error("get => o: {}, error: {}", o, e.getMessage()); return null; } } @Override public T get(@NonNull final Object o, final Class aClass) { MyRedisCacheManager.log.debug("get => o: {}, clazz: {}", o, aClass); try { return this.cache.get(o, aClass); } catch (final Exception e) { MyRedisCacheManager.log.error("get => o: {}, clazz: {}, error: {}", o, aClass, e.getMessage()); return null; } } @Override public T get(@NonNull final Object o, @NonNull final Callable callable) { MyRedisCacheManager.log.debug("get => o: {}", o); try { return this.cache.get(o, callable); } catch (final Exception e) { MyRedisCacheManager.log.error("get => o: {}, error: {}", o, e.getMessage()); return null; } } @Override public void put(@NonNull final Object o, final Object o1) { MyRedisCacheManager.log.debug("put => o: {}, o1: {}", o, o1); try { this.cache.put(o, o1); } catch (final Exception e) { MyRedisCacheManager.log.error("put => o: {}, o1: {}, error: {}", o, o1, e.getMessage()); } } @Override public ValueWrapper putIfAbsent(@NonNull final Object o, final Object o1) { MyRedisCacheManager.log.debug("put if absent => o: {}, o1: {}", o, o1); try { return this.cache.putIfAbsent(o, o1); } catch (final Exception e) { MyRedisCacheManager.log.error("put if absent => o: {}, o1: {}, error: {}", o, o1, e.getMessage()); return null; } } @Override public void evict(@NonNull final Object o) { MyRedisCacheManager.log.debug("evict => o: {}", o); try { this.cache.evict(o); } catch (final Exception e) { MyRedisCacheManager.log.error("evict => o: {}, error: {}", o, e.getMessage()); } } @Override public void clear() { MyRedisCacheManager.log.debug("clear"); try { this.cache.clear(); } catch (final Exception e) { MyRedisCacheManager.log.error("clear => error: {}", e.getMessage()); } } } } ================================================ FILE: back/src/main/java/com/msy/plus/core/config/JasyptConfig.java ================================================ package com.msy.plus.core.config; import com.msy.plus.core.rsa.RsaUtils; import org.jasypt.encryption.StringEncryptor; import org.jasypt.encryption.pbe.PooledPBEStringEncryptor; import org.jasypt.encryption.pbe.config.SimpleStringPBEConfig; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.util.Base64Utils; import javax.annotation.Resource; /** * Jasypt 配置(2.1.1可以配置非对称加密,但是测试还有问题,等解决再更新) * * @author MoShuying * @date 2018/07/21 */ @Configuration public class JasyptConfig { @Value("${jasypt.encryptor.password}") private String passwordEncryptedByBase64AndRsa; @Resource private RsaUtils rsaUtils; @Bean public StringEncryptor myStringEncryptor() throws Exception { // 先 Base64,后 RSA 加密的密码 final byte[] passwordEncryptedByRsa = Base64Utils.decodeFromString(this.passwordEncryptedByBase64AndRsa); final String password = new String(this.rsaUtils.decrypt(passwordEncryptedByRsa)); // 配置 final SimpleStringPBEConfig config = new SimpleStringPBEConfig() { { this.setPassword(password); // 加密算法 this.setAlgorithm("PBEWithMD5AndDES"); this.setKeyObtentionIterations("1000"); this.setPoolSize("1"); this.setProviderName("SunJCE"); this.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator"); this.setStringOutputType("base64"); } }; final PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor(); encryptor.setConfig(config); return encryptor; } } ================================================ FILE: back/src/main/java/com/msy/plus/core/config/RedisCacheConfig.java ================================================ package com.msy.plus.core.config; import com.msy.plus.core.cache.MyRedisCacheManager; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.data.redis.RedisProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CachingConfigurerSupport; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.interceptor.CacheErrorHandler; import org.springframework.cache.interceptor.KeyGenerator; import org.springframework.cache.interceptor.SimpleCacheErrorHandler; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheWriter; import org.springframework.data.redis.connection.RedisConnectionFactory; import javax.annotation.Resource; import java.util.Optional; /** * Redis缓存配置 * * @author MoShuying * @date 2018/07/11 */ @Slf4j @Configuration @EnableCaching(proxyTargetClass = true) @ConditionalOnProperty(name = "spring.redis.host") @EnableConfigurationProperties(RedisProperties.class) public class RedisCacheConfig extends CachingConfigurerSupport { @Resource private RedisConnectionFactory redisConnectionFactory; @Bean @Override public CacheManager cacheManager() { // 初始化一个 RedisCacheWriter final RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(this.redisConnectionFactory); final RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig() // 不缓存 null 值 // .disableCachingNullValues() // 使用注解时的序列化、反序列化对 .serializeKeysWith(MyRedisCacheManager.STRING_PAIR) .serializeValuesWith(MyRedisCacheManager.FASTJSON_PAIR); // 初始化RedisCacheManager return new MyRedisCacheManager(redisCacheWriter, defaultCacheConfig); } /** * 如果 @Cacheable、@CachePut、@CacheEvict 等注解没有配置 key,则使用这个自定义 key 生成器 * *

自定义缓存的 key 时,难以保证 key 的唯一性 * *

此时最好指定方法名,比如:@Cacheable(value="", key="{#root.methodName, #id}") */ @Bean @Override public KeyGenerator keyGenerator() { // 比如 User 类 list(Integer page, Integer size) 方法 // 用户 A 请求:list(1, 2) // redis 缓存的 key:User.list#1,2 return (target, method, params) -> { final String dot = "."; final StringBuilder sb = new StringBuilder(32); // 类名 sb.append(target.getClass().getSimpleName()); sb.append(dot); // 方法名 sb.append(method.getName()); // 如果存在参数 if (0 < params.length) { sb.append("#"); // 带上参数 String comma = ""; for (final Object param : params) { sb.append(comma); if (!Optional.ofNullable(param).isPresent()) { sb.append("NULL"); } else { sb.append(param.toString()); } comma = ","; } } return sb.toString(); }; } /** 错误处理,主要是打印日志 */ @Bean @Override public CacheErrorHandler errorHandler() { return new SimpleCacheErrorHandler() { @Override public void handleCacheGetError( final RuntimeException e, final Cache cache, final Object key) { RedisCacheConfig.log.error("==> cache: {}", cache); RedisCacheConfig.log.error("==> key: {}", key); RedisCacheConfig.log.error("==> error: {}", e.getMessage()); super.handleCacheGetError(e, cache, key); } @Override public void handleCachePutError( final RuntimeException e, final Cache cache, final Object key, final Object value) { RedisCacheConfig.log.error("==> cache: {}", cache); RedisCacheConfig.log.error("==> key: {}", key); RedisCacheConfig.log.error("==> value: {}", value); RedisCacheConfig.log.error("==> error: {}", e.getMessage()); super.handleCachePutError(e, cache, key, value); } @Override public void handleCacheEvictError( final RuntimeException e, final Cache cache, final Object key) { RedisCacheConfig.log.error("==> cache: {}", cache); RedisCacheConfig.log.error("==> key: {}", key); RedisCacheConfig.log.error("==> error: {}", e.getMessage()); super.handleCacheEvictError(e, cache, key); } @Override public void handleCacheClearError(final RuntimeException e, final Cache cache) { RedisCacheConfig.log.error("==> cache: {}", cache); RedisCacheConfig.log.error("==> error: {}", e.getMessage()); super.handleCacheClearError(e, cache); } }; } } ================================================ FILE: back/src/main/java/com/msy/plus/core/config/RedisConfig.java ================================================ package com.msy.plus.core.config; import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer; import com.msy.plus.core.cache.MyRedisCacheManager; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.data.redis.RedisProperties; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisStandaloneConfiguration; import org.springframework.data.redis.connection.jedis.JedisClientConfiguration; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.StringRedisSerializer; import redis.clients.jedis.JedisPoolConfig; import javax.annotation.Resource; import java.time.Duration; /** * Redis配置 * * @author MoShuying * @date 2018/05/27 */ @Configuration public class RedisConfig { @Resource private RedisProperties redisProperties; @Bean @ConfigurationProperties(prefix = "spring.redis.jedis.pool") public JedisPoolConfig jedisPoolConfig() { return new JedisPoolConfig(); } @Bean @ConfigurationProperties(prefix = "spring.redis") public RedisConnectionFactory redisConnectionFactory( @Qualifier(value = "jedisPoolConfig") final JedisPoolConfig jedisPoolConfig) { // 方法上的 @ConfigurationProperties 不生效 // 未知 bug,暂时这样手动设置 // fixme // 单机版 jedis final RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration( this.redisProperties.getHost(), this.redisProperties.getPort()); redisStandaloneConfiguration.setDatabase(this.redisProperties.getDatabase()); redisStandaloneConfiguration.setPassword(this.redisProperties.getPassword()); // 获得默认的连接池构造器 final JedisClientConfiguration.JedisPoolingClientConfigurationBuilder jpcb = (JedisClientConfiguration.JedisPoolingClientConfigurationBuilder) JedisClientConfiguration.builder(); // 指定 jedisPoolConifig 来修改默认的连接池构造器 jpcb.poolConfig(jedisPoolConfig); // 连接超时 jpcb.and().connectTimeout(Duration.ofSeconds(10)); // 通过构造器来构造 jedis 客户端配置 final JedisClientConfiguration jedisClientConfiguration = jpcb.build(); // 单机配置 + 客户端配置 = jedis 连接工厂 return new JedisConnectionFactory(redisStandaloneConfiguration, jedisClientConfiguration); } /** * 配置 RedisTemplate,配置 key 和 value 的序列化类 * *

key 序列化使用 StringRedisSerializer, 不配置的话,key 会出现乱码 */ @Bean public RedisTemplate redisTemplate( @Qualifier(value = "redisConnectionFactory") final RedisConnectionFactory factory) { final RedisTemplate redisTemplate = new RedisTemplate<>(); // 设置 key 的序列化器为字符串 serializer final StringRedisSerializer stringSerializer = MyRedisCacheManager.STRING_SERIALIZER; redisTemplate.setKeySerializer(stringSerializer); redisTemplate.setHashKeySerializer(stringSerializer); // 设置 value 的序列化器为 fastjson serializer final GenericFastJsonRedisSerializer fastSerializer = MyRedisCacheManager.FASTJSON_SERIALIZER; redisTemplate.setValueSerializer(fastSerializer); redisTemplate.setHashValueSerializer(fastSerializer); // 如果 KeySerializer 或者 ValueSerializer 没有配置 // 则对应的 KeySerializer、ValueSerializer 才使用 fastjson serializer redisTemplate.setDefaultSerializer(fastSerializer); redisTemplate.setConnectionFactory(factory); redisTemplate.afterPropertiesSet(); return redisTemplate; } } ================================================ FILE: back/src/main/java/com/msy/plus/core/config/Swagger3Config.java ================================================ package com.msy.plus.core.config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; import springfox.documentation.service.*; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.spi.service.contexts.SecurityContext; import java.util.Collections; import java.util.List; import static com.msy.plus.core.constant.ProjectConstant.SPRING_PROFILE_PRODUCTION; /** * Swagger3 * * @author MoShuying * @date 2020/11/06 */ @PropertySource( value = "classpath:/META-INF/swagger3.yml", factory = YamlPropertySourceFactory.class) @Configuration public class Swagger3Config { @Value("${spring.profiles.active}") private String activeProfile; @Value("${application.name}") private String applicationName; @Value("${application.version}") private String applicationVersion; @Value("${application.description}") private String applicationDescription; @Value("${application.url.service}") private String applicationServiceUrl; @Value("${application.license}") private String applicationLicense; @Value("${application.url.license}") private String applicationLicenseUrl; @Value("${application.apis.selector}") private String selector; @Value("${author.name}") private String authorName; @Value("${author.url}") private String authorUrl; @Value("${author.email}") private String authorEmail; private ApiInfo apiInfo() { return new ApiInfoBuilder() .title(applicationName) .version(applicationVersion) .description(applicationDescription) .termsOfServiceUrl(applicationServiceUrl) .contact(new Contact(authorName, authorUrl, authorEmail)) .license(applicationLicense) .licenseUrl(applicationLicenseUrl) .build(); } @Bean public Docket docket() { //添加head参数配置start return new Docket(DocumentationType.SWAGGER_2) .apiInfo(this.apiInfo()) // 仅在非生产环境下生效 .enable(!SPRING_PROFILE_PRODUCTION.equals(this.activeProfile)) .select() .apis(RequestHandlerSelectors.basePackage(selector)) .paths(PathSelectors.any()) .build() .securitySchemes(securitySchemes()) .securityContexts(securityContexts()); } private List securitySchemes(){ return Collections.singletonList(new ApiKey("Authorization", "Authorization", "header")); } private List securityContexts() { return Collections.singletonList( SecurityContext.builder() .securityReferences(defaultAuth()) .operationSelector(null) .build() ); } private List defaultAuth() { AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything"); return Collections.singletonList(new SecurityReference("Authorization", new AuthorizationScope[]{authorizationScope})); } } ================================================ FILE: back/src/main/java/com/msy/plus/core/config/ValidatorConfig.java ================================================ package com.msy.plus.core.config; import org.hibernate.validator.HibernateValidator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.validation.beanvalidation.MethodValidationPostProcessor; import javax.validation.Validation; import javax.validation.Validator; import javax.validation.ValidatorFactory; /** * 参数校验 * https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#section-constraint-violation-methods * * @author MoShuying * @date 2018/05/27 */ @Configuration public class ValidatorConfig { @Bean public MethodValidationPostProcessor methodValidationPostProcessor() { final MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor(); // 设置 validator 模式为快速失败返回 postProcessor.setValidator(this.validatorFailFast()); return postProcessor; // 默认是普通模式,会返回所有的验证不通过信息集合 // return new MethodValidationPostProcessor(); } @Bean public Validator validatorFailFast() { final ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) .configure() .addProperty("hibernate.validator.fail_fast", "true") .buildValidatorFactory(); return validatorFactory.getValidator(); } } ================================================ FILE: back/src/main/java/com/msy/plus/core/config/WebMvcConfig.java ================================================ package com.msy.plus.core.config; import com.alibaba.fastjson.serializer.SerializerFeature; import com.alibaba.fastjson.support.config.FastJsonConfig; import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter; import org.springframework.context.annotation.Configuration; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; /** * Spring MVC 配置 * * @author MoShuying * @date 2018/05/27 */ @Configuration public class WebMvcConfig extends WebMvcConfigurationSupport { /** 使用阿里 FastJson 作为 JSON MessageConverter */ @Override public void configureMessageConverters(final List> converters) { final FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter(); final FastJsonConfig config = new FastJsonConfig(); // 支持的输出类型 final List supportedMediaTypes = new ArrayList<>(); supportedMediaTypes.add(MediaType.APPLICATION_JSON); supportedMediaTypes.add(MediaType.APPLICATION_OCTET_STREAM); supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED); supportedMediaTypes.add(MediaType.TEXT_HTML); converter.setSupportedMediaTypes(supportedMediaTypes); config.setSerializerFeatures( // 保留空的字段 // SerializerFeature.WriteMapNullValue, // Number null -> 0 SerializerFeature.WriteNullNumberAsZero, // 美化输出 SerializerFeature.PrettyFormat); converter.setFastJsonConfig(config); converter.setDefaultCharset(StandardCharsets.UTF_8); converters.add(converter); } /** 资源控制器 */ @Override public void addResourceHandlers(final ResourceHandlerRegistry registry) { if (!registry.hasMappingForPattern("/webjars/**")) { registry .addResourceHandler("/webjars/**") .addResourceLocations("classpath:/META-INF/resources/webjars/"); } if (!registry.hasMappingForPattern("/swagger-ui/**")) { // It is recommended by Springfox 3.x to disable caching of the static Swagger page content registry .addResourceHandler("/swagger-ui/**") .addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/") .resourceChain(false); } } @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addRedirectViewController("/swagger-ui.html", "/swagger-ui/index.html"); registry.addRedirectViewController("/swagger-ui", "/swagger-ui/index.html"); registry.addRedirectViewController("/swagger-ui/", "/swagger-ui/index.html"); } } ================================================ FILE: back/src/main/java/com/msy/plus/core/config/WebSecurityConfig.java ================================================ package com.msy.plus.core.config; import com.msy.plus.filter.AuthenticationFilter; import com.msy.plus.filter.MyAuthenticationEntryPoint; import com.msy.plus.service.impl.UserDetailsServiceImpl; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import javax.annotation.Resource; /** * 安全设置 * * @author MoShuying * @date 2018/05/27 */ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) class WebSecurityConfig extends WebSecurityConfigurerAdapter { private static final String[] ANONYMOUS_LIST = { "/druid/**", "/swagger-ui/", "/swagger-ui/index.html", "/swagger-ui.html", "/swagger-ui/**", "/v2/api-docs", "/v3/api-docs", "/swagger-resources", "/swagger-resources/**", "/webjars/**", }; @Resource private MyAuthenticationEntryPoint myAuthenticationEntryPoint; @Resource private AuthenticationFilter authenticationFilter; /** 使用随机加盐哈希算法对密码进行加密 */ @Bean public static PasswordEncoder passwordEncoder() { // 默认强度10,可以指定 4 到 31 之间的强度 return new BCryptPasswordEncoder(); } @Bean @Override public UserDetailsServiceImpl userDetailsService() { return new UserDetailsServiceImpl(); } @Override protected void configure(final AuthenticationManagerBuilder auth) throws Exception { auth // 自定义获取账户信息 .userDetailsService(this.userDetailsService()) // 设置密码加密 .passwordEncoder(WebSecurityConfig.passwordEncoder()); } @Override protected void configure(final HttpSecurity http) throws Exception { http // 禁用页面缓存 .headers() .cacheControl() .and() .and() // 关闭 cors 验证 .cors() .disable() // 关闭 csrf 验证 .csrf() .disable() // 无状态 session .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() // 异常处理 .exceptionHandling() // 因为 RESTFul 没有登录界面所以只显示未登录 Json .authenticationEntryPoint(this.myAuthenticationEntryPoint) .and() // 身份过滤器 .addFilterBefore(this.authenticationFilter, UsernamePasswordAuthenticationFilter.class) // 认证访问 .authorizeRequests() // 允许匿名请求 .antMatchers(ANONYMOUS_LIST) .permitAll() // 注册和登录 .antMatchers(HttpMethod.POST, "/account", "/account/token") .permitAll() // 预请求 .antMatchers(HttpMethod.OPTIONS) .permitAll() // 对除上面特别请求外所有都鉴权认证 .anyRequest() .authenticated(); } } ================================================ FILE: back/src/main/java/com/msy/plus/core/config/YamlPropertySourceFactory.java ================================================ package com.msy.plus.core.config; import com.msy.plus.core.exception.YamlNotFoundException; import org.springframework.beans.factory.config.YamlPropertiesFactoryBean; import org.springframework.core.env.PropertiesPropertySource; import org.springframework.core.env.PropertySource; import org.springframework.core.io.support.EncodedResource; import org.springframework.core.io.support.PropertySourceFactory; import java.util.Optional; import java.util.Properties; /** * Yml配置文件工厂 * * @author MoShuying * @date 2020/11/12 */ public class YamlPropertySourceFactory implements PropertySourceFactory { @Override public PropertySource createPropertySource(final String name, final EncodedResource resource) { final YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); factory.setResources(resource.getResource()); factory.afterPropertiesSet(); final Properties ymlProperties = Optional.ofNullable(factory.getObject()).orElseThrow(YamlNotFoundException::new); final String propertyName = Optional.ofNullable(name).orElse(resource.getResource().getFilename()); Optional.ofNullable(propertyName).orElseThrow(YamlNotFoundException::new); return new PropertiesPropertySource(propertyName, ymlProperties); } } ================================================ FILE: back/src/main/java/com/msy/plus/core/constant/ProjectConstant.java ================================================ package com.msy.plus.core.constant; /** * 项目常量 * * @author MoShuying * @date 2018/05/27 */ public final class ProjectConstant { /** 开发环境 */ public static final String SPRING_PROFILE_DEVELOPMENT = "dev"; /** 生产环境 */ public static final String SPRING_PROFILE_PRODUCTION = "prod"; /** 测试环境 */ public static final String SPRING_PROFILE_TEST = "test"; /** 项目基础包名称 */ public static final String BASE_PACKAGE = "com.msy.plus"; /** Entity 所在包 */ public static final String ENTITY_PACKAGE = BASE_PACKAGE + ".entity"; /** Mapper 所在包 */ public static final String MAPPER_PACKAGE = BASE_PACKAGE + ".mapper"; /** Filter 所在包 */ public static final String FILTER_PACKAGE = BASE_PACKAGE + ".filter"; /** Service 所在包 */ public static final String SERVICE_PACKAGE = BASE_PACKAGE + ".service"; /** ServiceImpl 所在包 */ public static final String SERVICE_IMPL_PACKAGE = SERVICE_PACKAGE + ".impl"; /** Controller 所在包 */ public static final String CONTROLLER_PACKAGE = BASE_PACKAGE + ".controller"; /** Mapper 插件基础接口的完全限定名 */ public static final String MAPPER_INTERFACE_REFERENCE = BASE_PACKAGE + ".core.mapper.MyMapper"; } ================================================ FILE: back/src/main/java/com/msy/plus/core/dto/AbstractConverter.java ================================================ package com.msy.plus.core.dto; import com.google.common.base.Converter; import org.springframework.beans.BeanUtils; import javax.annotation.ParametersAreNonnullByDefault; import java.lang.reflect.ParameterizedType; /** * DTO -> DO 抽象转换器 * * @author MoShuying * @date 2018/11/28 */ public abstract class AbstractConverter extends Converter { private final Class doClass; private final DTO theDTO = this.setDTO(); private DO theDO; public AbstractConverter() { final ParameterizedType parameterizedType = (ParameterizedType) this.getClass().getGenericSuperclass(); this.doClass = (Class) parameterizedType.getActualTypeArguments()[1]; } /** * 设置 DTO * * @return DTO */ protected abstract DTO setDTO(); @Override @ParametersAreNonnullByDefault public DO doForward(final DTO theDTO) { BeanUtils.copyProperties(theDTO, this.theDO); return this.theDO; } @Override @ParametersAreNonnullByDefault public DTO doBackward(final DO theDO) { BeanUtils.copyProperties(theDO, this.theDTO); return this.theDTO; } public DO convertToDO() { try { this.theDO = this.doClass.getDeclaredConstructor().newInstance(); return this.convert(this.theDTO); } catch (final Exception ignored) { return null; } } public DTO convertFor(final DO aDO) { return this.reverse().convert(aDO); } } ================================================ FILE: back/src/main/java/com/msy/plus/core/exception/ExceptionResolver.java ================================================ package com.msy.plus.core.exception; import com.msy.plus.core.response.Result; import com.msy.plus.core.response.ResultCode; import com.msy.plus.core.response.ResultGenerator; import com.msy.plus.util.UrlUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.dao.DataAccessException; import org.springframework.http.HttpStatus; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.servlet.NoHandlerFoundException; import javax.servlet.http.HttpServletRequest; import javax.validation.ConstraintViolation; import javax.validation.ConstraintViolationException; import java.sql.SQLException; import java.util.stream.Collectors; /** * 统一异常处理 * * 对于业务异常:返回头 Http 状态码一律使用500,避免浏览器缓存,在响应 Result 中指明异常的状态码 code * * @author MoShuying * @date 2018/06/09 */ @Slf4j @RestControllerAdvice public class ExceptionResolver { @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ExceptionHandler(ConstraintViolationException.class) public Result validatorException(final ConstraintViolationException e) { final String msg = e.getConstraintViolations().stream() .map(ConstraintViolation::getMessage) .collect(Collectors.joining(",")); // e.toString 多了不需要用户知道的属性路径 ExceptionResolver.log.error("==> 验证实体异常: {}", e.toString()); e.printStackTrace(); return ResultGenerator.genFailedResult(ResultCode.VIOLATION_EXCEPTION, msg); } @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ExceptionHandler({ServiceException.class}) public Result serviceException(final ServiceException e) { ExceptionResolver.log.error("==> 服务异常: {}", e.getMessage()); e.printStackTrace(); return ResultGenerator.genFailedResult(e.getResultCode(), e.getMessage()); } @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ExceptionHandler({ResourcesNotFoundException.class}) public Result resourcesException(final Throwable e) { ExceptionResolver.log.error("==> 资源异常: {}", e.getMessage()); e.printStackTrace(); return ResultGenerator.genFailedResult(ResultCode.FIND_FAILED); } @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ExceptionHandler({SQLException.class, DataAccessException.class}) public Result databaseException(final Throwable e) { ExceptionResolver.log.error("==> 数据库异常: {}", e.getMessage()); e.printStackTrace(); return ResultGenerator.genFailedResult(ResultCode.DATABASE_EXCEPTION); } @ResponseStatus(HttpStatus.UNAUTHORIZED) @ExceptionHandler({BadCredentialsException.class, AuthenticationException.class}) public Result authException(final Throwable e) { ExceptionResolver.log.error("==> 身份验证异常: {}", e.getMessage()); e.printStackTrace(); return ResultGenerator.genFailedResult(ResultCode.UNAUTHORIZED_EXCEPTION); } @ResponseStatus(HttpStatus.FORBIDDEN) @ExceptionHandler({AccessDeniedException.class, UsernameNotFoundException.class}) public Result accountException(final Throwable e) { ExceptionResolver.log.error("==> 账户异常: {}", e.getMessage()); e.printStackTrace(); return ResultGenerator.genFailedResult(e.getMessage()); } @ResponseStatus(HttpStatus.NOT_FOUND) @ExceptionHandler(NoHandlerFoundException.class) public Result apiNotFoundException(final Throwable e, final HttpServletRequest request) { ExceptionResolver.log.error("==> API不存在: {}", e.getMessage()); e.printStackTrace(); return ResultGenerator.genFailedResult( "API [" + UrlUtils.getMappingUrl(request) + "] not existed"); } @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ExceptionHandler(Exception.class) public Result globalException(final HttpServletRequest request, final Throwable e) { ExceptionResolver.log.error("==> 全局异常: {}", e.getMessage()); e.printStackTrace(); return ResultGenerator.genFailedResult( HttpStatus.INTERNAL_SERVER_ERROR.value(), String.format("%s => %s", UrlUtils.getMappingUrl(request), e.getMessage())); } } ================================================ FILE: back/src/main/java/com/msy/plus/core/exception/ResourcesNotFoundException.java ================================================ package com.msy.plus.core.exception; /** * 资源没找到异常(更新和删除都需先确认存在才操作) * * @author MoShuying * @date 2018/07/20 */ public class ResourcesNotFoundException extends RuntimeException { private static final long serialVersionUID = -4770095291206546216L; private static final String DEFAULT_MESSAGE = "资源不存在"; public ResourcesNotFoundException() { super(ResourcesNotFoundException.DEFAULT_MESSAGE); } public ResourcesNotFoundException(final String message) { super(message); } public ResourcesNotFoundException(final String message, final Throwable cause) { super(message, cause); } } ================================================ FILE: back/src/main/java/com/msy/plus/core/exception/RsaException.java ================================================ package com.msy.plus.core.exception; /** * Rsa 异常 * * @author MoShuying * @date 2018/05/27 */ public class RsaException extends RuntimeException { private static final long serialVersionUID = 5010582133829256626L; private static final String DEFAULT_MESSAGE = "Rsa 异常"; public RsaException() { super(RsaException.DEFAULT_MESSAGE); } public RsaException(final String message) { super(message); } public RsaException(final String message, final Throwable cause) { super(message, cause); } } ================================================ FILE: back/src/main/java/com/msy/plus/core/exception/ServiceException.java ================================================ package com.msy.plus.core.exception; import com.msy.plus.core.response.ResultCode; /** * Service 异常 * * @author MoShuying * @date 2018/05/27 */ public class ServiceException extends RuntimeException { private static final long serialVersionUID = 770293933438435163L; private ResultCode resultCode; public ServiceException(final String message) { super(message); } public ServiceException(final String message, final Throwable cause) { super(message, cause); } public ServiceException(final ResultCode resultCode, final String message) { super(message); this.resultCode = resultCode; } public ServiceException(final ResultCode resultCode) { super(resultCode.getReason()); this.resultCode = resultCode; } public ResultCode getResultCode() { return this.resultCode; } public void setResultCode(final ResultCode resultCode) { this.resultCode = resultCode; } } ================================================ FILE: back/src/main/java/com/msy/plus/core/exception/UsernameNotFoundException2.java ================================================ package com.msy.plus.core.exception; import org.springframework.security.core.userdetails.UsernameNotFoundException; /** * 用户名没找到异常 * * @author MoShuying * @date 2020/11/12 */ public class UsernameNotFoundException2 extends UsernameNotFoundException { private static final long serialVersionUID = 90476943748478489L; private static final String DEFAULT_MESSAGE = "账户名不存在"; public UsernameNotFoundException2() { super(UsernameNotFoundException2.DEFAULT_MESSAGE); } public UsernameNotFoundException2(final String message) { super(message); } public UsernameNotFoundException2(final String message, final Throwable cause) { super(message, cause); } } ================================================ FILE: back/src/main/java/com/msy/plus/core/exception/YamlNotFoundException.java ================================================ package com.msy.plus.core.exception; /** * Yml配置没找到异常 * * @author MoShuying * @date 2020/11/12 */ public class YamlNotFoundException extends RuntimeException { private static final long serialVersionUID = 7081775224645592074L; private static final String DEFAULT_MESSAGE = "Yml不存在"; public YamlNotFoundException() { super(YamlNotFoundException.DEFAULT_MESSAGE); } public YamlNotFoundException(final String message) { super(message); } public YamlNotFoundException(final String message, final Throwable cause) { super(message, cause); } } ================================================ FILE: back/src/main/java/com/msy/plus/core/jasypt/MyEncryptablePropertyDetector.java ================================================ package com.msy.plus.core.jasypt; import com.ulisesbocchio.jasyptspringboot.EncryptablePropertyDetector; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; /** * 自定义被加密值的发现器 默认:ENC(abc) 自定义:MyEnc({abc}) * *

https://github.com/ulisesbocchio/jasypt-spring-boot#provide-a-custom-encryptablepropertydetector * *

如果只是单纯想让前后缀不同,可以直接配置前后缀属性: * *

https://github.com/ulisesbocchio/jasypt-spring-boot#provide-a-custom-encrypted-property-prefix-and-suffix * *

jasypt.encryptor.property.prefix=TEST( * *

jasypt.encryptor.property.suffix=) * * @author MoShuying * @date 2018/07/20 */ @Component public class MyEncryptablePropertyDetector implements EncryptablePropertyDetector { /** 前缀 */ private static final String PREFIX = "MyEnc({"; /** 后缀 */ private static final String SUFFIX = "})"; @Override public boolean isEncrypted(final String property) { if (StringUtils.isBlank(property)) { return false; } final String trimmedProperty = property.trim(); return trimmedProperty.startsWith(MyEncryptablePropertyDetector.PREFIX) && trimmedProperty.endsWith(MyEncryptablePropertyDetector.SUFFIX); } @Override public String unwrapEncryptedValue(final String property) { return property.substring( MyEncryptablePropertyDetector.PREFIX.length(), property.length() - MyEncryptablePropertyDetector.SUFFIX.length()); } } ================================================ FILE: back/src/main/java/com/msy/plus/core/jwt/JwtConfigurationProperties.java ================================================ package com.msy.plus.core.jwt; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import java.time.Duration; /** * Json web token 配置 * * @author MoShuying * @date 2018/06/09 */ @Data @Component @ConfigurationProperties(prefix = "jwt") public class JwtConfigurationProperties { /** claim authorities key */ private String claimKeyAuth; /** token 前缀 */ private String tokenType; /** 请求头或请求参数的key */ private String header; /** 有效期 */ private Duration expireTime; } ================================================ FILE: back/src/main/java/com/msy/plus/core/jwt/JwtUtil.java ================================================ package com.msy.plus.core.jwt; import com.msy.plus.core.rsa.RsaUtils; import com.msy.plus.util.RedisUtils; import io.jsonwebtoken.*; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.stereotype.Component; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import java.security.PublicKey; import java.time.Duration; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.Optional; import java.util.function.Supplier; import java.util.stream.Collectors; /** * Json web token 工具 验证、生成 token * * @author MoShuying * @date 2018/05/27 */ @Slf4j @Component public class JwtUtil { @Resource private JwtConfigurationProperties jwtProperties; @Resource private RedisUtils redisUtils; @Resource private RsaUtils rsaUtils; public JwtConfigurationProperties getJwtProperties(){ return this.jwtProperties; } /** 根据 token 得到账户名 */ public Optional getName(final String token) { final Optional claims = this.parseToken(token); return claims.map(Claims::getSubject); } public Optional getId(final String token){ final Optional claims = this.parseToken(token); return claims.map(Claims::getId); } /** * 签发 token * * @param name 账户名 * @param grantedAuthorities 账户权限信息[ADMIN, TEST, ...] */ public String sign( final String name, final Collection grantedAuthorities,Long id) { // 函数式创建 token,避免重复书写 final Supplier createToken = () -> this.createToken(name, grantedAuthorities,id); // 看看缓存有没有账户token final String token = (String) this.redisUtils.getValue(name); // 没有登录过 if (StringUtils.isBlank(token)) { return createToken.get(); } final boolean isValidate = (boolean) this.redisUtils.getValue(token); // 有 token,仍有效,将 token 置为无效,并重新签发(防止 token 被利用) if (isValidate) { this.invalidRedisToken(name); } // 重新签发 return createToken.get(); } /** * 清除账户在 Redis 中缓存的 token * * @param name 账户名 */ public void invalidRedisToken(final String name) { // 将 token 设置为无效 Object value = this.redisUtils.getValue(name); if(value==null){ return; } Optional.ofNullable((String) value).ifPresent(_token -> this.redisUtils.setValue(_token, false)); } /** 从请求头或请求参数中获取 token */ public String getTokenFromRequest(final HttpServletRequest httpRequest) { final String header = this.jwtProperties.getHeader(); final String token = httpRequest.getHeader(header); return StringUtils.isNotBlank(token) ? token : httpRequest.getParameter(header); } /** 返回账户认证 */ public UsernamePasswordAuthenticationToken getAuthentication( final String name, final String token) { // 解析 token 的 payload final Optional claims = this.parseToken(token); final String claimKeyAuth = this.jwtProperties.getClaimKeyAuth(); // 账户角色列表 final String[] auths = claims.map(c -> c.get(claimKeyAuth).toString().split(",")).orElse(new String[0]); // 将元素转换为 GrantedAuthority 接口集合 final Collection authorities = Arrays.stream(auths).map(SimpleGrantedAuthority::new).collect(Collectors.toList()); final User user = new User(name, "", authorities); return new UsernamePasswordAuthenticationToken(user, null, authorities); } /** 验证 token 是否正确 */ public boolean validateToken(final String token) { boolean isValidate = true; final Object redisTokenValidate = this.redisUtils.getValue(token); // 可能 redis 部署出现了问题 // 或者清空了缓存导致 token 键不存在 if (redisTokenValidate != null) { isValidate = (boolean) redisTokenValidate; } // 能正确解析 token,并且 redis 中缓存的 token 也是有效的 return this.parseToken(token).isPresent() && isValidate; } /** 生成 token */ private String createToken( final String name, final Collection grantedAuthorities, Long id) { // 获取账户的角色字符串,如 USER,ADMIN final String authorities = grantedAuthorities.stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.joining(",")); JwtUtil.log.debug("==> Account<{}> authorities: {}", name, authorities); // 过期时间 final Duration expireTime = this.jwtProperties.getExpireTime(); // 当前时间 + 有效时长 final Date expireDate = new Date(System.currentTimeMillis() + expireTime.toMillis()); // 创建 token,比如 "Bearer abc1234" final String token = this.jwtProperties.getTokenType() + " " + Jwts.builder() // 设置账户名 .setSubject(name) .setId(id.toString()) // 添加权限属性 .claim(this.jwtProperties.getClaimKeyAuth(), authorities) // 设置失效时间 .setExpiration(expireDate) // 私钥加密生成签名 .signWith(SignatureAlgorithm.RS256, this.rsaUtils.loadPrivateKey()) // 使用LZ77算法与哈夫曼编码结合的压缩算法进行压缩 .compressWith(CompressionCodecs.DEFLATE) .compact(); // 保存账户 token // 因为账户注销后 JWT 本身只要没过期就仍然有效,所以只能通过 redis 缓存来校验有无效 // 校验时只要 redis 中的 token 无效即可(JWT 本身可以校验有无过期,而 redis 过期即被删除了) // true 有效 this.redisUtils.setValue(token, true, expireTime); // redis 过期时间和 JWT 的一致 this.redisUtils.setValue(name, token, expireTime); JwtUtil.log.debug("==> Redis set Account<{}> token: {}", name, token); return token; } /** 解析 token */ private Optional parseToken(final String token) { Optional claims = Optional.empty(); try { final PublicKey publicKey = this.rsaUtils.loadPublicKey(); claims = Optional.of( Jwts.parser() // 公钥解密 .setSigningKey(publicKey) .parseClaimsJws(token.replace(this.jwtProperties.getTokenType(), "")) .getBody()); } catch (final SignatureException e) { // 签名异常 JwtUtil.log.debug("Invalid JWT signature"); } catch (final MalformedJwtException e) { // 格式错误 JwtUtil.log.debug("Invalid JWT token"); } catch (final ExpiredJwtException e) { // 过期 JwtUtil.log.debug("Expired JWT token"); } catch (final UnsupportedJwtException e) { // 不支持该JWT JwtUtil.log.debug("Unsupported JWT token"); } catch (final IllegalArgumentException e) { // 参数错误异常 JwtUtil.log.debug("JWT token compact of handler are invalid"); } return claims; } } ================================================ FILE: back/src/main/java/com/msy/plus/core/mapper/MyMapper.java ================================================ package com.msy.plus.core.mapper; import tk.mybatis.mapper.common.BaseMapper; import tk.mybatis.mapper.common.ConditionMapper; import tk.mybatis.mapper.common.IdsMapper; import tk.mybatis.mapper.common.special.InsertListMapper; /** * 定制版 MyBatis Mapper 插件接口,如需其他接口参考官方文档自行添加 * * @author MoShuying * @date 2018/05/27 */ public interface MyMapper extends BaseMapper, ConditionMapper, IdsMapper, InsertListMapper {} ================================================ FILE: back/src/main/java/com/msy/plus/core/response/Result.java ================================================ package com.msy.plus.core.response; import com.alibaba.fastjson.JSON; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; /** * @author MoShuying * @date 2018/07/15 */ @ApiModel(value = "响应结果") public class Result { @ApiModelProperty(value = "状态码") private Integer code; @ApiModelProperty(value = "消息") private String message; @ApiModelProperty(value = "数据") private T data; @Override public String toString() { return JSON.toJSONString(this); } public Integer getCode() { return this.code; } public Result setCode(final Integer code) { this.code = code; return this; } public String getMessage() { return this.message; } public Result setMessage(final String message) { this.message = message; return this; } public T getData() { return this.data; } public Result setData(final T data) { this.data = data; return this; } } ================================================ FILE: back/src/main/java/com/msy/plus/core/response/ResultCode.java ================================================ package com.msy.plus.core.response; /** * 响应状态码枚举类 * *

自定义业务异常 2*** 开始 * *

原有类异常 4*** 开始 * * @author MoShuying * @date 2018/07/14 */ public enum ResultCode { /** 成功请求,但结果不是期望的成功结果 */ SUCCEED_REQUEST_FAILED_RESULT(1000, "成功请求,但结果不是期望的成功结果"), /** 查询失败 */ FIND_FAILED(2000, "查询失败"), /** 保存失败 */ SAVE_FAILED(2001, "保存失败"), /** 更新失败 */ UPDATE_FAILED(2002, "更新失败"), /** 删除失败 */ DELETE_FAILED(2003, "删除失败"), /** 账户名重复 */ DUPLICATE_NAME(2004, "账户名重复"), /** 数据库异常 */ DATABASE_EXCEPTION(4001, "数据库异常"), /** 认证异常 */ UNAUTHORIZED_EXCEPTION(4002, "认证异常"), /** 验证异常 */ VIOLATION_EXCEPTION(4003, "验证异常"); private final int value; private final String reason; ResultCode(final int value, final String reason) { this.value = value; this.reason = reason; } public int getValue() { return this.value; } public String getReason() { return this.reason; } public String format(final Object... objects) { return objects.length > 0 ? String.format(this.getReason(), objects) : this.getReason(); } } ================================================ FILE: back/src/main/java/com/msy/plus/core/response/ResultGenerator.java ================================================ package com.msy.plus.core.response; import org.springframework.http.HttpStatus; /** * 响应结果生成工具 * * @author MoShuying * @date 2018/06/09 */ public class ResultGenerator { /** * 成功响应结果 * * @param data 内容 * @return 响应结果 */ public static Result genOkResult(final T data) { return new Result().setCode(HttpStatus.OK.value()).setData(data); } /** * 成功响应结果 * * @return 响应结果 */ public static Result genOkResult() { return ResultGenerator.genOkResult(null); } /** * 失败响应结果 * * @param code 状态码 * @param message 消息 * @return 响应结果 */ public static Result genFailedResult( final int code, final String message, final Object... objects) { return new Result().setCode(code).setMessage(String.format(message, objects)); } /** * 失败响应结果 * * @param resultCode 状态码枚举 * @return 响应结果 */ public static Result genFailedResult(final ResultCode resultCode) { return ResultGenerator.genFailedResult(resultCode.getValue(), resultCode.getReason()); } /** * 失败响应结果 * * @param resultCode 状态码枚举 * @param message 消息 * @return 响应结果 */ public static Result genFailedResult( final ResultCode resultCode, final String message, final Object... objects) { return ResultGenerator.genFailedResult(resultCode.getValue(), message, objects); } /** * 失败响应结果 * * @param message 消息 * @return 响应结果 */ public static Result genFailedResult(final String message, final Object... objects) { return ResultGenerator.genFailedResult( ResultCode.SUCCEED_REQUEST_FAILED_RESULT.getValue(), message, objects); } /** * 失败响应结果 * * @return 响应结果 */ public static Result genFailedResult() { return ResultGenerator.genFailedResult(ResultCode.SUCCEED_REQUEST_FAILED_RESULT); } } ================================================ FILE: back/src/main/java/com/msy/plus/core/rsa/RsaConfigurationProperties.java ================================================ package com.msy.plus.core.rsa; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; /** * RSA 配置 * * @author MoShuying * @date 2019/08/12 */ @Data @Component @ConfigurationProperties(prefix = "rsa") public class RsaConfigurationProperties { /** 公钥头 */ private String publicKeyHead = "-----BEGIN PUBLIC KEY-----"; /** 公钥尾 */ private String publicKeyTail = "-----END PUBLIC KEY-----"; /** 私钥头 */ private String privateKeyHead = "-----BEGIN PRIVATE KEY-----"; /** 私钥尾 */ private String privateKeyTail = "-----END PRIVATE KEY-----"; /** 公钥位置,默认在 rsa 文件夹下 */ private String publicKeyPath = "classpath:rsa/public-key.pem"; /** 私钥位置,默认在 rsa 文件夹下 */ private String privateKeyPath = "classpath:rsa/private-key.pem"; /** 使用文件还是直接使用字符串,默认使用字符串 */ private boolean useFile = false; /** 私钥 */ private String privateKey; /** 公钥 */ private String publicKey; } ================================================ FILE: back/src/main/java/com/msy/plus/core/rsa/RsaUtils.java ================================================ package com.msy.plus.core.rsa; import com.msy.plus.core.exception.RsaException; import lombok.extern.slf4j.Slf4j; import org.apache.commons.codec.binary.Base64; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.stereotype.Component; import org.springframework.util.FileCopyUtils; import javax.crypto.Cipher; import java.io.IOException; import java.security.*; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.Optional; /** * RSA 工具 * *

用openssl生成512位RSA: * *

生成私钥: openssl genrsa -out key.pem 512 * *

从私钥中导出公钥: openssl rsa -in key.pem -pubout -out public-key.pem * *

公钥加密: openssl rsautl -encrypt -in xx.file -inkey public-key.pem -pubin -out xx.en * *

私钥解密: openssl rsautl -decrypt -in xx.en -inkey key.pem -out xx.de * *

pkcs8编码(Java): openssl pkcs8 -topk8 -inform PEM -in key.pem -outform PEM -out private-key.pem * -nocrypt * *

最后将公私玥放在/resources/rsa/:private-key.pem public-key.pem * * @author MoShuying * @date 2018/07/20 */ @Slf4j @Component public class RsaUtils { private static final String ALGORITHM = "RSA"; private final ResourceLoader resourceLoader = new DefaultResourceLoader(); @javax.annotation.Resource private RsaConfigurationProperties rsaProperties; public RsaUtils() { if (!Optional.ofNullable(this.rsaProperties).isPresent()) { this.rsaProperties = new RsaConfigurationProperties(); } } /** * 生成密钥对 * * @param keyLength 密钥长度(最少512位) * @return 密钥对 公钥 keyPair.getPublic() 私钥 keyPair.getPrivate() * @throws Exception e */ public static KeyPair genKeyPair(final int keyLength) throws Exception { final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(RsaUtils.ALGORITHM); keyPairGenerator.initialize(keyLength); return keyPairGenerator.generateKeyPair(); } /** * 公钥加密 * * @param content 待加密数据 * @param publicKey 公钥 * @return 加密内容 * @throws Exception e */ public static byte[] encrypt(final byte[] content, final PublicKey publicKey) throws Exception { final Cipher cipher = Cipher.getInstance(RsaUtils.ALGORITHM); cipher.init(Cipher.ENCRYPT_MODE, publicKey); return cipher.doFinal(content); } /** * 私钥解密 * * @param content 加密数据 * @param privateKey 私钥 * @return 解密内容 * @throws Exception e */ public static byte[] decrypt(final byte[] content, final PrivateKey privateKey) throws Exception { final Cipher cipher = Cipher.getInstance(RsaUtils.ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, privateKey); return cipher.doFinal(content); } /** * 公钥加密 * * @param content 待加密数据 * @return 加密内容 * @throws Exception e */ public byte[] encrypt(final byte[] content) throws Exception { return RsaUtils.encrypt(content, this.loadPublicKey()); } /** * 私钥解密 * * @param content 加密数据 * @return 解密内容 * @throws Exception e */ public byte[] decrypt(final byte[] content) throws Exception { return RsaUtils.decrypt(content, this.loadPrivateKey()); } /** * 加载pem格式X509编码的公钥 * * @return 公钥 */ public PublicKey loadPublicKey() throws RsaException { final byte[] decoded; if (this.rsaProperties.isUseFile()) { decoded = this.loadAndReplaceAndDecodeByBase64( this.rsaProperties.getPublicKeyPath(), this.rsaProperties.getPublicKeyHead(), this.rsaProperties.getPublicKeyTail()); } else { decoded = Base64.decodeBase64(this.rsaProperties.getPublicKey()); } final X509EncodedKeySpec spec = new X509EncodedKeySpec(decoded); try { final KeyFactory keyFactory = KeyFactory.getInstance(RsaUtils.ALGORITHM); return keyFactory.generatePublic(spec); } catch (final NoSuchAlgorithmException | InvalidKeySpecException e) { RsaUtils.log.error("==> {}", e.getMessage()); return null; } } /** * 加载pem格式PKCS8编码的私钥 * * @return 私钥 */ public PrivateKey loadPrivateKey() throws RsaException { final byte[] decoded; if (this.rsaProperties.isUseFile()) { decoded = this.loadAndReplaceAndDecodeByBase64( this.rsaProperties.getPrivateKeyPath(), this.rsaProperties.getPrivateKeyHead(), this.rsaProperties.getPrivateKeyTail()); } else { decoded = Base64.decodeBase64(this.rsaProperties.getPrivateKey()); } final PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(decoded); try { final KeyFactory keyFactory = KeyFactory.getInstance(RsaUtils.ALGORITHM); return keyFactory.generatePrivate(spec); } catch (final NoSuchAlgorithmException | InvalidKeySpecException e) { RsaUtils.log.error("==> {}", e.getMessage()); return null; } } /** * 加载文件后替换头和尾并解密 * * @return 文件字节 */ private byte[] loadAndReplaceAndDecodeByBase64( final String keyPath, final String headReplace, final String tailReplace) throws RsaException { final Resource resource = Optional.ofNullable(keyPath).map(this.resourceLoader::getResource).get(); if (!resource.exists()) { throw new RsaException("==> 密钥不存在"); } try { final String key = new String(FileCopyUtils.copyToByteArray(resource.getInputStream())); return Base64.decodeBase64( key.replace(headReplace, "").trim().replace(tailReplace, "").trim()); } catch (final IOException e) { throw new RsaException("==> 密钥读取异常: {}", e); } } } ================================================ FILE: back/src/main/java/com/msy/plus/core/service/AbstractService.java ================================================ package com.msy.plus.core.service; import com.msy.plus.core.exception.ResourcesNotFoundException; import com.msy.plus.core.exception.ServiceException; import com.msy.plus.core.mapper.MyMapper; import com.msy.plus.core.response.ResultCode; import com.msy.plus.util.AssertUtils; import org.apache.ibatis.exceptions.TooManyResultsException; import org.springframework.beans.factory.annotation.Autowired; import tk.mybatis.mapper.entity.Condition; import java.lang.reflect.Field; import java.lang.reflect.ParameterizedType; import java.util.List; import java.util.Optional; /** * 基于通用 MyBatis Mapper 插件的 Service 接口的实现 * * @author MoShuying * @date 2018/05/27 */ public abstract class AbstractService implements Service { /** 当前泛型的实体 Class */ private final Class entityClass; @Autowired protected MyMapper mapper; protected AbstractService() { final ParameterizedType pt = (ParameterizedType) this.getClass().getGenericSuperclass(); //noinspection unchecked this.entityClass = (Class) pt.getActualTypeArguments()[0]; } private static void assertSave(final boolean statement) { AssertUtils.asserts(statement, ResultCode.SAVE_FAILED); } private static void assertDelete(final boolean statement) { AssertUtils.asserts(statement, ResultCode.DELETE_FAILED); } private static void assertUpdate(final boolean statement) { AssertUtils.asserts(statement, ResultCode.UPDATE_FAILED); } private T getEntity(final String fieldName, final Object value) throws Exception { final T entity = this.entityClass.getDeclaredConstructor().newInstance(); final Field field = this.entityClass.getDeclaredField(fieldName); field.setAccessible(true); field.set(entity, value); return entity; } @Override public void assertById(final Object id) { Optional.ofNullable(this.mapper.selectByPrimaryKey(id)) .orElseThrow(ResourcesNotFoundException::new); } @Override public void assertBy(final T entity) { Optional.ofNullable(this.mapper.select(entity)).orElseThrow(ResourcesNotFoundException::new); } @Override public void assertByIds(final String ids) { final int count = this.countByIds(ids); // id数和列表数不对应 if (ids.split(",").length > count) { throw new ResourcesNotFoundException(); } } @Override public int countByIds(final String ids) { return this.mapper.selectByIds(ids).size(); } @Override public int countByCondition(final Condition condition) { return this.mapper.selectByCondition(condition).size(); } @Override public void save(final T entity) { AbstractService.assertSave(this.mapper.insertSelective(entity) == 1); } @Override public void save(final List entities) { AbstractService.assertSave(this.mapper.insertList(entities) == entities.size()); } @Override public void deleteById(final Object id) { this.assertById(id); AbstractService.assertDelete(this.mapper.deleteByPrimaryKey(id) == 1); } @Override public void deleteBy(final String fieldName, final Object value) throws TooManyResultsException { try { final T entity = this.getEntity(fieldName, value); this.assertBy(entity); AbstractService.assertDelete(this.mapper.delete(entity) == 1); } catch (final Exception e) { throw new ServiceException(e.getMessage(), e); } } @Override public void deleteByIds(final String ids) { this.assertByIds(ids); AbstractService.assertDelete(this.mapper.deleteByIds(ids) == ids.split(",").length); } @Override public void deleteByCondition(final Condition condition) { final int count = this.countByCondition(condition); AbstractService.assertDelete(this.mapper.deleteByCondition(condition) == count); } @Override public void update(final T entity) { AbstractService.assertUpdate(this.mapper.updateByPrimaryKeySelective(entity) == 1); } @Override public void updateByCondition(final T entity, final Condition condition) { AbstractService.assertUpdate(this.mapper.updateByConditionSelective(entity, condition) == 1); } @Override public T getById(final Object id) { return this.mapper.selectByPrimaryKey(id); } @Override public T getBy(final String fieldName, final Object value) throws TooManyResultsException { try { final T entity = this.getEntity(fieldName, value); return this.mapper.selectOne(entity); } catch (final Exception e) { throw new ServiceException(e.getMessage(), e); } } @Override public List listByIds(final String ids) { return this.mapper.selectByIds(ids); } @Override public List listByCondition(final Condition condition) { return this.mapper.selectByCondition(condition); } @Override public List listAll() { return this.mapper.selectAll(); } } ================================================ FILE: back/src/main/java/com/msy/plus/core/service/Service.java ================================================ package com.msy.plus.core.service; import com.msy.plus.core.exception.ResourcesNotFoundException; import org.apache.ibatis.exceptions.TooManyResultsException; import tk.mybatis.mapper.entity.Condition; import java.util.List; /** * Service 层基础接口 * * @author MoShuying * @date 2018/05/27 */ public interface Service { /** * 确保实体存在 * * @param id 实体id * @throws ResourcesNotFoundException 不存在实体异常 */ void assertById(Object id); /** * 确保实体存在 * * @param entity 实体 * @throws ResourcesNotFoundException 不存在实体异常 */ void assertBy(T entity); /** * 确保实体存在 * * @param ids ids */ void assertByIds(String ids); /** * 根据 ids 获取实体数 * * @param ids ids */ int countByIds(String ids); /** * 根据条件获取实体数 * * @param condition 条件 */ int countByCondition(Condition condition); /** * 持久化 * * @param entity 实体 */ void save(T entity); /** * 批量持久化 * * @param entities 实体列表 */ void save(List entities); /** * 通过主鍵刪除 * * @param id id */ void deleteById(Object id); /** * 通过实体中某个成员变量名称(非数据表中 column 的名称)刪除 * * @param fieldName 字段名 * @param value 字段值 * @throws TooManyResultsException 多条结果异常 */ void deleteBy(String fieldName, Object value) throws TooManyResultsException; /** * 批量刪除 ids -> “1,2,3,4” * * @param ids ids */ void deleteByIds(String ids); /** * 根据条件刪除 * * @param condition 条件 */ void deleteByCondition(Condition condition); /** * 按组件更新 * * @param entity 实体 */ void update(T entity); /** * 按条件更新 * * @param entity 实体 * @param condition 条件 */ void updateByCondition(T entity, Condition condition); /** * 通过 id 查找 * * @param id id * @return 实体 */ T getById(Object id); /** * 通过实体中某个成员变量名称查找 value 需符合 unique 约束 * * @param fieldName 字段名 * @param value 字段值 * @return 实体 * @throws TooManyResultsException 多条结果异常 */ T getBy(String fieldName, Object value) throws TooManyResultsException; /** * 通过多个 id 查找 ids -> “1,2,3,4” * * @param ids ids * @return 实体列表 */ List listByIds(String ids); /** * 按条件查找 * * @param condition 条件 * @return 实体列表 */ List listByCondition(Condition condition); /** * 获取所有实体 * * @return 实体列表 */ List listAll(); } ================================================ FILE: back/src/main/java/com/msy/plus/core/upload/UploadConfigurationProperties.java ================================================ package com.msy.plus.core.upload; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import org.springframework.util.unit.DataSize; /** * 上传配置 * * @author MoShuying * @date 2019/08/13 */ @Data @Component @ConfigurationProperties(prefix = "upload") public class UploadConfigurationProperties { /** 本地路径 */ private String localPath; /** 最小 */ private DataSize min; /** 最大 */ private DataSize max; } ================================================ FILE: back/src/main/java/com/msy/plus/dto/AccountDTO.java ================================================ package com.msy.plus.dto; import com.msy.plus.core.dto.AbstractConverter; import com.msy.plus.entity.AccountDO; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import javax.validation.constraints.Email; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.Size; import java.io.Serializable; /** * @author MoShuying * @date 2018/07/14 */ @Data @Schema(name = "账户传输实体") @EqualsAndHashCode(callSuper = true) public class AccountDTO extends AbstractConverter implements Serializable { private static final long serialVersionUID = 1473352811666797847L; @Schema(name = "账户Id", accessMode = Schema.AccessMode.READ_ONLY) private Long id; @Schema(name = "邮箱", example = "123@qq.com") @Email(message = "邮箱格式不正确") private String email; @Schema(name = "账户名", accessMode = Schema.AccessMode.READ_ONLY, example = "admin") @NotEmpty(message = "账户名不能为空") @Size(min = 1, message = "账户名长度不能小于1") private String name; @Schema(name = "密码", example = "admin") @NotEmpty(message = "密码不能为空") @Size(min = 5, message = "密码长度不能小于5") private String password; @Override protected AccountDTO setDTO() { return this; } } ================================================ FILE: back/src/main/java/com/msy/plus/dto/AccountLoginDTO.java ================================================ package com.msy.plus.dto; import com.msy.plus.core.dto.AbstractConverter; import com.msy.plus.entity.AccountDO; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.Size; import java.io.Serializable; /** * @author MoShuying * @date 2018/07/15 */ @Data @Schema(name = "账户登录传输实体") @EqualsAndHashCode(callSuper = true) public class AccountLoginDTO extends AbstractConverter implements Serializable { private static final long serialVersionUID = 1945186812588516555L; @Schema(name = "账户名", example = "admin") @NotEmpty(message = "账户名不能为空") @Size(min = 1, message = "账户名长度不能小于1") private String name; @Schema(name = "密码", example = "admin") @NotEmpty(message = "密码不能为空") @Size(min = 5, message = "密码长度不能小于5") private String password; @Override protected AccountLoginDTO setDTO() { return this; } } ================================================ FILE: back/src/main/java/com/msy/plus/dto/AnalysisQuery.java ================================================ package com.msy.plus.dto; import lombok.*; import org.springframework.format.annotation.DateTimeFormat; import java.util.Date; @Getter @Setter @NoArgsConstructor @AllArgsConstructor @ToString public class AnalysisQuery { private Integer page=1; private Integer size=10; private String name=""; @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) private Date startTime; @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) private Date endTime; /** * groupType * 1 员工 * 2 年 * 3 月 * 4 日 */ private int groupType; } ================================================ FILE: back/src/main/java/com/msy/plus/dto/CustomerHandoverList.java ================================================ package com.msy.plus.dto; import lombok.Getter; import lombok.Setter; import java.util.Date; @Getter @Setter public class CustomerHandoverList { private Integer id; private String customerName; private Date transTime; private String transUser; private String oldSeller; private String newSeller; private String transReason; } ================================================ FILE: back/src/main/java/com/msy/plus/dto/CustomerManagerList.java ================================================ package com.msy.plus.dto; import lombok.Getter; import lombok.Setter; import javax.persistence.Column; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import java.util.Date; @Getter @Setter public class CustomerManagerList { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; /** * 客户姓名 */ private String name; /** * 客户年龄 */ private Integer age; /** * 客户性别 页面为下拉框 */ private Integer gender; /** * 电话号码 */ private String tel; private String qq; private String job; /** * 客户来源 */ private String source; /** * 负责人 填写为当前登录用户 */ private Integer seller; /** * 创建人 填写为当前登录用户 */ @Column(name = "inputUser") private String inputuser; private String inputUserId; @Column(name = "inputTime") private Date inputtime; /** * -2:流失 -1:开发失败 0:潜在客户 1:正式客户 2:资源池客户 */ private Integer status; /** * 转正时间 */ @Column(name = "positiveTime") private Date positivetime; } ================================================ FILE: back/src/main/java/com/msy/plus/dto/LoginResultDTO.java ================================================ package com.msy.plus.dto; import com.msy.plus.core.dto.AbstractConverter; import com.msy.plus.entity.LoginResultDO; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import java.io.Serializable; import java.util.*; @Getter @Setter @Data @Schema(name = "登录传输实体") @EqualsAndHashCode(callSuper = true) public class LoginResultDTO extends AbstractConverter implements Serializable { private static final long serialVersionUID = -12322384324L; @Schema(name = "账户令牌") private String token; @Schema(name = "过期时间") private Date expireAt; private List permissions = new ArrayList<>(); private List roles = new ArrayList<>(); @Schema(name = "用户信息") private Map user = new HashMap<>(); @Schema(name = "用户登录成功的提示") private String message = "欢迎回来"; public void setUserName(String name) { this.getUser().put("name", name); } public LoginResultDTO() { // 注入虚拟数据 Map permissions = new HashMap<>(); permissions.put("id", "queryForm"); permissions.put("operation", new String[]{"add", "edit"}); this.permissions.add(permissions); this.user.put("address", "贺州市"); this.user.put("avatar", "https://gw.alipayobjects.com/zos/rmsportal/WhxKECPNujWoWEFNdnJE.png"); Map position = new HashMap<>(); position.put("CN", "前端工程师 | 蚂蚁金服-计算服务事业群-REACT平台"); position.put("HK", "前端工程師 | 螞蟻金服-計算服務事業群-REACT平台"); position.put("US", "Front-end engineer | Ant Financial - Computing services business group - REACT platform"); this.user.put("position", position); // Date date = new Date(); //取时间 // Calendar calendar = new GregorianCalendar(); // calendar.setTime(date); //// calendar.add(calendar.DATE, 1); //把日期往后增加一天,整数 往后推,负数往前移动 // calendar.add(calendar.HOUR, 4); // this.expireAt = calendar.getTime().toString(); } @Override protected LoginResultDTO setDTO() { return this; } } ================================================ FILE: back/src/main/java/com/msy/plus/dto/RoleDTO.java ================================================ package com.msy.plus.dto; import com.msy.plus.core.dto.AbstractConverter; import com.msy.plus.entity.RoleDO; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import javax.persistence.Column; import java.io.Serializable; /** * @author MoShuying * @date 2018/07/15 */ @Data @Schema(name = "角色传输实体") @EqualsAndHashCode(callSuper = true) public class RoleDTO extends AbstractConverter implements Serializable { private static final long serialVersionUID = -145221735177809163L; @Schema(name = "角色Id", accessMode = Schema.AccessMode.READ_ONLY) private Long id; @Schema(name = "角色名称") private String name; /** 角色编号 */ @Column(name = "sn") private String sn; @Override protected RoleDTO setDTO() { return this; } } ================================================ FILE: back/src/main/java/com/msy/plus/dto/RoleWithPermissionDTO.java ================================================ package com.msy.plus.dto; import com.msy.plus.entity.Permission; import com.msy.plus.entity.RoleWithPermissionDO; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import javax.persistence.Column; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.validation.constraints.NotEmpty; import java.util.List; @Getter @Setter @Data @Schema(name = "角色及权限传输实体") @EqualsAndHashCode(callSuper = true) public class RoleWithPermissionDTO extends RoleWithPermissionDO { private static final long serialVersionUID = -123223812341212L; /** 角色Id */ @NotEmpty(message = "ID不能为空") @Schema(name = "角色Id", accessMode = Schema.AccessMode.READ_ONLY) private Long id; /** 角色名称 */ @Schema(name = "角色名称") private String name; /** 角色编号 */ @Schema(name = "角色编号") private String sn; /** 角色权限 */ @Schema(name = "角色权限列表") private List permissions; } ================================================ FILE: back/src/main/java/com/msy/plus/entity/AccountDO.java ================================================ package com.msy.plus.entity; import lombok.Data; import javax.persistence.*; import java.sql.Timestamp; /** * @author MoShuying * @date 2018/05/27 */ @Data @Table(name = "employee") public class AccountDO { /** 账户Id */ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") private Long id; /** 邮箱 */ @Column(name = "email") private String email; /** 账户名 */ @Column(name = "name") private String name; /** 密码 */ @Column(name = "password") private String password; /** 注册时间 */ @Column(name = "register_time") private Timestamp registerTime; /** 上一次登录时间 */ @Column(name = "login_time") private Timestamp loginTime; } ================================================ FILE: back/src/main/java/com/msy/plus/entity/AccountWithRoleDO.java ================================================ package com.msy.plus.entity; import lombok.Data; import lombok.EqualsAndHashCode; import java.util.List; /** * 包含角色信息的账户实体 * * @author MoShuying * @date 2018/07/15 */ @Data @EqualsAndHashCode(callSuper = false) public class AccountWithRoleDO extends AccountDO { /** 账户的角色列表 */ private List roles; } ================================================ FILE: back/src/main/java/com/msy/plus/entity/Analysis.java ================================================ package com.msy.plus.entity; import lombok.Getter; import lombok.Setter; @Getter @Setter public class Analysis { private String name; private int count; } ================================================ FILE: back/src/main/java/com/msy/plus/entity/CFUHSearch.java ================================================ package com.msy.plus.entity; import lombok.Getter; import lombok.Setter; /** * 客户跟进历史分页查询搜索 */ @Getter @Setter public class CFUHSearch extends CustomerFollowUpHistory{ private String name; } ================================================ FILE: back/src/main/java/com/msy/plus/entity/CustomerFollowUpHistory.java ================================================ package com.msy.plus.entity; import java.util.Date; import javax.persistence.*; @Table(name = "customer_follow_up_history") public class CustomerFollowUpHistory { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; /** * 跟进时间 */ @Column(name = "traceTime") private Date tracetime; /** * 跟进方式 计划采用如电话、邀约上门等 数据字典 */ @Column(name = "traceType") private Integer tracetype; /** * 跟进效果 优----3、中----2、差----1 */ @Column(name = "traceResult") private Integer traceresult; /** * 跟进客户 编辑时不可编辑 潜在客户对象/客户对象 */ @Column(name = "customerID") private Integer customerid; /** * 创建人 自动填入当前登录用户,用户不可更改 员工对象 */ @Column(name = "inputUser") private Integer inputuser; /** * 跟进类型 0:潜在开发计划 1:客户跟进历史 */ private Integer type; /** * 跟进内容 计划的详细内容 */ @Column(name = "traceDetails") private String tracedetails; private String comment; /** * @return id */ public Integer getId() { return id; } /** * @param id */ public void setId(Integer id) { this.id = id; } /** * 获取跟进时间 * * @return traceTime - 跟进时间 */ public Date getTracetime() { return tracetime; } /** * 设置跟进时间 * * @param tracetime 跟进时间 */ public void setTracetime(Date tracetime) { this.tracetime = tracetime; } /** * 获取跟进方式 计划采用如电话、邀约上门等 数据字典 * * @return traceType - 跟进方式 计划采用如电话、邀约上门等 数据字典 */ public Integer getTracetype() { return tracetype; } /** * 设置跟进方式 计划采用如电话、邀约上门等 数据字典 * * @param tracetype 跟进方式 计划采用如电话、邀约上门等 数据字典 */ public void setTracetype(Integer tracetype) { this.tracetype = tracetype; } /** * 获取跟进效果 优----3、中----2、差----1 * * @return traceResult - 跟进效果 优----3、中----2、差----1 */ public Integer getTraceresult() { return traceresult; } /** * 设置跟进效果 优----3、中----2、差----1 * * @param traceresult 跟进效果 优----3、中----2、差----1 */ public void setTraceresult(Integer traceresult) { this.traceresult = traceresult; } /** * 获取跟进客户 编辑时不可编辑 潜在客户对象/客户对象 * * @return customerID - 跟进客户 编辑时不可编辑 潜在客户对象/客户对象 */ public Integer getCustomerid() { return customerid; } /** * 设置跟进客户 编辑时不可编辑 潜在客户对象/客户对象 * * @param customerid 跟进客户 编辑时不可编辑 潜在客户对象/客户对象 */ public void setCustomerid(Integer customerid) { this.customerid = customerid; } /** * 获取创建人 自动填入当前登录用户,用户不可更改 员工对象 * * @return inputUser - 创建人 自动填入当前登录用户,用户不可更改 员工对象 */ public Integer getInputuser() { return inputuser; } /** * 设置创建人 自动填入当前登录用户,用户不可更改 员工对象 * * @param inputuser 创建人 自动填入当前登录用户,用户不可更改 员工对象 */ public void setInputuser(Integer inputuser) { this.inputuser = inputuser; } /** * 获取跟进类型 0:潜在开发计划 1:客户跟进历史 * * @return type - 跟进类型 0:潜在开发计划 1:客户跟进历史 */ public Integer getType() { return type; } /** * 设置跟进类型 0:潜在开发计划 1:客户跟进历史 * * @param type 跟进类型 0:潜在开发计划 1:客户跟进历史 */ public void setType(Integer type) { this.type = type; } /** * 获取跟进内容 计划的详细内容 * * @return traceDetails - 跟进内容 计划的详细内容 */ public String getTracedetails() { return tracedetails; } /** * 设置跟进内容 计划的详细内容 * * @param tracedetails 跟进内容 计划的详细内容 */ public void setTracedetails(String tracedetails) { this.tracedetails = tracedetails; } /** * @return comment */ public String getComment() { return comment; } /** * @param comment */ public void setComment(String comment) { this.comment = comment; } } ================================================ FILE: back/src/main/java/com/msy/plus/entity/CustomerHandover.java ================================================ package com.msy.plus.entity; import java.util.Date; import javax.persistence.*; @Table(name = "customer_handover") public class CustomerHandover { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; /** * 客户 客户对象 */ @Column(name = "customerID") private Integer customerid; /** * 移交人员 实行移交操作的管理人员 */ @Column(name = "transUser") private Integer transuser; @Column(name = "transTime") private Date transtime; /** * 老市场专员 客户上的原始市场人员 */ @Column(name = "oldSeller") private Integer oldseller; /** * 新市场专员 由公司重新指派后的新市场人员 */ @Column(name = "newSeller") private Integer newseller; /** * 移交原因 */ @Column(name = "transReason") private String transreason; /** * @return id */ public Integer getId() { return id; } /** * @param id */ public void setId(Integer id) { this.id = id; } /** * 获取客户 客户对象 * * @return customerID - 客户 客户对象 */ public Integer getCustomerid() { return customerid; } /** * 设置客户 客户对象 * * @param customerid 客户 客户对象 */ public void setCustomerid(Integer customerid) { this.customerid = customerid; } /** * 获取移交人员 实行移交操作的管理人员 * * @return transUser - 移交人员 实行移交操作的管理人员 */ public Integer getTransuser() { return transuser; } /** * 设置移交人员 实行移交操作的管理人员 * * @param transuser 移交人员 实行移交操作的管理人员 */ public void setTransuser(Integer transuser) { this.transuser = transuser; } /** * @return transTime */ public Date getTranstime() { return transtime; } /** * @param transtime */ public void setTranstime(Date transtime) { this.transtime = transtime; } /** * 获取老市场专员 客户上的原始市场人员 * * @return oldSeller - 老市场专员 客户上的原始市场人员 */ public Integer getOldseller() { return oldseller; } /** * 设置老市场专员 客户上的原始市场人员 * * @param oldseller 老市场专员 客户上的原始市场人员 */ public void setOldseller(Integer oldseller) { this.oldseller = oldseller; } /** * 获取新市场专员 由公司重新指派后的新市场人员 * * @return newSeller - 新市场专员 由公司重新指派后的新市场人员 */ public Integer getNewseller() { return newseller; } /** * 设置新市场专员 由公司重新指派后的新市场人员 * * @param newseller 新市场专员 由公司重新指派后的新市场人员 */ public void setNewseller(Integer newseller) { this.newseller = newseller; } /** * 获取移交原因 * * @return transReason - 移交原因 */ public String getTransreason() { return transreason; } /** * 设置移交原因 * * @param transreason 移交原因 */ public void setTransreason(String transreason) { this.transreason = transreason; } } ================================================ FILE: back/src/main/java/com/msy/plus/entity/CustomerManager.java ================================================ package com.msy.plus.entity; import java.util.Date; import javax.persistence.*; @Table(name = "customer_manager") public class CustomerManager { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; /** * 客户姓名 */ private String name; /** * 客户年龄 */ private Integer age; /** * 客户性别 页面为下拉框 */ private Integer gender; /** * 电话号码 */ private String tel; private String qq; private Integer job; /** * 客户来源 */ private Integer source; /** * 负责人 填写为当前登录用户 */ private Integer seller; /** * 创建人 填写为当前登录用户 */ @Column(name = "inputUser") private Integer inputuser; @Column(name = "inputTime") private Date inputtime; /** * -2:流失 -1:开发失败 0:潜在客户 1:正式客户 2:资源池客户 */ private Integer status; /** * 转正时间 */ @Column(name = "positiveTime") private Date positivetime; /** * @return id */ public Integer getId() { return id; } /** * @param id */ public void setId(Integer id) { this.id = id; } /** * 获取客户姓名 * * @return name - 客户姓名 */ public String getName() { return name; } /** * 设置客户姓名 * * @param name 客户姓名 */ public void setName(String name) { this.name = name; } /** * 获取客户年龄 * * @return age - 客户年龄 */ public Integer getAge() { return age; } /** * 设置客户年龄 * * @param age 客户年龄 */ public void setAge(Integer age) { this.age = age; } /** * 获取客户性别 页面为下拉框 * * @return gender - 客户性别 页面为下拉框 */ public Integer getGender() { return gender; } /** * 设置客户性别 页面为下拉框 * * @param gender 客户性别 页面为下拉框 */ public void setGender(Integer gender) { this.gender = gender; } /** * 获取电话号码 * * @return tel - 电话号码 */ public String getTel() { return tel; } /** * 设置电话号码 * * @param tel 电话号码 */ public void setTel(String tel) { this.tel = tel; } /** * @return qq */ public String getQq() { return qq; } /** * @param qq */ public void setQq(String qq) { this.qq = qq; } /** * @return job */ public Integer getJob() { return job; } /** * @param job */ public void setJob(Integer job) { this.job = job; } /** * 获取客户来源 * * @return source - 客户来源 */ public Integer getSource() { return source; } /** * 设置客户来源 * * @param source 客户来源 */ public void setSource(Integer source) { this.source = source; } /** * 获取负责人 填写为当前登录用户 * * @return seller - 负责人 填写为当前登录用户 */ public Integer getSeller() { return seller; } /** * 设置负责人 填写为当前登录用户 * * @param seller 负责人 填写为当前登录用户 */ public void setSeller(Integer seller) { this.seller = seller; } /** * 获取 创建人 填写为当前登录用户 * * @return inputUser - 创建人 填写为当前登录用户 */ public Integer getInputuser() { return inputuser; } /** * 设置 创建人 填写为当前登录用户 * * @param inputuser 创建人 填写为当前登录用户 */ public void setInputuser(Integer inputuser) { this.inputuser = inputuser; } /** * @return inputTime */ public Date getInputtime() { return inputtime; } /** * @param inputtime */ public void setInputtime(Date inputtime) { this.inputtime = inputtime; } /** * 获取-2:流失 -1:开发失败 0:潜在客户 1:正式客户 2:资源池客户 * * @return status - -2:流失 -1:开发失败 0:潜在客户 1:正式客户 2:资源池客户 */ public Integer getStatus() { return status; } /** * 设置-2:流失 -1:开发失败 0:潜在客户 1:正式客户 2:资源池客户 * * @param status -2:流失 -1:开发失败 0:潜在客户 1:正式客户 2:资源池客户 */ public void setStatus(Integer status) { this.status = status; } /** * 获取转正时间 * * @return positiveTime - 转正时间 */ public Date getPositivetime() { return positivetime; } /** * 设置转正时间 * * @param positivetime 转正时间 */ public void setPositivetime(Date positivetime) { this.positivetime = positivetime; } } ================================================ FILE: back/src/main/java/com/msy/plus/entity/Department.java ================================================ package com.msy.plus.entity; import javax.persistence.*; public class Department { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private String sn; private String name; /** * @return id */ public Integer getId() { return id; } /** * @param id */ public void setId(Integer id) { this.id = id; } /** * @return sn */ public String getSn() { return sn; } /** * @param sn */ public void setSn(String sn) { this.sn = sn; } /** * @return name */ public String getName() { return name; } /** * @param name */ public void setName(String name) { this.name = name; } } ================================================ FILE: back/src/main/java/com/msy/plus/entity/DictionaryContents.java ================================================ package com.msy.plus.entity; import javax.persistence.*; @Table(name = "dictionary_contents") public class DictionaryContents { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; /** * 字典目录编号 */ private String sn; /** * 字典目录名称 */ private String title; /** * 字典目录简介 */ private String intro; /** * @return id */ public Integer getId() { return id; } /** * @param id */ public void setId(Integer id) { this.id = id; } /** * 获取字典目录编号 * * @return sn - 字典目录编号 */ public String getSn() { return sn; } /** * 设置字典目录编号 * * @param sn 字典目录编号 */ public void setSn(String sn) { this.sn = sn; } /** * 获取字典目录名称 * * @return title - 字典目录名称 */ public String getTitle() { return title; } /** * 设置字典目录名称 * * @param title 字典目录名称 */ public void setTitle(String title) { this.title = title; } /** * 获取字典目录简介 * * @return intro - 字典目录简介 */ public String getIntro() { return intro; } /** * 设置字典目录简介 * * @param intro 字典目录简介 */ public void setIntro(String intro) { this.intro = intro; } } ================================================ FILE: back/src/main/java/com/msy/plus/entity/DictionaryDetails.java ================================================ package com.msy.plus.entity; import javax.persistence.*; @Table(name = "dictionary_details") public class DictionaryDetails { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; /** * 字典明细名称 */ private String title; /** * 字典明细序号 */ private Integer sequence; @Column(name = "parentId") private Integer parentid; /** * @return id */ public Integer getId() { return id; } /** * @param id */ public void setId(Integer id) { this.id = id; } /** * 获取字典明细名称 * * @return title - 字典明细名称 */ public String getTitle() { return title; } /** * 设置字典明细名称 * * @param title 字典明细名称 */ public void setTitle(String title) { this.title = title; } /** * 获取字典明细序号 * * @return sequence - 字典明细序号 */ public Integer getSequence() { return sequence; } /** * 设置字典明细序号 * * @param sequence 字典明细序号 */ public void setSequence(Integer sequence) { this.sequence = sequence; } /** * @return parentId */ public Integer getParentid() { return parentid; } /** * @param parentid */ public void setParentid(Integer parentid) { this.parentid = parentid; } } ================================================ FILE: back/src/main/java/com/msy/plus/entity/Employee.java ================================================ package com.msy.plus.entity; import lombok.Getter; import lombok.Setter; import java.util.Date; import javax.persistence.*; @Getter @Setter public class Employee { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String password; private String email; private Integer age; private Integer dept; @Column(name = "hireDate") private Date hiredate; /** * 状态 1正常 0离职 */ private Integer state; /** * 超级管理员身份 1超管 0普通 */ private Integer admin; } ================================================ FILE: back/src/main/java/com/msy/plus/entity/EmployeeDetail.java ================================================ package com.msy.plus.entity; import lombok.*; import java.util.List; @Getter @Setter @AllArgsConstructor @NoArgsConstructor @ToString public class EmployeeDetail extends Employee{ List roleIds; } ================================================ FILE: back/src/main/java/com/msy/plus/entity/EmployeeWithRoleDO.java ================================================ package com.msy.plus.entity; import lombok.*; @Getter @Setter @AllArgsConstructor @NoArgsConstructor @ToString public class EmployeeWithRoleDO extends Employee{ private String departmentName; String roleNames; } ================================================ FILE: back/src/main/java/com/msy/plus/entity/LoginResultDO.java ================================================ package com.msy.plus.entity; import lombok.Data; import java.util.Date; import java.util.Map; @Data public class LoginResultDO { private String token; private String expireAt = new Date().toString(); private Map permissions; private Object roles; private Object position; private Map user; } ================================================ FILE: back/src/main/java/com/msy/plus/entity/Permission.java ================================================ package com.msy.plus.entity; import lombok.Getter; import lombok.Setter; import javax.persistence.*; @Getter @Setter public class Permission { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; /** * 权限名称 */ private String name; /** * 资源地址 */ private String expression; } ================================================ FILE: back/src/main/java/com/msy/plus/entity/RoleDO.java ================================================ package com.msy.plus.entity; import lombok.Data; import lombok.Getter; import lombok.Setter; import javax.persistence.*; /** * 角色实体 * * @author MoShuying * @date 2018/05/27 */ @Data @Getter @Setter @Table(name = "role") public class RoleDO { /** 角色Id */ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") private Long id; /** 角色名称 */ @Column(name = "name") private String name; /** 角色编号 */ @Column(name = "sn") private String sn; } ================================================ FILE: back/src/main/java/com/msy/plus/entity/RolePermissionDO.java ================================================ package com.msy.plus.entity; import lombok.*; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; /** * 角色和权限中间表数据模型 */ @Getter @Setter @AllArgsConstructor @NoArgsConstructor @ToString public class RolePermissionDO { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; /** * 角色ID */ private Long role_id; /** * 权限ID */ private Long permission_id; } ================================================ FILE: back/src/main/java/com/msy/plus/entity/RoleWithPermissionDO.java ================================================ package com.msy.plus.entity; import lombok.*; import java.util.List; @Getter @Setter @AllArgsConstructor @NoArgsConstructor @ToString public class RoleWithPermissionDO extends RoleDO { List permissions; } ================================================ FILE: back/src/main/java/com/msy/plus/entity/Test.java ================================================ package com.msy.plus.entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; public class Test { /** 测试Id */ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; /** 测试名称 */ private String name; /** * 获取测试Id * * @return id - 测试Id */ public Long getId() { return id; } /** * 设置测试Id * * @param id 测试Id */ public void setId(Long id) { this.id = id; } /** * 获取测试名称 * * @return name - 测试名称 */ public String getName() { return name; } /** * 设置测试名称 * * @param name 测试名称 */ public void setName(String name) { this.name = name; } } ================================================ FILE: back/src/main/java/com/msy/plus/filter/AuthenticationFilter.java ================================================ package com.msy.plus.filter; import com.msy.plus.core.jwt.JwtUtil; import com.msy.plus.util.IpUtils; import com.msy.plus.util.UrlUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.core.annotation.Order; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import javax.annotation.Resource; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Optional; /** * 身份认证过滤器 * * @author MoShuying * @date 2018/05/27 */ @Slf4j @Order(2) @Component public class AuthenticationFilter implements Filter { @Resource private JwtUtil jwtUtil; @Override public void init(final FilterConfig filterConfig) { log.debug("==> AuthenticationFilter init"); } @Override public void doFilter( final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException { final HttpServletRequest request = (HttpServletRequest) servletRequest; final HttpServletResponse response = (HttpServletResponse) servletResponse; final String token = this.jwtUtil.getTokenFromRequest(request); if (!StringUtils.isEmpty(token)) { final Optional name = this.jwtUtil.getName(token); log.debug("==> Account<{}> token: {}", name.orElse(""), token); if (name.isPresent() && !Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) .isPresent()) { if (this.jwtUtil.validateToken(token)) { final UsernamePasswordAuthenticationToken authentication = this.jwtUtil.getAuthentication(name.get(), token); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); // 向 security 上下文中注入已认证的账户 // 之后可以直接在控制器 controller 的入参获得 Principal 或 Authentication SecurityContextHolder.getContext().setAuthentication(authentication); log.debug("==> Account<{}> is authorized, set security context", name); } } } else { log.debug( "==> IP<{}> Request: [{}] {}", IpUtils.getIpAddress(), request.getMethod(), UrlUtils.getMappingUrl(request)); } filterChain.doFilter(request, response); } @Override public void destroy() { log.debug("==> AuthenticationFilter destroy"); } } ================================================ FILE: back/src/main/java/com/msy/plus/filter/CorsFilter.java ================================================ package com.msy.plus.filter; import com.msy.plus.util.IpUtils; import com.msy.plus.util.UrlUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.annotation.Order; import org.springframework.http.HttpMethod; import org.springframework.stereotype.Component; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import static com.msy.plus.core.constant.ProjectConstant.SPRING_PROFILE_DEVELOPMENT; import static com.msy.plus.core.constant.ProjectConstant.SPRING_PROFILE_PRODUCTION; /** * 跨域过滤器 * * @author MoShuying * @date 2018/06/04 */ @Slf4j @Order(1) @Component public class CorsFilter implements Filter { @Value("${spring.profiles.active}") private String activeProfile; @Override public void init(final FilterConfig filterConfig) { log.debug("==> CorsFilter init"); } @Override public void doFilter( final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException { final HttpServletRequest request = (HttpServletRequest) servletRequest; final HttpServletResponse response = (HttpServletResponse) servletResponse; // 仅在生产环境下生效 if (SPRING_PROFILE_PRODUCTION.equals(this.activeProfile)) { // 设置允许多个域名请求 String[] allowDomains = {"http://project.crm3.msy.plus","https://project.crm3.msy.plus"}; Set allowOrigins = new HashSet(Arrays.asList(allowDomains)); String originHeads = request.getHeader("Origin"); if(allowOrigins.contains(originHeads)){ //设置允许跨域的配置 // 这里填写你允许进行跨域的主机ip(正式上线时可以动态配置具体允许的域名和IP) response.setHeader("Access-Control-Allow-Origin", originHeads); } } if(SPRING_PROFILE_DEVELOPMENT.equals(this.activeProfile)){ // 允许所有来源 response.setHeader("Access-Control-Allow-Origin", "*"); } response.setHeader("Access-Control-Allow-Credentials", "true"); response.setHeader( "Access-Control-Allow-Headers", "Content-Type, Content-Length, Authorization"); // 明确允许通过的方法,不建议使用 * response.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, PATCH, OPTIONS"); response.setHeader("Access-Control-Max-Age", "3600"); response.setHeader("Access-Control-Expose-Headers", "*"); // 预请求后,直接返回 // 返回码必须为 200 否则视为请求失败 if (HttpMethod.OPTIONS.matches(request.getMethod())) { return; } log.debug( "==> IP<{}> Request: [{}] {}", IpUtils.getIpAddress(), request.getMethod(), UrlUtils.getMappingUrl(request)); filterChain.doFilter(request, response); } @Override public void destroy() { log.debug("==> CorsFilter destroy"); } } ================================================ FILE: back/src/main/java/com/msy/plus/filter/MyAuthenticationEntryPoint.java ================================================ package com.msy.plus.filter; import com.msy.plus.core.response.ResultCode; import com.msy.plus.core.response.ResultGenerator; import org.springframework.http.MediaType; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.Serializable; import java.nio.charset.StandardCharsets; /** * 认证入口点 因为 RESTFul 没有登录界面所以只显示未登录提示 * * @author MoShuying * @date 2018/05/27 */ @Component public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable { @Override public void commence( final HttpServletRequest request, final HttpServletResponse response, final AuthenticationException authException) throws IOException { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setHeader("Content-type", MediaType.APPLICATION_JSON_VALUE); response.setCharacterEncoding(StandardCharsets.UTF_8.displayName()); response .getWriter() .println(ResultGenerator.genFailedResult(ResultCode.UNAUTHORIZED_EXCEPTION).toString()); response.getWriter().close(); } } ================================================ FILE: back/src/main/java/com/msy/plus/filter/RequestWrapper.java ================================================ package com.msy.plus.filter; import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.util.Optional; /** * 请求装饰器,用于多次读取请求流 * * @author MoShuying * @date 2018/07/13 */ public class RequestWrapper extends HttpServletRequestWrapper { private final StringBuilder body; public RequestWrapper(final HttpServletRequest request) throws IOException { super(request); this.body = new StringBuilder(); final BufferedReader bufferedReader = request.getReader(); String line; while (Optional.ofNullable(line = bufferedReader.readLine()).isPresent()) { this.body.append(line); } } @Override public ServletInputStream getInputStream() { final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.body.toString().getBytes()); return new ServletInputStream() { @Override public int read() { return byteArrayInputStream.read(); } @Override public boolean isFinished() { return false; } @Override public boolean isReady() { return false; } @Override public void setReadListener(final ReadListener readListener) {} }; } @Override public BufferedReader getReader() { return new BufferedReader(new InputStreamReader(this.getInputStream())); } public String getJson() { return this.getReader() .lines() .sequential() .reduce(System.lineSeparator(), (accumulator, actual) -> accumulator + actual); } } ================================================ FILE: back/src/main/java/com/msy/plus/mapper/AccountMapper.java ================================================ package com.msy.plus.mapper; import com.msy.plus.core.mapper.MyMapper; import com.msy.plus.entity.AccountDO; import com.msy.plus.entity.AccountWithRoleDO; import com.msy.plus.query.AccountQuery; import org.apache.ibatis.annotations.Param; /** * @author MoShuying * @date 2018/05/27 */ public interface AccountMapper extends MyMapper { /** * 按条件查询账户 * * @param accountQuery 账户查询条件 * @return 账户 */ AccountWithRoleDO getByQueryWithRole(AccountQuery accountQuery); /** * 按账户名更新最后登陆时间 * * @param name 账户名 * @return 影响行数 */ int updateLoginTimeByName(@Param("name") String name); } ================================================ FILE: back/src/main/java/com/msy/plus/mapper/CustomerFollowUpHistoryMapper.java ================================================ package com.msy.plus.mapper; import com.msy.plus.core.mapper.MyMapper; import com.msy.plus.entity.CFUHSearch; import com.msy.plus.entity.CustomerFollowUpHistory; import java.util.List; public interface CustomerFollowUpHistoryMapper extends MyMapper { List listAndSearch(String keyword, String startTime, String endTime, Integer type); } ================================================ FILE: back/src/main/java/com/msy/plus/mapper/CustomerHandoverMapper.java ================================================ package com.msy.plus.mapper; import com.msy.plus.core.mapper.MyMapper; import com.msy.plus.dto.CustomerHandoverList; import com.msy.plus.entity.CustomerHandover; import java.util.Date; import java.util.List; public interface CustomerHandoverMapper extends MyMapper { List listAndSearch(String keyword, Date startTime, Date endTime); } ================================================ FILE: back/src/main/java/com/msy/plus/mapper/CustomerManagerMapper.java ================================================ package com.msy.plus.mapper; import com.msy.plus.core.mapper.MyMapper; import com.msy.plus.dto.AnalysisQuery; import com.msy.plus.entity.Analysis; import com.msy.plus.entity.CustomerManager; import com.msy.plus.dto.CustomerManagerList; import java.util.List; public interface CustomerManagerMapper extends MyMapper { List listAllWithDictionary(String keyword,Integer status); CustomerManager getDetailById(Object id); List queryAnalysis(AnalysisQuery analysisQuery); } ================================================ FILE: back/src/main/java/com/msy/plus/mapper/DepartmentMapper.java ================================================ package com.msy.plus.mapper; import com.msy.plus.core.mapper.MyMapper; import com.msy.plus.entity.Department; public interface DepartmentMapper extends MyMapper { } ================================================ FILE: back/src/main/java/com/msy/plus/mapper/DictionaryContentsMapper.java ================================================ package com.msy.plus.mapper; import com.msy.plus.core.mapper.MyMapper; import com.msy.plus.entity.DictionaryContents; import java.util.List; public interface DictionaryContentsMapper extends MyMapper { List listWithKeyword(String keyword); } ================================================ FILE: back/src/main/java/com/msy/plus/mapper/DictionaryDetailsMapper.java ================================================ package com.msy.plus.mapper; import com.msy.plus.core.mapper.MyMapper; import com.msy.plus.entity.DictionaryContents; import com.msy.plus.entity.DictionaryDetails; import java.util.List; public interface DictionaryDetailsMapper extends MyMapper { List listWithKeyword(int id,String keyword); } ================================================ FILE: back/src/main/java/com/msy/plus/mapper/EmployeeMapper.java ================================================ package com.msy.plus.mapper; import com.msy.plus.core.mapper.MyMapper; import com.msy.plus.entity.Employee; import com.msy.plus.entity.EmployeeDetail; import com.msy.plus.entity.EmployeeWithRoleDO; import java.util.List; public interface EmployeeMapper extends MyMapper { EmployeeDetail getDetailById(Long id); /** * 分页查询员工 * @return */ List listEmployeeWithRole(String keyword,int dept); /** * 保存员工角色信息 * @param id * @param roles * @return */ void saveRoles(Long id, List roles); /** * 删除员工权限 * @param id * @return */ int deleteEmployeeWithRole(Long id); int deleteEmployeeWithRoleItem(Long id,Long roleId); /** * 获取所有中间表的id * @param id * @return List */ List getAllEmployeeRoleTableRow(Long id); } ================================================ FILE: back/src/main/java/com/msy/plus/mapper/PermissionMapper.java ================================================ package com.msy.plus.mapper; import com.msy.plus.core.mapper.MyMapper; import com.msy.plus.entity.Permission; public interface PermissionMapper extends MyMapper { } ================================================ FILE: back/src/main/java/com/msy/plus/mapper/RoleMapper.java ================================================ package com.msy.plus.mapper; import com.msy.plus.core.mapper.MyMapper; import com.msy.plus.entity.Permission; import com.msy.plus.entity.RoleDO; import com.msy.plus.entity.RolePermissionDO; import com.msy.plus.entity.RoleWithPermissionDO; import org.apache.ibatis.annotations.Param; import java.util.List; /** * @author MoShuying * @date 2018/07/15 */ public interface RoleMapper extends MyMapper { /** * 赋予默认角色给账户 * * @param accountId 账户Id * @return 影响行数 */ int saveAsDefaultRole(@Param("accountId") Long accountId); /** * 获取角色信息并查询角色权限 * @param id * @return */ RoleWithPermissionDO getDetailById(Long id); /** * 保存用户权限 * @param permissions */ int savePermissions(Long roleId,List permissions); /** * 删除中间表信息 * @param roleId * @param permissionId */ void deleteRolePermissionItem(Long roleId,Long permissionId); /** * 获取所有权限表中的字段 * @param roleId * @return List */ List getAllRolePermissionTableRow(Long roleId); } ================================================ FILE: back/src/main/java/com/msy/plus/query/AccountQuery.java ================================================ package com.msy.plus.query; import lombok.Builder; import java.io.Serializable; /** * 账户查询实体 * * @author MoShuying * @date 2018/07/15 */ @Builder public class AccountQuery implements Serializable { private static final long serialVersionUID = 4063412382769589319L; /** 账户Id */ private final Long id; /** 邮箱 */ private final String email; /** 账户名 */ private final String name; /** 密码 */ private final String password; } ================================================ FILE: back/src/main/java/com/msy/plus/service/AccountService.java ================================================ package com.msy.plus.service; import com.msy.plus.core.service.Service; import com.msy.plus.dto.AccountDTO; import com.msy.plus.entity.AccountDO; import com.msy.plus.entity.AccountWithRoleDO; /** * @author MoShuying * @date 2018/05/27 */ public interface AccountService extends Service { /** * 保存账户 * * @param accountDTO 账户传输实体 */ void save(AccountDTO accountDTO); /** * 按账户名查询带有角色信息的账户 * * @param name 账户名 * @return 账户 */ AccountWithRoleDO getByNameWithRole(String name); /** * 按账户Id查询带有角色信息的账户 * * @param id 账户Id * @return 账户 */ AccountWithRoleDO getByIdWithRole(Long id); /** * 更新账户 * * @param accountDTO 账户传输实体 */ void updateByName(AccountDTO accountDTO); /** * 按账户名更新最后一次登录时间 * * @param name 账户名 * @return 是否更新成功 */ boolean updateLoginTimeByName(String name); /** * 验证账户密码 * * @param rawPassword 原密码 * @param encodedPassword 加密后的密码 * @return {boolean} */ boolean verifyPassword(String rawPassword, String encodedPassword); } ================================================ FILE: back/src/main/java/com/msy/plus/service/CustomerFollowUpHistoryService.java ================================================ package com.msy.plus.service; import com.msy.plus.entity.CFUHSearch; import com.msy.plus.entity.CustomerFollowUpHistory; import com.msy.plus.core.service.Service; import java.util.Date; import java.util.List; /** * @author MoShuYing * @date 2021/05/21 */ public interface CustomerFollowUpHistoryService extends Service { List listAndSearch(String keyword, Date startTime, Date endTime, Integer type); } ================================================ FILE: back/src/main/java/com/msy/plus/service/CustomerHandoverService.java ================================================ package com.msy.plus.service; import com.msy.plus.dto.CustomerHandoverList; import com.msy.plus.entity.CustomerHandover; import com.msy.plus.core.service.Service; import java.util.Date; import java.util.List; /** * @author MoShuYing * @date 2021/05/21 */ public interface CustomerHandoverService extends Service { List listAndSearch(String keyword, Date startTime, Date endTime); } ================================================ FILE: back/src/main/java/com/msy/plus/service/CustomerManagerService.java ================================================ package com.msy.plus.service; import com.msy.plus.dto.AnalysisQuery; import com.msy.plus.dto.CustomerManagerList; import com.msy.plus.entity.Analysis; import com.msy.plus.entity.CustomerManager; import com.msy.plus.core.service.Service; import java.util.List; /** * @author MoShuYing * @date 2021/05/20 */ public interface CustomerManagerService extends Service { List listAllWithDictionary(String keyword, Integer status); List queryAnalysis(AnalysisQuery analysisQuery); } ================================================ FILE: back/src/main/java/com/msy/plus/service/DepartmentService.java ================================================ package com.msy.plus.service; import com.msy.plus.entity.Department; import com.msy.plus.core.service.Service; /** * @author MoShuYing * @date 2021/05/12 */ public interface DepartmentService extends Service { } ================================================ FILE: back/src/main/java/com/msy/plus/service/DictionaryContentsService.java ================================================ package com.msy.plus.service; import com.msy.plus.entity.DictionaryContents; import com.msy.plus.core.service.Service; import java.util.List; /** * @author MoShuYing * @date 2021/05/18 */ public interface DictionaryContentsService extends Service { List listWithKeyword(String keyword); } ================================================ FILE: back/src/main/java/com/msy/plus/service/DictionaryDetailsService.java ================================================ package com.msy.plus.service; import com.msy.plus.entity.DictionaryContents; import com.msy.plus.entity.DictionaryDetails; import com.msy.plus.core.service.Service; import java.util.List; /** * @author MoShuYing * @date 2021/05/18 */ public interface DictionaryDetailsService extends Service { List listWithKeyword(int id,String keyword); } ================================================ FILE: back/src/main/java/com/msy/plus/service/EmployeeService.java ================================================ package com.msy.plus.service; import com.msy.plus.entity.Employee; import com.msy.plus.core.service.Service; import com.msy.plus.entity.EmployeeDetail; import com.msy.plus.entity.EmployeeWithRoleDO; import java.util.List; /** * @author MoShuYing * @date 2021/05/15 */ public interface EmployeeService extends Service { EmployeeDetail getDetailById(Long id); /** * 分页查询员工 * @return */ List listEmployeeWithRole(String keyword,Integer dept); /** * 保存员工角色信息 * @param id * @param roles * @return */ void saveRoles(Long id,List roles); /** * 删除员工权限 * @param id * @return */ int deleteEmployeeWithRole(Long id); int deleteEmployeeWithRoleItem(Long id,Long roleId); /** * 获取所有中间表的id * @param id * @return List */ List getAllEmployeeRoleTableRow(Long id); } ================================================ FILE: back/src/main/java/com/msy/plus/service/PermissionService.java ================================================ package com.msy.plus.service; import com.msy.plus.entity.Permission; import com.msy.plus.core.service.Service; /** * @author MoShuYing * @date 2021/05/14 */ public interface PermissionService extends Service { } ================================================ FILE: back/src/main/java/com/msy/plus/service/RoleService.java ================================================ package com.msy.plus.service; import com.msy.plus.core.service.Service; import com.msy.plus.dto.RoleDTO; import com.msy.plus.entity.RoleDO; import com.msy.plus.entity.RolePermissionDO; import com.msy.plus.entity.RoleWithPermissionDO; import java.util.List; /** * @author MoShuying * @date 2018/05/27 */ public interface RoleService extends Service { /** * 赋予默认角色给账户 * * @param accountId 账户Id */ void saveAsDefaultRole(Long accountId); /** * 保存角色 * * @param roleDTO 角色传输实体 */ void save(RoleDTO roleDTO); /** * 更新角色 * * @param roleDTO 角色传输实体 */ void update(RoleDTO roleDTO); /** * 获取角色信息并查询角色权限 * @param id * @return */ RoleWithPermissionDO getDetailById(Long id); /** * 保存用户权限 * @param permissions */ void savePermissions(Long roleId,List permissions); /** * 删除中间表信息 * @param roleId * @param permissionId */ void deleteRolePermissionItem(Long roleId,Long permissionId); /** * 获取所有权限表中的字段 * @param roleId * @return List */ List getAllRolePermissionTableRow(Long roleId); } ================================================ FILE: back/src/main/java/com/msy/plus/service/impl/AccountServiceImpl.java ================================================ package com.msy.plus.service.impl; import com.msy.plus.core.response.ResultCode; import com.msy.plus.core.service.AbstractService; import com.msy.plus.dto.AccountDTO; import com.msy.plus.entity.AccountDO; import com.msy.plus.entity.AccountWithRoleDO; import com.msy.plus.mapper.AccountMapper; import com.msy.plus.query.AccountQuery; import com.msy.plus.service.AccountService; import com.msy.plus.service.RoleService; import com.msy.plus.util.AssertUtils; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import tk.mybatis.mapper.entity.Condition; import javax.annotation.Resource; import java.util.Optional; /** * @author MoShuying * @date 2018/05/27 */ @Slf4j @Service @Transactional(rollbackFor = Exception.class) public class AccountServiceImpl extends AbstractService implements AccountService { @Resource private AccountMapper accountMapper; @Resource private RoleService roleService; @Resource private PasswordEncoder passwordEncoder; /** 重写 save 方法,密码加密后再存,并且赋予默认角色 */ @Override public void save(final AccountDTO accountDTO) { AssertUtils.asserts( !Optional.ofNullable(this.getBy("name", accountDTO.getName())).isPresent(), ResultCode.DUPLICATE_NAME); final AccountDO accountDO = accountDTO.convertToDO(); accountDO.setPassword(this.passwordEncoder.encode(accountDTO.getPassword().trim())); this.save(accountDO); log.debug("==> Create Account<{}> Id<{}>", accountDO.getName(), accountDO.getId()); // 新建账户默认角色 this.roleService.saveAsDefaultRole(accountDO.getId()); } @Override public void updateByName(final AccountDTO accountDTO) { final AccountDO accountDO = accountDTO.convertToDO(); // 如果修改了密码 if (StringUtils.isNotBlank(accountDTO.getPassword())) { // 密码修改后需要加密 accountDO.setPassword(this.passwordEncoder.encode(accountDTO.getPassword().trim())); } // 不能修改账户名 final String name = accountDO.getName(); accountDO.setName(null); // 按 name 字段更新 final Condition condition = new Condition(AccountDO.class); condition.createCriteria().andCondition("name = ", name); this.updateByCondition(accountDO, condition); } @Override public AccountWithRoleDO getByIdWithRole(final Long id) { final AccountQuery accountQuery = AccountQuery.builder().id(id).build(); return this.accountMapper.getByQueryWithRole(accountQuery); } @Override public AccountWithRoleDO getByNameWithRole(final String name) { final AccountQuery accountQuery = AccountQuery.builder().name(name).build(); return this.accountMapper.getByQueryWithRole(accountQuery); } @Override public boolean verifyPassword(final String rawPassword, final String encodedPassword) { return this.passwordEncoder.matches(rawPassword, encodedPassword); } @Override public boolean updateLoginTimeByName(final String name) { final boolean success = this.accountMapper.updateLoginTimeByName(name) == 1; if (!success) { log.error("==> Update Account<{}> login time error", name); } return success; } } ================================================ FILE: back/src/main/java/com/msy/plus/service/impl/CustomerFollowUpHistoryServiceImpl.java ================================================ package com.msy.plus.service.impl; import com.msy.plus.entity.CFUHSearch; import com.msy.plus.mapper.CustomerFollowUpHistoryMapper; import com.msy.plus.entity.CustomerFollowUpHistory; import com.msy.plus.service.CustomerFollowUpHistoryService; import com.msy.plus.core.service.AbstractService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.sql.Timestamp; import java.util.Date; import java.util.List; /** * @author MoShuYing * @date 2021/05/21 */ @Service @Transactional(rollbackFor = Exception.class) public class CustomerFollowUpHistoryServiceImpl extends AbstractService implements CustomerFollowUpHistoryService { @Resource private CustomerFollowUpHistoryMapper customerFollowUpHistoryMapper; @Override public List listAndSearch(String keyword, Date startTime, Date endTime, Integer type) { String st=null,et=null; if(startTime!=null){ st=new Timestamp(startTime.getTime()).toString(); } if(endTime!=null){ et=new Timestamp(endTime.getTime()).toString(); } return this.customerFollowUpHistoryMapper.listAndSearch(keyword, st,et, type); } } ================================================ FILE: back/src/main/java/com/msy/plus/service/impl/CustomerHandoverServiceImpl.java ================================================ package com.msy.plus.service.impl; import com.msy.plus.dto.CustomerHandoverList; import com.msy.plus.mapper.CustomerHandoverMapper; import com.msy.plus.entity.CustomerHandover; import com.msy.plus.service.CustomerHandoverService; import com.msy.plus.core.service.AbstractService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.util.Date; import java.util.List; /** * @author MoShuYing * @date 2021/05/21 */ @Service @Transactional(rollbackFor = Exception.class) public class CustomerHandoverServiceImpl extends AbstractService implements CustomerHandoverService { @Resource private CustomerHandoverMapper customerHandoverMapper; @Override public List listAndSearch(String keyword, Date startTime, Date endTime) { return this.customerHandoverMapper.listAndSearch(keyword, startTime, endTime); } } ================================================ FILE: back/src/main/java/com/msy/plus/service/impl/CustomerManagerServiceImpl.java ================================================ package com.msy.plus.service.impl; import com.msy.plus.dto.AnalysisQuery; import com.msy.plus.dto.CustomerManagerList; import com.msy.plus.entity.Analysis; import com.msy.plus.mapper.CustomerManagerMapper; import com.msy.plus.entity.CustomerManager; import com.msy.plus.service.CustomerManagerService; import com.msy.plus.core.service.AbstractService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.util.List; /** * @author MoShuYing * @date 2021/05/20 */ @Service @Transactional(rollbackFor = Exception.class) public class CustomerManagerServiceImpl extends AbstractService implements CustomerManagerService { @Resource private CustomerManagerMapper customerManagerMapper; @Override public CustomerManager getById(Object id) { return this.customerManagerMapper.getDetailById(id); } @Override public List listAllWithDictionary(String keyword, Integer status) { return customerManagerMapper.listAllWithDictionary(keyword,status); } @Override public List queryAnalysis(AnalysisQuery analysisQuery) { if(analysisQuery.getName()==null){ analysisQuery.setName(""); } return customerManagerMapper.queryAnalysis(analysisQuery); } } ================================================ FILE: back/src/main/java/com/msy/plus/service/impl/DepartmentServiceImpl.java ================================================ package com.msy.plus.service.impl; import com.msy.plus.mapper.DepartmentMapper; import com.msy.plus.entity.Department; import com.msy.plus.service.DepartmentService; import com.msy.plus.core.service.AbstractService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; /** * @author MoShuYing * @date 2021/05/12 */ @Service @Transactional(rollbackFor = Exception.class) public class DepartmentServiceImpl extends AbstractService implements DepartmentService { @Resource private DepartmentMapper departmentMapper; } ================================================ FILE: back/src/main/java/com/msy/plus/service/impl/DictionaryContentsServiceImpl.java ================================================ package com.msy.plus.service.impl; import com.msy.plus.mapper.DictionaryContentsMapper; import com.msy.plus.entity.DictionaryContents; import com.msy.plus.service.DictionaryContentsService; import com.msy.plus.core.service.AbstractService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.util.List; /** * @author MoShuYing * @date 2021/05/18 */ @Service @Transactional(rollbackFor = Exception.class) public class DictionaryContentsServiceImpl extends AbstractService implements DictionaryContentsService { @Resource private DictionaryContentsMapper dictionaryContentsMapper; @Override public List listWithKeyword(String keyword) { return dictionaryContentsMapper.listWithKeyword(keyword); } } ================================================ FILE: back/src/main/java/com/msy/plus/service/impl/DictionaryDetailsServiceImpl.java ================================================ package com.msy.plus.service.impl; import com.msy.plus.entity.DictionaryContents; import com.msy.plus.mapper.DictionaryDetailsMapper; import com.msy.plus.entity.DictionaryDetails; import com.msy.plus.service.DictionaryDetailsService; import com.msy.plus.core.service.AbstractService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.util.List; /** * @author MoShuYing * @date 2021/05/18 */ @Service @Transactional(rollbackFor = Exception.class) public class DictionaryDetailsServiceImpl extends AbstractService implements DictionaryDetailsService { @Resource private DictionaryDetailsMapper dictionaryDetailsMapper; @Override public List listWithKeyword(int id,String keyword) { return dictionaryDetailsMapper.listWithKeyword(id,keyword); } } ================================================ FILE: back/src/main/java/com/msy/plus/service/impl/EmployeeServiceImpl.java ================================================ package com.msy.plus.service.impl; import com.msy.plus.entity.EmployeeDetail; import com.msy.plus.entity.EmployeeWithRoleDO; import com.msy.plus.mapper.EmployeeMapper; import com.msy.plus.entity.Employee; import com.msy.plus.service.EmployeeService; import com.msy.plus.core.service.AbstractService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.util.List; /** * @author MoShuYing * @date 2021/05/15 */ @Service @Transactional(rollbackFor = Exception.class) public class EmployeeServiceImpl extends AbstractService implements EmployeeService { @Resource private EmployeeMapper employeeMapper; @Override public EmployeeDetail getDetailById(Long id) { return this.employeeMapper.getDetailById(id); } @Override public List listEmployeeWithRole(String keyword,Integer dept){ if(dept==null){ dept=0; } return this.employeeMapper.listEmployeeWithRole(keyword,dept); } @Override public void saveRoles(Long id, List roles) { this.employeeMapper.saveRoles(id,roles); } @Override public int deleteEmployeeWithRole(Long id) { return this.employeeMapper.deleteEmployeeWithRole(id); } @Override public int deleteEmployeeWithRoleItem(Long id, Long roleId) { return this.employeeMapper.deleteEmployeeWithRoleItem(id,roleId); } @Override public List getAllEmployeeRoleTableRow(Long id) { return this.employeeMapper.getAllEmployeeRoleTableRow(id); } } ================================================ FILE: back/src/main/java/com/msy/plus/service/impl/PermissionServiceImpl.java ================================================ package com.msy.plus.service.impl; import com.msy.plus.mapper.PermissionMapper; import com.msy.plus.entity.Permission; import com.msy.plus.service.PermissionService; import com.msy.plus.core.service.AbstractService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; /** * @author MoShuYing * @date 2021/05/14 */ @Service @Transactional(rollbackFor = Exception.class) public class PermissionServiceImpl extends AbstractService implements PermissionService { @Resource private PermissionMapper permissionMapper; } ================================================ FILE: back/src/main/java/com/msy/plus/service/impl/RoleServiceImpl.java ================================================ package com.msy.plus.service.impl; import com.msy.plus.core.response.ResultCode; import com.msy.plus.core.service.AbstractService; import com.msy.plus.dto.RoleDTO; import com.msy.plus.entity.RoleDO; import com.msy.plus.entity.RolePermissionDO; import com.msy.plus.entity.RoleWithPermissionDO; import com.msy.plus.mapper.RoleMapper; import com.msy.plus.service.RoleService; import com.msy.plus.util.AssertUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.util.List; /** * @author MoShuying * @date 2018/05/27 */ @Service @Transactional(rollbackFor = Exception.class) public class RoleServiceImpl extends AbstractService implements RoleService { @Resource private RoleMapper roleMapper; @Override public void saveAsDefaultRole(final Long accountId) { final boolean success = this.roleMapper.saveAsDefaultRole(accountId) == 1; AssertUtils.asserts(success, ResultCode.SAVE_FAILED, "账户默认角色保存失败"); } @Override public void save(final RoleDTO roleDTO) { final RoleDO role = roleDTO.convertToDO(); this.save(role); } @Override public void update(final RoleDTO roleDTO) { final RoleDO role = roleDTO.convertToDO(); this.update(role); } @Override public RoleWithPermissionDO getDetailById(Long id) { return this.roleMapper.getDetailById(id); } @Override public void savePermissions(Long roleId,List permissions) { this.roleMapper.savePermissions(roleId,permissions); // AssertUtils.asserts(success, ResultCode.SAVE_FAILED, "账户角色权限保存失败"); } @Override public void deleteRolePermissionItem(Long roleId, Long permissionId) { this.roleMapper.deleteRolePermissionItem(roleId,permissionId); } @Override public List getAllRolePermissionTableRow(Long roleId) { return this.roleMapper.getAllRolePermissionTableRow(roleId); } } ================================================ FILE: back/src/main/java/com/msy/plus/service/impl/UserDetailsServiceImpl.java ================================================ package com.msy.plus.service.impl; import com.msy.plus.core.exception.UsernameNotFoundException2; import com.msy.plus.entity.AccountWithRoleDO; import com.msy.plus.service.AccountService; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.util.ArrayList; import java.util.List; import java.util.Optional; /** * @author MoShuying * @date 2018/05/27 */ @Service @Transactional(rollbackFor = Exception.class) public class UserDetailsServiceImpl implements UserDetailsService { @Resource private AccountService accountService; @Override public UserDetails loadUserByUsername(final String name) throws UsernameNotFoundException2 { final AccountWithRoleDO account = this.accountService.getByNameWithRole(name); Optional.ofNullable(account).orElseThrow(UsernameNotFoundException2::new); final List authorities = new ArrayList<>(); account .getRoles() .forEach(roleDO -> authorities.add(new SimpleGrantedAuthority(roleDO.getName()))); return new org.springframework.security.core.userdetails.User( account.getName(), account.getPassword(), authorities); } } ================================================ FILE: back/src/main/java/com/msy/plus/util/AssertUtils.java ================================================ package com.msy.plus.util; import com.msy.plus.core.exception.ServiceException; import com.msy.plus.core.response.ResultCode; /** * 断言工具 * * @author MoShuying * @date 2018/11/29 */ public class AssertUtils { public static void throwIf( final boolean statement, final ResultCode resultCode, final String message) { if (statement) { throw toThrow(resultCode, message); } } public static void throwIf( final boolean statement, final ResultCode resultCode, final Object... messages) { throwIf(statement, resultCode, resultCode.format(messages)); } public static RuntimeException toThrow(final ResultCode resultCode, final Object... messages) { return new ServiceException(resultCode, resultCode.format(messages)); } public static void asserts( final boolean statement, final ResultCode resultCode, final Object... messages) { throwIf(!statement, resultCode, messages); } } ================================================ FILE: back/src/main/java/com/msy/plus/util/ContextUtils.java ================================================ package com.msy.plus.util; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; /** * 上下文工具 * * @author MoShuying * @date 2018/07/20 */ public class ContextUtils { private ContextUtils() {} /** * 获取 request * * @return request */ public static HttpServletRequest getRequest() { final ServletRequestAttributes attributes = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()); return attributes == null ? null : attributes.getRequest(); } } ================================================ FILE: back/src/main/java/com/msy/plus/util/DateUtils.java ================================================ package com.msy.plus.util; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; /** * 线程安全的日期工具 * * @author MoShuying * @date 2018/07/20 */ public class DateUtils { private static final DateTimeFormatter DTF_YEAR = DateTimeFormatter.ofPattern("yyyy"); private static final DateTimeFormatter DTF_DAY = DateTimeFormatter.ofPattern("yyyy-MM-dd"); private static final DateTimeFormatter DTF_DAYS = DateTimeFormatter.ofPattern("yyyyMMdd"); private static final DateTimeFormatter DTF_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); private static final DateTimeFormatter DTF_TIMES = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); private DateUtils() {} /** * 现在时间(年) * * @return DTF_YEAR */ public static String getThisYear() { return DTF_YEAR.format(LocalDateTime.now()); } /** * 校验时间(年)是否合法 * * @param dateString 时间字符串 * @return Boolean */ public static Boolean validateYear(final String dateString) { return validate(dateString, DTF_YEAR); } /** * 现在时间(天) * * @return DTF_DAY */ public static String getThisDay() { return DTF_DAY.format(LocalDateTime.now()); } /** * 校验时间(天)是否合法 * * @param dateString 时间字符串 * @return Boolean */ public static Boolean validateDay(final String dateString) { return validate(dateString, DTF_DAY); } /** * 现在时间(天) * * @return DTF_DAYS */ public static String getThisDays() { return DTF_DAYS.format(LocalDateTime.now()); } /** * 校验时间(天)是否合法 * * @param dateString 时间字符串 * @return Boolean */ public static Boolean validateDays(final String dateString) { return validate(dateString, DTF_DAYS); } /** * 现在时间(秒) * * @return DTF_TIME */ public static String getThisTime() { return DTF_TIME.format(LocalDateTime.now()); } /** * 校验时间(秒)是否合法 * * @param dateString 时间字符串 * @return Boolean */ public static Boolean validateTime(final String dateString) { return validate(dateString, DTF_TIME); } /** * 现在时间(秒) * * @return DTF_TIMES */ public static String getThisTimes() { return DTF_TIMES.format(LocalDateTime.now()); } /** * 校验时间(秒)是否合法 * * @param dateString 时间字符串 * @return Boolean */ public static Boolean validateTimes(final String dateString) { return validate(dateString, DTF_TIMES); } /** * 校验日期是否合法 * * @param dateString 时间字符串 * @param dateTimeFormatString 时间格式字符串 * @return Boolean */ public static Boolean validate(final String dateString, final String dateTimeFormatString) { final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(dateTimeFormatString); try { LocalDateTime.parse(dateString, dateTimeFormatter); return true; } catch (final Exception e) { return false; } } /** * 校验日期是否合法 * * @param dateString 时间字符串 * @param dateTimeFormatter 时间格式器 * @return Boolean */ public static Boolean validate( final String dateString, final DateTimeFormatter dateTimeFormatter) { try { LocalDateTime.parse(dateString, dateTimeFormatter); return true; } catch (final Exception e) { return false; } } /** * 比较两个时间的大小 * * @param dateString1 时间字符串1 * @param dateString2 时间字符串2 * @param dateTimeFormatString 时间格式 * @return -1:时间1小于时间2 | 0:时间1等于时间2 | 1:时间1大于时间2 */ public static Integer compare( final String dateString1, final String dateString2, final String dateTimeFormatString) { final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(dateTimeFormatString); final LocalDateTime dateTime1 = LocalDateTime.parse(dateString1, dateTimeFormatter); final LocalDateTime dateTime2 = LocalDateTime.parse(dateString2, dateTimeFormatter); if (dateTime1.isBefore(dateTime2)) { return -1; } else if (dateTime1.equals(dateTime2)) { return 0; } else { return 1; } } /** * 在原时间上增加x个时间单位 * * @param dateString 时间字符串 * @param x x个时间单位 * @param chronoUnit 时间单位 * @param dateTimeFormatString 时间格式 * @return 增加后的时间 */ public static String add( final String dateString, final Long x, final ChronoUnit chronoUnit, final String dateTimeFormatString) { final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(dateTimeFormatString); final LocalDateTime dateTime = LocalDateTime.parse(dateString, dateTimeFormatter); final LocalDateTime newTime = dateTime.plus(x, chronoUnit); return dateTimeFormatter.format(newTime); } /** * 增加x小时 * * @param dateString 时间字符串 * @param hours 小时 * @param dateTimeFormatString 时间格式 * @return 增加后的时间 */ public static String addHours( final String dateString, final Long hours, final String dateTimeFormatString) { return add(dateString, hours, ChronoUnit.HOURS, dateTimeFormatString); } /** * 增加x分钟 * * @param dateString 时间字符串 * @param minutes 分钟 * @param dateTimeFormatString 时间格式 * @return 增加后的时间 */ public static String addMinutes( final String dateString, final Long minutes, final String dateTimeFormatString) { return add(dateString, minutes, ChronoUnit.MINUTES, dateTimeFormatString); } /** * 增加x秒 * * @param dateString 时间字符串 * @param seconds 秒 * @param dateTimeFormatString 时间格式 * @return 增加后的时间 */ public static String addSeconds( final String dateString, final Long seconds, final String dateTimeFormatString) { return add(dateString, seconds, ChronoUnit.SECONDS, dateTimeFormatString); } /** * 是否为闰年 * * @param dateString 时间字符串 * @param dateTimeFormatString 时间格式字符串 * @return Boolean */ public static Boolean isLeapYear(final String dateString, final String dateTimeFormatString) { final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(dateTimeFormatString); final LocalDate dateTime = LocalDate.parse(dateString, dateTimeFormatter); return dateTime.isLeapYear(); } } ================================================ FILE: back/src/main/java/com/msy/plus/util/FileUtils.java ================================================ package com.msy.plus.util; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.Map; public class FileUtils { /** 常见文件头信息 */ public static final Map FILE_TYPE_MAP = new HashMap() { private static final long serialVersionUID = 2413557023486330089L; { this.put("jpg", "FFD8FF"); // JPEG this.put("png", "89504E47"); // PNG this.put("gif", "47494638"); // GIF this.put("tif", "49492A00"); // TIFF this.put("bmp", "424D"); // Windows Bitmap this.put("dwg", "41433130"); // CAD this.put("html", "68746D6C3E"); // HTML this.put("rtf", "7B5C727466"); // Rich Text Format this.put("xml", "3C3F786D6C"); this.put("zip", "504B0304"); // ZIP Archive this.put("rar", "52617221"); // RAR Archive this.put("7z", "377ABCAF271C"); // 7z Archive this.put("psd", "38425053"); // PhotoShop this.put("eml", "44656C69766572792D646174653A"); // Email [thorough only] this.put("dbx", "CFAD12FEC5FD746F"); // Outlook Express this.put("pst", "2142444E"); // Outlook this.put("xls", "D0CF11E0"); // MS Word this.put("doc", "D0CF11E0"); // MS Excel 注意:word 和 excel的文件头一样 this.put("mdb", "5374616E64617264204A"); // MS Access this.put("wpd", "FF575043"); // WordPerfect this.put("eps", "252150532D41646F6265"); this.put("ps", "252150532D41646F6265"); this.put("pdf", "255044462D312E"); // Adobe Acrobat this.put("qdf", "AC9EBD8F"); // Quicken this.put("pwl", "E3828596"); // Windows Password this.put("wav", "57415645"); // Wave this.put("avi", "41564920"); this.put("ram", "2E7261FD"); // Real Audio this.put("rm", "2E524D46"); // Real Media this.put("mpg", "000001BA"); // Moving Pictures Experts Group this.put("mov", "6D6F6F76"); // QuickTime this.put("asf", "3026B2758E66CF11"); // Windows Media this.put("mid", "4D546864"); // MIDI } }; private FileUtils() {} /** * 获取文件类型 * * @param file 文件 * @return 文件类型 */ public static String getFileType(final File file) { String fileType = null; final byte[] b = new byte[50]; try { final InputStream is = new FileInputStream(file); is.read(b); fileType = FileUtils.getFileType(b); is.close(); } catch (final IOException e) { e.printStackTrace(); } return fileType; } /** * 获取文件类型 * * @param fileBytes 文件二进制数据 * @return 文件类型 */ public static String getFileType(final byte[] fileBytes) { final String fileTypeHex = String.valueOf(FileUtils.getFileHexString(fileBytes)); for (final Map.Entry entry : FileUtils.FILE_TYPE_MAP.entrySet()) { final String fileTypeHexValue = entry.getValue(); if (fileTypeHex.toUpperCase().startsWith(fileTypeHexValue)) { return entry.getKey(); } } return null; } public static String getFileHexString(final byte[] b) { final StringBuilder stringBuilder = new StringBuilder(); if (b == null || b.length <= 0) { return null; } for (final byte value : b) { final int v = value & 0xFF; final String hv = Integer.toHexString(v); if (hv.length() < 2) { stringBuilder.append(0); } stringBuilder.append(hv); } return stringBuilder.toString(); } } ================================================ FILE: back/src/main/java/com/msy/plus/util/IdCardUtils.java ================================================ package com.msy.plus.util; import lombok.extern.slf4j.Slf4j; import javax.validation.constraints.NotBlank; import java.time.LocalDate; import java.time.format.DateTimeFormatter; /** * 二代身份证工具 * *

【中华人民共和国国家标准GB11643-1999】关于公民身份号码的规定: 公民身份号码是特征组合码,由十七位数字本体码和一位数字校验码组成。 * *

排列顺序从左至右依次为:6位数字地址码,8位数字出生日期码,3位数字顺序码和1位数字校验码。 * *

1-2位数字:所在省份的代码; * *

3-4位数字:所在城市的代码; * *

5-6位数字:所在区县的代码; * *

7-14位数字:出生xxxx年xx月xx日; * *

15-16位数字:所在地的派出所的代码; * *

17位数字表示性别:奇数表示男性,偶数表示女性; (在同一地址码所标识的区域范围内,对同年、同月、同 日出生的人编定的顺序号,顺序码的奇数分配给男性,偶数分配给女性。) * *

18位数字是校检码:检验身份证的正确性。校检码可以是数字0~9和字符X。 * *

校验码的计算方法为: * *

1.将前面的身份证号码17位数分别乘以不同的系数。从第1位到第17位的系数分别为:7 9 10 5 8 4 2 1 6 3 7 9 10 5 8 4 2; * *

2.将这17位数字和系数相乘的结果相加; * *

3.用加出来和除以11,看余数是多少; * *

4.余数只可能有0 1 2 3 4 5 6 7 8 9 10这11个数字。其分别对应的最后一位身份证的号码为1 0 X 9 8 7 6 5 4 3 2; * (比如余数是2,就会在身份证的第18位数字上出现罗马数字的Ⅹ。如果余数是10,身份证的最后一位号码就是2) * * @author MoShuying * @date 2018/11/27 */ @Slf4j public class IdCardUtils { /** 省、直辖市代码表 */ private static final String AREA_CODE[][] = { {"11", "北京"}, {"12", "天津"}, {"13", "河北"}, {"14", "山西"}, {"15", "内蒙古"}, {"21", "辽宁"}, {"22", "吉林"}, {"23", "黑龙江"}, {"31", "上海"}, {"32", "江苏"}, {"33", "浙江"}, {"34", "安徽"}, {"35", "福建"}, {"36", "江西"}, {"37", "山东"}, {"41", "河南"}, {"42", "湖北"}, {"43", "湖南"}, {"44", "广东"}, {"45", "广西"}, {"46", "海南"}, {"50", "重庆"}, {"51", "四川"}, {"52", "贵州"}, {"53", "云南"}, {"54", "西藏"}, {"61", "陕西"}, {"62", "甘肃"}, {"63", "青海"}, {"64", "宁夏"}, {"65", "新疆"}, {"71", "台湾"}, {"81", "香港"}, {"82", "澳门"}, {"91", "国外"} }; /** 最后一位校验码 */ private static final String[] LAST_CODE = {"1", "0", "X", "9", "8", "7", "6", "5", "4", "3", "2"}; /** 每位加权因子 */ private static final int[] POWER = {7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2}; private IdCardUtils() {} public static void main(final String[] args) { System.out.println(getProvince("44532319960516121a", false)); } /** * 判断二代身份证合法性 * * @param idCard 身份证 * @return Boolean */ public static Boolean validate(@NotBlank final String idCard) { // 18位长度 if (idCard.length() != 18) { log.error("二代身份证长度错误"); return false; } // 前17位全为数字 final String idCard17 = idCard.substring(0, 17); if (!isDigital(idCard17)) { log.error("前17位不全为数字"); return false; } // 校验地区 if (getProvince(idCard, false) == null) { log.error("省份错误"); return false; } // 校验生日 if (getBirthday(idCard, false) == null) { log.error("生日错误"); return false; } // 校验第18位 final String idCard18Code = idCard.substring(17, 18); int powerSum = 0; for (int i = 0; i < 17; i++) { powerSum += Integer.parseInt(String.valueOf(idCard17.charAt(i))) * POWER[i]; } // 将对权值和取11模得到余数 final String lastCode = LAST_CODE[powerSum % 11]; // 身份的第18位与算出来的校码进行匹配 if (!idCard18Code.equalsIgnoreCase(lastCode)) { log.error("第18位错误"); return false; } return true; } /** * 判断是否全为数字 * * @param string 字符串 * @return Boolean */ private static Boolean isDigital(final String string) { final char[] cs = string.toCharArray(); for (final char c : cs) { if (48 > c || c > 57) { return false; } } return true; } /** * 获取省份 * * @param idCard 身份证 * @param toValidate 是否校验 * @return 省份 */ public static String getProvince(@NotBlank final String idCard, final boolean toValidate) { if (toValidate && !validate(idCard)) { return null; } final String provinceCode = getProvinceCode(idCard); for (final String[] area : AREA_CODE) { if (area[0].equals(provinceCode)) { return area[1]; } } return null; } /** * 获取生日 生日格式:yyyy-mm-dd * * @param idCard 身份证 * @param toValidate 是否校验 * @return 生日 */ public static String getBirthday(@NotBlank final String idCard, final boolean toValidate) { if (toValidate && !validate(idCard)) { return null; } try { final String birthday = getBirthdayCode(idCard); final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMdd"); return LocalDate.parse(birthday, dateTimeFormatter).toString(); } catch (final Exception e) { e.printStackTrace(); } return null; } /** * 获取性别 * * @param idCard 身份证 * @param toValidate 是否校验 * @return 女 | 男 */ public static String getSex(@NotBlank final String idCard, final boolean toValidate) { if (toValidate && !validate(idCard)) { return null; } final String sex = getSexCode(idCard); return (Integer.valueOf(sex) & 1) == 0 ? "女" : "男"; } private static String getProvinceCode(@NotBlank final String idCard) { return idCard.substring(0, 2); } private static String getBirthdayCode(@NotBlank final String idCard) { return idCard.substring(6, 14); } private static String getSexCode(@NotBlank final String idCard) { return idCard.substring(16, 17); } } ================================================ FILE: back/src/main/java/com/msy/plus/util/IdUtils.java ================================================ package com.msy.plus.util; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang3.RandomStringUtils; import java.nio.ByteBuffer; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.UUID; /** * ID工具 * * @author MoShuying * @date 2018/05/27 */ public class IdUtils { private static final DateTimeFormatter DTF_TIMES = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); private IdUtils() {} public static String uuid16() { return UUID.randomUUID().toString().replaceAll("-", "").toLowerCase(); } public static String uuid64() { final UUID uuid = UUID.randomUUID(); final ByteBuffer bb = ByteBuffer.wrap(new byte[16]); bb.putLong(uuid.getMostSignificantBits()); bb.putLong(uuid.getLeastSignificantBits()); return Base64.encodeBase64URLSafeString(bb.array()); } public static String timeId() { return DTF_TIMES.format(LocalDate.now()) + RandomStringUtils.randomNumeric(5); } } ================================================ FILE: back/src/main/java/com/msy/plus/util/IpUtils.java ================================================ package com.msy.plus.util; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import org.springframework.util.StringUtils; import javax.servlet.http.HttpServletRequest; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.InetAddress; import java.net.URL; import java.net.UnknownHostException; import java.util.Optional; /** * IP工具 * * @author MoShuying * @date 2018/05/27 */ public class IpUtils { private static final String UNKNOWN = "unknown"; private static final String LOCALHOST_IPV4 = "127.0.0.1"; private static final String LOCALHOST_IPV6 = "0:0:0:0:0:0:0:1"; private IpUtils() {} public static String getIpAddress() { return getIpAddress(ContextUtils.getRequest()); } /** * 获取请求中的 ip 地址 * * @param request request * @return IP */ public static String getIpAddress(final HttpServletRequest request) { String ip = LOCALHOST_IPV4; if (Optional.ofNullable(request).isPresent()) { ip = request.getHeader("x-forwarded-for"); if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_CLIENT_IP"); } if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_X_FORWARDED_FOR"); } if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); // request.getRemoteAddr() 获取客户端的 IP 地址在大部分情况下都是有效的 // 但是在通过了 Apache,Squid 等反向代理软件就不能获取到客户端的真实 IP 地址 // 如果通过了多级反向代理的话 X-Forwarded-For 的值并不止一个 // 而是一串 IP 值,例如:192.168.1.110,192.168.1.120,192.168.1.130,192.168.1.100 // 其中第一个 192.168.1.110 才是用户真实的 IP if (LOCALHOST_IPV4.equals(ip) || LOCALHOST_IPV6.equals(ip)) { // 根据网卡取本机配置的 IP,而不是环回地址 try { ip = InetAddress.getLocalHost().getHostAddress(); } catch (final UnknownHostException ignored) { } } } // 多个 IP 中取第一个 final String ch = ","; if (!StringUtils.isEmpty(ip) && ip.contains(ch)) { ip = ip.substring(0, ip.indexOf(ch)); } } return ip; } /** * 通过 IP 获取相关信息(需要联网,调用淘宝的IP库) * * @param ip ip * @return IP相关信息 */ public static String getInfoByIP(final String ip) { try { final URL url = new URL("http://ip.taobao.com/service/getIpInfo.php?ip=" + ip); final HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); connection.setDoOutput(true); connection.setDoInput(true); connection.setUseCaches(false); final InputStream in = connection.getInputStream(); final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(in)); final StringBuilder buffer = new StringBuilder(); String line = bufferedReader.readLine(); while (Optional.ofNullable(line).isPresent()) { buffer.append(line).append("\r\n"); line = bufferedReader.readLine(); } bufferedReader.close(); final JSONObject obj = (JSONObject) JSON.parse(buffer.toString()); final StringBuilder info = new StringBuilder(); final int responseCode = obj.getIntValue("code"); if (responseCode == 0) { final JSONObject data = obj.getJSONObject("data"); info.append(data.getString("country")).append(" "); info.append(data.getString("region")).append(" "); info.append(data.getString("city")).append(" "); info.append(data.getString("isp")); } return info.toString(); } catch (final IOException e) { e.printStackTrace(); return null; } } } ================================================ FILE: back/src/main/java/com/msy/plus/util/JsonUtils.java ================================================ package com.msy.plus.util; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.serializer.SimplePropertyPreFilter; import java.util.Arrays; /** * Json工具 * * @author MoShuying * @date 2018/07/11 */ public class JsonUtils { private JsonUtils() {} /** * 保留某些字段 * * @param target 目标对象 * @param fields 字段 * @return 保留字段后的对象 */ public static T keepFields(final Object target, final Class clz, final String... fields) { final SimplePropertyPreFilter filter = new SimplePropertyPreFilter(); filter.getIncludes().addAll(Arrays.asList(fields)); return done(target, clz, filter); } /** * 去除某些字段 * * @param target 目标对象 * @param fields 字段 * @return 去除字段后的对象 */ public static T deleteFields( final Object target, final Class clz, final String... fields) { final SimplePropertyPreFilter filter = new SimplePropertyPreFilter(); filter.getExcludes().addAll(Arrays.asList(fields)); return done(target, clz, filter); } private static T done( final Object target, final Class clz, final SimplePropertyPreFilter filter) { final String jsonString = JSON.toJSONString(target, filter); return JSON.parseObject(jsonString, clz); } } ================================================ FILE: back/src/main/java/com/msy/plus/util/RedisUtils.java ================================================ package com.msy.plus.util; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import javax.annotation.Resource; import javax.validation.constraints.NotBlank; import java.time.Duration; import java.util.*; import java.util.concurrent.TimeUnit; /** * Redis工具 * * @author MoShuying * @date 2018/05/27 */ @Component public class RedisUtils { @Resource private RedisTemplate redisTemplate; // =============================common============================ /** * 设置缓存失效时间 * * @param key 键 * @param timeout 时间 * @return {Boolean} */ public Boolean setExpire(@NotBlank final String key, @NotBlank final Duration timeout) { if (timeout.getSeconds() > 0) { return this.redisTemplate.expire(key, timeout.getSeconds(), TimeUnit.SECONDS); } return false; } /** * 获取缓存失效时间 * * @param key 键 * @return 时间(秒) 0为永久有效 */ public Long getExpire(@NotBlank final String key) { return this.redisTemplate.getExpire(key, TimeUnit.SECONDS); } /** * key 是否存在 * * @param key 键 * @return {Boolean} */ public Boolean hasKey(@NotBlank final String key) { return this.redisTemplate.hasKey(key); } /** * 删除缓存 * * @param keys 键 */ public Boolean delete(@NotBlank final String... keys) { return keys.length == Optional.ofNullable(this.redisTemplate.delete(Arrays.asList(keys))).orElse(-1L); } // ============================String============================= /** * 获取普通缓存 * * @param key 键 * @return 值 */ public Object getValue(@NotBlank final String key) { return this.redisTemplate.opsForValue().get(key); } /** * 设置普通缓存 * * @param key 键 * @param value 值 */ public void setValue(@NotBlank final String key, @NotBlank final Object value) { this.redisTemplate.opsForValue().set(key, value); } /** * 设置普通缓存 * * @param key 键 * @param value 值 * @param timeout 时间 小于等于0时将设为无限期 */ public void setValue( @NotBlank final String key, @NotBlank final Object value, @NotBlank final Duration timeout) { this.redisTemplate.opsForValue().set(key, value, timeout); } /** * 递增 * * @param key 键 * @param delta 要增加几(大于0) * @return 加上指定值之后 key 的值 */ public Long incrementValue(@NotBlank final String key, @NotBlank final long delta) { if (delta > 0) { throw new RuntimeException("递增因子必须大于0"); } return this.redisTemplate.opsForValue().increment(key, delta); } /** * 递减 * * @param key 键 * @param delta 要减少几(小于0) * @return 减少指定值之后 key 的值 */ public Long decrementValue(@NotBlank final String key, @NotBlank final long delta) { if (delta < 0) { throw new RuntimeException("递减因子必须大于0"); } return this.redisTemplate.opsForValue().increment(key, -delta); } // ================================Map================================= /** * HashGet * * @param key 键 * @param item 项 * @return 值 */ public Object getHash(@NotBlank final String key, @NotBlank final String item) { return this.redisTemplate.opsForHash().get(key, item); } /** * 获取hashKey对应的所有键值 * * @param key 键 * @return 对应的多个键值 */ public Map getHash(@NotBlank final String key) { return this.redisTemplate.opsForHash().entries(key); } /** * HashSet * * @param key 键 * @param map 对应多个键值 */ public void putHash(@NotBlank final String key, @NotBlank final Map map) { this.redisTemplate.opsForHash().putAll(key, map); } /** * HashSet 并设置时间 * * @param key 键 * @param map 对应多个键值 * @param timeout 时间 */ public void putHash( @NotBlank final String key, @NotBlank final Map map, @NotBlank final Duration timeout) { this.redisTemplate.opsForHash().putAll(key, map); this.setExpire(key, timeout); } /** * 向一张hash表中放入数据,如果不存在将创建 * * @param key 键 * @param item 项 * @param value 值 */ public void putHash( @NotBlank final String key, @NotBlank final String item, @NotBlank final Object value) { this.redisTemplate.opsForHash().put(key, item, value); } /** * 向一张hash表中放入数据,如果不存在将创建 * * @param key 键 * @param item 项 * @param value 值 * @param timeout 时间 注意:如果已存在的hash表有时间,这里将会替换原有的时间 */ public void putHash( @NotBlank final String key, @NotBlank final String item, @NotBlank final Object value, @NotBlank final Duration timeout) { this.redisTemplate.opsForHash().put(key, item, value); this.setExpire(key, timeout); } /** * 删除hash表中的值 * * @param key 键 * @param item 项 */ public void deleteHash(@NotBlank final String key, @NotBlank final Object... item) { this.redisTemplate.opsForHash().delete(key, item); } /** * 判断hash表中是否有该项的值 * * @param key 键 * @param item 项 * @return {Boolean} */ public Boolean hasKeyHash(@NotBlank final String key, @NotBlank final String item) { return this.redisTemplate.opsForHash().hasKey(key, item); } /** * hash递增 如果不存在,就会创建一个 并把新增后的值返回 * * @param key 键 * @param item 项 * @param by 要增加几(大于0) * @return 加上指定值之后 key 的值 */ public Double incrementHash( @NotBlank final String key, @NotBlank final String item, @NotBlank final double by) { return this.redisTemplate.opsForHash().increment(key, item, by); } /** * hash递减 * * @param key 键 * @param item 项 * @param by 要减少记(小于0) * @return 减少指定值之后 key 的值 */ public Double decrementHash( @NotBlank final String key, @NotBlank final String item, @NotBlank final double by) { return this.redisTemplate.opsForHash().increment(key, item, -by); } // ============================set============================= /** * 根据 key 获取 Set 中的所有值 * * @param key 键 * @return Set */ public Set getSet(@NotBlank final String key) { return this.redisTemplate.opsForSet().members(key); } /** * 根据 value 从一个 set 中查询,是否存在 * * @param key 键 * @param value 值 * @return {Boolean} */ public Boolean hasKeySet(@NotBlank final String key, @NotBlank final Object value) { return this.redisTemplate.opsForSet().isMember(key, value); } /** * 将数据放入set缓存 * * @param key 键 * @param values 值 * @return 放入个数 */ public Long addSet(@NotBlank final String key, @NotBlank final Object... values) { return this.redisTemplate.opsForSet().add(key, values); } /** * 将set数据放入缓存 * * @param key 键 * @param timeout 时间 * @param values 值 * @return 放入个数 */ public Long addSet( @NotBlank final String key, @NotBlank final Duration timeout, @NotBlank final Object... values) { final Long num = this.redisTemplate.opsForSet().add(key, values); this.setExpire(key, timeout); return num; } /** * 获取set缓存的长度 * * @param key 键 * @return 缓存的长度 */ public Long getSetSize(@NotBlank final String key) { return this.redisTemplate.opsForSet().size(key); } /** * 移除值为value的 * * @param key 键 * @param values 值 * @return 移除个数 */ public Long removeSet(@NotBlank final String key, @NotBlank final Object... values) { return this.redisTemplate.opsForSet().remove(key, values); } // ===============================list================================= /** * 获取list缓存的内容 * * @param key 键 * @param start 开始 * @param end 结束 0 到 -1代表所有值 * @return list缓存的内容 */ public List getList( @NotBlank final String key, @NotBlank final Long start, @NotBlank final Long end) { return this.redisTemplate.opsForList().range(key, start, end); } /** * 获取list缓存的长度 * * @param key 键 * @return list缓存的长度 */ public Long getListSize(@NotBlank final String key) { return this.redisTemplate.opsForList().size(key); } /** * 通过索引 获取list中的值 * * @param key 键 * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推 * @return list中的值 */ public Object getListIndex(@NotBlank final String key, @NotBlank final Long index) { return this.redisTemplate.opsForList().index(key, index); } /** * 将list放入缓存 * * @param key 键 * @param value 值 * @return 放入个数 */ public Long pushList(@NotBlank final String key, @NotBlank final Object value) { return this.redisTemplate.opsForList().rightPush(key, value); } /** * 将list放入缓存 * * @param key 键 * @param value 值 * @param timeout 时间 */ public Long pushList( @NotBlank final String key, @NotBlank final Object value, @NotBlank final Duration timeout) { final Long num = this.redisTemplate.opsForList().rightPush(key, value); this.setExpire(key, timeout); return num; } /** * 将list放入缓存 * * @param key 键 * @param value 值 * @return 放入个数 */ public Long pushList(@NotBlank final String key, @NotBlank final List value) { return this.redisTemplate.opsForList().rightPushAll(key, value); } /** * 将list放入缓存 * * @param key 键 * @param value 值 * @param timeout 时间 * @return 放入个数 */ public Long pushList( @NotBlank final String key, @NotBlank final List value, @NotBlank final Duration timeout) { final Long num = this.redisTemplate.opsForList().rightPushAll(key, value); this.setExpire(key, timeout); return num; } /** * 根据索引修改 list 中的某条数据 * * @param key 键 * @param index 索引 * @param value 值 */ public void updateListIndex( @NotBlank final String key, @NotBlank final Long index, @NotBlank final Object value) { this.redisTemplate.opsForList().set(key, index, value); } /** * 移除N个值为value * * @param key 键 * @param count 移除多少个 * @param value 值 * @return 移除个数 */ public Long removeList( @NotBlank final String key, @NotBlank final Long count, @NotBlank final Object value) { return this.redisTemplate.opsForList().remove(key, count, value); } } ================================================ FILE: back/src/main/java/com/msy/plus/util/UrlUtils.java ================================================ package com.msy.plus.util; import javax.servlet.ServletRequest; import javax.servlet.http.HttpServletRequest; /** * Url工具 * * @author MoShuying * @date 2018/07/13 */ public class UrlUtils { private UrlUtils() {} /** * 请求的相对路径 /user/list * * @param request request * @return 相对路径 */ public static String getMappingUrl(final ServletRequest request) { return getMappingUrl((HttpServletRequest) request); } public static String getMappingUrl(final HttpServletRequest request) { return request.getRequestURI().substring(request.getContextPath().length()); } } ================================================ FILE: back/src/main/resources/META-INF/spring-devtools.yml ================================================ # devtools 热重启导致 mapper 出错的解决 # https://github.com/abel533/MyBatis-Spring-Boot#spring-devtools-%E9%85%8D%E7%BD%AE restart: include: mapper: /mapper-[\\w-\\.]+jar pagehelper: /pagehelper-[\\w-\\.]+jar ================================================ FILE: back/src/main/resources/META-INF/swagger3.yml ================================================ application: # 项目名称 name: APIs doc # 项目版本信息 version: 1.0 # 项目描述信息 description: RESTFul APIs # 项目许可 license: Apache License 2.0 url: service: https://github.com/Zoctan/spring-boot-api-plus license: https://github.com/Zoctan/spring-boot-api-plus/blob/master/LICENSE # 扫描路径选择 apis.selector: com.msy.plus.controller # 作者信息 author: name: Zoctan url: https://github.com/Zoctan email: 752481828@qq.com ================================================ FILE: back/src/main/resources/application-dev.yml ================================================ server: port: 80 spring: devtools: restart: # 修改代码后自动重启 enabled: true servlet: multipart: # 最大文件上传大小 max-file-size: 20MB # 最大请求大小 max-request-size: 20MB # 数据源(应该全部加密) datasource: druid: # 连接,注意各个配置,尤其是要一次性执行多条 SQL 时,要 allowMultiQueries=true url: jdbc:mysql://localhost:3306/crm3?useUnicode=true&useSSL=false&useLegacyDatetimeCode=false&allowMultiQueries=true&characterEncoding=utf-8&serverTimezone=UTC # 用户名 root username: root # 密码 root password: root # 驱动类 driver-class-name: MyEnc({mfkB3F21902N35InZN3DR4jdSuJR1w2bo+3Z4w1jgtfWVkHRZuclaw==}) # 连接池配置 连接数量、最小、最大、获取连接等待超时的时间 initial-size: 1 min-idle: 1 max-active: 20 max-wait: 60000 # 配置一个连接在池中最小生存的时间,单位是毫秒 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false # 配置监控统计拦截的过滤器,去掉后监控界面 SQL 无法统计,wall 用于 SQL 防火墙防注入 filters: stat,wall # WebStatFilter 配置 web-stat-filter: enabled: true url-pattern: /* # 不统计 exclusions: /druid/*,*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico session-stat-enable: true session-stat-max-count: 10 principal-session-name: session_name principal-cookie-name: cookie_name # StatViewServlet 配置 stat-view-servlet: enabled: true # 配置 DruidStatViewServlet url-pattern: /druid/* # 禁止手动重置监控数据 reset-enable: false # 监控页面登录的用户名/密码 login-username: admin login-password: admin # IP白名单(没有配置或者为空,则允许所有访问) allow: 127.0.0.1 # IP黑名单(存在共同时,deny优先于allow) deny: # Spring 监控,对内部各接口调用的监控 aop-patterns: com.msy.plus.controller.*,com.msy.plus.dto.*,com.msy.plus.mapper.*,com.msy.plus.service.* cache: # 缓存类型 type: redis redis: # key 前缀 key-prefix: msy.plus[DEV] # 过期时间 time-to-live: 60s redis: # 数据库索引(默认为0) database: 0 # 服务器地址 host: 127.0.0.1 # 服务器连接端口 port: 6379 # 服务器连接密码 root password: jedis.pool: # 连接池最大连接数(使用负值表示没有限制) max-active: 8 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 8 # 连接池中的最小空闲连接 min-idle: 0 logging: # 日志级别 level.com.msy.plus: debug # Json web token jwt: # 过期时间(分钟) expire-time: 300m # claim 权限 key claim-key-auth: auth # 请求头或请求参数的 key header: Authorization # token 类型 token-type: Bearer upload: # 上传路径 local-path: /tmp/ # 最小文件上传大小 min: 1KB # 最大文件上传大小 max: 10MB ================================================ FILE: back/src/main/resources/application-test.yml ================================================ server: port: 8082 ================================================ FILE: back/src/main/resources/application.yml ================================================ spring: profiles: # 激活的配置 active: dev # 终端彩色输出信息 output.ansi.enabled: ALWAYS resources: # 不映射工程中的静态资源文件比如:html、css # 如果某些情况需要映射 # 比如 swagger2,可以在 addResourceHandlers 和 addViewControllers 中特别添加,参考 WebMvcConfig add-mappings: false mvc: # 当出现 404 错误时,直接抛出异常(默认是显示一个错误页面) throw-exception-if-no-handler-found: true freemarker: # 关闭模版检查 checkTemplateLocation: false rsa: # 私钥 private-key: MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEArD4P1yMYRsS4YSEbB7V3LQs/+6MrTbAdnlF8CdangeD89gRrp9sz8MutI1s2xOPnpavSv8HeB3VpwE1Iw1WK2QIDAQABAkB4wFWolJD7ZASDC4uAnwZ6zK1Bg8XjA/nvuN6Fozfxw5s40HSPyild32CX47fCOYlt94shRrNaIHIN78N8+ioVAiEA4hWDEnzyqT1mkrLCdgVNnH36aow3/jonp0trQpSagKcCIQDDCLEDfjcPUovDkp9XQZ3LlYU8+zPGJ9Nccck0YtGIfwIhAOGGcgScTXhTfqGx3lfavGvyIz3r9+MLYgj5K9rz4BebAiB4CAtZSP598aGO1dg3DW0d9IGxzDBLDguo42afVQn75QIgBy9s8n1ZyWyLloCBb4+Wf0iTOUJC7II9Xq1LUF2QJGo= # 公钥 public-key: MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKw+D9cjGEbEuGEhGwe1dy0LP/ujK02wHZ5RfAnWp4Hg/PYEa6fbM/DLrSNbNsTj56Wr0r/B3gd1acBNSMNVitkCAwEAAQ== jasypt: encryptor: # 自定义的加密器 bean: myStringEncryptor # 自定义被加密值的发现器 property: detector-bean: myEncryptablePropertyDetector # 先 RSA,后 Base64 加密的密码 # 在 JasyptConfig#myStringEncryptor 中先解密后再使用 password: fnMa4sWpCFSG1Wl3+tkjSRKfdApiZBGms5NE75TqzudMq1/9py5uvKk7urU4dKnuV+3/Tq69Y2E4gohJlAD3cA== mybatis: # 存放实体的位置 type-aliases-package: com.msy.plus.entity # 存放 mapper 映射文件的位置 mapper-locations: classpath:mapper/*.xml mapper: # 多个接口时逗号隔开 mappers: com.msy.plus.core.mapper.MyMapper # insert 和 update 中,判断字符串类型 != '' not-empty: false # 取回主键的方式 identity: MYSQL # 分页插件 # https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/zh/HowToUse.md pagehelper: # pageSize=0 时查出所有结果,相当于没分页 page-size-zero: true # 数据库方言 helperDialect: mysql # 分页合理化 # pageNum <= 0 时会查询第一页 # pageNum > pages(超过总数时),会查询最后一页 reasonable: true # 支持通过 Mapper 接口参数来传递分页参数 supportMethodsArguments: true # 日志 #logging: # # 以文件方式记录日志 # file: plus.log # # 设置目录 # path: /var/log ================================================ FILE: back/src/main/resources/banner.txt ================================================ //////////////////////////////////////// // // // :: // // ttttii;;, // // itttttii;i;, // // tiiitiiitji;;i;, // // :,.iiiiiiiii;;i;; // // iiitiiii f jjjjtttti, // // ,;iiiiii j G G iiiittii // // iiii f GE :iiitii // // ii DG ti: // // ; DG ti // // D ; // // DL // // GL // // GG // // Great oaks // // from little acorns grow. // // // //////////////////////////////////////// ================================================ FILE: back/src/main/resources/mapper/AccountMapper.xml ================================================ UPDATE employee SET login_time = NOW() WHERE name = #{name} ================================================ FILE: back/src/main/resources/mapper/CustomerFollowUpHistoryMapper.xml ================================================ ================================================ FILE: back/src/main/resources/mapper/CustomerHandoverMapper.xml ================================================ ================================================ FILE: back/src/main/resources/mapper/CustomerManagerMapper.xml ================================================ ================================================ FILE: back/src/main/resources/mapper/DepartmentMapper.xml ================================================ ================================================ FILE: back/src/main/resources/mapper/DictionaryContentsMapper.xml ================================================ ================================================ FILE: back/src/main/resources/mapper/DictionaryDetailsMapper.xml ================================================ ================================================ FILE: back/src/main/resources/mapper/EmployeeMapper.xml ================================================ delete from employee_role where employeeId = #{id} delete from employee_role where employeeId = #{id} and roleId = #{roleId} ================================================ FILE: back/src/main/resources/mapper/PermissionMapper.xml ================================================ ================================================ FILE: back/src/main/resources/mapper/RoleMapper.xml ================================================ INSERT INTO employee_role (employeeId, roleId) VALUES (#{accountId}, (SELECT r.id FROM role r WHERE name="USER")) insert into role_permission (role_id, permission_id) values ( #{roleId}, #{item} ) delete from role_permission where role_id = #{roleId} and permission_id = #{permissionId} ================================================ FILE: back/src/main/resources/rsa/private-key.pem ================================================ -----BEGIN PRIVATE KEY----- MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEArD4P1yMYRsS4YSEb B7V3LQs/+6MrTbAdnlF8CdangeD89gRrp9sz8MutI1s2xOPnpavSv8HeB3VpwE1I w1WK2QIDAQABAkB4wFWolJD7ZASDC4uAnwZ6zK1Bg8XjA/nvuN6Fozfxw5s40HSP yild32CX47fCOYlt94shRrNaIHIN78N8+ioVAiEA4hWDEnzyqT1mkrLCdgVNnH36 aow3/jonp0trQpSagKcCIQDDCLEDfjcPUovDkp9XQZ3LlYU8+zPGJ9Nccck0YtGI fwIhAOGGcgScTXhTfqGx3lfavGvyIz3r9+MLYgj5K9rz4BebAiB4CAtZSP598aGO 1dg3DW0d9IGxzDBLDguo42afVQn75QIgBy9s8n1ZyWyLloCBb4+Wf0iTOUJC7II9 Xq1LUF2QJGo= -----END PRIVATE KEY----- ================================================ FILE: back/src/main/resources/rsa/public-key.pem ================================================ -----BEGIN PUBLIC KEY----- MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKw+D9cjGEbEuGEhGwe1dy0LP/ujK02w HZ5RfAnWp4Hg/PYEa6fbM/DLrSNbNsTj56Wr0r/B3gd1acBNSMNVitkCAwEAAQ== -----END PUBLIC KEY----- ================================================ FILE: back/src/test/java/CodeGenerator.java ================================================ import com.google.common.base.CaseFormat; import freemarker.template.TemplateExceptionHandler; import org.apache.commons.lang3.StringUtils; import org.mybatis.generator.api.MyBatisGenerator; import org.mybatis.generator.config.*; import org.mybatis.generator.internal.DefaultShellCallback; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.*; import static com.msy.plus.core.constant.ProjectConstant.*; /** * 代码生成器 根据数据表名称生成对应的 Entity、Mapper、Service、Controller 简化开发 * * @author MoShuying * @date 2018/05/27 */ class CodeGenerator { private static final String DATABASE = "mysql"; // JDBC配置,请修改为你项目的实际配置 private static final String JDBC_URL = "jdbc:mysql://localhost:3306/crm3" + "?useUnicode=true&characterEncoding=utf-8&useLegacyDatetimeCode=false&serverTimezone=UTC"; private static final String JDBC_USERNAME = "crm3"; private static final String JDBC_PASSWORD = "a1Bks32BAsdj12"; private static final String JDBC_DIVER_CLASS_NAME = "com.mysql.cj.jdbc.Driver"; // 项目在硬盘上的基础路径 private static final String PROJECT_PATH = System.getProperty("user.dir"); // 模板位置 private static final String TEMPLATE_FILE_PATH = CodeGenerator.PROJECT_PATH + "/src/test/resources/generator/template"; // java文件路径 private static final String JAVA_PATH = "/src/main/java"; // 资源文件路径 private static final String RESOURCES_PATH = "/src/main/resources"; // 生成的Service存放路径 private static final String PACKAGE_PATH_SERVICE = CodeGenerator.packageConvertPath(SERVICE_PACKAGE); // 生成的Service实现存放路径 private static final String PACKAGE_PATH_SERVICE_IMPL = CodeGenerator.packageConvertPath(SERVICE_IMPL_PACKAGE); // 生成的Controller存放路径 private static final String PACKAGE_PATH_CONTROLLER = CodeGenerator.packageConvertPath(CONTROLLER_PACKAGE); // @author private static final String AUTHOR = "MoShuYing"; // @date private static final String DATE = new SimpleDateFormat("yyyy/MM/dd").format(new Date()); private static final boolean isRestful = true; public static void main(final String[] args) { final Scanner scanner = new Scanner(System.in); System.out.print("可能已存在相关文件,请尽可能确保无误。y/n:"); if (!scanner.next().equals("y")) { return; } CodeGenerator.genCode("customer_follow_up_history"); // genCodeByCustomModelName("employee",null); } /** * 通过数据表名称生成代码,Model 名称通过解析数据表名称获得,下划线转大驼峰的形式。 如输入表名称 "t_user_detail" 将生成 * TUserDetail、TUserDetailMapper、TUserDetailService ... * * @param tableNames 数据表名称... */ private static void genCode(final String... tableNames) { for (final String tableName : tableNames) { CodeGenerator.genCodeByCustomModelName(tableName, null); } } /** * 通过数据表名称,和自定义的 Model 名称生成代码 如输入表名称 "t_user_detail" 和自定义的 Model 名称 "sysUser" 将生成 * sysUser、UserMapper、UserService ... * * @param tableName 数据表名称 * @param modelName 自定义的 Model 名称 */ private static void genCodeByCustomModelName(final String tableName, final String modelName) { CodeGenerator.genModelAndMapper(tableName, modelName); CodeGenerator.genService(tableName, modelName); CodeGenerator.genController(tableName, modelName); } private static void genModelAndMapper(final String tableName, String modelName) { final Context context = new Context(ModelType.FLAT); context.setId("Potato"); context.setTargetRuntime("MyBatis3Simple"); context.addProperty(PropertyRegistry.CONTEXT_BEGINNING_DELIMITER, "`"); context.addProperty(PropertyRegistry.CONTEXT_ENDING_DELIMITER, "`"); final JDBCConnectionConfiguration jdbcConnectionConfiguration = new JDBCConnectionConfiguration(); jdbcConnectionConfiguration.setConnectionURL(CodeGenerator.JDBC_URL); jdbcConnectionConfiguration.setUserId(CodeGenerator.JDBC_USERNAME); jdbcConnectionConfiguration.setPassword(CodeGenerator.JDBC_PASSWORD); jdbcConnectionConfiguration.setDriverClass(CodeGenerator.JDBC_DIVER_CLASS_NAME); context.setJdbcConnectionConfiguration(jdbcConnectionConfiguration); final PluginConfiguration pluginConfiguration = new PluginConfiguration(); pluginConfiguration.setConfigurationType("tk.mybatis.mapper.generator.MapperPlugin"); pluginConfiguration.addProperty("mappers", MAPPER_INTERFACE_REFERENCE); context.addPluginConfiguration(pluginConfiguration); final JavaModelGeneratorConfiguration javaModelGeneratorConfiguration = new JavaModelGeneratorConfiguration(); javaModelGeneratorConfiguration.setTargetProject( CodeGenerator.PROJECT_PATH + CodeGenerator.JAVA_PATH); javaModelGeneratorConfiguration.setTargetPackage(ENTITY_PACKAGE); context.setJavaModelGeneratorConfiguration(javaModelGeneratorConfiguration); final SqlMapGeneratorConfiguration sqlMapGeneratorConfiguration = new SqlMapGeneratorConfiguration(); sqlMapGeneratorConfiguration.setTargetProject( CodeGenerator.PROJECT_PATH + CodeGenerator.RESOURCES_PATH); sqlMapGeneratorConfiguration.setTargetPackage("mapper"); context.setSqlMapGeneratorConfiguration(sqlMapGeneratorConfiguration); final JavaClientGeneratorConfiguration javaClientGeneratorConfiguration = new JavaClientGeneratorConfiguration(); javaClientGeneratorConfiguration.setTargetProject( CodeGenerator.PROJECT_PATH + CodeGenerator.JAVA_PATH); javaClientGeneratorConfiguration.setTargetPackage(MAPPER_PACKAGE); javaClientGeneratorConfiguration.setConfigurationType("XMLMAPPER"); context.setJavaClientGeneratorConfiguration(javaClientGeneratorConfiguration); final TableConfiguration tableConfiguration = new TableConfiguration(context); tableConfiguration.setTableName(tableName); if (StringUtils.isNotEmpty(modelName)) { tableConfiguration.setDomainObjectName(modelName); } final GeneratedKey generatedKey; if ("oracle".equalsIgnoreCase(CodeGenerator.DATABASE)) { generatedKey = new GeneratedKey("id", "SELECT SEQ_{1}.NEXTVAL FROM DUAL", false, "pre"); } else { generatedKey = new GeneratedKey("id", "MYSQL", true, null); } tableConfiguration.setGeneratedKey(generatedKey); context.addTableConfiguration(tableConfiguration); final List warnings; final MyBatisGenerator generator; try { final Configuration config = new Configuration(); config.addContext(context); config.validate(); final DefaultShellCallback callback = new DefaultShellCallback(true); warnings = new ArrayList<>(); generator = new MyBatisGenerator(config, callback, warnings); generator.generate(null); } catch (final Exception e) { throw new RuntimeException("生成 Model 和 Mapper 失败", e); } if (generator.getGeneratedJavaFiles().isEmpty() || generator.getGeneratedXmlFiles().isEmpty()) { throw new RuntimeException("生成 Model 和 Mapper 失败:" + warnings); } if (StringUtils.isEmpty(modelName)) { modelName = CodeGenerator.tableNameConvertUpperCamel(tableName); } System.out.println(modelName + ".java 生成成功"); System.out.println(modelName + "MyMapper.java 生成成功"); System.out.println(modelName + "MyMapper.xml 生成成功"); } private static void genService(final String tableName, final String modelName) { try { final freemarker.template.Configuration cfg = CodeGenerator.getConfiguration(); final Map data = new HashMap<>(); data.put("date", CodeGenerator.DATE); data.put("author", CodeGenerator.AUTHOR); final String modelNameUpperCamel = StringUtils.isEmpty(modelName) ? CodeGenerator.tableNameConvertUpperCamel(tableName) : modelName; data.put("modelNameUpperCamel", modelNameUpperCamel); data.put("modelNameLowerCamel", CodeGenerator.tableNameConvertLowerCamel(tableName)); data.put("basePackage", BASE_PACKAGE); final File file = CodeGenerator.createFileDir( CodeGenerator.PROJECT_PATH + CodeGenerator.JAVA_PATH + CodeGenerator.PACKAGE_PATH_SERVICE + modelNameUpperCamel + "Service.java"); cfg.getTemplate("service.ftl").process(data, new FileWriter(file)); System.out.println(modelNameUpperCamel + "Service.java 生成成功"); final File file1 = CodeGenerator.createFileDir( CodeGenerator.PROJECT_PATH + CodeGenerator.JAVA_PATH + CodeGenerator.PACKAGE_PATH_SERVICE_IMPL + modelNameUpperCamel + "ServiceImpl.java"); cfg.getTemplate("service-impl.ftl").process(data, new FileWriter(file1)); System.out.println(modelNameUpperCamel + "ServiceImpl.java 生成成功"); } catch (final Exception e) { throw new RuntimeException("生成Service失败", e); } } private static File createFileDir(final String name) throws RuntimeException { final File file = new File(name); if (!file.getParentFile().exists()) { final boolean createSuccess = file.getParentFile().mkdirs(); if (!createSuccess) { throw new RuntimeException("文件夹创建失败"); } } return file; } private static void genController(final String tableName, final String modelName) { try { final freemarker.template.Configuration cfg = CodeGenerator.getConfiguration(); final Map data = new HashMap<>(); data.put("date", CodeGenerator.DATE); data.put("author", CodeGenerator.AUTHOR); final String modelNameUpperCamel = StringUtils.isEmpty(modelName) ? CodeGenerator.tableNameConvertUpperCamel(tableName) : modelName; data.put( "baseRequestMapping", CodeGenerator.modelNameConvertMappingPath(modelNameUpperCamel)); data.put("modelNameUpperCamel", modelNameUpperCamel); data.put( "modelNameLowerCamel", CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_CAMEL, modelNameUpperCamel)); data.put("basePackage", BASE_PACKAGE); final File file = CodeGenerator.createFileDir( CodeGenerator.PROJECT_PATH + CodeGenerator.JAVA_PATH + CodeGenerator.PACKAGE_PATH_CONTROLLER + modelNameUpperCamel + "Controller.java"); if (CodeGenerator.isRestful) { cfg.getTemplate("controller-restful.ftl").process(data, new FileWriter(file)); } else { cfg.getTemplate("controller.ftl").process(data, new FileWriter(file)); } System.out.println(modelNameUpperCamel + "Controller.java 生成成功"); } catch (final Exception e) { throw new RuntimeException("生成Controller失败", e); } } private static freemarker.template.Configuration getConfiguration() throws IOException { final freemarker.template.Configuration cfg = new freemarker.template.Configuration(freemarker.template.Configuration.VERSION_2_3_23); cfg.setDirectoryForTemplateLoading(new File(CodeGenerator.TEMPLATE_FILE_PATH)); cfg.setDefaultEncoding("UTF-8"); cfg.setTemplateExceptionHandler(TemplateExceptionHandler.IGNORE_HANDLER); return cfg; } private static String tableNameConvertLowerCamel(final String tableName) { return CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, tableName.toLowerCase()); } private static String tableNameConvertUpperCamel(final String tableName) { return CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, tableName.toLowerCase()); } private static String tableNameConvertMappingPath(String tableName) { tableName = tableName.toLowerCase(); // 兼容使用大写的表名 return "/" + (tableName.contains("_") ? tableName.replaceAll("_", "/") : tableName); } private static String modelNameConvertMappingPath(final String modelName) { final String tableName = CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, modelName); return CodeGenerator.tableNameConvertMappingPath(tableName); } private static String packageConvertPath(final String packageName) { return String.format( "/%s/", packageName.contains(".") ? packageName.replaceAll("\\.", "/") : packageName); } } ================================================ FILE: back/src/test/java/JasyptStringEncryptor.java ================================================ import com.msy.plus.Application; import org.jasypt.encryption.StringEncryptor; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; /** * jasypt 用于加密配置文件 https://github.com/ulisesbocchio/jasypt-spring-boot * * @author MoShuying * @date 2018/05/27 */ @RunWith(SpringRunner.class) @SpringBootTest(classes = Application.class) public class JasyptStringEncryptor { @Qualifier("myStringEncryptor") @Autowired private StringEncryptor stringEncryptor; @Test public void encode() throws Exception { final String mysql = this.stringEncryptor.encrypt("com.mysql.cj.jdbc.Driver"); final String name = this.stringEncryptor.encrypt("crm3"); final String password = this.stringEncryptor.encrypt("123456"); System.err.println("name = " + name); System.err.println("mysql = " + mysql); System.err.println("password = " + password); } } ================================================ FILE: back/src/test/java/PasswordEncryptor.java ================================================ import com.msy.plus.Application; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.test.context.junit4.SpringRunner; /** * @author MoShuying * @date 2018/05/27 */ @RunWith(SpringRunner.class) @SpringBootTest(classes = Application.class) public class PasswordEncryptor { @Autowired private PasswordEncoder passwordEncoder; @Test public void encode() throws Exception { final String admin = this.passwordEncoder.encode("admin"); final String user = this.passwordEncoder.encode("1234657"); System.err.println("admin password = " + admin); System.err.println("user password = " + user); } } // MyEnc({cSs3wYoZ0BTijYqdYVj9xg==}) ================================================ FILE: back/src/test/java/RsaEncryptor.java ================================================ import com.msy.plus.core.rsa.RsaUtils; import org.junit.Assert; import org.junit.Test; import org.springframework.util.Base64Utils; import java.security.KeyPair; import java.security.PrivateKey; import java.security.PublicKey; import java.util.Base64; /** * RSA工具测试 * * @author MoShuying * @date 2018/05/27 */ public class RsaEncryptor { private final RsaUtils rsaUtil = new RsaUtils(); /** 加载公私钥pem格式文件测试 */ @Test public void test1() throws Exception { final PublicKey publicKey = this.rsaUtil.loadPublicKey(); final PrivateKey privateKey = this.rsaUtil.loadPrivateKey(); Assert.assertNotNull(publicKey); Assert.assertNotNull(privateKey); System.out.println("公钥:" + publicKey); System.out.println("私钥:" + privateKey); final String data = "msy"; // 公钥加密 final byte[] encrypted = this.rsaUtil.encrypt(data.getBytes()); System.out.println("加密后:" + Base64Utils.encodeToString(encrypted)); // 私钥解密 final byte[] decrypted = this.rsaUtil.decrypt(encrypted); System.out.println("解密后:" + new String(decrypted)); } /** 生成RSA密钥对并进行加解密测试 */ @Test public void test2() throws Exception { final String data = "hello word"; final KeyPair keyPair = RsaUtils.genKeyPair(512); // 获取公钥,并以base64格式打印出来 final PublicKey publicKey = keyPair.getPublic(); System.out.println("公钥:" + new String(Base64.getEncoder().encode(publicKey.getEncoded()))); // 获取私钥,并以base64格式打印出来 final PrivateKey privateKey = keyPair.getPrivate(); System.out.println("私钥:" + new String(Base64.getEncoder().encode(privateKey.getEncoded()))); // 公钥加密 final byte[] encrypted = RsaUtils.encrypt(data.getBytes(), publicKey); System.out.println("加密后:" + new String(encrypted)); // 私钥解密 final byte[] decrypted = RsaUtils.decrypt(encrypted, privateKey); System.out.println("解密后:" + new String(decrypted)); } } ================================================ FILE: back/src/test/java/com/msy/plus/AccountControllerTest.java ================================================ package com.msy.plus; import com.msy.plus.dto.AccountDTO; import com.msy.plus.dto.AccountLoginDTO; import org.junit.FixMethodOrder; import org.junit.Test; import org.junit.runners.MethodSorters; /** * 账户接口测试 * * @author MoShuying * @date 2018/11/29 */ @FixMethodOrder(MethodSorters.NAME_ASCENDING) public class AccountControllerTest extends BaseControllerTest { private final String resource = "/account"; /** register */ @Test(timeout = 5000) public void test1() throws Exception { final String targetUrl = this.resource; final AccountDTO account = new AccountDTO(); account.setEmail("12345@qq.com"); account.setName("xxxxx"); account.setPassword("12345"); this.post(targetUrl, account, null); } /** login */ @Test(timeout = 5000) public void test2() throws Exception { final String targetUrl = this.resource + "/token"; final AccountLoginDTO accountLogin = new AccountLoginDTO(); accountLogin.setName("xxxxx"); accountLogin.setPassword("12345"); this.post(targetUrl, accountLogin, null); } /** logout */ @Test(timeout = 5000) public void test3() throws Exception { final String targetUrl = this.resource + "/token"; final AccountLoginDTO accountLogin = new AccountLoginDTO(); accountLogin.setName("admin"); accountLogin.setPassword("admin"); // 先登录获取token final String token = (String) this.post(targetUrl, accountLogin, null).getData(); this.delete(targetUrl, null, token); } /** update */ @Test(timeout = 5000) @WithCustomUser(name = "user") public void test4() throws Exception { final String targetUrl = this.resource; final AccountDTO accountDTO = new AccountDTO(); accountDTO.setName("user"); accountDTO.setEmail("xxxxx@qq.com"); this.patch(targetUrl, accountDTO, null); } /** detail */ @Test(timeout = 5000) @WithCustomUser(name = "xxxxx") public void test5() throws Exception { final String targetUrl = this.resource + "/3"; this.get(targetUrl, null, null); } /** list */ @Test(timeout = 5000) @WithCustomUser(name = "user") public void test6() throws Exception { final String targetUrl = this.resource + "?page=1&size=3"; this.get(targetUrl, null, null); } /** delete */ @Test(timeout = 5000) @WithCustomUser(name = "admin") public void test7() throws Exception { final String targetUrl = this.resource + "/3"; this.delete(targetUrl, null, null); } } ================================================ FILE: back/src/test/java/com/msy/plus/BaseControllerTest.java ================================================ package com.msy.plus; import com.alibaba.fastjson.JSON; import com.msy.plus.core.response.Result; import com.msy.plus.filter.AuthenticationFilter; import com.msy.plus.filter.CorsFilter; import org.apache.commons.lang3.StringUtils; import org.junit.Before; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.test.annotation.Rollback; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.context.WebApplicationContext; import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** * MockMvc 测试控制器 * * @author MoShuying * @date 2018/11/29 */ // 测试时如果为 true,就不会修改数据库数据 @Rollback(value = false) @Transactional @AutoConfigureMockMvc @RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest(classes = {Application.class}) public abstract class BaseControllerTest { @Autowired protected WebApplicationContext context; @Autowired protected CorsFilter corsFilter; @Autowired protected AuthenticationFilter authenticationFilter; protected MockMvc mockMvc; @Before public void setUp() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .addFilters(this.corsFilter) // 不添加则无法维持 SecurityContext .apply(springSecurity()) .build(); } private Result execute( final HttpMethod method, final String targetUrl, final Object args, final String token) throws Exception { final MockHttpServletRequestBuilder builders = MockMvcRequestBuilders.request(method, targetUrl) .contentType(MediaType.APPLICATION_JSON); if (args != null) { builders.content(JSON.toJSONString(args)); } if (StringUtils.isNotBlank(token)) { builders.header("Authorization", token); } return JSON.parseObject( this.mockMvc .perform(builders) .andDo(print()) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) .andReturn() .getResponse() .getContentAsString(), Result.class); } protected Result get(final String targetUrl, final Object args, final String token) throws Exception { return this.execute(HttpMethod.GET, targetUrl, args, token); } protected Result post(final String targetUrl, final Object args, final String token) throws Exception { return this.execute(HttpMethod.POST, targetUrl, args, token); } protected Result delete(final String targetUrl, final Object args, final String token) throws Exception { return this.execute(HttpMethod.DELETE, targetUrl, args, token); } protected Result patch(final String targetUrl, final Object args, final String token) throws Exception { return this.execute(HttpMethod.PATCH, targetUrl, args, token); } } ================================================ FILE: back/src/test/java/com/msy/plus/WithCustomSecurityContextFactory.java ================================================ package com.msy.plus; import com.msy.plus.service.impl.UserDetailsServiceImpl; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.test.context.support.WithSecurityContextFactory; import javax.annotation.Resource; /** * 设置用户登陆时的 SecurityContext * * @author MoShuying * @date 2018/11/29 */ public class WithCustomSecurityContextFactory implements WithSecurityContextFactory { @Resource private UserDetailsServiceImpl userDetailsService; @Override public SecurityContext createSecurityContext(final WithCustomUser customUser) { final SecurityContext context = SecurityContextHolder.createEmptyContext(); final UserDetails userDetails = this.userDetailsService.loadUserByUsername(customUser.name()); final Authentication auth = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); context.setAuthentication(auth); return context; } } ================================================ FILE: back/src/test/java/com/msy/plus/WithCustomUser.java ================================================ package com.msy.plus; import org.springframework.security.test.context.support.WithSecurityContext; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * 访问控制器时以某用户已登录状态操作 * * @author MoShuying * @date 2018/11/29 */ @Retention(RetentionPolicy.RUNTIME) @WithSecurityContext(factory = WithCustomSecurityContextFactory.class) public @interface WithCustomUser { String name() default "admin"; } ================================================ FILE: back/src/test/java/com/msy/plus/util/JsonUtilsTest.java ================================================ package com.msy.plus.util; import com.alibaba.fastjson.JSONObject; import com.msy.plus.entity.AccountDO; import org.junit.Test; import java.util.Arrays; import java.util.List; public class JsonUtilsTest { private List getAccountList() { final AccountDO account1 = new AccountDO() { { this.setName("ll"); this.setId(1L); this.setEmail("ll@qq.com"); this.setPassword("llllllllllll"); } }; final AccountDO account2 = new AccountDO() { { this.setName("aa"); this.setId(2L); this.setEmail("aa@qq.com"); this.setPassword("aaaaaaaaaa"); } }; return Arrays.asList(account1, account2); } @Test public void keepFields() throws Exception { final List accountList = JsonUtils.keepFields(this.getAccountList(), List.class, "password"); for (final JSONObject account : accountList) { assert account.get("id") == null; assert account.get("password") != null; } } @Test public void deleteFields() throws Exception { final List accountList = JsonUtils.deleteFields(this.getAccountList(), List.class, "password"); for (final JSONObject account : accountList) { assert account.get("id") != null; assert account.get("password") == null; } } } ================================================ FILE: back/src/test/resources/generator/template/controller-restful.ftl ================================================ package ${basePackage}.controller; import ${basePackage}.core.response.Result; import ${basePackage}.core.response.ResultGenerator; import ${basePackage}.entity.${modelNameUpperCamel}; import ${basePackage}.service.${modelNameUpperCamel}Service; import io.swagger.v3.oas.annotations.Operation; import io.swagger.annotations.Api; import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiImplicitParams; import io.swagger.annotations.ApiOperation; import org.springframework.security.access.prepost.PreAuthorize; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import java.util.List; /** * @author ${author} * @date ${date} */ @PreAuthorize("hasAuthority('ADMIN')") @Api(tags={"生成接口"}) @RestController @RequestMapping("${baseRequestMapping}") public class ${modelNameUpperCamel}Controller { @Resource private ${modelNameUpperCamel}Service ${modelNameLowerCamel}Service; @Operation(description = "生成添加") @PostMapping public Result add(@RequestBody ${modelNameUpperCamel} ${modelNameLowerCamel}) { ${modelNameLowerCamel}Service.save(${modelNameLowerCamel}); return ResultGenerator.genOkResult(); } @Operation(description = "生成删除") @DeleteMapping("/{id}") public Result delete(@PathVariable Long id) { ${modelNameLowerCamel}Service.deleteById(id); return ResultGenerator.genOkResult(); } @Operation(description = "生成更新") @PutMapping public Result update(@RequestBody ${modelNameUpperCamel} ${modelNameLowerCamel}) { ${modelNameLowerCamel}Service.update(${modelNameLowerCamel}); return ResultGenerator.genOkResult(); } @Operation(description = "生成获取详细信息") @GetMapping("/{id}") public Result detail(@PathVariable Long id) { ${modelNameUpperCamel} ${modelNameLowerCamel} = ${modelNameLowerCamel}Service.getById(id); return ResultGenerator.genOkResult(${modelNameLowerCamel}); } @Operation(description = "生成分页查询") @GetMapping @ApiOperation(value="分页查询生成", notes="分页查询 ") @ApiImplicitParams({ @ApiImplicitParam(name = "page", value = "第几页", required = true, dataType = "Integer", paramType="query"), @ApiImplicitParam(name = "size", value = "一页有几条", required = true, dataType = "Integer", paramType="query") }) public Result list(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size) { PageHelper.startPage(page, size); List<${modelNameUpperCamel}> list = ${modelNameLowerCamel}Service.listAll(); PageInfo<${modelNameUpperCamel}> pageInfo = PageInfo.of(list); return ResultGenerator.genOkResult(pageInfo); } } ================================================ FILE: back/src/test/resources/generator/template/controller.ftl ================================================ package ${basePackage}.controller; import ${basePackage}.core.response.Result; import ${basePackage}.core.response.ResultGenerator; import ${basePackage}.entity.${modelNameUpperCamel}; import ${basePackage}.service.${modelNameUpperCamel}Service; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.security.access.prepost.PreAuthorize; 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; import javax.annotation.Resource; import java.util.List; /** * @author ${author} * @date ${date} */ @PreAuthorize("hasAuthority('ADMIN')") @Tag(name = "接口") @RestController @RequestMapping("${baseRequestMapping}") public class ${modelNameUpperCamel}Controller { @Resource private ${modelNameUpperCamel}Service ${modelNameLowerCamel}Service; @Operation(description = "添加") @PostMapping("/add") public Result add(${modelNameUpperCamel} ${modelNameLowerCamel}) { ${modelNameLowerCamel}Service.save(${modelNameLowerCamel}); return ResultGenerator.genOkResult(); } @Operation(description = "删除") @PostMapping("/delete") public Result delete(@RequestParam Long id) { ${modelNameLowerCamel}Service.deleteById(id); return ResultGenerator.genOkResult(); } @Operation(description = "更新") @PostMapping("/update") public Result update(${modelNameUpperCamel} ${modelNameLowerCamel}) { ${modelNameLowerCamel}Service.update(${modelNameLowerCamel}); return ResultGenerator.genOkResult(); } @Operation(description = "获取详细信息") @PostMapping("/detail") public Result detail(@RequestParam Long id) { ${modelNameUpperCamel} ${modelNameLowerCamel} = ${modelNameLowerCamel}Service.getById(id); return ResultGenerator.genOkResult(${modelNameLowerCamel}); } @Operation(description = "分页查询") @PostMapping("/list") public Result list(@RequestParam(defaultValue = "0") Integer page, @RequestParam(defaultValue = "0") Integer size) { PageHelper.startPage(page, size); List<${modelNameUpperCamel}> list = ${modelNameLowerCamel}Service.listAll(); PageInfo<${modelNameUpperCamel}> pageInfo = PageInfo.of(list); return ResultGenerator.genOkResult(pageInfo); } } ================================================ FILE: back/src/test/resources/generator/template/service-impl.ftl ================================================ package ${basePackage}.service.impl; import ${basePackage}.mapper.${modelNameUpperCamel}Mapper; import ${basePackage}.entity.${modelNameUpperCamel}; import ${basePackage}.service.${modelNameUpperCamel}Service; import ${basePackage}.core.service.AbstractService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; /** * @author ${author} * @date ${date} */ @Service @Transactional(rollbackFor = Exception.class) public class ${modelNameUpperCamel}ServiceImpl extends AbstractService<${modelNameUpperCamel}> implements ${modelNameUpperCamel}Service { @Resource private ${modelNameUpperCamel}Mapper ${modelNameLowerCamel}Mapper; } ================================================ FILE: back/src/test/resources/generator/template/service.ftl ================================================ package ${basePackage}.service; import ${basePackage}.entity.${modelNameUpperCamel}; import ${basePackage}.core.service.Service; /** * @author ${author} * @date ${date} */ public interface ${modelNameUpperCamel}Service extends Service<${modelNameUpperCamel}> { } ================================================ FILE: back/src/test/resources/sql/dev/account.sql ================================================ -- MySQL dump 10.16 Distrib 10.1.34-MariaDB, for Linux (x86_64) -- -- Host: localhost Database: seedling_dev -- ------------------------------------------------------ -- Server version 10.1.34-MariaDB /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; /*!40101 SET NAMES utf8 */; /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; /*!40103 SET TIME_ZONE='+00:00' */; /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; -- -- Table structure for table `account` -- DROP TABLE IF EXISTS `account`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `account` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '账户Id', `email` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '邮箱', `name` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '账户名', `password` varchar(256) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '密码', `register_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间', `login_time` datetime DEFAULT NULL COMMENT '上一次登录时间', PRIMARY KEY (`id`), UNIQUE KEY `idx_account_name` (`name`), UNIQUE KEY `idx_account_email` (`email`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='账户表'; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `account` -- LOCK TABLES `account` WRITE; /*!40000 ALTER TABLE `account` DISABLE KEYS */; INSERT INTO `account` VALUES (1,'admin@qq.com','admin','$2a$10$OG1zaFHT2LUy4SGcQ4EnRu9sPQMjMGEE6jARz61aQwRQ3316N6ikG','2018-01-01 00:00:00','2018-02-01 00:00:00'); INSERT INTO `account` VALUES (2,'user@qq.com','user','$2a$10$yjfcoyNWgoUh3QQ3I6Lwmux57rCz3mZP1j8V4BK60EIVdwT3SkwFO','2018-01-01 00:00:00','2018-02-01 00:00:00'); /*!40000 ALTER TABLE `account` ENABLE KEYS */; UNLOCK TABLES; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; -- Dump completed on 2018-07-14 16:51:45 ================================================ FILE: back/src/test/resources/sql/dev/account_role.sql ================================================ -- MySQL dump 10.16 Distrib 10.1.34-MariaDB, for Linux (x86_64) -- -- Host: localhost Database: seedling_dev -- ------------------------------------------------------ -- Server version 10.1.34-MariaDB /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; /*!40101 SET NAMES utf8 */; /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; /*!40103 SET TIME_ZONE='+00:00' */; /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; -- -- Table structure for table `account_role` -- DROP TABLE IF EXISTS `account_role`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `account_role` ( `account_id` bigint(20) unsigned NOT NULL COMMENT '账户Id', `role_id` bigint(20) unsigned NOT NULL COMMENT '角色Id', PRIMARY KEY (`account_id`,`role_id`), KEY `fk_ref_role` (`role_id`), CONSTRAINT `fk_ref_account` FOREIGN KEY (`account_id`) REFERENCES `account` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT `fk_ref_role` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='账户角色表'; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `account_role` -- LOCK TABLES `account_role` WRITE; /*!40000 ALTER TABLE `account_role` DISABLE KEYS */; INSERT INTO `account_role` VALUES (1,2); INSERT INTO `account_role` VALUES (1,3); INSERT INTO `account_role` VALUES (2,1); /*!40000 ALTER TABLE `account_role` ENABLE KEYS */; UNLOCK TABLES; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; -- Dump completed on 2018-07-14 16:51:59 ================================================ FILE: back/src/test/resources/sql/dev/role.sql ================================================ -- MySQL dump 10.16 Distrib 10.1.34-MariaDB, for Linux (x86_64) -- -- Host: localhost Database: seedling_dev -- ------------------------------------------------------ -- Server version 10.1.34-MariaDB /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; /*!40101 SET NAMES utf8 */; /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; /*!40103 SET TIME_ZONE='+00:00' */; /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; -- -- Table structure for table `role` -- DROP TABLE IF EXISTS `role`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `role` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '角色Id', `name` varchar(64) DEFAULT NULL COMMENT '角色名称', PRIMARY KEY (`id`), UNIQUE KEY `name` (`name`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COMMENT='角色表'; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `role` -- LOCK TABLES `role` WRITE; /*!40000 ALTER TABLE `role` DISABLE KEYS */; INSERT INTO `role` VALUES (1,'USER'); INSERT INTO `role` VALUES (2,'ADMIN'); INSERT INTO `role` VALUES (3,'TEST'); /*!40000 ALTER TABLE `role` ENABLE KEYS */; UNLOCK TABLES; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; -- Dump completed on 2018-07-14 16:51:33 ================================================ FILE: back/src/test/rest-test/upload.http ================================================ // https://www.jetbrains.com/help/idea/http-client-in-product-code-editor.html POST http://0.0.0.0:8080/account/token Accept: application/json;charset=UTF-8 Cache-Control: no-cache Content-Type: application/json;charset=UTF-8 { "name": "admin", "password": "admin" } > {% client.global.set("token", response.body.data); %} ### POST http://0.0.0.0:8080/upload Authorization: Bearer {{token}} Content-Type: multipart/form-data; boundary=boundary --boundary Content-Disposition: form-data; name="file"; filename="README.md" Content-Type: application/octet-stream // 上传的文件路径 < /home/zoctan/spring-boot-api-seedling/README.md ### ================================================ FILE: docs/crm商业计划书.pptx ================================================ [File too large to display: 13.1 MB] ================================================ FILE: front/.github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] - Browser [e.g. chrome, safari] - Version [e.g. 22] **Smartphone (please complete the following information):** - Device: [e.g. iPhone6] - OS: [e.g. iOS8.1] - Browser [e.g. stock browser, safari] - Version [e.g. 22] **Additional context** Add any other context about the problem here. ================================================ FILE: front/.github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: front/.gitignore ================================================ .DS_Store node_modules/ dist/ admindb/ npm-debug.log* yarn-debug.log* yarn-error.log* /test/unit/coverage/ /test/e2e/reports/ selenium-debug.log # Editor directories and files .idea .vscode *.suo *.ntvs* *.njsproj *.sln package-lock.json .env.production.local ================================================ FILE: front/LICENSE ================================================ MIT License Copyright (c) 2018 iczer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: front/README.en-US.md ================================================ [简体中文](./README.md) | English

Vue Antd Admin

[Ant Design Pro](https://github.com/ant-design/ant-design-pro)'s implementation with Vue. An out-of-box UI solution for enterprise applications as a React boilerplate. [![MIT](https://img.shields.io/github/license/iczer/vue-antd-admin)](https://github.com/iczer/vue-antd-admin/blob/master/LICENSE) [![Dependence](https://img.shields.io/david/iczer/vue-antd-admin)](https://david-dm.org/iczer/vue-antd-admin) [![DevDependencies](https://img.shields.io/david/dev/iczer/vue-antd-admin)](https://david-dm.org/iczer/vue-antd-admin?type=dev) [![Release](https://img.shields.io/github/v/release/iczer/vue-antd-admin)](https://github.com/iczer/vue-antd-admin/releases/latest) ![image](./src/assets/img/preview.png) Multiple theme modes available: ![image](./src/assets/img/preview-nine.png)
- Preview:https://iczer.gitee.io/vue-antd-admin - Documentation:https://iczer.gitee.io/vue-antd-admin-docs - FAQ:https://iczer.gitee.io/vue-antd-admin-docs/start/faq.html - Mirror Repo in China:https://gitee.com/iczer/vue-antd-admin ## Browsers support Modern browsers and IE10. | [IE / Edge](http://godban.github.io/browsers-support-badges/)
IE / Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | [Opera](http://godban.github.io/browsers-support-badges/)
Opera | | --- | --- | --- | --- | --- | | IE10, Edge | last 2 versions | last 2 versions | last 2 versions | last 2 versions | ## Usage ### clone ```bash $ git clone https://github.com/iczer/vue-antd-admin.git ``` ### yarn ```bash $ yarn install $ yarn serve ``` ### or npm ``` $ npm install $ npm run serve ``` More instructions at [documentation](https://iczer.gitee.io/vue-antd-admin-docs). ## Contributing Any type of contribution is welcome, here are some examples of how you may contribute to this project: :star2:: - Use Vue Antd Admin in your daily work. - Submit [Issue](https://github.com/iczer/vue-antd-admin/issues) to report :bug: or ask questions. - Propose [Pull Request](https://github.com/iczer/vue-antd-admin/pulls) to improve our code. - Join the community and share your experiences with us. QQ Group:942083829、812277510(full)、610090280(full) ================================================ FILE: front/README.md ================================================ 简体中文 | [English](./README.en-US.md)

Vue Antd Admin

[Ant Design Pro](https://github.com/ant-design/ant-design-pro) 的 Vue 实现版本 开箱即用的中后台前端/设计解决方案 [![MIT](https://img.shields.io/github/license/iczer/vue-antd-admin)](https://github.com/iczer/vue-antd-admin/blob/master/LICENSE) [![Dependence](https://img.shields.io/david/iczer/vue-antd-admin)](https://david-dm.org/iczer/vue-antd-admin) [![DevDependencies](https://img.shields.io/david/dev/iczer/vue-antd-admin)](https://david-dm.org/iczer/vue-antd-admin?type=dev) [![Release](https://img.shields.io/github/v/release/iczer/vue-antd-admin)](https://github.com/iczer/vue-antd-admin/releases/latest) ![image](./src/assets/img/preview.png) 多种主题模式可选: ![image](./src/assets/img/preview-nine.png)
- 预览地址:https://iczer.gitee.io/vue-antd-admin - 使用文档:https://iczer.gitee.io/vue-antd-admin-docs - 常见问题:https://iczer.gitee.io/vue-antd-admin-docs/start/faq.html - 国内镜像:https://gitee.com/iczer/vue-antd-admin ## 浏览器支持 现代浏览器及 IE10 | [IE / Edge](http://godban.github.io/browsers-support-badges/)
IE / Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | [Opera](http://godban.github.io/browsers-support-badges/)
Opera | | --- | --- | --- | --- | --- | | IE10, Edge | last 2 versions | last 2 versions | last 2 versions | last 2 versions | ## 使用 ### clone ```bash $ git clone https://github.com/iczer/vue-antd-admin.git ``` ### yarn ```bash $ yarn install $ yarn serve ``` ### or npm ``` $ npm install $ npm run serve ``` 更多信息参考 [使用文档](https://iczer.gitee.io/vue-antd-admin-docs) ## 参与贡献 我们非常欢迎你的贡献,你可以通过以下方式和我们一起共建 :star2:: - 在你的公司或个人项目中使用 Vue Antd Admin。 - 通过 [Issue](https://github.com/iczer/vue-antd-admin/issues) 报告:bug:或进行咨询。 - 提交 [Pull Request](https://github.com/iczer/vue-antd-admin/pulls) 改进 Admin 的代码。 - 加入社群,与小伙伴们一同交流心得。QQ群:942083829、 812277510(已满)、610090280(已满) ## 打赏 如果该项目对您有所帮助,可以请作者喝一杯咖啡。

================================================ FILE: front/babel.config.js ================================================ const IS_PROD = ['production', 'prod'].includes(process.env.NODE_ENV) const plugins = [] if (IS_PROD) { plugins.push('transform-remove-console') } module.exports = { presets: [ '@vue/cli-plugin-babel/preset' ], plugins } ================================================ FILE: front/docs/.vuepress/components/Alert.vue ================================================ ================================================ FILE: front/docs/.vuepress/components/Color.vue ================================================ ================================================ FILE: front/docs/.vuepress/components/ColorList.vue ================================================ ================================================ FILE: front/docs/.vuepress/config.js ================================================ module.exports = { title: 'Vue Antd Admin', description: 'Vue Antd Admin', base: '/vue-antd-admin-docs/', head: [ ['link', { rel: 'icon', href: '/favicon.ico' }] ], themeConfig: { logo: '/logo.png', repo: 'iczer/vue-antd-admin', docsDir: 'docs', editLinks: true, editLinkText: '在 Github 上帮助我们编辑此页', nav: [ {text: '指南', link: '/'}, {text: '配置', link: '/develop/layout'}, {text: '主题', link: '/advance/theme'}, ], lastUpdated: 'Last Updated', sidebar: [ { title: '开始', collapsable: false, children: [ '/start/use', '/start/faq' ] }, { title: '开发', collapsable: false, children: [ '/develop/layout', '/develop/router', '/develop/page', '/develop/theme', '/develop/service', '/develop/mock' ] }, { title: '进阶', collapsable: false, children: [ '/advance/i18n', '/advance/async', '/advance/authority', '/advance/login', '/advance/guard', '/advance/interceptors', '/advance/api' ] }, { title: '其它', collapsable: false, children: [ '/other/upgrade', '/other/community' ] } ], nextLinks: true, prevLinks: true, }, plugins: ['@vuepress/back-to-top', require('./plugins/alert')], markdown: { lineNumbers: true } } ================================================ FILE: front/docs/.vuepress/plugins/alert/Alert.vue ================================================ ================================================ FILE: front/docs/.vuepress/plugins/alert/alertMixin.js ================================================ import Alert from './Alert' const AlertMixin = { install(Vue) { Vue.mixin({ methods: { $alert(message, duration = 2000) { let Constructor= Vue.extend(Alert) let alert = new Constructor() alert.$slots.default = message alert.$props.alert = this.$page.alert alert.$mount() document.body.appendChild(alert.$el) const appendHeight = alert.$el.offsetHeight + 16 this.$page.alert.top += appendHeight setTimeout(() => { this.$page.alert.top -= appendHeight this.triggerRemoveAlert(appendHeight) setTimeout(() => { alert.$destroy() alert.$el.remove() }, 100) }, duration) }, triggerRemoveAlert(height) { const event = new CustomEvent('alert_remove', { detail: {height} }) window.dispatchEvent(event) } } }) } } export default AlertMixin ================================================ FILE: front/docs/.vuepress/plugins/alert/clientRootMixin.js ================================================ export default { updated() { this.$page.alert.top = 100 } } ================================================ FILE: front/docs/.vuepress/plugins/alert/enhanceApp.js ================================================ import AlertMixin from './alertMixin' export default ({Vue}) => { Vue.use(AlertMixin) } ================================================ FILE: front/docs/.vuepress/plugins/alert/index.js ================================================ const path = require('path') module.exports = (options, ctx) => { return { clientRootMixin: path.resolve(__dirname, 'clientRootMixin.js'), extendPageData($page) { $page.alert = { top: 100 } }, enhanceAppFiles: path.resolve(__dirname, 'enhanceApp.js') } } ================================================ FILE: front/docs/.vuepress/styles/index.styl ================================================ .custom-block.tip{ border-color: #1890ff } .theme-default-content code .token.inserted{ color: #60bd90; } //.custom-block.warning{ // border-color: #fa8c16 //} //.custom-block.error{ // border-color: #f5222d //} ================================================ FILE: front/docs/.vuepress/styles/palette.styl ================================================ $accentColor = #1890ff $contentWidth = 940px ================================================ FILE: front/docs/README.md ================================================ --- title: 首页 home: true heroImage: /logo.png heroText: Vue Antd Admin tagline: 开箱即用的中台前端/设计解决方案 actionText: 快速上手 → actionLink: /start/use features: - title: 简洁 details: 以 Markdown 为中心的项目结构,以最少的配置帮助你专注于写作。 - title: 优雅 details: 享受 Vue + webpack 的开发体验,在 Markdown 中使用 Vue 组件,同时可以使用 Vue 来开发自定义主题。 - title: 自然 details: VuePress 为每个页面预渲染生成静态的 HTML,同时在页面被加载的时候,将作为 SPA 运行。 footer: MIT Licensed | Copyright © 2018-present iczer --- ================================================ FILE: front/docs/advance/README.md ================================================ --- title: 进阶 lang: zn-CN --- # 进阶 ================================================ FILE: front/docs/advance/api.md ================================================ --- title: 全局API lang: zn-CN --- # 全局API 我们提供了一些全局Api,在日常功能开发中或许会有帮助,它们均被绑定到了页面组件或子组件实例上。 在组件内可以直接通过`this.$[apiName]`的方式调用。如下: ## 多页签 ### $closePage(closeRoute, nextRoute) 该api用于关闭当前已打开的页签,接收两个参数: * **closeRoute** 要关闭的页签对应的 route 对象,可简写为路由的 fullPath 字符串值。 * **nextRoute** 关闭页签要后跳转的 route 对象,可不传,不传则会自动选择打开页签(临近原则)。 ### $refreshPage(route) 该api用于刷新路由对应的页签,接收一个参数: * **route** 要刷新的页签对应的 route 对象,可简写为路由的 fullPath 字符串值。 ### $openPage(route, title) 该api用于打开一个新页签,接收两个参数: * **route** 要打开的页签对应的 route 对象,可简写为路由的 fullPath 字符串值。 * **title** 设置打开页签的标题,可不传。 ### $setPageTitle(route, title) 该api用于设置页签的标题,接收两个参数: * **route** 要设置的页签对应的 route 对象,可简写为路由的 fullPath 字符串值。 * **title** 页签的标题。 ## 权限 ### $auth(check, type) 该api可以用于操作权限校验,接收两个参数: * **check** 需要要校验的操作权限 * **type** 操作权限校验类别,可选 `permission` 和 `role`,即通过权限校验还是角色进行校验,可不传(不传的话,会对两种类型都进行匹配,任意一种匹配成功即校验通过)。 ================================================ FILE: front/docs/advance/async.md ================================================ --- title: 异步路由和菜单 lang: zn-CN --- # 异步路由和菜单 在现实业务中,存在这样的场景,系统的路由和菜单会根据用户的角色变化而变化,或者路由菜单根据用户的权限动态生成。我们为此准备了一套完整的异步加载方案, 可以让你很方便的从服务端加载路由和菜单配置,并应用到系统中。 ## 异步加载路由 动态路由的实现主要有以下四个步骤: ### 开启异步路由设置 在 `/config/config.js` 文件中设置 `asyncRoutes` 的值为 true: ```js {7} module.exports = { theme: { color: '#13c2c2', mode: 'night' }, multiPage: true, asyncRoutes: true, //异步加载路由,true:开启,false:不开启 animate: { name: 'roll', direction: 'default' } } ``` ### 注册路由组件 基础路由组件包含路由基本配置和对应的视图组件,我们统一在 `/router/async/router.map.js` 文件中注册它们。它和正常的路由配置基本无异,相当于把完整的路由拆分成单个的路由配置进行注册,为后面的路由动态配置打好基础。 一个单独的路由组件注册示例如下: ```jsx registerName: { //路由组件注册名称,唯一标识 path: 'path', //路由path,可缺省,默认取路由注册名称 registerName 的值 name: '演示页', //路由名称 redirect: '/login', //路由重定向 component: () => import('@/pages/demo'), //路由视图 icon: 'permission', //路由的菜单icon,会注入到路由元数据meta中 invisible: false, //是否隐藏菜单项,true 隐藏,false 不隐藏,会注入到路由元数据meta中。 authority: { //路由权限配置,会注入到路由元数据meta中。可缺省,默认为 ‘*’, 即无权限限制 permission: 'form', //路由需要的权限 role: 'admin' //路由需要的角色。当permission未设置,通过 role 检查权限 }, page: { //路由的页面数据,会注入到路由元数据meta中 title: '演示页', //页面标题 breadcrumb: ['首页', '演示页'] //页面面包屑 } } ``` :::details 点击查看完整的路由注册示例: ```js // 视图组件 const view = { tabs: () => import('@/layouts/tabs'), blank: () => import('@/layouts/BlankView'), page: () => import('@/layouts/PageView') } // 路由组件注册 const routerMap = { login: { authority: '*', path: '/login', component: () => import('@/pages/login') }, demo: { name: '演示页', renderMenu: false, component: () => import('@/pages/demo') }, exp403: { authority: '*', name: 'exp403', path: '403', component: () => import('@/pages/exception/403') }, exp404: { name: 'exp404', path: '404', component: () => import('@/pages/exception/404') }, exp500: { name: 'exp500', path: '500', component: () => import('@/pages/exception/500') }, root: { path: '/', name: '首页', redirect: '/login', component: view.tabs }, parent1: { name: '父级路由1', icon: 'dashboard', component: view.blank }, parent2: { name: '父级路由2', icon: 'form', component: view.page }, exception: { name: '异常页', icon: 'warning', component: view.blank } } export default routerMap ``` ::: ### 配置基本路由 如果没有任何路由,你的应用是无法访问的,所以我们需要在本地配置一些基本的路由,比如登录页、404、403 等。你可以在 `/router/async/config.async.js` 文件中配置一些本地必要的路由。如下: ```js const routesConfig = [ 'login', //匹配 router.map.js 中注册的 registerName = login 的路由 'root', //匹配 router.map.js 中注册的 registerName = root 的路由 { router: 'exp404', //匹配 router.map.js 中注册的 registerName = exp404 的路由 path: '*', //重写 exp404 路由的 path 属性 name: '404' //重写 exp404 路由的 name 属性 }, { router: 'exp403', //匹配 router.map.js 中注册的 registerName = exp403 的路由 path: '/403', //重写 exp403 路由的 path 属性 name: '403' //重写 exp403 路由的 name 属性 } ] ``` 完成配置后,即可通过 `routesConfig` 和已注册的 `routerMap` 生成 [router.options.routes](https://router.vuejs.org/zh/api/#router-%E6%9E%84%E5%BB%BA%E9%80%89%E9%A1%B9) 配置,如下: ```js const options = { routes: parseRoutes(routesConfig, routerMap) } ``` :::details 点击查看完整的 config.async.js 代码 ```js import routerMap from './router.map' import {parseRoutes} from '@/utils/routerUtil' // 异步路由配置 const routesConfig = [ 'login', 'root', { router: 'exp404', path: '*', name: '404' }, { router: 'exp403', path: '/403', name: '403' } ] const options = { routes: parseRoutes(routesConfig, routerMap) } export default options ``` ::: 完成以上设置后,本地就已经有了包含 login、404、403 页面的路由,并且这些路由是可以直接访问的。 ### 异步获取路由配置 当用户登录后(或者其它的前提条件),你可能想根据不同用户加载不同的路由和菜单。 那么我们就需要先从后端服务获取异步路由配置,后端返回的异步路由配置 `routesConfig` 是一个异步路由配置数组, 应当如下格式: ```jsx [{ router: 'root', //匹配 router.map.js 中注册名 registerName = root 的路由 children: [ //root 路由的子路由配置 { router: 'dashboard', //匹配 router.map.js 中注册名 registerName = dashboard 的路由 children: ['workplace', 'analysis'], //dashboard 路由的子路由配置,依次匹配 registerName 为 workplace 和 analysis 的路由 }, { router: 'form', //匹配 router.map.js 中注册名 registerName = form 的路由 children: [ //form 路由的子路由配置 'basicForm', //匹配 router.map.js 中注册名 registerName = basicForm 的路由 'stepForm', //匹配 router.map.js 中注册名 registerName = stepForm 的路由 { router: 'advanceForm', //匹配 router.map.js 中注册名 registerName = advanceForm 的路由 path: 'advance' //重写 advanceForm 路由的 path 属性 } ] }, { router: 'basicForm', //匹配 router.map.js 中注册名 registerName = basicForm 的路由 name: '验权表单', //重写 basicForm 路由的 name 属性 icon: 'file-excel', //重写 basicForm 路由的 icon 属性 authority: 'form' //重写 basicForm 路由的 authority 属性 } ] }] ``` 其中 `router` 属性 对应 `router.map.js` 中已注册的`基础路由`的注册名称 `registerName`,`children` 属性为路由的嵌套子路由配置。 有些情况下你可能想重写已注册路由的属性,你可以为 `routesConfig` 配置同名属性去覆盖它。如上面的`验权表单`路由覆盖了注册路由的 `name`、`icon`、`authority` 属性。 ### 加载路由并应用 我们提供了一个路由加载工具,你只需调用 `/utils/routerUtil.js` 中的 `loadRoutes` 方法加载上一步获取到的 `routesConfig` 即可,如下: ```js {3} getRoutesConfig().then(result => { const routesConfig = result.data.data loadRoutes(routesConfig) }) ``` 至此,异步路由的加载就完成了,你可以访问异步加载的路由了。 :::tip 上面获取异步路由的代码,在 /pages/login/Login.vue 文件中可以找到。 loadRoutes 方法会合并 /router/async/config.async.js 文件中配置的基本路由。 ::: :::details 点击查看 loadRoutes 的详细代码 ```js /** * 加载路由 * @param routesConfig 路由配置 */ function loadRoutes(routesConfig) { // 如果 routesConfig 有值,则更新到本地,否则从本地获取 if (routesConfig) { store.commit('account/setRoutesConfig', routesConfig) } else { routesConfig = store.getters['account/routesConfig'] } // 如果开启了异步路由,则加载异步路由配置 const asyncRoutes = store.state.setting.asyncRoutes if (asyncRoutes) { if (routesConfig && routesConfig.length > 0) { const routes = parseRoutes(routesConfig, routerMap) formatAuthority(routes) const finalRoutes = mergeRoutes(router.options.routes, routes) router.options = {...router.options, routes: finalRoutes} router.matcher = new Router({...router.options, routes:[]}).matcher router.addRoutes(finalRoutes) } } // 初始化Admin后台菜单数据 const rootRoute = router.options.routes.find(item => item.path === '/') const menuRoutes = rootRoute && rootRoute.children if (menuRoutes) { mergeI18nFromRoutes(i18n, menuRoutes) store.commit('setting/setMenuData', menuRoutes) } } ``` ::: ## 异步加载菜单 Vue Antd Admin 的菜单,是根据路由配置自动生成的,默认获取根路由 `‘/’` 下所有子路由作为菜单配置。 当你完成了异步路由的加载,菜单也会随之改变,无需你做其它额外的操作。主要代码如下: ```js // 初始化Admin后台菜单数据 const rootRoute = router.options.routes.find(item => item.path === '/') const menuRoutes = rootRoute && rootRoute.children if (menuRoutes) { mergeI18nFromRoutes(i18n, menuRoutes) store.commit('setting/setMenuData', menuRoutes) } ``` :::tip 如果你不想从根路由 `‘/’` 下获取菜单数据,可以根据自己的需求更改。 ::: ================================================ FILE: front/docs/advance/authority.md ================================================ --- title: 权限管理 lang: zn-CN --- # 权限管理 权限控制是中后台系统中常见的需求之一,你可以利用 Vue Antd Admin 提供的权限控制脚手架,实现一些基本的权限控制功能。 ## 角色和权限 通常情况下有两种方式可以控制用户权限,一种是通过用户角色 role 来控制权限,另一种是通过更细致的权限 permission 来控制。 这两种方式 Vue Antd Admin 都支持。 我们定义了 role 和 permission 的基本格式,如果你获取的 role 和 permission 数据格式与 Vue Antd Admin 不一致, 你需要在获取到 role 和 permission 后将其转换为 Vue Antd Admin 的格式。 ### 角色 Vue Antd Admin 的 `角色/role` 包含 `id` 和 `operation` 两个属性。其中 `id` 为 `角色/role` 的 id,`operation` 为 `角色/role` 具有的操作权限,是一个字符串数组。 ```js role = { id: 'admin', //角色ID operation: ['add', 'delete', 'edit', 'close'] //角色的操作权限 } ``` 你也可以设置 role 的值为字符串,比如 role = 'admin', 它等同于: ```js role = { id: 'admin' } ``` ### 权限 Vue Antd Admin 的 `权限/permission` 也包含 `id` 和 `operation` 两个属性。其中 `id` 为 `权限/permission` 的 id,`operation` 为 `权限/permission` 下的操作权限,是一个字符串数组。 ```js permission = { id: 'form', //权限ID operation: ['add', 'delete', 'edit', 'close'] //权限下的操作权限 } ``` 你也可以设置 role 的值为字符串,比如 permission = 'form', 它等同于: ```js permission = { id: 'form' } ``` ### 设置用户的角色和权限 你只需为用户配置 roles 和 permissions 两者中的其中一种,即可完成权限管理功能。当然你也可以两者都配置。 获取到用户权限或角色后,将其格式化转为 Vue Antd Admin 可用的格式,然后使用 `store.commit('account/setPermissions', permissions)` 或 `store.commit('account/setRoles', roles)` 将其存在本地即可。如下: ```js getPermissions().then(res => { const permissions = res.data this.$store.commit('account/setPermissions', permissions) }) getRoles().then(res => { const roles = res.data this.$store.commit('account/setRoles', roles) }) ``` :::tip 注意,存在本地的 permissions 和 roles 都应该是数组。 你可以在 /pages/login/Login.vue 查看完整的用户角色和权限设置代码。 ::: ## 页面权限 如果你想给一些页面设置准入权限,只需要给该页面对应的路由设置元数据 authority 即可。 authority 的值可以是一个字符串,也可以是对象。 如下路由配置,则表明 `验权页面` 需要准入权限(permission): `form` ```js {5} const route = { name: '验权页面', path: 'auth/demo', meta: { authority: 'form', }, component: () => import('@/pages/demo') } ``` 下面是 authority 的值为对象的写法,这种写法和上面字符串的写法具有相同的效果: ```js {5-7} const route = { name: '验权页面', path: 'auth/demo', meta: { authority: { permission: 'form' } }, component: () => import('@/pages/demo') } ``` 有时你可能需要通过用户角色来配置页面权限,我们同样支持,用法和上面类似。 如下配置,表明 `验权页面` 需要准入角色(role) `admin`: ```js {5-7} const route = { name: '验权页面', path: 'auth/demo', meta: { authority: { role: 'admin' } }, component: () => import('@/pages/demo') } ``` :::tip 当你未设置 authority 或 设置 authority 的值 为 `*` 时,等同于该页面无需权限限制,我们会忽略此页面的权限检查。 ::: :::tip 当 authority 的值为字符串时,会以 [权限/permission](#权限) 验证权限。如果你需要以 [角色/role](#角色) 验证权限,请以对象形式设置 authority 的值。 ::: ## 操作权限 在一些复杂的些场景下,权限可能不仅仅是页面层级这么简单。在一些页面你可能需要校验用户是否具有某些操作的权限,比如 增、删、改、查等。 为此,我们提供了 `权限校验注入` 和 `权限校验指令` 两个实用的功能。 ### 权限校验注入 通过对Vue组件的实例方法进行 `权限校验注入`,我们可以控制该实例方法的执行权限,从而精准且安全的验证用户操作。 比如,QueryList 页面的 deleteRecord 方法,我们希望具有操作权限 `delete` 的用户才能调用此方法。 只需为 `deleteRecord` 方法注入权限校验,按如下方式配置 `authorize` 即可: ```vue {9-11,13} ``` 如果用户没有 `delete` 权限,调用 deleteRecord 方法,会看到如下提示: ![无此权限](../assets/permission.png) ### 操作权限校验的类型 `authorize` 会根据当前页面匹配到的权限类型([permission](#权限) / [role](#角色)),来判断是使用 `permission.operation` 还是 `role.operation` 来进行权限校验。 如果当前页面同时匹配到了 permission 和 role 权限,则默认通过 permission.operation 来进行操作权限校验。 当然你也可以指定操作权限校验的类型,如下设置即可: ```js {2-5} authorize: { deleteRecord: { //需要 注入权限校验 的方法名:deleteRecord check: 'delete', //需要校验的操作权限:check type: 'role' //指定操作权限校验的类型,可选 permission 和 role。这里指定以 role.operation 校验操作权限 } } ``` ### 权限校验指令 有时我们可能希望用户能够更直观的了解自己的操作权限。比如给没有操作权限的控件应用 disable 样式,禁用 click 事件等。 我们提供了权限校验指令 `v-auth` 来实现这个功能。 比如,我们想为 QueryList 页面的删除控件进行 `delete` 操作权限校验,只需为删除控件设置 v-auth="\`delete\`" 指令即可,如下: ```vue {6} ``` 假如用户没有 `delete` 操作权限,则控件会被应用 disable 样式,且 click 事件无效,如下图: ![权限校验指令](../assets/auth.png) :::warning 重要!!! v-auth 是我们自定义的一个 [Vue指令](https://cn.vuejs.org/v2/guide/custom-directive.html#ad)。因为 `Vue指令` 的值需要是一个 javascript 表达式,因此你不能直接给 v-auth 赋值为字符串, 需要把 v-auth 的字符串值用 ` `` ` 包裹起来,否则可能会报 undefined 错误。 ::: ### 权限校验指令的类型 你同样也可以指定 v-auth 的权限校验类型,可选 [permission](#权限) 和 [role](#角色)。它的校验方式和 [authorize](#权限校验注入) 类似,如未指定则会自动识别。 `v-auth:role` 表示通过 `role.operation` 进行校验,`v-auth:permission` 表示通过 `permission.operation` 进行校验。 如下,指定通过 `role.operation` 校验删除控件的操作权限: ```vue {3}
... 删除 ...
``` ## 异步路由权限 异步路由同样可以进行权限校验配置,它和正常的路由权限配置基本无异,只是无需把 [authority](#页面权限) 配置在元数据属性 meta 里。 你可以在路由组件注册时设置 authority,也可以在异步路由配置里设置 authority。 路由组件注册时设置 [authority](#页面权限): ```js {6} // 路由组件注册 const routerMap = { ... demo: { name: '演示页', authority: 'form', component: () => import('@/pages/demo') } ... } ``` 异步路由配置里设置 [authority](#页面权限): ```js {11-13} const routesConfig = [{ router: 'root', children: ['demo', {router: 'parent1'...}, ... { router: 'demo', icon: 'file-ppt', path: 'auth/demo', name: '验权页面', authority: { permission: 'form', } } ] }] ``` ================================================ FILE: front/docs/advance/chart.md ================================================ --- title: 图表 lang: zn-CN --- # 图表 ### 作者还没来得及编辑该页面,如果你感兴趣,可以点击下方链接,帮助作者完善此页 ================================================ FILE: front/docs/advance/error.md ================================================ --- title: 错误处理 lang: zn-CN --- # 错误处理 ### 作者还没来得及编辑该页面,如果你感兴趣,可以点击下方链接,帮助作者完善此页 ================================================ FILE: front/docs/advance/guard.md ================================================ --- title: 路由守卫 lang: zn-CN --- # 路由守卫 Vue Antd Admin 使用 vue-router 实现路由导航功能,因此可以为路由配置一些守卫。 我们统一把导航守卫配置在 router/guards.js 文件中。 ## 前置守卫 Vue Antd Admin 为每个前置导航守卫函数注入 to,from,next,options 四个参数: * `to: Route`: 即将要进入的目标[路由对象](https://router.vuejs.org/zh/api/#%E8%B7%AF%E7%94%B1%E5%AF%B9%E8%B1%A1) * `from: Route`: 当前导航正要离开的路由对象 * `next: Function`: 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。详情查看 [Vue Router #导航守卫](https://router.vuejs.org/zh/guide/advanced/navigation-guards.html) * `options: Object`: 应用配置,包含: {router, i18n, store, message},可根据需要扩展。 如下,是登录拦截导航守卫的定义 ```js const loginGuard = (to, from, next, options) => { const {message} = options if (!loginIgnore.includes(to) && !checkAuthorization()) { message.warning('登录已失效,请重新登录') next({path: '/login'}) } else { next() } } ``` ## 后置守卫 你也可以定义后置导航守卫,Vue Antd Admin 为每个后置导航函数注入 to,from,options 三个参数: * `to: Route`: 即将要进入的目标[路由对象](https://router.vuejs.org/zh/api/#%E8%B7%AF%E7%94%B1%E5%AF%B9%E8%B1%A1) * `from: Route`: 当前导航正要离开的路由对象 * `options: Object`: 应用配置,包含: {router, i18n, store, message},可根据需要扩展。 如下,是一个后置导航守卫的定义 ```js const afterGuard = (to, from, options) => { const {store, message} = options // 做些什么 message.info('do something') } ``` ## 导出守卫配置 定义好导航守卫后,只需按照类别在 guard.js 中导出即可。分为两类,`前置守卫`和`后置守卫`。如下: ```js export default { beforeEach: [loginGuard, authorityGuard], afterEach: [afterGuard] } ``` :::details 点击查看完整的导航守卫配置 ```js import {loginIgnore} from '@/router/index' import {checkAuthorization} from '@/utils/request' /** * 登录守卫 * @param to * @param form * @param next * @param options */ const loginGuard = (to, from, next, options) => { const {message} = options if (!loginIgnore.includes(to) && !checkAuthorization()) { message.warning('登录已失效,请重新登录') next({path: '/login'}) } else { next() } } /** * 权限守卫 * @param to * @param form * @param next * @param options */ const authorityGuard = (to, from, next, options) => { const {store, message} = options const permissions = store.getters['account/permissions'] const roles = store.getters['account/roles'] if (!hasAuthority(to, permissions, roles)) { message.warning(`对不起,您无权访问页面: ${to.fullPath},请联系管理员`) next({path: '/403'}) } else { next() } } /** * 后置守卫 * @param to * @param form * @param options */ const afterGuard = (to, from, options) => { const {store, message} = options // 做些什么 message.info('do something') } export default { beforeEach: [loginGuard, authorityGuard], afterEach: [afterGuard] } ``` ::: ================================================ FILE: front/docs/advance/i18n.md ================================================ --- title: 国际化 lang: zn-CN --- # 国际化 vue-antd-admin 采用 [vue-i18n](https://kazupon.github.io/vue-i18n/) 插件来实现国际化,该项目已经内置并且加载好了基础配置。可以直接上手使用。 > 如果你还没有看快速入门,请先移步查看: [页面 -> i18n国际化配置](../develop/page.html#i18n国际化配置) ## 菜单和路由 ### 默认情况 如果你没有对菜单进行国际化配置,admin 默认会从路由数据中提取数据作为国际化配置。route.name 作为中文语言,route.path 作为英文语言。 国际化提取函数定义在 `@/utils/i18n.js` 文件中,会在路由加载时调用,如下: ```js /** * 从路由提取国际化数据 * @param i18n * @param routes */ function mergeI18nFromRoutes(i18n, routes) { formatFullPath(routes) const CN = generateI18n(new Object(), routes, 'name') const US = generateI18n(new Object(), routes, 'path') i18n.mergeLocaleMessage('CN', CN) i18n.mergeLocaleMessage('US', US) const messages = routesI18n.messages Object.keys(messages).forEach(lang => { i18n.mergeLocaleMessage(lang, messages[lang]) }) } ``` ### 自定义 如果你想自定义菜单国际化数据,可在 `@/router/i18n.js` 文件中配置。我们以路由的 path 作为 key(嵌套path 的写法也会被解析),name 作为 国际化语言的值。 假设你有一个路由的配置如下: ```js [{ path: 'parent', ... children: [{ path: 'self', ... }] }] or [{ path: 'other', ... children: [{ path: '/parent/self', // 在国际化配置中 key 会解析为 parent.self ... }] }] ``` 那么你需要在 `@/router/i18n.js` 中这样配置: ```jsx messages: { CN: { parent: { name: '父級菜單', self: {name: '菜單名'}, }, US: { parent: { name: 'parent menu', self: {name: 'menu name'}, }, HK: { parent: { name: '父級菜單', self: {name: '菜單名'}, }, ``` ## 添加语言 首先在 `@/layouts/header/AdminHeader.vue` ,新增一门语言 (多个同理)。 ```vue {15} ``` > TIP: 后续开发建议把这里改成动态配置的方式! 然后开始往 `@/router/i18n.js` 和 `@/pages/你的页面/i18n.js` 里面分别添加上语言的翻译。 ```vue {12,13,14} module.exports = { messages: { CN: { home: {name: '首页'}, }, US: { home: {name: 'home'}, }, HK: { home: {name: '首頁'}, }, JP: { home: {name: '最初のページ'}, }, } } ``` > Notice: 更多用法请移步到 [vue-i18n](https://kazupon.github.io/vue-i18n/) 。 ================================================ FILE: front/docs/advance/interceptors.md ================================================ --- title: 拦截器配置 lang: zn-CN --- # 拦截器配置 Vue Antd Admin 基于 aixos 封装了 http 通信功能,我们可以为 http 请求响应配置一些拦截器。拦截器统一配置在 /utils/axios-interceptors.js 文件中。 ## 请求拦截器 你可以为每个请求拦截器配置 `onFulfilled` 或 `onRejected` 两个钩子函数。 ### onFulfilled 我们会为 onFulfilled 钩子函数注入 config 和 options 两个参数: * `config: AxiosRequestConfig`: axios 请求配置,详情参考 [axios 请求配置](http://www.axios-js.com/zh-cn/docs/#%E8%AF%B7%E6%B1%82%E9%85%8D%E7%BD%AE) * `options: Object`: 应用配置,包含: {router, i18n, store, message},可根据需要扩展。 ### onRejected 我们会为 onFulfilled 钩子函数注入 error 和 options 两个参数: * `error: Error`: axios 请求错误对象 * `options: Object`: 应用配置,包含: {router, i18n, store, message},可根据需要扩展。 如下,为一个完整的请求拦截器配置: ```js const tokenCheck = { // 发送请求之前做些什么 onFulfilled(config, options) { const {message} = options const {url, xsrfCookieName} = config if (url.indexOf('login') === -1 && xsrfCookieName && !Cookie.get(xsrfCookieName)) { message.warning('认证 token 已过期,请重新登录') } return config }, // 请求出错时做点什么 onRejected(error, options) { const {message} = options message.error(error.message) return Promise.reject(error) } } ``` ## 响应拦截器 响应拦截器也同样可以配置 `onFulfilled` 或 `onRejected` 两个钩子函数。 ### onFulfilled 我们会为 onFulfilled 钩子函数注入 response 和 options 两个参数: * `response: AxiosResponse`: axios 响应对象,详情参考 [axios 响应对象](http://www.axios-js.com/zh-cn/docs/#%E5%93%8D%E5%BA%94%E7%BB%93%E6%9E%84) * `options: Object`: 应用配置,包含: {router, i18n, store, message},可根据需要扩展。 ### onRejected 我们会为 onFulfilled 钩子函数注入 error 和 options 两个参数: * `error: Error`: axios 请求错误对象 * `options: Object`: 应用配置,包含: {router, i18n, store, message},可根据需要扩展。 如下,为一个完整的响应拦截器配置: ```js const resp401 = { // 响应数据之前做点什么 onFulfilled(response, options) { const {message} = options if (response.status === 401) { message.error('无此接口权限') } return response }, // 响应出错时做点什么 onRejected(error, options) { const {message} = options if (response.status === 401) { message.error('无此接口权限') } return Promise.reject(error) } } ``` ## 导出拦截器 定义好拦截器后,只需在 axios-interceptors.js 文件中导出即可。分为两类,`请求拦截器`和`响应拦截器`。如下: ```js export default { request: [tokenCheck], // 请求拦截 response: [resp401] // 响应拦截 } ``` :::details 点击查看完整的拦截器配置示例 ```js import Cookie from 'js-cookie' // 401拦截 const resp401 = { onFulfilled(response, options) { const {message} = options if (response.status === 401) { message.error('无此接口权限') } return response }, onRejected(error, options) { const {message} = options message.error(error.message) return Promise.reject(error) } } const resp403 = { onFulfilled(response, options) { const {message} = options if (response.status === 403) { message.error(`请求被拒绝`) } return response } } const reqCommon = { onFulfilled(config, options) { const {message} = options const {url, xsrfCookieName} = config if (url.indexOf('login') === -1 && xsrfCookieName && !Cookie.get(xsrfCookieName)) { message.warning('认证 token 已过期,请重新登录') } return config }, onRejected(error, options) { const {message} = options message.error(error.message) return Promise.reject(error) } } export default { request: [reqCommon], // 请求拦截 response: [resp401, resp403] // 响应拦截 } ``` ::: ================================================ FILE: front/docs/advance/login.md ================================================ --- title: 登录认证 lang: zn-CN --- # 登录认证 Vue Antd Admin 使用 js-cookie.js 管理用户的 token,结合 axios 配置,可以为每个请求头加上 token 信息。 ## token名称 后端系统通常会从请求 header 中获取用户的 token,因此我们需要配置好 token 名称,好让后端能正确的识别到用户 token。 Vue Antd Admin 默认token 名称为 `Authorization`,你可以在 /utils/request.js 中修改它。 ```js{5} import axios from 'axios' import Cookie from 'js-cookie' // 跨域认证信息 header 名 const xsrfHeaderName = 'Authorization' ... ``` ## token 设置 调用登录接口后拿到用户的 token 和 token 过期时间(如无过期时间,可忽略),并使用 /utils/request.js #setAuthorization 方法保存token。 ```js{5} import {setAuthorization} from '@/utils/request' login(name, password).then(res => { const {token, expireAt} = res.data setAuthorization({token, expireAt: new Date(expireAt)}) }) ``` ## token 校验 Vue Antd Admin 默认添加了登录导航守卫,如检查到本地cookie 中不包含 token 信息,则会拦截跳转至登录页。你可以在 /router/index.js 中配置 不需要登录拦截的路由 ```js // 不需要登录拦截的路由配置 const loginIgnore = { names: ['404', '403'], //根据路由名称匹配 paths: ['/login'], //根据路由fullPath匹配 /** * 判断路由是否包含在该配置中 * @param route vue-router 的 route 对象 * @returns {boolean} */ includes(route) { return this.names.includes(route.name) || this.paths.includes(route.path) } } ``` 或者在 /router/guards.js 中移出登录守卫 ```diff ... export default { - beforeEach: [loginGuard, authorityGuard, redirectGuard], + beforeEach: [authorityGuard, redirectGuard], afterEach: [] } ``` ## Api ### setAuthorization(auth, authType) 来源:/utils/request.js 该方法用于保存用户 token,接收两个参数: * **auth** 认证信息,包含 token、expireAt 等认证数据。 * **authType** 认证类型,默认为 `AUTH_TYPE.BEARER`(AUTH_TYPE.BEARER 默认会给token 加上 Bearer 识别前缀),可根据自己的认证类型自行扩展。 ### checkAuthorization(authType) 该方法用于校验用户 token 是否过期,接收一个参数: * **authType** 认证类型,默认为 `AUTH_TYPE.BEARER`。 ### removeAuthorization(authType) 该方法用于移出用户本地存储的 token,接收一个参数: * **authType** 认证类型,默认为 `AUTH_TYPE.BEARER`。 :::tip 以上 Api 均可在 /utils/request.js 文件中找到。 ::: ================================================ FILE: front/docs/advance/skill.md ================================================ --- title: 108个小技巧 lang: zn-CN --- # 108个小技巧 ## 自定义菜单icon ## 隐藏页面标题 ## 关闭页签API ## 权限校验PI ================================================ FILE: front/docs/advance/theme.md ================================================ --- title: 更换主题 lang: zn-CN --- # 更换主题 ### 作者还没来得及编辑该页面,如果你感兴趣,可以点击下方链接,帮助作者完善此页 ================================================ FILE: front/docs/develop/README.md ================================================ --- title: 开发 lang: zh-CN --- # 开发 ================================================ FILE: front/docs/develop/layout.md ================================================ --- title: 布局 lang: zh-CN --- # 布局 页面整体布局是一个产品最外层的框架结构,往往会包含导航、页脚、侧边栏、通知栏以及内容等。在页面之中,也有很多区块的布局结构。在真实项目中,页面布局通常统领整个应用的界面,有非常重要的作用。 ## Admin 的布局 在 Vue Antd Admin 中,我们抽离了使用过程中一些常用的布局,都放在 layouts 目录中,分别为: * [AdminLayout](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/AdminLayout.vue) / **管理后台布局**,包含了头部导航,侧边导航、内容区和页脚,一般用于后台系统的整体布局 ![admin-layout](../assets/admin-layout.png) * [PageLayout](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/PageLayout.vue) / **页面布局**,包含了页头和内容区,常用于需要页头(包含面包屑、标题、额外操作等)的页面 ![page-layout](../assets/page-layout.png) * [CommonLayout](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/CommonLayout.vue) / **通用布局**,仅包含内容区和页脚的简单布局,项目中常用于注册、登录或展示页面 ![common-layout](../assets/common-layout.png) ## Admin 的视图 在 Vue Antd Admin 中,除了基本布局外,通常有很多页面的结构是相似的。因此,我们把这部分结构抽离为视图组件。 一个视图组件通常包含一个基本布局组件、视图公共区块、路由视图内容区、页脚等,常常结合路由配置使用。它们也被放入了 layouts 目录中,分别为: * [TabsView](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/TabsView.vue) / **多页签视图**,包含了 AdminLayout 布局、多页签头和路由视图内容区 ![tabs-view](../assets/tabs-view.png) * [PageView](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/PageView.vue) / **页面视图**,包含了 PageLayout 布局和路由视图内容区 ![page-view](../assets/page-view.png) * [BlankView](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/BlankView.vue) / **空白视图**,仅包含一个路由视图内容区 ![blank-view](../assets/blank-view.png) ## 如何使用 通常我们会把视图组件和路由配置结合一起使用,我们把配置信息抽离在路由配置文件中 [src/router/config.js](https://github.com/iczer/vue-antd-admin/blob/master/src/router/config.js) 。如下: ```jsx {7,12} { path: 'form', name: '表单页', meta: { icon: 'form', }, component: PageView, children: [ { path: 'basic', name: '基础表单', component: () => import('@/pages/form/basic/BasicForm'), } ] } ``` 当然,如果这满足不了你的需求,你也可以自定义一些视图组件,或者直接在页面组件中使用布局。参考 [workplace](https://github.com/iczer/vue-antd-admin/blob/master/src/pages/dashboard/workplace/WorkPlace.vue) 页面: ```vue {2,13} ``` ## 其它布局组件 除了 Admin 里的内建布局以外,在一些页面中需要进行布局,还可以使用 Ant Design Vue 提供的布局组件:Grid 和 Layout。 ### Grid 组件 栅格布局是网页中最常用的布局,其特点就是按照一定比例划分页面,能够随着屏幕的变化依旧保持比例,从而具有弹性布局的特点。 而 Ant Design Vue 的栅格组件提供的功能更为强大,能够设置间距、具有支持响应式的比例设置,以及支持 flex 模式,基本上涵盖了大部分的布局场景,详情查看:[Grid](https://www.antdv.com/components/grid-cn/)。 ### Layout 组件 如果你需要辅助页面框架级别的布局设计,那么 Layout 则是你最佳的选择,它抽象了大部分框架布局结构,使得只需要填空就可以开发规范专业的页面整体布局,详情查看:[Layout](https://www.antdv.com/components/layout-cn/)。 ### 根据不同场景区分抽离布局组件 在大部分场景下,我们需要基于上面两个组件封装一些适用于当下具体业务的组件,包含了通用的导航、侧边栏、顶部通知、页面标题等元素。例如 Vue Antd Admin 的 [AdminLayout](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/AdminLayout.vue)。 通常,我们会把抽象出来的布局组件,放到 layouts 文件夹中方便管理。需要注意的是,这些布局组件和我们平时使用的其它组件并没有什么不同,只不过功能性上是为了处理布局问题而单独归类。 ================================================ FILE: front/docs/develop/mock.md ================================================ --- title: Mock lang: zh-CN --- # Mock ### 作者还没来得及编辑该页面,如果你感兴趣,可以点击下方链接,帮助作者完善此页 ================================================ FILE: front/docs/develop/page.md ================================================ --- title: 页面 lang: zh-CN --- # 页面 这里的『页面』包含新建页面文件,配置路由、样式文件及i18n国际化等。通常情况下,你仅需简单的配置就可以添加一个新的页面。 ## 新建页面文件 在 src/pages 下创建新的 .vue 文件。如果页面相关文件过多,您可以创建一个文件夹来放置这些文件。 ```diff ├── public ├── src │ ├── assets # 本地静态资源 : : │ ├── pages # 页面组件和通用模板 + │ │ └── NewPage.vue # 新页面文件 or + │ │ └── newPage # 为新页面创建一个文件夹 + │ │ ├── NewPage.vue # 新页面文件 + │ │ ├── index.less # 页面样式文件 + │ │ └── index.js # import 引导文件 : : │ └── main.js # 应用入口js ├── package.json # package.json ├── README.md # README.md └── vue.config.js # vue 配置文件 ``` 为了更好地演示,我们初始化 NewPage.vue 文件如下: ```vue ``` index.less 文件: ```less .new-page{ height: 100%; background-color: @base-bg-color; text-align: center; padding: 200px 0 0 0; margin-top: -24px; h1{ font-size: 48px; } } ``` index.js 文件: ```js import NewPage from './NewPage' export default NewPage ``` ## 配置路由 路由配置在 src/router/config.js 文件中,我们把上面创建的页面文件加入路由配置中 ```js {10-14} const options = { routes: [ {name: '登录页'...}, { path: '/', name: '首页', component: TabsView, redirect: '/login', children: [ { path: 'newPage', name: '新页面', component: () => import('@/pages/newPage'), }, { path: 'dashboard', name: 'Dashboard', meta: { icon: 'dashboard' }, component: BlankView, children: [...] } ] ... } ] } ``` :::tip 我们建议使用英文设置路由的 path 属性,用中文设置路由的 name 属性。因为系统将自动提取路由的 path 和 name 属性作为国际化配置。这在后面的章节 [进阶>国际化](../advance/i18n.md)中将会讲到。 当然,如果你的项目不需要国际化,可以忽略。 ::: 启动服务,你将看到新增页面如下: ![newPage](../assets/new-page.png) 如果你想把它配置为二级页面或更深层级的页面,只需为它配置一个父级路由,并为父级路由配置一个[视图组件](./layout.md#admin-的视图), 这里我们选择 [PageView](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/PageView.vue),如下: ```js {10-21} const options = { routes: [ {name: '登录页'...}, { path: '/', name: '首页', component: TabsView, redirect: '/login', children: [ { path: 'parent', name: '父级路由', component: PageView, children: [ { path: 'newPage', name: '新页面', component: () => import('@/pages/newPage'), } ] }, {name: 'dashboard'...} ] ... } ] } ``` :::warning 页面所有父级路由的组件必须配置为[视图组件](../develop/layout.md#admin-的视图),否则页面的内容可能不会显示。 目前有 [PageView](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/PageView.vue)、 [TabsView](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/tabs/TabsView.vue) 和 [BlankView](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/BlankView.vue) 可选, 你也可以自己创建视图组件。([什么是视图组件?](../develop/layout.md#admin-的视图)) ::: 页面如下: ![newPage2](../assets/new-page-2.png) ## i18n国际化配置 如果你想为页面增加i18n国际化配置,只需在页面同级文件夹下创建 i18n.js 文件,然后在页面文件中引入并使用即可。 创建 i18n.js 文件: ```diff {9} ├── public ├── src │ ├── assets # 本地静态资源 : : │ ├── pages # 页面组件和通用模板 │ │ └── newPage # 为新页面创建一个文件夹 │ │ ├── NewPage.vue # 新页面文件 │ │ ├── index.less # 页面样式文件 + │ │ ├── i18n.js # i18n 国际化配置文件 │ │ └── index.js # import 引导文件 : : │ └── main.js # 应用入口js ├── package.json # package.json ├── README.md # README.md └── vue.config.js # vue 配置文件 ``` i18n.js 文件内容: ```js module.exports = { messages: { CN: { content: '演示页面', description: '这是一个演示页面' }, HK: { content: '演示頁面', description: '這是一個演示頁面' }, US: { content: 'Demo Page', description: 'This is a demo page' } } } ``` 在 NewPage.vue 文件中引入 i18n.js,并添加需要国际化的内容。如下修改: ```vue {3,10,13-15} ``` 然后页面右上角语言项选择 ``English``,你会发现,页面语言切换为英文了。如下: ![newPageUs](../assets/new-page-us.png) 一切就是这么的简单! :::tip 如果你尝试切换为繁体语言,可能会发现``页面标题``和``面包屑``显示为英文。 这涉及到路由的国际化配置,在章节 [进阶 > 国际化](../advance/i18n.md) 中,我们会对此作详细讲解。 ::: ================================================ FILE: front/docs/develop/router.md ================================================ --- title: 路由和菜单 lang: zh-CN --- # 路由和菜单 路由和菜单起到组织一个应用的关键骨架的作用,Vue Antd Admin 使用 [vue-router](https://router.vuejs.org/zh/) 来配置和管理我们的路由和菜单。 ## 基本结构 得益于 vue-router 路由配置的可扩展性,Vue Antd Admin 通过结合 router 配置文件、基本算法及 [menu.js](https://github.com/iczer/vue-antd-admin/blob/master/src/components/menu/menu.js) 菜单生成工具,搭建了路由和菜单的基本框架,主要涉及以下几个模块/功能: |功能 |配置 | |:----------|:-------------------------------| |*路由管理* |通过 [vue-router](https://router.vuejs.org/zh/) 的路由规则进行管理和配置| |*菜单生成* |根据路由配置自动生成菜单,菜单项名称、图标和层级等全部可以通过路由配置进行自定义| |*面包屑* |布局组件 [PageLayout](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/PageLayout.vue) 提取当前页面路由,并根据当前路由层次关系自动生成面包屑,当然你也可以自定义面包屑| |*页面标题* |同面包屑,布局组件 [PageLayout](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/PageLayout.vue) 根据提取到的当前页面的路由名称设置为页面标题,你也同样可以自定义标题| ## 路由 Vue Antd Admin 的路由配置完全遵循 vue-router 的 [routes 配置规则](https://router.vuejs.org/zh/api/#routes)。 另外我们还在 routes 的元数据属性 [meta](https://router.vuejs.org/zh/guide/advanced/meta.html#%E8%B7%AF%E7%94%B1%E5%85%83%E4%BF%A1%E6%81%AF) 中注入了三个属性 icon、invisible 和 page,它们将在生成菜单和页头时发挥作用。配置示例如下: ```js {7,13} const options = { routes: [{ path: '/', name: '首页', component: TabsView, meta: { invisible: true }, children: [{ path: 'dashboard', name: 'Dashboard', meta: { icon: 'dashboard' }, component: BlankView, children: [{ path: 'workplace', name: '工作台', component: () => import('@/pages/dashboard/workplace/WorkPlace'), }, { path: 'analysis', name: '分析页', component: () => import('@/pages/dashboard/analysis/Analysis'), }] }] }] } ``` 完整配置示例,请查看 [src/router/config.js](https://github.com/iczer/vue-antd-admin/blob/master/src/router/config.js) ## 菜单 Admin 系统的菜单直接通过路由配置生成,路由属性和菜单功能对应关系如下 |路由属性|对应菜单功能| |:-----------------|:-------| |**name** |菜单名称 | |**path** |点击菜单时的跳转链接| |**meta.icon** |菜单图标,图标使用 ant-design-vue 图标库,对应 [Icon](https://www.antdv.com/components/icon-cn/#API) 组件 的 type 属性| |**meta.invisible**|是否不将此路由项渲染为菜单项,默认false;如设置为 true,则生成菜单时将忽略此路由| 假如使用上面 [路由](#路由) 文档中的 [配置示例](#路由),将会生成如下菜单: ![menu-demo](../assets/menu-demo.png) 实际项目中,我们是在 AdminLayout 组件创建之前,提取 router 配置中根路由 '/' 下所有子路由配置, 并将此配置传递给 menu.js 插件,从而生成菜单。如下: ```vue {4,12,13,14} ``` 详细代码可查看 [layouts/AdminLayout#L83](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/AdminLayout.vue#L83)。 当然你也可以不使用 router 配置生成菜单,你只需按照配置规则给菜单传递你所定义配置即可。菜单组件配置规则如下: ```jsx {} [{ name: '菜单标题', path: '菜单路由', meta: { icon: '菜单图标', invisible: 'boolean, 是否隐藏此菜单项, 默认 false', }, children: [ //子菜单配置 { name: '子菜单标题', path: '子菜单路由', meta: { icon: '子菜单图标', invisible: 'boolean, 是否隐藏此菜单项, 默认 false', }, } ] }] ``` 更多细节可查看 [components/menu/menu.js](https://github.com/iczer/vue-antd-admin/blob/master/src/components/menu/menu.js) ## 面包屑 面包屑由 [PageHeader](https://github.com/iczer/vue-antd-admin/blob/master/src/components/page/PageHeader.vue) 实现,PageLayout 组件会从当前页面路由提取面包屑配置(如未设置,则根据当前路由层次关系生成面包屑)。所以只要页面中使用了 PageLayout 布局或者它的父级组件使用了 PageLayout 布局,面包屑都将自动生成。 当然,如果你想在某个页面自定义面包屑,只需在对应的路由元数据 meta 中定义 page.breadcrumb 属性即可。Vue Antd Admin 将会优先使用路由元数据 meta 中定义的面包屑配置。 比如,想自定义工作台页面面包屑,可以在工作台的 route 配置中如下设置: ```jsx {5,6,7} { path: 'workplace', name: '工作台', meta: { page: { breadcrumb: ['首页', 'Dashboard', '自定义'] } }, component: () => import('@/pages/dashboard/workplace/WorkPlace'), } ``` 更多细节可查看 [layouts/PageLayout.vue#L55](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/PageLayout.vue#L55) ## 页面标题 页面标题的实现方式与面包屑基本一致,也是由 PageLayout 组件从当前页面路由提取标题(如未设置,则提取当前路由名称作为标题)。 如果你想自定义页面标题,在页面对应的路由元数据 meta 中定义 page.title 属性即可,如下示例,定义了工作台页面的标题: ```jsx {5,6,7} { path: 'workplace', name: '工作台', meta: { page: { title: '自定义标题' } }, component: () => import('@/pages/dashboard/workplace/WorkPlace'), } ``` 更多细节可查看 [layouts/PageLayout.vue#L48](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/PageLayout.vue#L48) ================================================ FILE: front/docs/develop/service.md ================================================ --- title: 服务端交互 lang: zh-CN --- # 服务端交互 数据服务是一个应用的灵魂,它驱动着应用的各个功能模块的正常运转。Vue Antd Admin 在 service 模块封装了服务端交互,通过 API 的形式可以和任何技术栈的服务端应用一起工作。 ## 服务交互流程 在 Vue Antd Admin 中,服务端交互流程如下: * 组件内调用 service 服务 API * service 服务 API 封装请求数据,通过 request.js 发送请求 * 组件获取 service 返回的数据,更新视图数据或触发其它行为 我们以登录为例,Login.vue 组件内,用户输入账号密码,点击登录,调用 services/user/login api ```vue {5,17} ``` `services/user/login` 封装账户密码数据,通过 `request.js` 发送登录服务请求 ```js import {request, METHOD} from '@/utils/request' /** * 登录服务 * @param name 账户名 * @param password 账户密码 * @returns {Promise>} */ async function login(name, password) { return request(LOGIN, METHOD.POST, { name: name, password: password }) } ``` Login.vue 获取登录服务返回的数据,进行后续操作 ```vue {14,18-23} ``` ## 服务模块结构 服务模块结构如下: ```bash ... ├── src │ └── services # 数据服务模块 │ ├── user.js # 用户数据服务 │ ├── product.js # 产品服务 │ ... │ ├── api.js # api 地址管理 │ └── index.js # 服务模块导出 ... │ └── utils # 数据服务模块 │ ├── request.js # 基于 axios 的 http 请求工具 ... ``` services 文件夹下, api.js 用于服务请求地址的统一管理,index.js 用于模块化导出服务,其它 *.js 文件对应各个服务模块。 ## request.js request.js 基于 axios 封装了一些常用的函数,如下: ```js export { METHOD, //http method 常量 AUTH_TYPE, //凭证认证类型 常量 request, //http请求函数 setAuthorization, //设置身份凭证函数 removeAuthorization, //移除身份凭证函数 checkAuthorization //检查身份凭证是否过期函数 } ``` :::tip 凭证认证类型默认为 [Bearer](https://www.jianshu.com/p/8f7009456abc),你可以根据自己的需要实现其它类型的认证 ::: ## Base url 配置 你可以在项目根目录下的环境变量文件(.env 和 .env.development)中配置你的 API 服务 base url 地址。 生产环境,.env 文件 ```properties VUE_APP_API_BASE_URL=https://www.server.com ``` 开发环境,.env.development 文件: ```properties VUE_APP_API_BASE_URL=https://localhost:8000 ``` ## 跨域设置 在开发环境中,通常我们的Vue应用和服务应用运行在不同的地址或端口上。我们可以通过简单的设置,代理前端请求,来避免跨域问题。如下: 首先,在 services/api.js 文件中设置 API_PROXY_PREFIX 常量,BASE_URL 像下面这样设置: ```js {2,4} //跨域代理前缀 const API_PROXY_PREFIX='/api' //base url const BASE_URL = process.env.NODE_ENV === 'production' ? process.env.VUE_APP_API_BASE_URL : API_PROXY_PREFIX //导出api服务地址 module.exports = { LOGIN: `${BASE_URL}/login`, ROUTES: `${BASE_URL}/routes` } ``` 然后,在 vue.config.js 文件中配置代理: ```js model.exports = { devServer: { proxy: { '/api': { //此处要与 /services/api.js 中的 API_PROXY_PREFIX 值保持一致 target: process.env.VUE_APP_API_BASE_URL, changeOrigin: true, pathRewrite: { '^/api': '' } } } } } ``` :::tip 此代理配置仅适用于开发环境,生产环境的跨域代理请在自己的web服务器配置。 ::: ================================================ FILE: front/docs/develop/theme.md ================================================ --- title: 主题定制 lang: zh-CN --- # 主题定制 ## 主题颜色 ### 主题色 我们内置了一个色盘供您选择 如果这不能满足你的需求,你也可以使用任何你喜欢的颜色,只需要在 src/config/config.js 文件中配置你的主题色即可。如: ```js {3} module.exports = { theme: { color: '#13c2c2', //换成任何你喜欢的颜色,支持 hex 色值 mode: 'night' }, multiPage: true, animate: { name: 'roll', direction: 'default' } } ``` 当你设置好主题色后,系统会根据这个主题色为你生成一系列配套的颜色,并应用到vue组件中。 :::tip 你可以在你的样式文件中直接使用 less 变量 ``@theme-color``。 ::: :::warning 主题色目前只支持 ``hex`` 模式的色值。如果设置为 ``rgb`` 或其它模式的色值,可能会导致配套颜色无法生成。 ::: ### 功能色 除了主题色,系统还有一些功能性颜色,分别为:成功色、警告色和错误色。默认色值分别为: |名称|success |warning |error | |:-:|:--------:|:-------:|:-----:| |色值|``#52c41a``|``#faad14``|``#f5222d``| |颜色|||| |less变量|@success-color|@warning-color|@error-color| 你也可以在 src/config/config.js 重新定义这些功能色 ```js {5-7} module.exports = { theme: { color: '#13c2c2', mode: 'night', success: '#52c41a', //定义成功色,支持 hex 色值 warning: '#faad14', //定义警告色,支持 hex 色值 error: '#f5222d' //定义错误色,支持 hex 色值 }, multiPage: true, animate: { name: 'roll', direction: 'default' } } ``` :::tip 想在在你的样式文件中使用以上各功能色,引用各功能色对应的 less 变量即可。 ::: :::warning 功能色目前也只支持 ``hex`` 模式的色值。如果设置为 ``rgb`` 或其它模式的色值,可能会导致配套颜色无法生成。 ::: ### 文本色
主题模式 标题色 文本色 次级文本色
light/dark
rgba(0,0,0,0.85) rgba(0,0,0,0.65) rgba(0,0,0,0.45)
night
rgba(255,255,255,0.85) rgba(255,255,255,0.65) rgba(255,255,255,0.45)
less变量 @title-color @text-color @text-color-second
:::tip 想在在你的样式文件中使用以上文本色,引用各文本色对应的 less 变量即可。 ::: :::warning 目前不支持自定义文本色,因为涉及到主题模式切换时文本色的置换问题。如强行修改,可能会导致主题模式切换时出现样式异常。 如果你的项目不需要主题模式切换,可自行替换以上文本色。 ::: ### 背景色
主题模式 布局背景色 基础背景色 hover背景色 边框颜色 阴影颜色
light/dark
#f0f2f5 #fff rgba(0,0,0,0.025) #f0f0f0 rgba(0,0,0,0.15)
night
#000 #141414 rgba(255,255,255,0.025) #303030 rgba(255,255,255,0.15)
less变量 @layout-bg-color @base-bg-color @hover-bg-color @border-color @shadow-color
:::tip 想在在你的样式文件中使用以上背景色,引用各背景色对应的 less 变量即可。 ::: :::warning 目前也不支持自定义背景色,因为涉及到主题模式切换时背景色的置换问题。如强行修改,可能会导致主题模式切换时出现样式异常。 如果你的项目不需要主题模式切换,可自行替换以上背景色。 ::: ### antd 的色系 除了以上颜色,我们还引入了 ant-design 内置的色系。如下:
色系 类型 颜色
blue/拂晓蓝 色盘
less变量 @blue-1@blue-2 ... @blue-10
purple/酱紫 色盘
less变量 @purple-1@purple-2 ... @purple-10
cyan/明青 色盘
less变量 @cyan-1@cyan-2 ... @cyan-10
green/极光绿 色盘
less变量 @green-1@green-2 ... @green-10
magenta/法式洋红 色盘
less变量 @magenta-1@magenta-2 ... @magenta-10
red/薄暮 色盘
less变量 @red-1@red-2 ... @red-10
orange/日暮 色盘
less变量 @orange-1@orange-2 ... @orange-10
yellow/日出 色盘
less变量 @yellow-1@yellow-2 ... @yellow-10
volcano/火山 色盘
less变量 @volcano-1@volcano-2 ... @volcano-10
geekblue/极客蓝 色盘
less变量 @geekblue-1@geekblue-2 ... @geekblue-10
lime/青柠 色盘
less变量 @lime-1@lime-2 ... @lime-10
gold/金盏花 色盘
less变量 @gold-1@gold-2 ... @gold-10
以上色系对应的less变量均可以在你的样式代码中直接使用。 :::tip 我们建议在开发中使用 `less变量` 而不是直接使用 `颜色值` 来设置颜色。这样做对主题色和主题模式切换很有帮助。 ::: ## 主题模式 Vue Antd Admin 有三种主题模式,分别为:`light/亮色菜单模式`、`dark/暗色菜单模式` 和 `night/黑夜模式`。 light / 亮色菜单模式: ![light](../assets/mode-light.png) dark / 暗色菜单模式: ![dark](../assets/mode-dark.png) night / 黑夜模式: ![night](../assets/mode-night.png) 你可以在这三种模式之间随意切换,也可以在 src/config/config.js 中设置默认的主题模式。 ```js {4} module.exports = { theme: { color: '#13c2c2', mode: 'night' //设置你的默认主题模式,可选 light、dark 和 night }, multiPage: true, animate: { name: 'roll', direction: 'default' } } ``` ## 导航布局 Vue Antd Admin 有两种导航布局,`side/侧边导航` 和 `head/顶部导航`。 默认为侧边导航,你可以在 src/config/config.js 中修改导航布局 ```js {6} module.exports = { theme: { color: '#13c2c2', mode: 'night' }, layout: 'side', //设置你的默认导航布局,有 side 和 head 可选 multiPage: true, animate: { name: 'roll', direction: 'default' } } ``` ## 动画 Vue Antd Admin 内置了 [animate.css](https://animate.style) 动画库,在页面切换时会应用动画效果。你可以在 src/config/config.js 中配置动画效果或者禁用动画。 ```js {7-11} module.exports = { theme: { color: '#13c2c2', mode: 'night' }, multiPage: true, animate: { disabled: false, //禁用动画,true:禁用,false:启用 name: 'roll', //动画效果,支持的动画效果可参考 src/config/default/animate.config.js direction: 'default' //动画方向,切换页面时动画的方向,参考 src/config/default/animate.config.js } } ``` 支持的动画特效种类,可以参考 src/config/default/animate.config.js 文件。 ## 其它 ### 色弱模式 对于有视觉障碍的群体,我们提供了色弱模式,你可以通过配置 src/config/config.js 启用色弱模式 ```js {7} module.exports = { theme: { color: '#13c2c2', mode: 'night' }, multiPage: true, weekMode: false, //色弱模式,true:开启,false:不开启 animate: { name: 'roll', direction: 'default' } } ``` ### 多页签 在 src/config/config.js 设置 multiPage 来启用或关闭多页签模式 ```js {6} module.exports = { theme: { color: '#13c2c2', mode: 'night' }, multiPage: true, //多页签模式,true:开启,false:不开启 animate: { name: 'roll', direction: 'default' } } ``` 完整的系统设置参考 src/config/default/setting.config.js :::tip 以上所有主题设置项,均已映射到 vuex/setting 模块的 state 中,你可以通过提交 setting/mutations 实时修改设置项。 如何使用 [mutations](https://vuex.vuejs.org/zh/guide/mutations.html) ? ::: ================================================ FILE: front/docs/other/README.md ================================================ --- title: 其它 lang: zh-CN --- # 其它 ================================================ FILE: front/docs/other/community.md ================================================ --- title: 社区 lang: zh-CN --- # 社区 ## 交流学习 ### QQ群:812277510、610090280(已满) ================================================ FILE: front/docs/other/upgrade.md ================================================ --- title: 更新日志 lang: zh-CN --- # 更新日志 ================================================ FILE: front/docs/start/README.md ================================================ --- title: 开始 lang: zh-CN --- ## 开始 ================================================ FILE: front/docs/start/faq.md ================================================ --- title: 常见问题 lang: zh-CN --- # 常见问题 ### 为什么不是 Ant Design Pro Vue ? [Ant Design Pro Vue](https://github.com/vueComponent/ant-design-vue-pro) 是 [Ant Design Pro](https://github.com/ant-design/ant-design-pro) 的 Vue 版本,其中项目结构、组件、 布局和使用方法等基本与 Ant Design Pro 的 react 版本保持一致。如果你比较熟悉 react 版,或者你已经在使用它,这确实是一个不错的选择。 [Vue Antd Admin](https://github.com/iczer/vue-antd-admin) 同样实现了 Ant Design Pro 的所有功能。与此同时,我们还根据 Vue 的特性,对 Ant Design Pro 的一些组件和布局作出了相应的修改及优化,同时不影响保持与 Ant Design Pro 的一致。 另外,我们还在添加一些 Ant Design Pro 没有的功能,比如全局动画、多页签模式等。 如果你想使用 Ant Design Pro,但又觉得它缺乏一些你想要的功能,不妨看看 [Vue Antd Admin](https://github.com/iczer/vue-antd-admin),我们会认真考虑每个用户的需求。 因此,如果你有一些不错的想法和建议,欢迎随时和我们交流,很可能你的想法就在我们下一个版本中实现。 ### 如何使用 Vue Antd Admin ? 请阅读文档 [开始使用](./use.md)。有任何疑问,欢迎在 github 上给我们提交 [issue](https://github.com/iczer/vue-antd-admin/issues/new)。 ### 是否支持国际化 ? Vue Antd Admin 引入了 vue-i18n 支持。因此你可以使用 vue-i18n 的特性对项目做国际化修改,详细请查看 [国际化](../advance/i18n.md) ================================================ FILE: front/docs/start/use.md ================================================ --- title: 使用 lang: zh-CN --- # 使用 ## 准备 你的本地环境需要安装 yarn、node 和 git。我们的技术栈基于 ES2015+、Vue、Antd,提前学习这些知识会非常有帮助。 ## 安装 克隆本项目到本地 ```bash $ git clone https://github.com/iczer/vue-antd-admin.git ``` 安装依赖 ```bash $ yarn install or $ npm install ``` :::tip master 分支是 Vue Antd Admin 的标准版代码,此分支代码适合用于用于学习研究,不推荐在此分支做正式开发。 我们在 basic 分支提供了 Vue Antd Admin 的基础版代码,正式开发请切换至此分支,以便于后续的版本更新。 ::: :::warning 如果基于 `master分支` 进行开发,在版本更新时遇到的代码冲突问题请自行解决,我们不对基于 `master分支` 开发时遇到的问题提供技术支持。 再次强调,`master分支` 仅推荐用于学习参考,正式开发请切换至 `basic` 分支!!! ::: ## 目录结构 我们已经为你生成了一个完整的开发框架,提供了涵盖中后台开发的各类功能和坑位,下面是整个项目的目录结构。 ```bash ├── docs # 使用文档 ├── public │ └── favicon.png # favicon │ └── index.html # 入口 HTML ├── src │ ├── assets # 本地静态资源 │ ├── components # 内置通用组件 │ ├── config # 系统配置 │ ├── layouts # 通用布局 │ ├── mock # 本地 mock 数据 │ ├── pages # 页面组件和通用模板 │ ├── plugins # vue 插件 │ ├── router # 路由配置 │ ├── services # 数据服务模块 │ ├── store # vuex 状态管理配置 │ ├── theme # 主题相关 │ ├── utils # js 工具 │ ├── App.vue # 应用入口组件 │ ├── bootstrap.js # 应用启动引导js │ └── main.js # 应用入口js ├── package.json # package.json ├── README.md # README.md └── vue.config.js # vue 配置文件 ``` ## 本地开发 启动服务 ```bash $ yarn serve or $ npm run serve ``` 启动成功后,会看到一个本地预览地址,通常是 http://localhost:8080 。接下来就可以修改代码,并实时预览修改结果啦! ================================================ FILE: front/package.json ================================================ { "name": "vue-antd-admin", "version": "0.7.4", "homepage": "https://iczer.github.io/vue-antd-admin", "private": true, "scripts": { "serve": "vue-cli-service serve", "build": "vue-cli-service build", "lint": "vue-cli-service lint", "predeploy": "yarn build", "deploy": "gh-pages -d dist -b pages -r https://gitee.com/iczer/vue-antd-admin.git", "docs:dev": "vuepress dev docs", "docs:build": "vuepress build docs", "docs:deploy": "vuepress build docs && gh-pages -d docs/.vuepress/dist -b master -r https://gitee.com/iczer/vue-antd-admin-docs.git" }, "dependencies": { "@antv/data-set": "^0.11.4", "animate.css": "^4.1.0", "ant-design-vue": "1.7.2", "axios": "^0.21.1", "clipboard": "^2.0.6", "core-js": "^3.6.5", "date-fns": "^2.14.0", "echarts": "^5.1.1", "enquire.js": "^2.1.6", "highlight.js": "^10.2.1", "js-cookie": "^2.2.1", "lodash": "^4.17.21", "mockjs": "^1.1.0", "nprogress": "^0.2.0", "viser-vue": "^2.4.8", "vue": "^2.6.11", "vue-i18n": "^8.18.2", "vue-router": "^3.3.4", "vuedraggable": "^2.23.2", "vuex": "^3.4.0", "xlsx": "^0.17.0" }, "devDependencies": { "@ant-design/colors": "^4.0.1", "@vue/cli-plugin-babel": "^4.4.0", "@vue/cli-plugin-eslint": "^4.4.0", "@vue/cli-service": "^4.4.0", "@vuepress/plugin-back-to-top": "^1.5.2", "babel-eslint": "^10.1.0", "babel-plugin-transform-remove-console": "^6.9.4", "babel-polyfill": "^6.26.0", "compression-webpack-plugin": "^2.0.0", "deepmerge": "^4.2.2", "eslint": "^6.7.2", "eslint-plugin-vue": "^6.2.2", "fast-deep-equal": "^3.1.3", "gh-pages": "^3.1.0", "less-loader": "^6.1.1", "style-resources-loader": "^1.3.2", "vue-cli-plugin-style-resources-loader": "^0.1.4", "vue-template-compiler": "^2.6.11", "vuepress": "^1.5.2", "webpack-theme-color-replacer": "1.3.18", "whatwg-fetch": "^3.0.0" }, "eslintConfig": { "root": true, "env": { "node": true }, "extends": [ "plugin:vue/essential", "eslint:recommended" ], "parserOptions": { "parser": "babel-eslint" }, "rules": {} }, "browserslist": [ "> 1%", "last 2 versions", "not ie <= 10" ] } ================================================ FILE: front/public/index.html ================================================ <%= process.env.VUE_APP_NAME %> <% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %> <% } %>
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %> <% } %> ================================================ FILE: front/src/App.vue ================================================ ================================================ FILE: front/src/bootstrap.js ================================================ import {loadRoutes, loadGuards, setAppOptions} from '@/utils/routerUtil' import {loadInterceptors} from '@/utils/request' import guards from '@/router/guards' import interceptors from '@/utils/axios-interceptors' /** * 启动引导方法 * 应用启动时需要执行的操作放在这里 * @param router 应用的路由实例 * @param store 应用的 vuex.store 实例 * @param i18n 应用的 vue-i18n 实例 * @param i18n 应用的 message 实例 */ function bootstrap({router, store, i18n, message}) { // 设置应用配置 setAppOptions({router, store, i18n}) // 加载 axios 拦截器 loadInterceptors(interceptors, {router, store, i18n, message}) // 加载路由 loadRoutes() // 加载路由守卫 loadGuards(guards, {router, store, i18n, message}) } export default bootstrap ================================================ FILE: front/src/components/cache/AKeepAlive.js ================================================ import {isDef, isRegExp, remove} from '@/utils/util' const patternTypes = [String, RegExp, Array] function matches (pattern, name) { if (Array.isArray(pattern)) { if (pattern.indexOf(name) > -1) { return true } else { for (let item of pattern) { if (isRegExp(item) && item.test(name)) { return true } } return false } } else if (typeof pattern === 'string') { return pattern.split(',').indexOf(name) > -1 } else if (isRegExp(pattern)) { return pattern.test(name) } /* istanbul ignore next */ return false } function getComponentName (opts) { return opts && (opts.Ctor.options.name || opts.tag) } function getComponentKey (vnode) { const {componentOptions, key} = vnode return key == null ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '') : key + componentOptions.Ctor.cid } function getFirstComponentChild (children) { if (Array.isArray(children)) { for (let i = 0; i < children.length; i++) { const c = children[i] if (isDef(c) && (isDef(c.componentOptions) || c.isAsyncPlaceholder)) { return c } } } } function pruneCache (keepAliveInstance, filter) { const { cache, keys, _vnode } = keepAliveInstance for (const key in cache) { const cachedNode = cache[key] if (cachedNode) { const name = getComponentName(cachedNode.componentOptions) const componentKey = getComponentKey(cachedNode) if (name && !filter(name, componentKey)) { pruneCacheEntry(cache, key, keys, _vnode) } } } } function pruneCacheEntry2(cache, key, keys) { const cached = cache[key] if (cached) { cached.componentInstance.$destroy() } cache[key] = null remove(keys, key) } function pruneCacheEntry (cache, key, keys, current) { const cached = cache[key] if (cached && (!current || cached.tag !== current.tag)) { cached.componentInstance.$destroy() } cache[key] = null remove(keys, key) } export default { name: 'AKeepAlive', abstract: true, model: { prop: 'clearCaches', event: 'clear', }, props: { include: patternTypes, exclude: patternTypes, excludeKeys: patternTypes, max: [String, Number], clearCaches: Array }, watch: { clearCaches: function(val) { if (val && val.length > 0) { const {cache, keys} = this val.forEach(key => { pruneCacheEntry2(cache, key, keys) }) this.$emit('clear', []) } } }, created() { this.cache = Object.create(null) this.keys = [] }, destroyed () { for (const key in this.cache) { pruneCacheEntry(this.cache, key, this.keys) } }, mounted () { this.$watch('include', val => { pruneCache(this, (name) => matches(val, name)) }) this.$watch('exclude', val => { pruneCache(this, (name) => !matches(val, name)) }) this.$watch('excludeKeys', val => { pruneCache(this, (name, key) => !matches(val, key)) }) }, render () { const slot = this.$slots.default const vnode = getFirstComponentChild(slot) const componentOptions = vnode && vnode.componentOptions if (componentOptions) { // check pattern const name = getComponentName(componentOptions) const componentKey = getComponentKey(vnode) const { include, exclude, excludeKeys } = this if ( // not included (include && (!name || !matches(include, name))) || // excluded (exclude && name && matches(exclude, name)) || (excludeKeys && componentKey && matches(excludeKeys, componentKey)) ) { return vnode } const { cache, keys } = this const key = vnode.key == null // same constructor may get registered as different local components // so cid alone is not enough (#3269) ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '') : vnode.key + componentOptions.Ctor.cid if (cache[key]) { vnode.componentInstance = cache[key].componentInstance // make current key freshest remove(keys, key) keys.push(key) } else { cache[key] = vnode keys.push(key) // prune oldest entry if (this.max && keys.length > parseInt(this.max)) { pruneCacheEntry(cache, keys[0], keys, this._vnode) } } vnode.data.keepAlive = true } return vnode || (slot && slot[0]) } } ================================================ FILE: front/src/components/card/ChartCard.vue ================================================ ================================================ FILE: front/src/components/chart/Bar.vue ================================================ ================================================ FILE: front/src/components/chart/MiniArea.vue ================================================ ================================================ FILE: front/src/components/chart/MiniBar.vue ================================================ ================================================ FILE: front/src/components/chart/MiniProgress.vue ================================================ ================================================ FILE: front/src/components/chart/Radar.vue ================================================ ================================================ FILE: front/src/components/chart/RankingList.vue ================================================ ================================================ FILE: front/src/components/chart/Trend.vue ================================================ ================================================ FILE: front/src/components/chart/index.less ================================================ .mini-chart{ position: relative; width: 100%; .chart-content{ position: absolute; bottom: -28px; width: 100%; } } ================================================ FILE: front/src/components/checkbox/ColorCheckbox.vue ================================================ ================================================ FILE: front/src/components/checkbox/ImgCheckbox.vue ================================================ ================================================ FILE: front/src/components/checkbox/index.js ================================================ import ColorCheckbox from '@/components/checkbox/ColorCheckbox' import ImgCheckbox from '@/components/checkbox/ImgCheckbox' export { ColorCheckbox, ImgCheckbox } ================================================ FILE: front/src/components/exception/ExceptionPage.vue ================================================ ================================================ FILE: front/src/components/exception/typeConfig.js ================================================ const config = { 403: { img: 'https://gw.alipayobjects.com/zos/rmsportal/wZcnGqRDyhPOEYFcZDnb.svg', title: '403', desc: '抱歉,你无权访问该页面' }, 404: { img: 'https://gw.alipayobjects.com/zos/rmsportal/KpnpchXsobRgLElEozzI.svg', title: '404', desc: '抱歉,你访问的页面不存在或仍在开发中' }, 500: { img: 'https://gw.alipayobjects.com/zos/rmsportal/RVRUAYdCGeYNBWoKiIwB.svg', title: '500', desc: '抱歉,服务器出错了' } } export default config ================================================ FILE: front/src/components/form/FormRow.vue ================================================ ================================================ FILE: front/src/components/input/IInput.vue ================================================ ================================================ FILE: front/src/components/menu/Contextmenu.vue ================================================ ================================================ FILE: front/src/components/menu/SideMenu.vue ================================================ ================================================ FILE: front/src/components/menu/index.less ================================================ .shadow{ box-shadow: 2px 0 6px rgba(0, 21, 41, .35); } .side-menu{ min-height: 100vh; overflow-y: auto; z-index: 10; .logo{ height: 64px; position: relative; line-height: 64px; padding-left: 24px; -webkit-transition: all .3s; transition: all .3s; overflow: hidden; background-color: @layout-trigger-background; &.light{ background-color: #fff; h1{ color: @primary-color; } } h1{ color: @menu-dark-highlight-color; font-size: 20px; margin: 0 0 0 12px; display: inline-block; vertical-align: middle; } img{ width: 32px; vertical-align: middle; } } } .menu{ padding: 16px 0; } ================================================ FILE: front/src/components/menu/menu.js ================================================ /** * 该插件可根据菜单配置自动生成 ANTD menu组件 * menuOptions示例: * [ * { * name: '菜单名称', * path: '菜单路由', * meta: { * icon: '菜单图标', * invisible: 'boolean, 是否不可见, 默认 false', * }, * children: [子菜单配置] * }, * { * name: '菜单名称', * path: '菜单路由', * meta: { * icon: '菜单图标', * invisible: 'boolean, 是否不可见, 默认 false', * }, * children: [子菜单配置] * } * ] * * i18n: 国际化配置。系统默认会根据 options route配置的 path 和 name 生成英文以及中文的国际化配置,如需自定义或增加其他语言,配置 * 此项即可。如: * i18n: { * messages: { * CN: {dashboard: {name: '监控中心'}} * HK: {dashboard: {name: '監控中心'}} * } * } **/ import Menu from 'ant-design-vue/es/menu' import Icon from 'ant-design-vue/es/icon' import fastEqual from 'fast-deep-equal' import {getI18nKey} from '@/utils/routerUtil' const {Item, SubMenu} = Menu const resolvePath = (path, params = {}) => { let _path = path Object.entries(params).forEach(([key, value]) => { _path = _path.replace(new RegExp(`:${key}`, 'g'), value) }) return _path } const toRoutesMap = (routes) => { const map = {} routes.forEach(route => { map[route.fullPath] = route if (route.children && route.children.length > 0) { const childrenMap = toRoutesMap(route.children) Object.assign(map, childrenMap) } }) return map } export default { name: 'IMenu', props: { options: { type: Array, required: true }, theme: { type: String, required: false, default: 'dark' }, mode: { type: String, required: false, default: 'inline' }, collapsed: { type: Boolean, required: false, default: false }, i18n: Object, openKeys: Array }, data () { return { selectedKeys: [], sOpenKeys: [], cachedOpenKeys: [] } }, computed: { menuTheme() { return this.theme == 'light' ? this.theme : 'dark' }, routesMap() { return toRoutesMap(this.options) } }, created () { this.updateMenu() if (this.options.length > 0 && !this.options[0].fullPath) { this.formatOptions(this.options, '') } // 自定义国际化配置 if(this.i18n && this.i18n.messages) { const messages = this.i18n.messages Object.keys(messages).forEach(key => { this.$i18n.mergeLocaleMessage(key, messages[key]) }) } }, watch: { options(val) { if (val.length > 0 && !val[0].fullPath) { this.formatOptions(this.options, '') } }, i18n(val) { if(val && val.messages) { const messages = this.i18n.messages Object.keys(messages).forEach(key => { this.$i18n.mergeLocaleMessage(key, messages[key]) }) } }, collapsed (val) { if (val) { this.cachedOpenKeys = this.sOpenKeys this.sOpenKeys = [] } else { this.sOpenKeys = this.cachedOpenKeys } }, '$route': function () { this.updateMenu() }, sOpenKeys(val) { this.$emit('openChange', val) this.$emit('update:openKeys', val) } }, methods: { renderIcon: function (h, icon, key) { if (this.$scopedSlots.icon && icon && icon !== 'none') { const vnodes = this.$scopedSlots.icon({icon, key}) vnodes.forEach(vnode => { vnode.data.class = vnode.data.class ? vnode.data.class : [] vnode.data.class.push('anticon') }) return vnodes } return !icon || icon == 'none' ? null : h(Icon, {props: {type: icon}}) }, renderMenuItem: function (h, menu) { let tag = 'router-link' const path = resolvePath(menu.fullPath, menu.meta.params) let config = {props: {to: {path, query: menu.meta.query}, }, attrs: {style: 'overflow:hidden;white-space:normal;text-overflow:clip;'}} if (menu.meta && menu.meta.link) { tag = 'a' config = {attrs: {style: 'overflow:hidden;white-space:normal;text-overflow:clip;', href: menu.meta.link, target: '_blank'}} } return h( Item, {key: menu.fullPath}, [ h(tag, config, [ this.renderIcon(h, menu.meta ? menu.meta.icon : 'none', menu.fullPath), this.$t(getI18nKey(menu.fullPath)) ] ) ] ) }, renderSubMenu: function (h, menu) { let this_ = this let subItem = [h('span', {slot: 'title', attrs: {style: 'overflow:hidden;white-space:normal;text-overflow:clip;'}}, [ this.renderIcon(h, menu.meta ? menu.meta.icon : 'none', menu.fullPath), this.$t(getI18nKey(menu.fullPath)) ] )] let itemArr = [] menu.children.forEach(function (item) { itemArr.push(this_.renderItem(h, item)) }) return h(SubMenu, {key: menu.fullPath}, subItem.concat(itemArr) ) }, renderItem: function (h, menu) { const meta = menu.meta if (!meta || !meta.invisible) { let renderChildren = false const children = menu.children if (children != undefined) { for (let i = 0; i < children.length; i++) { const childMeta = children[i].meta if (!childMeta || !childMeta.invisible) { renderChildren = true break } } } return (menu.children && renderChildren) ? this.renderSubMenu(h, menu) : this.renderMenuItem(h, menu) } }, renderMenu: function (h, menuTree) { let this_ = this let menuArr = [] menuTree.forEach(function (menu, i) { menuArr.push(this_.renderItem(h, menu, '0', i)) }) return menuArr }, formatOptions(options, parentPath) { options.forEach(route => { let isFullPath = route.path.substring(0, 1) == '/' route.fullPath = isFullPath ? route.path : parentPath + '/' + route.path if (route.children) { this.formatOptions(route.children, route.fullPath) } }) }, updateMenu () { this.selectedKeys = this.getSelectedKeys() let openKeys = this.selectedKeys.filter(item => item !== '') openKeys = openKeys.slice(0, openKeys.length -1) if (!fastEqual(openKeys, this.sOpenKeys)) { this.collapsed || this.mode === 'horizontal' ? this.cachedOpenKeys = openKeys : this.sOpenKeys = openKeys } }, getSelectedKeys() { let matches = this.$route.matched const route = matches[matches.length - 1] let chose = this.routesMap[route.path] if (chose.meta && chose.meta.highlight) { chose = this.routesMap[chose.meta.highlight] const resolve = this.$router.resolve({path: chose.fullPath}) matches = (resolve.resolved && resolve.resolved.matched) || matches } return matches.map(item => item.path) } }, render (h) { return h( Menu, { props: { theme: this.menuTheme, mode: this.$props.mode, selectedKeys: this.selectedKeys, openKeys: this.openKeys ? this.openKeys : this.sOpenKeys }, on: { 'update:openKeys': (val) => { this.sOpenKeys = val }, click: (obj) => { obj.selectedKeys = [obj.key] this.$emit('select', obj) } } }, this.renderMenu(h, this.options) ) } } ================================================ FILE: front/src/components/page/header/PageHeader.vue ================================================ ================================================ FILE: front/src/components/page/header/index.less ================================================ .page-header{ background: @base-bg-color; padding: 16px 24px; &.head.fixed{ margin: auto; max-width: 1400px; } .page-header-wide{ .breadcrumb{ margin-bottom: 20px; } .detail{ display: flex; .row { display: flex; flex-wrap: wrap; justify-content: space-between; } .avatar { margin:0 24px 0 0; } .main{ width: 100%; .title{ font-size: 20px; color: @title-color; margin-bottom: 16px; } .content{ display: flex; flex-wrap: wrap; color: @text-color-second; } .extra{ display: flex; } } } } } ================================================ FILE: front/src/components/result/Result.vue ================================================ ================================================ FILE: front/src/components/setting/Setting.vue ================================================ ================================================ FILE: front/src/components/setting/SettingItem.vue ================================================ ================================================ FILE: front/src/components/setting/i18n.js ================================================ module.exports = { messages: { CN: { theme: { title: '整体风格设置', light: '亮色菜单风格', dark: '暗色菜单风格', night: '深夜模式', color: '主题色' }, navigate: { title: '导航设置', side: '侧边导航', head: '顶部导航', mix: '混合导航', content: { title: '内容区域宽度', fluid: '流式', fixed: '定宽' }, fixedHeader: '固定Header', fixedSideBar: '固定侧边栏', }, other: { title: '其他设置', weekMode: '色弱模式', multiPages: '多页签模式', hideSetting: '隐藏设置抽屉' }, animate: { title: '页面切换动画', disable: '禁用动画', effect: '动画效果', direction: '动画方向' }, alert: '拷贝配置后,直接覆盖文件 src/config/config.js 中的全部内容,然后重启即可。(注意:仅会拷贝与默认配置不同的项)', copy: '拷贝配置', save: '保存配置', reset: '重置配置', }, HK: { theme: { title: '整體風格設置', light: '亮色菜單風格', dark: '暗色菜單風格', night: '深夜模式', color: '主題色' }, navigate: { title: '導航設置', side: '側邊導航', head: '頂部導航', content: { title: '內容區域寬度', fluid: '流式', fixed: '定寬' }, fixedHeader: '固定Header', fixedSideBar: '固定側邊欄', }, other: { title: '其他設置', weekMode: '色弱模式', multiPages: '多頁簽模式', hideSetting: '隱藏設置抽屜' }, animate: { title: '頁面切換動畫', disable: '禁用動畫', effect: '動畫效果', direction: '動畫方向' }, alert: '拷貝配置后,直接覆蓋文件 src/config/config.js 中的全部內容,然後重啟即可。(注意:僅會拷貝與默認配置不同的項)', copy: '拷貝配置', save: '保存配置', reset: '重置配置', }, US: { theme: { title: 'Page Style Setting', light: 'Light Style', dark: 'Dark Style', night: 'Night Style', color: 'Theme Color' }, navigate: { title: 'Navigation Mode', side: 'Side Menu Layout', head: 'Top Menu Layout', mix: 'Mix Menu Layout', content: { title: 'Content Width', fluid: 'Fluid', fixed: 'Fixed' }, fixedHeader: 'Fixed Header', fixedSideBar: 'Fixed SideBar', }, other: { title: 'Other Setting', weekMode: 'Week Mode', multiPages: 'Multi Pages', hideSetting: 'Hide Setting Drawer' }, animate: { title: 'Page Toggle Animation', disable: 'Disable', effect: 'Effect', direction: 'Direction' }, alert: 'After copying the configuration code, directly cover all contents in the file src/config/config.js, then restart the server. (Note: only items that are different from the default configuration will be copied)', copy: 'Copy Setting', save: 'Save', reset: 'Reset', } } } ================================================ FILE: front/src/components/table/StandardTable.vue ================================================ ================================================ FILE: front/src/components/table/advance/ActionColumns.vue ================================================ ================================================ FILE: front/src/components/table/advance/ActionSize.vue ================================================ ================================================ FILE: front/src/components/table/advance/AdvanceTable.vue ================================================ ================================================ FILE: front/src/components/table/advance/SearchArea.vue ================================================ ================================================ FILE: front/src/components/table/advance/index.js ================================================ import AdvanceTable from './AdvanceTable' export default AdvanceTable ================================================ FILE: front/src/components/table/api/ApiTable.vue ================================================ ================================================ FILE: front/src/components/task/TaskGroup.vue ================================================ ================================================ FILE: front/src/components/task/TaskItem.vue ================================================ ================================================ FILE: front/src/components/tool/AStepItem.vue ================================================ ================================================ FILE: front/src/components/tool/AvatarList.vue ================================================ ================================================ FILE: front/src/components/tool/DetailList.vue ================================================ ================================================ FILE: front/src/components/tool/Drawer.vue ================================================ ================================================ FILE: front/src/components/tool/FooterToolBar.vue ================================================ ================================================ FILE: front/src/components/tool/HeadInfo.vue ================================================ ================================================ FILE: front/src/components/tool/TagSelect.vue ================================================ ================================================ FILE: front/src/components/tool/TagSelectOption.vue ================================================ ================================================ FILE: front/src/components/transition/PageToggleTransition.vue ================================================ ================================================ FILE: front/src/config/config.js ================================================ // 自定义配置,参考 ./default/setting.config.js,需要自定义的属性在这里配置即可 module.exports = { pageWidth: 'fluid', multiPage: true, animate: { disabled: false, name: 'back', direction: 'left' } } ================================================ FILE: front/src/config/default/admin.config.js ================================================ // admin 配置 const ADMIN = { palettes: ['#f5222d', '#fa541c', '#fadb14', '#3eaf7c', '#13c2c2', '#1890ff', '#722ed1', '#eb2f96'], animates: require('./animate.config').preset, theme: { mode: { DARK: 'dark', LIGHT: 'light', NIGHT: 'night' } }, layout: { SIDE: 'side', HEAD: 'head' } } module.exports = ADMIN ================================================ FILE: front/src/config/default/animate.config.js ================================================ const direct_s = ['left', 'right'] const direct_1 = ['left', 'right', 'down', 'up'] const direct_1_b = ['downBig', 'upBig', 'leftBig', 'rightBig'] const direct_2 = ['topLeft', 'bottomRight', 'topRight', 'bottomLeft'] const direct_3 = ['downLeft', 'upRight', 'downRight', 'upLeft'] // animate.css 配置 const ANIMATE = { preset: [ //预设动画配置 {name: 'back', alias: '渐近', directions: direct_1}, {name: 'bounce', alias: '弹跳', directions: direct_1.concat('default')}, {name: 'fade', alias: '淡化', directions: direct_1.concat(direct_1_b).concat(direct_2).concat('default')}, {name: 'flip', alias: '翻转', directions: ['x', 'y']}, {name: 'lightSpeed', alias: '光速', directions: direct_s}, {name: 'rotate', alias: '旋转', directions: direct_3.concat('default')}, {name: 'roll', alias: '翻滚', directions: ['default']}, {name: 'zoom', alias: '缩放', directions: direct_1.concat('default')}, {name: 'slide', alias: '滑动', directions: direct_1}, ] } module.exports = ANIMATE ================================================ FILE: front/src/config/default/antd.config.js ================================================ // antd 配置 const ANTD = { primary: { color: '#1890ff', warning: '#faad14', success: '#52c41a', error: '#f5222d', light: { menuColors: ['#000c17', '#001529', '#002140'] }, dark: { menuColors: ['#000c17', '#001529', '#002140'] }, night: { menuColors: ['#151515', '#1f1f1f', '#1e1e1e'], } }, theme: { dark: { 'layout-body-background': '#f0f2f5', 'body-background': '#fff', 'component-background': '#fff', 'heading-color': 'rgba(0, 0, 0, 0.85)', 'text-color': 'rgba(0, 0, 0, 0.65)', 'text-color-inverse': '#fff', 'text-color-secondary': 'rgba(0, 0, 0, 0.45)', 'shadow-color': 'rgba(0, 0, 0, 0.15)', 'border-color-split': '#f0f0f0', 'background-color-light': '#fafafa', 'background-color-base': '#f5f5f5', 'table-selected-row-bg': '#fafafa', 'table-expanded-row-bg': '#fbfbfb', 'checkbox-check-color': '#fff', 'disabled-color': 'rgba(0, 0, 0, 0.25)', 'menu-dark-color': 'rgba(254, 254, 254, 0.65)', 'menu-dark-highlight-color': '#fefefe', 'menu-dark-arrow-color': '#fefefe', 'btn-primary-color': '#fff', }, light: { 'layout-body-background': '#f0f2f5', 'body-background': '#fff', 'component-background': '#fff', 'heading-color': 'rgba(0, 0, 0, 0.85)', 'text-color': 'rgba(0, 0, 0, 0.65)', 'text-color-inverse': '#fff', 'text-color-secondary': 'rgba(0, 0, 0, 0.45)', 'shadow-color': 'rgba(0, 0, 0, 0.15)', 'border-color-split': '#f0f0f0', 'background-color-light': '#fafafa', 'background-color-base': '#f5f5f5', 'table-selected-row-bg': '#fafafa', 'table-expanded-row-bg': '#fbfbfb', 'checkbox-check-color': '#fff', 'disabled-color': 'rgba(0, 0, 0, 0.25)', 'menu-dark-color': 'rgba(1, 1, 1, 0.65)', 'menu-dark-highlight-color': '#fefefe', 'menu-dark-arrow-color': '#fefefe', 'btn-primary-color': '#fff', }, night: { 'layout-body-background': '#000', 'body-background': '#141414', 'component-background': '#141414', 'heading-color': 'rgba(255, 255, 255, 0.85)', 'text-color': 'rgba(255, 255, 255, 0.85)', 'text-color-inverse': '#141414', 'text-color-secondary': 'rgba(255, 255, 255, 0.45)', 'shadow-color': 'rgba(255, 255, 255, 0.15)', 'border-color-split': '#303030', 'background-color-light': '#ffffff0a', 'background-color-base': '#2a2a2a', 'table-selected-row-bg': '#ffffff0a', 'table-expanded-row-bg': '#ffffff0b', 'checkbox-check-color': '#141414', 'disabled-color': 'rgba(255, 255, 255, 0.25)', 'menu-dark-color': 'rgba(254, 254, 254, 0.65)', 'menu-dark-highlight-color': '#fefefe', 'menu-dark-arrow-color': '#fefefe', 'btn-primary-color': '#141414', } } } module.exports = ANTD ================================================ FILE: front/src/config/default/index.js ================================================ const ANTD = require('./antd.config') const ADMIN = require('./admin.config') const ANIMATE = require('./animate.config') const setting = require('./setting.config') module.exports = {ANTD, ADMIN, ANIMATE, setting} ================================================ FILE: front/src/config/default/setting.config.js ================================================ // 此配置为系统默认设置,需修改的设置项,在src/config/config.js中添加修改项即可。也可直接在此文件中修改。 module.exports = { lang: 'CN', //语言,可选 CN(简体)、HK(繁体)、US(英语),也可扩展其它语言 theme: { //主题 color: '#1890ff', //主题色 mode: 'dark', //主题模式 可选 dark、 light 和 night success: '#52c41a', //成功色 warning: '#faad14', //警告色 error: '#f5222f', //错误色 }, layout: 'side', //导航布局,可选 side 和 head,分别为侧边导航和顶部导航 fixedHeader: false, //固定头部状态栏,true:固定,false:不固定 fixedSideBar: true, //固定侧边栏,true:固定,false:不固定 fixedTabs: false, //固定页签头,true:固定,false:不固定 pageWidth: 'fixed', //内容区域宽度,fixed:固定宽度,fluid:流式宽度 weekMode: false, //色弱模式,true:开启,false:不开启 multiPage: false, //多页签模式,true:开启,false:不开启 cachePage: true, //是否缓存页面数据,仅多页签模式下生效,true 缓存, false 不缓存 hideSetting: false, //隐藏设置抽屉,true:隐藏,false:不隐藏 systemName: '客户关系管理系统', //系统名称 copyright: '2021 抒颖工作室出品', //copyright asyncRoutes: false, //异步加载路由,true:开启,false:不开启 showPageTitle: true, //是否显示页面标题(PageLayout 布局中的页面标题),true:显示,false:不显示 filterMenu: true, //根据权限过滤菜单,true:过滤,false:不过滤 animate: { //动画设置 disabled: false, //禁用动画,true:禁用,false:启用 name: 'bounce', //动画效果,支持的动画效果可参考 ./animate.config.js direction: 'left' //动画方向,切换页面时动画的方向,参考 ./animate.config.js }, footerLinks: [ //页面底部链接,{link: '链接地址', name: '名称/显示文字', icon: '图标,支持 ant design vue 图标库'} {link: 'https://pro.ant.design', name: 'Pro首页'}, {link: 'https://msy.plus', name: '官方网站'}, {link: 'https://github.com/moshuying/project-3-crm',name: ' 项目地址 欢迎star ', icon: 'github'}, {link: 'https://ant.design', name: 'Ant Design'} ], } ================================================ FILE: front/src/config/index.js ================================================ const deepMerge = require('deepmerge') const _config = require('./config') const {setting} = require('./default') const config = deepMerge(setting, _config) module.exports = config ================================================ FILE: front/src/config/replacer/index.js ================================================ /** * webpack-theme-color-replacer 配置 * webpack-theme-color-replacer 是一个高效的主题色替换插件,可以实现系统运行时动态切换主题功能。 * 但有些情景下,我们需要为 webpack-theme-color-replacer 配置一些规则,以达到我们的个性化需求的目的 * * @cssResolve: css处理规则,在 webpack-theme-color-replacer 提取 需要替换主题色的 css 后,应用此规则。一般在 * webpack-theme-color-replacer 默认规则无法达到我们的要求时使用。 */ const cssResolve = require('./resolve.config') module.exports = {cssResolve} ================================================ FILE: front/src/config/replacer/resolve.config.js ================================================ /** * webpack-theme-color-replacer 插件的 resolve 配置 * 为特定的 css 选择器(selector)配置 resolve 规则。 * * key 为 css selector 值或合法的正则表达式字符串 * 当 key 设置 css selector 值时,会匹配对应的 css * 当 key 设置为正则表达式时,会匹配所有满足此正则表达式的的 css * * value 可以设置为 boolean 值 false 或 一个对象 * 当 value 为 false 时,则会忽略此 css,即此 css 不纳入 webpack-theme-color-replacer 管理 * 当 value 为 对象时,会调用该对象的 resolve 函数,并传入 cssText(原始的 css文本) 和 cssObj(css对象)参数; resolve函数应该返 * 回一个处理后的、合法的 css字符串(包含 selector) * 注意: value 不能设置为 true */ const cssResolve = { '.ant-checkbox-checked .ant-checkbox-inner::after': { resolve(cssText, cssObj) { cssObj.rules.push('border-top:0', 'border-left:0') return cssObj.toText() } }, '.ant-tree-checkbox-checked .ant-tree-checkbox-inner::after': { resolve(cssText, cssObj) { cssObj.rules.push('border-top:0', 'border-left:0') return cssObj.toText() } }, '.ant-checkbox-checked .ant-checkbox-inner:after': { resolve(cssText, cssObj) { cssObj.rules.push('border-top:0', 'border-left:0') return cssObj.toText() } }, '.ant-tree-checkbox-checked .ant-tree-checkbox-inner:after': { resolve(cssText, cssObj) { cssObj.rules.push('border-top:0', 'border-left:0') return cssObj.toText() } }, '.ant-menu-dark .ant-menu-inline.ant-menu-sub': { resolve(cssText, cssObj) { cssObj.rules = cssObj.rules.filter(rule => rule.indexOf('box-shadow') == -1) return cssObj.toText() } }, '.ant-menu-horizontal>.ant-menu-item:hover,.ant-menu-horizontal>.ant-menu-submenu:hover,.ant-menu-horizontal>.ant-menu-item-active,.ant-menu-horizontal>.ant-menu-submenu-active,.ant-menu-horizontal>.ant-menu-item-open,.ant-menu-horizontal>.ant-menu-submenu-open,.ant-menu-horizontal>.ant-menu-item-selected,.ant-menu-horizontal>.ant-menu-submenu-selected': { resolve(cssText, cssObj) { cssObj.selector = cssObj.selector.replace(/.ant-menu-horizontal/g, '.ant-menu-horizontal:not(.ant-menu-dark)') return cssObj.toText() } }, '.ant-menu-horizontal>.ant-menu-item-active,.ant-menu-horizontal>.ant-menu-item-open,.ant-menu-horizontal>.ant-menu-item-selected,.ant-menu-horizontal>.ant-menu-item:hover,.ant-menu-horizontal>.ant-menu-submenu-active,.ant-menu-horizontal>.ant-menu-submenu-open,.ant-menu-horizontal>.ant-menu-submenu-selected,.ant-menu-horizontal>.ant-menu-submenu:hover': { resolve(cssText, cssObj) { cssObj.selector = cssObj.selector.replace(/.ant-menu-horizontal/g, '.ant-menu-horizontal:not(.ant-menu-dark)') return cssObj.toText() } }, '.ant-layout-sider': { resolve(cssText, cssObj) { cssObj.selector = '.ant-layout-sider-dark' return cssObj.toText() } }, '/keyframes/': false } module.exports = cssResolve ================================================ FILE: front/src/layouts/AdminLayout.vue ================================================ ================================================ FILE: front/src/layouts/BlankView.vue ================================================ ================================================ FILE: front/src/layouts/CommonLayout.vue ================================================ ================================================ FILE: front/src/layouts/PageLayout.vue ================================================ ================================================ FILE: front/src/layouts/PageView.vue ================================================ ================================================ FILE: front/src/layouts/footer/PageFooter.vue ================================================ ================================================ FILE: front/src/layouts/header/AdminHeader.vue ================================================ ================================================ FILE: front/src/layouts/header/HeaderAvatar.vue ================================================ ================================================ FILE: front/src/layouts/header/HeaderNotice.vue ================================================ ================================================ FILE: front/src/layouts/header/HeaderSearch.vue ================================================ ================================================ FILE: front/src/layouts/header/index.less ================================================ .admin-header{ padding: 0; z-index: 2; box-shadow: @shadow-down; position: relative; background: @base-bg-color; .head-menu{ height: 64px; line-height: 64px; vertical-align: middle; box-shadow: none; } &.dark{ background: @header-bg-color-dark; color: white; } &.night{ .head-menu{ background: @base-bg-color; } } .admin-header-wide{ padding-left: 24px; &.head.fixed{ max-width: 1400px; margin: auto; padding-left: 0; } &.side{ padding-right: 12px; } .logo { height: 64px; line-height: 58px; vertical-align: top; display: inline-block; padding: 0 12px 0 24px; cursor: pointer; font-size: 20px; color: inherit; &.pc{ padding: 0 12px 0 0; } img { vertical-align: middle; } h1{ color: inherit; display: inline-block; font-size: 16px; } } .trigger { font-size: 20px; line-height: 64px; padding: 0 24px; cursor: pointer; transition: color .3s; &:hover{ color: @primary-color; } } .admin-header-menu{ display: inline-block; } .admin-header-right{ float: right; display: flex; color: inherit; .header-item{ color: inherit; padding: 0 12px; cursor: pointer; align-self: center; a{ color: inherit; i{ font-size: 16px; } } } each(@theme-list, { &.@{value} .header-item{ &:hover{ @class: ~'hover-bg-color-@{value}'; background-color: @@class; } } }) } } } ================================================ FILE: front/src/layouts/tabs/TabsHead.vue ================================================ ================================================ FILE: front/src/layouts/tabs/TabsView.vue ================================================ ================================================ FILE: front/src/layouts/tabs/i18n.js ================================================ module.exports = { messages: { CN: { closeLeft: '关闭左侧', closeRight: '关闭右侧', closeOthers: '关闭其它', refresh: '刷新页面', warn: '这是最后一页,不能再关闭了', }, HK: { closeLeft: '關閉左側', closeRight: '關閉右側', closeOthers: '關閉其它', refresh: '刷新頁面', warn: '這是最後一頁,不能再關閉了', }, US: { closeLeft: 'close left', closeRight: 'close right', closeOthers: 'close others', refresh: 'refresh the page', warn: 'This is the last page, you can\'t close it', }, } } ================================================ FILE: front/src/layouts/tabs/index.js ================================================ import TabsView from './TabsView' export default TabsView ================================================ FILE: front/src/main.js ================================================ import Vue from 'vue' import App from './App.vue' import {initRouter} from './router' import './theme/index.less' import Antd from 'ant-design-vue' import Viser from 'viser-vue' import '@/mock' import store from './store' import 'animate.css/source/animate.css' import Plugins from '@/plugins' import {initI18n} from '@/utils/i18n' import bootstrap from '@/bootstrap' import 'moment/locale/zh-cn' const router = initRouter(store.state.setting.asyncRoutes) const i18n = initI18n('CN', 'US') Vue.use(Antd) Vue.config.productionTip = false Vue.use(Viser) Vue.use(Plugins) bootstrap({router, store, i18n, message: Vue.prototype.$message}) new Vue({ router, store, i18n, render: h => h(App), }).$mount('#app') ================================================ FILE: front/src/mock/common/activityData.js ================================================ import {users, groups} from './index' const events = [ { type: 0, event: '八月迭代' }, { type: 1, event: '留言' }, { type: 2, event: '项目进展' } ] const activities = users.map((user, index) => { return { user: Object.assign({}, user, {group: groups[user.groupId]}), activity: events[index % events.length], template: '' } }) const templates = [ (user, activity) => { return `${user.name} 在 ${user.group} 新建项目 ${activity.event}` }, (user, activity) => { return `${user.name} 在 ${user.group} 发布了 ${activity.event}` }, (user, activity) => { return `${user.name} 将 ${activity.event} 更新至已发布状态` } ] export {activities, templates} ================================================ FILE: front/src/mock/common/index.js ================================================ const avatars = [ 'https://gw.alipayobjects.com/zos/rmsportal/cnrhVkzwxjPwAaCfPbdc.png', 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png', 'https://gw.alipayobjects.com/zos/rmsportal/gaOngJwsRYRaVAuXXcmB.png', 'https://gw.alipayobjects.com/zos/rmsportal/WhxKECPNujWoWEFNdnJE.png', 'https://gw.alipayobjects.com/zos/rmsportal/ubnKSIfAJTxIgXOKlciN.png', 'https://gw.alipayobjects.com/zos/rmsportal/jZUIxmJycoymBprLOUbT.png' ] const positions = [ { CN: 'Java工程师 | 蚂蚁金服-计算服务事业群-微信平台部', HK: 'Java工程師 | 螞蟻金服-計算服務事業群-微信平台部', US: 'Java engineer | Ant financial - Computing services business group - WeChat platform division' },{ CN: '前端工程师 | 蚂蚁金服-计算服务事业群-VUE平台', HK: '前端工程師 | 螞蟻金服-計算服務事業群-VUE平台', US: 'Front-end engineer | Ant Financial - Computing services business group - VUE platform' },{ CN: '前端工程师 | 蚂蚁金服-计算服务事业群-REACT平台', HK: '前端工程師 | 螞蟻金服-計算服務事業群-REACT平台', US: 'Front-end engineer | Ant Financial - Computing services business group - REACT platform' },{ CN: '产品分析师 | 蚂蚁金服-计算服务事业群-IOS平台部', HK: '產品分析師 | 螞蟻金服-計算服務事業群-IOS平台部', US: 'Product analyst | Ant Financial - Computing services business group - IOS platform division' } ] const sayings = [ '那是一种内在的东西,他们到达不了,也无法触及的', '希望是一个好东西,也许是最好的,好东西是不会消亡的', '城镇中有那么多的酒馆,她却偏偏走进了我的酒馆', '那时候我只会想自己想要什么,从不想自己拥有什么' ] const logos = [ 'https://gw.alipayobjects.com/zos/rmsportal/WdGqmHpayyMjiEhcKoVE.png', 'https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png', 'https://gw.alipayobjects.com/zos/rmsportal/dURIMkkrRFpPgTuzkwnB.png', 'https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png', 'https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png' ] const admins = ['ICZER', 'JACK', 'LUIS', 'DAVID'] const groups = ['高逼格设计天团', '中二少女团', '科学搬砖组', '骗你学计算机', '程序员日常'] const users = [ { name: '曲丽丽', avatar: avatars[0], groupId: 0 }, { name: '付晓晓', avatar: avatars[1], groupId: 0 }, { name: '林东东', avatar: avatars[2], groupId: 1 }, { name: '周星星', avatar: avatars[3], groupId: 2 }, { name: '朱偏右', avatar: avatars[4], groupId: 3 }, { name: '勒个', avatar: avatars[5], groupId: 4 } ] const teams = groups.map((item, index) => { return { name: item, avatar: avatars[index] } }) export {logos, sayings, positions, avatars, admins, groups, users, teams} ================================================ FILE: front/src/mock/common/tableData.js ================================================ const operation1 = [ { key: 'op1', type: '订购关系生效', name: '曲丽丽', status: 'agree', updatedAt: '2017-10-03 19:23:12', memo: '-' }, { key: 'op2', type: '财务复审', name: '付小小', status: 'reject', updatedAt: '2017-10-03 19:23:12', memo: '不通过原因' }, { key: 'op3', type: '部门初审', name: '周毛毛', status: 'agree', updatedAt: '2017-10-03 19:23:12', memo: '-' }, { key: 'op4', type: '提交订单', name: '林东东', status: 'agree', updatedAt: '2017-10-03 19:23:12', memo: '很棒' }, { key: 'op5', type: '创建订单', name: '汗牙牙', status: 'agree', updatedAt: '2017-10-03 19:23:12', memo: '-' } ] const operation2 = [ { key: 'op2', type: '财务复审', name: '付小小', status: 'reject', updatedAt: '2017-10-03 19:23:12', memo: '不通过原因' }, { key: 'op3', type: '部门初审', name: '周毛毛', status: 'agree', updatedAt: '2017-10-03 19:23:12', memo: '-' }, { key: 'op4', type: '提交订单', name: '林东东', status: 'agree', updatedAt: '2017-10-03 19:23:12', memo: '很棒' } ] const operation3 = [ { key: 'op2', type: '财务复审', name: '付小小', status: 'reject', updatedAt: '2017-10-03 19:23:12', memo: '不通过原因' }, { key: 'op3', type: '部门初审', name: '周毛毛', status: 'agree', updatedAt: '2017-10-03 19:23:12', memo: '-' } ] const operationColumns = [ { title: '操作类型', dataIndex: 'type', key: 'type' }, { title: '操作人', dataIndex: 'name', key: 'name' }, { title: '执行结果', dataIndex: 'status', key: 'status' }, { title: '操作时间', dataIndex: 'updatedAt', key: 'updatedAt' }, { title: '备注', dataIndex: 'memo', key: 'memo' } ] export {operation1, operation2, operation3, operationColumns} ================================================ FILE: front/src/mock/goods/index.js ================================================ import Mock from 'mockjs' import '@/mock/extend' import {parseUrlParams} from '@/utils/request' const current = new Date().getTime() const goodsList = Mock.mock({ 'list|200': [{ 'id|+1': 0, 'name': '@GOODS', 'orderId': `${current}-@integer(1,100)`, 'status|1-4': 1, 'send': '@BOOLEAN', 'sendTime': '@DATETIME', 'orderDate': '@DATE', 'auditTime': '@TIME' }] }) Mock.mock(RegExp(`${process.env.VUE_APP_API_BASE_URL}/goods` + '.*'),'get', ({url}) => { const params = parseUrlParams(decodeURI(url)) let {page, pageSize} = params page = eval(page) - 1 || 0 pageSize = eval(pageSize) || 10 delete params.page delete params.pageSize let result = goodsList.list.filter(item => { for (let [key, value] of Object.entries(params)) { if (item[key] != value) { return false } } return true }) const total = result.length if ((page) * pageSize > total) { result = [] } else { result = result.slice(page * pageSize, (page + 1) * pageSize) } return { code: 0, message: 'success', data: { page: page + 1, pageSize, total, list: result } } }) const columnsConfig = [ { title: '商品名称', dataIndex: 'name', searchAble: true }, { title: '订单号', dataIndex: 'orderId' }, { searchAble: true, dataIndex: 'status', dataType: 'select', slots: {title: 'statusTitle'}, scopedSlots: {customRender: 'status'}, search: { selectOptions: [ {title: '已下单', value: 1}, {title: '已付款', value: 2}, {title: '已审核', value: 3}, // {title: '已发货', value: 4} ] } }, { title: '发货', searchAble: true, dataIndex: 'send', dataType: 'boolean', scopedSlots: {customRender: 'send'} }, { title: '发货时间', dataIndex: 'sendTime', dataType: 'datetime' }, { title: '下单日期', searchAble: true, dataIndex: 'orderDate', dataType: 'date', visible: false }, { title: '审核时间', dataIndex: 'auditTime', dataType: 'time', }, ] Mock.mock(`${process.env.VUE_APP_API_BASE_URL}/columns`, 'get', () => { return columnsConfig }) ================================================ FILE: front/src/mock/index.js ================================================ import Mock from 'mockjs' import '@/mock/workplace' // 设置全局延时 Mock.setup({ timeout: '200-400' }) ================================================ FILE: front/src/mock/user/login.js ================================================ import Mock from 'mockjs' import '@/mock/extend' const user = Mock.mock({ name: '@ADMIN', avatar: '@AVATAR', address: '@CITY', position: '@POSITION' }) Mock.mock(`${process.env.VUE_APP_API_BASE_URL}/login`, 'post', ({body}) => { let result = {data: {}} const {name, password} = JSON.parse(body) let success = false if (name === 'admin' && password === '888888') { success = true result.data.permissions = [{id: 'queryForm', operation: ['add', 'edit']}] result.data.roles = [{id: 'admin', operation: ['add', 'edit', 'delete']}] } else if (name === 'user' || password === '888888') { success = true result.data.permissions = [{id: 'queryForm', operation: ['add', 'edit']}] result.data.roles = [{id: 'test', operation: ['add', 'edit', 'delete']}] } else { success = false } if (success) { result.code = 0 result.message = Mock.mock('@TIMEFIX').CN + ',欢迎回到这里' result.data.user = user result.data.token = 'Authorization:' + Math.random() result.data.expireAt = new Date(new Date().getTime() + 30 * 60 * 1000) } else { result.code = -1 result.message = '账户名或密码错误(admin/888888 or test/888888)' } return result }) ================================================ FILE: front/src/mock/user/routes.js ================================================ import Mock from 'mockjs' Mock.mock(`${process.env.VUE_APP_API_BASE_URL}/routes`, 'get', () => { let result = {} result.code = 0 result.data = [{ router: 'root', children: [ { router: 'dashboard', children: ['workplace', 'analysis'], }, { router: 'form', children: ['basicForm', 'stepForm', 'advanceForm'] }, { router: 'basicForm', name: '验权表单', icon: 'file-excel', authority: 'queryForm' }, { router: 'antdv', path: 'antdv', name: 'Ant Design Vue', icon: 'ant-design', link: 'https://www.antdv.com/docs/vue/introduce-cn/' }, { router: 'document', path: 'document', name: '使用文档', icon: 'file-word', link: 'https://iczer.gitee.io/vue-antd-admin-docs/' } ] }] return result }) ================================================ FILE: front/src/mock/workplace/index.js ================================================ import Mock from 'mockjs' import {activities, templates} from '@/mock/common/activityData' import {teams} from '@/mock/common' activities.forEach(item => { item.template = templates[item.activity.type](item.user, item.activity) }) Mock.mock('/work/activity', 'get', () => { return activities }) Mock.mock('/work/team', 'get', () => { return teams }) ================================================ FILE: front/src/pages/analysis/index.vue ================================================ ================================================ FILE: front/src/pages/components/Palette.vue ================================================ ================================================ FILE: front/src/pages/components/TaskCard.vue ================================================ ================================================ FILE: front/src/pages/components/table/Api.vue ================================================ ================================================ FILE: front/src/pages/components/table/Table.vue ================================================ ================================================ FILE: front/src/pages/components/table/index.js ================================================ import Table from './Table' export default Table ================================================ FILE: front/src/pages/customer/followHistory.vue ================================================ ================================================ FILE: front/src/pages/customer/handoverHistory.vue ================================================ ================================================ FILE: front/src/pages/customer/manager.vue ================================================ ================================================ FILE: front/src/pages/customer/official.vue ================================================ ================================================ FILE: front/src/pages/customer/resource.vue ================================================ ================================================ FILE: front/src/pages/dashboard/workplace/WorkPlace.vue ================================================ ================================================ FILE: front/src/pages/dashboard/workplace/i18n.js ================================================ module.exports = { messages: { CN: { project: '项目数', ranking: '团队排名', visit: '项目访问', progress: '进行中的项目', all: '全部项目', access: '快速开始/便捷导航', dynamic: '动态', degree: '指数', team: '团队', add: '添加' }, HK: { project: '項目數', ranking: '團隊排名', visit: '項目訪問', progress: '進行中的項目', all: '全部項目', access: '快速開始/便捷導航', dynamic: '動態', degree: '指數', team: '團隊', add: '添加' }, US: { project: 'Project', ranking: 'Ranking', visit: 'Visit', progress: 'Projects in progress', all: 'All projects', access: 'Quick start / Easy navigation', dynamic: 'Dynamic', degree: 'degree', team: 'Team', add: 'Add' }, } } ================================================ FILE: front/src/pages/dashboard/workplace/index.js ================================================ import WorkPlace from './WorkPlace' export default WorkPlace ================================================ FILE: front/src/pages/dashboard/workplace/index.less ================================================ .project-list { .card-title { span{ vertical-align: middle; &:last-child{ margin-left: 12px; } } } .project-item { display: flex; justify-content: space-between; margin-top: 8px; overflow: hidden; font-size: 12px; color: inherit; .group{ color: @text-color; flex: 1 1 0; &:hover { color: @primary-color; } } .datetime { color: @text-color-second; flex: 0 0 auto; } } .ant-card-meta-description { height: 44px; line-height: 22px; overflow: hidden; } } .item-group{ padding: 20px 0 8px 24px; font-size: 0; a{ color: inherit; display: inline-block; font-size: 14px; margin-bottom: 13px; width: 25%; } } .members { a { display: block; margin: 12px 0; color: @text-color; &:hover { color: @primary-color; } .member { vertical-align: middle; margin-left: 12px; } } } ================================================ FILE: front/src/pages/department/index.vue ================================================ ================================================ FILE: front/src/pages/dictionary/contents.vue ================================================ ================================================ FILE: front/src/pages/dictionary/details.vue ================================================ ================================================ FILE: front/src/pages/employee/index.vue ================================================ ================================================ FILE: front/src/pages/exception/403.vue ================================================ ================================================ FILE: front/src/pages/exception/404.vue ================================================ ================================================ FILE: front/src/pages/exception/500.vue ================================================ ================================================ FILE: front/src/pages/login/Login.vue ================================================ ================================================ FILE: front/src/pages/login/index.js ================================================ import Login from './Login' export default Login ================================================ FILE: front/src/pages/permission/index.vue ================================================ ================================================ FILE: front/src/pages/result/Error.vue ================================================ ================================================ FILE: front/src/pages/result/Success.vue ================================================ ================================================ FILE: front/src/pages/role/index.vue ================================================ ================================================ FILE: front/src/plugins/authority-plugin.js ================================================ /** * 获取路由需要的权限 * @param permissions * @param route * @returns {Permission} */ const getRoutePermission = (permissions, route) => permissions.find(item => item.id === route.meta.authority.permission) /** * 获取路由需要的角色 * @param roles * @param route * @returns {Array[Role]} */ const getRouteRole = (roles, route) => { const requiredRoles = route.meta.authority.role return requiredRoles ? roles.filter(item => requiredRoles.findIndex(required => required === item.id) !== -1) : [] } /** * 判断是否已为方法注入权限认证 * @param method * @returns {boolean} */ const hasInjected = (method) => method.toString().indexOf('//--auth-inject') !== -1 /** * 操作权限校验 * @param authConfig * @param permission * @param role * @param permissions * @param roles * @returns {boolean} */ const auth = function(authConfig, permission, role, permissions, roles) { const {check, type} = authConfig if (check && typeof check === 'function') { return check.apply(this, [permission, role, permissions, roles]) } if (type === 'permission') { return checkFromPermission(check, permission) } else if (type === 'role') { return checkFromRoles(check, role) } else { return checkFromPermission(check, permission) || checkFromRoles(check, role) } } /** * 检查权限是否有操作权限 * @param check 需要检查的操作权限 * @param permission 权限 * @returns {boolean} */ const checkFromPermission = function(check, permission) { return permission && permission.operation && permission.operation.indexOf(check) !== -1 } /** * 检查 roles 是否有操作权限 * @param check 需要检查的操作权限 * @param roles 角色数组 * @returns {boolean} */ const checkFromRoles = function(check, roles) { if (!roles) { return false } for (let role of roles) { const {operation} = role if (operation && operation.indexOf(check) !== -1) { return true } } return false } const checkInject = function (el, binding,vnode) { const type = binding.arg const check = binding.value const instance = vnode.context const $auth = instance.$auth if (!$auth || !$auth(check, type)) { addDisabled(el) } else { removeDisabled(el) } } const addDisabled = function (el) { if (el.tagName === 'BUTTON') { el.disabled = true } else { el.classList.add('disabled') } el.setAttribute('title', '无此权限') } const removeDisabled = function (el) { el.disabled = false el.classList.remove('disabled') el.removeAttribute('title') } const AuthorityPlugin = { install(Vue) { Vue.directive('auth', { bind(el, binding,vnode) { setTimeout(() => checkInject(el, binding, vnode), 10) }, componentUpdated(el, binding,vnode) { setTimeout(() => checkInject(el, binding, vnode), 10) }, unbind(el) { removeDisabled(el) } }) Vue.mixin({ beforeCreate() { if (this.$options.authorize) { const authorize = this.$options.authorize Object.keys(authorize).forEach(key => { if (this.$options.methods[key]) { const method = this.$options.methods[key] if (!hasInjected(method)) { let authConfig = authorize[key] authConfig = (typeof authConfig === 'string') ? {check: authConfig} : authConfig const {check, type, onFailure} = authConfig this.$options.methods[key] = function () { //--auth-inject if (this.$auth(check, type)) { return method.apply(this, arguments) } else { if (onFailure && typeof onFailure === 'function') { this[`$${check}Failure`] = onFailure return this[`$${check}Failure`](check) } else { this.$message.error(`对不起,您没有操作权限:${check}`) } return 0 } } } } }) } }, methods: { /** * 操作权限校验 * @param check 需要校验的操作名 * @param type 校验类型,通过 permission 校验,还是通过 role 校验。 * 如未设置,则自动识别,如匹配到当前路由 permission 则 type = permission,否则 type = role * @returns {boolean} 是否校验通过 */ $auth(check, type) { const permissions = this.$store.getters['account/permissions'] const roles = this.$store.getters['account/roles'] const permission = getRoutePermission(permissions, this.$route) const role = getRouteRole(roles, this.$route) return auth.apply(this, [{check, type}, permission, role, permissions, roles]) } } }) } } export default AuthorityPlugin ================================================ FILE: front/src/plugins/i18n-extend.js ================================================ // 语句模式 const MODE = { STATEMENTS: 's', //语句模式 PHRASAL: 'p', //词组模式 } const VueI18nPlugin = { install: function (Vue) { Vue.mixin({ methods: { $ta(syntaxKey, mode) { let _mode = mode || MODE.STATEMENTS let keys = syntaxKey.split('|') let _this = this let locale = this.$i18n.locale let message = '' let splitter = locale == 'US' ? ' ' : '' // 拼接 message keys.forEach(key => { message += _this.$t(key) + splitter }) // 英文环境语句模式下,转换单词大小写 if (keys.length > 0 && _mode == MODE.STATEMENTS && locale == 'US') { message = message.charAt(0).toUpperCase() + message.toLowerCase().substring(1) } return message } } }) } } export default VueI18nPlugin ================================================ FILE: front/src/plugins/index.js ================================================ import VueI18nPlugin from './i18n-extend' import AuthorityPlugin from './authority-plugin' import TabsPagePlugin from './tabs-page-plugin' const Plugins = { install: function (Vue) { Vue.use(VueI18nPlugin) Vue.use(AuthorityPlugin) Vue.use(TabsPagePlugin) } } export default Plugins ================================================ FILE: front/src/plugins/tabs-page-plugin.js ================================================ const TabsPagePlugin = { install(Vue) { Vue.mixin({ methods: { $closePage(closeRoute, nextRoute) { const event = new CustomEvent('page:close', {detail:{closeRoute, nextRoute}}) window.dispatchEvent(event) }, $refreshPage(route) { const path = typeof route === 'object' ? route.path : route const event = new CustomEvent('page:refresh', {detail:{pageKey: path}}) window.dispatchEvent(event) }, $openPage(route, title) { this.$setPageTitle(route, title) this.$router.push(route) }, $setPageTitle(route, title) { if (title) { let path = typeof route === 'object' ? route.path : route path = path && path.split('?')[0] this.$store.commit('setting/setCustomTitle', {path, title}) } } }, computed: { customTitle() { const customTitles = this.$store.state.setting.customTitles const path = this.$route.path.split('?')[0] const custom = customTitles.find(item => item.path === path) return custom && custom.title } } }) } } export default TabsPagePlugin ================================================ FILE: front/src/router/async/config.async.js ================================================ import routerMap from './router.map' import {parseRoutes} from '@/utils/routerUtil' // 异步路由配置 const routesConfig = [ 'login', 'root', { router: 'exp404', path: '*', name: '404' }, { router: 'exp403', path: '/403', name: '403' } ] const options = { routes: parseRoutes(routesConfig, routerMap) } export default options ================================================ FILE: front/src/router/async/router.map.js ================================================ // 视图组件 const view = { tabs: () => import('@/layouts/tabs'), blank: () => import('@/layouts/BlankView'), page: () => import('@/layouts/PageView') } // 路由组件注册 const routerMap = { login: { authority: '*', path: '/login', component: () => import('@/pages/login') }, root: { path: '/', name: '首页', redirect: '/login', component: view.tabs }, dashboard: { name: 'Dashboard', component: view.blank }, workplace: { name: '工作台', component: () => import('@/pages/dashboard/workplace') }, result: { name: '结果页', icon: 'check-circle-o', component: view.page }, success: { name: '成功', component: () => import('@/pages/result/Success') }, error: { name: '失败', component: () => import('@/pages/result/Error') }, exception: { name: '异常页', icon: 'warning', component: view.blank }, exp403: { authority: '*', name: 'exp403', path: '403', component: () => import('@/pages/exception/403') }, exp404: { name: 'exp404', path: '404', component: () => import('@/pages/exception/404') }, exp500: { name: 'exp500', path: '500', component: () => import('@/pages/exception/500') }, components: { name: '小组件', icon: 'appstore-o', component: view.page }, taskCard: { name: '任务卡片', component: () => import('@/pages/components/TaskCard') }, palette: { name: '颜色复选框', component: () => import('@/pages/components/Palette') } } export default routerMap ================================================ FILE: front/src/router/config.js ================================================ import TabsView from '@/layouts/tabs/TabsView' import PageView from '@/layouts/PageView' // 路由配置 const options = { routes: [ { path: '/login', name: '登录页', component: () => import('@/pages/login') }, { path: '*', name: '404', component: () => import('@/pages/exception/404'), }, { path: '/403', name: '403', component: () => import('@/pages/exception/403'), }, { path: '/', name: '首页', component: TabsView, redirect: '/login', children: [ { path: 'dashboard/workplace', name: 'Dashboard', meta: { icon: 'dashboard' }, component: () => import('@/pages/dashboard/workplace') }, { path: '/system', name: '系统设置', meta: { icon: 'setting', page: { cacheAble: false } }, component: PageView, children: [ { path:'role', name:'角色管理', component:()=>import('@/pages/role/index') }, { path:'permission', name:'权限管理', component:()=>import('@/pages/permission/index') }, { path:'department', name:'部门管理', component:()=>import('@/pages/department/index') }, { path:'employee', name:'员工管理', component:()=>import('@/pages/employee/index') }, { path: 'dictionary/contents', name: '字典列表', component:()=>import('@/pages/dictionary/contents') }, { path: 'dictionary/details', name: '字典明细', component:()=>import('@/pages/dictionary/details') } ] }, { path: '/customer', name: '客户管理', meta: { icon: 'team', page: { cacheAble: false } }, component: PageView, children: [ { path:'manager', name:'潜在客户管理', component:()=>import('@/pages/customer/manager') }, { path:'official', name:'正式客户管理', component:()=>import('@/pages/customer/official') }, { path:'resource', name:'客户资源池', component:()=>import('@/pages/customer/resource') }, { path:'followHistory', name:'跟进历史', component:()=>import('@/pages/customer/followHistory') }, { path:'handoverHistory', name:'移交历史查询', component:()=>import('@/pages/customer/handoverHistory') }, ] }, { path: '/analysis', name: '统计分析', meta: { icon: 'monitor', page: { cacheAble: false } }, component: ()=>import('@/pages/analysis/index'), }, { name: '关于创作者', path: 'antdv', meta: { icon: 'ant-design', link: 'https://msy.plus' } } ] }, ] } export default options ================================================ FILE: front/src/router/guards.js ================================================ import {hasAuthority} from '@/utils/authority-utils' import {loginIgnore} from '@/router/index' import {checkAuthorization} from '@/utils/request' import NProgress from 'nprogress' NProgress.configure({ showSpinner: false }) /** * 进度条开始 * @param to * @param form * @param next */ const progressStart = (to, from, next) => { // start progress bar if (!NProgress.isStarted()) { NProgress.start() } next() } /** * 登录守卫 * @param to * @param form * @param next * @param options */ const loginGuard = (to, from, next, options) => { const {message} = options if (!loginIgnore.includes(to) && !checkAuthorization()) { message.warning('登录已失效,请重新登录') next({path: '/login'}) } else { next() } } /** * 权限守卫 * @param to * @param form * @param next * @param options */ const authorityGuard = (to, from, next, options) => { const {store, message} = options const permissions = store.getters['account/permissions'] const roles = store.getters['account/roles'] if (!hasAuthority(to, permissions, roles)) { message.warning(`对不起,您无权访问页面: ${to.fullPath},请联系管理员`) next({path: '/403'}) // NProgress.done() } else { next() } } /** * 混合导航模式下一级菜单跳转重定向 * @param to * @param from * @param next * @param options * @returns {*} */ const redirectGuard = (to, from, next, options) => { const {store} = options const getFirstChild = (routes) => { const route = routes[0] if (!route.children || route.children.length === 0) { return route } return getFirstChild(route.children) } if (store.state.setting.layout === 'mix') { const firstMenu = store.getters['setting/firstMenu'] if (firstMenu.find(item => item.fullPath === to.fullPath)) { store.commit('setting/setActivatedFirst', to.fullPath) const subMenu = store.getters['setting/subMenu'] if (subMenu.length > 0) { const redirect = getFirstChild(subMenu) return next({path: redirect.fullPath}) } } } next() } /** * 进度条结束 * @param to * @param form * @param options */ const progressDone = () => { // finish progress bar NProgress.done() } export default { beforeEach: [progressStart, loginGuard, authorityGuard, redirectGuard], afterEach: [progressDone] } ================================================ FILE: front/src/router/i18n.js ================================================ module.exports = { messages: { CN: { home: {name: '首页'}, }, US: { home: {name: 'home'}, }, HK: { home: {name: '首頁'}, dashboard: { name: 'Dashboard', workplace: {name: '工作台'}, analysis: {name: '分析頁'} }, form: { name: '表單頁', basic: {name: '基礎表單'}, step: {name: '分步表單'}, advance: {name: '分步表單'} }, list: { name: '列表頁', query: {name: '查詢表格'}, primary: {name: '標準列表'}, card: {name: '卡片列表'}, search: { name: '搜索列表', article: {name: '文章'}, application: {name: '應用'}, project: {name: '項目'} } }, details: { name: '詳情頁', basic: {name: '基礎詳情頁'}, advance: {name: '高級詳情頁'} }, result: { name: '結果頁', success: {name: '成功'}, error: {name: '失敗'} }, exception: { name: '異常頁', 404: {name: '404'}, 403: {name: '403'}, 500: {name: '500'} }, components: { name: '小組件', taskCard: {name: '任務卡片'}, palette: {name: '顏色複選框'} } } } } ================================================ FILE: front/src/router/index.js ================================================ import Vue from 'vue' import Router from 'vue-router' import {formatRoutes} from '@/utils/routerUtil' Vue.use(Router) // 不需要登录拦截的路由配置 const loginIgnore = { names: ['404', '403'], //根据路由名称匹配 paths: ['/login'], //根据路由fullPath匹配 /** * 判断路由是否包含在该配置中 * @param route vue-router 的 route 对象 * @returns {boolean} */ includes(route) { return this.names.includes(route.name) || this.paths.includes(route.path) } } /** * 初始化路由实例 * @param isAsync 是否异步路由模式 * @returns {VueRouter} */ function initRouter(isAsync) { const options = isAsync ? require('./async/config.async').default : require('./config').default formatRoutes(options.routes) return new Router(options) } export {loginIgnore, initRouter} ================================================ FILE: front/src/services/analysis.js ================================================ import {METHOD, request} from '@/utils/request' import {ANALYSIS} from './api' export async function list(params) { return request(ANALYSIS, METHOD.POST, params) } ================================================ FILE: front/src/services/api.js ================================================ //跨域代理前缀 // const API_PROXY_PREFIX='/api' const BASE_URL = process.env.NODE_ENV === 'production' ? '/spring-boot-api-seeding' : process.env.VUE_APP_API_BASE_URL // const BASE_URL = process.env.VUE_APP_API_BASE_URL // const BASE_URL = process.env.VUE_APP_API_DEV_URL module.exports = { BASE_URL, // LOGIN: `${BASE_URL}/login`, LOGIN: `${BASE_URL}/account/token`, // ROUTES: `${BASE_URL}/routes`, GOODS: `${BASE_URL}/goods`, GOODS_COLUMNS: `${BASE_URL}/columns`, DEPARTMENT:`${BASE_URL}/department`, // method CRUD ROLE:`${BASE_URL}/role`, // method CRUD PERMISSION:`${BASE_URL}/permission`, // method CRUD EMPLOYEE:`${BASE_URL}/employee`, // method CRUD DICTIONARY_CONTENTS:`${BASE_URL}/dictionary/contents`, // method CRUD DICTIONARY_DETAILS:`${BASE_URL}/dictionary/details`, // method CRUD CUSTOMER_MANAGER:`${BASE_URL}/customer/manager`, // method CRUD CUSTOMER_HANDOVER:`${BASE_URL}/customer/handover`, // method CRUD CUSTOMER_FOLLOW_UP_HISTORY:`${BASE_URL}/customer/follow/up/history`, // method CRUD ANALYSIS:`${BASE_URL}/analysis`, // method CRUD } ================================================ FILE: front/src/services/customerFollowUpHistory.js ================================================ import {request, METHOD} from '@/utils/request' import {CUSTOMER_FOLLOW_UP_HISTORY} from './api' /** * 获取员工管理明细信息,分页获取 * @param page {{size: number, page: number}} * @returns {Promise>} */ export async function list(page ) { return request(CUSTOMER_FOLLOW_UP_HISTORY, METHOD.GET, page||{"page": 1, "size": 10}) } /** * 删除一个员工管理明细信息 * @param id {string|number} * @returns {Promise>} */ export async function deleteItem(id) { return request(CUSTOMER_FOLLOW_UP_HISTORY + '/' + id, METHOD.DELETE) } /** * 获取单个员工管理明细的详细信息 * @param id * @returns {Promise>} */ export async function getDetail(id) { return request(CUSTOMER_FOLLOW_UP_HISTORY + '/' + id, METHOD.GET) } /** * 修改员工管理明细信息 * @param object {{ "comment": "string", "customerid": 0, "inputuser": 0, "id":0, "tracedetails": "string", "traceresult": 0, "tracetime": "2021-05-21T12:29:28.902Z", "tracetype": 0, "type": 0 }} * @returns {Promise>} */ export async function update(object) { return request(CUSTOMER_FOLLOW_UP_HISTORY, METHOD.PUT, object) } /** * 添加员工管理明细信息 * @param object {{ "comment": "string", "customerid": 0, "inputuser": 0, "tracedetails": "string", "traceresult": 0, "tracetime": "2021-05-21T12:29:28.902Z", "tracetype": 0, "type": 0 }} * @returns {Promise>} */ export async function add(object) { return request(CUSTOMER_FOLLOW_UP_HISTORY, METHOD.POST, object) } export const type ={ 1:"客户跟进历史", 0:"潜在开发计划" } export const traceresult={ 3:"优", 2:"中", 1:"差" } ================================================ FILE: front/src/services/customerHandover.js ================================================ import {request, METHOD} from '@/utils/request' import {CUSTOMER_HANDOVER} from './api' /** * 获取员工管理明细信息,分页获取 * @param page {{size: number, page: number}} * @returns {Promise>} */ export async function list(page ) { return request(CUSTOMER_HANDOVER, METHOD.GET, page||{"page": 1, "size": 10}) } /** * 删除一个员工管理明细信息 * @param id {string|number} * @returns {Promise>} */ export async function deleteItem(id) { return request(CUSTOMER_HANDOVER + '/' + id, METHOD.DELETE) } /** * 获取单个员工管理明细的详细信息 * @param id * @returns {Promise>} */ export async function getDetail(id) { return request(CUSTOMER_HANDOVER + '/' + id, METHOD.GET) } /** * 修改员工管理明细信息 * @param object {{ "customerid": 0, "newseller": 0, "oldseller": 0, "transreason": "string", "transtime": "2021-05-21T06:33:22.300Z", "transuser": 0 }} * @returns {Promise>} */ export async function update(object) { return request(CUSTOMER_HANDOVER, METHOD.PUT, object) } /** * 添加员工管理明细信息 * @param object {{ "customerid": 0, "id": 0, "newseller": 0, "oldseller": 0, "transreason": "string", "transtime": "2021-05-21T06:33:22.300Z", "transuser": 0 }} * @returns {Promise>} */ export async function add(object) { return request(CUSTOMER_HANDOVER, METHOD.POST, object) } ================================================ FILE: front/src/services/customerManager.js ================================================ import {request, METHOD} from '@/utils/request' import {CUSTOMER_MANAGER} from './api' /** * 获取员工管理明细信息,分页获取 * @param page {{size: number, page: number}} * @returns {Promise>} */ export async function list(page ) { return request(CUSTOMER_MANAGER, METHOD.GET, page||{"page": 1, "size": 10}) } /** * 删除一个员工管理明细信息 * @param id {string|number} * @returns {Promise>} */ export async function deleteItem(id) { return request(CUSTOMER_MANAGER + '/' + id, METHOD.DELETE) } /** * 获取单个员工管理明细的详细信息 * @param id * @returns {Promise>} */ export async function getDetail(id) { return request(CUSTOMER_MANAGER + '/' + id, METHOD.GET) } /** * 修改员工管理明细信息 * @param object {{ "age": 18, "gender": 1, "id": 1, "inputtime": 1621539029000, "inputuser": 1, "job": 3, "name": "马云", "positivetime": 1621539062000, "qq": "100001", "seller": 1, "source": 17, "status": 1, "tel": "18888888888" }} * @returns {Promise>} */ export async function update(object) { return request(CUSTOMER_MANAGER, METHOD.PUT, object) } /** * 添加员工管理明细信息 * @param object {{ "age": 18, "gender": 1, "inputuser": 1, "job": 3, "name": "马云", "positivetime": 1621539062000, "qq": "100001", "seller": 1, "source": 17, "status": 1, "tel": "18888888888" }} * @returns {Promise>} */ export async function add(object) { return request(CUSTOMER_MANAGER, METHOD.POST, object) } export const statusMap = { "-2": "流失", "-1": "开发失败", "0": "潜在客户", "1": "正式客户", "2": "资源池客户", } ================================================ FILE: front/src/services/dataSource.js ================================================ import {GOODS, GOODS_COLUMNS} from './api' import {METHOD, request} from '@/utils/request' export async function goodsList(params) { return request(GOODS, METHOD.GET, params) } export async function goodsColumns() { return request(GOODS_COLUMNS, METHOD.GET) } export default {goodsList, goodsColumns} ================================================ FILE: front/src/services/department.js ================================================ import {request, METHOD} from '@/utils/request' import {DEPARTMENT} from './api' /** * 获取部门信息,分页获取 * @param page {{size: number, page: number}} * @returns {Promise>} */ export async function list(page = { "page": 1, "size": 10 }) { return request(DEPARTMENT, METHOD.GET, page) } /** * 添加部门信息 * @param object {{ "name": "测试部门", "sn": "test department" }} * @returns {Promise>} */ export async function add(object) { return request(DEPARTMENT, METHOD.POST, object) } /** * 删除一个部门信息 * @param id {string|number} * @returns {Promise>} */ export async function deleteItem(id) { return request(DEPARTMENT + '/' + id, METHOD.DELETE) } /** * 获取单个部门的详细信息 * @param id * @returns {Promise>} */ export async function getDetail(id) { return request(DEPARTMENT + '/' + id, METHOD.GET) } /** * * @param object {{ "id":1, "name": "name", "sn": "test department" }} * @returns {Promise>} */ export async function update(object) { return request(DEPARTMENT, METHOD.PATCH, object) } ================================================ FILE: front/src/services/dictionaryContents.js ================================================ import {request, METHOD} from '@/utils/request' import {DICTIONARY_CONTENTS} from './api' /** * 获取字典列表信息,分页获取 * @param page {{size: number, page: number}} * @returns {Promise>} */ export async function list(page ) { return request(DICTIONARY_CONTENTS, METHOD.GET, page||{"page": 1, "size": 10}) } /** * 删除一个字典列表信息 * @param id {string|number} * @returns {Promise>} */ export async function deleteItem(id) { return request(DICTIONARY_CONTENTS + '/' + id, METHOD.DELETE) } /** * 获取单个字典列表的详细信息 * @param id * @returns {Promise>} */ export async function getDetail(id) { return request(DICTIONARY_CONTENTS + '/' + id, METHOD.GET) } /** * 修改字典列表信息 * @returns {Promise>} */ export async function update(object) { return request(DICTIONARY_CONTENTS, METHOD.PUT, object) } /** * 添加字典列表信息 * @returns {Promise>} */ export async function add(object) { return request(DICTIONARY_CONTENTS, METHOD.POST, object) } ================================================ FILE: front/src/services/dictionaryDetails.js ================================================ import {request, METHOD} from '@/utils/request' import {DICTIONARY_DETAILS} from './api' /** * 获取字典明细信息,分页获取 * @param page {{size: number, page: number}} * @returns {Promise>} */ export async function list(page ) { return request(DICTIONARY_DETAILS, METHOD.GET, page||{"page": 1, "size": 10}) } /** * 删除一个字典明细信息 * @param id {string|number} * @returns {Promise>} */ export async function deleteItem(id) { return request(DICTIONARY_DETAILS + '/' + id, METHOD.DELETE) } /** * 获取单个字典明细的详细信息 * @param id * @returns {Promise>} */ export async function getDetail(id) { return request(DICTIONARY_DETAILS + '/' + id, METHOD.GET) } /** * 修改字典明细信息 * @param object {{ "expression": "string", "id": 0, "name": "string" }} * @returns {Promise>} */ export async function update(object) { return request(DICTIONARY_DETAILS, METHOD.PUT, object) } /** * 添加字典明细信息 * @param object {{ "expression": "string", "name": "string" }} * @returns {Promise>} */ export async function add(object) { return request(DICTIONARY_DETAILS, METHOD.POST, object) } ================================================ FILE: front/src/services/employee.js ================================================ import {request, METHOD} from '@/utils/request' import {EMPLOYEE} from './api' /** * 获取角色信息,分页获取 * @param page {{size: number, page: number}} * @returns {Promise>} */ export async function list(page ) { return request(EMPLOYEE, METHOD.GET, page||{"page": 1, "size": 10}) } /** * 添加角色信息 * @param object {{ "admin": 0, "age": 22, "dept": 2, "email": "c@c.c", "hiredate": "2021-05-16T01:09:17.045Z", "name": "testErT3", "password": "string", "roleIds": [4,5,8,9], "state": 0 }} * @returns {Promise>} */ export async function add(object) { return request(EMPLOYEE, METHOD.POST, object) } /** * 删除一个角色信息 * @param id {string|number} * @returns {Promise>} */ export async function deleteItem(id) { return request(EMPLOYEE + '/' + id, METHOD.DELETE) } /** * 获取单个角色的详细信息 * @param id * @returns {Promise>} */ export async function getDetail(id) { return request(EMPLOYEE + '/' + id, METHOD.GET) } /** * 修改角色信息 * @param object {{ "id":1008, "admin": 0, "age": 22, "dept": 2, "email": "c@c.c", "hiredate": "2021-05-16T01:09:17.045Z", "name": "testErT3", "password": "string", "roleIds": [8,9,10,11,12], "state": 0 }} * @returns {Promise>} */ export async function update(object) { return request(EMPLOYEE, METHOD.PUT, object) } ================================================ FILE: front/src/services/index.js ================================================ import userService from './user' import dataSource from './dataSource' export { userService, dataSource } ================================================ FILE: front/src/services/permission.js ================================================ import {request, METHOD} from '@/utils/request' import {PERMISSION} from './api' /** * 获取权限信息,分页获取 * @param page {{size: number, page: number}} * @returns {Promise>} */ export async function list(page ) { return request(PERMISSION, METHOD.GET, page||{"page": 1, "size": 10}) } /** * 删除一个权限信息 * @param id {string|number} * @returns {Promise>} */ export async function deleteItem(id) { return request(PERMISSION + '/' + id, METHOD.DELETE) } /** * 获取单个权限的详细信息 * @param id * @returns {Promise>} */ export async function getDetail(id) { return request(PERMISSION + '/' + id, METHOD.GET) } /** * 修改权限信息 * @param object {{ "expression": "string", "id": 0, "name": "string" }} * @returns {Promise>} */ export async function update(object) { return request(PERMISSION, METHOD.PUT, object) } /** * 添加权限信息 * @param object {{ "expression": "string", "name": "string" }} * @returns {Promise>} */ export async function add(object) { return request(PERMISSION, METHOD.POST, object) } ================================================ FILE: front/src/services/role.js ================================================ import {request, METHOD} from '@/utils/request' import {ROLE} from './api' /** * 获取角色信息,分页获取 * @param page {{size: number, page: number}} * @returns {Promise>} */ export async function list(page ) { return request(ROLE, METHOD.GET, page||{"page": 1, "size": 10}) } /** * 添加角色信息 * @param object {{ "name": "市场经理", "permission": "1", "sn": "Market Manager" }} * @returns {Promise>} */ export async function add(object) { return request(ROLE, METHOD.POST, object) } /** * 删除一个角色信息 * @param id {string|number} * @returns {Promise>} */ export async function deleteItem(id) { return request(ROLE + '/' + id, METHOD.DELETE) } /** * 获取单个角色的详细信息 * @param id * @returns {Promise>} */ export async function getDetail(id) { return request(ROLE + '/' + id, METHOD.GET) } /** * 修改角色信息 * @param object {{ "id": 4, "name": "市场经理", "permission": "1", "sn": "Market Manager" }} * @returns {Promise>} */ export async function update(object) { return request(ROLE, METHOD.PUT, object) } ================================================ FILE: front/src/services/user.js ================================================ import {LOGIN, ROUTES} from '@/services/api' import {request, METHOD, removeAuthorization} from '@/utils/request' /** * 登录服务 * @param name 账户名 * @param password 账户密码 * @returns {Promise>} */ export async function login(name, password) { return request(LOGIN, METHOD.POST, { name: name, password: password }) } /** * 登出服务 * @returns {Promise>} */ export async function logoutRequest() { return request(LOGIN, METHOD.DELETE,) } export async function getRoutesConfig() { return request(ROUTES, METHOD.GET) } /** * 退出登录 使得token失效 */ export function logout() { logoutRequest().then(()=>undefined); localStorage.removeItem(process.env.VUE_APP_ROUTES_KEY) localStorage.removeItem(process.env.VUE_APP_PERMISSIONS_KEY) localStorage.removeItem(process.env.VUE_APP_ROLES_KEY) removeAuthorization() } export default { login, logout, getRoutesConfig } ================================================ FILE: front/src/store/index.js ================================================ import Vue from 'vue' import Vuex from 'vuex' import modules from './modules' Vue.use(Vuex) const store = new Vuex.Store({modules}) export default store ================================================ FILE: front/src/store/modules/account.js ================================================ export default { namespaced: true, state: { user: undefined, permissions: null, roles: null, routesConfig: null }, getters: { user: state => { if (!state.user) { try { const user = localStorage.getItem(process.env.VUE_APP_USER_KEY) state.user = JSON.parse(user) } catch (e) { console.error(e) } } return state.user }, permissions: state => { if (!state.permissions) { try { const permissions = localStorage.getItem(process.env.VUE_APP_PERMISSIONS_KEY) state.permissions = JSON.parse(permissions) state.permissions = state.permissions ? state.permissions : [] } catch (e) { console.error(e.message) } } return state.permissions }, roles: state => { if (!state.roles) { try { const roles = localStorage.getItem(process.env.VUE_APP_ROLES_KEY) state.roles = JSON.parse(roles) state.roles = state.roles ? state.roles : [] } catch (e) { console.error(e.message) } } return state.roles }, routesConfig: state => { if (!state.routesConfig) { try { const routesConfig = localStorage.getItem(process.env.VUE_APP_ROUTES_KEY) state.routesConfig = JSON.parse(routesConfig) state.routesConfig = state.routesConfig ? state.routesConfig : [] } catch (e) { console.error(e.message) } } return state.routesConfig } }, mutations: { setUser (state, user) { state.user = user localStorage.setItem(process.env.VUE_APP_USER_KEY, JSON.stringify(user)) }, setPermissions(state, permissions) { state.permissions = permissions localStorage.setItem(process.env.VUE_APP_PERMISSIONS_KEY, JSON.stringify(permissions)) }, setRoles(state, roles) { state.roles = roles localStorage.setItem(process.env.VUE_APP_ROLES_KEY, JSON.stringify(roles)) }, setRoutesConfig(state, routesConfig) { state.routesConfig = routesConfig localStorage.setItem(process.env.VUE_APP_ROUTES_KEY, JSON.stringify(routesConfig)) } } } ================================================ FILE: front/src/store/modules/index.js ================================================ import account from './account' import setting from './setting' export default {account, setting} ================================================ FILE: front/src/store/modules/setting.js ================================================ import config from '@/config' import {ADMIN} from '@/config/default' import {formatFullPath} from '@/utils/i18n' import {filterMenu} from '@/utils/authority-utils' import {getLocalSetting} from '@/utils/themeUtil' import deepClone from 'lodash.clonedeep' const localSetting = getLocalSetting(true) const customTitlesStr = sessionStorage.getItem(process.env.VUE_APP_TBAS_TITLES_KEY) const customTitles = (customTitlesStr && JSON.parse(customTitlesStr)) || [] export default { namespaced: true, state: { isMobile: false, animates: ADMIN.animates, palettes: ADMIN.palettes, pageMinHeight: 0, menuData: [], activatedFirst: undefined, customTitles, ...config, ...localSetting }, getters: { menuData(state, getters, rootState) { if (state.filterMenu) { const {permissions, roles} = rootState.account return filterMenu(deepClone(state.menuData), permissions, roles) } return state.menuData }, firstMenu(state, getters) { const {menuData} = getters if (menuData.length > 0 && !menuData[0].fullPath) { formatFullPath(menuData) } return menuData.map(item => { const menuItem = {...item} delete menuItem.children return menuItem }) }, subMenu(state) { const {menuData, activatedFirst} = state if (menuData.length > 0 && !menuData[0].fullPath) { formatFullPath(menuData) } const current = menuData.find(menu => menu.fullPath === activatedFirst) return current && current.children || [] } }, mutations: { setDevice (state, isMobile) { state.isMobile = isMobile }, setTheme (state, theme) { state.theme = theme }, setLayout (state, layout) { state.layout = layout }, setMultiPage (state, multiPage) { state.multiPage = multiPage }, setAnimate (state, animate) { state.animate = animate }, setWeekMode(state, weekMode) { state.weekMode = weekMode }, setFixedHeader(state, fixedHeader) { state.fixedHeader = fixedHeader }, setFixedSideBar(state, fixedSideBar) { state.fixedSideBar = fixedSideBar }, setLang(state, lang) { state.lang = lang }, setHideSetting(state, hideSetting) { state.hideSetting = hideSetting }, correctPageMinHeight(state, minHeight) { state.pageMinHeight += minHeight }, setMenuData(state, menuData) { state.menuData = menuData }, setAsyncRoutes(state, asyncRoutes) { state.asyncRoutes = asyncRoutes }, setPageWidth(state, pageWidth) { state.pageWidth = pageWidth }, setActivatedFirst(state, activatedFirst) { state.activatedFirst = activatedFirst }, setFixedTabs(state, fixedTabs) { state.fixedTabs = fixedTabs }, setCustomTitle(state, {path, title}) { if (title) { const obj = state.customTitles.find(item => item.path === path) if (obj) { obj.title = title } else { state.customTitles.push({path, title}) } sessionStorage.setItem(process.env.VUE_APP_TBAS_TITLES_KEY, JSON.stringify(state.customTitles)) } } } } ================================================ FILE: front/src/theme/antd/ant-menu.less ================================================ .ant-menu-inline-collapsed-tooltip a{ color: @text-color-inverse; } ================================================ FILE: front/src/theme/antd/ant-message.less ================================================ .ant-message{ z-index: 1100; } ================================================ FILE: front/src/theme/antd/ant-table.less ================================================ .ant-table-thead{ tr{ th{ &.ant-table-column-has-actions{ &.ant-table-column-has-sorters:hover{ background-color: @background-color-base; } &.ant-table-column-has-filters{ &:hover{ .anticon-filter, .anticon-filter:hover{ background-color: @background-color-base; } } .anticon-filter.ant-table-filter-open{ background-color: @background-color-base; } } } } } } ================================================ FILE: front/src/theme/antd/ant-time-picker.less ================================================ .ant-time-picker-panel-input{ background-color: @component-background; } ================================================ FILE: front/src/theme/antd/index.less ================================================ @import "ant-time-picker"; @import "ant-message"; @import "ant-table"; @import "ant-menu"; ================================================ FILE: front/src/theme/default/color.less ================================================ @import '~ant-design-vue/lib/style/themes/default'; @gray-1: #ffffff; @gray-2: #fafafa; @gray-3: #f5f5f5; @gray-4: #f0f0f0; @gray-5: #d9d9d9; @gray-6: #bfbfbf; @gray-7: #8c8c8c; @gray-8: #595959; @gray-9: #434343; @gray-10: #262626; @gray-11: #1f1f1f; @gray-12: #141414; @gray-13: #000000; @primary-color: @primary-color; @success-color: @success-color; @warning-color: @warning-color; @error-color: @warning-color; @title-color: @heading-color; @text-color: @text-color; @text-color-second: @text-color-secondary; @layout-bg-color: @layout-body-background; @base-bg-color: @body-background; @hover-bg-color: rgba(0, 0, 0, 0.025); @border-color: @border-color-split; @shadow-color: @shadow-color; @text-color-inverse: @text-color-inverse; @hover-bg-color-light: @hover-bg-color; @hover-bg-color-dark: @primary-7; @hover-bg-color-night: rgba(255, 255, 255, 0.025); @header-bg-color-dark: @layout-header-background; @shadow-down: @shadow-1-down; @shadow-up: @shadow-1-up; @shadow-left: @shadow-1-left; @shadow-right: @shadow-1-right; @theme-list: light, dark, night; ================================================ FILE: front/src/theme/default/index.less ================================================ @import "color"; @import "style"; @import "nprogress"; ================================================ FILE: front/src/theme/default/nprogress.less ================================================ @import '~ant-design-vue/lib/style/themes/default'; /* Make clicks pass-through */ #nprogress { pointer-events: none; } #nprogress .bar { background: @primary-color; position: fixed; z-index: 1031; top: 0; left: 0; width: 100%; height: 2px; } /* Fancy blur effect */ #nprogress .peg { display: block; position: absolute; right: 0px; width: 100px; height: 100%; box-shadow: 0 0 10px @primary-color, 0 0 5px @primary-color; opacity: 1.0; -webkit-transform: rotate(3deg) translate(0px, -4px); -ms-transform: rotate(3deg) translate(0px, -4px); transform: rotate(3deg) translate(0px, -4px); } /* Remove these to get rid of the spinner */ #nprogress .spinner { display: block; position: fixed; z-index: 1031; top: 15px; right: 15px; } #nprogress .spinner-icon { width: 18px; height: 18px; box-sizing: border-box; border: solid 2px transparent; border-top-color: @primary-color; border-left-color: @primary-color; border-radius: 50%; -webkit-animation: nprogress-spinner 400ms linear infinite; animation: nprogress-spinner 400ms linear infinite; } .nprogress-custom-parent { overflow: hidden; position: relative; } .nprogress-custom-parent #nprogress .spinner, .nprogress-custom-parent #nprogress .bar { position: absolute; } @-webkit-keyframes nprogress-spinner { 0% { -webkit-transform: rotate(0deg); } 100% { -webkit-transform: rotate(360deg); } } @keyframes nprogress-spinner { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } ================================================ FILE: front/src/theme/default/style.less ================================================ .week-mode{ overflow: hidden; filter: invert(80%); } .beauty-scroll{ scrollbar-color: @primary-color @primary-2; scrollbar-width: thin; -ms-overflow-style:none; position: relative; &::-webkit-scrollbar{ width: 3px; height: 1px; } &::-webkit-scrollbar-thumb { border-radius: 3px; background: @primary-color; } &::-webkit-scrollbar-track { -webkit-box-shadow: inset 0 0 1px rgba(0,0,0,0); border-radius: 3px; background: @primary-3; } } .split-right{ &:not(:last-child) { border-right: 1px solid rgba(98, 98, 98, 0.2); } } .disabled{ cursor: not-allowed; color: @disabled-color; pointer-events: none; } ================================================ FILE: front/src/theme/index.less ================================================ @import '~ant-design-vue/dist/antd.less'; @import "default/index"; @import "antd/index"; ================================================ FILE: front/src/theme/theme.less ================================================ @import "default/index"; ================================================ FILE: front/src/utils/Objects.js ================================================ /** * 给对象注入属性 * @param keys 属性key数组, 如 keys = ['config', 'path'] , 则会给对象注入 object.config.path 的属性 * @param value 属性值 * @returns {Object} */ Object.defineProperty(Object.prototype, 'assignProps', { writable: false, enumerable: false, configurable: true, value: function (keys, value) { let props = this for (let i = 0; i < keys.length; i++) { let key = keys[i] if (i == keys.length - 1) { props[key] = value } else { props[key] = props[key] == undefined ? {} : props[key] props = props[key] } } return this } }) ================================================ FILE: front/src/utils/authority-utils.js ================================================ /** * 判断是否有路由的权限 * @param authority 路由权限配置 * @param permissions 用户权限集合 * @returns {boolean|*} */ function hasPermission(authority, permissions) { let required = '*' if (typeof authority === 'string') { required = authority } else if (typeof authority === 'object') { required = authority.permission } return required === '*' || (permissions && permissions.findIndex(item => item === required || item.id === required) !== -1) } /** * 判断是否有路由需要的角色 * @param authority 路由权限配置 * @param roles 用户角色集合 */ function hasRole(authority, roles) { let required = undefined if (typeof authority === 'object') { required = authority.role } return authority === '*' || hasAnyRole(required, roles) } /** * 判断是否有需要的任意一个角色 * @param required {String | Array[String]} 需要的角色,可以是单个角色或者一个角色数组 * @param roles 拥有的角色 * @returns {boolean} */ function hasAnyRole(required, roles) { if (!required) { return false } else if(Array.isArray(required)) { return roles.findIndex(role => { return required.findIndex(item => item === role || item === role.id) !== -1 }) !== -1 } else { return roles.findIndex(role => role === required || role.id === required) !== -1 } } /** * 路由权限校验 * @param route 路由 * @param permissions 用户权限集合 * @param roles 用户角色集合 * @returns {boolean} */ function hasAuthority(route, permissions, roles) { const authorities = [...route.meta.pAuthorities, route.meta.authority] for (let authority of authorities) { if (!hasPermission(authority, permissions) && !hasRole(authority, roles)) { return false } } return true } /** * 根据权限配置过滤菜单数据 * @param menuData * @param permissions * @param roles */ function filterMenu(menuData, permissions, roles) { return menuData.filter(menu => { if (menu.meta && menu.meta.invisible === undefined) { if (!hasAuthority(menu, permissions, roles)) { return false } } if (menu.children && menu.children.length > 0) { menu.children = filterMenu(menu.children, permissions, roles) } return true }) } export {filterMenu, hasAuthority} ================================================ FILE: front/src/utils/axios-interceptors.js ================================================ import Cookie from 'js-cookie' // import {LOGIN} from "@/services/api"; // 401拦截 // const resp401 = { // /** // * 响应数据之前做点什么 // * @param response 响应对象 // * @param options 应用配置 包含: {router, i18n, store, message} // * @returns {*} // */ // onFulfilled(response, options) { // const {message} = options // if (response.code === 401) { // message.error('无此权限') // } // return response // }, // /** // * 响应出错时执行 // * @param error 错误对象 // * @param options 应用配置 包含: {router, i18n, store, message} // * @returns {Promise} // */ // onRejected(error, options) { // const {message} = options // const {response} = error // if (response.status === 401) { // message.error('无此权限') // } // return Promise.reject(error) // } // } // const resp403 = { onFulfilled(response, options) { const {message} = options if (response.code === 1000) { message.error('没有权限访问!') } return response }, onRejected(error, options) { const {message} = options const {response} = error if (response.status === 403) { message.error('请求被拒绝!没有权限访问!') } return Promise.reject(error) } } const resp500 = { onFulfilled(response, options) { const {message} = options if (response.code === 500) { message.error('服务器错误') } return response }, onRejected(error, options) { const {message} = options const {response} = error if (response.status === 500) { message.error('服务器错误') } return Promise.reject(error) } } const respA = { onFulfilled(response, options) { const {message} = options if (response.code === 500) { message.error('服务器错误') } return response }, onRejected(error, options) { const {message} = options const {response} = error if (response.status === 500) { message.error('服务器错误') } return Promise.reject(error) } } const reqCommon = { /** * 发送请求之前做些什么 * @param config axios config * @param options 应用配置 包含: {router, i18n, store, message} * @returns {*} */ onFulfilled(config, options) { const {message} = options const { xsrfCookieName} = config if (options.router.currentRoute.fullPath !== '/login' && xsrfCookieName && !Cookie.get(xsrfCookieName)) { message.warning('认证 token 已过期,请重新登录') options.router.push('/login') } return config }, /** * 请求出错时做点什么 * @param error 错误对象 * @param options 应用配置 包含: {router, i18n, store, message} * @returns {Promise} */ onRejected(error, options) { const {message} = options message.error(error.message) return Promise.reject(error) } } export default { request: [reqCommon], // 请求拦截 response: [resp500,resp403,respA] // 响应拦截 } ================================================ FILE: front/src/utils/colors.js ================================================ const varyColor = require('webpack-theme-color-replacer/client/varyColor') const {generate} = require('@ant-design/colors') const {ADMIN, ANTD} = require('../config/default') const Config = require('../config') const themeMode = ADMIN.theme.mode // 获取 ant design 色系 function getAntdColors(color, mode) { let options = mode && (mode == themeMode.NIGHT) ? {theme: 'dark'} : undefined return generate(color, options) } // 获取功能性颜色 function getFunctionalColors(mode) { let options = mode && (mode == themeMode.NIGHT) ? {theme: 'dark'} : undefined let {success, warning, error} = ANTD.primary const {success: s1, warning: w1, error: e1} = Config.theme success = success && s1 warning = success && w1 error = success && e1 const successColors = generate(success, options) const warningColors = generate(warning, options) const errorColors = generate(error, options) return { success: successColors, warning: warningColors, error: errorColors } } // 获取菜单色系 function getMenuColors(color, mode) { if (mode == themeMode.NIGHT) { return ANTD.primary.night.menuColors } else if (color == ANTD.primary.color) { return ANTD.primary.dark.menuColors } else { return [varyColor.darken(color, 0.93), varyColor.darken(color, 0.83), varyColor.darken(color, 0.73)] } } // 获取主题模式切换色系 function getThemeToggleColors(color, mode) { //主色系 const mainColors = getAntdColors(color, mode) const primary = mainColors[5] //辅助色系,因为 antd 目前没针对夜间模式设计,所以增加辅助色系以保证夜间模式的正常切换 const subColors = getAntdColors(primary, themeMode.LIGHT) //菜单色系 const menuColors = getMenuColors(color, mode) //内容色系(包含背景色、文字颜色等) const themeCfg = ANTD.theme[mode] let contentColors = Object.keys(themeCfg) .map(key => themeCfg[key]) .map(color => isHex(color) ? color : toNum3(color).join(',')) // 内容色去重 contentColors = [...new Set(contentColors)] // rgb 格式的主题色 let rgbColors = [toNum3(primary).join(',')] let functionalColors = getFunctionalColors(mode) return {primary, mainColors, subColors, menuColors, contentColors, rgbColors, functionalColors} } function toNum3(color) { if (isHex(color)) { return varyColor.toNum3(color) } let colorStr = '' if (isRgb(color)) { colorStr = color.slice(5, color.length) } else if (isRgba(color)) { colorStr = color.slice(6, color.lastIndexOf(',')) } let rgb = colorStr.split(',') const r = parseInt(rgb[0]) const g = parseInt(rgb[1]) const b = parseInt(rgb[2]) return [r, g, b] } function isHex(color) { return color.length >= 4 && color[0] == '#' } function isRgb(color) { return color.length >= 10 && color.slice(0, 3) == 'rgb' } function isRgba(color) { return color.length >= 13 && color.slice(0, 4) == 'rgba' } module.exports = { isHex, isRgb, isRgba, toNum3, getAntdColors, getMenuColors, getThemeToggleColors, getFunctionalColors } ================================================ FILE: front/src/utils/formatter.js ================================================ /** * 把对象按照 js配置文件的格式进行格式化 * @param obj 格式化的对象 * @param dep 层级,此项无需传值 * @returns {string} */ function formatConfig(obj, dep) { dep = dep || 1 const LN = '\n', TAB = ' ' let indent = '' for (let i = 0; i < dep; i++) { indent += TAB } let isArray = false, arrayLastIsObj = false let str = '', prefix = '{', subfix = '}' if (Array.isArray(obj)) { isArray = true prefix = '[' subfix = ']' str = obj.map((item, index) => { let format = '' if (typeof item == 'function') { // } else if (typeof item == 'object') { arrayLastIsObj = true format = `${LN}${indent}${formatConfig(item,dep + 1)},` } else if ((typeof item == 'number' && !isNaN(item)) || typeof item == 'boolean') { format = `${item},` } else if (typeof item == 'string') { format = `'${item}',` } if (index == obj.length - 1) { format = format.substring(0, format.length - 1) } else { arrayLastIsObj = false } return format }).join('') } else if (typeof obj != 'function' && typeof obj == 'object') { str = Object.keys(obj).map((key, index, keys) => { const val = obj[key] let format = '' if (typeof val == 'function') { // } else if (typeof val == 'object') { format = `${LN}${indent}${key}: ${formatConfig(val,dep + 1)},` } else if ((typeof val == 'number' && !isNaN(val)) || typeof val == 'boolean') { format = `${LN}${indent}${key}: ${val},` } else if (typeof val == 'string') { format = `${LN}${indent}${key}: '${val}',` } if (index == keys.length - 1) { format = format.substring(0, format.length - 1) } return format }).join('') } const len = TAB.length if (indent.length >= len) { indent = indent.substring(0, indent.length - len) } if (!isArray || arrayLastIsObj) { subfix = LN + indent +subfix } return`${prefix}${str}${subfix}` } module.exports = {formatConfig} ================================================ FILE: front/src/utils/i18n.js ================================================ import Vue from 'vue' import VueI18n from 'vue-i18n' import routesI18n from '@/router/i18n' import './Objects' import {getI18nKey} from '@/utils/routerUtil' /** * 创建 i18n 配置 * @param locale 本地化语言 * @param fallback 回退语言 * @returns {VueI18n} */ function initI18n(locale, fallback) { Vue.use(VueI18n) let i18nOptions = { locale, fallbackLocale: fallback, silentFallbackWarn: true, } return new VueI18n(i18nOptions) } /** * 根据 router options 配置生成 国际化语言 * @param lang * @param routes * @param valueKey * @returns {*} */ function generateI18n(lang, routes, valueKey) { routes.forEach(route => { let keys = getI18nKey(route.fullPath).split('.') let value = valueKey === 'path' ? route[valueKey].split('/').filter(item => !item.startsWith(':') && item != '').join('.') : route[valueKey] lang.assignProps(keys, value) if (route.children) { generateI18n(lang, route.children, valueKey) } }) return lang } /** * 格式化 router.options.routes,生成 fullPath * @param routes * @param parentPath */ function formatFullPath(routes, parentPath = '') { routes.forEach(route => { let isFullPath = route.path.substring(0, 1) === '/' route.fullPath = isFullPath ? route.path : (parentPath === '/' ? parentPath + route.path : parentPath + '/' + route.path) if (route.children) { formatFullPath(route.children, route.fullPath) } }) } /** * 从路由提取国际化数据 * @param i18n * @param routes */ function mergeI18nFromRoutes(i18n, routes) { formatFullPath(routes) const CN = generateI18n(new Object(), routes, 'name') const US = generateI18n(new Object(), routes, 'path') i18n.mergeLocaleMessage('CN', CN) i18n.mergeLocaleMessage('US', US) const messages = routesI18n.messages Object.keys(messages).forEach(lang => { i18n.mergeLocaleMessage(lang, messages[lang]) }) } export { initI18n, mergeI18nFromRoutes, formatFullPath } ================================================ FILE: front/src/utils/request.js ================================================ import axios from 'axios' import Cookie from 'js-cookie' import {message} from "ant-design-vue"; // 跨域认证信息 header 名 const xsrfHeaderName = 'Authorization' axios.defaults.timeout = 5000 axios.defaults.withCredentials= true axios.defaults.xsrfHeaderName= xsrfHeaderName axios.defaults.xsrfCookieName= xsrfHeaderName // 认证类型 const AUTH_TYPE = { BEARER: 'Bearer', BASIC: 'basic', AUTH1: 'auth1', AUTH2: 'auth2', } // http method const METHOD = { GET: 'get', POST: 'post', PATCH:'patch', PUT:'put', DELETE:'delete' } /** * axios请求 * @param url 请求地址 * @param method {METHOD} http method * @param params 请求参数 * @returns {Promise>} */ async function request(url, method, params, config) { let promise =null; switch (method) { case METHOD.GET: promise = axios.get(url, {params, ...config}); break; case METHOD.POST: promise = axios.post(url, params, config); break; case METHOD.PUT: promise = axios.put(url, params, config); break; case METHOD.PATCH: promise = axios.patch(url,params,config); break; case METHOD.DELETE: promise = axios.delete(url,config); break; default: promise = axios.get(url, {params, ...config}); break; } promise.catch(()=>{ message.warning('请求出错,您可能已经退出登录') }) return promise; } /** * 设置认证信息 * @param auth {Object} * @param authType {AUTH_TYPE} 认证类型,默认:{AUTH_TYPE.BEARER} */ function setAuthorization(auth, authType = AUTH_TYPE.BEARER) { switch (authType) { case AUTH_TYPE.BEARER: // Cookie.set(xsrfHeaderName, 'Bearer ' + auth.token, {expires: auth.expireAt}) Cookie.set(xsrfHeaderName, auth.token, {expires: auth.expireAt}) break case AUTH_TYPE.BASIC: case AUTH_TYPE.AUTH1: case AUTH_TYPE.AUTH2: default: break } } /** * 移出认证信息 * @param authType {AUTH_TYPE} 认证类型 */ function removeAuthorization(authType = AUTH_TYPE.BEARER) { switch (authType) { case AUTH_TYPE.BEARER: Cookie.remove(xsrfHeaderName) break case AUTH_TYPE.BASIC: case AUTH_TYPE.AUTH1: case AUTH_TYPE.AUTH2: default: break } } /** * 检查认证信息 * @param authType * @returns {boolean} */ function checkAuthorization(authType = AUTH_TYPE.BEARER) { switch (authType) { case AUTH_TYPE.BEARER: if (Cookie.get(xsrfHeaderName)) { return true } break case AUTH_TYPE.BASIC: case AUTH_TYPE.AUTH1: case AUTH_TYPE.AUTH2: default: break } return false } /** * 加载 axios 拦截器 * @param interceptors * @param options */ function loadInterceptors(interceptors, options) { const {request, response} = interceptors // 加载请求拦截器 request.forEach(item => { let {onFulfilled, onRejected} = item if (!onFulfilled || typeof onFulfilled !== 'function') { onFulfilled = config => config } if (!onRejected || typeof onRejected !== 'function') { onRejected = error => Promise.reject(error) } axios.interceptors.request.use( config => onFulfilled(config, options), error => onRejected(error, options) ) }) // 加载响应拦截器 response.forEach(item => { let {onFulfilled, onRejected} = item if (!onFulfilled || typeof onFulfilled !== 'function') { onFulfilled = response => response } if (!onRejected || typeof onRejected !== 'function') { onRejected = error => Promise.reject(error) } axios.interceptors.response.use( response => onFulfilled(response, options), error => onRejected(error, options) ) }) } /** * 解析 url 中的参数 * @param url * @returns {Object} */ function parseUrlParams(url) { const params = {} if (!url || url === '' || typeof url !== 'string') { return params } const paramsStr = url.split('?')[1] if (!paramsStr) { return params } const paramsArr = paramsStr.replace(/&|=/g, ' ').split(' ') for (let i = 0; i < paramsArr.length / 2; i++) { const value = paramsArr[i * 2 + 1] params[paramsArr[i * 2]] = value === 'true' ? true : (value === 'false' ? false : value) } return params } export { METHOD, AUTH_TYPE, request, setAuthorization, removeAuthorization, checkAuthorization, loadInterceptors, parseUrlParams } ================================================ FILE: front/src/utils/routerUtil.js ================================================ import routerMap from '@/router/async/router.map' import {mergeI18nFromRoutes} from '@/utils/i18n' import Router from 'vue-router' import deepMerge from 'deepmerge' import basicOptions from '@/router/async/config.async' //应用配置 let appOptions = { router: undefined, i18n: undefined, store: undefined } /** * 设置应用配置 * @param options */ function setAppOptions(options) { const {router, store, i18n} = options appOptions.router = router appOptions.store = store appOptions.i18n = i18n } /** * 根据 路由配置 和 路由组件注册 解析路由 * @param routesConfig 路由配置 * @param routerMap 本地路由组件注册配置 */ function parseRoutes(routesConfig, routerMap) { let routes = [] routesConfig.forEach(item => { // 获取注册在 routerMap 中的 router,初始化 routeCfg let router = undefined, routeCfg = {} if (typeof item === 'string') { router = routerMap[item] routeCfg = {path: (router && router.path) || item, router: item} } else if (typeof item === 'object') { router = routerMap[item.router] routeCfg = item } if (!router) { console.warn(`can't find register for router ${routeCfg.router}, please register it in advance.`) router = typeof item === 'string' ? {path: item, name: item} : item } // 从 router 和 routeCfg 解析路由 const meta = { authority: router.authority, icon: router.icon, page: router.page, link: router.link, params: router.params, query: router.query, ...router.meta } const cfgMeta = { authority: routeCfg.authority, icon: routeCfg.icon, page: routeCfg.page, link: routeCfg.link, params: routeCfg.params, query: routeCfg.query, ...routeCfg.meta } Object.keys(cfgMeta).forEach(key => { if (cfgMeta[key] === undefined || cfgMeta[key] === null || cfgMeta[key] === '') { delete cfgMeta[key] } }) Object.assign(meta, cfgMeta) const route = { path: routeCfg.path || router.path || routeCfg.router, name: routeCfg.name || router.name, component: router.component, redirect: routeCfg.redirect || router.redirect, meta: {...meta, authority: meta.authority || '*'} } if (routeCfg.invisible || router.invisible) { route.meta.invisible = true } if (routeCfg.children && routeCfg.children.length > 0) { route.children = parseRoutes(routeCfg.children, routerMap) } routes.push(route) }) return routes } /** * 加载路由 * @param routesConfig {RouteConfig[]} 路由配置 */ function loadRoutes(routesConfig) { //兼容 0.6.1 以下版本 /*************** 兼容 version < v0.6.1 *****************/ if (arguments.length > 0) { const arg0 = arguments[0] if (arg0.router || arg0.i18n || arg0.store) { routesConfig = arguments[1] console.error('the usage of signature loadRoutes({router, store, i18n}, routesConfig) is out of date, please use the new signature: loadRoutes(routesConfig).') console.error('方法签名 loadRoutes({router, store, i18n}, routesConfig) 的用法已过时, 请使用新的方法签名 loadRoutes(routesConfig)。') } } /*************** 兼容 version < v0.6.1 *****************/ // 应用配置 const {router, store, i18n} = appOptions // 如果 routesConfig 有值,则更新到本地,否则从本地获取 if (routesConfig) { store.commit('account/setRoutesConfig', routesConfig) } else { routesConfig = store.getters['account/routesConfig'] } // 如果开启了异步路由,则加载异步路由配置 const asyncRoutes = store.state.setting.asyncRoutes if (asyncRoutes) { if (routesConfig && routesConfig.length > 0) { const routes = parseRoutes(routesConfig, routerMap) const finalRoutes = mergeRoutes(basicOptions.routes, routes) formatRoutes(finalRoutes) router.options = {...router.options, routes: finalRoutes} router.matcher = new Router({...router.options, routes:[]}).matcher router.addRoutes(finalRoutes) } } // 提取路由国际化数据 mergeI18nFromRoutes(i18n, router.options.routes) // 初始化Admin后台菜单数据 const rootRoute = router.options.routes.find(item => item.path === '/') const menuRoutes = rootRoute && rootRoute.children if (menuRoutes) { store.commit('setting/setMenuData', menuRoutes) } } /** * 合并路由 * @param target {Route[]} * @param source {Route[]} * @returns {Route[]} */ function mergeRoutes(target, source) { const routesMap = {} target.forEach(item => routesMap[item.path] = item) source.forEach(item => routesMap[item.path] = item) return Object.values(routesMap) } /** * 深度合并路由 * @param target {Route[]} * @param source {Route[]} * @returns {Route[]} */ function deepMergeRoutes(target, source) { // 映射路由数组 const mapRoutes = routes => { const routesMap = {} routes.forEach(item => { routesMap[item.path] = { ...item, children: item.children ? mapRoutes(item.children) : undefined } }) return routesMap } const tarMap = mapRoutes(target) const srcMap = mapRoutes(source) // 合并路由 const merge = deepMerge(tarMap, srcMap) // 转换为 routes 数组 const parseRoutesMap = routesMap => { return Object.values(routesMap).map(item => { if (item.children) { item.children = parseRoutesMap(item.children) } else { delete item.children } return item }) } return parseRoutesMap(merge) } /** * 格式化路由 * @param routes 路由配置 */ function formatRoutes(routes) { routes.forEach(route => { const {path} = route if (!path.startsWith('/') && path !== '*') { route.path = '/' + path } }) formatAuthority(routes) } /** * 格式化路由的权限配置 * @param routes 路由 * @param pAuthorities 父级路由权限配置集合 */ function formatAuthority(routes, pAuthorities = []) { routes.forEach(route => { const meta = route.meta const defaultAuthority = pAuthorities[pAuthorities.length - 1] || {permission: '*'} if (meta) { let authority = {} if (!meta.authority) { authority = defaultAuthority }else if (typeof meta.authority === 'string') { authority.permission = meta.authority } else if (typeof meta.authority === 'object') { authority = meta.authority const {role} = authority if (typeof role === 'string') { authority.role = [role] } if (!authority.permission && !authority.role) { authority = defaultAuthority } } meta.authority = authority } else { const authority = defaultAuthority route.meta = {authority} } route.meta.pAuthorities = pAuthorities if (route.children) { formatAuthority(route.children, [...pAuthorities, route.meta.authority]) } }) } /** * 从路由 path 解析 i18n key * @param path * @returns {*} */ function getI18nKey(path) { const keys = path.split('/').filter(item => !item.startsWith(':') && item != '') keys.push('name') return keys.join('.') } /** * 加载导航守卫 * @param guards * @param options */ function loadGuards(guards, options) { const {beforeEach, afterEach} = guards const {router} = options beforeEach.forEach(guard => { if (guard && typeof guard === 'function') { router.beforeEach((to, from, next) => guard(to, from, next, options)) } }) afterEach.forEach(guard => { if (guard && typeof guard === 'function') { router.afterEach((to, from) => guard(to, from, options)) } }) } export {parseRoutes, loadRoutes, formatAuthority, getI18nKey, loadGuards, deepMergeRoutes, formatRoutes, setAppOptions} ================================================ FILE: front/src/utils/theme-color-replacer-extend.js ================================================ const {cssResolve} = require('../config/replacer') // 修正 webpack-theme-color-replacer 插件提取的 css 结果 function resolveCss(output, srcArr) { let regExps = [] // 提取 resolve 配置中所有的正则配置 Object.keys(cssResolve).forEach(key => { let isRegExp = false let reg = {} try { reg = eval(key) isRegExp = reg instanceof RegExp } catch (e) { isRegExp = false } if (isRegExp) { regExps.push([reg, cssResolve[key]]) } }) // 去重 srcArr = dropDuplicate(srcArr) // 处理 css let outArr = [] srcArr.forEach(text => { // 转换为 css 对象 let cssObj = parseCssObj(text) // 根据selector匹配配置,匹配成功,则按配置处理 css if (cssResolve[cssObj.selector] != undefined) { let cfg = cssResolve[cssObj.selector] if (cfg) { outArr.push(cfg.resolve(text, cssObj)) } } else { let cssText = '' // 匹配不成功,则测试是否有匹配的正则配置,有则按正则对应的配置处理 for (let regExp of regExps) { if (regExp[0].test(cssObj.selector)) { let cssCfg = regExp[1] cssText = cssCfg ? cssCfg.resolve(text, cssObj) : '' break } // 未匹配到正则,则设置 cssText 为默认的 css(即不处理) cssText = text } if (cssText != '') { outArr.push(cssText) } } }) output = outArr.join('\n') return output } // 数组去重 function dropDuplicate(arr) { let map = {} let r = [] for (let s of arr) { if (!map[s]) { r.push(s) map[s] = 1 } } return r } /** * 从字符串解析 css 对象 * @param cssText * @returns {{ * name: String, * rules: Array[String], * toText: function * }} */ function parseCssObj(cssText) { let css = {} const ruleIndex = cssText.indexOf('{') css.selector = cssText.substring(0, ruleIndex) const ruleBody = cssText.substring(ruleIndex + 1, cssText.length - 1) const rules = ruleBody.split(';') css.rules = rules css.toText = function () { let body = '' this.rules.forEach(item => {body += item + ';'}) return `${this.selector}{${body}}` } return css } module.exports = {resolveCss} ================================================ FILE: front/src/utils/themeUtil.js ================================================ const client = require('webpack-theme-color-replacer/client') const {theme} = require('../config') const {getMenuColors, getAntdColors, getThemeToggleColors, getFunctionalColors} = require('../utils/colors') const {ANTD} = require('../config/default') function getThemeColors(color, $theme) { const _color = color || theme.color const mode = $theme || theme.mode const replaceColors = getThemeToggleColors(_color, mode) const themeColors = [ ...replaceColors.mainColors, ...replaceColors.subColors, ...replaceColors.menuColors, ...replaceColors.contentColors, ...replaceColors.rgbColors, ...replaceColors.functionalColors.success, ...replaceColors.functionalColors.warning, ...replaceColors.functionalColors.error, ] return themeColors } function changeThemeColor(newColor, $theme) { let promise = client.changer.changeColor({newColors: getThemeColors(newColor, $theme)}) return promise } function modifyVars(color) { let _color = color || theme.color const palettes = getAntdColors(_color, theme.mode) const menuColors = getMenuColors(_color, theme.mode) const {success, warning, error} = getFunctionalColors(theme.mode) const primary = palettes[5] return { 'primary-color': primary, 'primary-1': palettes[0], 'primary-2': palettes[1], 'primary-3': palettes[2], 'primary-4': palettes[3], 'primary-5': palettes[4], 'primary-6': palettes[5], 'primary-7': palettes[6], 'primary-8': palettes[7], 'primary-9': palettes[8], 'primary-10': palettes[9], 'info-color': primary, 'success-color': success[5], 'warning-color': warning[5], 'error-color': error[5], 'alert-info-bg-color': palettes[0], 'alert-info-border-color': palettes[2], 'alert-success-bg-color': success[0], 'alert-success-border-color': success[2], 'alert-warning-bg-color': warning[0], 'alert-warning-border-color': warning[2], 'alert-error-bg-color': error[0], 'alert-error-border-color': error[2], 'processing-color': primary, 'menu-dark-submenu-bg': menuColors[0], 'layout-header-background': menuColors[1], 'layout-trigger-background': menuColors[2], 'btn-danger-bg': error[4], 'btn-danger-border': error[4], ...ANTD.theme[theme.mode] } } function loadLocalTheme(localSetting) { if (localSetting && localSetting.theme) { let {color, mode} = localSetting.theme color = color || theme.color mode = mode || theme.mode changeThemeColor(color, mode) } } /** * 获取本地保存的配置 * @param load {boolean} 是否加载配置中的主题 * @returns {Object} */ function getLocalSetting(loadTheme) { let localSetting = {} try { const localSettingStr = localStorage.getItem(process.env.VUE_APP_SETTING_KEY) localSetting = JSON.parse(localSettingStr) } catch (e) { console.error(e) } if (loadTheme) { loadLocalTheme(localSetting) } return localSetting } module.exports = { getThemeColors, changeThemeColor, modifyVars, loadLocalTheme, getLocalSetting } ================================================ FILE: front/src/utils/util.js ================================================ import enquireJs from 'enquire.js' export function isDef (v){ return v !== undefined && v !== null } /** * Remove an item from an array. */ export function remove (arr, item) { if (arr.length) { const index = arr.indexOf(item) if (index > -1) { return arr.splice(index, 1) } } } export function isRegExp (v) { return _toString.call(v) === '[object RegExp]' } export function enquireScreen(call) { const handler = { match: function () { call && call(true) }, unmatch: function () { call && call(false) } } enquireJs.register('only screen and (max-width: 767.99px)', handler) } const _toString = Object.prototype.toString ================================================ FILE: front/src/utils/validators.js ================================================ const passwordReg = /(?=.*[0-9])(?=.*[A-Z])(?=.*[a-z])(?=.*[^a-zA-Z0-9]).{8,30}/ const passwordMsg="密码中必须包含大小写字母、数字、特称字符,至少8个字符,最多30个字符" const emailReg = /\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/ const validators = { passwordReg, passwordMsg, password(){ return function(rule, value,callback){ let reg = passwordReg const msg = passwordMsg if(!reg.test(value)){ if(!callback)return msg; callback(msg) } } }, email(){ return function(rule, value,callback){ let reg =emailReg; const msg = "您输入的email地址不正确" if(!reg.test(value)){ if(!callback) return msg; callback(msg) } } }, emailReg, phoneReg:/^1(3\d|4[5-9]|5[0-35-9]|6[2567]|7[0-8]|8\d|9[0-35-9])\d{8}$/, phoneMsg:"手机号格式不正确", qqReg:/^[1-9][0-9]{4,9}$/gim, qqMsg:"您输入的QQ号不正确", install:function(Vue) { Vue.prototype.validators = validators; } } export default validators ================================================ FILE: front/vue.config.js ================================================ let path = require('path') const webpack = require('webpack') const ThemeColorReplacer = require('webpack-theme-color-replacer') const {getThemeColors, modifyVars} = require('./src/utils/themeUtil') const {resolveCss} = require('./src/utils/theme-color-replacer-extend') const CompressionWebpackPlugin = require('compression-webpack-plugin') const productionGzipExtensions = ['js', 'css'] const isProd = process.env.NODE_ENV === 'production' const assetsCDN = { // webpack build externals externals: { vue: 'Vue', 'vue-router': 'VueRouter', vuex: 'Vuex', axios: 'axios', nprogress: 'NProgress', clipboard: 'ClipboardJS', '@antv/data-set': 'DataSet', 'js-cookie': 'Cookies' }, css: [ ], js: [ '//cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.min.js', '//cdn.jsdelivr.net/npm/vue-router@3.3.4/dist/vue-router.min.js', '//cdn.jsdelivr.net/npm/vuex@3.4.0/dist/vuex.min.js', '//cdn.jsdelivr.net/npm/axios@0.19.2/dist/axios.min.js', '//cdn.jsdelivr.net/npm/nprogress@0.2.0/nprogress.min.js', '//cdn.jsdelivr.net/npm/clipboard@2.0.6/dist/clipboard.min.js', '//cdn.jsdelivr.net/npm/@antv/data-set@0.11.4/build/data-set.min.js', '//cdn.jsdelivr.net/npm/js-cookie@2.2.1/src/js.cookie.min.js' ] } module.exports = { devServer: { // proxy: { // '/api': { //此处要与 /services/api.js 中的 API_PROXY_PREFIX 值保持一致 // target: process.env.VUE_APP_API_BASE_URL, // changeOrigin: true, // pathRewrite: { // '^/api': '' // } // } // } }, pluginOptions: { 'style-resources-loader': { preProcessor: 'less', patterns: [path.resolve(__dirname, "./src/theme/theme.less")], } }, configureWebpack: config => { config.entry.app = ["babel-polyfill", "whatwg-fetch", "./src/main.js"]; config.performance = { hints: false } config.plugins.push( new ThemeColorReplacer({ fileName: 'css/theme-colors-[contenthash:8].css', matchColors: getThemeColors(), injectCss: true, resolveCss }) ) // Ignore all locale files of moment.js config.plugins.push(new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)) // 生产环境下将资源压缩成gzip格式 if (isProd) { // add `CompressionWebpack` plugin to webpack plugins config.plugins.push(new CompressionWebpackPlugin({ algorithm: 'gzip', test: new RegExp('\\.(' + productionGzipExtensions.join('|') + ')$'), threshold: 10240, minRatio: 0.8 })) } // if prod, add externals if (isProd) { config.externals = assetsCDN.externals } }, chainWebpack: config => { // 生产环境下关闭css压缩的 colormin 项,因为此项优化与主题色替换功能冲突 if (isProd) { config.plugin('optimize-css') .tap(args => { args[0].cssnanoOptions.preset[1].colormin = false return args }) } // 生产环境下使用CDN if (isProd) { config.plugin('html') .tap(args => { args[0].cdn = assetsCDN return args }) } }, css: { loaderOptions: { less: { lessOptions: { modifyVars: modifyVars(), javascriptEnabled: true } } } }, publicPath: process.env.VUE_APP_PUBLIC_PATH, outputDir: 'dist', assetsDir: 'static', productionSourceMap: false } ================================================ FILE: mysql/dev.sql ================================================ -- MySQL dump 10.13 Distrib 8.0.23, for Win64 (x86_64) -- -- Host: 127.0.0.1 Database: crm3 -- ------------------------------------------------------ -- Server version 8.0.23 /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; /*!50503 SET NAMES utf8mb4 */; /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; /*!40103 SET TIME_ZONE='+00:00' */; /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; /* crm */; -- -- Table structure for table `customer_follow_up_history` -- DROP TABLE IF EXISTS `customer_follow_up_history`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `customer_follow_up_history` ( `id` int NOT NULL AUTO_INCREMENT, `traceTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '跟进时间', `traceDetails` text COMMENT '跟进内容 计划的详细内容', `traceType` int DEFAULT NULL COMMENT '跟进方式 计划采用如电话、邀约上门等 数据字典', `traceResult` int DEFAULT NULL COMMENT '跟进效果 优----3、中----2、差----1', `customerID` int DEFAULT NULL COMMENT '跟进客户 编辑时不可编辑 潜在客户对象/客户对象', `inputUser` int DEFAULT NULL COMMENT '创建人 自动填入当前登录用户,用户不可更改/员工对象', `type` int DEFAULT NULL COMMENT '跟进类型 0:潜在开发计划 1:客户跟进历史', `comment` text, PRIMARY KEY (`id`), UNIQUE KEY `customer_follow_up_history_id_uindex` (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `customer_follow_up_history` -- LOCK TABLES `customer_follow_up_history` WRITE; /*!40000 ALTER TABLE `customer_follow_up_history` DISABLE KEYS */; INSERT INTO `customer_follow_up_history` VALUES (1,'2021-05-21 20:10:22','还不错',24,3,1,5,1,'阿迪斯发打发士大夫'),(2,'2021-05-21 21:05:46','123',24,2,1,2,0,'123123'),(3,'2021-05-21 21:06:46','还可以',24,3,7,3,0,'还不错'),(4,'2021-05-21 21:07:40','123',24,2,9,4,0,'13123'),(5,'2021-05-22 01:24:50','还可以1',24,2,1,5,0,'12313'),(6,'2021-05-01 08:00:17','123123',24,1,7,1,0,'拉了哭了'),(7,'2021-05-22 01:30:00','1231',24,2,7,6,1,'123123'),(8,'2021-05-22 01:31:11','123132',24,2,2,1,0,'123123'),(9,'2021-05-22 01:32:16','1231',24,1,4,7,0,'1231132'),(10,'2021-05-22 01:33:54','123',24,1,8,9,0,'123123'),(11,'2021-05-21 17:34:20','string',0,0,0,7,0,'string'),(12,'2021-05-22 01:36:53','123123',24,1,8,6,1,'德邦物流沟通不利'),(13,'2021-05-14 07:58:04','哔哩哔哩八零八零八',25,3,8,2,1,'叭叭叭粑粑'),(14,'2021-05-08 12:52:18','234234',26,3,3,5,1,'214143'),(15,'2021-05-23 02:37:17','天天',25,2,1,1031,0,'天天'),(16,'2021-05-23 02:47:09','天天',24,2,8,1031,0,'UI'),(17,'2021-05-23 03:06:37','天天',24,1,3,1031,0,'21'); /*!40000 ALTER TABLE `customer_follow_up_history` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `customer_handover` -- DROP TABLE IF EXISTS `customer_handover`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `customer_handover` ( `id` int NOT NULL AUTO_INCREMENT, `customerID` int DEFAULT NULL COMMENT '客户 客户对象', `transUser` int DEFAULT NULL COMMENT '移交人员 实行移交操作的管理人员', `transTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `oldSeller` int DEFAULT NULL COMMENT '老市场专员 客户上的原始市场人员', `newSeller` int DEFAULT NULL COMMENT '新市场专员 由公司重新指派后的新市场人员', `transReason` varchar(255) DEFAULT NULL COMMENT '移交原因', PRIMARY KEY (`id`), UNIQUE KEY `customer_handover_id_uindex` (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `customer_handover` -- LOCK TABLES `customer_handover` WRITE; /*!40000 ALTER TABLE `customer_handover` DISABLE KEYS */; INSERT INTO `customer_handover` VALUES (1,7,2,'2021-05-22 08:25:53',2,2,'下放任务'),(3,1,1,'2021-05-22 08:25:53',5,4,'123'),(4,1,1,'2021-05-22 08:25:53',3,2,'123'),(5,1,1,'2021-05-22 08:25:53',4,4,'123'),(6,1,1,'2021-05-22 08:25:53',7,1,'123'),(7,1,1,'2021-05-22 08:25:53',9,3,'123'),(8,1,1,'2021-05-22 08:25:53',6,3,'23423424'),(9,1,1,'2021-05-21 11:06:38',3,4,'123'),(10,1,1031,'2021-05-23 02:38:04',4,7,'天天天天'),(11,1,1031,'2021-05-23 02:38:14',7,3,'人员人员'),(12,8,1031,'2021-05-23 02:41:29',7,7,'让他'),(13,12,1031,'2021-05-23 02:43:15',2,7,'体验'),(14,6,5,'2021-05-23 02:44:34',6,9,'阿斯蒂芬'),(15,15,1031,'2021-05-23 02:46:45',1031,7,'一天'),(16,3,1,'2021-05-23 12:10:18',3,8,'任务'),(17,4,1092,'2021-05-24 08:33:16',5,1032,'123'); /*!40000 ALTER TABLE `customer_handover` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `customer_manager` -- DROP TABLE IF EXISTS `customer_manager`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `customer_manager` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL COMMENT '客户姓名', `age` int NOT NULL COMMENT '客户年龄', `gender` int NOT NULL COMMENT '客户性别 页面为下拉框 1男 0女', `tel` varchar(255) NOT NULL COMMENT '电话号码', `qq` varchar(255) DEFAULT NULL, `job` int NOT NULL, `source` int NOT NULL COMMENT '客户来源', `seller` int DEFAULT NULL COMMENT '负责人 填写为当前登录用户', `inputUser` int NOT NULL COMMENT ' 创建人 填写为当前登录用户', `inputTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `status` int NOT NULL DEFAULT '0' COMMENT '-2:流失 -1:开发失败 0:潜在客户 1:正式客户 2:资源池客户', `positiveTime` datetime DEFAULT NULL COMMENT '转正时间', PRIMARY KEY (`id`), UNIQUE KEY `customer_manager_id_uindex` (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=19 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `customer_manager` -- LOCK TABLES `customer_manager` WRITE; /*!40000 ALTER TABLE `customer_manager` DISABLE KEYS */; INSERT INTO `customer_manager` VALUES (1,'秦农',24,1,'13766546213','100001',3,17,1,1,'2021-05-24 07:15:25',0,'2021-05-20 19:31:02'),(2,'马腾',33,1,'188888888','100002',3,17,2,2,'2019-05-22 08:26:27',2,'2021-05-20 19:31:02'),(3,'张云',21,1,'18888888888','100001',3,17,3,3,'2021-05-23 03:07:02',1,'2021-05-20 19:31:02'),(4,'权志龙',18,1,'18888888888','100001',3,17,5,5,'2021-04-22 08:26:27',2,'2021-05-20 19:31:02'),(5,'马钊',23,1,'16666666666','100001',3,17,4,4,'2021-05-23 12:16:30',1,'2021-05-20 19:31:02'),(6,'合理吗?🎃',18,0,'18888888888','100001',3,17,6,6,'2021-05-21 03:56:13',-2,'2021-05-20 19:31:02'),(7,'酒剑仙🗡',18,1,'17777777777','100001',3,17,5,5,'2021-05-22 08:26:27',0,'2021-05-20 19:31:02'),(8,'赵',21,1,'18888888888','100001',3,17,7,7,'2021-05-23 02:40:26',1,'2021-05-20 19:31:02'),(9,'伊泽',45,0,'1999999999','100001',3,17,8,8,'2021-05-23 08:26:27',2,'2021-05-20 19:31:02'),(10,'阿斯顿',18,1,'18888888888','100001',3,17,9,9,'2021-05-22 08:26:27',-1,'2021-05-20 19:31:02'),(11,'廖嘉积',54,0,'18888888888','100001',3,17,3,3,'2021-05-22 08:26:27',-2,'2021-05-20 19:31:02'),(12,'郭晋安',18,1,'18888888888','100001',3,17,2,2,'2021-05-22 08:26:27',2,'2021-05-20 19:31:02'),(13,'埃里克森',18,1,'13333333333','100002',3,17,2,2,'2021-05-22 08:26:27',2,'2021-05-20 19:31:02'),(14,'张🗡男',22,1,'123123123','123123',28,18,2,5,'2021-05-23 10:41:48',0,NULL),(15,'张华梁',12,1,'23444','2123',28,20,1,1031,'2021-05-23 10:43:50',1,NULL),(16,'林阳露',22,0,'1234567','12345678',5,21,2,1032,'2021-05-23 10:41:48',0,NULL),(17,'贵',21,1,'23444','6532',27,18,1,1031,'2021-05-23 10:41:48',0,NULL),(18,'胡12',123,1,'123','123',28,20,1,1,'2021-05-23 10:42:53',0,NULL); /*!40000 ALTER TABLE `customer_manager` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `department` -- DROP TABLE IF EXISTS `department`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `department` ( `id` int NOT NULL AUTO_INCREMENT, `sn` varchar(255) DEFAULT NULL, `name` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `department_id_uindex` (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=46 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `department` -- LOCK TABLES `department` WRITE; /*!40000 ALTER TABLE `department` DISABLE KEYS */; INSERT INTO `department` VALUES (2,'All Department Manager','总经办'),(3,'Human Resources Department','人力资源部'),(5,'Order Department','采购部'),(6,'Warehousing Department','仓储部'),(7,'Finance Department','财务部'),(11,'Publicity department','宣传部门'),(17,'Cultural Department🥼🧥👔👕','文化部门'),(20,'Testing department','测试部门'),(31,'Data center','数据中心'),(32,'Laboratory','实验中心'),(33,'123333Quality inspection department','质量校验部门'),(40,'Financial Business Department','金融事业部'),(41,'Executive Department','执行部'),(42,'Training place','培训部'),(43,'Marketing Department','市场部'),(44,'Channel Location Division','渠道选址事业部'),(45,'Data Collection Department','数据采集部'); /*!40000 ALTER TABLE `department` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `dictionary_contents` -- DROP TABLE IF EXISTS `dictionary_contents`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `dictionary_contents` ( `id` int NOT NULL AUTO_INCREMENT, `sn` varchar(255) DEFAULT NULL COMMENT '字典目录编号', `title` varchar(255) DEFAULT NULL COMMENT '字典目录名称', `intro` varchar(255) DEFAULT NULL COMMENT '字典目录简介', PRIMARY KEY (`id`), UNIQUE KEY `dictionary_contents_id_uindex` (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `dictionary_contents` -- LOCK TABLES `dictionary_contents` WRITE; /*!40000 ALTER TABLE `dictionary_contents` DISABLE KEYS */; INSERT INTO `dictionary_contents` VALUES (1,'job','职业','做什么的'),(2,'source','来源','客户来源渠道'),(3,'intentionDegree','意向程度','有多么想入坑'),(4,'subject','学科','学科分类'),(5,'Collection type','收款类型','学费收款方式'),(6,'School nature','办学性质','School nature'),(7,'Customer importance','客户重要程度','Customer importance'),(8,'Foreign language proficiency','外语水平','Foreign language proficiency'),(9,'Career test','职业测试','Career test'),(10,'Follow-up method','跟进方式','客户跟进的方式'); /*!40000 ALTER TABLE `dictionary_contents` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `dictionary_details` -- DROP TABLE IF EXISTS `dictionary_details`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `dictionary_details` ( `id` int NOT NULL AUTO_INCREMENT, `title` varchar(255) DEFAULT NULL COMMENT '字典明细名称', `sequence` int DEFAULT NULL COMMENT '字典明细序号', `parentId` int NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY ` dictionary_details_id_uindex` (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=50 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `dictionary_details` -- LOCK TABLES `dictionary_details` WRITE; /*!40000 ALTER TABLE `dictionary_details` DISABLE KEYS */; INSERT INTO `dictionary_details` VALUES (1,'教育学研究人员(GBM20104)',2,1),(2,'专业技术人员(GBM20000)',1,1),(3,'企业负责人(GBM10601)',1,1),(4,'党的机关、国家机关、群众团体和社会组织、企事业单位负责人(GBM10000)',12,1),(5,'教师',3,1),(8,'微信',1,2),(9,'抖音',5,2),(10,'微博',7,2),(11,'测试信息',1,3),(12,'支付宝',1,5),(13,'公立院校',1,6),(14,'重要',1,7),(15,'微信',2,5),(16,'私立院校',2,6),(17,'QQ',1,2),(18,'街头小广告',1,2),(19,'头条号',1,2),(20,'微信公众号',2,2),(21,'报纸',1,2),(22,'Bilibili',4,2),(23,'大学英语四级CET-4(四级)',1,8),(24,'营销QQ',1,10),(25,'营销微信',1,10),(26,'营销抖音',1,10),(27,'司机',999,1),(28,'编辑',45,1),(29,'办事人员和有关人员(GBM30000)',999,1),(30,'书信',12,10),(31,'中考英语分数',2,8),(32,'高考英语分数',2,8),(33,'大学英语六级CET-6(六级)',2,8),(34,'专业英语4级(专四)(TEM-4)',3,8),(35,'专业英语8级(专八)(TEM-8)',2,8),(36,'全国英语等级考试(PETS)',3,8),(37,'商务英语考试 (BEC)',2,8),(38,'翻译专业资格考试(CATTI)',2,8),(39,'上海外语口译证书',2,8),(40,'雅思(IELTS)',1,8),(41,'托福(TOEFL)',1,8),(42,'托业(TOEIC)',1,8),(43,'社会生产服务和生活服务人员(GBM40000)',1,1),(44,'其他批发与零售服务人员(GBM40199)',1,1),(45,'农、林、牧、渔业生产及辅助人员(GBM50000)',1,1),(46,'生产制造及有关人员(GBM60000)',12,1),(47,'军人GBM70000)',1,1),(48,'不便分类的其他从业人员(GBM80000)',1,1),(49,'宗教组织负责人(GBM10406)',11,1); /*!40000 ALTER TABLE `dictionary_details` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `employee` -- DROP TABLE IF EXISTS `employee`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `employee` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, `password` varchar(255) NOT NULL, `email` varchar(255) NOT NULL, `age` int DEFAULT NULL, `dept` int NOT NULL, `hireDate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入职时间', `state` int NOT NULL DEFAULT '1' COMMENT '状态 1正常 0离职', `admin` int NOT NULL DEFAULT '0' COMMENT '超级管理员身份 1超管 0普通', `login_time` datetime DEFAULT NULL, `register_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `employee_id_uindex` (`id`), UNIQUE KEY `employee_name_uindex` (`name`), UNIQUE KEY `employee_email_uindex` (`email`) ) ENGINE=InnoDB AUTO_INCREMENT=1093 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `employee` -- LOCK TABLES `employee` WRITE; /*!40000 ALTER TABLE `employee` DISABLE KEYS */; INSERT INTO `employee` VALUES (1,'admin','$2a$10$OG1zaFHT2LUy4SGcQ4EnRu9sPQMjMGEE6jARz61aQwRQ3316N6ikG','1623@163.com',20,2,'2021-05-14 00:28:00',1,1,'2021-05-26 14:00:55','2021-05-21 08:46:19'),(2,'肖总','$2a$10$./YLhMGRhhqMwJOoxJGKYeuKsXehDyTt5C6Eq9CfAshnGWlPL8SNG','163@163.com',35,43,'2021-05-16 01:19:51',1,1,'2021-05-21 16:46:24','2021-05-21 08:46:25'),(3,'赵一明','$2a$10$/h22UTKprujOhSnaugy0/.dJHpNsox.OvPuzWCMMKoFm2FOrBurwO','g@gmail.com',25,3,'2021-05-16 01:22:38',1,1,'2021-05-21 16:46:27','2021-05-21 08:46:27'),(4,'刘九江','$2a$10$4zNrZ/O1SsOcsFB6Hi9tPOGazrbU8dmV2igZaTxClNyQjONHDr3g2','msy@msy.plus',14,2,'2021-05-21 11:07:36',1,1,'2021-05-21 20:37:44','2021-05-21 11:07:29'),(5,'墨抒颖','$2a$10$H5uwoLQIGQCmZpH98UCLbezAFKBcV6XxziDXH89JuAy2LBzspoGjO','msy.plus@qq.com',101,2,'2021-05-21 11:49:52',1,0,'2021-05-26 10:48:16','2021-05-21 11:49:52'),(6,'Ralph V. Livengood','123123','RalphVLivengood@gmail.com',35,6,'2021-05-18 06:49:32',1,0,'2021-05-21 16:46:28','2021-05-21 08:46:29'),(7,'Lauren C. Young','123333','LaurenCYoung@gmail.com',33,7,'2021-05-18 07:10:31',1,0,'2021-05-21 16:46:29','2021-05-21 08:46:30'),(8,'钟汉良','123333','zhl@outlook.com',35,6,'2021-05-18 07:11:19',1,0,'2021-05-21 16:46:31','2021-05-21 08:46:31'),(9,'陈乔恩','$2a$10$meRc5DPOldNhSMJ3O61bAejjYrh9.0RCA4C7v5Vtg8ws7/Tci10hu','c@qq.com',23,2,'2021-05-21 11:34:48',1,0,NULL,'2021-05-21 11:34:48'),(1031,'宋佳鑫','$2a$10$enbn9aSc32x8o4a3mMdI0eMY2S1DIO6f70NIVhJGV0qix5JQSKaUy','songjiaxin@qq.com',18,2,'2021-05-23 01:49:58',1,1,'2021-05-23 10:18:02','2021-05-23 01:49:58'),(1032,'沈瑞渊','$2a$10$XCpj.stZ0YXnUjIyRRhzReVEX.XcPPr7fXnm0T3A0LjnWspRy6mcW','iosfgjksdkgkldsjfgl@qq.cp',22,2,'2021-05-23 02:07:37',1,0,'2021-05-23 10:27:10','2021-05-23 02:07:37'),(1091,'诺基亚','$2a$10$99f1zjjDOwsaeGcP8Qn4Bu10zPsDEA1FTHC7nFpKSX81bht3fEwh2','7231083332@qq.com',18,2,'2021-05-24 00:57:42',1,0,NULL,'2021-05-24 00:57:42'),(1092,'马♥','$2a$10$Er9G1wdLAv5CD9t0BGll8uaicn1TPuTKd1ALH88Yy9U/dLFN8wFSS','1460234233332@qq.com',18,7,'2021-05-24 03:39:34',1,0,'2021-05-24 16:22:58','2021-05-24 03:39:34'); /*!40000 ALTER TABLE `employee` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `employee_role` -- DROP TABLE IF EXISTS `employee_role`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `employee_role` ( `id` int NOT NULL AUTO_INCREMENT, `employeeId` int DEFAULT NULL, `roleId` int DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `employee_role_id_uindex` (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=166 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `employee_role` -- LOCK TABLES `employee_role` WRITE; /*!40000 ALTER TABLE `employee_role` DISABLE KEYS */; INSERT INTO `employee_role` VALUES (14,1001,4),(15,1001,5),(16,1001,8),(17,1001,9),(18,1002,4),(19,1002,5),(20,1002,8),(21,1002,9),(22,1000,4),(23,1000,5),(24,1000,8),(25,1000,9),(39,1,2),(40,1,4),(44,1010,8),(45,1011,5),(49,1013,5),(50,1013,4),(51,1016,5),(52,1016,4),(53,1015,8),(54,1015,5),(55,1015,4),(57,4,1),(58,4,2),(59,4,3),(60,NULL,1),(61,NULL,2),(62,NULL,3),(63,NULL,1),(64,NULL,1),(65,NULL,1),(66,NULL,1),(67,NULL,1),(68,1018,1),(69,1030,1),(70,1030,2),(71,1030,3),(76,1031,2),(77,1031,1),(78,1031,3),(79,1032,2),(84,1031,4),(85,1031,5),(86,1031,7),(87,1031,8),(88,1031,9),(89,1031,10),(90,1031,11),(91,1031,12),(92,1031,13),(93,1031,15),(94,1031,17),(95,1031,18),(98,2,7),(109,9,42),(110,7,37),(111,6,42),(112,8,42),(113,3,42),(118,1032,1),(160,1092,1),(161,1091,42),(165,5,42); /*!40000 ALTER TABLE `employee_role` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `permission` -- DROP TABLE IF EXISTS `permission`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `permission` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL COMMENT '权限名称', `expression` varchar(255) DEFAULT NULL COMMENT '资源地址', PRIMARY KEY (`id`), UNIQUE KEY `permission_id_uindex` (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `permission` -- LOCK TABLES `permission` WRITE; /*!40000 ALTER TABLE `permission` DISABLE KEYS */; INSERT INTO `permission` VALUES (1,'客户列表','customer:list'),(2,'客户状态修改','customer:changeStatus'),(3,'客户新增修改','customer:saveOrUpdate'),(5,'客户池列表','customerPool:list'),(6,'跟进历史列表','followHistory:list'),(7,'跟进历史新增/修改','followHistory:saveOrUpdate'),(8,'移交历史列表','transferHistory:list'),(9,'移交历史新增/修改','transferHistory:saveOrUpdate'),(10,'部门列表','department:list'),(11,'部门删除','department:delete'),(12,'部门新增/修改','department:addOrUpdate'),(13,'员工删除','employee:delete'),(14,'员工列表','employee:list'),(15,'员工编辑','employee:edit'),(16,'员工批量删除','employee:deleteMultiple'),(21,'客户角色管理角色新增','12'),(23,'角色列表','role:list'),(24,'角色删除','role:delete'),(27,'角色新增/修改','role:addOrUpdate'),(28,'数据字典列表','dictionaryContents:list'),(29,'数据列表添加/修改','dictionaryContents:addOrUpdate'),(30,'字典明细列表','dictionaryDetails:list'),(31,'字典明细新增/修改','dictionaryDetails:addOrUpdate'),(32,'客户管理列表','CM:list'),(33,'客户管理新增/修改','CM:addOrUpdate'),(34,'跟进历史新增/修改','CF:addOrUpdate'),(35,'客户移交新增','CH:add'),(36,'跟进历史列表','CH:list'),(37,'统计分析列表','statisticalAnalysis:list'); /*!40000 ALTER TABLE `permission` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `role` -- DROP TABLE IF EXISTS `role`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `role` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '角色Id', `name` varchar(255) DEFAULT NULL COMMENT '角色名称', `sn` varchar(255) DEFAULT NULL COMMENT '角色编号', PRIMARY KEY (`id`), UNIQUE KEY `name` (`name`) ) ENGINE=InnoDB AUTO_INCREMENT=49 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='角色表'; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `role` -- LOCK TABLES `role` WRITE; /*!40000 ALTER TABLE `role` DISABLE KEYS */; INSERT INTO `role` VALUES (1,'董事长','Chairman of the board'),(2,'ADMIN','System administrator'),(3,'主席','Chairman'),(4,'高级主席','Senior Chairman'),(5,'副主席','Vice Chairman'),(7,'总裁','Chairman'),(8,'会长','President'),(9,'高级总裁','Senior President'),(10,'高级副总裁','Senior Vice President'),(11,'副总裁','Vice president'),(12,'总经理','General manager'),(13,'副总经理','Deputy General Manager'),(22,'总监','Director'),(27,'经理','Manager'),(28,'高级经理','Senior Manager'),(36,'副经理','Deputy manager'),(37,'主任','Director'),(38,'高级主任','Senior Director'),(39,'副主任','Deputy director'),(40,'组长','Group leader'),(41,'副组长','Deputy head'),(42,'普通员工','Worker'),(45,'人事专员','Personnel Specialist'),(46,'市场专员','Marketing Specialist'),(47,'市场主管','Marketing Director'),(48,'销售主管','Sales Executive'); /*!40000 ALTER TABLE `role` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `role_permission` -- DROP TABLE IF EXISTS `role_permission`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `role_permission` ( `id` int NOT NULL AUTO_INCREMENT, `role_id` int NOT NULL COMMENT '角色id', `permission_id` int NOT NULL COMMENT '权限id', PRIMARY KEY (`id`), UNIQUE KEY `role_permission_id_uindex` (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=415 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='角色权限中间表'; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `role_permission` -- LOCK TABLES `role_permission` WRITE; /*!40000 ALTER TABLE `role_permission` DISABLE KEYS */; INSERT INTO `role_permission` VALUES (188,4,5),(193,4,1),(194,4,2),(195,4,3),(213,13,1),(214,13,2),(215,13,3),(216,13,5),(217,2,1),(218,2,2),(219,2,3),(220,2,5),(221,2,6),(222,2,7),(223,2,8),(224,2,9),(225,2,10),(226,2,11),(227,2,12),(228,2,13),(229,2,14),(230,2,15),(231,2,16),(256,36,9),(257,37,8),(258,1,1),(259,1,2),(260,1,3),(261,1,5),(262,1,6),(263,1,7),(264,1,8),(265,1,9),(266,1,10),(267,1,11),(268,1,12),(269,1,13),(270,1,14),(271,1,15),(272,1,16),(273,1,21),(274,2,21),(275,3,1),(276,3,2),(277,3,3),(278,3,5),(279,3,6),(280,3,7),(281,3,8),(282,3,9),(283,3,10),(284,3,11),(285,3,12),(286,3,13),(287,3,14),(288,3,15),(289,3,16),(290,3,21),(291,5,1),(292,5,2),(293,5,3),(294,5,5),(295,5,6),(296,4,16),(297,4,21),(298,4,6),(299,4,7),(300,4,8),(301,4,9),(302,4,10),(303,4,11),(304,4,12),(305,4,13),(306,4,14),(307,4,15),(308,5,16),(309,5,21),(310,5,7),(311,5,8),(312,5,9),(313,5,10),(314,5,11),(315,5,12),(316,5,13),(317,5,14),(318,5,15),(319,7,1),(320,7,2),(321,7,3),(322,7,5),(323,7,6),(324,7,7),(325,7,8),(326,7,9),(327,7,10),(328,7,11),(329,7,12),(330,7,13),(331,7,14),(332,7,15),(333,7,16),(334,7,21),(335,8,1),(336,8,2),(337,8,3),(338,8,5),(339,8,6),(340,8,7),(341,8,8),(342,8,9),(343,8,10),(344,8,11),(345,8,12),(346,8,13),(347,8,14),(348,8,15),(349,8,16),(350,8,21),(351,12,1),(352,12,2),(353,12,3),(354,12,5),(355,12,6),(356,12,7),(357,12,8),(358,12,9),(359,12,10),(360,12,11),(361,12,12),(362,12,13),(363,12,14),(364,12,15),(365,12,16),(366,12,21),(367,38,9),(368,39,3),(369,40,9),(370,40,8),(371,41,9),(375,45,13),(376,45,14),(377,45,16),(378,45,15),(379,42,1),(380,42,2),(381,42,3),(382,42,5),(383,42,7),(384,46,3),(385,46,1),(386,47,1),(387,47,2),(388,2,32),(389,2,33),(390,2,34),(391,2,35),(392,2,36),(393,2,37),(394,2,23),(395,2,24),(396,2,27),(397,2,28),(398,2,29),(399,2,30),(400,2,31),(401,1,32),(402,1,33),(403,1,34),(404,1,35),(405,1,36),(406,1,37),(407,1,23),(408,1,24),(409,1,27),(410,1,28),(411,1,29),(412,1,30),(413,1,31),(414,48,36); /*!40000 ALTER TABLE `role_permission` ENABLE KEYS */; UNLOCK TABLES; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; -- Dump completed on 2021-05-26 19:23:20