From cf5790229a863dcd8ee6b0537889345ca60388c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=83=AD=E5=BA=86=E6=B3=B0?= <12369755+htcloud1@user.noreply.gitee.com> Date: Sat, 15 Aug 2026 12:41:10 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E5=90=88=E5=B9=B6=20biz=5Fsupport?= =?UTF-8?q?=5Funit/biz=5Fexecution=5Funit/biz=5Fservice=5Forg=20=E4=B8=BA?= =?UTF-8?q?=20biz=5Forg,=20=E5=88=A0=E9=99=A4=202=20=E5=BC=A0=E5=AD=A4?= =?UTF-8?q?=E5=84=BF=E8=A1=A8,=20=E6=94=B9=20biz=5Fperson.work=5Funit=20?= =?UTF-8?q?=E2=86=92=20org=5Fid=20FK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主要改动: - SQL: biz_support_unit → biz_org, 加 org_type, 加 business_nature; 删 biz_execution_unit, biz_service_org - SQL: biz_project.support_unit_id/name + service_org_id/name → org_id/name/type - SQL: biz_meeting.support_unit_name → org_name - SQL: biz_person.work_unit → org_id (FK) - 后端: 新 BizOrg entity/mapper/service/controller - 后端: BizProject/BizMeeting 字段重命名 - 后端: 删 12 个 BizSupportUnit/BizExecutionUnit/BizServiceOrg Java 文件 - 后端: BizPersonImportVO.workUnit → orgName (导入时查 biz_org 取 org_id) - 后端: BizAuthController.registerExecutor 完整实现 (原 registerSupplier stub) - 前端: 新 admin/Orgs.vue + manager/Orgs.vue (原 SupportUnits) - 前端: RegisterExecutor.vue (原 RegisterSupplier, 单页 2 步) - 前端: sponsor/executor/manager 多个文件 workUnit → orgId/orgName 重命名 - 前端: 统一 supplier → executor, 业务命名 sponsor(赞助方) / executor(执行方=供应商) - 前端: 全工程 execution → executor (company type / role / person unit_type) --- .gitignore | 86 + proto/html/admin.html | 105 + .../html/components/account-info-expert.html | 837 +++++ proto/html/components/account-info.html | 247 ++ proto/html/components/account-manage.html | 229 ++ .../admin-execution-unit-manage.html | 171 + .../html/components/admin-expert-manage.html | 276 ++ proto/html/components/admin-home.html | 107 + .../html/components/admin-intent-support.html | 112 + .../html/components/admin-project-detail.html | 240 ++ .../html/components/admin-project-manage.html | 240 ++ .../components/admin-project-plan-detail.html | 113 + .../html/components/admin-project-public.html | 218 ++ .../components/admin-resource-manage.html | 162 + proto/html/components/admin-role-manage.html | 169 + .../html/components/admin-service-manage.html | 179 + proto/html/components/admin-settings.html | 171 + proto/html/components/admin-user-manage.html | 180 + proto/html/components/compliance-view-2.html | 86 + proto/html/components/compliance-view.html | 86 + proto/html/components/edit-meeting-labor.html | 79 + .../components/edit-meeting-materials.html | 79 + proto/html/components/edit-project-plan.html | 169 + proto/html/components/empty.html | 59 + proto/html/components/execution-intent.html | 321 ++ .../html/components/execution-unit-view.html | 136 + proto/html/components/executor-units.html | 375 ++ proto/html/components/expert-review.html | 851 +++++ proto/html/components/home.html | 374 ++ proto/html/components/labor-voucher.html | 78 + proto/html/components/leader-home.html | 125 + proto/html/components/leader-projects.html | 221 ++ .../components/meeting-detail-executor.html | 225 ++ .../components/meeting-detail-page-1.html | 300 ++ .../html/components/meeting-detail-page.html | 300 ++ proto/html/components/meeting-detail.html | 300 ++ proto/html/components/meeting-manage-1.html | 575 +++ proto/html/components/meeting-manage.html | 618 ++++ proto/html/components/meetings.html | 546 +++ proto/html/components/messages.html | 431 +++ proto/html/components/my-activities.html | 413 +++ proto/html/components/my-registrations.html | 317 ++ proto/html/components/new-edit.html | 88 + proto/html/components/new-expert.html | 137 + proto/html/components/new-meeting.html | 248 ++ proto/html/components/new-person-2.html | 118 + proto/html/components/new-person.html | 380 ++ proto/html/components/new-project.html | 645 ++++ proto/html/components/new-submission.html | 376 ++ proto/html/components/overview.html | 293 ++ proto/html/components/people-1-1.html | 74 + proto/html/components/people-1-2.html | 74 + proto/html/components/people.html | 434 +++ proto/html/components/project-assign-1.html | 602 ++++ proto/html/components/project-assign.html | 663 ++++ .../components/project-detail-compliance.html | 238 ++ .../components/project-detail-executor.html | 117 + proto/html/components/project-detail.html | 219 ++ proto/html/components/project-manage.html | 676 ++++ proto/html/components/project-plan.html | 644 ++++ proto/html/components/projects.html | 430 +++ proto/html/components/settle-labor.html | 84 + proto/html/components/settle-meeting.html | 84 + proto/html/components/sponsor-account.html | 40 + proto/html/components/sponsor-home.html | 73 + .../components/sponsor-meeting-detail.html | 34 + proto/html/components/sponsor-meetings.html | 307 ++ proto/html/components/sponsor-new-person.html | 118 + proto/html/components/sponsor-people.html | 146 + .../components/sponsor-project-detail.html | 47 + proto/html/components/sponsor-projects.html | 262 ++ proto/html/components/sponsor-view.html | 86 + .../submission-detail-compliance.html | 207 ++ proto/html/components/submission-detail.html | 207 ++ proto/html/components/submissions.html | 482 +++ proto/html/components/support-intent.html | 322 ++ .../components/support-unit-manage-1-1.html | 66 + .../html/components/support-unit-manage.html | 458 +++ .../html/components/support-unit-view-1.html | 49 + proto/html/components/support-unit-view.html | 49 + proto/html/components/view-person.html | 272 ++ proto/html/components/workbench.html | 67 + proto/html/doctor.html | 252 ++ proto/html/doubao_html_20260807_194253.html | 104 + proto/html/executor.html | 251 ++ proto/html/images/index/u1.png | Bin 0 -> 2477 bytes proto/html/images/index/u18_menu.png | Bin 0 -> 236 bytes proto/html/images/index/u215.png | Bin 0 -> 151 bytes proto/html/images/index/u217.png | Bin 0 -> 3164 bytes proto/html/images/index/u225.png | Bin 0 -> 11709 bytes proto/html/images/index/u229.png | Bin 0 -> 3284 bytes proto/html/images/index/u22_menu.png | Bin 0 -> 374 bytes proto/html/images/index/u24.png | Bin 0 -> 207 bytes proto/html/images/index/u267.png | Bin 0 -> 4632 bytes proto/html/images/index/u277.png | Bin 0 -> 167962 bytes proto/html/images/index/u279.png | Bin 0 -> 7274 bytes proto/html/images/index/u283.png | Bin 0 -> 7449 bytes proto/html/images/index/u287.png | Bin 0 -> 6610 bytes proto/html/images/index/u291.png | Bin 0 -> 6694 bytes proto/html/images/index/u295.png | Bin 0 -> 5937 bytes proto/html/images/index/u299.png | Bin 0 -> 5946 bytes proto/html/images/index/u3.png | Bin 0 -> 152 bytes proto/html/images/index/u35_menu.png | Bin 0 -> 712 bytes proto/html/images/index/u53_menu.png | Bin 0 -> 304 bytes proto/html/images/index/u71_menu.png | Bin 0 -> 237 bytes proto/html/images/index/u75_menu.png | Bin 0 -> 468 bytes proto/html/images/index/u77.png | Bin 0 -> 220 bytes proto/html/images/index/u79.png | Bin 0 -> 207 bytes proto/html/images/index/u83.png | Bin 0 -> 210 bytes proto/html/images/index/u90_menu.png | Bin 0 -> 509 bytes proto/html/images/index/学会系统_u20.png | Bin 0 -> 210 bytes proto/html/images/index/学会项目_u73.png | Bin 0 -> 210 bytes proto/html/images/公示详情/u552.jpg | Bin 0 -> 221629 bytes proto/html/images/年度项目计划/u320.png | Bin 0 -> 10805 bytes proto/html/images/年度项目计划/u333.png | Bin 0 -> 150 bytes proto/html/images/年度项目计划/u337.png | Bin 0 -> 2656 bytes proto/html/images/年度项目计划/u359.png | Bin 0 -> 1037 bytes proto/html/images/年度项目计划/u376.png | Bin 0 -> 9338 bytes proto/html/index2.html | 850 +++++ proto/html/leader.html | 248 ++ proto/html/manager.html | 256 ++ proto/html/sponsor.html | 60 + proto/html/专家注册.html | 579 ++++ proto/html/供应商注册.html | 518 +++ proto/html/公示详情.html | 739 ++++ proto/html/支持函详情.html | 755 ++++ proto/html/登录.html | 563 +++ proto/html/赞助方注册.html | 533 +++ proto/html/项目公示.html | 787 +++++ proto/html/首页.html | 1292 +++++++ ry-api/.github/FUNDING.yml | 1 + ry-api/.gitignore | 47 + ry-api/LICENSE | 20 + ry-api/README.md | 115 + ry-api/doc/若依环境使用手册.docx | Bin 0 -> 428152 bytes ry-api/pom.xml | 243 ++ ry-api/ruoyi-admin/pom.xml | 95 + .../main/java/com/ruoyi/RuoYiApplication.java | 30 + .../com/ruoyi/RuoYiServletInitializer.java | 18 + .../controller/common/CaptchaController.java | 94 + .../controller/common/CommonController.java | 162 + .../web/controller/common/OssController.java | 170 + .../controller/monitor/CacheController.java | 122 + .../controller/monitor/ServerController.java | 27 + .../monitor/SysLogininforController.java | 82 + .../monitor/SysOperlogController.java | 69 + .../monitor/SysUserOnlineController.java | 83 + .../system/SysConfigController.java | 133 + .../controller/system/SysDeptController.java | 147 + .../system/SysDictDataController.java | 121 + .../system/SysDictTypeController.java | 131 + .../controller/system/SysIndexController.java | 64 + .../controller/system/SysLoginController.java | 138 + .../controller/system/SysMenuController.java | 165 + .../system/SysNoticeController.java | 150 + .../controller/system/SysPostController.java | 129 + .../system/SysProfileController.java | 216 ++ .../system/SysRegisterController.java | 38 + .../controller/system/SysRoleController.java | 254 ++ .../controller/system/SysUserController.java | 256 ++ .../web/controller/tool/TestController.java | 175 + .../ruoyi/web/core/config/SwaggerConfig.java | 64 + .../META-INF/spring-devtools.properties | 1 + .../src/main/resources/application-druid.yml | 50 + .../src/main/resources/application.yml | 167 + .../ruoyi-admin/src/main/resources/banner.txt | 24 + .../main/resources/i18n/messages.properties | 38 + .../src/main/resources/logback.xml | 93 + .../main/resources/mybatis/mybatis-config.xml | 20 + ry-api/ruoyi-business/pom.xml | 40 + .../controller/BizAuthController.java | 130 + .../controller/BizDashboardController.java | 56 + .../controller/BizDictController.java | 142 + .../BizExecutionIntentController.java | 122 + .../controller/BizExecutorController.java | 74 + .../controller/BizExpertController.java | 74 + .../controller/BizInvitationController.java | 53 + .../controller/BizLaborVoucherController.java | 53 + .../controller/BizMeetingController.java | 53 + .../BizMeetingSettlementController.java | 53 + .../controller/BizMessageController.java | 128 + .../business/controller/BizOrgController.java | 53 + .../controller/BizPersonController.java | 172 + .../controller/BizProjectController.java | 214 ++ .../controller/BizProjectPlanController.java | 53 + .../BizProjectRatingController.java | 71 + .../controller/BizPublicController.java | 74 + .../controller/BizPublicityController.java | 66 + .../controller/BizRegisterController.java | 109 + .../controller/BizRegisterSmsController.java | 40 + .../controller/BizResourceController.java | 53 + .../controller/BizSubmissionController.java | 79 + .../BizSupportIntentController.java | 53 + .../BizSupportLetterController.java | 53 + .../business/controller/SmsController.java | 41 + .../ruoyi/business/domain/BizDepartment.java | 35 + .../ruoyi/business/domain/BizDoctorTitle.java | 35 + .../business/domain/BizExecutionIntent.java | 82 + .../com/ruoyi/business/domain/BizExpert.java | 137 + .../ruoyi/business/domain/BizInvitation.java | 78 + .../business/domain/BizLaborVoucher.java | 87 + .../com/ruoyi/business/domain/BizMeeting.java | 103 + .../business/domain/BizMeetingSettlement.java | 87 + .../com/ruoyi/business/domain/BizMessage.java | 78 + .../com/ruoyi/business/domain/BizOrg.java | 75 + .../com/ruoyi/business/domain/BizPerson.java | 108 + .../business/domain/BizPersonImportVO.java | 51 + .../com/ruoyi/business/domain/BizProject.java | 212 ++ .../business/domain/BizProjectAssign.java | 50 + .../ruoyi/business/domain/BizProjectPlan.java | 94 + .../business/domain/BizProjectRating.java | 100 + .../domain/BizProjectSponsorAssign.java | 42 + .../ruoyi/business/domain/BizPublicity.java | 61 + .../ruoyi/business/domain/BizResource.java | 69 + .../ruoyi/business/domain/BizSubmission.java | 86 + .../business/domain/BizSupportIntent.java | 78 + .../business/domain/BizSupportLetter.java | 76 + .../com/ruoyi/business/dto/SmsValidForm.java | 22 + .../business/enums/SubmissionStatus.java | 43 + .../business/mapper/BizDepartmentMapper.java | 22 + .../business/mapper/BizDoctorTitleMapper.java | 22 + .../mapper/BizExecutionIntentMapper.java | 16 + .../business/mapper/BizExpertMapper.java | 19 + .../business/mapper/BizInvitationMapper.java | 11 + .../mapper/BizLaborVoucherMapper.java | 16 + .../business/mapper/BizMeetingMapper.java | 16 + .../mapper/BizMeetingSettlementMapper.java | 16 + .../business/mapper/BizMessageMapper.java | 31 + .../ruoyi/business/mapper/BizOrgMapper.java | 12 + .../business/mapper/BizPersonMapper.java | 20 + .../mapper/BizProjectAssignMapper.java | 18 + .../business/mapper/BizProjectMapper.java | 18 + .../business/mapper/BizProjectPlanMapper.java | 16 + .../mapper/BizProjectRatingMapper.java | 18 + .../mapper/BizProjectSponsorAssignMapper.java | 11 + .../business/mapper/BizPublicityMapper.java | 15 + .../business/mapper/BizResourceMapper.java | 16 + .../business/mapper/BizSubmissionMapper.java | 16 + .../mapper/BizSupportIntentMapper.java | 16 + .../mapper/BizSupportLetterMapper.java | 11 + .../service/IBizDepartmentService.java | 21 + .../service/IBizDoctorTitleService.java | 21 + .../service/IBizExecutionIntentService.java | 17 + .../business/service/IBizExpertService.java | 20 + .../service/IBizInvitationService.java | 17 + .../service/IBizLaborVoucherService.java | 17 + .../business/service/IBizMeetingService.java | 17 + .../service/IBizMeetingSettlementService.java | 17 + .../business/service/IBizMessageService.java | 31 + .../business/service/IBizOrgService.java | 12 + .../business/service/IBizPersonService.java | 24 + .../service/IBizProjectAssignService.java | 18 + .../service/IBizProjectPlanService.java | 17 + .../service/IBizProjectRatingService.java | 19 + .../business/service/IBizProjectService.java | 19 + .../IBizProjectSponsorAssignService.java | 11 + .../service/IBizPublicityService.java | 17 + .../business/service/IBizResourceService.java | 17 + .../service/IBizSubmissionService.java | 17 + .../service/IBizSupportIntentService.java | 17 + .../service/IBizSupportLetterService.java | 17 + .../ruoyi/business/service/SysSmsService.java | 111 + .../impl/BizDepartmentServiceImpl.java | 43 + .../impl/BizDoctorTitleServiceImpl.java | 43 + .../impl/BizExecutionIntentServiceImpl.java | 33 + .../service/impl/BizExpertServiceImpl.java | 45 + .../impl/BizInvitationServiceImpl.java | 33 + .../impl/BizLaborVoucherServiceImpl.java | 33 + .../service/impl/BizMeetingServiceImpl.java | 33 + .../impl/BizMeetingSettlementServiceImpl.java | 33 + .../service/impl/BizMessageServiceImpl.java | 69 + .../service/impl/BizOrgServiceImpl.java | 37 + .../service/impl/BizPersonServiceImpl.java | 89 + .../impl/BizProjectAssignServiceImpl.java | 40 + .../impl/BizProjectPlanServiceImpl.java | 33 + .../impl/BizProjectRatingServiceImpl.java | 42 + .../service/impl/BizProjectServiceImpl.java | 36 + .../BizProjectSponsorAssignServiceImpl.java | 31 + .../service/impl/BizPublicityServiceImpl.java | 39 + .../service/impl/BizResourceServiceImpl.java | 33 + .../impl/BizSubmissionServiceImpl.java | 33 + .../impl/BizSupportIntentServiceImpl.java | 33 + .../impl/BizSupportLetterServiceImpl.java | 33 + .../ruoyi/business/sms/AliyunSmsSender.java | 89 + .../mapper/business/BizDepartmentMapper.xml | 83 + .../mapper/business/BizDoctorTitleMapper.xml | 83 + .../business/BizExecutionIntentMapper.xml | 92 + .../mapper/business/BizExpertMapper.xml | 218 ++ .../mapper/business/BizInvitationMapper.xml | 57 + .../mapper/business/BizLaborVoucherMapper.xml | 68 + .../mapper/business/BizMeetingMapper.xml | 76 + .../business/BizMeetingSettlementMapper.xml | 74 + .../mapper/business/BizMessageMapper.xml | 131 + .../mapper/business/BizOrgMapper.xml | 98 + .../mapper/business/BizPersonMapper.xml | 160 + .../business/BizProjectAssignMapper.xml | 104 + .../mapper/business/BizProjectMapper.xml | 256 ++ .../mapper/business/BizProjectPlanMapper.xml | 76 + .../business/BizProjectRatingMapper.xml | 95 + .../BizProjectSponsorAssignMapper.xml | 40 + .../mapper/business/BizPublicityMapper.xml | 110 + .../mapper/business/BizResourceMapper.xml | 63 + .../mapper/business/BizSubmissionMapper.xml | 103 + .../business/BizSupportIntentMapper.xml | 74 + .../business/BizSupportLetterMapper.xml | 54 + ry-api/ruoyi-common/pom.xml | 123 + .../ruoyi/common/annotation/Anonymous.java | 19 + .../ruoyi/common/annotation/DataScope.java | 43 + .../ruoyi/common/annotation/DataSource.java | 28 + .../com/ruoyi/common/annotation/Excel.java | 198 ++ .../com/ruoyi/common/annotation/Excels.java | 18 + .../java/com/ruoyi/common/annotation/Log.java | 51 + .../ruoyi/common/annotation/RateLimiter.java | 40 + .../ruoyi/common/annotation/RepeatSubmit.java | 31 + .../ruoyi/common/annotation/Sensitive.java | 24 + .../com/ruoyi/common/config/RuoYiConfig.java | 164 + .../serializer/SensitiveJsonSerializer.java | 77 + .../ruoyi/common/constant/CacheConstants.java | 44 + .../com/ruoyi/common/constant/Constants.java | 204 ++ .../ruoyi/common/constant/GenConstants.java | 120 + .../com/ruoyi/common/constant/HttpStatus.java | 94 + .../common/constant/ScheduleConstants.java | 50 + .../ruoyi/common/constant/UserConstants.java | 81 + .../core/controller/BaseController.java | 202 ++ .../ruoyi/common/core/domain/AjaxResult.java | 216 ++ .../ruoyi/common/core/domain/BaseEntity.java | 118 + .../java/com/ruoyi/common/core/domain/R.java | 115 + .../ruoyi/common/core/domain/TreeEntity.java | 79 + .../ruoyi/common/core/domain/TreeSelect.java | 93 + .../common/core/domain/entity/SysDept.java | 203 ++ .../core/domain/entity/SysDictData.java | 176 + .../core/domain/entity/SysDictType.java | 96 + .../common/core/domain/entity/SysMenu.java | 274 ++ .../common/core/domain/entity/SysRole.java | 241 ++ .../common/core/domain/entity/SysUser.java | 307 ++ .../common/core/domain/model/LoginBody.java | 69 + .../common/core/domain/model/LoginUser.java | 278 ++ .../core/domain/model/RegisterBody.java | 11 + .../ruoyi/common/core/page/PageDomain.java | 101 + .../ruoyi/common/core/page/TableDataInfo.java | 85 + .../ruoyi/common/core/page/TableSupport.java | 56 + .../ruoyi/common/core/redis/RedisCache.java | 268 ++ .../ruoyi/common/core/text/CharsetKit.java | 86 + .../com/ruoyi/common/core/text/Convert.java | 1018 ++++++ .../ruoyi/common/core/text/StrFormatter.java | 92 + .../ruoyi/common/enums/BusinessStatus.java | 20 + .../com/ruoyi/common/enums/BusinessType.java | 59 + .../ruoyi/common/enums/DataSourceType.java | 19 + .../ruoyi/common/enums/DesensitizedType.java | 59 + .../com/ruoyi/common/enums/HttpMethod.java | 36 + .../com/ruoyi/common/enums/LimitType.java | 20 + .../com/ruoyi/common/enums/OperatorType.java | 24 + .../com/ruoyi/common/enums/UserStatus.java | 30 + .../common/exception/DemoModeException.java | 15 + .../common/exception/GlobalException.java | 58 + .../common/exception/ServiceException.java | 74 + .../ruoyi/common/exception/UtilException.java | 26 + .../common/exception/base/BaseException.java | 97 + .../common/exception/file/FileException.java | 19 + .../FileNameLengthLimitExceededException.java | 16 + .../file/FileSizeLimitExceededException.java | 16 + .../exception/file/FileUploadException.java | 61 + .../file/InvalidExtensionException.java | 80 + .../common/exception/job/TaskException.java | 34 + .../exception/user/BlackListException.java | 16 + .../exception/user/CaptchaException.java | 16 + .../user/CaptchaExpireException.java | 16 + .../common/exception/user/UserException.java | 18 + .../user/UserNotExistsException.java | 16 + .../user/UserPasswordNotMatchException.java | 16 + ...UserPasswordRetryLimitExceedException.java | 16 + .../filter/PropertyPreExcludeFilter.java | 24 + .../ruoyi/common/filter/RefererFilter.java | 77 + .../ruoyi/common/filter/RepeatableFilter.java | 52 + .../filter/RepeatedlyRequestWrapper.java | 76 + .../com/ruoyi/common/filter/XssFilter.java | 75 + .../filter/XssHttpServletRequestWrapper.java | 111 + .../java/com/ruoyi/common/utils/Arith.java | 113 + .../com/ruoyi/common/utils/DateUtils.java | 192 ++ .../ruoyi/common/utils/DesensitizedUtil.java | 49 + .../com/ruoyi/common/utils/DictUtils.java | 217 ++ .../com/ruoyi/common/utils/ExceptionUtil.java | 39 + .../java/com/ruoyi/common/utils/LogUtils.java | 18 + .../com/ruoyi/common/utils/MessageUtils.java | 26 + .../com/ruoyi/common/utils/PageUtils.java | 35 + .../com/ruoyi/common/utils/SecurityUtils.java | 188 + .../com/ruoyi/common/utils/ServletUtils.java | 218 ++ .../com/ruoyi/common/utils/StringUtils.java | 921 +++++ .../java/com/ruoyi/common/utils/Threads.java | 99 + .../ruoyi/common/utils/bean/BeanUtils.java | 110 + .../common/utils/bean/BeanValidators.java | 24 + .../common/utils/file/FileTypeUtils.java | 76 + .../common/utils/file/FileUploadUtils.java | 260 ++ .../ruoyi/common/utils/file/FileUtils.java | 303 ++ .../ruoyi/common/utils/file/ImageUtils.java | 98 + .../common/utils/file/MimeTypeUtils.java | 59 + .../ruoyi/common/utils/html/EscapeUtil.java | 167 + .../ruoyi/common/utils/html/HTMLFilter.java | 570 +++ .../ruoyi/common/utils/http/HttpHelper.java | 55 + .../ruoyi/common/utils/http/HttpUtils.java | 293 ++ .../common/utils/http/UserAgentUtils.java | 254 ++ .../ruoyi/common/utils/id/IdGenerator.java | 83 + .../ruoyi/common/utils/id/SnowflakeId.java | 36 + .../ruoyi/common/utils/ip/AddressUtils.java | 56 + .../com/ruoyi/common/utils/ip/IpUtils.java | 382 ++ .../common/utils/poi/ExcelHandlerAdapter.java | 24 + .../ruoyi/common/utils/poi/ExcelSheet.java | 85 + .../com/ruoyi/common/utils/poi/ExcelUtil.java | 2067 +++++++++++ .../common/utils/reflect/ReflectUtils.java | 412 +++ .../com/ruoyi/common/utils/sign/Base64.java | 291 ++ .../com/ruoyi/common/utils/sign/Md5Utils.java | 67 + .../common/utils/spring/SpringUtils.java | 164 + .../com/ruoyi/common/utils/sql/SqlUtil.java | 71 + .../com/ruoyi/common/utils/uuid/IdUtils.java | 49 + .../java/com/ruoyi/common/utils/uuid/Seq.java | 86 + .../com/ruoyi/common/utils/uuid/UUID.java | 484 +++ .../main/java/com/ruoyi/common/xss/Xss.java | 27 + .../com/ruoyi/common/xss/XssValidator.java | 39 + ry-api/ruoyi-framework/pom.xml | 64 + .../framework/aspectj/DataScopeAspect.java | 160 + .../framework/aspectj/DataSourceAspect.java | 72 + .../ruoyi/framework/aspectj/LogAspect.java | 264 ++ .../framework/aspectj/RateLimiterAspect.java | 89 + .../framework/config/ApplicationConfig.java | 19 + .../ruoyi/framework/config/CaptchaConfig.java | 83 + .../ruoyi/framework/config/DruidConfig.java | 126 + .../config/FastJson2JsonRedisSerializer.java | 52 + .../ruoyi/framework/config/FilterConfig.java | 80 + .../ruoyi/framework/config/I18nConfig.java | 43 + .../framework/config/KaptchaTextCreator.java | 68 + .../ruoyi/framework/config/MyBatisConfig.java | 132 + .../ruoyi/framework/config/RedisConfig.java | 70 + .../framework/config/ResourcesConfig.java | 72 + .../framework/config/SecurityConfig.java | 86 + .../ruoyi/framework/config/ServerConfig.java | 32 + .../framework/config/ThreadPoolConfig.java | 63 + .../config/properties/DruidProperties.java | 89 + .../properties/PermitAllUrlProperties.java | 74 + .../datasource/DynamicDataSource.java | 26 + .../DynamicDataSourceContextHolder.java | 45 + .../interceptor/RepeatSubmitInterceptor.java | 56 + .../impl/SameUrlDataInterceptor.java | 110 + .../ruoyi/framework/manager/AsyncManager.java | 55 + .../framework/manager/ShutdownManager.java | 39 + .../manager/factory/AsyncFactory.java | 102 + .../context/AuthenticationContextHolder.java | 28 + .../context/PermissionContextHolder.java | 27 + .../filter/JwtAuthenticationTokenFilter.java | 44 + .../handle/AuthenticationEntryPointImpl.java | 34 + .../handle/LogoutSuccessHandlerImpl.java | 53 + .../ruoyi/framework/web/domain/Server.java | 240 ++ .../framework/web/domain/server/Cpu.java | 101 + .../framework/web/domain/server/Jvm.java | 130 + .../framework/web/domain/server/Mem.java | 61 + .../framework/web/domain/server/Sys.java | 84 + .../framework/web/domain/server/SysFile.java | 114 + .../web/exception/GlobalExceptionHandler.java | 201 ++ .../web/service/PermissionService.java | 159 + .../web/service/SysLoginService.java | 176 + .../web/service/SysPasswordService.java | 86 + .../web/service/SysPermissionService.java | 89 + .../web/service/SysRegisterService.java | 117 + .../framework/web/service/TokenService.java | 270 ++ .../web/service/UserDetailsServiceImpl.java | 66 + ry-api/ruoyi-generator/pom.xml | 40 + .../com/ruoyi/generator/config/GenConfig.java | 87 + .../generator/controller/GenController.java | 264 ++ .../com/ruoyi/generator/domain/GenTable.java | 411 +++ .../generator/domain/GenTableColumn.java | 373 ++ .../mapper/GenTableColumnMapper.java | 60 + .../generator/mapper/GenTableMapper.java | 91 + .../service/GenTableColumnServiceImpl.java | 68 + .../service/GenTableServiceImpl.java | 564 +++ .../service/IGenTableColumnService.java | 44 + .../generator/service/IGenTableService.java | 131 + .../com/ruoyi/generator/util/GenUtils.java | 257 ++ .../generator/util/VelocityInitializer.java | 34 + .../ruoyi/generator/util/VelocityUtils.java | 492 +++ .../src/main/resources/generator.yml | 12 + .../mapper/generator/GenTableColumnMapper.xml | 128 + .../mapper/generator/GenTableMapper.xml | 214 ++ .../main/resources/vm/java/controller.java.vm | 115 + .../src/main/resources/vm/java/domain.java.vm | 105 + .../src/main/resources/vm/java/mapper.java.vm | 91 + .../main/resources/vm/java/service.java.vm | 61 + .../resources/vm/java/serviceImpl.java.vm | 169 + .../main/resources/vm/java/sub-domain.java.vm | 76 + .../src/main/resources/vm/js/api.js.vm | 44 + .../src/main/resources/vm/sql/sql.vm | 22 + .../src/main/resources/vm/ts/api.ts.vm | 51 + .../src/main/resources/vm/ts/index.ts.vm | 9 + .../src/main/resources/vm/ts/type.ts.vm | 51 + .../main/resources/vm/vue/index-tree.vue.vm | 565 +++ .../src/main/resources/vm/vue/index.vue.vm | 660 ++++ .../resources/vm/vue/v3/index-tree.vue.vm | 526 +++ .../src/main/resources/vm/vue/v3/index.vue.vm | 639 ++++ .../src/main/resources/vm/vue/v3/view.vue.vm | 83 + .../resources/vm/vue/v3ts/index-tree.vue.vm | 528 +++ .../main/resources/vm/vue/v3ts/index.vue.vm | 644 ++++ .../main/resources/vm/vue/v3ts/view.vue.vm | 84 + .../src/main/resources/vm/vue/view.vue.vm | 86 + .../src/main/resources/vm/xml/mapper.xml.vm | 140 + ry-api/ruoyi-quartz/pom.xml | 34 + .../ruoyi/quartz/config/ScheduleConfig.java | 57 + .../quartz/controller/SysJobController.java | 185 + .../controller/SysJobLogController.java | 92 + .../java/com/ruoyi/quartz/domain/SysJob.java | 171 + .../com/ruoyi/quartz/domain/SysJobLog.java | 158 + .../ruoyi/quartz/mapper/SysJobLogMapper.java | 64 + .../com/ruoyi/quartz/mapper/SysJobMapper.java | 67 + .../quartz/service/ISysJobLogService.java | 56 + .../ruoyi/quartz/service/ISysJobService.java | 102 + .../service/impl/SysJobLogServiceImpl.java | 87 + .../service/impl/SysJobServiceImpl.java | 261 ++ .../java/com/ruoyi/quartz/task/RyTask.java | 28 + .../ruoyi/quartz/util/AbstractQuartzJob.java | 106 + .../java/com/ruoyi/quartz/util/CronUtils.java | 63 + .../com/ruoyi/quartz/util/JobInvokeUtil.java | 182 + .../QuartzDisallowConcurrentExecution.java | 21 + .../ruoyi/quartz/util/QuartzJobExecution.java | 19 + .../com/ruoyi/quartz/util/ScheduleUtils.java | 141 + .../mapper/quartz/SysJobLogMapper.xml | 100 + .../resources/mapper/quartz/SysJobMapper.xml | 111 + ry-api/ruoyi-system/pom.xml | 28 + .../com/ruoyi/system/domain/SysCache.java | 81 + .../com/ruoyi/system/domain/SysConfig.java | 111 + .../ruoyi/system/domain/SysLogininfor.java | 144 + .../com/ruoyi/system/domain/SysNotice.java | 117 + .../ruoyi/system/domain/SysNoticeRead.java | 76 + .../com/ruoyi/system/domain/SysOperLog.java | 269 ++ .../java/com/ruoyi/system/domain/SysPost.java | 124 + .../com/ruoyi/system/domain/SysRoleDept.java | 46 + .../com/ruoyi/system/domain/SysRoleMenu.java | 46 + .../ruoyi/system/domain/SysUserOnline.java | 113 + .../com/ruoyi/system/domain/SysUserPost.java | 46 + .../com/ruoyi/system/domain/SysUserRole.java | 46 + .../com/ruoyi/system/domain/vo/MetaVo.java | 106 + .../com/ruoyi/system/domain/vo/RouterVo.java | 148 + .../ruoyi/system/mapper/SysConfigMapper.java | 76 + .../ruoyi/system/mapper/SysDeptMapper.java | 125 + .../system/mapper/SysDictDataMapper.java | 95 + .../system/mapper/SysDictTypeMapper.java | 83 + .../system/mapper/SysLogininforMapper.java | 42 + .../ruoyi/system/mapper/SysMenuMapper.java | 141 + .../ruoyi/system/mapper/SysNoticeMapper.java | 60 + .../system/mapper/SysNoticeReadMapper.java | 75 + .../ruoyi/system/mapper/SysOperLogMapper.java | 48 + .../ruoyi/system/mapper/SysPostMapper.java | 99 + .../system/mapper/SysRoleDeptMapper.java | 44 + .../ruoyi/system/mapper/SysRoleMapper.java | 107 + .../system/mapper/SysRoleMenuMapper.java | 44 + .../ruoyi/system/mapper/SysUserMapper.java | 148 + .../system/mapper/SysUserPostMapper.java | 44 + .../system/mapper/SysUserRoleMapper.java | 62 + .../system/service/ISysConfigService.java | 89 + .../ruoyi/system/service/ISysDeptService.java | 132 + .../system/service/ISysDictDataService.java | 60 + .../system/service/ISysDictTypeService.java | 98 + .../system/service/ISysLogininforService.java | 40 + .../ruoyi/system/service/ISysMenuService.java | 160 + .../system/service/ISysNoticeReadService.java | 62 + .../system/service/ISysNoticeService.java | 60 + .../system/service/ISysOperLogService.java | 48 + .../ruoyi/system/service/ISysPostService.java | 99 + .../ruoyi/system/service/ISysRoleService.java | 173 + .../system/service/ISysUserOnlineService.java | 48 + .../ruoyi/system/service/ISysUserService.java | 234 ++ .../service/impl/SysConfigServiceImpl.java | 229 ++ .../service/impl/SysDeptServiceImpl.java | 364 ++ .../service/impl/SysDictDataServiceImpl.java | 111 + .../service/impl/SysDictTypeServiceImpl.java | 223 ++ .../impl/SysLogininforServiceImpl.java | 65 + .../service/impl/SysMenuServiceImpl.java | 618 ++++ .../impl/SysNoticeReadServiceImpl.java | 83 + .../service/impl/SysNoticeServiceImpl.java | 92 + .../service/impl/SysOperLogServiceImpl.java | 76 + .../service/impl/SysPostServiceImpl.java | 178 + .../service/impl/SysRoleServiceImpl.java | 426 +++ .../impl/SysUserOnlineServiceImpl.java | 96 + .../service/impl/SysUserServiceImpl.java | 589 ++++ .../mapper/system/SysConfigMapper.xml | 117 + .../resources/mapper/system/SysDeptMapper.xml | 163 + .../mapper/system/SysDictDataMapper.xml | 124 + .../mapper/system/SysDictTypeMapper.xml | 105 + .../mapper/system/SysLogininforMapper.xml | 57 + .../resources/mapper/system/SysMenuMapper.xml | 215 ++ .../mapper/system/SysNoticeMapper.xml | 90 + .../mapper/system/SysNoticeReadMapper.xml | 88 + .../mapper/system/SysOperLogMapper.xml | 87 + .../resources/mapper/system/SysPostMapper.xml | 122 + .../mapper/system/SysRoleDeptMapper.xml | 34 + .../resources/mapper/system/SysRoleMapper.xml | 152 + .../mapper/system/SysRoleMenuMapper.xml | 34 + .../resources/mapper/system/SysUserMapper.xml | 237 ++ .../mapper/system/SysUserPostMapper.xml | 34 + .../mapper/system/SysUserRoleMapper.xml | 44 + ry-api/ry.bat | 67 + ry-api/ry.sh | 86 + ry-api/sql/quartz.sql | 174 + ry-api/sql/ry_20260417.sql | 725 ++++ ry-vue3/index.html | 12 + ry-vue3/package-lock.json | 3068 +++++++++++++++++ ry-vue3/package.json | 26 + ry-vue3/public/images/id-back.png | Bin 0 -> 3915 bytes ry-vue3/public/images/id-front.png | Bin 0 -> 3713 bytes ry-vue3/src/App.vue | 6 + ry-vue3/src/api/auth.js | 21 + ry-vue3/src/api/business/person.js | 19 + ry-vue3/src/api/business/project.js | 32 + ry-vue3/src/api/dict.js | 54 + ry-vue3/src/api/public.js | 93 + ry-vue3/src/api/system.js | 22 + ry-vue3/src/assets/main.scss | 7 + ry-vue3/src/assets/theme.js | 116 + ry-vue3/src/assets/theme.scss | 39 + ry-vue3/src/components/AreaCascader.vue | 152 + ry-vue3/src/components/DictSelect.vue | 92 + ry-vue3/src/components/DoctorDeptSelect.vue | 55 + ry-vue3/src/components/DoctorTitleSelect.vue | 55 + ry-vue3/src/components/IdCardUploader.vue | 140 + ry-vue3/src/components/OssImageUploader.vue | 120 + ry-vue3/src/components/Preview.vue | 72 + ry-vue3/src/layout/AdminLayout.vue | 133 + ry-vue3/src/layout/PortalLayout.vue | 13 + ry-vue3/src/main.js | 19 + ry-vue3/src/router/index.js | 115 + ry-vue3/src/store/user.js | 38 + ry-vue3/src/utils/areaData.json | 1 + ry-vue3/src/utils/oss.js | 72 + ry-vue3/src/utils/request.js | 46 + ry-vue3/src/utils/snowflake.js | 65 + ry-vue3/src/utils/submissionStatus.js | 41 + ry-vue3/src/views/admin/Account.vue | 113 + ry-vue3/src/views/admin/Department.vue | 172 + ry-vue3/src/views/admin/Experts.vue | 460 +++ ry-vue3/src/views/admin/Meetings.vue | 90 + ry-vue3/src/views/admin/Orgs.vue | 262 ++ ry-vue3/src/views/admin/Projects.vue | 104 + ry-vue3/src/views/admin/Roles.vue | 90 + ry-vue3/src/views/admin/Title.vue | 172 + ry-vue3/src/views/admin/Users.vue | 182 + ry-vue3/src/views/admin/Workbench.vue | 112 + ry-vue3/src/views/auth/Login.vue | 689 ++++ ry-vue3/src/views/auth/RegisterExecutor.vue | 207 ++ ry-vue3/src/views/auth/RegisterExpert.vue | 199 ++ ry-vue3/src/views/auth/RegisterSponsor.vue | 36 + ry-vue3/src/views/doctor/Account.vue | 549 +++ ry-vue3/src/views/doctor/Home.vue | 177 + ry-vue3/src/views/doctor/Meetings.vue | 241 ++ ry-vue3/src/views/doctor/Messages.vue | 106 + ry-vue3/src/views/doctor/Projects.vue | 150 + ry-vue3/src/views/doctor/SubmissionDetail.vue | 193 ++ ry-vue3/src/views/doctor/SubmissionNew.vue | 280 ++ ry-vue3/src/views/doctor/Submissions.vue | 177 + ry-vue3/src/views/executor/Account.vue | 76 + ry-vue3/src/views/executor/Labor.vue | 215 ++ ry-vue3/src/views/executor/Meetings.vue | 197 ++ ry-vue3/src/views/executor/Overview.vue | 84 + ry-vue3/src/views/executor/People.vue | 198 ++ ry-vue3/src/views/executor/Projects.vue | 275 ++ ry-vue3/src/views/leader/Account.vue | 80 + ry-vue3/src/views/leader/Home.vue | 154 + ry-vue3/src/views/leader/Meetings.vue | 287 ++ ry-vue3/src/views/leader/Projects.vue | 351 ++ ry-vue3/src/views/manager/Accounts.vue | 100 + ry-vue3/src/views/manager/ExecIntent.vue | 69 + ry-vue3/src/views/manager/Experts.vue | 446 +++ .../views/manager/ManagerProjectDetail.vue | 439 +++ ry-vue3/src/views/manager/MeetingNew.vue | 112 + ry-vue3/src/views/manager/Meetings.vue | 319 ++ ry-vue3/src/views/manager/Orgs.vue | 148 + ry-vue3/src/views/manager/Plans.vue | 312 ++ ry-vue3/src/views/manager/Projects.vue | 834 +++++ ry-vue3/src/views/manager/ProjectsNew.vue | 504 +++ ry-vue3/src/views/manager/SupportIntent.vue | 83 + ry-vue3/src/views/manager/Workbench.vue | 131 + ry-vue3/src/views/portal/Home.vue | 859 +++++ ry-vue3/src/views/portal/Invitation.vue | 118 + ry-vue3/src/views/portal/Publicity.vue | 713 ++++ ry-vue3/src/views/portal/PublicityDetail.vue | 1211 +++++++ ry-vue3/src/views/portal/SupportLetter.vue | 106 + ry-vue3/src/views/sponsor/Account.vue | 502 +++ ry-vue3/src/views/sponsor/Home.vue | 156 + ry-vue3/src/views/sponsor/Meetings.vue | 317 ++ ry-vue3/src/views/sponsor/NewPerson.vue | 261 ++ ry-vue3/src/views/sponsor/PersonDetail.vue | 149 + ry-vue3/src/views/sponsor/Projects.vue | 433 +++ ry-vue3/src/views/sponsor/SponsorPeople.vue | 308 ++ ry-vue3/src/views/sponsor/SponsorProjects.vue | 393 +++ ry-vue3/vite.config.js | 28 + scripts/redis-clear-config.sh | 16 + sql/biz_data.sql | 220 ++ sql/biz_schema.sql | 507 +++ test_20rounds.py | 348 ++ 694 files changed, 102090 insertions(+) create mode 100644 .gitignore create mode 100644 proto/html/admin.html create mode 100644 proto/html/components/account-info-expert.html create mode 100644 proto/html/components/account-info.html create mode 100644 proto/html/components/account-manage.html create mode 100644 proto/html/components/admin-execution-unit-manage.html create mode 100644 proto/html/components/admin-expert-manage.html create mode 100644 proto/html/components/admin-home.html create mode 100644 proto/html/components/admin-intent-support.html create mode 100644 proto/html/components/admin-project-detail.html create mode 100644 proto/html/components/admin-project-manage.html create mode 100644 proto/html/components/admin-project-plan-detail.html create mode 100644 proto/html/components/admin-project-public.html create mode 100644 proto/html/components/admin-resource-manage.html create mode 100644 proto/html/components/admin-role-manage.html create mode 100644 proto/html/components/admin-service-manage.html create mode 100644 proto/html/components/admin-settings.html create mode 100644 proto/html/components/admin-user-manage.html create mode 100644 proto/html/components/compliance-view-2.html create mode 100644 proto/html/components/compliance-view.html create mode 100644 proto/html/components/edit-meeting-labor.html create mode 100644 proto/html/components/edit-meeting-materials.html create mode 100644 proto/html/components/edit-project-plan.html create mode 100644 proto/html/components/empty.html create mode 100644 proto/html/components/execution-intent.html create mode 100644 proto/html/components/execution-unit-view.html create mode 100644 proto/html/components/executor-units.html create mode 100644 proto/html/components/expert-review.html create mode 100644 proto/html/components/home.html create mode 100644 proto/html/components/labor-voucher.html create mode 100644 proto/html/components/leader-home.html create mode 100644 proto/html/components/leader-projects.html create mode 100644 proto/html/components/meeting-detail-executor.html create mode 100644 proto/html/components/meeting-detail-page-1.html create mode 100644 proto/html/components/meeting-detail-page.html create mode 100644 proto/html/components/meeting-detail.html create mode 100644 proto/html/components/meeting-manage-1.html create mode 100644 proto/html/components/meeting-manage.html create mode 100644 proto/html/components/meetings.html create mode 100644 proto/html/components/messages.html create mode 100644 proto/html/components/my-activities.html create mode 100644 proto/html/components/my-registrations.html create mode 100644 proto/html/components/new-edit.html create mode 100644 proto/html/components/new-expert.html create mode 100644 proto/html/components/new-meeting.html create mode 100644 proto/html/components/new-person-2.html create mode 100644 proto/html/components/new-person.html create mode 100644 proto/html/components/new-project.html create mode 100644 proto/html/components/new-submission.html create mode 100644 proto/html/components/overview.html create mode 100644 proto/html/components/people-1-1.html create mode 100644 proto/html/components/people-1-2.html create mode 100644 proto/html/components/people.html create mode 100644 proto/html/components/project-assign-1.html create mode 100644 proto/html/components/project-assign.html create mode 100644 proto/html/components/project-detail-compliance.html create mode 100644 proto/html/components/project-detail-executor.html create mode 100644 proto/html/components/project-detail.html create mode 100644 proto/html/components/project-manage.html create mode 100644 proto/html/components/project-plan.html create mode 100644 proto/html/components/projects.html create mode 100644 proto/html/components/settle-labor.html create mode 100644 proto/html/components/settle-meeting.html create mode 100644 proto/html/components/sponsor-account.html create mode 100644 proto/html/components/sponsor-home.html create mode 100644 proto/html/components/sponsor-meeting-detail.html create mode 100644 proto/html/components/sponsor-meetings.html create mode 100644 proto/html/components/sponsor-new-person.html create mode 100644 proto/html/components/sponsor-people.html create mode 100644 proto/html/components/sponsor-project-detail.html create mode 100644 proto/html/components/sponsor-projects.html create mode 100644 proto/html/components/sponsor-view.html create mode 100644 proto/html/components/submission-detail-compliance.html create mode 100644 proto/html/components/submission-detail.html create mode 100644 proto/html/components/submissions.html create mode 100644 proto/html/components/support-intent.html create mode 100644 proto/html/components/support-unit-manage-1-1.html create mode 100644 proto/html/components/support-unit-manage.html create mode 100644 proto/html/components/support-unit-view-1.html create mode 100644 proto/html/components/support-unit-view.html create mode 100644 proto/html/components/view-person.html create mode 100644 proto/html/components/workbench.html create mode 100644 proto/html/doctor.html create mode 100644 proto/html/doubao_html_20260807_194253.html create mode 100644 proto/html/executor.html create mode 100644 proto/html/images/index/u1.png create mode 100644 proto/html/images/index/u18_menu.png create mode 100644 proto/html/images/index/u215.png create mode 100644 proto/html/images/index/u217.png create mode 100644 proto/html/images/index/u225.png create mode 100644 proto/html/images/index/u229.png create mode 100644 proto/html/images/index/u22_menu.png create mode 100644 proto/html/images/index/u24.png create mode 100644 proto/html/images/index/u267.png create mode 100644 proto/html/images/index/u277.png create mode 100644 proto/html/images/index/u279.png create mode 100644 proto/html/images/index/u283.png create mode 100644 proto/html/images/index/u287.png create mode 100644 proto/html/images/index/u291.png create mode 100644 proto/html/images/index/u295.png create mode 100644 proto/html/images/index/u299.png create mode 100644 proto/html/images/index/u3.png create mode 100644 proto/html/images/index/u35_menu.png create mode 100644 proto/html/images/index/u53_menu.png create mode 100644 proto/html/images/index/u71_menu.png create mode 100644 proto/html/images/index/u75_menu.png create mode 100644 proto/html/images/index/u77.png create mode 100644 proto/html/images/index/u79.png create mode 100644 proto/html/images/index/u83.png create mode 100644 proto/html/images/index/u90_menu.png create mode 100644 proto/html/images/index/学会系统_u20.png create mode 100644 proto/html/images/index/学会项目_u73.png create mode 100644 proto/html/images/公示详情/u552.jpg create mode 100644 proto/html/images/年度项目计划/u320.png create mode 100644 proto/html/images/年度项目计划/u333.png create mode 100644 proto/html/images/年度项目计划/u337.png create mode 100644 proto/html/images/年度项目计划/u359.png create mode 100644 proto/html/images/年度项目计划/u376.png create mode 100644 proto/html/index2.html create mode 100644 proto/html/leader.html create mode 100644 proto/html/manager.html create mode 100644 proto/html/sponsor.html create mode 100644 proto/html/专家注册.html create mode 100644 proto/html/供应商注册.html create mode 100644 proto/html/公示详情.html create mode 100644 proto/html/支持函详情.html create mode 100644 proto/html/登录.html create mode 100644 proto/html/赞助方注册.html create mode 100644 proto/html/项目公示.html create mode 100644 proto/html/首页.html create mode 100644 ry-api/.github/FUNDING.yml create mode 100644 ry-api/.gitignore create mode 100644 ry-api/LICENSE create mode 100644 ry-api/README.md create mode 100644 ry-api/doc/若依环境使用手册.docx create mode 100644 ry-api/pom.xml create mode 100644 ry-api/ruoyi-admin/pom.xml create mode 100644 ry-api/ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java create mode 100644 ry-api/ruoyi-admin/src/main/java/com/ruoyi/RuoYiServletInitializer.java create mode 100644 ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CaptchaController.java create mode 100644 ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java create mode 100644 ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/OssController.java create mode 100644 ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/CacheController.java create mode 100644 ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/ServerController.java create mode 100644 ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysLogininforController.java create mode 100644 ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysOperlogController.java create mode 100644 ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysUserOnlineController.java create mode 100644 ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysConfigController.java create mode 100644 ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDeptController.java create mode 100644 ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDictDataController.java create mode 100644 ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDictTypeController.java create mode 100644 ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysIndexController.java create mode 100644 ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java create mode 100644 ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysMenuController.java create mode 100644 ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysNoticeController.java create mode 100644 ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysPostController.java create mode 100644 ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java create mode 100644 ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRegisterController.java create mode 100644 ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRoleController.java create mode 100644 ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java create mode 100644 ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/TestController.java create mode 100644 ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/core/config/SwaggerConfig.java create mode 100644 ry-api/ruoyi-admin/src/main/resources/META-INF/spring-devtools.properties create mode 100644 ry-api/ruoyi-admin/src/main/resources/application-druid.yml create mode 100644 ry-api/ruoyi-admin/src/main/resources/application.yml create mode 100644 ry-api/ruoyi-admin/src/main/resources/banner.txt create mode 100644 ry-api/ruoyi-admin/src/main/resources/i18n/messages.properties create mode 100644 ry-api/ruoyi-admin/src/main/resources/logback.xml create mode 100644 ry-api/ruoyi-admin/src/main/resources/mybatis/mybatis-config.xml create mode 100644 ry-api/ruoyi-business/pom.xml create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizAuthController.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizDashboardController.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizDictController.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizExecutionIntentController.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizExecutorController.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizExpertController.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizInvitationController.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizLaborVoucherController.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingController.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingSettlementController.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMessageController.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizOrgController.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPersonController.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizProjectController.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizProjectPlanController.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizProjectRatingController.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPublicController.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPublicityController.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizRegisterController.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizRegisterSmsController.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizResourceController.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizSubmissionController.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizSupportIntentController.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizSupportLetterController.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/SmsController.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizDepartment.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizDoctorTitle.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizExecutionIntent.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizExpert.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizInvitation.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizLaborVoucher.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeeting.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeetingSettlement.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMessage.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizOrg.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizPerson.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizPersonImportVO.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProject.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectAssign.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectPlan.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectRating.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectSponsorAssign.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizPublicity.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizResource.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizSubmission.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizSupportIntent.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizSupportLetter.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/dto/SmsValidForm.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/enums/SubmissionStatus.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizDepartmentMapper.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizDoctorTitleMapper.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizExecutionIntentMapper.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizExpertMapper.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizInvitationMapper.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizLaborVoucherMapper.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingMapper.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingSettlementMapper.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMessageMapper.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizOrgMapper.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizPersonMapper.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizProjectAssignMapper.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizProjectMapper.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizProjectPlanMapper.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizProjectRatingMapper.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizProjectSponsorAssignMapper.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizPublicityMapper.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizResourceMapper.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizSubmissionMapper.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizSupportIntentMapper.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizSupportLetterMapper.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizDepartmentService.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizDoctorTitleService.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizExecutionIntentService.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizExpertService.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizInvitationService.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizLaborVoucherService.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingService.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingSettlementService.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMessageService.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizOrgService.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizPersonService.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizProjectAssignService.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizProjectPlanService.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizProjectRatingService.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizProjectService.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizProjectSponsorAssignService.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizPublicityService.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizResourceService.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizSubmissionService.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizSupportIntentService.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizSupportLetterService.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/SysSmsService.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizDepartmentServiceImpl.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizDoctorTitleServiceImpl.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizExecutionIntentServiceImpl.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizExpertServiceImpl.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizInvitationServiceImpl.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizLaborVoucherServiceImpl.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingServiceImpl.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingSettlementServiceImpl.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMessageServiceImpl.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizOrgServiceImpl.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizPersonServiceImpl.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectAssignServiceImpl.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectPlanServiceImpl.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectRatingServiceImpl.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectServiceImpl.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectSponsorAssignServiceImpl.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizPublicityServiceImpl.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizResourceServiceImpl.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizSubmissionServiceImpl.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizSupportIntentServiceImpl.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizSupportLetterServiceImpl.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/sms/AliyunSmsSender.java create mode 100644 ry-api/ruoyi-business/src/main/resources/mapper/business/BizDepartmentMapper.xml create mode 100644 ry-api/ruoyi-business/src/main/resources/mapper/business/BizDoctorTitleMapper.xml create mode 100644 ry-api/ruoyi-business/src/main/resources/mapper/business/BizExecutionIntentMapper.xml create mode 100644 ry-api/ruoyi-business/src/main/resources/mapper/business/BizExpertMapper.xml create mode 100644 ry-api/ruoyi-business/src/main/resources/mapper/business/BizInvitationMapper.xml create mode 100644 ry-api/ruoyi-business/src/main/resources/mapper/business/BizLaborVoucherMapper.xml create mode 100644 ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingMapper.xml create mode 100644 ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingSettlementMapper.xml create mode 100644 ry-api/ruoyi-business/src/main/resources/mapper/business/BizMessageMapper.xml create mode 100644 ry-api/ruoyi-business/src/main/resources/mapper/business/BizOrgMapper.xml create mode 100644 ry-api/ruoyi-business/src/main/resources/mapper/business/BizPersonMapper.xml create mode 100644 ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectAssignMapper.xml create mode 100644 ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectMapper.xml create mode 100644 ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectPlanMapper.xml create mode 100644 ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectRatingMapper.xml create mode 100644 ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectSponsorAssignMapper.xml create mode 100644 ry-api/ruoyi-business/src/main/resources/mapper/business/BizPublicityMapper.xml create mode 100644 ry-api/ruoyi-business/src/main/resources/mapper/business/BizResourceMapper.xml create mode 100644 ry-api/ruoyi-business/src/main/resources/mapper/business/BizSubmissionMapper.xml create mode 100644 ry-api/ruoyi-business/src/main/resources/mapper/business/BizSupportIntentMapper.xml create mode 100644 ry-api/ruoyi-business/src/main/resources/mapper/business/BizSupportLetterMapper.xml create mode 100644 ry-api/ruoyi-common/pom.xml create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Anonymous.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataScope.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataSource.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Excel.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Excels.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Log.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/annotation/RateLimiter.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/annotation/RepeatSubmit.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Sensitive.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/config/RuoYiConfig.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/config/serializer/SensitiveJsonSerializer.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/constant/Constants.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/constant/GenConstants.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/constant/HttpStatus.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/constant/ScheduleConstants.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/constant/UserConstants.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/controller/BaseController.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/AjaxResult.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/BaseEntity.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/R.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/TreeEntity.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/TreeSelect.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDept.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictData.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictType.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysMenu.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysRole.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginBody.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUser.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/RegisterBody.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/page/PageDomain.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/page/TableDataInfo.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/page/TableSupport.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/redis/RedisCache.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/text/CharsetKit.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/text/Convert.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/text/StrFormatter.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/BusinessStatus.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/BusinessType.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/DataSourceType.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/DesensitizedType.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/HttpMethod.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/LimitType.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/OperatorType.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/UserStatus.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/DemoModeException.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/GlobalException.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/ServiceException.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/UtilException.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/base/BaseException.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileException.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileNameLengthLimitExceededException.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileSizeLimitExceededException.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileUploadException.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/InvalidExtensionException.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/job/TaskException.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/BlackListException.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/CaptchaException.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/CaptchaExpireException.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserException.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserNotExistsException.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserPasswordNotMatchException.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserPasswordRetryLimitExceedException.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/filter/PropertyPreExcludeFilter.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/filter/RefererFilter.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/filter/RepeatableFilter.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/filter/RepeatedlyRequestWrapper.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/filter/XssFilter.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/filter/XssHttpServletRequestWrapper.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/Arith.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/DateUtils.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/DesensitizedUtil.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/DictUtils.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/ExceptionUtil.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/LogUtils.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/MessageUtils.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/PageUtils.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/SecurityUtils.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/ServletUtils.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/StringUtils.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/Threads.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/bean/BeanUtils.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/bean/BeanValidators.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileTypeUtils.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUploadUtils.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUtils.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/ImageUtils.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/MimeTypeUtils.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/html/EscapeUtil.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/html/HTMLFilter.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpHelper.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpUtils.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/UserAgentUtils.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/id/IdGenerator.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/id/SnowflakeId.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/ip/AddressUtils.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/ip/IpUtils.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelHandlerAdapter.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelSheet.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/reflect/ReflectUtils.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/Base64.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/Md5Utils.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/spring/SpringUtils.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/sql/SqlUtil.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/IdUtils.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/Seq.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/UUID.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/xss/Xss.java create mode 100644 ry-api/ruoyi-common/src/main/java/com/ruoyi/common/xss/XssValidator.java create mode 100644 ry-api/ruoyi-framework/pom.xml create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataScopeAspect.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataSourceAspect.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/LogAspect.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RateLimiterAspect.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ApplicationConfig.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/CaptchaConfig.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/DruidConfig.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FastJson2JsonRedisSerializer.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FilterConfig.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/I18nConfig.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/KaptchaTextCreator.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/MyBatisConfig.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/RedisConfig.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ResourcesConfig.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ServerConfig.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ThreadPoolConfig.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/DruidProperties.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/PermitAllUrlProperties.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSource.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSourceContextHolder.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/RepeatSubmitInterceptor.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/impl/SameUrlDataInterceptor.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/AsyncManager.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/ShutdownManager.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/factory/AsyncFactory.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/AuthenticationContextHolder.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/PermissionContextHolder.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/AuthenticationEntryPointImpl.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/LogoutSuccessHandlerImpl.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/Server.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Cpu.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Jvm.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Mem.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Sys.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/SysFile.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/PermissionService.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPasswordService.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPermissionService.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysRegisterService.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java create mode 100644 ry-api/ruoyi-generator/pom.xml create mode 100644 ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/config/GenConfig.java create mode 100644 ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/controller/GenController.java create mode 100644 ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenTable.java create mode 100644 ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenTableColumn.java create mode 100644 ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/GenTableColumnMapper.java create mode 100644 ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/GenTableMapper.java create mode 100644 ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenTableColumnServiceImpl.java create mode 100644 ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenTableServiceImpl.java create mode 100644 ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/service/IGenTableColumnService.java create mode 100644 ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/service/IGenTableService.java create mode 100644 ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/util/GenUtils.java create mode 100644 ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityInitializer.java create mode 100644 ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityUtils.java create mode 100644 ry-api/ruoyi-generator/src/main/resources/generator.yml create mode 100644 ry-api/ruoyi-generator/src/main/resources/mapper/generator/GenTableColumnMapper.xml create mode 100644 ry-api/ruoyi-generator/src/main/resources/mapper/generator/GenTableMapper.xml create mode 100644 ry-api/ruoyi-generator/src/main/resources/vm/java/controller.java.vm create mode 100644 ry-api/ruoyi-generator/src/main/resources/vm/java/domain.java.vm create mode 100644 ry-api/ruoyi-generator/src/main/resources/vm/java/mapper.java.vm create mode 100644 ry-api/ruoyi-generator/src/main/resources/vm/java/service.java.vm create mode 100644 ry-api/ruoyi-generator/src/main/resources/vm/java/serviceImpl.java.vm create mode 100644 ry-api/ruoyi-generator/src/main/resources/vm/java/sub-domain.java.vm create mode 100644 ry-api/ruoyi-generator/src/main/resources/vm/js/api.js.vm create mode 100644 ry-api/ruoyi-generator/src/main/resources/vm/sql/sql.vm create mode 100644 ry-api/ruoyi-generator/src/main/resources/vm/ts/api.ts.vm create mode 100644 ry-api/ruoyi-generator/src/main/resources/vm/ts/index.ts.vm create mode 100644 ry-api/ruoyi-generator/src/main/resources/vm/ts/type.ts.vm create mode 100644 ry-api/ruoyi-generator/src/main/resources/vm/vue/index-tree.vue.vm create mode 100644 ry-api/ruoyi-generator/src/main/resources/vm/vue/index.vue.vm create mode 100644 ry-api/ruoyi-generator/src/main/resources/vm/vue/v3/index-tree.vue.vm create mode 100644 ry-api/ruoyi-generator/src/main/resources/vm/vue/v3/index.vue.vm create mode 100644 ry-api/ruoyi-generator/src/main/resources/vm/vue/v3/view.vue.vm create mode 100644 ry-api/ruoyi-generator/src/main/resources/vm/vue/v3ts/index-tree.vue.vm create mode 100644 ry-api/ruoyi-generator/src/main/resources/vm/vue/v3ts/index.vue.vm create mode 100644 ry-api/ruoyi-generator/src/main/resources/vm/vue/v3ts/view.vue.vm create mode 100644 ry-api/ruoyi-generator/src/main/resources/vm/vue/view.vue.vm create mode 100644 ry-api/ruoyi-generator/src/main/resources/vm/xml/mapper.xml.vm create mode 100644 ry-api/ruoyi-quartz/pom.xml create mode 100644 ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/config/ScheduleConfig.java create mode 100644 ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/controller/SysJobController.java create mode 100644 ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/controller/SysJobLogController.java create mode 100644 ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/domain/SysJob.java create mode 100644 ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/domain/SysJobLog.java create mode 100644 ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/mapper/SysJobLogMapper.java create mode 100644 ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/mapper/SysJobMapper.java create mode 100644 ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/ISysJobLogService.java create mode 100644 ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/ISysJobService.java create mode 100644 ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/impl/SysJobLogServiceImpl.java create mode 100644 ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/impl/SysJobServiceImpl.java create mode 100644 ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/RyTask.java create mode 100644 ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/AbstractQuartzJob.java create mode 100644 ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/CronUtils.java create mode 100644 ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/JobInvokeUtil.java create mode 100644 ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/QuartzDisallowConcurrentExecution.java create mode 100644 ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/QuartzJobExecution.java create mode 100644 ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/ScheduleUtils.java create mode 100644 ry-api/ruoyi-quartz/src/main/resources/mapper/quartz/SysJobLogMapper.xml create mode 100644 ry-api/ruoyi-quartz/src/main/resources/mapper/quartz/SysJobMapper.xml create mode 100644 ry-api/ruoyi-system/pom.xml create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysCache.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysConfig.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysLogininfor.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysNotice.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysNoticeRead.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysOperLog.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysPost.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysRoleDept.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysRoleMenu.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserOnline.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserPost.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserRole.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/MetaVo.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/RouterVo.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysConfigMapper.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDeptMapper.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictDataMapper.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictTypeMapper.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysLogininforMapper.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysMenuMapper.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysNoticeMapper.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysNoticeReadMapper.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysOperLogMapper.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysPostMapper.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleDeptMapper.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleMapper.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleMenuMapper.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserPostMapper.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserRoleMapper.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysConfigService.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDeptService.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDictDataService.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDictTypeService.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysLogininforService.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysMenuService.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysNoticeReadService.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysNoticeService.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysOperLogService.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysPostService.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysRoleService.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserOnlineService.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysConfigServiceImpl.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDeptServiceImpl.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictDataServiceImpl.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysLogininforServiceImpl.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysMenuServiceImpl.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysNoticeReadServiceImpl.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysNoticeServiceImpl.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOperLogServiceImpl.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysPostServiceImpl.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysRoleServiceImpl.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserOnlineServiceImpl.java create mode 100644 ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java create mode 100644 ry-api/ruoyi-system/src/main/resources/mapper/system/SysConfigMapper.xml create mode 100644 ry-api/ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml create mode 100644 ry-api/ruoyi-system/src/main/resources/mapper/system/SysDictDataMapper.xml create mode 100644 ry-api/ruoyi-system/src/main/resources/mapper/system/SysDictTypeMapper.xml create mode 100644 ry-api/ruoyi-system/src/main/resources/mapper/system/SysLogininforMapper.xml create mode 100644 ry-api/ruoyi-system/src/main/resources/mapper/system/SysMenuMapper.xml create mode 100644 ry-api/ruoyi-system/src/main/resources/mapper/system/SysNoticeMapper.xml create mode 100644 ry-api/ruoyi-system/src/main/resources/mapper/system/SysNoticeReadMapper.xml create mode 100644 ry-api/ruoyi-system/src/main/resources/mapper/system/SysOperLogMapper.xml create mode 100644 ry-api/ruoyi-system/src/main/resources/mapper/system/SysPostMapper.xml create mode 100644 ry-api/ruoyi-system/src/main/resources/mapper/system/SysRoleDeptMapper.xml create mode 100644 ry-api/ruoyi-system/src/main/resources/mapper/system/SysRoleMapper.xml create mode 100644 ry-api/ruoyi-system/src/main/resources/mapper/system/SysRoleMenuMapper.xml create mode 100644 ry-api/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml create mode 100644 ry-api/ruoyi-system/src/main/resources/mapper/system/SysUserPostMapper.xml create mode 100644 ry-api/ruoyi-system/src/main/resources/mapper/system/SysUserRoleMapper.xml create mode 100644 ry-api/ry.bat create mode 100644 ry-api/ry.sh create mode 100644 ry-api/sql/quartz.sql create mode 100644 ry-api/sql/ry_20260417.sql create mode 100644 ry-vue3/index.html create mode 100644 ry-vue3/package-lock.json create mode 100644 ry-vue3/package.json create mode 100644 ry-vue3/public/images/id-back.png create mode 100644 ry-vue3/public/images/id-front.png create mode 100644 ry-vue3/src/App.vue create mode 100644 ry-vue3/src/api/auth.js create mode 100644 ry-vue3/src/api/business/person.js create mode 100644 ry-vue3/src/api/business/project.js create mode 100644 ry-vue3/src/api/dict.js create mode 100644 ry-vue3/src/api/public.js create mode 100644 ry-vue3/src/api/system.js create mode 100644 ry-vue3/src/assets/main.scss create mode 100644 ry-vue3/src/assets/theme.js create mode 100644 ry-vue3/src/assets/theme.scss create mode 100644 ry-vue3/src/components/AreaCascader.vue create mode 100644 ry-vue3/src/components/DictSelect.vue create mode 100644 ry-vue3/src/components/DoctorDeptSelect.vue create mode 100644 ry-vue3/src/components/DoctorTitleSelect.vue create mode 100644 ry-vue3/src/components/IdCardUploader.vue create mode 100644 ry-vue3/src/components/OssImageUploader.vue create mode 100644 ry-vue3/src/components/Preview.vue create mode 100644 ry-vue3/src/layout/AdminLayout.vue create mode 100644 ry-vue3/src/layout/PortalLayout.vue create mode 100644 ry-vue3/src/main.js create mode 100644 ry-vue3/src/router/index.js create mode 100644 ry-vue3/src/store/user.js create mode 100644 ry-vue3/src/utils/areaData.json create mode 100644 ry-vue3/src/utils/oss.js create mode 100644 ry-vue3/src/utils/request.js create mode 100644 ry-vue3/src/utils/snowflake.js create mode 100644 ry-vue3/src/utils/submissionStatus.js create mode 100644 ry-vue3/src/views/admin/Account.vue create mode 100644 ry-vue3/src/views/admin/Department.vue create mode 100644 ry-vue3/src/views/admin/Experts.vue create mode 100644 ry-vue3/src/views/admin/Meetings.vue create mode 100644 ry-vue3/src/views/admin/Orgs.vue create mode 100644 ry-vue3/src/views/admin/Projects.vue create mode 100644 ry-vue3/src/views/admin/Roles.vue create mode 100644 ry-vue3/src/views/admin/Title.vue create mode 100644 ry-vue3/src/views/admin/Users.vue create mode 100644 ry-vue3/src/views/admin/Workbench.vue create mode 100644 ry-vue3/src/views/auth/Login.vue create mode 100644 ry-vue3/src/views/auth/RegisterExecutor.vue create mode 100644 ry-vue3/src/views/auth/RegisterExpert.vue create mode 100644 ry-vue3/src/views/auth/RegisterSponsor.vue create mode 100644 ry-vue3/src/views/doctor/Account.vue create mode 100644 ry-vue3/src/views/doctor/Home.vue create mode 100644 ry-vue3/src/views/doctor/Meetings.vue create mode 100644 ry-vue3/src/views/doctor/Messages.vue create mode 100644 ry-vue3/src/views/doctor/Projects.vue create mode 100644 ry-vue3/src/views/doctor/SubmissionDetail.vue create mode 100644 ry-vue3/src/views/doctor/SubmissionNew.vue create mode 100644 ry-vue3/src/views/doctor/Submissions.vue create mode 100644 ry-vue3/src/views/executor/Account.vue create mode 100644 ry-vue3/src/views/executor/Labor.vue create mode 100644 ry-vue3/src/views/executor/Meetings.vue create mode 100644 ry-vue3/src/views/executor/Overview.vue create mode 100644 ry-vue3/src/views/executor/People.vue create mode 100644 ry-vue3/src/views/executor/Projects.vue create mode 100644 ry-vue3/src/views/leader/Account.vue create mode 100644 ry-vue3/src/views/leader/Home.vue create mode 100644 ry-vue3/src/views/leader/Meetings.vue create mode 100644 ry-vue3/src/views/leader/Projects.vue create mode 100644 ry-vue3/src/views/manager/Accounts.vue create mode 100644 ry-vue3/src/views/manager/ExecIntent.vue create mode 100644 ry-vue3/src/views/manager/Experts.vue create mode 100644 ry-vue3/src/views/manager/ManagerProjectDetail.vue create mode 100644 ry-vue3/src/views/manager/MeetingNew.vue create mode 100644 ry-vue3/src/views/manager/Meetings.vue create mode 100644 ry-vue3/src/views/manager/Orgs.vue create mode 100644 ry-vue3/src/views/manager/Plans.vue create mode 100644 ry-vue3/src/views/manager/Projects.vue create mode 100644 ry-vue3/src/views/manager/ProjectsNew.vue create mode 100644 ry-vue3/src/views/manager/SupportIntent.vue create mode 100644 ry-vue3/src/views/manager/Workbench.vue create mode 100644 ry-vue3/src/views/portal/Home.vue create mode 100644 ry-vue3/src/views/portal/Invitation.vue create mode 100644 ry-vue3/src/views/portal/Publicity.vue create mode 100644 ry-vue3/src/views/portal/PublicityDetail.vue create mode 100644 ry-vue3/src/views/portal/SupportLetter.vue create mode 100644 ry-vue3/src/views/sponsor/Account.vue create mode 100644 ry-vue3/src/views/sponsor/Home.vue create mode 100644 ry-vue3/src/views/sponsor/Meetings.vue create mode 100644 ry-vue3/src/views/sponsor/NewPerson.vue create mode 100644 ry-vue3/src/views/sponsor/PersonDetail.vue create mode 100644 ry-vue3/src/views/sponsor/Projects.vue create mode 100644 ry-vue3/src/views/sponsor/SponsorPeople.vue create mode 100644 ry-vue3/src/views/sponsor/SponsorProjects.vue create mode 100644 ry-vue3/vite.config.js create mode 100644 scripts/redis-clear-config.sh create mode 100644 sql/biz_data.sql create mode 100644 sql/biz_schema.sql create mode 100644 test_20rounds.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4d5507e --- /dev/null +++ b/.gitignore @@ -0,0 +1,86 @@ +# ===== Java / Maven ===== +target/ +build/ +*.class +*.jar +*.war +hs_err_pid*.log +replay_pid*.log +.mvn/ +mvnw +mvnw.cmd + +# ===== Node / Vue ===== +node_modules/ +dist/ +.cache/ +.vite/ +*.local +.npm +.yarn + +# ===== IDE ===== +.idea/ +.vscode/ +*.iml +*.iws +*.ipr +.project +.classpath +.settings/ +.factorypath + +# ===== OS ===== +.DS_Store +Thumbs.db +desktop.ini +$RECYCLE.BIN/ + +# ===== Logs ===== +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# ===== Env ===== +.env +.env.local +.env.*.local + +# ===== Build outputs ===== +out/ +bin/ +*.exe +*.dll +*.so +*.dylib + +# ===== Misc ===== +*.bak +*.tmp +*.swp +*.swo +*~ +.gradle/ +*.iml + +# ===== Session / planning 内部笔记 (不进版本库) ===== +.claude/ +.hermes/ +.planning/ +findings.md +progress.md +task_plan.md +entity-config.json + +# ===== 大体积 / 二进制 / 备份 (不进版本库) ===== +RuoYi-Vue-master.zip +apache-maven-3.9.16/ +apache-maven-3.9.16-bin.zip +ry0808_mysql_dump.sql +ry8080/ +ry8080.zip +ry-console/ +ry-vue2/ # 旧 vue 控制台 diff --git a/proto/html/admin.html b/proto/html/admin.html new file mode 100644 index 0000000..963d111 --- /dev/null +++ b/proto/html/admin.html @@ -0,0 +1,105 @@ + + + + + + + 后台管理 - 项目管理系统 + + + + + +
+
+ +
+
+ +
+
+ + + + \ No newline at end of file diff --git a/proto/html/components/account-info-expert.html b/proto/html/components/account-info-expert.html new file mode 100644 index 0000000..9c9fa47 --- /dev/null +++ b/proto/html/components/account-info-expert.html @@ -0,0 +1,837 @@ + + + + + + 账号信息 - 项目管理系统 + + + + +
+ + + + + +

账号信息

+ + +
+ + +
+ + +
+
+ + +
+
+ 张 + 点击更换 +
+
+
张三
+
专家账号
+
+
+ + +
基本资料
+
+
+ + +
+
+ +
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
银行信息
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+

*专家可更新上传相应的证明文件及身份证照片

+

*签署劳务协议时,先验证是否有执业证书、职称证明、身份证照片,如果没有,必须先上传

+
+ + +
资质证件
+ + +
+ +
+
+
+ 🪪 + 正面 +
+
+
+
+ 🪪 + 反面 +
+
+
+ +
+
+
+ + +
+ +
+
+
+ 📜 + 证书 +
+
+
+ +
+
+
+ + +
+ +
+
+
+ 🏅 + 证明 +
+
+
+ +
+
+
+ + +
+ + +
+
+
+ + +
+
+
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ +
+ + +
+ + +
+
+
+
+ +
+ + + + \ No newline at end of file diff --git a/proto/html/components/account-info.html b/proto/html/components/account-info.html new file mode 100644 index 0000000..5526ca5 --- /dev/null +++ b/proto/html/components/account-info.html @@ -0,0 +1,247 @@ + + + + + + 账号信息 - 项目管理系统 + + + + +
+

账号信息

+ +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+
+
+
+ + + + \ No newline at end of file diff --git a/proto/html/components/account-manage.html b/proto/html/components/account-manage.html new file mode 100644 index 0000000..8269362 --- /dev/null +++ b/proto/html/components/account-manage.html @@ -0,0 +1,229 @@ + + + + + + 账号管理 + + + +
+ + + +
+ +
+ +
+
基本信息
+ +
+ + +
+ +
+ + +
+
+ + +
+
修改密码
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +

*密码长度 6-20 位,支持数字、字母、特殊字符;留空表示不修改密码

+
+ + +
+ + +
+
+
+ +
+ + + \ No newline at end of file diff --git a/proto/html/components/admin-execution-unit-manage.html b/proto/html/components/admin-execution-unit-manage.html new file mode 100644 index 0000000..bc52356 --- /dev/null +++ b/proto/html/components/admin-execution-unit-manage.html @@ -0,0 +1,171 @@ + + + + + + + 执行单位管理 - 后台 + + + +
+ + + +

执行单位管理

+ +
+
+
公司名称:
+
+ 创建日期: +
+ + + +
+
+
+ 合作状态: + +
+
公司地址:
+
税号/统一社会信用代码:
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
公司名称公司地址税号/统一社会信用代码合作状态创建日期操作
北京XXXXXXX公司北京市海淀区XXX路XXXX号91330101MA2GPR961K正常2026.05.22人员管理查看禁用
北京XXXXXXX公司北京市海淀区XXX路XXXX号91330101MA2GPR961K正常2026.05.22人员管理查看禁用
北京XXXXXXX公司北京市海淀区XXX路XXXX号91330101MA2GPR961K正常2026.05.22人员管理查看禁用
北京XXXXXXX公司北京市海淀区XXX路XXXX号91330101MA2GPR961K正常2026.05.22人员管理查看禁用
北京XXXXXXX公司北京市海淀区XXX路XXXX号91330101MA2GPR961K正常2026.05.22人员管理查看禁用
北京XXXXXXX公司北京市海淀区XXX路XXXX号91330101MA2GPR961K正常2026.05.22人员管理查看禁用
+
+ +
+ + + \ No newline at end of file diff --git a/proto/html/components/admin-expert-manage.html b/proto/html/components/admin-expert-manage.html new file mode 100644 index 0000000..06fc72d --- /dev/null +++ b/proto/html/components/admin-expert-manage.html @@ -0,0 +1,276 @@ + + + + + + + 专家管理 - 后台 + + + +
+ + + +

专家管理

+ +
+
+
姓名:
+
手机号:
+
工作单位:
+ +
+
+ +
+ + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
姓名手机号工作单位科室职称证书操作
张三13534621147北京XXXXXXX医院呼吸科主任医师 +
+ 查看 + 下载 +
+
+ +
李四13534621147北京XXXXXXX医院血液科副主任医师 +
+ 查看 + 下载 +
+
+ +
张三13534621147北京XXXXXXX医院呼吸科主任医师 +
+ 查看 + 下载 +
+
+ +
李四13534621147北京XXXXXXX医院血液科副主任医师 +
+ 查看 + 下载 +
+
+ +
张三13534621147北京XXXXXXX医院呼吸科主任医师 +
+ 查看 + 下载 +
+
+ +
李四13534621147北京XXXXXXX医院血液科副主任医师 +
+ 查看 + 下载 +
+
+ +
张三13534621147北京XXXXXXX医院呼吸科主任医师 +
+ 查看 + 下载 +
+
+ +
+
+ +
+ + + \ No newline at end of file diff --git a/proto/html/components/admin-home.html b/proto/html/components/admin-home.html new file mode 100644 index 0000000..74fe859 --- /dev/null +++ b/proto/html/components/admin-home.html @@ -0,0 +1,107 @@ + + + + + + + 首页 - 后台 + + + +
+

首页

+ + +
+ +
用户总数
+
128
+
前往用户管理
+
+ +
角色数量
+
6
+
前往角色管理
+
+ +
项目总数
+
42
+
前往项目管理
+
+ +
专家数量
+
36
+
前往专家管理
+
+ +
单位数量
+
24
+
前往支持单位管理
+
+ +
公告数量
+
15
+
前往项目公示
+
+
+ + +
+

消息通知更多 →

+ +
+
+ + \ No newline at end of file diff --git a/proto/html/components/admin-intent-support.html b/proto/html/components/admin-intent-support.html new file mode 100644 index 0000000..c2e8a30 --- /dev/null +++ b/proto/html/components/admin-intent-support.html @@ -0,0 +1,112 @@ + + + + + + + 意向支持 - 后台 + + + +
+ + + +

意向支持管理

+ +
+
+
意向项目名称:
+
姓名:
+
工作单位:
+ +
+

*项目公示中点击支持意向后, 显示在此

+
+ +
+ + + + + + + + + + + + + + + + + +
意向项目编号意向项目名称姓名单位名称部门职务手机号
ZH-2026-658小牛血清创新应用研讨会张三北京XXXXXX公司华北大区项目负责人13512321597
ZH-2026-658小牛血清创新应用研讨会李四北京XXXXXX公司华北大区项目负责人13512321597
ZH-2026-658小牛血清创新应用研讨会张三北京XXXXXX公司华北大区项目负责人13512321597
ZH-2026-658小牛血清创新应用研讨会李四北京XXXXXX公司华北大区项目负责人13512321597
ZH-2026-658小牛血清创新应用研讨会张三北京XXXXXX公司华北大区项目负责人13512321597
ZH-2026-658小牛血清创新应用研讨会李四北京XXXXXX公司华北大区项目负责人13512321597
ZH-2026-658小牛血清创新应用研讨会张三北京XXXXXX公司华北大区项目负责人13512321597
ZH-2026-658小牛血清创新应用研讨会李四北京XXXXXX公司华北大区项目负责人13512321597
+
+ +
+ + + \ No newline at end of file diff --git a/proto/html/components/admin-project-detail.html b/proto/html/components/admin-project-detail.html new file mode 100644 index 0000000..015790c --- /dev/null +++ b/proto/html/components/admin-project-detail.html @@ -0,0 +1,240 @@ + + + + + 项目详情 - 后台 + + + +
+ +

项目详情

+ + +
+
项目信息
+
+
项目编号:
+
项目名称:
+
总场次/总期数:
+
总金额:
+
管理费及税金:
+
支持单位:
+
服务机构:
+
项目形式:
+
项目评价:4.5
+
+

*点击后可查看具体评价维度,评价标准与供应商系统保持一致

+
+ + +
+
支持单位监督员
+
+
+ 1. + 张三 +
+ 删除 +
+
+
+ 2. + 李四 +
+ 删除 +
+
+
+ +

*增加只显示已点支持意向的

+
+ + +
+
角色劳务设置
+ + + + + + +
角色劳务金额
主席5,000
主持3,000
+
+ + +
+
公告文件
+ +
+
邀请函
+ +
+ +
+
已发布公告
+ +
+ +
+
支持函
+ +
+
+ +

*项目详情可修改,只有创建人可修改

+ +
+ +
+
+ + + + \ No newline at end of file diff --git a/proto/html/components/admin-project-manage.html b/proto/html/components/admin-project-manage.html new file mode 100644 index 0000000..43f55f8 --- /dev/null +++ b/proto/html/components/admin-project-manage.html @@ -0,0 +1,240 @@ + + + + + + + 项目管理 - 后台 + + + +
+ + + +

项目管理

+ + +
+
+
项目编号:
+
项目名称:
+
是否结题:
+
是否结算:
+
支持单位:
+
服务机构:
+
项目形式:
+ +
+

*支持多个支持单位筛选 / *支持多个服务机构筛选

+
+ + +
+ + + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
项目编号项目名称总场次/总期数已执行未执行总金额可用金额已支付劳务费已支付会务费项目评价分数支持单位服务机构项目形式是否结题操作
ZH-2026-658小牛血清创新应用研讨会2616103,112,0001,112,0001,000,0001,000,0005.0北京XXXX有限公司北京XXXX公司线上未结题分配结题查看编辑开通删除公告
ZH-2026-658小牛血清创新应用研讨会2616103,112,0001,112,0001,000,0001,000,0003.5北京XXXX有限公司北京XXXX公司线下未结题分配结题查看编辑开通删除公告
ZH-2026-658小牛血清创新应用研讨会2616103,112,0001,112,0001,000,0001,000,0004.5北京XXXX有限公司北京XXXX公司线上+线下已结题分配结题查看编辑开通删除公告
ZH-2026-658小牛血清创新应用研讨会2616103,112,0001,112,0001,000,0001,000,0003.0北京XXXX有限公司北京XXXX公司其他未结题分配结题查看编辑开通删除公告
ZH-2026-658小牛血清创新应用研讨会2616103,112,0001,112,0001,000,0001,000,0005.0北京XXXX有限公司北京XXXX公司线上未结题分配结题查看编辑开通删除公告
ZH-2026-658小牛血清创新应用研讨会2616103,112,0001,112,0001,000,0001,000,0003.5北京XXXX有限公司北京XXXX公司线下已结题分配结题查看编辑开通删除公告
+
+

*可用金额=总金额*(1-管理费税费)-累计劳务-累计会务(总发票的金额) / *点击后可查看具体评价维度,评价标准与供应商系统保持一致

+ +
+ + + \ No newline at end of file diff --git a/proto/html/components/admin-project-plan-detail.html b/proto/html/components/admin-project-plan-detail.html new file mode 100644 index 0000000..3aca53e --- /dev/null +++ b/proto/html/components/admin-project-plan-detail.html @@ -0,0 +1,113 @@ + + + + + + + 项目计划详情 - 后台 + + + +
+ + + +

项目计划管理

+ +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
项目计划年度项目计划名称状态创建日期操作人操作
2026北京整合医学学会2026-2030项目规划正常2026.05.10张三编辑下架
2025北京整合医学学会2020-2025项目规划下架2020.05.10张三编辑上架
+
+ +
+ + + \ No newline at end of file diff --git a/proto/html/components/admin-project-public.html b/proto/html/components/admin-project-public.html new file mode 100644 index 0000000..ddb4b82 --- /dev/null +++ b/proto/html/components/admin-project-public.html @@ -0,0 +1,218 @@ + + + + + + + 项目公示 - 后台 + + + +
+ + + +

项目公示

+ +
+
+
全部
+
邀请函
+
支持函
+
公告文件
+
+ +
+ + +
+ +
+
+ + + + + + + + + + + + + + + + + + + +
标题名称发布时间操作
邀请函2026第2323号 "恺启新生—胃癌 CAR-T 细胞治疗系列会"项目 邀请函2026-05-06删除
会议通知2026第2277号 北京同仁医院建院140周年系列学术活动之2026年第三届北京整合医学学会急诊医学分会学术会议2026-05-06删除
邀请函2025第7276号 "整合之光"慢病防治与管理多学科融合新计划 — 慢病合并真菌感染多学科交流会2026-05-06删除
支持函2026第2127号 骨领新生—骨质疏松规范诊疗临床病例实战精析会2026-05-06删除
通知2026第2577号 深圳市医学会2026年感染病学分会学术年会资助项目通知2026-05-06删除
邀请函2026第2323号 "恺启新生—胃癌 CAR-T 细胞治疗系列会"项目 邀请函2026-05-06删除
会议通知2026第2277号 北京同仁医院建院140周年系列学术活动之2026年第三届北京整合医学学会急诊医学分会学术会议2026-05-06删除
邀请函2025第7276号 "整合之光"慢病防治与管理多学科融合新计划 — 慢病合并真菌感染多学科交流会2026-05-06删除
支持函2026第2127号 骨领新生—骨质疏松规范诊疗临床病例实战精析会2026-05-06删除
通知2026第2577号 深圳市医学会2026年感染病学分会学术年会资助项目通知2026-05-06删除
+
+
+ + +
+
+ + + + + + +
标题名称发布时间操作
2026第2323号 "恺启新生—胃癌 CAR-T 细胞治疗系列会"项目 邀请函2026-05-06删除
2025第7276号 "整合之光"慢病防治与管理多学科融合新计划 — 慢病合并真菌感染多学科交流会2026-05-06删除
+
+
+ + +
+
+ + + + + +
标题名称发布时间操作
2026第2127号 骨领新生—骨质疏松规范诊疗临床病例实战精析会2026-05-06删除
+
+
+ + +
+
+ + + + + +
标题名称发布时间操作
2026第2577号 深圳市医学会2026年感染病学分会学术年会资助项目通知2026-05-06删除
+
+
+
+ + + + +
+ + + \ No newline at end of file diff --git a/proto/html/components/admin-resource-manage.html b/proto/html/components/admin-resource-manage.html new file mode 100644 index 0000000..6c2db8e --- /dev/null +++ b/proto/html/components/admin-resource-manage.html @@ -0,0 +1,162 @@ + + + + + + + 资料库管理 - 后台 + + + +
+ + + +

资料库管理

+ +
+
+
资料名称:
+
分类:
+
上传人:
+ +
+
+ +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
资料名称分类文件大小上传人上传时间状态操作
📄项目策划模板 V2.0.docx项目模板2.3 MB张三2026-07-15 10:30已发布下载删除
📄会议材料模板 V1.5.docx会议材料1.8 MB李四2026-07-12 14:20已发布下载删除
📄合规审核要点手册.pdf合规文档5.6 MB王五2026-07-08 09:15已发布下载删除
📄专家评审标准 V3.0.pdf专家库3.2 MB赵六2026-06-25 16:45已发布下载删除
📄项目操作培训视频.mp4培训资料256 MB钱七2026-06-15 11:00已发布下载删除
📄劳务材料汇总表.xlsx表单模板0.5 MB孙八2026-06-08 08:50已发布下载删除
+
+ +
+ + + \ No newline at end of file diff --git a/proto/html/components/admin-role-manage.html b/proto/html/components/admin-role-manage.html new file mode 100644 index 0000000..ff03528 --- /dev/null +++ b/proto/html/components/admin-role-manage.html @@ -0,0 +1,169 @@ + + + + + + + 角色管理 - 后台 + + + +
+ + + +

角色管理

+ +
+
+
角色名称:
+
状态:
+ +
+
+ +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
角色名称角色编码描述权限数状态创建时间操作
后台管理员admin系统超级管理员,拥有全部权限128启用2026-01-15 09:00权限配置编辑删除
合规人员compliance负责项目、会议、专家等合规审核45启用2026-01-15 09:00权限配置编辑删除
项目负责人project_leader负责项目的整体执行和会议管理28启用2026-01-15 09:00权限配置编辑删除
执行方executor负责项目的具体执行工作22启用2026-01-15 09:00权限配置编辑删除
赞助方sponsor提供资金支持,查看项目信息15启用2026-01-15 09:00权限配置编辑删除
专家expert项目设计投稿和会议参与18启用2026-01-15 09:00权限配置编辑删除
+
+ +
+ + + \ No newline at end of file diff --git a/proto/html/components/admin-service-manage.html b/proto/html/components/admin-service-manage.html new file mode 100644 index 0000000..c618798 --- /dev/null +++ b/proto/html/components/admin-service-manage.html @@ -0,0 +1,179 @@ + + + + + + + 服务机构管理 - 后台 + + + +
+ + + +

服务机构管理

+ +
+
+
姓名:
+
手机号:
+
工作单位:
+ +
+
+ +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
姓名手机号企业名称部门职务操作
张三13534621147北京XXXXXXX公司华北大区项目负责人查看禁用
李四13534621147北京XXXXXXX公司华北大区会务人员查看禁用
张三13534621147北京XXXXXXX公司华北大区会务执行查看禁用
李四13534621147北京XXXXXXX公司华北大区项目负责人查看禁用
张三13534621147北京XXXXXXX公司华北大区会务人员查看禁用
李四13534621147北京XXXXXXX公司华北大区项目负责人查看禁用
+
+ +
+ + + \ No newline at end of file diff --git a/proto/html/components/admin-settings.html b/proto/html/components/admin-settings.html new file mode 100644 index 0000000..655d3b7 --- /dev/null +++ b/proto/html/components/admin-settings.html @@ -0,0 +1,171 @@ + + + + + + + 设置管理 - 后台 + + + +
+ + + +

设置管理

+ +
+
+
基本设置
+
安全设置
+
邮件设置
+
短信设置
+
通知设置
+
+ + +
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
系统通知
站内消息通知开关
+
+
+
+
邮件通知
关键操作通过邮件通知用户
+
+
+
+
短信通知
紧急情况通过短信通知用户
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/proto/html/components/admin-user-manage.html b/proto/html/components/admin-user-manage.html new file mode 100644 index 0000000..16c8759 --- /dev/null +++ b/proto/html/components/admin-user-manage.html @@ -0,0 +1,180 @@ + + + + + + + 用户管理 - 后台 + + + +
+ + + +

用户管理

+ + +
+
+
用户名:
+
姓名:
+
手机号:
+
角色:
+
状态:
+ +
+
+ + +
+ + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
用户名姓名手机号角色状态创建时间操作
admin_zhang张三13800138001后台管理员启用2026-01-15 09:00编辑重置密码禁用
compliance_li李四13800138002合规人员启用2026-02-20 14:30编辑重置密码禁用
leader_wang王五13800138003项目负责人启用2026-03-10 10:15编辑重置密码禁用
expert_zhao赵六13800138004专家启用2026-04-05 16:45编辑重置密码禁用
sponsor_qian钱七13800138005赞助方禁用2026-05-12 11:20编辑重置密码启用
executor_sun孙八13800138006执行方启用2026-06-08 08:50编辑重置密码禁用
+
+ +
+ + + \ No newline at end of file diff --git a/proto/html/components/compliance-view-2.html b/proto/html/components/compliance-view-2.html new file mode 100644 index 0000000..07f9e1b --- /dev/null +++ b/proto/html/components/compliance-view-2.html @@ -0,0 +1,86 @@ + + + + + 查看人员 v2 + + + +
+ +

查看人员

+
+
人员详情
+
姓名:张三
+
手机号:13534621147
+
工作单位:北京XXXXXXX公司
+
部门:华北大区
+
职务:项目负责人
+
角色:监察员
+
状态:正常
+
+
+ + +
+
+ + + \ No newline at end of file diff --git a/proto/html/components/compliance-view.html b/proto/html/components/compliance-view.html new file mode 100644 index 0000000..cc4db08 --- /dev/null +++ b/proto/html/components/compliance-view.html @@ -0,0 +1,86 @@ + + + + + 查看人员 + + + +
+ +

查看人员

+
+
人员详情
+
姓名:张三
+
手机号:13534621147
+
工作单位:北京XXXXXXX公司
+
部门:华北大区
+
职务:项目负责人
+
角色:监察员
+
状态:正常
+
+
+ + +
+
+ + + \ No newline at end of file diff --git a/proto/html/components/edit-meeting-labor.html b/proto/html/components/edit-meeting-labor.html new file mode 100644 index 0000000..ccd8f8d --- /dev/null +++ b/proto/html/components/edit-meeting-labor.html @@ -0,0 +1,79 @@ + + + + + 编辑会议-劳务材料 + + + +
+ +

编辑劳务材料

+
+
+
+
+
+
+ +
+
+
+
+ + + +
+
+
+

*材料类型 + 材料名称 + 附件 三者必填;支持 PDF/Word/Excel/图片,单个文件不超过 20MB

+
+ + +
+
+
+
+ + + \ No newline at end of file diff --git a/proto/html/components/edit-meeting-materials.html b/proto/html/components/edit-meeting-materials.html new file mode 100644 index 0000000..59da863 --- /dev/null +++ b/proto/html/components/edit-meeting-materials.html @@ -0,0 +1,79 @@ + + + + + 编辑-会务材料 + + + +
+ +

编辑会务材料

+
+
+
+
+
+
+ +
+
+
+
+ + + +
+
+
+

*材料类型 + 材料名称 + 附件 三者必填;支持 PDF/Word/Excel/图片,单个文件不超过 20MB

+
+ + +
+
+
+
+ + + \ No newline at end of file diff --git a/proto/html/components/edit-project-plan.html b/proto/html/components/edit-project-plan.html new file mode 100644 index 0000000..b829c3f --- /dev/null +++ b/proto/html/components/edit-project-plan.html @@ -0,0 +1,169 @@ + + + + + 编辑项目策划方案 + + + +
+ +

编辑项目策划方案

+
+
基本信息
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + 查看 + 下载 + +
+
+
+ + +
+
+ + +
+ +
+ + 待审核 + (由审核流程决定,不可编辑) +
+
+ + 未结算 + (由结算流程决定,不可编辑) +
+ +

*策划方案名称 / 项目方向 / 项目类别 / 项目形式 / 设计文件 / 项目编号 均为必填项;重新上传文件后,请确认原文件版本已被归档

+ +
+ + +
+
+
+
+ + + \ No newline at end of file diff --git a/proto/html/components/empty.html b/proto/html/components/empty.html new file mode 100644 index 0000000..8565cef --- /dev/null +++ b/proto/html/components/empty.html @@ -0,0 +1,59 @@ + + + + + + 开发中 + + + +
+
+
🚧
+
开发中
+
该功能模块正在建设中,敬请期待
+
+
+ + \ No newline at end of file diff --git a/proto/html/components/execution-intent.html b/proto/html/components/execution-intent.html new file mode 100644 index 0000000..c7c7abb --- /dev/null +++ b/proto/html/components/execution-intent.html @@ -0,0 +1,321 @@ + + + + + + 执行意向 - 项目管理系统 + + + +
+ + + + + +
+
+
+ 意向项目名称: + +
+
+ 姓名: + +
+
+ 工作单位: + +
+ + +
+
+ + +
+ *项目公示中点击执行意向后,显示在此 + *执行意向为在供应商系统已注册账号,并点击了执行申请的账号 +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
执行意向项目编号执行意向项目名称姓名工作单位名称部门职务手机号是否入库
ZH-2026-658小牛血清创新应用研讨会张三北京XXXXXX公司华北大区项目负责人13512321597已入库
ZH-2026-658小牛血清创新应用研讨会李四北京XXXXXX公司华北大区项目负责人13512321597未入库
ZH-2026-658小牛血清创新应用研讨会张三北京XXXXXX公司华北大区项目负责人13512321597已入库
ZH-2026-658小牛血清创新应用研讨会李四北京XXXXXX公司华北大区项目负责人13512321597已入库
ZH-2026-658小牛血清创新应用研讨会张三北京XXXXXX公司华北大区项目负责人13512321597已入库
ZH-2026-658小牛血清创新应用研讨会李四北京XXXXXX公司华北大区项目负责人13512321597已入库
ZH-2026-658小牛血清创新应用研讨会张三北京XXXXXX公司华北大区项目负责人13512321597已入库
ZH-2026-658小牛血清创新应用研讨会李四北京XXXXXX公司华北大区项目负责人13512321597已入库
+
+ +
+ + \ No newline at end of file diff --git a/proto/html/components/execution-unit-view.html b/proto/html/components/execution-unit-view.html new file mode 100644 index 0000000..7e104da --- /dev/null +++ b/proto/html/components/execution-unit-view.html @@ -0,0 +1,136 @@ + + + + + + + 执行单位查看 + + + +
+ + + + + +

单位查看

+ +
+
+ +
+
单位名称
+
北京XXXXXXX公司
+
+ +
+
单位地址
+
北京市海淀区XXX路XXXX号
+
+ +
+
税号/统一社会信用代码
+
91330101MA2GPR961K
+
+ +
+
合作状态
+
正常
+
+ +
+
创建日期
+
2026.05.22
+
+ +
+
管理员
+
张三
+
+ +
+
账号数量
+
36
+
+
+
+ + +
+ +
+ +
+ + + diff --git a/proto/html/components/executor-units.html b/proto/html/components/executor-units.html new file mode 100644 index 0000000..2153c23 --- /dev/null +++ b/proto/html/components/executor-units.html @@ -0,0 +1,375 @@ + + + + + + + 执行单位管理 + + + +
+ + + + +
+
+
+ 单位名称: + +
+
+ 单位地址: + +
+
+ 税号/统一社会信用代码: + +
+
+ 创建日期: +
+ + + +
+
+ +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
单位名称单位地址税号/统一社会信用代码合作状态创建日期操作
北京XXXXXXX公司北京市海淀区XXX路XXXX号91330101MA2GPR961K正常2026.05.22 + 查看 + 人员管理 + 禁用 +
北京XXXXXXX公司北京市海淀区XXX路XXXX号91330101MA2GPR961K正常2026.05.22 + 查看 + 人员管理 + 禁用 +
北京XXXXXXX公司北京市海淀区XXX路XXXX号91330101MA2GPR961K正常2026.05.22 + 查看 + 人员管理 + 禁用 +
北京XXXXXXX公司北京市海淀区XXX路XXXX号91330101MA2GPR961K正常2026.05.22 + 查看 + 人员管理 + 禁用 +
北京XXXXXXX公司北京市海淀区XXX路XXXX号91330101MA2GPR961K正常2026.05.22 + 查看 + 人员管理 + 禁用 +
北京XXXXXXX公司北京市海淀区XXX路XXXX号91330101MA2GPR961K已禁用2026.05.22 + 查看 + 启用 +
北京XXXXXXX公司北京市海淀区XXX路XXXX号91330101MA2GPR961K正常2026.05.22 + 查看 + 人员管理 + 禁用 +
北京XXXXXXX公司北京市海淀区XXX路XXXX号91330101MA2GPR961K正常2026.05.22 + 查看 + 人员管理 + 禁用 +
+
+ +
+ + + \ No newline at end of file diff --git a/proto/html/components/expert-review.html b/proto/html/components/expert-review.html new file mode 100644 index 0000000..a3fdfaf --- /dev/null +++ b/proto/html/components/expert-review.html @@ -0,0 +1,851 @@ + + + + + + 专家审核 - 项目管理系统 + + + +
+ + + + + +
+

*新建和批量导入审核状态直接为审核通过

+

*退回后,如专家重新提交,则审核状态变为待审核

+

*审核通过后,进入专家库,管理后台可查看专家库

+

*禁用后则无法登录,提示您的账号已被禁用

+
+ + +
+
筛选条件
+
+
+ 姓名: + +
+
+ 手机号: + +
+
+ 工作单位: + +
+
+ 审核状态: + +
+ +
+
+ + +
+ + + + + +
+ + +
+
全部
+
待审核 (50)
+
审核通过
+
已退回
+
+ + +

*提示:需将证明文件名称修改为手机号进行导入

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
姓名手机号工作单位科室职称执业证书/证明职称证明审核状态审批时间审批人操作
张三13534621147北京XXXXXXX医院呼吸科主任医师查看下载查看下载待审核2026.05.11  14:30张三 + 审核 + 查看 + 修改 + 禁用 +
李四13534621147北京XXXXXXX医院血液科副主任医师查看下载查看下载审核通过2026.05.11  14:30李四 + 审核 + 查看 + 修改 + 恢复 +
张三13534621147北京XXXXXXX医院呼吸科主任医师查看下载查看下载已退回2026.05.11  14:30张三 + 审核 + 查看 + 修改 + 禁用 +
李四13534621147北京XXXXXXX医院血液科副主任医师查看下载查看下载待审核2026.05.11  14:30李四 + 审核 + 查看 + 修改 + 禁用 +
张三13534621147北京XXXXXXX医院呼吸科主任医师查看下载查看下载待审核2026.05.11  14:30张三 + 审核 + 查看 + 修改 + 禁用 +
李四13534621147北京XXXXXXX医院血液科副主任医师查看下载查看下载已退回2026.05.11  14:30李四 + 审核 + 查看 + 修改 + 禁用 +
张三13534621147北京XXXXXXX医院呼吸科主任医师查看下载查看下载待审核2026.05.11  14:30张三 + 审核 + 查看 + 修改 + 禁用 +
李四13534621147北京XXXXXXX医院血液科副主任医师查看下载查看下载审核通过2026.05.11  14:30李四 + 审核 + 查看 + 修改 + 禁用 +
+
+ +
+ + + + + + + + + + \ No newline at end of file diff --git a/proto/html/components/home.html b/proto/html/components/home.html new file mode 100644 index 0000000..97eeed8 --- /dev/null +++ b/proto/html/components/home.html @@ -0,0 +1,374 @@ + + + + + + + 首页 - 项目管理系统 + + + +
+ + +
+
+

下午好,张三专家 👋

+

欢迎使用项目管理系统

+
+
+
+
+
+
+ + +
+
+

+ 待参加的会议 + 更多 → +

+
    +
  • +
    +
    📅
    + 2024 年度临床医学研究项目专家评审会议 +
    + 09:00 +
  • +
  • +
    +
    📅
    + 智慧医院建设项目方案论证会 +
    + 明天 +
  • +
  • +
    +
    📅
    + 基层医疗改革试点项目中期评估会 +
    + 08-12 +
  • +
  • +
    +
    📅
    + 数字化医疗转型方案评审会 +
    + 08-18 +
  • +
+
+
+

+ 待签署的协议 + 更多 → +

+
    +
  • +
    +
    📄
    + 《数字化医疗转型方案》专家意见书 +
    + 待签署 +
  • +
  • +
    +
    📄
    + 《智慧医院建设方案》专家评审协议 +
    + 待签署 +
  • +
  • +
    +
    📄
    + 《基层卫生服务能力提升方案》保密协议 +
    + 已完成 +
  • +
+
+
+ + +
+

+ 消息通知 + 更多 → +

+ +
+ +
+ + + \ No newline at end of file diff --git a/proto/html/components/labor-voucher.html b/proto/html/components/labor-voucher.html new file mode 100644 index 0000000..87b812f --- /dev/null +++ b/proto/html/components/labor-voucher.html @@ -0,0 +1,78 @@ + + + + + 劳务凭证 + + + +
+ +

劳务凭证

+
+
+
会议名称:
+
专家姓名:
+
审核状态: + +
+ +
+
+
+ +
+
+ + + + + + + + +
凭证编号会议名称专家姓名身份证号银行账号金额提交日期审核状态操作
LV-2026-001小牛血清创新应用研讨会陈教授110101********12346222****5678¥ 3,0002026-05-12待审核审核驳回
LV-2026-002整合医学学会项目评审会王教授310101********56786222****1234¥ 2,5002026-04-02已审核查看
LV-2026-003智慧医院建设项目方案论证李教授440101********90126222****3456¥ 4,0002026-03-16已审核查看
LV-2026-004基层医疗改革试点中期评估张教授320101********34566222****7890¥ 3,5002026-02-02已驳回查看重审
+
+
+ + + \ No newline at end of file diff --git a/proto/html/components/leader-home.html b/proto/html/components/leader-home.html new file mode 100644 index 0000000..7fb991f --- /dev/null +++ b/proto/html/components/leader-home.html @@ -0,0 +1,125 @@ + + + + + + + 首页 - 项目负责人 + + + +
+

首页

+ + +
+ + +
项目总数量
+
26
+
查看全部项目
+
+ + +
已结算数量
+
16
+
查看已结算项目
+
+ + +
已结题数量
+
10
+
查看已结题项目
+
+
+ + +
+ + +
未执行会议
+
58
+
查看未执行会议
+
+ + +
已执行会议
+
68
+
查看已执行会议
+
+
+ + +
+

消息通知更多 →

+ +
+
+ + + \ No newline at end of file diff --git a/proto/html/components/leader-projects.html b/proto/html/components/leader-projects.html new file mode 100644 index 0000000..2989e48 --- /dev/null +++ b/proto/html/components/leader-projects.html @@ -0,0 +1,221 @@ + + + + + + + 项目列表 - 项目负责人 + + + +
+ + + +

项目列表

+ + +
+
+
项目编号:
+
项目名称:
+
+ 项目形式: + +
+
+ 是否结题: + +
+
+ 是否结算: + +
+ +
+

*支持多个支持单位筛选 / *支持多个服务机构筛选

+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
项目编号项目名称总场次/总期数已执行未执行总金额可用金额已支付劳务费已支付会务费执行单位得分支持单位服务机构项目形式当前阶段是否结题执行单位得分(合规)执行单位评价(支持方)操作
ZH-2026-658小牛血清创新应用研讨会2616103,112,0001,112,0001,000,0001,000,0005.0北京XXXX有限公司北京XXXX公司线上执行中未结题5.0查看查看
ZH-2026-658小牛血清创新应用研讨会2616103,112,0001,112,0001,000,0001,000,0003.5北京XXXX有限公司北京XXXX公司线下执行中未结题查看查看
ZH-2026-658小牛血清创新应用研讨会2616103,112,0001,112,0001,000,0001,000,0004.5北京XXXX有限公司北京XXXX公司线上+线下已结题已结题查看查看
ZH-2026-658小牛血清创新应用研讨会2616103,112,0001,112,0001,000,0001,000,0003.0北京XXXX有限公司北京XXXX公司其他筹备中未结题查看查看
ZH-2026-658小牛血清创新应用研讨会2616103,112,0001,112,0001,000,0001,000,000北京XXXX有限公司北京XXXX公司线上执行中未结题查看
ZH-2026-658小牛血清创新应用研讨会2616103,112,0001,112,0001,000,0001,000,0003.5北京XXXX有限公司北京XXXX公司线下已结题已结题查看查看
+
+ +
+ + + \ No newline at end of file diff --git a/proto/html/components/meeting-detail-executor.html b/proto/html/components/meeting-detail-executor.html new file mode 100644 index 0000000..8f79026 --- /dev/null +++ b/proto/html/components/meeting-detail-executor.html @@ -0,0 +1,225 @@ + + + + + + + 会议详情 - 项目管理系统 + + + +
+ + + +

会议详情

+ + +
+
+ +
+
会议基本信息
+
+
项目编号:ZH-2026-658
+
会议名称:小牛血清创新应用研讨会
+
项目名称:小牛血清创新应用研讨会
+
期数:3/26
+
总场次/总期数:26
+
会议开始时间:2026.05.13 11:30
+
支持单位:北京XXXX有限公司
+
会议结束时间:2026.05.15 11:30
+
创建时间:2026.05.11 11:30
+
创建人员:张三
+
+
+ +
+
材料管理
+
+ + + + +
+
+
+
+ 物料制作: + 提示:物料实物照片,盖章版结算单,发票 +
+
+ 酒店: + 提示:酒店盖章版水单,酒店发票(餐饮普票,住宿场地费专票) +
+
+ 大交通: + 提示:行程单/盖章版结算单,发票 +
+
+ 小交通: + 提示:行程单/盖章版结算单,发票 +
+
+ 执行费: + 提示:合同,盖章版结算单,发票,其他材料 +
+
+ 设计费: + 提示:设计稿,PPT +
+
+ 其他: +
+
+ 总结算单: +
+
+ 总发票: + *总发票单独推送至OA,单独下载 +
+
+
+
+
+
劳务明细表:劳务明细表下载
+
劳务协议:劳务协议.zip
+
+
+
+
+
劳务付款凭证:提示:劳务付款凭证文件
+
+
+
+
+
会务付款凭证:提示:会务付款凭证文件
+
+
+
+ + 会务材料文件夹模板下载 + + + +
+

*结算后,已完结状态,执行方不能再进行编辑

+
+
+ + +
+
审核时间轴
+
+
+
执行方提交材料
+
已提交 · 2026.05.15 11:30
+
+
+
会议已执行
+
2026.05.25 10:30
+
+
+
合规审核
+
已通过 · 材料完整 · 2026.05.30 14:30
+
+
+
监察意见
+
通过 · 材料完整 · 2026.06.03 15:30
+
+
+
+
+ + +
+ +
+ +
+ + + + \ No newline at end of file diff --git a/proto/html/components/meeting-detail-page-1.html b/proto/html/components/meeting-detail-page-1.html new file mode 100644 index 0000000..70fd277 --- /dev/null +++ b/proto/html/components/meeting-detail-page-1.html @@ -0,0 +1,300 @@ + + + + + + 会议详情页 v1 (合规) + + + + +
+ + + + + +

会议基本信息

+ + +
+
+
+ 项目编号: + ZH-2026-658 +
+
+ 会议名称: + 小牛血清创新应用研讨会 +
+
+ 项目名称: + 小牛血清创新应用研讨会 +
+
+ 期数: + 第 3 期 +
+
+ 总场次/总期数: + 26 +
+
+ 会议开始时间: + 2026.05.13 11:30 +
+
+ 支持单位: + 北京XXXX有限公司 +
+
+ 会议结束时间: + 2026.05.15 11:30 +
+
+ 创建时间: + 2026.05.11 11:30 +
+
+ 创建人员: + 张三 +
+
+
+ + +

*点击缩略图可查看原图和下载

+ + +
+
邀请函
+
+
+ 📄 + 邀请函 +
+
+ 📄 + 邀请函 +
+
+
+ 下载 +
+
+ + +
+
会议日程
+
+
+ 📅 + 会议日程 +
+
+ 📅 + 会议日程 +
+
+
+ 下载 +
+
+ + +
+ +
+ +
+ + + + \ No newline at end of file diff --git a/proto/html/components/meeting-detail-page.html b/proto/html/components/meeting-detail-page.html new file mode 100644 index 0000000..447dff3 --- /dev/null +++ b/proto/html/components/meeting-detail-page.html @@ -0,0 +1,300 @@ + + + + + + 会议详情页 (合规) + + + + +
+ + + + + +

会议基本信息

+ + +
+
+
+ 项目编号: + ZH-2026-658 +
+
+ 会议名称: + 小牛血清创新应用研讨会 +
+
+ 项目名称: + 小牛血清创新应用研讨会 +
+
+ 期数: + 第 3 期 +
+
+ 总场次/总期数: + 26 +
+
+ 会议开始时间: + 2026.05.13 11:30 +
+
+ 支持单位: + 北京XXXX有限公司 +
+
+ 会议结束时间: + 2026.05.15 11:30 +
+
+ 创建时间: + 2026.05.11 11:30 +
+
+ 创建人员: + 张三 +
+
+
+ + +

*点击缩略图可查看原图和下载

+ + +
+
邀请函
+
+
+ 📄 + 邀请函 +
+
+ 📄 + 邀请函 +
+
+
+ 下载 +
+
+ + +
+
会议日程
+
+
+ 📅 + 会议日程 +
+
+ 📅 + 会议日程 +
+
+
+ 下载 +
+
+ + +
+ +
+ +
+ + + + \ No newline at end of file diff --git a/proto/html/components/meeting-detail.html b/proto/html/components/meeting-detail.html new file mode 100644 index 0000000..c2d54e9 --- /dev/null +++ b/proto/html/components/meeting-detail.html @@ -0,0 +1,300 @@ + + + + + + 会议详情 + + + + +
+ + + + + +

会议基本信息

+ + +
+
+
+ 项目编号: + ZH-2026-658 +
+
+ 会议名称: + 小牛血清创新应用研讨会 +
+
+ 项目名称: + 小牛血清创新应用研讨会 +
+
+ 期数: + 第 3 期 +
+
+ 总场次/总期数: + 26 +
+
+ 会议开始时间: + 2026.05.13 11:30 +
+
+ 支持单位: + 北京XXXX有限公司 +
+
+ 会议结束时间: + 2026.05.15 11:30 +
+
+ 创建时间: + 2026.05.11 11:30 +
+
+ 创建人员: + 张三 +
+
+
+ + +

*点击缩略图可查看原图和下载

+ + +
+
邀请函
+
+
+ 📄 + 邀请函 +
+
+ 📄 + 邀请函 +
+
+
+ 下载 +
+
+ + +
+
会议日程
+
+
+ 📅 + 会议日程 +
+
+ 📅 + 会议日程 +
+
+
+ 下载 +
+
+ + +
+ +
+ +
+ + + + \ No newline at end of file diff --git a/proto/html/components/meeting-manage-1.html b/proto/html/components/meeting-manage-1.html new file mode 100644 index 0000000..2685d6b --- /dev/null +++ b/proto/html/components/meeting-manage-1.html @@ -0,0 +1,575 @@ + + + + + + 会议管理 - 项目管理系统 + + + +
+ + + +

会议管理

+ + +
+
+
+ 项目编号: + +
+
+ 会议名称: + +
+
+ 备注: + +
+
+ 会议时间: + +
+
+ 期: +
+ + + +
+
+
+ 项目形式: + +
+
+ + +
+
+ +
+ + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
项目编号会议ID项目形式会议名称会议开始时间会议结束时间总期数期数当前阶段备注操作
ZH-2026-6585643145673线上小牛血清创新应用研讨会2026.05.11 11:302026.05.11 14:3026第 3 期未执行首次会议 + 提交 + 查看 + 修改 + 复制 +
ZH-2026-6585643145673线上小牛血清创新应用研讨会2026.05.11 11:302026.05.11 14:3026第 3 期执行中XXXXXX + 提交 + 查看 + 修改 + 复制 +
ZH-2026-6585643145673线上小牛血清创新应用研讨会2026.05.11 11:302026.05.11 14:3026第 3 期待审核XXXXXX + 查看 + 修改 + 复制 +
ZH-2026-6585643145673线上小牛血清创新应用研讨会2026.05.11 11:302026.05.11 14:3026第 3 期待结算XXXXXX + 查看 + 修改 + 复制 +
ZH-2026-6585643145673线上小牛血清创新应用研讨会2026.05.11 11:302026.05.11 14:3026第 3 期已完结XXXXXX + 查看 + 复制 +
ZH-2026-6585643145673线上小牛血清创新应用研讨会2026.05.11 11:302026.05.11 14:3026第 3 期冻结中XXXXXX + 查看 + 复制 +
ZH-2026-6585643145673线上小牛血清创新应用研讨会2026.05.11 11:302026.05.11 14:3026第 3 期执行中XXXXXX + 提交 + 查看 + 修改 + 复制 +
ZH-2026-6585643145673线上小牛血清创新应用研讨会2026.05.11 11:302026.05.11 14:3026第 3 期未执行XXXXXX + 提交 + 查看 + 修改 + 复制 +
ZH-2026-6585643145673线上小牛血清创新应用研讨会2026.05.11 11:302026.05.11 14:3026第 3 期执行中XXXXXX + 提交 + 查看 + 修改 + 复制 +
ZH-2026-6585643145673线上小牛血清创新应用研讨会2026.05.11 11:302026.05.11 14:3026第 3 期未执行XXXXXX + 提交 + 查看 + 修改 + 复制 +
ZH-2026-6585643145673线上小牛血清创新应用研讨会2026.05.11 11:302026.05.11 14:3026第 3 期执行中XXXXXX + 提交 + 查看 + 修改 + 复制 +
+
+ + +
+

*执行方只能在项目列表中进行建会

+

*执行方的当前阶段跟支持方基本一致

+

*提交审核后,在OA新建流程时,才能选择该场会,合规人员才能看到,是否为待审核状态时,OA才能看到该场会议

+

*点击复制后,则跳转到新建会议并将会议基本信息自动带入

+

*点击总期数,可以筛选出该系列会的所有会议

+

*提交后,返回提交成功的提示,三秒消失

+

*统一状态(当前阶段): 未执行=会议开始时间前; 执行中=会议开始时间后执行方未提交材料; 待审核=执行方提交材料后合规审核; 待结算=支持方审核通过后; 已完结=合规人员点击结算后; 冻结中=会议结束后未按时提交

+

*冻结中时,提交按钮置灰,无法点击

+

*增加最后期限,到期前24小时,每4小时,给执行方发送短信催促一次

+
+ +
+ + + \ No newline at end of file diff --git a/proto/html/components/meeting-manage.html b/proto/html/components/meeting-manage.html new file mode 100644 index 0000000..908348e --- /dev/null +++ b/proto/html/components/meeting-manage.html @@ -0,0 +1,618 @@ + + + + + + 会议管理 - 项目管理系统 + + + +
+ + + +

会议管理

+ + +
+
+
+ 项目编号: + +
+
+ 会议ID: + +
+
+ 会议名称: + +
+
+ + + +
+
+ 会议时间: + + + +
+
+
+
+ 项目形式: + +
+
+ 当前阶段: + +
+
+ 备注: + +
+
+ + +
+
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
项目编号会议ID项目形式会议名称会议开始时间会议结束时间总期数期数当前阶段备注操作
ZH-2026-6585643145673线上小牛血清创新应用研讨会2026.05.11 11:302026.05.11 14:3026第 3 期未执行首次会议 + 提交 + 查看 + 修改 + 复制 +
ZH-2026-6585643145673线上小牛血清创新应用研讨会2026.05.11 11:302026.05.11 14:3026第 3 期执行中XXXXXX + 提交 + 查看 + 修改 + 复制 +
ZH-2026-6585643145673线上小牛血清创新应用研讨会2026.05.11 11:302026.05.11 14:3026第 3 期待审核XXXXXX + 查看 + 修改 + 复制 +
ZH-2026-6585643145673线上小牛血清创新应用研讨会2026.05.11 11:302026.05.11 14:3026第 3 期待结算XXXXXX + 查看 + 修改 + 复制 +
ZH-2026-6585643145673线上小牛血清创新应用研讨会2026.05.11 11:302026.05.11 14:3026第 3 期已完结XXXXXX + 查看 + 复制 +
ZH-2026-6585643145673线上小牛血清创新应用研讨会2026.05.11 11:302026.05.11 14:3026第 3 期冻结中XXXXXX + 查看 + 复制 +
ZH-2026-6585643145673线上小牛血清创新应用研讨会2026.05.11 11:302026.05.11 14:3026第 3 期执行中XXXXXX + 提交 + 查看 + 修改 + 复制 +
ZH-2026-6585643145673线上小牛血清创新应用研讨会2026.05.11 11:302026.05.11 14:3026第 3 期未执行XXXXXX + 提交 + 查看 + 修改 + 复制 +
ZH-2026-6585643145673线上小牛血清创新应用研讨会2026.05.11 11:302026.05.11 14:3026第 3 期执行中XXXXXX + 提交 + 查看 + 修改 + 复制 +
ZH-2026-6585643145673线上小牛血清创新应用研讨会2026.05.11 11:302026.05.11 14:3026第 3 期未执行XXXXXX + 提交 + 查看 + 修改 + 复制 +
ZH-2026-6585643145673线上小牛血清创新应用研讨会2026.05.11 11:302026.05.11 14:3026第 3 期执行中XXXXXX + 提交 + 查看 + 修改 + 复制 +
+
+ + +
+

*执行方只能在项目列表中进行建会

+

*执行方的当前阶段跟支持方基本一致

+

*提交审核后,在OA新建流程时,才能选择该场会,合规人员才能看到,是否为待审核状态时,OA才能看到该场会议

+

*点击复制后,则跳转到新建会议并将会议基本信息自动带入

+

*点击总期数,可以筛选出该系列会的所有会议

+

*提交后,返回提交成功的提示,三秒消失

+

*统一状态(当前阶段): 未执行=会议开始时间前; 执行中=会议开始时间后执行方未提交材料; 待审核=执行方提交材料后合规审核; 待结算=支持方审核通过后; 已完结=合规人员点击结算后; 冻结中=会议结束后未按时提交

+

*冻结中时,提交按钮置灰,无法点击

+

*增加最后期限,到期前24小时,每4小时,给执行方发送短信催促一次

+
+ +
+ + + \ No newline at end of file diff --git a/proto/html/components/meetings.html b/proto/html/components/meetings.html new file mode 100644 index 0000000..d8beb3e --- /dev/null +++ b/proto/html/components/meetings.html @@ -0,0 +1,546 @@ + + + + + + 我参与的会议 + + + + +
+ + + + + + + +
+
筛选条件
+
+
+ 项目编号: + +
+
+ 会议名称: + +
+
+ 当前阶段: + +
+
+ 签署状态: + +
+ +
+
+ + +

*邀请函根据执行方上传的邀请函模板和专家列表进行生成并显示到专家的会议列表中

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
项目编号会议名称当前阶段日程邀请函签署状态操作
ZH-2026-658小牛血清创新应用研讨会未开始查看 下载查看 下载未签署签署劳务
ZH-2026-659整合医学学会项目评审会已结束查看 下载查看 下载已签署查看劳务
ZH-2026-650智慧医院建设项目方案论证会未开始查看 下载查看 下载未签署签署劳务
ZH-2026-645基层医疗改革试点中期评估会未开始查看 下载查看 下载未签署签署劳务
ZH-2026-640数字化医疗转型方案评审会已结束查看 下载查看 下载已签署查看劳务
ZH-2026-635临床医学研究方案评议会未开始查看 下载查看 下载未签署签署劳务
ZH-2026-628公共卫生应急体系建设研讨会已结束查看 下载查看 下载已签署查看劳务
ZH-2026-622中医诊疗标准化研究论坛未开始查看 下载查看 下载未签署签署劳务
ZH-2026-618医疗 AI 辅助诊断应用论坛未开始查看 下载查看 下载未签署签署劳务
ZH-2026-610医院信息化标准研讨会已结束查看 下载查看 下载已签署查看劳务
ZH-2026-605重大疾病防控体系建设讨论会未开始查看 下载查看 下载未签署签署劳务
+
+ + +
+

*未签署时显示「签署劳务」,已签署后显示「查看劳务」

+

*在会议编辑中,导入专家信息,并点击推送电子签后,专家才可以查看到签署按钮

+

*点击签署劳务跳转到电子签

+

*签署劳务协议时,先验证是否有执业证书、职称证明、身份证照片,如果没有,必须先上传

+
+ +
+ + + + + + + + + \ No newline at end of file diff --git a/proto/html/components/messages.html b/proto/html/components/messages.html new file mode 100644 index 0000000..a1b9415 --- /dev/null +++ b/proto/html/components/messages.html @@ -0,0 +1,431 @@ + + + + + + 消息通知 + + + + +
+ + + + + + + +
+
+
+ + + 标为已读 + + 删除选中 +
+ 共 12 条 +
+ +
+ +
+
+ 系统通知: 您有新的会议邀请 + 2 小时前 +
+
+ 2024 年度临床医学研究项目专家评审会议邀请已发出,时间:2026-08-05 09:00,请准时参加。会议链接将通过短信发送。 +
+
+
+
+ +
+ +
+
+ 审核结果: 投稿方案已通过初审 + 昨天 16:30 +
+
+ 您提交的《基层卫生服务能力提升方案》已通过初审,请等待终审结果。如需修改请在终审前完成。 +
+
+
+
+ +
+ +
+
+ 项目动态: 新阶段开启 + 2 天前 +
+
+ 您参与的"基层医疗改革试点项目"已进入实施阶段,请关注后续任务安排。 +
+
+
+
+ +
+ +
+
+ 系统升级公告 + 2026-07-30 +
+
+ 系统将于本周日 02:00 - 04:00 进行升级维护,请提前安排工作。升级期间可能无法访问。 +
+
+
+
+ +
+ +
+
+ 会议提醒: 智慧医院方案论证会 + 2026-07-28 +
+
+ 您报名的"智慧医院建设项目方案论证会"将于 2026-08-12 14:00 举行,请提前查看会议材料并准备发言。 +
+
+
+
+ +
+ +
+
+ 审核结果: 投稿已被退回 + 2026-07-25 +
+
+ 您提交的《远程医疗会诊系统方案》需要补充执行细节,已退回。请修改后重新提交。 +
+
+
+
+ +
+ +
+
+ 账号安全提醒 + 2026-07-20 +
+
+ 您的账号在异地登录,如非本人操作请及时修改密码。建议定期更换登录密码以保障账号安全。 +
+
+
+
+ +
+ +
+
+ 协议签署完成通知 + 2026-07-15 +
+
+ 您的劳务协议已签署完成,可前往"我参与的会议"或"我的项目设计投稿"查看详情。 +
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/proto/html/components/my-activities.html b/proto/html/components/my-activities.html new file mode 100644 index 0000000..395fd37 --- /dev/null +++ b/proto/html/components/my-activities.html @@ -0,0 +1,413 @@ + + + + + + 我的执会 + + + + +
+ + + + + +
+
+
+ 项目编号: + +
+
+ 会议名称: + +
+
+ 是否结题: + +
+
+ 是否系列会: + +
+ +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
项目编号会议名称是否系列会期数/总期数当前阶段操作
ZH-2026-658小牛血清创新应用研讨会3/26已执行 + 提交 + 查看 + 修改 + 复制 +
ZH-2026-650整合医学学会项目评审会5/26已执行 + 查看 + 修改 + 复制 +
ZH-2026-645基层医疗改革试点中期评估2/12未执行 + 提交 + 查看 + 修改 + 复制 +
ZH-2026-640智慧医院建设项目方案论证1/8已执行 + 查看 + 修改 + 复制 +
ZH-2026-635数字化医疗转型方案评审3/15未执行 + 提交 + 查看 + 修改 + 复制 +
+
+ 已选 0 +
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/proto/html/components/my-registrations.html b/proto/html/components/my-registrations.html new file mode 100644 index 0000000..3d4bb23 --- /dev/null +++ b/proto/html/components/my-registrations.html @@ -0,0 +1,317 @@ + + + + + + 我报名的项目 + + + + +
+ + + + +

我报名的项目

+

查看您已报名的项目,跟踪项目状态

+ + +
+
+
+ 项目编号: + +
+
+ 项目名称: + +
+
+ 会议名称: + +
+ +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
项目编号项目名称会议名称状态操作
+ + +

*下载项目公示的红头文件

+
+ +
+ + + + \ No newline at end of file diff --git a/proto/html/components/new-edit.html b/proto/html/components/new-edit.html new file mode 100644 index 0000000..ad7991d --- /dev/null +++ b/proto/html/components/new-edit.html @@ -0,0 +1,88 @@ + + + + + 新建/编辑 + + + +
+ +

新建人员

+
+
+
+
+
+
+
+
+ +
+

*所有字段必填;手机号唯一;新建后可在人员列表中查看

+
+ + +
+
+
+
+ + + \ No newline at end of file diff --git a/proto/html/components/new-expert.html b/proto/html/components/new-expert.html new file mode 100644 index 0000000..ad34ec5 --- /dev/null +++ b/proto/html/components/new-expert.html @@ -0,0 +1,137 @@ + + + + + 新建专家 + + + +
+ +

新建专家

+
+
+
+
+
+
+
+ +
+
+
+ + + +
+
+
+
+ + + +
+
+
+
+
+ + + +
+
+
+
+
+ +
+
+ +
+
+ +
+ + +
+
+
+
+ + + \ No newline at end of file diff --git a/proto/html/components/new-meeting.html b/proto/html/components/new-meeting.html new file mode 100644 index 0000000..17ceb6f --- /dev/null +++ b/proto/html/components/new-meeting.html @@ -0,0 +1,248 @@ + + + + + + + 新建会议 - 项目管理系统 + + + +
+ + + + + +

新建会议

+ + +

*会议名称默认为项目名称,不可修改

+ + +
+
+ +
+ 项目编号: + ZH-2026-658 +
+
+ 项目名称: + 小牛血清创新应用研讨会 +
+
+ 总场次/总期数: + 26 +
+
+ 支持单位: + 北京XXXX有限公司 +
+
+ 创建时间: + 2026.05.11 11:30 +
+
+ 创建人员: + 张三 +
+ + +
+ 会议名称: + 小牛血清创新应用研讨会 +
+
+ 期数: + +
+
+ 会议开始时间: + +
+
+ 会议结束时间: + +
+
+ 备注: + +
+
+ + +
+ + +
+
+ +
+ + + + \ No newline at end of file diff --git a/proto/html/components/new-person-2.html b/proto/html/components/new-person-2.html new file mode 100644 index 0000000..4c0f52e --- /dev/null +++ b/proto/html/components/new-person-2.html @@ -0,0 +1,118 @@ + + + + + 新建人员 (合规) v2 + + + +
+ +

新建人员

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+

*手机号为唯一值

+

*全部为必填项

+
+ + +
+
+
+
+ + + \ No newline at end of file diff --git a/proto/html/components/new-person.html b/proto/html/components/new-person.html new file mode 100644 index 0000000..5d8731c --- /dev/null +++ b/proto/html/components/new-person.html @@ -0,0 +1,380 @@ + + + + + + 新建人员 - 项目管理系统 + + + + +
+ + + + + +

新建人员

+ + +
+

*手机号为唯一值

+

*全部为必填项

+
+ + +
+
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ +
+ + +
+ + +
+
+
+
+ + + + \ No newline at end of file diff --git a/proto/html/components/new-project.html b/proto/html/components/new-project.html new file mode 100644 index 0000000..fbfc5c5 --- /dev/null +++ b/proto/html/components/new-project.html @@ -0,0 +1,645 @@ + + + + + + 新建项目 - 项目管理系统 + + + +
+ + + + + + + + +
+ +
+ + +
+
项目信息
+ +
+
+ *项目名称 + +
+
+ *项目编号 + +
+
+ +
+
+ *项目负责人 + +
+
+ *项目形式 + +
+
+ +
+
+ *总场次/总期数 + +
+
+ *总金额 + +
+
+ +
+
+ *项目开始时间 + +
+
+ *项目结束时间 + +
+
+ +
+
+ *是否招标项目 + +
+
+
+ + +
+
角色劳务设置
+ +
+

*角色劳务设置针对该项目所有会议生效,

+

专家劳务明细当会议劳务金额超过角色劳务设置时,

+

该劳务金额为税后金额,则系统进行提示:该角色劳务金额已超

+
+ +
+
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+
+ +
+ + +
+ +

*当选择其他时,需手动填写角色名称,显示填写框

+
+ + +
+
公告文件
+ +
+
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+
+ +

*名称规则:公告文件类型+取项目编号的后两段+项目名称

+
+ + +
+ + + +
+ +
+ + + + \ No newline at end of file diff --git a/proto/html/components/new-submission.html b/proto/html/components/new-submission.html new file mode 100644 index 0000000..451a2f3 --- /dev/null +++ b/proto/html/components/new-submission.html @@ -0,0 +1,376 @@ + + + + + + 新建项目设计投稿 + + + + +
+ + + + + +

新建项目设计投稿

+ + +
+

*项目规划后台可配置,合规/战略/学术进行配置,待定

+
+ +
+
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ +
+ + +
+
+ + +
+ +
+ + +
0/500
+
+
+ +
+ + +

*除备注外,其他均为必填项

+ + +
+ + +
+
+
+ +
+ + + + \ No newline at end of file diff --git a/proto/html/components/overview.html b/proto/html/components/overview.html new file mode 100644 index 0000000..855152c --- /dev/null +++ b/proto/html/components/overview.html @@ -0,0 +1,293 @@ + + + + + + + 首页 - 项目管理系统 + + + +
+ + + + +
+ +
项目总数量
+
26
+
查看全部项目
+
+ +
已结算数量
+
16
+
查看已结算项目
+
+ +
已结题数量
+
10
+
查看已结题项目
+
+ +
已执行会议
+
58
+
查看已执行会议
+
+ +
未执行会议
+
68
+
查看未执行会议
+
+
+ + +

*点击数字,跳转到相应的列表页

+ + +
+

+ 消息通知 + 更多 → +

+ +
+ +
+ + \ No newline at end of file diff --git a/proto/html/components/people-1-1.html b/proto/html/components/people-1-1.html new file mode 100644 index 0000000..60146de --- /dev/null +++ b/proto/html/components/people-1-1.html @@ -0,0 +1,74 @@ + + + + + 人员管理 v1.1 - 合规 + + + +
+

人员管理 (合规)

+
+
+
姓名:
+
手机号:
+
角色: + +
+ +
+
+
+ + + *新建人员时,管理员可指定角色:项目负责人 / 会议执行 / 监察员 +
+
+ + + + + + + + + +
姓名手机号工作单位部门职务角色操作
张三13534621147北京XXXX有限公司合规部经理项目负责人查看编辑禁用
李四13534621148北京XXXX有限公司合规部专员会议执行查看编辑禁用
王五13534621149北京XXXX有限公司监察组组长监察员查看编辑禁用
赵六13534621150北京XXXX有限公司监察组专员监察员查看编辑禁用
钱七13534621151北京XXXX有限公司合规部主管项目负责人查看编辑禁用
+
+
+ + + \ No newline at end of file diff --git a/proto/html/components/people-1-2.html b/proto/html/components/people-1-2.html new file mode 100644 index 0000000..709a598 --- /dev/null +++ b/proto/html/components/people-1-2.html @@ -0,0 +1,74 @@ + + + + + 人员管理 v1.1 - 合规 + + + +
+

人员管理 v1.2 (合规)

+
+
+
姓名:
+
手机号:
+
角色: + +
+ +
+
+
+ + + *新建人员时,管理员可指定角色:项目负责人 / 会议执行 / 监察员 +
+
+ + + + + + + + + +
姓名手机号工作单位部门职务角色操作
张三13534621147北京XXXX有限公司合规部经理项目负责人查看编辑禁用
李四13534621148北京XXXX有限公司合规部专员会议执行查看编辑禁用
王五13534621149北京XXXX有限公司监察组组长监察员查看编辑禁用
赵六13534621150北京XXXX有限公司监察组专员监察员查看编辑禁用
钱七13534621151北京XXXX有限公司合规部主管项目负责人查看编辑禁用
+
+
+ + + \ No newline at end of file diff --git a/proto/html/components/people.html b/proto/html/components/people.html new file mode 100644 index 0000000..92f95a9 --- /dev/null +++ b/proto/html/components/people.html @@ -0,0 +1,434 @@ + + + + + + + 人员管理 - 项目管理系统 + + + +
+ + + + + + +
+
筛选条件
+
+
+ 姓名: + +
+
+ 手机号: + +
+
+ 工作单位: + +
+
+ 部门: + +
+
+ 职务: + +
+ +
+
+ + +
+ + +
+ + +
+

*角色:执行方管理员新建人员时,默认只有会议执行的角色

+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
姓名手机号工作单位部门职务角色状态操作
张三13534621147北京XXXX有限公司会务部经理会务执行正常 + 查看 编辑 + 禁用 +
李四13534621148北京XXXX有限公司会务部专员会务执行正常 + 查看 编辑 + 禁用 +
王五13534621149北京XXXX有限公司会务部主管会务执行正常 + 查看 编辑 + 禁用 +
赵六13534621150北京XXXX有限公司会务部专员会务执行正常 + 查看 编辑 + 禁用 +
钱七13534621151北京XXXX有限公司会务部经理会务执行正常 + 查看 编辑 + 禁用 +
孙八13534621152北京XXXX有限公司会务部专员会务执行正常 + 查看 编辑 + 禁用 +
周九13534621153北京XXXX有限公司会务部主管会务执行禁用 + 查看 编辑 + 启用 +
吴十13534621154北京XXXX有限公司会务部专员会务执行正常 + 查看 编辑 + 禁用 +
+
+ +
+ + + + \ No newline at end of file diff --git a/proto/html/components/project-assign-1.html b/proto/html/components/project-assign-1.html new file mode 100644 index 0000000..cb04f0f --- /dev/null +++ b/proto/html/components/project-assign-1.html @@ -0,0 +1,602 @@ + + + + + + 项目分配 v1 - 项目管理系统 + + + +
+ + + + + + + + +
+
项目信息
+ +
+
+ 项目编号: + ZH-2026-658 +
+
+ 项目名称: + 小牛血清创新应用研讨会 +
+
+ 项目形式: + 线上 +
+
+ 总场次/总期数: + 26 +
+
+ 总金额: + 3,112,000 +
+
+ 管理费及税金: + 1,112,000 +
+
+
+ + +
+
+ *支持单位 + +
+

*支持单位只有一个,支持模糊查找,选择项

+
+ + +
+
+ *执行单位 + +
+ +
+
+ 1. + +
+ 场数: + + 总金额: + +
+
+ + +
+
+ +
+ 2. + +
+ 场数: + + 总金额: + +
+
+ + +
+
+ +
+ 3. + +
+ 场数: + + 总金额: + +
+
+ + +
+
+
+ + + +

+ *支持多个执行方,给每个执行方分配场次和总金额;
+ 提交时验证多个执行方的总金额加起来是否超过项目总金额;
+ 执行方总金额显示在执行方的项目列表中对应项目的总金额 +

+ +

+ *如未选择执行单位,则场次和总金额为非必填项,如选择了执行单位,则为必填项
+ *可用金额=总金额×(1-管理费税费)-累计劳务-累计会务(总发票的金额)
+ *项目列表中的为该项目的总数,执行方项目列表中显示的为该执行方的总金额,计算公式保持一致
+ *选择后,该项目则出现在支持单位管理员的项目列表中 +

+
+ + +
+
+ 提交材料截止时间 + + +
+

*按照会议截止时间,自然日,往后推对应的天数。如果提交后,被退回了,则时间重新开始计算。

+

*天数最大不能超过100,超过后报错提示

+

*每天0点取一次,合同可能会有多个,显示多个

+

+ 合同文件2 +

+
+ + +
+
+
+ 支持合同: + 合同文件 +
+
+ 执行合同: + 合同文件 + 合同文件2 +
+
+

*根据项目编号,同步OA的合同文件,可预览

+
+ + +
+ + +
+ +
+ + + + \ No newline at end of file diff --git a/proto/html/components/project-assign.html b/proto/html/components/project-assign.html new file mode 100644 index 0000000..7c33b86 --- /dev/null +++ b/proto/html/components/project-assign.html @@ -0,0 +1,663 @@ + + + + + + 项目分配 - 项目管理系统 + + + +
+ + + + + + + + +
+
项目信息
+ +
+
+ 项目编号: + ZH-2026-658 +
+
+ 项目名称: + 小牛血清创新应用研讨会 +
+
+ 项目形式: + 线上 +
+
+ 总场次/总期数: + 26 +
+
+ 总金额: + 3,112,000 +
+
+ 管理费及税金: + 1,112,000 +
+
+
+ + +
+
+ *支持单位 + +
+

*支持单位只有一个,支持模糊查找,选择项

+
+ + +
+
+ *执行单位 + +
+ +
+
+ 1. + +
+ 场数: + + 总金额: + +
+
+ + +
+
+ +
+ 2. + +
+ 场数: + + 总金额: + +
+
+ + +
+
+ +
+ 3. + +
+ 场数: + + 总金额: + +
+
+ + +
+
+
+ + + +

+ *支持多个执行方,给每个执行方分配场次和总金额;
+ 提交时验证多个执行方的总金额加起来是否超过项目总金额;
+ 执行方总金额显示在执行方的项目列表中对应项目的总金额 +

+ +

+ *如未选择执行单位,则场次和总金额为非必填项,如选择了执行单位,则为必填项
+ *可用金额=总金额×(1-管理费税费)-累计劳务-累计会务(总发票的金额)
+ *项目列表中的为该项目的总数,执行方项目列表中显示的为该执行方的总金额,计算公式保持一致
+ *选择后,该项目则出现在支持单位管理员的项目列表中 +

+
+ + +
+
+ 提交材料截止时间 + + +
+

*按照会议截止时间,自然日,往后推对应的天数。如果提交后,被退回了,则时间重新开始计算。

+

*天数最大不能超过100,超过后报错提示

+

*每天0点取一次,合同可能会有多个,显示多个

+

+ 合同文件2 +

+
+ + +
+
+
+ 支持合同: + 合同文件 +
+
+ 执行合同: + 合同文件 + 合同文件2 +
+
+

*根据项目编号,同步OA的合同文件,可预览

+
+ + +
+ + +
+ +
+ + + + \ No newline at end of file diff --git a/proto/html/components/project-detail-compliance.html b/proto/html/components/project-detail-compliance.html new file mode 100644 index 0000000..d369220 --- /dev/null +++ b/proto/html/components/project-detail-compliance.html @@ -0,0 +1,238 @@ + + + + + 项目详情 + + + +
+ +

项目详情

+ + +
+
项目信息
+
+
项目编号:
+
项目名称:
+
总场次/总期数:
+
总金额:
+
管理费及税金:
+
支持单位:
+
服务机构:
+
项目形式:
+
项目评价:4.5
+
+

*点击后可查看具体评价维度,评价标准与供应商系统保持一致

+
+ + +
+
支持单位监督员
+
+
+ 1. + 张三 +
+ 删除 +
+
+
+ 2. + 李四 +
+ 删除 +
+
+
+ +

*增加只显示已点支持意向的

+
+ + +
+
角色劳务设置
+ + + + + + +
角色劳务金额
主席5,000
主持3,000
+
+ + +
+
公告文件
+ +
+
邀请函
+ +
+ +
+
已发布公告
+ +
+ +
+
支持函
+ +
+
+ +

*项目详情可修改,只有创建人可修改

+ +
+ +
+
+ + + + \ No newline at end of file diff --git a/proto/html/components/project-detail-executor.html b/proto/html/components/project-detail-executor.html new file mode 100644 index 0000000..50263b9 --- /dev/null +++ b/proto/html/components/project-detail-executor.html @@ -0,0 +1,117 @@ + + + + + + + 项目详情 - 项目管理系统 + + + +
+ + + +

项目详情

+ + +
+
项目基本信息
+
+
项目编号:ZH-2026-658
+
项目形式:线上
+
项目名称:小牛血清创新应用研讨会
+
支持单位:北京XXXX有限公司
+
总场次/总期数:26
+
服务机构:北京XXXX公司
+
总金额:3,112,000
+
项目开始时间:2026.05.11
+
管理费及税金:1,112,000
+
项目结束时间:2026.06.30
+
+
+ + +
+
项目评价
+
项目评价:4.5
+

*点击后可查看具体评价维度,评价标准与供应商系统保持一致

+
+ + +
+
角色劳务设置
+ + + + + + + + +
角色劳务金额操作
主席5000查看下载
主持3000查看下载
+
+ + +
+
公告文件
+
+
邀请函:查看下载
+
支持函:查看下载
+
+
+ + +
+ +
+ +
+ + + + \ No newline at end of file diff --git a/proto/html/components/project-detail.html b/proto/html/components/project-detail.html new file mode 100644 index 0000000..baa663e --- /dev/null +++ b/proto/html/components/project-detail.html @@ -0,0 +1,219 @@ + + + + + + 项目详情 - 项目管理系统 + + + + +
+ + + + + +

项目详情

+ + +
+
+
+ 项目编号: + ZH-2026-658 +
+
+ 项目名称: + 小牛血清创新应用研讨会 +
+
+ 会议名称: + 小牛血清创新应用研讨会 +
+
+ 项目形式: + 线上 +
+
+ 状态: + 已报名 +
+
+
+ + +
+
公告
+
+
+ 邀请函: + 查看 + 下载 +
+
+
+ + +
+ +
+ +
+ + + + \ No newline at end of file diff --git a/proto/html/components/project-manage.html b/proto/html/components/project-manage.html new file mode 100644 index 0000000..2c20163 --- /dev/null +++ b/proto/html/components/project-manage.html @@ -0,0 +1,676 @@ + + + + + 项目管理 + + + +
+ + +
+
+
支持单位:
+
服务机构:
+
项目形式: + +
+
项目编号:
+
+
+
项目名称:
+
项目时间: +
+
+
是否结题: + +
+
评价得分:
+
+
+ + + + + +
+
+ +

*项目分配给执行方,执行方管理员只能看到已分配的项目

+

*支持多个支持单位筛选

+

*支持按查询条件进行项目评价导出

+

*点击后可查看具体评价维度,评价标准与供应商系统保持一致

+ +
+ 新建项目 + 项目分配 + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
项目编号项目名称总场次/总期数已执行未执行总金额可用金额已支付劳务费已支付会务费执行单位得分(合规)执行单位评价(支持方)支持单位服务机构项目形式是否结题项目开始时间项目结束时间操作
ZH-2026-658小牛血清创新应用研讨会2616103,112,0001,112,0001,000,0001,000,0005.0-北京XXXX有限公司北京XXXX公司线上未结题2026-05-112026-06.30 + 建会 + 分配 + 结题 + 查看 + 编辑 + 开通 + 删除公告 + 执行单位评分 +
ZH-2026-659整合医学学会项目评审181262,500,000980,000900,000620,0004.53.5北京XXXX有限公司北京XXXX公司线下未结题2026-05-112026-06.30 + 建会 + 分配 + 结题 + 查看 + 编辑 + 开通 + 删除公告 + 执行单位评分 +
ZH-2026-650智慧医院建设项目12751,800,000760,000700,000340,0004.04.5北京XXXX有限公司北京XXXX公司线上+线下未结题2026-05-112026-06.30 + 建会 + 分配 + 结题 + 查看 + 编辑 + 开通 + 删除公告 + 执行单位评分 +
ZH-2026-645基层医疗改革试点项目201462,800,0001,050,000950,000800,0004.83.0北京XXXX有限公司北京XXXX公司线下未结题2026-05-112026-06.30 + 建会 + 分配 + 结题 + 查看 + 编辑 + 开通 + 删除公告 + 执行单位评分 +
ZH-2026-640数字化医疗转型方案221573,000,0001,200,0001,100,000700,0004.24.0北京XXXX有限公司北京XXXX公司线上未结题2026-05-112026-06.30 + 建会 + 分配 + 结题 + 查看 + 编辑 + 开通 + 删除公告 + 执行单位评分 +
ZH-2026-635临床医学研究方案评议14952,200,000820,000740,000640,0004.64.2北京XXXX有限公司北京XXXX公司线下已结题2026-05-112026-06.30 + 建会 + 结题 + 查看 + 分配 + 开通 + 编辑 + 删除公告 + 执行单位评分 +
ZH-2026-628公共卫生应急体系建设项目161152,400,000900,000820,000680,0004.43.8北京XXXX有限公司北京XXXX公司线下未结题2026-05-112026-06.30 + 建会 + 分配 + 结题 + 查看 + 编辑 + 开通 + 删除公告 + 执行单位评分 +
ZH-2026-622中医诊疗标准化研究10641,500,000560,000500,000440,0004.74.5北京XXXX有限公司北京XXXX公司线下未结题2026-05-112026-06.30 + 建会 + 分配 + 结题 + 查看 + 编辑 + 开通 + 删除公告 + 执行单位评分 +
ZH-2026-618医疗 AI 辅助诊断应用论坛8441,200,000480,000420,000300,0004.94.8北京XXXX有限公司北京XXXX公司线上+线下未结题2026-05-112026-06.30 + 建会 + 分配 + 结题 + 查看 + 编辑 + 开通 + 删除公告 + 执行单位评分 +
+
+
+ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/proto/html/components/project-plan.html b/proto/html/components/project-plan.html new file mode 100644 index 0000000..1e1bdc6 --- /dev/null +++ b/proto/html/components/project-plan.html @@ -0,0 +1,644 @@ + + + + + + 项目策划方案管理 + + + +
+ + + + +
+
+
+ 策划方案名称: + +
+
+ 项目方向: + +
+
+ 结题状态: + +
+
+ 备注: + +
+
+ +
+
+ 项目类别: + +
+
+ 是否结算: + +
+
+ 项目编号: + +
+ +
+
+ + +
+

*批量审核:策划案名称,投稿ID,审核结果,审核意见,只能审核不通过

+
+ + +
+ + +
+ + +

*项目形式是否保留?

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
策划方案名称项目方向项目类别项目形式设计文件状态是否结算项目编号备注操作
小牛血清创新应用研讨会专项计划一:《规范化诊疗及医疗质.....会议项目线上查看 下载待审核已结算ZH-2026-658XXXXXX + 审核 + 修改 + 结算 +
小牛血清创新应用研讨会专项计划一:《规范化诊疗及医疗质.....会议项目线下查看 下载审核通过未结算ZH-2026-658XXXXXX + 审核 + 修改 + 结算 +
小牛血清创新应用研讨会专项计划一:《规范化诊疗及医疗质.....会议项目线上+线下查看 下载已退回未结算ZH-2026-658XXXXXX + 审核 + 修改 + 结算 +
整合医学学会项目设计专项计划一:《规范化诊疗及医疗质.....科研项目线上查看 下载待审核未结算ZH-2026-659XXXXXX + 审核 + 修改 + 结算 +
智慧医院建设项目方案专项计划一:《规范化诊疗及医疗质.....学术项目线下查看 下载审核通过未结算ZH-2026-650XXXXXX + 审核 + 修改 + 结算 +
基层医疗改革试点方案专项计划一:《规范化诊疗及医疗质.....指南线下查看 下载待审核未结算ZH-2026-645XXXXXX + 审核 + 修改 + 结算 +
公共卫生应急体系建设方案专项计划一:《规范化诊疗及医疗质.....对外交流线上查看 下载审核通过未结算ZH-2026-640XXXXXX + 审核 + 修改 + 结算 +
数字化医疗转型方案专项计划一:《规范化诊疗及医疗质.....共识线下查看 下载已退回未结算ZH-2026-635XXXXXX + 审核 + 修改 + 结算 +
+
+ +
+ + + + + + + + + + \ No newline at end of file diff --git a/proto/html/components/projects.html b/proto/html/components/projects.html new file mode 100644 index 0000000..ff806fa --- /dev/null +++ b/proto/html/components/projects.html @@ -0,0 +1,430 @@ + + + + + + + 项目列表 - 项目管理系统 + + + +
+ + + + +
+
筛选条件
+
+
+ 项目编号: + +
+
+ 项目名称: + +
+
+ 项目时间: + + + +
+ +
+
+ + +
+ +
+ + +
+

*支持将项目分配给执行人员

+

*执行方看到的总金额是项目分配时设置的总金额,根据相同的公式进行计算可用金额

+

*执行方看到的总金额是项目分配时设置的总金额

+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
项目编号项目名称总场次/总期数已执行未执行总金额可用金额已支付劳务费已支付会务费项目开始时间项目结束时间操作
ZH-2026-658小牛血清创新应用研讨会2616102,400,000900,000820,000680,0002026.05.132026.06.15 + 建会 + 查看 +
ZH-2026-659整合医学学会项目评审181262,400,000900,000820,000680,0002026.05.132026.06.15 + 建会 + 查看 +
ZH-2026-650智慧医院建设项目12751,500,000560,000500,000440,0002026.06.012026.08.30 + 建会 + 查看 +
ZH-2026-645基层医疗改革试点项目201461,500,000560,000500,000440,0002026.06.012026.08.30 + 建会 + 查看 +
ZH-2026-640数字化医疗转型方案221571,500,000560,000500,000440,0002026.06.012026.08.30 + 建会 + 查看 +
ZH-2026-635临床医学研究方案评议14952,400,000900,000820,000680,0002026.05.132026.06.15 + 建会 + 查看 +
ZH-2026-628公共卫生应急体系建设项目161151,500,000560,000500,000440,0002026.06.012026.08.30 + 建会 + 查看 +
ZH-2026-622中医诊疗标准化研究10642,400,000900,000820,000680,0002026.05.132026.06.15 + 建会 + 查看 +
+
+ +
+ + + + \ No newline at end of file diff --git a/proto/html/components/settle-labor.html b/proto/html/components/settle-labor.html new file mode 100644 index 0000000..9f35282 --- /dev/null +++ b/proto/html/components/settle-labor.html @@ -0,0 +1,84 @@ + + + + + 结算-劳务 + + + +
+ +

结算-会务

+
+
会议名称:小牛血清创新应用研讨会
+
项目编号:ZH-2026-658
+
执行单位:北京会务服务公司
+
会议期数:第 3 期
+
会议时间:2026-05-11 11:30 ~ 14:30
+
+
+
劳务费用明细
+
+ + + + + + + + + +
费用项单价数量小计备注审核状态
专家劳务费¥ 5,0001¥ 5,000会议室 A已审核
评审费¥ 3,0001¥ 3,000投影+音响已审核
差旅费¥ 20030¥ 6,000工作午餐已审核
材料费¥ 5040¥ 2,000会议手册待审核
其他费用¥ 1,0001¥ 1,000茶水等已审核
+
+
+
已审核¥ 16,000
+
待审核¥ 2,000
+
合计¥ 18,000
+
+
+
+ + +
+
+ + + \ No newline at end of file diff --git a/proto/html/components/settle-meeting.html b/proto/html/components/settle-meeting.html new file mode 100644 index 0000000..3f70e2b --- /dev/null +++ b/proto/html/components/settle-meeting.html @@ -0,0 +1,84 @@ + + + + + 结算-会务 + + + +
+ +

结算-会务

+
+
会议名称:小牛血清创新应用研讨会
+
项目编号:ZH-2026-658
+
执行单位:北京会务服务公司
+
会议期数:第 3 期
+
会议时间:2026-05-11 11:30 ~ 14:30
+
+
+
会务费用明细
+
+ + + + + + + + + +
费用项单价数量小计备注审核状态
场地租赁¥ 5,0001¥ 5,000会议室 A已审核
设备租赁¥ 3,0001¥ 3,000投影+音响已审核
餐饮服务¥ 20030¥ 6,000工作午餐已审核
资料印刷¥ 5040¥ 2,000会议手册待审核
其他杂费¥ 1,0001¥ 1,000茶水等已审核
+
+
+
已审核¥ 16,000
+
待审核¥ 2,000
+
合计¥ 18,000
+
+
+
+ + +
+
+ + + \ No newline at end of file diff --git a/proto/html/components/sponsor-account.html b/proto/html/components/sponsor-account.html new file mode 100644 index 0000000..82d7cd6 --- /dev/null +++ b/proto/html/components/sponsor-account.html @@ -0,0 +1,40 @@ + + + + + 账号信息 + + + +
+

账号信息

+
+
+
+
+
+
+
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/proto/html/components/sponsor-home.html b/proto/html/components/sponsor-home.html new file mode 100644 index 0000000..ab354ab --- /dev/null +++ b/proto/html/components/sponsor-home.html @@ -0,0 +1,73 @@ + + + + + + + 首页 - 项目管理系统 + + + +
+

首页

+ +
+
项目总数量
26
查看全部项目
+
已结算数量
16
查看已结算项目
+
已结题数量
10
查看已结题项目
+
已执行会议
58
查看已执行会议
+
未执行会议
68
查看未执行会议
+
+ +

*点击数字,跳转到相应的列表页

+ +
+

消息通知更多 →

+ +
+
+ + \ No newline at end of file diff --git a/proto/html/components/sponsor-meeting-detail.html b/proto/html/components/sponsor-meeting-detail.html new file mode 100644 index 0000000..0a12d32 --- /dev/null +++ b/proto/html/components/sponsor-meeting-detail.html @@ -0,0 +1,34 @@ + + + + + 会议详情 + + + +
+ +

会议详情

+
会议基本信息
+
+
项目编号:ZH-2026-658
会议名称:小牛血清创新应用研讨会
+
项目名称:小牛血清创新应用研讨会
期数:3/26
+
总场次/总期数:26
会议开始时间:2026.05.13 11:30
+
支持单位:北京XXXX有限公司
会议结束时间:2026.05.15 11:30
+
创建时间:2026.05.11 11:30
创建人员:张三
+
+
+
+ + + \ No newline at end of file diff --git a/proto/html/components/sponsor-meetings.html b/proto/html/components/sponsor-meetings.html new file mode 100644 index 0000000..aa6b281 --- /dev/null +++ b/proto/html/components/sponsor-meetings.html @@ -0,0 +1,307 @@ + + + + + + + 会议列表 + + + +
+

会议列表

+
+
+
项目编号:
+
会议名称:
+
+ + + +
+
+ 会议时间: +
+ + + +
+
+
当前阶段: + +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
项目编号会议ID项目形式会议名称会议开始时间会议结束时间总期数期数当前阶段操作
ZH-2026-6585643145673线上小牛血清创新应用研讨会2026-05-11 11:302026-05-11 14:3026第 3 期待监管查看监察意见
ZH-2026-6505643145674线下整合医学学会项目评审会2026-04-01 09:002026-04-01 17:0018第 18 期监管通过查看查看监察意见
ZH-2026-6455643145675线上+线下智慧医院建设项目方案论证会2026-03-15 14:002026-03-15 16:3012第 7 期待整改查看监察意见
ZH-2026-6405643145676线上基层医疗改革试点中期评估会2026-02-01 09:302026-02-01 12:0020第 14 期已结算查看查看监察意见
ZH-2026-6355643145677线下数字化医疗转型方案评审会2026-01-10 10:002026-01-10 17:0022第 15 期已结题查看监察意见
+
+ +

*支持方点击监察意见保存确认后,按钮变为查看监察意见

+
+

*会议详情页,增加学会已审核及审核意见,增加支持方监督意见详情,两个意见,合规人员能看到,执行方也可看到

+
+
+

*支持方状态(当前阶段):

+

未执行:会议开始时间前,显示为未执行;

+

已执行未传材料:合规人员未审核通过前,一直显示该状态;

+

待监管:支持方未填写监管意见时,显示该状态;

+

监管通过:支持方选择确认无误后,则显示该状态;监管通过默认只显示 1 个工作日,之后显示为待结算

+

待整改:监管未通过后,则显示该状态;

+

待结算:支持方监管通过后,1 个工作日后变为待结算,显示该状态;

+

已结算:合规人员当操作结算后,状态变为已结算;

+
+
+ + + + + + + \ No newline at end of file diff --git a/proto/html/components/sponsor-new-person.html b/proto/html/components/sponsor-new-person.html new file mode 100644 index 0000000..f6c50a6 --- /dev/null +++ b/proto/html/components/sponsor-new-person.html @@ -0,0 +1,118 @@ + + + + + 新建人员 + + + +
+ +

新建人员

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+

*手机号为唯一值

+

*全部为必填项

+
+ + +
+
+
+
+ + + \ No newline at end of file diff --git a/proto/html/components/sponsor-people.html b/proto/html/components/sponsor-people.html new file mode 100644 index 0000000..424eefe --- /dev/null +++ b/proto/html/components/sponsor-people.html @@ -0,0 +1,146 @@ + + + + + 人员管理 + + + +
+

人员管理

+
+
+
姓名:
+
手机号:
+
工作单位:
+
部门:
+
职务:
+ +
+
+
+ + + *新建时,管理员只能新建监察员 + *批量导入模板与列表表项一致 +
+
+ + + + + + + + + +
姓名手机号工作单位部门职务状态操作
张三13534621147北京XXXX有限公司会务部经理启用查看编辑禁用
李四13534621148北京XXXX有限公司会务部专员启用查看编辑禁用
王五13534621149北京XXXX有限公司会务部主管禁用查看编辑恢复
赵六13534621150北京XXXX有限公司会务部专员启用查看编辑禁用
钱七13534621151北京XXXX有限公司会务部经理启用查看编辑禁用
+
+

*点击禁用后,状态变为禁用,按钮变成恢复;禁用后,账号无法登录,提示请联系管理员

+
+ + + + + + \ No newline at end of file diff --git a/proto/html/components/sponsor-project-detail.html b/proto/html/components/sponsor-project-detail.html new file mode 100644 index 0000000..400035f --- /dev/null +++ b/proto/html/components/sponsor-project-detail.html @@ -0,0 +1,47 @@ + + + + + 项目详情 + + + +
+ +

项目详情

+
+
项目基本信息
+
+
项目编号:ZH-2026-658
+
项目形式:线上
+
项目名称:小牛血清创新应用研讨会
+
支持单位:北京XXXX有限公司
+
总场次/总期数:26
+
服务机构:北京XXXX公司
+
总金额:3,112,000
+
项目开始时间:2026.05.11
+
管理费及税金:1,112,000
+
项目结束时间:2026.06.30
+
+
+
项目评价
项目评价:4.5

*点击后可查看具体评价维度,评价标准与供应商系统保持一致

+
角色劳务设置
角色劳务金额操作
主席5000查看下载
主持3000查看下载
+
+
+ + + \ No newline at end of file diff --git a/proto/html/components/sponsor-projects.html b/proto/html/components/sponsor-projects.html new file mode 100644 index 0000000..ba2ab36 --- /dev/null +++ b/proto/html/components/sponsor-projects.html @@ -0,0 +1,262 @@ + + + + + + + 项目列表 - 项目管理系统 + + + +
+

项目列表

+ +
+
+
项目编号:
+
项目名称:
+
项目时间: +
+ + + +
+
+
是否结题: + +
+ +
+
+ +
+ + + *支持方管理员在分配项目的时候,只能分配监察员 +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
项目编号项目名称总场次已执行未执行总金额可用金额劳务费会务费项目开始时间项目结束时间是否结题执行单位评价操作
ZH-2026-658小牛血清创新应用研讨会2616103,112,0001,112,0001,000,0001,000,0002026-05-112026-06-30未结题3.5项目评价分配查看
ZH-2026-650整合医学学会项目评审会181262,500,000980,000900,000620,0002026-04-012026-05-20已结题4.5项目评价分配查看
ZH-2026-645智慧医院建设项目方案论证12751,800,000760,000700,000340,0002026-03-152026-04-30未结题5.0项目评价分配查看
ZH-2026-640基层医疗改革试点中期评估201462,800,0001,050,000950,000800,0002026-02-012026-03-31已结题4.5项目评价分配查看
ZH-2026-635数字化医疗转型方案评审221573,000,0001,200,0001,100,000700,0002026-01-102026-02-28已结题5.0项目评价分配查看
+
+ +

*点击后可查看具体评价维度,评价标准与供应商系统保持一致

+
+ + + + + + + + + + \ No newline at end of file diff --git a/proto/html/components/sponsor-view.html b/proto/html/components/sponsor-view.html new file mode 100644 index 0000000..08813da --- /dev/null +++ b/proto/html/components/sponsor-view.html @@ -0,0 +1,86 @@ + + + + + 查看人员 + + + +
+ +

查看人员

+
+
人员详情
+
姓名:张三
+
手机号:13534621147
+
工作单位:北京XXXXXXX公司
+
部门:华北大区
+
职务:项目负责人
+
角色:监察员
+
状态:正常
+
+
+ + +
+
+ + + \ No newline at end of file diff --git a/proto/html/components/submission-detail-compliance.html b/proto/html/components/submission-detail-compliance.html new file mode 100644 index 0000000..889c630 --- /dev/null +++ b/proto/html/components/submission-detail-compliance.html @@ -0,0 +1,207 @@ + + + + + + 投稿详情 (合规) + + + + +
+ + + + + +

新建项目设计投稿

+ + +
+
+ + +
+ 项目方向: + 小牛血清创新应用研讨会 +
+ + +
+ 策划方案名称: + 临床医学交流会 +
+ + +
+ 项目形式: + 线上+线下 +
+ + +
+ 项目类别: + 学术会议类 +
+ + +
+ 设计文件: + 查看 + 下载 +
+ + +
+ 备注: + +
+ +
+
+ + +
+ +
+ +
+ + + + \ No newline at end of file diff --git a/proto/html/components/submission-detail.html b/proto/html/components/submission-detail.html new file mode 100644 index 0000000..937f899 --- /dev/null +++ b/proto/html/components/submission-detail.html @@ -0,0 +1,207 @@ + + + + + + 投稿详情 + + + + +
+ + + + + +

新建项目设计投稿

+ + +
+
+ + +
+ 项目方向: + 小牛血清创新应用研讨会 +
+ + +
+ 策划方案名称: + 临床医学交流会 +
+ + +
+ 项目形式: + 线上+线下 +
+ + +
+ 项目类别: + 学术会议类 +
+ + +
+ 设计文件: + 查看 + 下载 +
+ + +
+ 备注: + +
+ +
+
+ + +
+ +
+ +
+ + + + \ No newline at end of file diff --git a/proto/html/components/submissions.html b/proto/html/components/submissions.html new file mode 100644 index 0000000..b169296 --- /dev/null +++ b/proto/html/components/submissions.html @@ -0,0 +1,482 @@ + + + + + + 我的项目设计投稿 + + + + +
+ + + + + +
+
+
+ 投稿名称: + +
+
+ 学科方向: + +
+
+ 形式: + +
+
+ 状态: + +
+
+ 备注: + +
+ +
+
+ + +
+ + +
+ + +

*点击查看可进行预览

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
投稿名称学科方向形式设计文件状态备注操作
小牛血清创新应用研讨会投稿临床医学线上查看 下载未结题提交 修改
小牛血清临床应用二期临床医学线下查看 下载已结题查看 修改
整合医学学会项目设计临床医学线上+线下查看 下载已退回提交 修改
基层医疗改革试点方案投稿临床医学线下查看 下载未结题查看 修改
智慧医院建设项目设计临床医学线上查看 下载已结题查看 修改
数字化医疗转型方案 V2临床医学线上查看 下载未结题提交 修改
公共卫生应急体系设计稿临床医学线下查看 下载已结题查看 修改
中医诊疗标准化方案临床医学线下查看 下载已退回查看 修改
+
+ + +

*待审核和审核通过后,操作只显示查看按钮,无法进行修改

+ +
+ + + + + + + \ No newline at end of file diff --git a/proto/html/components/support-intent.html b/proto/html/components/support-intent.html new file mode 100644 index 0000000..8e22458 --- /dev/null +++ b/proto/html/components/support-intent.html @@ -0,0 +1,322 @@ + + + + + + 支持意向 + + + +
+ + + + +
+
+
+ 意向项目名称: + +
+
+ 姓名: + +
+
+ 工作单位: + +
+ + +
+
+ +

*项目公示中点击支持意向后,显示在此

+

*根据手机号与系统账号进行匹配

+

*支持意向不需要注册,但是如果已经注册账号,则显示账号状态

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
支持意向项目编号支持意向项目名称姓名工作单位名称部门职务手机号账号状态
ZH-2026-658小牛血清创新应用研讨会张三北京XXXXXXX公司华北大区项目负责人13534621147存在
ZH-2026-659整合医学学会项目评审李四北京XXXXXXX公司华北大区项目负责人13534621148存在
ZH-2026-650智慧医院建设项目张三北京XXXXXXX公司华北大区项目负责人13534621149不存在
ZH-2026-645基层医疗改革试点项目李四北京XXXXXXX公司华北大区项目负责人13534621150存在
ZH-2026-640数字化医疗转型方案张三北京XXXXXXX公司华北大区项目负责人13534621151不存在
+
+ +
+ + \ No newline at end of file diff --git a/proto/html/components/support-unit-manage-1-1.html b/proto/html/components/support-unit-manage-1-1.html new file mode 100644 index 0000000..76c63d2 --- /dev/null +++ b/proto/html/components/support-unit-manage-1-1.html @@ -0,0 +1,66 @@ + + + + + 支持单位管理 v1.1 + + + +
+

支持单位管理 v1.1

+
+
+
单位名称:
+
联系人:
+
状态: + +
+ +
+
+
+ +
+
+ + + + + + + + + +
单位名称联系人联系电话意向项目数状态创建时间操作
北京医学科学院王教授138001380018合作中2026-01-15查看编辑停用
中华医学会李主任1380013800212合作中2025-12-20查看编辑停用
中国医师协会张主任138001380035待签约2026-07-01查看编辑停用
国家卫生健康委员会刘处长138001380043合作中2025-08-10查看编辑停用
上海交通大学医学院陈教授138001380050已停用2024-11-05查看编辑启用
+
+
+ + \ No newline at end of file diff --git a/proto/html/components/support-unit-manage.html b/proto/html/components/support-unit-manage.html new file mode 100644 index 0000000..f5e4c43 --- /dev/null +++ b/proto/html/components/support-unit-manage.html @@ -0,0 +1,458 @@ + + + + + + + 支持单位管理 - 项目管理系统 + + + +
+ + + + + + + + +
+
+
+ 单位名称: + +
+
+ 创建日期: +
+ + + +
+
+
+ 合作状态: + +
+
+ 单位地址: + +
+
+ 税号/统一社会信用代码: + +
+ +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
单位名称单位地址税号/统一社会信用代码合作状态创建日期操作
北京XXXXXXX公司北京市海淀区XXX路XXXX号91330101MA2GPR961K正常2026.05.22 + 人员管理 + 查看 + 禁用 +
北京XXXXXXX公司北京市海淀区XXX路XXXX号91330101MA2GPR961K禁用2026.05.22 + 人员管理 + 查看 + 恢复 +
北京XXXXXXX公司北京市海淀区XXX路XXXX号91330101MA2GPR961K正常2026.05.22 + 人员管理 + 查看 + 禁用 +
北京XXXXXXX公司北京市海淀区XXX路XXXX号91330101MA2GPR961K禁用2026.05.22 + 人员管理 + 查看 + 恢复 +
北京XXXXXXX公司北京市海淀区XXX路XXXX号91330101MA2GPR961K正常2026.05.22 + 人员管理 + 查看 + 禁用 +
北京XXXXXXX公司北京市海淀区XXX路XXXX号91330101MA2GPR961K禁用2026.05.22 + 人员管理 + 查看 + 恢复 +
北京XXXXXXX公司北京市海淀区XXX路XXXX号91330101MA2GPR961K正常2026.05.22 + 人员管理 + 查看 + 禁用 +
北京XXXXXXX公司北京市海淀区XXX路XXXX号91330101MA2GPR961K正常2026.05.22 + 人员管理 + 查看 + 恢复 +
+
+ + +

*点击禁用后,该企业下所有账号则均不可登录

+ +
+ + + + \ No newline at end of file diff --git a/proto/html/components/support-unit-view-1.html b/proto/html/components/support-unit-view-1.html new file mode 100644 index 0000000..b2e8efc --- /dev/null +++ b/proto/html/components/support-unit-view-1.html @@ -0,0 +1,49 @@ + + + + + 支持单位查看 v1 + + + +
+

支持单位查看 v1

+
+
+
单位名称:
+
联系人:
+ +
+
+
+ + + + + + + + +
单位名称联系人联系电话意向项目数合作状态操作
北京医学科学院王教授138001380018合作中查看详情
中华医学会李主任1380013800212合作中查看详情
中国医师协会张主任138001380035待签约查看详情
国家卫生健康委员会刘处长138001380043合作中查看详情
+
+
+ + \ No newline at end of file diff --git a/proto/html/components/support-unit-view.html b/proto/html/components/support-unit-view.html new file mode 100644 index 0000000..e96bd6d --- /dev/null +++ b/proto/html/components/support-unit-view.html @@ -0,0 +1,49 @@ + + + + + 支持单位查看 + + + +
+

支持单位查看

+
+
+
单位名称:
+
联系人:
+ +
+
+
+ + + + + + + + +
单位名称联系人联系电话意向项目数合作状态操作
北京医学科学院王教授138001380018合作中查看详情
中华医学会李主任1380013800212合作中查看详情
中国医师协会张主任138001380035待签约查看详情
国家卫生健康委员会刘处长138001380043合作中查看详情
+
+
+ + \ No newline at end of file diff --git a/proto/html/components/view-person.html b/proto/html/components/view-person.html new file mode 100644 index 0000000..c95270e --- /dev/null +++ b/proto/html/components/view-person.html @@ -0,0 +1,272 @@ + + + + + + 查看人员 - 项目管理系统 + + + + +
+ + + + + +

查看人员

+ + +
+
+ + +
+ +
张三
+
+ + +
+ +
13534621147
+
+ + +
+ +
北京XXXXXXX公司
+
+ + +
+ +
华北大区
+
+ + +
+ +
项目负责人
+
+ + +
+ +
+ 监察员 +
+
+ + +
+ +
+ 正常 +
+
+ + +
+ +
+ +
+
+
+ + + + \ No newline at end of file diff --git a/proto/html/components/workbench.html b/proto/html/components/workbench.html new file mode 100644 index 0000000..9309235 --- /dev/null +++ b/proto/html/components/workbench.html @@ -0,0 +1,67 @@ + + + + + 工作台 - 合规 + + + +
+

工作台

+ +
+
项目总数量
26
查看全部项目
+
已结算的会议
16
查看已结算会议
+
已结题的项目
10
查看已结题项目
+
待结算的会议
68
查看待结算会议
+
未执行会议
58
查看未执行会议
+
待审核的专家
11
前往专家审核
+
待审核的投稿
18
前往投稿审核
+
+ +
+

消息通知更多 →

+ +
+
+ + \ No newline at end of file diff --git a/proto/html/doctor.html b/proto/html/doctor.html new file mode 100644 index 0000000..1e558fa --- /dev/null +++ b/proto/html/doctor.html @@ -0,0 +1,252 @@ + + + + + + 专家工作台 - 项目管理系统 + + + + + + + + +
+ + +
+ +
+ + +
+ +
+
+ + + + \ No newline at end of file diff --git a/proto/html/doubao_html_20260807_194253.html b/proto/html/doubao_html_20260807_194253.html new file mode 100644 index 0000000..290913f --- /dev/null +++ b/proto/html/doubao_html_20260807_194253.html @@ -0,0 +1,104 @@ + + + + + +2025-2030健康规划弧形时间轴 + + + +
+ + + + + + + + + + +
+ +
2025-2030年
+
+ + + +
+
01
+
+
触达100万+家医
疗卫生服务机构
+
+ +
+
02
+
+
300万+专业医护
人员广泛参与
+
+ +
+
03
+
+
培训100万+名健
康科普创作志愿
+
+ +
+
04
+
+
赋能10万+名健康
科普传播志愿者
+
+ +
+
05
+
+
知识普及10亿+
人次人民群众
+
+ +
+
06
+
+
覆盖4万+个
乡镇、街道
+
+ +
+
07
+
+
1000万个流量平
台(用户数10万+)
+
+ +
+
08
+
+
建设1万个
健康小屋
+
+
+ + diff --git a/proto/html/executor.html b/proto/html/executor.html new file mode 100644 index 0000000..176dba9 --- /dev/null +++ b/proto/html/executor.html @@ -0,0 +1,251 @@ + + + + + + 执行人员工作台 - 项目管理系统 + + + + + + + + +
+ + +
+ +
+ + +
+ +
+
+ + + + \ No newline at end of file diff --git a/proto/html/images/index/u1.png b/proto/html/images/index/u1.png new file mode 100644 index 0000000000000000000000000000000000000000..7eec7e0ca3e8592ea2d26da3e220b18f3edeae10 GIT binary patch literal 2477 zcmW+&3pkT)AD_%=#K_su!Fj#Hi-@;xTN7K6h6pJoW{X%tf5X61@2ykBY%0c8%bOfz%En7Qq4}P*=rDpJp0!W>$zGP z0KQ>xShRbz-%62>8{xcePdfr?d*{&yW0gK}i-vv54dG43WJ|l5$O+lXwA$9#V&p7( zb61SiJ{{MNHFvk)d;9FuFX+oy&`bSIs6W#LeEX)d6w9I31a1v^WjK6g^?S|g_uA&O zR4Uc5mnM*t+r7Kq>(?Sv_dm%~E6M!}oy-$?C^*#H+xyi5r7CXyqk6rUoKB`xBum?` zPR-;#O!KZ}onPzsS%@YA0fk`8PFL5~UJMUc?6?3A2?-e@HD~n&@C2XwD42)vWeq0o$ea&l^ldge*@3@d7QdRp$D6E;L1l3!HRy}Z2Ke(MRUZ*q1vWpwfO=pr=! zd9_RuhBRt#DcIgtcd6I|!o24Mg`!bU#&+t>81AJ3U!v`KV`C4?%EVPvRKCQts%vSn zx3;XIyTtH#eC4_IA-NK6N>Y-9`rYGCy$(i1Mm}n9*QLBbBqb&BKD95Lgyp2O*~gyy znoeG^MrdoJD@GIcO5f?e2#l&>4R#!Z2&DmGFMN4CUTJHqww0CDFB`hLDYL6mC0xhw zaC5JNs?9#d*WSY#(ubTJXdhc3H26sYXE(#FO;adB~#b#(x(3hRp9>8_;# zdTmt=tL zW|CZu28cDva+38=pT^J4&7IGVJNfwP4)nF^a8QD!afr1RNLa)XZ64{m_Ziz^qVkCo zC%k-pu|~?f&~N}~lbOgu=2F@}ji`bg5D%eBnM@}CDyhLi`R$uKbdqBGtS(nk7Zgg} z#Qb29ovV>@lex$O=K}bh{TtJ5AuxO4L4M#qM%n{VI9Udl9UZ0K^!L|;{BWb(objZL z$qQzsb#)4q7xur6?%mUO1LWPxZu;$8SY{9HZ$!8?+64Xs@l5tMS5Vf{g0P?ifq(+> zVzF32xxn)?DyH9KUHqT+CrzNNTW)?+3T)Ky@bHLU|0tvLN6hUYZt+844Mo2*ZL>4XT?@#V46v9J zeO(#P-#{B&JB%j~+`DWC(34Cmsbq5h{>WjH|9k%gJ z7&I_A*h#cZq+~ zsH%*$5i5Sr0;Tcu^TYGGYja`htM6JtHFk}UtLEVKb&lS=Gd_Oib!&Gj4iCa9$rP*mXP0=?Rm<-(i>m_hbDab6bQ8U zq_eYsT%7hnRaqvJ+2o<`t+01w`FG8`+ut{lJd)LaXIOdn`;IC-bd^rRh=G;?I#|VM zYHF&?^2*Ba2!nA>(b-BQod`k0gUvr_*_zh@AO2-8h(68}{iCZse>n(nr z*W5}2_?KQ5#GPE&pAj46n_#bVY8)}ow5!U9&CAU#k6Y3^(7%ms|gJ-Lt6THQrw8o6nl1c z_BfCy6C_fjy*XYyL~5L}67_0fPKj(w?FXm@jE!jfb}Qps zgwl#rB_G%+3gL~J)4c;~>>Hrrgg*Nj5xXoDa20>@#2|nz4<3AMhZGCA8k(B;Iy<`I b*KH|MKiAMi?RFHnVnJ{YF4$Vkg~a~>SQ(oG literal 0 HcmV?d00001 diff --git a/proto/html/images/index/u18_menu.png b/proto/html/images/index/u18_menu.png new file mode 100644 index 0000000000000000000000000000000000000000..e351626fc22d7141e5e4b02ce1d3eb1b62fc61f9 GIT binary patch literal 236 zcmeAS@N?(olHy`uVBq!ia0vp^SwO79!3HD~i`RDpDaPU;cPEB*=VV?2IV|apzK#qG z8~eHcB(eheoCO|{#S9F5he4R}c>anMpkSk?i(^QJ^V{2oe1{waSR4M1MG8<*$J50zB*Xdb2}4E(1s-MtwcT7=(sd$l pJkI${ss_q4G;F*?y}vd$@?2>@3(CItWh literal 0 HcmV?d00001 diff --git a/proto/html/images/index/u217.png b/proto/html/images/index/u217.png new file mode 100644 index 0000000000000000000000000000000000000000..4a94f74c60150f784aef9e099e3256f221a4f9b6 GIT binary patch literal 3164 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 HypsO|0eAL2 literal 0 HcmV?d00001 diff --git a/proto/html/images/index/u225.png b/proto/html/images/index/u225.png new file mode 100644 index 0000000000000000000000000000000000000000..3268e16d79aa3c2562a09c3b8b08bf7e90f0e886 GIT binary patch literal 11709 zcmd6Ni(iasAO4e48;K=UI#^zbjbu~lxSw8_-_da7|y>Z4g z{cWW);^p}*C zoi*lUYOxJWO}|WhCGY3k@5$rZaszd(<`fqf;~y1~lcu?Od5hTvnr9bDpD-WmV{4b2 zJ8)}i)<;`rmQ+^8T=Tr*&HFSos-q&mnA4A|R%&aP)5wR37Jcjw#*;Fiee&tM)y*9l z2`=s$I#G~YFZ*V2a@8U`-=Z0uw#rcM3UaNOOYgOK;ZdosUZswfwhFciwd%>|#%j!4 zlHRnpXN$aam22HrpV4AJFZuZOYv83zmjc-2DEyfYw|Cmwzid?eP+Mz}K)YC{IXWIu z+?8Uy+2+-~@da(MhhGJ%CM6|x_zeI3x8cf_E6b?a-8o>Io^74aVMirK--8yy|}tH8_S zvuI%8?%qK!XDhs%_dOotCMT?zasLYkZEsO|E?Z}ZM&#$`jik@|TwX50T zkeD}7B5AiNmK>Oko_l`|0EI>yx8Va15A6ZpLdOs&(LA7yc9)$5N-M)j~y@++}V zkKF!nKh#+kI}#RgE>aQWFpGP(T5afQuiiddo12?^rM*YpZO-|OuIW{;oJ&k*i+1v+ z8&_x7FSWst2$2)_U4^e}IyyYXulPtSiG97((CLa-vPL6^lc{<3uL7-x zg6N3q_7#GkrD)sTMrHc_xGjS--H{xLSyUwQw`0w0?d@L#RyLLR2L@g+tkeAxW@g?p zv#;c(--5K;J8za$Rar;+#vCTs-(~S$M#S*~kAZtHxFs6+gX^B|53&u;Pkq8Az48vr zOP@HRgujZUw`Dh7?|ePcziWfd;MoA}9WMe^)6F8SANi#TF*I8N7v1rA)ftF|o%bcq zGSk4&@O9}1&*(n#X{94!X;sIPubnCK&TQ}Lam*{JsOU;0<3;-*a?WEUC+FRlFDw5Z zkC;|__KL`Oa+++z;A13Rk@pUUo^s5hMbGtPN!j{qvYZc{g8HZ#_ z(PR$krC2y|ZwR~$Q|pj!b__igd1@;}A6F!?OG-;O$1L&Y)qVKzp;3{R$L&n)wUli* zJ=|IH@u2ROP%=@Qnwm7(tH)gx<)m9zW3=Xqr7EkSrNQtoYpLt>rg%s(@t^OWQ2 zZwPMXb}|@@X4yu^E?moBvp0|XtjK#$S){-Jocj8Dqxzl#t*Fe*%p=QWw%F9^@70>O z%;09ua?|j?6)K&z$9o(3 zx9fr?WhThpyw2BfW zjJSTmZ^iczqItotGQt>T^jDA*xQe3^!)}a{N0&OW;SJs!3k6wCFS2UleoG#F!cFFj} z%R?9HmKKlJT5sQuLOBra9{or7PnrTN1eu?|f4ut}UQrC2%^oPaqIvey*AQxpCNf&Q z=Ai)CHXvm1KETDM_nej%hhn+G;~CNVp5No;-Qb`e(&UMIe8fRvu$n_+ZFx{nSr(RY z>?H!Oyu2Js9AmdKiLG$Y0mD?wJZ==?WPtsCY^-80c3AWd@nx;4rxusV?IdIQ?8m;o zg1yo*RyNX+tV8!+k67#wWBQRoae#ft^x?~Of?kyS{rQ_X%kkQ0ubhsLk3XU$I-Mam zaTTB*E9DibWOBr5ko^Pd+971URkfGPyJ~P5;LU9H2`O_W>|Ky zR8$m+eXL#Ko+}NOKcIBm|X9NPl3%{L-w6L+^XDMm~v5*X) zpIuBNC%T?08cUihRWPYIMb3jeJr?>4k_o~$qv${aPj~{@GBBXqP_Qk285O2u#PQXP zsnGyt*4yFRIt|_IeiVMZh8z}rni7gKv$#faN!LtsrLHr1F^7+A)MFP=0j>QQf7ZvJ zZ*-S@98?M575N1RUyK-;XYpgDa~`?4#+St*7^`z2;Z8fAXL*a;sTf!9$uV!z&2RCt z`+3OMAXaMmXr}=W&z!37T&ETkH8Lg}hCLO%FN?DEIdbIP@G!$e z=TIW8?UMu{vLZu!`8mWUP`~oeK;|^Y^5d)d4#uYUob2r%yBC6B_Zi0-a|D0?{h9=^ z{5`ipF%Bkcgwu^pOj>2@K+=ol_?xjz^RYF8j~_L<|3F#;*6DR?LMxrYHkkVEC?EFr z9a*0#MopjBlb)Iy$R;9wz`U0K6AE^F#}C~*3-p@(%9wNSg(T6H|20Ha(97_s?#36L zot+BNG>3UTZIzvgQ;etM-a}8^3o+PKkERY?)3D(?bJ9}N(}Td%8B5gE)SkD!es)lS zs&};HMVegh;|Hg<7sX*nfTR zO6QHdq7EzJlP6Xx>gw6kx3mRJA;MaU{Ht?2F%P^U`1HUOlp4dkQWa~lZ+sA`)H&~Y ztpExsGSyGHl~1ShE>1G$cMVPzxUTw?C?USkJ5R^kOyx+P&IOv1Ln7D-Qo054+`N|z z_QBH1%GT1ARpG}zl^OV+mZA}=onH;XY^QOcQIH>5>|~04)~%6XR`w1TS~+ohNBV`S zNAit1F}72FXdtSd1Q@oqJkv>eaz%4mcB2Gymxqz%$Z#RMqoboS0ysgqU}1;lzo`JV zxGyCjnph%Ixokf|5g8Yi%s2FCuqVXI@Dy_;)|^kVhxM%HpgM#bw60`@4}EbeRv`E( z-g+s2Zqx7!4NY-w2GL1v-Cw6EQI(z{$CoLsu8tFLY|e533zX%61Rcd$OoSIYfL(FF zHd-_Q3sW=KliW?36*N0R!Z;o*5ENi$nN`z6RPxtz?I;p#;9J8TY6(EHyr~j{hWdo^Y3GH(j!sU&QF27+EuNDzHxL2S6>a-@r3}M?yw>a9o^C0R z!9NJr#BgPJ)_2}0$)76QKn(un<5c!K;*%c6~o`Ewa^OrL_?T6VrW?d{*R zwpvuTZ!h12@+&T$#y0_+g?n#Nc0h;4c8A0W7YUL5@zq2X8|=O;x2eS4-W~&8I{r*{A06BhOam{apA!{S8PPsUC}gll&DyQp#!JsTg|bzuYL6BQKR4dr1KJt z7)+xpm0Z>ucgSXNDA3cA#t$E8#d1t_H`DZ4L|FgH-FczU7p3wus{It&Z>xq zQ0S7$CsRJ1-jpfEhIzkA8XtS&J|`$B$Y^v)5}T%czMUSUs;(ZiRf7L=PJ)C;qqV(# zqrE-&90VW22eo7x=VZ;kOKE9zHt`9h7qlki$7f)V>FGTIy@R9UAWdCMFq+K$6KbsD z-YXK!^?(un!f@|^>soAG!`~Es_p{<#@gVjC#=rC30|Bbc6H7EDnbpZp+>M+(;vfAB znroC!4n={>VBOKu=#-Qc)HM)fgz39?BuTrX`47!DP{fuFp9McRY($h(w1gC!_u8jDFnBspcXebBH10#Xok2-G#}6sdEBLSgG_rdcVe)$ zG>2=8m%w;}DB$Y@f%&qiO#v#dyuiVU@zK=WUj)glA(&5yapkN_9HEhsLe!O2ma;_T z9~p8)#GPJ7Md9L4c%sK|g<4y2aV)AYbOpaMC&$3l)Y{AlOA@DUrkp9oT#7|!dK4k1 zuDdtsVgo^KQghEyi77fcQc4hcC-6|`<|M<7pg(lO#(-EBub^Ako2LhrS-byolLFx^ zfpw`^huy%RioClQ9XA=;TRy#b{<6{Ws#PlD@4Jp^I~8|+4FR5j9?=|>*~fS7M<52A zj`(}v(Hcjhc@B1_c|GbAqy&H$aQ&@deo4Sauve=t7s-LM-2&Qo5xMQ1QjVy#yy2VDCklA0@vVm7*l)6e> z8JgOze&@pmKZv~>JJy?VwMS&R5SbwJ|HK64n~V0k8x`Exdvbl>yghQqw(h>#L25;Q8LSFmulQLTkqQ z09=h)|2T1|y)lczgZGNNKK0 zS}SN6>9&9ZjkUIqJ0UHz^M-obNy$l=4cwV}Tl*QWkC3s8yAub2%!)hH(T!NhCf z6M^6+eobHy_mekdgD zH&A&`>HTNtgAP@g0Eoci;HQJ@LQASx*j~*8rogZvp<~AAha{JGNiGp1o5`?sg_QV} zG{eDwo%4$4$Wsc3zwoiJe_$L?6887NGQ|G0n5ge{S#**FpBX9tBXCY*0Kgknf8M=+ ze|4|)hKGmlZMpBzq>G1;R%*{>vFtWnDZ!tC0B0@e9lji4wB_SvwX|1B4}{(#tuSfk zI7%PW1k1T2o88^*P4ZnG(utt}kuOX7w!URXjZ~@hyre`7Lg^8M54rQ=e2g6WjEfm6 z&dr0r)(O5?%d93`BgvW0MUlrG&^ZWnLN)ReCw!nytdjw?&F={r4;46{ym4PE@)Vd!y2agAoBQbo4tc$)Z z(A%Kg!)@*D^{%U$WjSJdhL!#=jJH$|JUl1oZu~*R)DYIxi3IJbU$A7JVx%WTQb;4w z(lAfAL-+R7qTTUYJCSzr{4)M#Fl(_+w__CTxgG5*DSn=rYpLTknH^h8j&a`?#~ue^ zwMY}j9w%5m$V-+FS!KYg(=xQ!cJ+1tv`?Jh06!r4pPhSl@NS`czUEmmcQiOi7T24U z*fTJ(fyoA$yk&91N{++|J>#5q|}X^AUCMrN}t7BdCHb{|b#E<;!r1P=XMc zUmP+;Bko9n82+g6cK2BZm>C^SS(^!j< zR=XTBOyF%P@s{DSW0F)A8hxCwiI zU$fYV@TsT&dx9cbElQW$t7hq}o5_O3Dj~WBeQLK5{`&s!OmY!fD4)+h$duGY{uVpf z+xK=fWy8kwRT z2>)!zqpL`=c3`DMZe>Xvbd%t27??awped0)C}lhoW4X{C&K1z-RNpEqs-7@b!Wiw{ z#dVqht$%Ej?fU_B?dFiQjA?k&s*5^X2xpKcVhp;AsQL@_(uZuwO~ADveuU^Vn1ySH zS3p2X-V3j3EAqw|QMC_nos1Ct33dS0r1V#PLC9FzNc$4t70Q>pzIdU=i`zY+KM>5* zP_BTVyWXSvMr(`TUQF6d_{2lG&Hz^h{S~)H!Tkg^r_grG%AZXPpHRzNickdKffo$! z0h1k6m4cV%^_Y%Haf=t(@_%(FXKTNS|BeRoHU1y%c69Zv7@n zJ^%tj-Af(6{F{UpDBe9~E(#Xlb=VY$#mOC@6>w1h6v}ll8NH zs&$YlqaA!ck!Iv`dpl(fv!(;uJZC&8>hB^Qx!PBh0U>&Tk@Ehc8Zbeozp2A*tB6sP zleSz-xN=|yVLgnvYs{#^)KxK7uv~&qLtS@%D)_)cdcuVm2&sfGg{fojC=-{ckU+lQ zw)>AqSE=G3Cc_`WeQD2pQd897<|zE`-w$_m=gpVayMp0YfE2$7M1uq#V*Rscw#5p} zrEqM9&0lqXZ9MV^82qQPoHoiaWTz2vV^ zMafX$vN)}oJ|M>^!f08Kr*IbV_>YA%m_5k5Ve7bd(avwdNK>$QJ42fgX{iO>ffm!( z*C*ww^NS8wnc)LirFQ;a`bmRcd7?rZWeD4;JM$L9tj#dC?3gx}xWkF)Y@pjJ zAQaUwo3X&?zTV)$dRV}@q{+hCD+Y{#mTaTki^5Y%6||QiPMzQL|MoL>pnJ`Y*5he;$~2!J`J!-DSK9G054Dj3Y0(@^UUUg{Y`SJhsBl40n>%v~0E#XYU_Ca-ydsE<9d&{#16 z5yJt87WYL0C$(WLNp9J>pU8{JkYlU?ywL}p$!yBXx3JwncGDz?J4->I_oU>T6ujvC z`rM7Qc_>(86=H9+NllHWyW9v2U-ffPpV)r6wFGMcN-LfI1m^~T88>*hwAjON6JWE! zDxf@4t*U{aK8=qKVZyJqU-b-y|1gCx@Qr`u=CQOp%yek=T!tQY^wggmp=aKoMIi)H z#qGH*1Tu&ZN3`SR#yrJ6l{al|mf&36z%c44L9;`JkL}L{5xH}S--K2;Ff%o`@V+ab zKS$&wMPsRxQ_GzX1Jl6Bi04pdrYJ`mbfWbn^J*=|GNYvvvM&^dgX5fG4a_2l^kj3E zQ&!Lc%F%)=0ZW2Te?|J`q)QBk=RT(hy9 z^nu-hREVwdkDW13G@O91)4m zMQldv4NF#oW`-FFg8$`HnZ_qkl}q=&L?~V+bpO3QS&2Q~tB|3|d#X;=TuwztL-gQU zQ%B=@CUn@!!$-;+yQEHXL22?{gqdzHuN8N{$mTEU4R3ul7i}X(D`|$4J>Iiaca;;O zz{oQ*qvhpN+7|^?h6%s_k!QbVqdn4>9nD&iE<2egV_tZV4E*dUI7#So72W#Q~15C z5rp$&fyc$}_iX$hr;jwJaaKS5N%O=;wd&u|Ld1L2 zBJEpa07Mv!kbcSDelDD|j?zO0Dmb2$c9XFKeQ70-%ov*^?O|S#44)G^KBmyeRqcY! zsoo6w(W3>0RdrY8N4MG3Ib#!`0_|4eoCfV%;EKHxF2Fwt)c)?QQd-3kN4*fE^ z4i}TSVNH@aSlVI3qMy(-Gz;T!I8=AnxDZ~cZf>d534h9DvP`4_-Nh=<9=-JC-t#x% zfDwdbMbirK46r7GX@aT~l-4V57GOKY9XxP@x;4Rb$^QBm#Sen`hdlOja-)eh98f9i zmop%n1!q&8JaCol@zrUqJu$ZOr)q#aB(@SF%WsDmHcp8?+-E068?&J>?F>02QPol$ zO&%#>P)&ge1#_kC5Hn=kX$5)42Zc&^ohNDs!R+Z}I2HnZ&>4U}k%T^h>ob84a%6*- z9#6Iooo6?PqWC_tJQ4gG7!-JRd+^5pbhRSlGopQ<;z*t~z7Pf;no93*;EU>(A3S6dT8hZObh+BSun%uW($&ik}|qtLc;WlzdQX~9sBe7_I{I(N zQJtp%wqv`$Yo|zIN}rhX!8Ib%n%d>fTYmnB@4j}>(!7CGW^y~W#=HE8H1b|rt@1Q9 zT#AG@~&l=0dQ`R>D$Lm4V-5BuM5pLb620W^@_hNFEHH6|MSUJ_yBPH1CW2guNy1 z)S)WBCuYCe492Jx8~sOmE~D_8(6)-`75>mr1>u%$_FL=Jkl7A+fUEjg+XlxpA!7JU z3A{X$qPPFAmoHK9naf)fy z$PtSMyq~_ucY}mH`>FH&TtvHsAwGB3U7q{8jh4}jy{$h!$A$Mv$S)o8wRJmR} zz1vq`#~yZea>~jrjHsT}s9)3WXfrtc+h_WvKCt#Dt@uib3}MhqU>X_Seoug^1&MEc zhu!Pe44G=CJaPA2)+)A;bS=S*6)gN#bsr;#;Os*&x6Fc93bX}-!bzOJ3P@kyIgh&{ z^y}bqO|%zCC68uJY(5US6b6`y3n=bANS(H^CXFo|e4v*#epgs77j~&8kj+V(ThuWb z6l*+^JZ?vwp!}Weg@7C$#?dX;Rhw?+_Zde9)+@N!+YDy)Uv)_Az8Z&xhTg)*xfDkx zUU_&MkseQw##EP(ys@aMG+x#yFLEseJyKilR1MiIAJot$&x{eXJRSW*&-Ie+f(0R8 z7E5sEC9Ieil`zlLxvYBpRB?bhuHoPDj*}A8L-B58&2^+XOgtU?bZROsj&rgNM^lkP zaGWMCYmos>@QZQG1IK~_Q(MKyR$u$k0|UQa6(nz;o!~urO`Y?nnyTvfooY{V4)QyS z*LL;`IXG$C7(3eI-S~HI21s4jhr^h1YpWBa=KrPYaMSif@cO&fc7FWr8^;cRrazU2 zeR*;cHW5(JGpvuJ5lB8?^jlec96;5*aC2zishYRDW^S6WvMH*xbiird8;v+V@L~nH zb9tmHc{SsL_*4@Pj8IkQuAWz@lmAl9&It?hYyOJSj_mYz+L0rZ`OP@k+}G4NN&cOX be>o{RcjAw{&M3EZ^7qXa)*EuyxgP&N@t0O8 literal 0 HcmV?d00001 diff --git a/proto/html/images/index/u229.png b/proto/html/images/index/u229.png new file mode 100644 index 0000000000000000000000000000000000000000..dca1546a95a419a843418e1852f2bc93e71058ab GIT binary patch literal 3284 zcmdT{XH*m07LK8+G$~S~C@5S&6s3oD0Vx`q^iB|v5@~9XjseB16t7Z3i?kPN00HR% z!bLyX@z!Z*za+PmRiSmrE}Xw*_LW98ewWfc@yFr@GDh|_@` zKNR;_pW8z_j7N)ccBQjRUr%qsz=4@tn|(yZ-xRN^Y9~q7MUHX4u=_BN9Wa#+x$PtU zUfqmp@o6$1JJMcpB{e)RYa6>8lQM{GQ;wmx5AG8NNAItYD+?jShqo6Ykfz5(h+@Lu zu;Q|hK`$EaOwNfyht3)q8RZ)E&vCInyu$oHnQdzSLYjF=y2<%8aF|M`o0oV`H_)FK zny;&xOE~dw^wJZuV&ppI<>=(Mz7qNwmZvXsV-`#llQ>Q%}zBZ-P`fR zV|}^P{VmN2Y^Q{p`WgbXqjoGoI1=3ZY46wCi;3@cMA7I9X&D(zw5Xi1T)LIA2#TxA zn4QDLIznl_I4esSH|n>j7!^k(3UuGPIXXH@^03d`i}Vmjqvc<||3~d4{@#SZ(tW2a z(;f{DiL;8qhPc$0LC?LlC&J9E0`irfS7g&~XgvAM^vmNv(SzB~i8zRJ1YQMM_sHea zqG(+2DIuGr?%=S~(D-MSj&(g6Uf1p^=~l&b{k&p>$Kkp)=39!3r7K-q$r>Ebr4&mi zPF^PDqgKcoPh9I`4kH$Mq8@_m&ZyoA=4xJEz_^EUb+Um1YC^smE&hqj&dC`klc}N! z+&r1KzfBloMf0rHT|c?WY<|aK+^&WS();AAZMN1I2=$gAf*NJb~GXK`PR5D9be#Uu5)t65F;2#v^9^y;JI~LiyN87Ho?^iiF zJ7Y0l40010Coz)GQR_WLd5aEmo0JbZjK=Ml3KUXgKQ}K*r@FaW17{`cUFW!PS3@At zP(@9x0*5mMY_2{jE-EV7Wt5BZ05GlkeBje53s61d&zX( z&zcLY#RajhcrR$96}L~_^7p#?-$DFU3?`Mo0!EMzct-}T7^%iF$AghgpqRiX36 zNiFmpmK{9zfR7JDF5b7z&#B_RhFWIrgG4zvWpi&E-sxhru3&%u}P+?qaTZE3X zgF?SGQa3x_hq!X!uoz1zgoB?! z4ZU+=?R-%dL1&qJdFQYPW?WH>UTUOf5mDFGv6Pf*Y1BGZO49YhV*FAqSr&8%2t#g{ zAGK&3Ei+a*K45Z3&y`6JpqoiV-R!(n?xS2l^HE5?a2mA3%Q)AuMX8YJJ+m;|=f%Bi zC1WZ)cm6nhSS})j)G3*nHTTpn2Ydn?9#hKp&74ge|FuExtX;6XGOGlej29De zut5h@EWyn7U#NBFJRciAgNVn;$xi4hkoKQ-hX=}5mWLvplPC1`B=We&H&2%vFeMa7UDzo>ACUW#>6ho}lJ-!hSWQqp;38+6`d|foH`YXZ zm%p=GqMldCq+_R;Iz%gpeQiJ3PT%SM1C;3+?;+Mg>j)Qy#^(Wuxn@%;kY~TGO#isc zNRnpMSTtCx104Y>-kzN+qqw8{Z^=Nl{iU4RSP#Yk*Cz%g!4e4HczDkUw`}YI^sHi7 zx#=e|XyB>vb2fF2vc+jBk2hA@4aiaE(}Pa!VMsXQH6H$=7c~D(wM6=j2auS&A<}#R z?TEbp0Kv>UsDg>CRj-cxZLVT5wxlG$Z|Q+{(Q(=VY<}`Ejh({|KbuVqZ(+@d)AjY9g-#br5+P zzSemAdWVWTIU^$m<0|J5q-($WLm1UOnTQw{C*2-b;bD9%GS-gnuM_EAz3|I@J0%XL zFh7_UAD>22&CfM|BZ0+gyv{=n)_f`*UFX4o_tSjuPs9FD#vR%ms2&fvai<`^3XL!m zH02d)l5S;qZ8wT7mUnOmx2>|>qB0LhTmB%&)+=81!&38+<96?R%SZS{;EPWkCZ!)K+uj1sOD2=40mWO|cRvEJk;ep7CeQ%*e>OGpr)LL<98e~r(EqVz7CScv z3Si^=Cu_YE@3H?LFC;7+!ljU?DFo@^`H!V(voS&ByTv#lp6q|qj@ZUS^BfYid8yz} O1ajTb{92VhBJQ7%Rwsx6 literal 0 HcmV?d00001 diff --git a/proto/html/images/index/u22_menu.png b/proto/html/images/index/u22_menu.png new file mode 100644 index 0000000000000000000000000000000000000000..0b9fda2ce2856cf2e83ade2df351264c0ab7674f GIT binary patch literal 374 zcmeAS@N?(olHy`uVBq!ia0vp^SwNh?!3HG1?t1~14cNOEGxykaz#qVZUuidIwyD#n86UjRN&-d8RtA?}$2s*iPh$;?M#&Nr} lXLroMM`g$UV)!$YKkVETo*l|B`GH}@;OXk;vd$@?2>?>{YQF#g literal 0 HcmV?d00001 diff --git a/proto/html/images/index/u24.png b/proto/html/images/index/u24.png new file mode 100644 index 0000000000000000000000000000000000000000..9db7542bbafe083ff37774e441af326b0719b3ce GIT binary patch literal 207 zcmeAS@N?(olHy`uVBq!ia0vp^DL^d8!3HF6MeqI$q!^2X+?^QKos)S9a~60+7BevL9R^{>V1HbH_6#xvHK8ukj?O3oypeGHzielF{r5}E)PIz7bz literal 0 HcmV?d00001 diff --git a/proto/html/images/index/u267.png b/proto/html/images/index/u267.png new file mode 100644 index 0000000000000000000000000000000000000000..19ca5d35bcba3b9066b0047f525122d2ab2116d6 GIT binary patch literal 4632 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 z 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 z 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 bf8*b4F23#D!kqgZ5KvSeYr;Ouo4@@(`=OSd literal 0 HcmV?d00001 diff --git a/proto/html/images/index/u279.png b/proto/html/images/index/u279.png new file mode 100644 index 0000000000000000000000000000000000000000..f44f221249d756376959f87b9c2994ea8d1c128f GIT binary patch literal 7274 zcmbVxBc%j> zZ@z!P{osDEYjwpt&UwW-k$O6+MEG?05D0`wT}{aV0=b6(pP_g-;5b?&Sp?qhc^RlG zLMnbd+y-B;92K+`Adu=ff`2yH;QM_KHK-Q^Lfm=xy4UMoWCwxJ->NGq82MW7XN9;> zPSy6d^ZT)=y&0NiGtTVMX(j!1(8*6Ij{(C{t8DIQ5jP&6{WvG8TO0*XVjk^1F7(Mp zKK^EN*2?A@JPo5`qone&B11sL7n!jqnt#4{xP$mdyogI#l<3<+o1IKkJK7?T5}9fA za`gH86`pdOP+^zTC?YK)9ngN39q=bjSUr)A6ia~=>#chgPAD_pN)JNm9W!329c%co;TwMCT?#HVQ({r8a#zRF#-{f_*P#dpS$2&i~J}sB8 zae#c=A08Ghd4$JOna_ux!zeeDex%Wz$es$dDn~K6JAFHakCeFZPPn?7wQN#N%D4ue z*wG-fv&tSos@7_!UeEQ!mZdS{u{eUOpWTXviLF!Bb+iQkCHyFGkzGPe;?uqz4%R6` z5oxDrATRCtUhGw>v4v(VuL9}&d+e`Z(lD2BwqY(wOU{^S>LjDRpT5(yx2{?^<&*nVJnB zOM#MH87(KgE5!nqFwyXY6M@L6x!76gg zRwR(29C;>VW8<>MFSHLIVnNExs*zuQh|$F&D=I#UIU<}yqj*d!zBDyS@#H*;Vp$Bi z4VskU8}Xh+#`8NE=;x39KJsK(4>S5P8evO{`23mLtlVTCz7$k7iOa7boEQz<|i+G zr!XP8dYlCTatuRR(lo29t55$)o3#ha?)d~*(~C6f%f4R0S`0ka#tKsakIa;Ye=JX@ z!gvQUw`h-Y_|J^jtS#`#WaaZXRYpHHJ{!UholY(;cEeAC3Qswm&qkG>qyNME9{Zj% zY7324g~KK2=;`&L(EBNaV8#O`#-z$*{3!&-8^h0FFdh|E)q9W(3HNZ$D3{YvMM~xO zoi-<@r@5u21jHXO?d|QIoSdE-8d5YhHKHdB(9mL`F?Ovbp+?7ekMrURbkaUM*dzguSgZ2b$8N6 z_}^(tlI~FUOJV$={P<~B4Le)g?+3VFrjGab_J%ZcGvNP>{Ebn~8(onUsp;w8=iJ+4 z6B5X}Y=!DdaN>HyK7J&g%p_~sRcNsp3=d#=NW2P8EPKM|?Ckt=YzzakwY&Q)AVAsz z`{m1*e)L>QdHP@}CkY`QQdwKu{cQn_D3rl*nca~K&6rxQlXAztNRaVDuazyHGXWk7WoU8(U+OWjp4!K>6dS+_$`~5uO&2; zm9b$PDCY_8X%}%NmV}s*IQT8(n{%!0pZUhJCtR8-{Oe6&z7?z@w2ce|uks;WBY~b< z@!o5N?zErnl!6bvhUHxDPfgV{Z+m*~n-AYau6I`G_@tB_+L(UbI9@yI&wb`{+G1Nj z#||l3@s^r#4)>K|w13{bz<+TaTo=Wqm>XY$7y9LigaWD28x9VRhSMR+(h2UFeV_i- zLp6}4W7E^L0UA&QyfUFX$8qH>{#Etdej=OttEIWFl|x@PuA6JrTw!zHR9eImdg_;m z#Tslq7k=I1)hDJbVUS8l^wREHYI3N1OEQwp!Yg4=qLyk$O~L($pA!Ebq~0`%U@d+K zg%b8LG*HYX{rK=n=$}7sK1uw!dQRq(IJ_2hT9IQKY#>NPD2AJT_6?NDAIa$lOj*H; zUBtx1?h^a#LXE2NtZd3KTgSK6yOk{KL6m)dd^M+5c=%ylF{}*wF6e}k|f21%|Eq~m@Z1Sh^fMT942?iskZEg5#}scX?d+JRDkzp7p(MYQ=J+1&y^ zH6?fNO>e2g-(e)EjTm(;q1>~ftLxx}#Src-h8(3w#Fb>?&N8P(ubVtZjs>b5sAz&( zebI-#W;OVW7j^`ByS8YnHZ?7a_&)Eb(5YSH8NVC%(ut_&*E^;Iw<=_cE#X95;>gG? z2KOfQb^oD=&_&J;&H^mmpN3Ba9<&Cytd)*Xw*~( z6{W}7WwI$OCs#2eQBA9ClZv1zPtwQ_w-(iDx*LNg3t)}J-c|iiUFc-E?hJZt)6HenO({tM%fMq^EmEpR^WJ4vK&5JWCt7b(T@pZ}T97aYHfQz3pL7;tmT7OScgp zoG3MgCWY!FTen&0VgX>YlHz^aJ>`x(`pjq1W5V|;G4gtSFa=7%Z`IV5Ugf=QFquqN zYEn$|c_S1Za(h))Y{#Xh-^XFPbSVNeIA0m~lut3Wlo>(-<5pMFAM#CRj$#S^Ta#1m zpe>-OE6S39hozu4d%i|qITa1KTk;{Xk%@b-%Nca zG_YCgFibaTgi3DXq;2@n^|IG2DLe!A$4;BQ|Ib3R2qXJ{wY9GwGoS6vLFPLqmhT3)E%>>bvSu@4|R#8YXXiI z!zk*dcW>!PZy@rk3;28V#KGJbgM(GokT**h7`)>7fZ=#v;_H5ps+lgC9 zRNDpoLfBFwjS3a&=52Bv~b3Xy2Gfa1@?2N|fNH)3~qpREc#=^nd+sLbem6f&RmMkR0vRdEN z6jy=t;Al{wCPPaEs3Z#@)t}T!k<1gFXCg?(RaI5Naye%n&F?#Y`S%AMWaYPT$L z+?jWnxXJB)?$5`)l2SXIls=KTVJSSVj#9jH5T6F9vy4lHbBG9?EY)%tM)9!+V}<(( z9ax5Kt5Yuz>F#4hXTsGusH3+pi)9%}bh4xhcx6gUOUF+nhZCI#?3x#d<9acH!g}}a z-h0JIT3T996WM?UC4Tno+1B>SSE^vFqNNAEajMnv|m2 zyc`dq2!&>u8ye8g9;8GPHVYAFlN@C@o^kk3_x zEmbc%Fn~BIV5=aVie6d*I_~?jAY8QfBkiy)Th!5+eO(m;Wg0dCbyXpKVkjkIz4zlb z^t=>6}${_S6hiv!F1f8&*a(|hlhtH z{f_cEMs&FQkSywnsFM&UC%iSvco1$W;Dyb+N=RX1pa3}Mu|N~ z=4!8IUSHA6ON7yHjST4G^I`Ykm$YqC3PI@Bi7s9Ew0M1@eMmbAz3w|`{bKJZQBeOaB#mGWIT_=w^> zP%b7m*2dFw?3dh3VPz$Wb*03rioJ(NJbgLhd@az*iUFGh? z8n&z{C8SK8EGU)8q};q9E#tsf#zpLHT8akmjCD=|kA@1RTPs>_GT%m6CI)V~f9~lV zwUBRX;>tIrZM?%qKP&%t`sMt6Ck4_5eiG`8;hs~4j8|JyD-_Kt$WO4s!ouG^%{H~> zpQx%_EQ5ssRlp}CR$?P%#%7w{@H9V&Q2m|S`23iqUhd+uBjGD4iu+1V%%pQSsrz+fdC&&iO}PXSA6l{@U+ z4X>-7aZy(#@MlRO%(FBHVfBiDC~kYtgOVILZHPxH>xEMXVZ9f}NJJ%YrBN^d9xK0! z4Y{rbEzHduj_>X7@2CHwLG%FkG@P!Lq(!=h2#)-BIn}kXCzMLPFv(1L6pn z!VHOBN44Os+z)0+T)qTMUXj2+?(7K1eBox((4cgl%~Gd9vFDA|_52FRa<{8=##{i9env}5(@&T&8mX{bMQNY0Gbr;A?d=2M;(nV(0W1+Q!zHCx;gkDU*&*U zT(n*zP-?0=Ec5@gZc_~x{} zvP05;8p6bifeQi_`4<%yMAG-*%O59LXE;%l8(SCsJQa^g{sMt)$W2Qkc_usvte2C! zyD2+>GdpKLHFsYirqFj@rP=c8DV3cN>CF> zDL07+rJY|*jNODuH?E(dwY5cVu$RM1IKV;z?I9{ETBHb_X}tdcN^9`y>CrpRsCmZ1 zY$hr4$QLsxdv$uP_NB_#^woRQ)nPnHVW957fR%s~l@R8pi~YWPy3$Qk+s_I}_*3%l zmfcl-L&GpNHjEFQ2?oBN0Rhd$39*Cg!ZH;LZkyo}88){!*JbznUg$H#Wl6)4)z$hG z#VL}Hjr+h5hsr8cT${x+M+lc@F41L?g7@7O;{Oqcc10>)l*7a`v?cNdsH1aRTN#Ky zgw6G>lednnigC2`8_$Hy|NQyo6{T6G&e!c8R}jv6w4_X8z*cLsC|tzCRAkXck^lsX zcUK?9x!~a9&S($xb*hm96gq26NMc7}-`^f@wRRtf<{wvZH&R0i#^#HdgW6Gax@h?508+pIvD<)0h*N0cv^KYpCx1P?c{f%sz=q0o+4@ofh_qXS*6_u4!S(Soe{}O2yFUc8$+nM56UTg*7ci>*0E@B1KOGvTt9x#*CL*GD}CBj;J;K$F_F~N6m2l-?rU~X0s%I z9+R>@Cr^D+EaSL0=*3p`ILB%;QhbCry|_r<84om!r;;f_eX@uk^h;US>D9G{&vbvk zIw29!@>+jV_!KxNc?dKzHhB`}WiUheQ-fDDbd&zQpJIN{` z=K^EEAr(lJ@M>h;HU~oASf&xQ%BHWT@g+R}mnm!c zMGRDZi)(5O4Ndt?N{x@MuE)B%u$nWXA|vmslVaU}%Z3+P*aqzV!wf!PlS+aTZ8lO{ zYym&!h>44HKzXUPgCL0uZYe#zy)#zA-_d7Y@^SEBpJpqR8SmZiAOqG8ee?A=^NSt3 zzI}VUQA2v$7E=)m&nY)4-H@J?25Vq6F%Hn!`LDK#@7Vxv%dX?0$pm$goK20UlHQs;7mJ1^BNnWr%(s<)%0}gj@)im9&m{a zOJ&SzII;g(lVLqt@5x|4Tm&)Hk3Oq0*3wr)>Tzth2F0dxC#ny(@k$+8aF{TY;C`kNU=b^+Wg&{X}G<)8mGH? zOdhG^adBkda{NPjkAv5;fwX?!lVD>c$9+Ivpgpi}+uL@o&b9Dwf3k;U%>G--g^)jyv%YV zB!-B~xg-IfF)Y}>0?W%;b?CtZz{W&M2VH40PmKGeAAk$jHdA5t4 z5=v7hJ_aQ42^eXUln}~|p(01N(+%_tIG-3$BLdH7k`8CSJvHYqJ;IJoY|D-9wezLz zO$j6cn`cjpuStrfzn9O8A5nS3Cq6*K8!9LnaoHN6-^P53;D!K;-*vEpU?av-Nao#+ z{GQd#j*nsnOwu#7WOf?dXcN;WEp8`!R}4G?9Z$f&m& zi~&b)^(0PN_Z~^a6=e2uChXLSE0fv@lZAsmh}rb&%VE$jU{?S$QX?078Llx$H*Xp6 z*zM+T*7?T201vO>tnl2NZ55dbNvnp<3mUT;MX6nS6a$%^w?NG`6kOZim&Q3%Qr=xA z4HE)2&3W%ZzVi)P0=;TG^;x$Rqwl(oMn*3>04!g-y%!;of6o(ehH@b4SK?rZ`NMv< zoEIE?`f7WPUuSo9Qv@n3J1k%>>Ukf>th{|jr=rgoLt?~u?(RRZ>@kZh#J#%$p=6F^ zR$a7fey61(EMZV6d(r<>@W;(=r{yvfbXUe% zkSmUCco4mr1{|Iboej0c>h|rm#gSKyU?v4B=19S;z|Ez)5@f>8yzYy24@sEpPTItT zng4HQYP27AjWbLr4~YSH*F>`~gDbjKMge~#yNg`m$gw({Qr%l@xcgE=ch(QjLG$n zjASELMx@^celJ$mj$k8>G+;+Q2QK=U3W{8MD!c)2H^dvof8Qig}i2H#-hV}k@D&CkGYNPdl4ke#jAkUydSY4UaXVeSoRhlZwrUcni@D zn0J_YzFDM>QU-+!6sIw)o`P=19d@y=27|)y2L_%j&Q(=IxO}&Wg?)VMZGlyH!qq#H zY6v+EI=aj1dfQ7L`M|~w7>~X!tL1=S$4&HDSod_5cw)BsOpqP?`697smdGz&1sMzcgH%b&4xYa z8M7ykVk7Q<_l_4aCND4Tb%d#wb|0~;&;_3BTUg|Z#{d89`nVn?a?p_OYD+WOS8d-r z=!j8f$DDL`D|q|(?Aql#U!EADOd5h3nq*hw%d~}Cthz51fM|fdXO@lG+jF*Xa&!CN zekP`-cSzudrlzrpj8Y|)l_U|*!{z6mKP?z&eZ4%3AJ3K05wa(^eJedHiw8qht{f2X z%ziwVFl!8fM$3qci+`xAORC8A~4CT)6W{PpYC zL|g{u<_Z7)g%(uT)ac^E%6a+tlvGsi2uwWcd~fIINIaG$;xNeY=#jFT8lixRCkpmf zl>-E7TfP@N5D|3LMQ`5b2Cpjl_>uV4t5-5P%h)e2bzS9Ye89w9S399sJxxt%L6{em z6tSqIH4LU;ZqgpzQdB~jzLU&Jg2x@{RFA0!CmOu!SC%%7JEU!QzwI<*3I)*F`w4UeQJrX|?gNwRH6a@-DIq?j} zb*#M%yJShandsQA58kD=xmJk;L-yIyO#LWmfA%<~iZ?GtdujnUh$>%ZU9Y zQiQVqc7`dWA=z&=AU23KX((!RRNEx~DGuXTbZFt#as^2>B-U(JJd(|%c=!|jXZYcd ztIeTp#jq2ya`@AcmYpY?%2kucu1Bnj6DE1ym!Uo6&rO|OTo#>UWODdBgGL$!??x%9 zU@8UstPMh1r>6}EJVA|lQp%K#N%4vC`5~S05-{a3RW| zTnF#8i^XPeX+6z%_bqS@c5fOiGb4TA5ZhFE%UjQhM1cimw;%8(0@gtDJ%^zOk2xZL z%-^=0Saf~4vv*BMNJ!MC&3}2C)nj}ofs{jso$+z4H!)Gv$4{SbQ_q-4y?Nt!esN(t z5OYr^XV}keL4M#beldlQZ1QxB5Ez^9?TR^R$-doYP+mlB-dGtwq2=9dr_o$^8wQVX-p@q$%+9-@~viHezc4jw<5r|Jzy{~@%K_l-cyChot6>rCK~@fr4IYfVK|!^S#J{!q|JoCej%EB$~8uE>T4cS ze2$mSl#Q}?eIRymfXIJRM2p&_7B&(u7P)q0s<;DlQ(VRA6}Pt1X}o8O7+8NU$FgHa zIzwurQx=IydFF1Mzx$^6JkTIMqX`jPu#@onetiX!@ThQR9Q^a(K%6O!Gp>t}zGyOQ z7A2>nM3b&Z)VBP(-K`Ol$X!wLz2*CsDkwv~sO!trR5BAekq_L_QPZ!$c;x(IGi0*M zvMx}LfqXQTlWXoj6fqJtb)58EO5myJ(S$5?`kll1-dyz!dd2*ZNP57dSI@EHImBE& zocf$C9F0b&YqL?-OQ4(16lOB~<=$(shNRP~q;ksr{t>lzJw85;HfEa(DEy~7HeX}D ze&RoSEh{TKy&4)KZ!E_$WZ{QFG&DRBmy*)o_offnRWNJ!WBq##mkiPz8l@-liP+L9 zV3zK=`n%#qbtdWdSt#uC0r31S^IjnEZ{Yx7$Gpt#>}`ycl!%CQgjYs762(FHZWTVP zBcM0J<^z#t>Ay}GwXckyF)*2wDbSObS}4qTZCec2f%2VY%uX~yhMg`W5`RhiWE_eI_H5i9Y>-cBQVfcjd-f0OHeAJ>K1 z+=r*5s@w-J{|3s1sie5kOVG7$%D*&<0%!qRMsSQmF>&LIx=B7A>LZP6LCa!Nk(Afx zUGEKS+uHm{^V@P<+y+Uu9rA+(!C;W00CGsBnbeP8_P!x378h_PqiEUXnv#I!0e_tO zj7)_}XOlxmsJ{G71NXVT=zFKX8l+G0(s;P8)|{uP8^3&tFewTyp5saKZXN@0sj#`| zYR+e$e3I>Sc{$)8eSy?&Z4uuauJRAaj*jIYvIxYH*)aiNGpTdB@e;&$8@k;$&XWr&3(W?Zd>uY)0;?pyfktUl;ZTJ=1R4P){zc_ zJi|2eFFNA2T4=u3zggqV%k6jGs$)*P?;ghmUZW5bq7 z*7vKm6R+3LfKsETGJg*L1*I4Z*6NOdcR=U?X+0{B@&*5Wp(Oc>?BdYIrk>OQbs^Mz zXvvXi_AJvP>~SisK;j&ArL{xZYlLqIsE)TO{=gQ__YT;wM!~Vo0h@s*t@!|-Hm^!{ z$&7TwfT&CTwNj(K8Uw`Q*cb-m!1s>#oT;Fsf+{L1F@g3?%KoL#2EjiP_0%fqzy7e` znHfA_hN>1Rwdy>b{K%EHgj(jzZB}pASj_o~rN@3m=qmn^nES z9%mqRASv(lS3~GQ$32R8zM$dfPH9rFpoU%rJ4IoIw3VtHRPPcK*MIHKe5|SxJFr9+ zAWOC@6zAGJ#UDxh-brCN|IUnro_rOXtmhDf+WsrBe#pwI!A2=cFlBRE%b~iE!@sn& z)Lx`io2r9#$RKz3@DSxAd%BQqjGIPIP_&56KRx*ZHA0G3G}A~)GK{OUSXtJCxO6RR z&D9eyYHlC{u~S?%AK|K(9uognDnXOL9jYioNy4aP?qQEUrbH#Rx-OP@vFFS9j2@TP z)YQyf)z!nt=s=?FH+U~4k~Yl6&B2mX+S$2sEVtOJ*ZXoSCP(lOz5;t!O+iV=#PryjpVz!fW>sMiidsdznnTEp+!ji=CPbrW7q>t)FcN|3etqdXQk*}U@eer?S!gR~T}h#A_J-@yt5QA*~>4)f08 z#?B2e zI?;K*xDcb#<g!pZnqvF4c2?hL(3~&p_nx-6v5^|!LadQ}Y`E^OuC6`a>7BEN zSVIWJQ9v}iI-@=a=Qi+5mkP)Wj?E3CU~gTmRPv&zOp6ZRhLU`ZU$t_Li=l8h9JViI zHDZoTKC~nT5+{(z>31=tF60b7hWoA}V$*>WhQLfp`SKrQJlCNz_I= z7J7Tbr@DyJK=Rb1OD}AkXi<@zT0nxd8+@iz+p-2|2nZGK;b}zYOVVA@j(W;9e`Pr@ zzBOI}0kPn|admZySQXtIp%P#u%ep_dicQl{{$rVF)_=!dE_Hxhs1m0U==*Bs=;ZW7 zzLtxL!mEfd&Nt-pWay;I>MEYGbob{^(flgGrw@D_0cQ8B2^6lIC%OACi<2ZODhkr+ z-=J4Wwq=X2vFfq(Q>VmJtK`)3|GggTGXy>GPBwYGpP5NFGyO10%weLQBRxHx zjfV80OfV;n8ZQksdbnA8Gt=B{n%T%hreIH8jSDhSJ8ke*y|nz+$UyA`jv5*UV7R&tsyPp*4>j91BHmS7!u3;z9c{H*$|j^Vi( zpf)vZ;9RxYW$c9(5Mqb1B+;I3OVjEJ6(X?tBp?`fTd(Z~1157NTxNUB&CI-qz;N6l z1~_ODckZU@iCnJ7vNm!ELh3;PUNZ;~Q41pgfd|MnMwp#Kk$y$Gzf)(*&$rFq-rgTN zJDC76CbJDlLx4m?Gv@+K`nBQG)KqVxC!6PeK-A^tKB~RXM1n(PY|L$(`2X`OwG@)O*G2AJDCb;O(<*2>ZE9w+h;5eZj*5bWHDO0cSWLY_xsF(4CmWJj-PY-KoPtiX3`G%z#r&j-Fl49+@ z9sm^FcGkURe3++y|CUZ}xAgK78yy>?N*dBVBW9nzb+RFXzra2~FQ(IO$))E@`3g!R zfau@~(L)2M8Bgn(LxD9lG_={ZAHB=1#^;ElORy<3T7kY*-!s{{)w{s@)Gq*HXxt(x zpmkJ!ha&b4aB^<+;a+UNExfh~NDswm^dk!kiv#vM)4Yo8wtyZ5q@3^JO599^KA9|u zkbr5ean!uM(zqsPNyibRduOq1t+$Llb1tXW3PBVp7K?2yg!b*9b7P8?X7tk3*eEG| zN6o5flxv&UUa&vbULVN9imZ44aCyRvrGNR-Q)4gX4qj-Y<1qKzQ$QM^uWNpr+Q;-s zaPQl9H@zBq-~3|pIh(a}ZQk_kY;;TvQE?qhq#+L@+s)Yy3hLOs&BZB5Q85z-{53{s zu@r1@aKJOD?|v}>9G3Ei2PusY41xVv(lb&sH)q_V?pUmWH1s!Gv>4#GDIOYy~JTLAhC26R8gyJ9j zkGza&^KIEU2!rWExFa37K)s63l(bm1t@E~L0J$soG*S$=nz(R;A_FAhBEMKJzGZQa!u-~Sp!u$G^j0Qy|C1Y-^k=oh`%4iCL+vsb6C)^kh?J!|+2?>%6BYuEa z0JCIgNn%{F)lO{$&QJkUBth7y_gF~@2jEXEdUV-I2may(G(Di5*WaLSavyL9l@47x z-^QuNCyxl7d-k*8e1(GFOh^}OpBS}KQ0M6IOi*mjB-P&CJvcoXCNBAR=eNIIw0zmX zxs{^58epj{nkXlPop3I|k=s$LmYuLaLgelN;pr+RcPv|+X>;@PbY-n8W-9akL4W|L zWJqT46t?PyB}%-#oY`Nf(*VK&CQL8szUrE7Q}*LL@QJwiXN!~SL98z$46ON1IK#S= z4*3|?71*M;y0}>B>sV|+3~&+;6b#ZG{7gnhrv0ejw$&t>i#qN@Y3ZFjDfcg@+hb2$ z<^`*P2s5eXHVQt=TeDLm`kn(L6)V0fndSwA4v_iVxhRRhw|X>@=HWP?2U`r^{yRlY z?WO5UbE*^=kD;(hKMOVb^oV8^c_gQ(Y+6tB8}t6X9nM(M5bT60Yr8Qd4ZV{7BH&8& zQYb}JH9Jb3FO{PX7| z-Fify)u)oLS7hkv(v6CCJK#jlOO6suL4cdt)f#6Uv5y^S%~ zy{^WiL2dv^$(ucEJ+mW|4ctuR9~XRplrGRib#&wfDU1Q)_~1NHNMp*aEl)mU?B~yq zfL6Yz_DitSPzn!4k{%VJRzgS2NE^p-He+`7ni+7YWY`_XY@htF`LagI%^QQrKn<}D z>(~Z#*A^~`HEWZ{N_#|t_R~jeR;^6Py2|7#Y*swD zUhMl+rt!sX{jBEJ&3tKDS5yJq^VQ=_=FnVP#<$GRBG-lP*KcUO8qew3HBqqwz_D^U zQH+D;X$#!G_VrNZ)cm~tcRB+o;U`LM*F>nfIa$w|NWP3?762($*MArMcMANXQSkIc zTYoX5R3(x<=r)KncEHiiZ5~zEX(})%;W@)vwmOc??=-zI)u5Unld27Ue$HBEW9Q5*n4PtgOJht!ke)J(rmizOf|G2{nei&jbJ-NbmeeFRwTc z>34A?A=Qbvu3_;%-$G|1n$!FDaVzk}oCKLkK%}WJ-M1-g8hE3FseI;e@;=zArg8vV|w;H+L{=L5yt^C}U?wcl_ z{rozN;Ik9;cY-j$5&xq+s=a=tC_fZW?kGDM*EAq~{qrI{U8!#a$?KvgQgC+AuWVw+ zwx<|=mgF{cGbgmQp4Ku@FiCMQtETz4>eTg3fARRg%hKf&z?brA!Ysrh zf^ZgoYDiJ-A|tc-xhRoFTj4C3B_W{?sH$I8A|#P)r-=0Qo2tW$eA%)1dtbZG12ezX zgu2vGkz)p~uU@&!N=pZr0i}o2 z2;e@liui|sSya(%oXA5XXc05mT~JF@ywprO7rGf z4ZRT(_;*ZB3)qL5jIxZIslZ4c8Zk6|UH-79gKBS>eiOF6h(PeVxb@{0v#W&veWQPW z)mftasp#f^JVoEeH~1iELF}(bQvW)xm`-kPlL*`<{~I-ppyg=%yOt9KkQ=OeRT|7W&LE$T(kQ$&Sa?oQp&kE7} zhSez!zyzS8^B#x_y?C%eJGER~(nNud8GHQ=bNbmEjt-bO=)e87szVIBZ>EGw0yc%~ zX*o@@Cj3bbDLX40P^D89PHDw!lJODTjI66JM92}q4J7={6df5Tpa*JlxruGHZ!QGN od3LiQs=((9_y41yw|#v};Yq-i8+D&KP|gHtsp+ZKDqBbX4?JmN4gdfE literal 0 HcmV?d00001 diff --git a/proto/html/images/index/u287.png b/proto/html/images/index/u287.png new file mode 100644 index 0000000000000000000000000000000000000000..0391b2fe8a9e70d5acd9b55e2dcab638895cbf06 GIT binary patch literal 6610 zcmbt(^+S_y)c#PZNl8gcVn}x*(xNa#q~6E( z{r(5vA9kMIyLUgk?{lu5>zwO}($Y}E2UCGTAQ1j*Wd&^z2qPSL568g*>~w`_1#rP| z*H)4P)lSkJ0)r>kvg)!RP(#AgTQf{xjO(HdcL#w82LHVwjg(H%ZJwdLLX$xgeI;u4W3)mg14}FMz zk0V<8g&hN-lHbHIT&SyKt*&~MP(U;laLK%W8uMt77q57r zsE-$Ku%aFUSYCEcFkoZB|36dOTfb+PGkO|n^G!Zc5Y|+D-nl%6>2@V4x=x^aq$IZB z-~Jeg-28R9%+Ko|P*VA&HLoI3Z}W^i*f&sR@f`&+i?E#?OsA=CR*!yWrwBtSa&G(- zeeqEr!&HYv(qU^dZQSQ}CUVyr~zUK}L)qJY7wUgsP!{7(+xoT~FcsM>2(;VIexc}ky=Gj`j;;-#LqLQ(gF9`E8 zd!VQ?yv6x#J+y>SRkgokqCXzs4=STewoebMn_4OHrK_#lDPyK&JpH=fZCXU2t&}WE zHBWDyCN`_k`o3K=Sp)@=p`8Ql{OPs2So)K&xsvmOuMtI57b<>#dnaKjsX2jxb0xb? ztF15{jrQfm2w=;#PrYl6%BF#!G=|z2{rokjyTWzBqB*aV67S8$?#HFqojzo0qN)CB z`)>0d>(@^+<3BC|p^D}d$Wd@@l-xU)#B7fx+bo{IBR0Dkz#*}ggfjgf7tfbZ-8QiY zc_#vfgkbelEAaCV0SC&3^>$=nj;ady2s-%a%>DQ5(+~aCi;qBIr3n<14J7wyBk^uh zU{PS0eUXbeRVgcN%1DBFd#wf)E2A+8bT(|@_c8to>u+Tg047)5w+h<}Q@)4Q4B>{; z7iHVUqpv}I$uCj?jNst=A)X30OQ|MDYH}Zmx$50+0qctYy#dBs zh5l29+@fqMyBWT~@0>z>fWNsDTmMC@iAKzAY{u^buB@kFVda#p%v{07#7vB-);Uz+h^1RkfHtNg07afVjB0AUr%u z{$1A-L)Q6$1@wc(T5h5R~%u-A@0ZlC|Aj7cF(pIl!1gW5oH9_gj>e#UC>$@PM-XLRZ z9c5->{YHzZ$%UyYdeR)Vlyr!iLg&v2$*xK`6iz!&(A?O)cH3<0EYsYc=a8m{2~|Fi z3Lp*-4Si&%LTxau?dR{`;9d@^2q^K9g*=StG9b!az1y7tBq2tvEZVKTP{H*UmWMQv@Fm9e?3EIMzY);K2S zInR*;6BE;bJpmpb;#lu)#U)Ak$>ziTm1k>pqFUI)?+4HGiO2cLNJ8$B1GZMmvm-$x z9s}cCn)N2}>mOBhGY6w3-)J0_{|nB|OLo6s+OYD`SUwvU1Q`+bBN8_hOKWOSM5eeQ zGW4$`CDk=u@%^O#R^Q#-A$LK^g<8tqv~4(8VMu=rWdeNs=?XUay4+(Tf(XyAFIB^g zFySk+`)I#zK@}hivoa+`Zf83>I*wZKXZ@F#rvWUeWFQ4o?)*lh4|b+_`T6CX55a6# zq-C95t{vC5c6#tmPmYC8ZrRz6?)zhwx2;Yj(ROzA&G&2otYysR*{Rec;Ae7hzZ?P) z+rsWN2VOdV@Gl=40Ez?Ye2u)ik4K$Lzp@_>Wwva+YoPeC)*+8dm$*4;Lh7+toxOp> zU!&4>b#*CbG|@TkuSIEwH@f}jA1u2|jTr3iVvxQH-KELN#K1SU8b)5sE}=Bg;RiT@jJq2K8=lpy4T_C54##mwqXLD+RN`j?nMykp>-t8nuH)Cv0dmnqns^ zG&w`z+QGqr^Oc~WMZGwF(d`iy+a+nW@GM;4AiB3ZZ{7Ay8*39rJPh>*_pPiy8O&RO@A6I$#$9i=p_!F&I0AhvsF3MXt?DnGl zz=c{k2~vL{LS6BY?)`U~vfu293!8TgfwVC1>5WSlo|pGR;`ZmD@2ZaSYtm{jK|^5I z;R&hQO^N5hcZGY&D_%rzY7LHtH~Yh+=wd?E6);oD-zl?GIhsX}?25X5i!4NFO<2)% zOwUg~DUxGTX)~h#I90vzhV@zLQ

=VH=^;pT4d4Lk@$xBhvSLLS10mP!bd*x$ql) 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 Y<@y`k716iAa~RNTsD?tV93u380Oh=q4*&oF literal 0 HcmV?d00001 diff --git a/proto/html/images/index/u291.png b/proto/html/images/index/u291.png new file mode 100644 index 0000000000000000000000000000000000000000..e11bee7895a4e9160176eebd2b064635f04acbaa GIT binary patch literal 6694 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 Q?{+{c3L5YV`Bx$T12w3=vj6}9 literal 0 HcmV?d00001 diff --git a/proto/html/images/index/u295.png b/proto/html/images/index/u295.png new file mode 100644 index 0000000000000000000000000000000000000000..4c987c5b42f8780749ec3e9aab206ac5058fe233 GIT binary patch literal 5937 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 O4SELCQZ84rM*a^y+Ek?g literal 0 HcmV?d00001 diff --git a/proto/html/images/index/u299.png b/proto/html/images/index/u299.png new file mode 100644 index 0000000000000000000000000000000000000000..aefc21c49ba73a8d0b93dc082c450763f2e9c42e GIT binary patch literal 5946 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 NTU}qRR^>_5e*wvTmn;AP literal 0 HcmV?d00001 diff --git a/proto/html/images/index/u3.png b/proto/html/images/index/u3.png new file mode 100644 index 0000000000000000000000000000000000000000..bcbf2c235e1f71208d72094c326d06a869f2628c GIT binary patch literal 152 zcmeAS@N?(olHy`uVBq!ia0y~yU^NG_nK;;hWUuuLDImpI9OUlAulFz$#8yqVk0ku0uQr6Rhiz2n;Skm qs#bqd*~t!6z>qL;+h=(Y;{X@G);<=dC+b|&L0nH)KbLh*2~7Y}Z6_iC literal 0 HcmV?d00001 diff --git a/proto/html/images/index/u35_menu.png b/proto/html/images/index/u35_menu.png new file mode 100644 index 0000000000000000000000000000000000000000..dd19fbb568eb1cfe20f5f60b55e39a90b28d7bda GIT binary patch literal 712 zcmeAS@N?(olHy`uVBq!ia0vp^SwQ@agAGX9wF<2VQjEnx?oJHr&dIz4a#+$GeH|GX zHuiJ>Nn{1`ISV`@iy0XB4ude`@%$Aj3=B-Mo-U3d6>)DbZxlS_Ai(12a!veTyROYE zSEfrMckkT_{$0iRXKu2*aq+v^)oZuv)$U7s_C&JI|MNZe^Qs{&0fJ7h9HNS&jL|>< rr-Z^c$!X_mi`Q?xwv_+C{GSXVl2a}0LwDr>Qy+t;tDnm{r-UW|t(dfH literal 0 HcmV?d00001 diff --git a/proto/html/images/index/u53_menu.png b/proto/html/images/index/u53_menu.png new file mode 100644 index 0000000000000000000000000000000000000000..7e4e015baf850879537b322f2ec900ab0ed22900 GIT binary patch literal 304 zcmeAS@N?(olHy`uVBq!ia0vp^SwQT>!3HGv-*Y|#q!^2X+?^QKos)S9a~60+7BevL9R^{>xJnd*DzRz*MagEG0_GiDaw|8qU+_2Tol&&-KA{QaWM_k*RJqKY9c0fJ7h90S3q j_*sx>A8*HS=P>t?d6O^vOZ)m4=phDAS3j3^P6anMpkR}yi(^Pd+}qoRe1{waSR4Nn{1`ISV`@fr|JJgD~Uq{1quc4Vj)Ujv*CsZ!aI@ZFUe~aV)qdeXzB+{@Ozp zfwimd?};v!VX%2~=lGeJ-_K&NMQxvFZ(RJ*@Bi{YZ<_fM7q|2ZnR(sN7-e7!gcZf| m#>H=Emq%vbtz!DHLO(L)hyi=)@7utbVDNPHb6Mw<&;$ThowbJm literal 0 HcmV?d00001 diff --git a/proto/html/images/index/u77.png b/proto/html/images/index/u77.png new file mode 100644 index 0000000000000000000000000000000000000000..34b28bd658f94afcabc9be820cf833c2cf8b0be6 GIT binary patch literal 220 zcmeAS@N?(olHy`uVBq!ia0vp^1wgFA!3HD)|La~60+7BevL9R^{>lXAp3Uhf5T~PkEA-X71lNGpqOFj)z97%^CKn(XgPzYtDnm{r-UW|_(xA` literal 0 HcmV?d00001 diff --git a/proto/html/images/index/u79.png b/proto/html/images/index/u79.png new file mode 100644 index 0000000000000000000000000000000000000000..4fd4cbed4cc8c21904f8bbe8441571c86b3b96c1 GIT binary patch literal 207 zcmeAS@N?(olHy`uVBq!ia0vp^1wbsv!3HG1$jwRwQjEnx?oJHr&dIz4a#+$GeH|GX zHuiJ>Nn{1`ISV`@iy0XB4ude`@%$AjK*1DG7srqc=eHL(3N{!p95Fa_&A#@zopr0AQ?1 A00000 literal 0 HcmV?d00001 diff --git a/proto/html/images/index/u83.png b/proto/html/images/index/u83.png new file mode 100644 index 0000000000000000000000000000000000000000..b3a6bb10212f17efd53a703529cdb6a35c22761e GIT binary patch literal 210 zcmeAS@N?(olHy`uVBq!ia0vp^1wbsv!3HG1$jwRwQjEnx?oJHr&dIz4a#+$GeH|GX zHuiJ>Nn{1`ISV`@iy0XB4ude`@%$AjK*4lR7srqc=eM^v@*Xf?IJ{w5{O?~S@7Y<7 zTwI+tKlgj*>V|c>-~aY+6cGq_h+!;bJt9gfYvKpnb!}d` zGF=k6d+%28?<&SWbCczbi{H(zUb|JVc3;}FCz5skpYO4sR}Ekg;^qm04R)z4*}Q$iB}`Z|b@ literal 0 HcmV?d00001 diff --git a/proto/html/images/index/学会系统_u20.png b/proto/html/images/index/学会系统_u20.png new file mode 100644 index 0000000000000000000000000000000000000000..db2578e27908edaae4314ca44cd030657bd2236f GIT binary patch literal 210 zcmeAS@N?(olHy`uVBq!ia0vp^DL^d8!3HF6MeqI$q!^2X+?^QKos)S9a~60+7BevL9R^{>anMpkTVEi(^QJ^V{1Sc@G#c9Nw@j{`aqv_v|c3 zF0M|SpZmRYdBMEg?|=IZMfgH3LLLURI*XFZxbo6E;TOa8XPgh;t@gbEw35No)z4*} HQ$iB}3Sved literal 0 HcmV?d00001 diff --git a/proto/html/images/公示详情/u552.jpg b/proto/html/images/公示详情/u552.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c83628bafffd8ed7561452b99b9325c815e29947 GIT binary patch literal 221629 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

EwB#6u$Dh@3b;DkLz&JHS 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 BgVLZi2b3%xEH^Tf9eq|>GT=#nc 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

+ + + + + +
+ + +
+ + +
+
+ 2025-2030 +

年度项目规划

+

七 大 专 项

+

+ 围绕健康中国战略,聚焦医学整合创新,系统推进七大专项计划,
+ 全面构建覆盖诊疗、科研、人才、管理、公益、政学协作与组织建设的协同发展体系。 +

+
+
+ 项目提案 +
+
+ + +
+
年度项目规划概述
+

+ 依托国家战略层面《"健康中国2030"规划纲要》和健康中国行动的统领力量,切合《全民健康素养提升三年行动方案(2024-2027年)》的契机,践行学会的公益性服务的责任和使命,全方位、全领域、体系性、系统性、持续性地开展健康科普活动,通过征集全国范围300个病种的健康科普短视频、科普讲座、科普文章,进行整个医疗系统的健康科普总动员,以科普内容征集促进科普活动走近、走深、走实。 +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 01 + 02 + 03 + 04 + 05 + 06 + 07 + 08 + + + + + + + + 2025-2030年 + + + + 触达100万+家医疗卫生服务机构 + 300万+专业医护人员广泛参与 + 培训100万+名健康科普创作志愿者 + 赋能10万+名健康科普传播志愿者 + 知识普及10亿+人次人民群众 + 覆盖4万+个乡镇、街道 + 1000万个流量平台(用户数10万+) + 建设1万个健康小屋 + + +
+
+ + +
+
+
七大专项计划
+
+
+
01
+
+

《规范化诊疗及医疗质量提升专项计划(2026年)》

+

聚焦临床诊疗规范制定与质量提升,推动各级医疗机构诊疗标准化、同质化。

+
+ 查看详情 + 项目提案 +
+
+
+
+
02
+
+

《科研创新专项行动计划(2026-2030年)》

+

支持医学科研创新,推动整合医学研究成果转化与产业化应用,构建协同创新生态。

+
+ 查看详情 + 项目提案 +
+
+
+
+
03
+
+

《医疗卫生人才培育专项行动计划(2026-2030年)》

+

建立多层次医学人才培养体系,重点加强基层医疗人才与跨学科复合型人才建设。

+
+ 查看详情 + 项目提案 +
+
+
+
+
04
+
+

《医院管理及高质量发展促进专项计划(2026-2030年)》

+

推动医院管理现代化,提升运营效率与服务水平,助力公立医院高质量发展。

+
+ 查看详情 + 项目提案 +
+
+
+
+
05
+
+

《社会公益与可及性提升专项计划(2026-2030年)》

+

聚焦医疗服务的可及性与公平性,开展公益救助、基层帮扶与健康科普活动。

+
+ 查看详情 + 项目提案 +
+
+
+
+
06
+
+

《政学协作综合项目专项计划(2026-2030年)》

+

搭建政府与学术机构协同平台,推动政策研究与决策支持,促进政产学研用深度融合。

+
+ 查看详情 + 项目提案 +
+
+
+
+
07
+
+

《组织建设与内部治理专项计划(2026-2030年)》

+

完善学会组织架构,健全内部治理机制,提升规范化、专业化、信息化管理水平。

+
+ 查看详情 + 项目提案 +
+
+
+
+
+
+ + +
+
参与说明
+
+
    +
  • 登录要求 · 点击计划详情及项目提案,需先登录系统;
  • +
  • 支持方注册 · 注册后默认为监察员角色,合规人员可配置支持方的管理员账号;
  • +
  • 支持方管理 · 注册时选择的企业为主单位,默认进入该企业的人员管理页,由企业管理员进行管理,管理员账号可新建监督员账号;
  • +
  • 执行方注册 · 注册跳转至供应商系统,走入库流程,注册成功后该账号默认为管理员账号,管理员账号可新建执行人员账号。
  • +
+
+
+ +
+ + +
+
+ + +
+
+ + + + + diff --git a/proto/html/leader.html b/proto/html/leader.html new file mode 100644 index 0000000..8d8707c --- /dev/null +++ b/proto/html/leader.html @@ -0,0 +1,248 @@ + + + + + + 项目负责人工作台 - 项目管理系统 + + + + + + + + +
+ + +
+ +
+ + +
+ +
+
+ + + + \ No newline at end of file diff --git a/proto/html/manager.html b/proto/html/manager.html new file mode 100644 index 0000000..6ee2db2 --- /dev/null +++ b/proto/html/manager.html @@ -0,0 +1,256 @@ + + + + + + 合规人员工作台 - 项目管理系统 + + + + + + + + +
+ + +
+ +
+ + +
+ +
+
+ + + + \ No newline at end of file diff --git a/proto/html/sponsor.html b/proto/html/sponsor.html new file mode 100644 index 0000000..6580bd2 --- /dev/null +++ b/proto/html/sponsor.html @@ -0,0 +1,60 @@ + + + + + + + 赞助方工作台 - 项目管理系统 + + + + +
+
+ +
+ +
+
+ +
+
+ + + \ No newline at end of file diff --git a/proto/html/专家注册.html b/proto/html/专家注册.html new file mode 100644 index 0000000..ed993a7 --- /dev/null +++ b/proto/html/专家注册.html @@ -0,0 +1,579 @@ + + + + + + 专家注册 - 项目管理系统 + + + +
+ +
+
+ +
+ +
+
+
1
+
基本信息
+
+
+
+
2
+
注册成功
+
+
+ + +
+

专家注册

+ +
+
+ + +
+ +
+ + +
+

*工作单位输入后联想医院全称,为选择项,数据为数据库中已有医院名称

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + +
+ +
+ + +
+ +
+ + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + +
+ + + + +
+
+
+ +
+ +
+ + + + diff --git a/proto/html/供应商注册.html b/proto/html/供应商注册.html new file mode 100644 index 0000000..a867934 --- /dev/null +++ b/proto/html/供应商注册.html @@ -0,0 +1,518 @@ + + + + + + 供应商注册 - 项目管理系统 + + + +
+ +
+
+ +
+ +
+
+
1
+
基本信息
+
+
+
+
2
+
企业资质认证
+
+
+
+
3
+
完成认证
+
+
+
+
4
+
注册成功
+
+
+ + +
+

新供应商注册

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + + +
+
+
+ +
+ +
+ + + + diff --git a/proto/html/公示详情.html b/proto/html/公示详情.html new file mode 100644 index 0000000..7d2f9e1 --- /dev/null +++ b/proto/html/公示详情.html @@ -0,0 +1,739 @@ + + + + + + 公示详情 - 北京整合医学学会 + + + + + +
+ + + + +
+ +
+
+ + +
+ + + + + + 返回项目公示 + + + +
+ 邀请函 +
+ + +
+ + +
+ +
+ + +
+
+ + +
+
+ + + + + + + + diff --git a/proto/html/支持函详情.html b/proto/html/支持函详情.html new file mode 100644 index 0000000..97f9c4e --- /dev/null +++ b/proto/html/支持函详情.html @@ -0,0 +1,755 @@ + + + + + + 支持函详情 - 北京整合医学学会 + + + + + +
+ + + + +
+ +
+
+ + +
+ + + + + + 返回项目公示 + + + +
+ 支持函 +
+ + +
+ + +
+ + +
+

*支持不需要登录,只记录留痕

+

*已点支持的,记录在管理后台中,显示对应项目以及支持人员信息

+

*支持成功后隐藏我要支持按钮,并显示为已报名支持

+
+ +
+ + +
+
+ + +
+
+ + + + + + + + diff --git a/proto/html/登录.html b/proto/html/登录.html new file mode 100644 index 0000000..dde7189 --- /dev/null +++ b/proto/html/登录.html @@ -0,0 +1,563 @@ + + + + + + 登录 - 项目管理系统 + + + + + +
+ +
+ + +
+ + + + + + + +
+ + +
+

电话: 010-82089470    邮箱: contactus@bahim.org.cn

+

地址: 北京市海淀区知春路1号学院国际大厦908-2室    邮编: 100083

+

京ICP备2020035479号-1    京公网安备11010802034820

+
+ + + + + + + + diff --git a/proto/html/赞助方注册.html b/proto/html/赞助方注册.html new file mode 100644 index 0000000..0cc8c21 --- /dev/null +++ b/proto/html/赞助方注册.html @@ -0,0 +1,533 @@ + + + + + + 赞助方注册 - 项目管理系统 + + + +
+ +
+
+ +
+ +
+
+
1
+
基本信息
+
+
+
+
2
+
注册成功
+
+
+ + +
+

赞助商注册

+ +
+
+ + +
+

*企业名称输入后联想显示企业全称,为选择项,数据为数据库中已有企业名称

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + +
+ +
+ + +
+ +
+ + +
+ +

*赞助方管理员账号是唯一的,注册后,由后台进行分配;
*监督员可注册多个,注册后,管理员可在其人员管理中看到

+ +
+ + +
+ + + + +
+
+
+ +
+ +
+ + + + diff --git a/proto/html/项目公示.html b/proto/html/项目公示.html new file mode 100644 index 0000000..3c74b47 --- /dev/null +++ b/proto/html/项目公示.html @@ -0,0 +1,787 @@ + + + + + + 项目公示 - 北京整合医学学会 + + + + + +
+ + + + +
+ +
+
+ + +
+ + + + + +
+ 类型: + + + + + + +
+ + +
+ +
+ + + + +
+ + +
+
+ + +
+
+ + + + + diff --git a/proto/html/首页.html b/proto/html/首页.html new file mode 100644 index 0000000..7394de7 --- /dev/null +++ b/proto/html/首页.html @@ -0,0 +1,1292 @@ + + + + + + 首页 - 北京整合医学学会 + + + + + +
+ + + + +
+ + + + + + + 张三 + +
+
+ + +
+ + +
+
+ BAHIM +

健康中国 · 整合医学

+

+ 依托大政方针,整合医学资源,推动健康科普与公益事业,
+ 助力全民健康素养提升,共建共享健康中国。 +

+ 了解年度计划 +
+
+
+ 300+ + 病种覆盖 +
+
+ 30+ + 分支机构 +
+
+ 50+ + 合作单位 +
+
+
+ + +
+
项目方案招募
+
+
+
+ 招募中 + 01 +
+
+

百姓巡常行

+

面向社区开展健康科普巡讲,提升基层健康素养水平。

+ 第一季度 +
+
+
+
+ 招募中 + 02 +
+
+

专病联盟暨全国专家智库

+

汇聚全国专家力量,共建专病诊疗协同网络。

+ 第二季度 +
+
+
+
+ 招募中 + 03 +
+
+

乡村幸福安康

+

聚焦乡村振兴,推进乡村健康服务体系建设。

+ 第三季度 +
+
+
+
+ 招募中 + 04 +
+
+

临床科研资助计划

+

支持临床科研创新,推动整合医学成果转化应用。

+ 第四季度 +
+
+
+
+ + +
+
+
项目招募动态
+ 查看更多 → +
+
+
+
NEWS
+
+ 头条 +

健康中国行动 — 百姓健康卫士公益科普"百姓巡常行"项目试点成果发布

+ 2023-01-06 +
+
+
+
+ XXXXXXXXXXXXXXXXX项目正式启动 + 2023-01-06 +
+
+ 健康中国行动 — 百姓健康卫士公益科普"百姓巡常行"项目试点成果发布 + 2023-01-06 +
+
+ XXXXXXXXXXXXXXXXX项目圆满完成 + 2023-01-06 +
+
+ 乡村振兴 健康先行 | 北京整合医学学会2024年度慈善医疗帮扶 + 2023-01-06 +
+
+ 健康中国行动 — 百姓健康卫士公益科普"百姓巡常行"项目试点成果发布 + 2023-01-06 +
+
+ "前腺守护"上线!用数字化为患者提供支持 + 2023-01-06 +
+
+
+
+ + +
+
+
+
年度项目计划概述
+ 查看详情 → +
+
+ 依托国家战略层面《"健康中国2030"规划纲要》和健康中国行动的统领力量,切合《全民健康素养提升三年行动方案(2024-2027年)》的契机,践行学会的公益性服务的责任和使命,全方位、全领域、体系性、系统性、持续性地开展健康科普活动,通过征集全国范围300个病种的健康科普短视频、科普讲座、科普文章,进行整个医疗系统的健康科普总动员,以科普内容征集促进科普活动走近、走深、走实。 +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 01 + + + + 02 + + + + 03 + + + + 04 + + + + 05 + + + + 06 + + + + 07 + + + + 08 + + + + + + + + + + + + + 2025-2030年 + + + + + + + 触达100万+家 + 医疗卫生服务机构 + + + 300万+专业医护 + 人员广泛参与 + + + 培训100万+名 + 健康科普创作志愿者 + + + 赋能10万+名 + 健康科普传播志愿者 + + + 知识普及10亿+ + 人次人民群众 + + + 覆盖4万+个 + 乡镇、街道 + + + 1000万个流量平台 + (用户数10万+) + + + 建设1万个 + 健康小屋 + + + +
+
+
+ + +
+
项目解读
+ +
+
政
+
+ 依托国家战略层面《"健康中国2030"规划纲要》和健康中国行动的统领力量,切合《全民健康素养提升三年行动方案(2024-2027年)》的锲机,践行学会的公益性服务的责任和使命,全方位、全领域、体系性、系统性、持续性地开展健康科普活动。 +
+
+ +
+
威
+
+ 征集的科普内容经过专家评审、剪辑制作、编辑后,将发表到项目专属云平台、北京整合医学学会官方公众号、视频号,并同步转载至相关权威大健康科普媒体平台、项目支持单位官方媒体平台。优质科普作品,将由有关国家级出版社编辑出版科普图书,正式出版发行。 +
+
+ +
+
链
+
+ 科普短视频、科普讲座和科普图书内容相互依联,互为导入,观看科普视频和科普讲座,会链接导入科普图书,阅读科普图书,会通过图书内文的二维码链接导入到科普视频,触达项目专属云平台,实现健康内容按需查看,使科普形式和内容适用于不同的人群和偏好。 +
+
+ +
+
调
+
+ 在健康知识传播的同时,还将邀请权威专家讲解国家有关方针政策,穿插在科普活动中,增强人民群众对国家方针政策的理解和认可度,促进百姓对医疗机构和医务工作者的理解和认可,降低医患矛盾,调和医患关系,减少医疗纠纷。 +
+
+ +
+
信
+
+ 在健康知识传播的同时,也会对当前及未来可能出现的生命科学研究成果进行宣讲和展望,涉及到新的诊疗技术、新的药品、器械、新的研究方向等方面,使人民群众树立起强大的生活信心和对美好生活向往的决心和勇气。 +
+
+ +
+
智
+
+ 在健康知识传播的同时,结合现代移动互联的技术进步,绘制医疗服务机构地图,提供医疗专家资源的动态分布信息,使人民群众能够及时就医,精准就医,就近就医,减少不必要的隐性医疗支出和经济负担,节约医疗资源,助推分级诊疗政策的贯彻实施。 +
+
+
+ + +
+ 立即参与 +
+ +
+ + +
+
+ + +
+
+ + + + + diff --git a/ry-api/.github/FUNDING.yml b/ry-api/.github/FUNDING.yml new file mode 100644 index 0000000..fbcab77 --- /dev/null +++ b/ry-api/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: http://doc.ruoyi.vip/ruoyi-vue/other/donate.html diff --git a/ry-api/.gitignore b/ry-api/.gitignore new file mode 100644 index 0000000..ed8368a --- /dev/null +++ b/ry-api/.gitignore @@ -0,0 +1,47 @@ +###################################################################### +# Build Tools + +.gradle +/build/ +!gradle/wrapper/gradle-wrapper.jar + +target/ +!.mvn/wrapper/maven-wrapper.jar + +###################################################################### +# IDE + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### JRebel ### +rebel.xml + +### NetBeans ### +nbproject/private/ +build/* +nbbuild/ +dist/ +nbdist/ +.nb-gradle/ + +###################################################################### +# Others +*.log +*.xml.versionsBackup +*.swp + +!*/build/*.java +!*/build/*.html +!*/build/*.xml diff --git a/ry-api/LICENSE b/ry-api/LICENSE new file mode 100644 index 0000000..8564f29 --- /dev/null +++ b/ry-api/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2018 RuoYi + +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. \ No newline at end of file diff --git a/ry-api/README.md b/ry-api/README.md new file mode 100644 index 0000000..0b41120 --- /dev/null +++ b/ry-api/README.md @@ -0,0 +1,115 @@ +

+ logo +

+

RuoYi v3.9.2

+

基于SpringBoot+Vue前后端分离的Java快速开发框架

+

+ + + +

+ +## 平台简介 + +若依是一套全部开源的快速开发平台,毫无保留给个人及企业免费使用。 + +* 前端采用Vue、Element UI。 +* 后端采用Spring Boot、Spring Security、Redis & Jwt。 +* 权限认证使用Jwt,支持多终端认证系统。 +* 支持加载动态权限菜单,多方式轻松权限控制。 +* 高效率开发,使用代码生成器可以一键生成前后端代码。 +* 阿里云折扣场:[点我进入](http://aly.ruoyi.vip),腾讯云秒杀场:[点我进入](http://txy.ruoyi.vip)   + +# 版本分支 + +RuoYi-Vue 后端项目提供 Spring Boot 2.x / 3.x / 4.x 多版本分支的并行维护。 + +| 名称 | 说明 | 地址 | +| :---------------- | :------------------------ | :------------------------------------------------------ | +| master 默认分支 | Spring Boot 4.x (JDK 17+) | https://gitee.com/y_project/RuoYi-Vue | +| springboot3 分支 | Spring Boot 3.x (JDK 17+) | https://gitee.com/y_project/RuoYi-Vue/tree/springboot3 | +| springboot2 分支 | Spring Boot 2.x (JDK 8+) | https://gitee.com/y_project/RuoYi-Vue/tree/springboot2 | + +RuoYi-Vue 前端项目提供 Vue 2.x / 3.x / JavaScript TypeScript 版本均可混用搭配 + +| 项目名称 | **RuoYi-Vue** | **RuoYi-Vue3** | **RuoYi-Vue3-TypeScript** | +| :--- | :--- | :--- | :--- | +| **前端框架** | Vue 2 | Vue 3 | Vue 3 | +| **脚本语言** | JavaScript | JavaScript | TypeScript | +| **构建工具** | Vue CLI | Vite | Vite | +| **UI 组件库** | Element UI | Element Plus | Element Plus | +| **状态管理** | Vuex | Pinia | Pinia | +| **路由管理** | Vue Router 3 | Vue Router 4 | Vue Router 4 | +| **核心特点** | 1. 技术栈经典稳定
2. 社区资料丰富
3. 当前维护重心已转移 | 1. 现代前端技术栈
2. 开发体验与性能更优
3. 官方主推的活跃版本 | 1. 类型加持,减少沟通成本
2. 开发时有提示,效率更高
3. 多人协作企业级开发项目 | +| **仓库地址** | [RuoYi-Vue](https://gitee.com/y_project/RuoYi-Vue) | [RuoYi-Vue3](https://gitcode.com/yangzongzhuan/RuoYi-Vue3) | [RuoYi-Vue3-TypeScript](https://gitcode.com/yangzongzhuan/RuoYi-Vue3/tree/typescript) | + +## 内置功能 + +1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。 +2. 部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。 +3. 岗位管理:配置系统用户所属担任职务。 +4. 菜单管理:配置系统菜单,操作权限,按钮权限标识等。 +5. 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。 +6. 字典管理:对系统中经常使用的一些较为固定的数据进行维护。 +7. 参数管理:对系统动态配置常用参数。 +8. 通知公告:系统通知公告信息发布维护。 +9. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。 +10. 登录日志:系统登录日志记录查询包含登录异常。 +11. 在线用户:当前系统中活跃用户状态监控。 +12. 定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。 +13. 代码生成:前后端代码的生成(java、html、xml、sql)支持CRUD下载 。 +14. 系统接口:根据业务代码自动生成相关的api接口文档。 +15. 服务监控:监视当前系统CPU、内存、磁盘、堆栈等相关信息。 +16. 缓存监控:对系统的缓存信息查询,命令统计等。 +17. 在线构建器:拖动表单元素生成相应的HTML代码。 +18. 连接池监视:监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈。 + +## 在线体验 + +- admin/admin123 +- 陆陆续续收到一些打赏,为了更好的体验已用于演示服务器升级。谢谢各位小伙伴。 + +演示地址:http://vue.ruoyi.vip +文档地址:http://doc.ruoyi.vip + +## 演示图 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +## 若依前后端分离交流群 + +QQ群: [![加入QQ群](https://img.shields.io/badge/已满-937441-blue.svg)](https://jq.qq.com/?_wv=1027&k=5bVB1og) [![加入QQ群](https://img.shields.io/badge/已满-887144332-blue.svg)](https://jq.qq.com/?_wv=1027&k=5eiA4DH) [![加入QQ群](https://img.shields.io/badge/已满-180251782-blue.svg)](https://jq.qq.com/?_wv=1027&k=5AxMKlC) [![加入QQ群](https://img.shields.io/badge/已满-104180207-blue.svg)](https://jq.qq.com/?_wv=1027&k=51G72yr) [![加入QQ群](https://img.shields.io/badge/已满-186866453-blue.svg)](https://jq.qq.com/?_wv=1027&k=VvjN2nvu) [![加入QQ群](https://img.shields.io/badge/已满-201396349-blue.svg)](https://jq.qq.com/?_wv=1027&k=5vYAqA05) [![加入QQ群](https://img.shields.io/badge/已满-101456076-blue.svg)](https://jq.qq.com/?_wv=1027&k=kOIINEb5) [![加入QQ群](https://img.shields.io/badge/已满-101539465-blue.svg)](https://jq.qq.com/?_wv=1027&k=UKtX5jhs) [![加入QQ群](https://img.shields.io/badge/已满-264312783-blue.svg)](https://jq.qq.com/?_wv=1027&k=EI9an8lJ) [![加入QQ群](https://img.shields.io/badge/已满-167385320-blue.svg)](https://jq.qq.com/?_wv=1027&k=SWCtLnMz) [![加入QQ群](https://img.shields.io/badge/已满-104748341-blue.svg)](https://jq.qq.com/?_wv=1027&k=96Dkdq0k) [![加入QQ群](https://img.shields.io/badge/已满-160110482-blue.svg)](https://jq.qq.com/?_wv=1027&k=0fsNiYZt) [![加入QQ群](https://img.shields.io/badge/已满-170801498-blue.svg)](https://jq.qq.com/?_wv=1027&k=7xw4xUG1) [![加入QQ群](https://img.shields.io/badge/已满-108482800-blue.svg)](https://jq.qq.com/?_wv=1027&k=eCx8eyoJ) [![加入QQ群](https://img.shields.io/badge/已满-101046199-blue.svg)](https://jq.qq.com/?_wv=1027&k=SpyH2875) [![加入QQ群](https://img.shields.io/badge/已满-136919097-blue.svg)](https://jq.qq.com/?_wv=1027&k=tKEt51dz) [![加入QQ群](https://img.shields.io/badge/已满-143961921-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=0vBbSb0ztbBgVtn3kJS-Q4HUNYwip89G&authKey=8irq5PhutrZmWIvsUsklBxhj57l%2F1nOZqjzigkXZVoZE451GG4JHPOqW7AW6cf0T&noverify=0&group_code=143961921) [![加入QQ群](https://img.shields.io/badge/已满-174951577-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=ZFAPAbp09S2ltvwrJzp7wGlbopsc0rwi&authKey=HB2cxpxP2yspk%2Bo3WKTBfktRCccVkU26cgi5B16u0KcAYrVu7sBaE7XSEqmMdFQp&noverify=0&group_code=174951577) [![加入QQ群](https://img.shields.io/badge/已满-161281055-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=Fn2aF5IHpwsy8j6VlalNJK6qbwFLFHat&authKey=uyIT%2B97x2AXj3odyXpsSpVaPMC%2Bidw0LxG5MAtEqlrcBcWJUA%2FeS43rsF1Tg7IRJ&noverify=0&group_code=161281055) [![加入QQ群](https://img.shields.io/badge/已满-138988063-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=XIzkm_mV2xTsUtFxo63bmicYoDBA6Ifm&authKey=dDW%2F4qsmw3x9govoZY9w%2FoWAoC4wbHqGal%2BbqLzoS6VBarU8EBptIgPKN%2FviyC8j&noverify=0&group_code=138988063) [![加入QQ群](https://img.shields.io/badge/已满-151450850-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=DkugnCg68PevlycJSKSwjhFqfIgrWWwR&authKey=pR1Pa5lPIeGF%2FFtIk6d%2FGB5qFi0EdvyErtpQXULzo03zbhopBHLWcuqdpwY241R%2F&noverify=0&group_code=151450850) [![加入QQ群](https://img.shields.io/badge/已满-224622315-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=F58bgRa-Dp-rsQJThiJqIYv8t4-lWfXh&authKey=UmUs4CVG5OPA1whvsa4uSespOvyd8%2FAr9olEGaWAfdLmfKQk%2FVBp2YU3u2xXXt76&noverify=0&group_code=224622315) [![加入QQ群](https://img.shields.io/badge/已满-287842588-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=Nxb2EQ5qozWa218Wbs7zgBnjLSNk_tVT&authKey=obBKXj6SBKgrFTJZx0AqQnIYbNOvBB2kmgwWvGhzxR67RoRr84%2Bus5OadzMcdJl5&noverify=0&group_code=287842588) [![加入QQ群](https://img.shields.io/badge/已满-187944233-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=numtK1M_I4eVd2Gvg8qtbuL8JgX42qNh&authKey=giV9XWMaFZTY%2FqPlmWbkB9g3fi0Ev5CwEtT9Tgei0oUlFFCQLDp4ozWRiVIzubIm&noverify=0&group_code=187944233) [![加入QQ群](https://img.shields.io/badge/已满-228578329-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=G6r5KGCaa3pqdbUSXNIgYloyb8e0_L0D&authKey=4w8tF1eGW7%2FedWn%2FHAypQksdrML%2BDHolQSx7094Agm7Luakj9EbfPnSTxSi2T1LQ&noverify=0&group_code=228578329) [![加入QQ群](https://img.shields.io/badge/已满-191164766-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=GsOo-OLz53J8y_9TPoO6XXSGNRTgbFxA&authKey=R7Uy%2Feq%2BZsoKNqHvRKhiXpypW7DAogoWapOawUGHokJSBIBIre2%2FoiAZeZBSLuBc&noverify=0&group_code=191164766) [![加入QQ群](https://img.shields.io/badge/已满-174569686-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=PmYavuzsOthVqfdAPbo4uAeIbu7Ttjgc&authKey=p52l8%2FXa4PS1JcEmS3VccKSwOPJUZ1ZfQ69MEKzbrooNUljRtlKjvsXf04bxNp3G&noverify=0&group_code=174569686) [![加入QQ群](https://img.shields.io/badge/127358632-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=M9y5NjAl44lAL_Vh2crmEehZU_PMU6KS&authKey=ZSDz8hEREWSaPuxQV3gEwqGIaGjfRNnkB4rJjf0IvXhrSUGSGwQFmBA%2Boe8HFxyl&noverify=0&group_code=127358632) 点击按钮入群。 \ No newline at end of file diff --git a/ry-api/doc/若依环境使用手册.docx b/ry-api/doc/若依环境使用手册.docx new file mode 100644 index 0000000000000000000000000000000000000000..19d2fc3da32eee76d8539c2e72b76f7ef4723944 GIT binary patch literal 428152 zcmeFYgL7ry7A<^Y+eXK>ZTrNwZQFJy>7-+`!;WoqY}@EqUw-$#_tkyx-T&adRkhBp zbN1e+YVVq3jj_g@Q%M#K9321&fCc~nB!K3l)w^yG0ALLU06+skgX)Mn*t?q9yBer@ zIhwiXF?ib95*L7jQvC#geqI0n#{b1{U?5#lagPak@Jh-zVnk!UH82#}dbOVc9|R_` z71w80`z)ZLRH&POuOtq6E%`FNEJG^psPplz^^Yf;S4)X!SZ1RK1}}X?6@apH)?T~5 zbU3@0oE8FxP~nzROk3~n%MX1{vR-+uo67QWWsEtFvpb(IWt83`MpTsG zKPtr4cN(Q#jtJ^<W$G9HqXvD(5;jHH{evcR`V$-%S?7um5} zpKOGju9~J$^}G#{zo;q&stqN!8dWp)lig9GQ)u3=wg^km4+1ZV3Xelp^1q10=4T!p zZ5UA*;EVFE#P-$*kRr*B!3+=9qpl$tkvf%qS+2m#J0*|m6Xqb@l($-bqc8)0Ydg;(20RpmB;wjK=*8m0sp;CoqPs+XL8bApEp zs^vFR8X9sE3aA;=&flF{0>b36?^0N1g%m@MSbWbM+-%s`Q7R+fk-d(axIF4mfVTDmYX zmy^(oXY3n&a}kNaSspq!Er;`Jrx<3kO#a8;FgG2es}eJp;}E*g zktd6&&ga8z`93+5GIw5R&`@Ea6_Hs|a{h#$nXlm3)v?qkd;B+~u2D&CRk6c*+c7GwtA3vj$9 z;9B=8q|<1p&gTV+4N*~1Qoz9ilhY_y>gh%~xy^<(R^m}d7rhsgS#t*(Ol3dGMXzsD zyX!Up)F5P1>`uSVlgM+Yj4|d-DwNJtO^4JGeVH6i8U91HQ+CH<(zM=57;*Y2o+s5Ga;akXx=k#g&>CWT z0<>6WevmT*nTR;b40(6NC{#^~`C^9=B|Q`}GWs;h&xjaieEPs~lZ)GLuLt?ZChE@* zweP4ZC~@?L!<0pL-pX^lX28_uU(pt;J;u%2B5Bb-3DO$W6KP+Mm2PDa*6Ca$q%cr;BeJ!q7WMoiEqDl0ocFz8RO!J)=U zhZn&quK%cp33AgerBIH$`sj4e7KUR8Ze?VJCu}A+?RBgWvg>*WY)6j0zTIbFr3o>` zL;w%rFjE$}6J1jP^*8y*g#yGY(^>E#6N-#6D~yj9QYgY~)S$c(kcqT;Pf&eeb@r;p zk8CodjhQYPn<<;9hq)-fA9d=pIx%4eFc>oBxs)tA_sIN|Ji<9ew%VEncOrKE z3pV#$YzK?DVh3DKj<~Dtnk`?WbK51XTHK7Ujk9UQGr5%~aZ2;A39brhE#4%&($>C;qn!Jvd8e7i&W0ID}d z?7NJ(1+Tq(?TZbT;9SS_Z)Ixfut3FKv6g}<8sc3?4Rqmq^|{~S6=nU@@Ji@}fhCFH zTt;;I4jpTC-WkPBAUP4_ALHkC*Mbcdwsaf;nK998l)I^F61_iNlTBFFW-K?ugzb=% z$9t|~g#ow;s`!x7@*@=`pIQvER!pX#Q`2539~b8!%{|7iLwdA6F_ zZb447VPc(*e?CsH#6U~*GkrIORdgN>T!s`Qi(DDc%~wr26@J=;Ge-B{h+D3_~6^f(u&AT4&AuAUIdk$D^&;ZR5$Q+3nNZk_z3r zNr?QEQ9W)!w&_RP>eRx}l$*)3s9FNXGOx({pcbG6S;s*SYD{!MKDu|7TAK^;iNqEa z1@etQgbm!T9=pGYRkwBZ#jyh)#cbIDOk`$Q+r<$*@Fz<$a;q9oh-6g*2>(Q1@(#f+ z6FOu$9d>bkq+_4BEQzxy!8WohstE*sJZ<0i8fRsH*ZS#AQ4@PRp%f&B4-MpD>bY;- zKjs4hw9_cafHX&BlW%M7xbN^@kQp2hyud}*dQEsq0h>-L{MG8TJne)osF1qDA%S?tiY~AqZH$F&zfh8>-!g1>8tfl>0vXP+Px=%L2J!S|)a%60Qf>@y3T#Gtf zX-|qbpE}BYpJ#__GLJM?FI0Tqel^ld2YQ11$Y23DmwXRA6p-7(kCUAFGuAJEq({bL zd)-Ice%eeiuJ^v)T6uoriuq*x)>Ah(5~KGCre-wF1mn3wEwsP9d(5j7nKdNyowCWA z+8s4!harRc`l@Kf|22cQB01p)Si^{%CM{r+rYM6hqi| zft;1z9QiEWR~!p*y|G6$*x_?@)T!`@EC?Q`!)kS-(`e_Z`Mn~VLo=Aa%$73OS%~!C zN>j&G6e3F42{n2-rIc`JY1&-ssinYN2Rq;iF`8goD@C3H#p#=0&D$Ib-Ct9k;A`?V z8v{%@SDO#y`VVmmp>0!(XY!&up39;FBTsLc*NS8W5?M z59)X@j-^7B15Lef_nsU}!4eDs5LKd|YBWbda1(iuaak(lH#u6?LY5dKRnt%?+F>JA z%`W51T?@XQR_8qYKih*?sM80fmgPa&XZB|3gJULEC8bE6JBA;bQ$)$zSTZRPD9dCOyd ztZ;i2e{WA)bg?iV6lR4!Ob%UuR=2`}3w9@|Go8Ww%j}i#i5cU(i4PSKTRgO|v3S1A z_-hJ2*2vna1h&uqr+d9-th|;nt+f=xcE*aN(xJcmvAw-ovOER#`j)HdRi?M5%Nf!7 z?+&AalW>swuY?SCMhCxhD?^SuJwMH5#qXR*A-T{GRCP zssp&J-!?SJIrJ8P^I~Q9_&x2(Sa*#hm(Qutw#46yC~0{mmj;Lq^sr#z;fbCpBO990 z&DsxfMy0sRC(awY2A{Gx@iZ7YLX7ZDHX%R*q7Nk?2Ku!U??F9Mlg6ds6E5=!4WmZ^ z*V zH1NGvvux8{50S6GB!f#pcxHpVTH^U%T1aOJazI0LJ#Mwy=xiX+L*!Jo!Vll8d7G{@ z0Y|Y&b6WAkpo(qv77e@DV)^yKnz^Cb%GG@8tN76^wOZ4f)e)GPPzBGII3e3?TUg)2 z*?vN0Sn~?9b+sFIywyLyZyEZsblcH&{8e1hz&E2*Tk}?4>#~)Qt-Nm+#FNc#`oV7E zwGmyAHO+vB%-_V*9jLed}d1@=dK4vm_XIR}*3JTnNAuu_(M>$85G+7xAq zg^Nk{Jbi46ABc)5YqpD zv4vVY{RSj<9i4OQ?zMMs`M?1T-({10jUaM+Qi ze-FN1a*T z)9unHu>|7kA3l?D3qxBRam7T|I#UDaeXV_L@7vt%bOR;aQjE4CEJ1KCL2R3wGjgRDy}xGj0}SoQ@_H|p9l*#%j!sKR##lf$<2}Z81-c^1`I|0&6dNjc30>U&yW%_lww2n1L|`?mh72pO8r9lOck>rg^2wbc!k;9by5L11wey zqD;y}T)wH2=ktb+c+qgWo6HZF1*O6}rdb87L?7^5^$r3LlYV|8W&Jk=EMDppKVDnI zcM(@%G>(5V%Icc5dDD1s9s|`MVYym0R5R+|j4)9*zJa4Cw`n zL>_Nfgt-%$q-6-c>pd!9UQDx2?C|?jbwa0G;1NMs#HN==JCPS$+n&su0gjST?`bzj z0U4}}G_1M%@i)6~O%)Ta4j}xHX}v#_0ftqboTgsgy*33$9dV(r^SJV`u36lqFyABK z!5q5F?d&*@^6+W{8uhEeG8&bpAqJ=@zN1J5cyYf|qBxPJ$lNC7g?(;%Xe0Wi(&8L^ zm*(V*qdgm%OgUXK^Q%G~S7Kg`JU6>IY(LR}WKXlp*?0)H! z--8`fu+C8cfxCIOrVNF^s#q5$qhOhLqTXn1cqyDejw# zei-i7LC@&X1do0Ft!S`L?kGhRL*d15OSA>6sZu+nwIab=qYT7dDhK!7pgDkq=q&7& zp$X#+G5i>2?KdbhIwt4jP|AD=yOb$jKU>r`NqM55Ng>SMm580!kJ()o(-4fpuWW}8 zXX=ypYTyS+Y?9<&9M33J(;@!O&-M@m0_}&g@HTzcMLhF4K%z-A?C?SAA`_?0(!=%Q zVTc@=2G^x$ChS}zRd|nWS$?T4LOEjTtNIoK?1Q>PW1^FvnX+vxw(+@){(f)Bz6t1j zo4%%EX%uGi8Wux4n!;cqVN+qs#8%SkKyiXeYK8Bw*nnvz@C;Q;eSp?zyRJ*$sBE5F`n>)UP9gsA6aU4_g%E@n&YKspYU zoJmza`ktjG0`j?fMGb60|254G`}Hl5`lDVvj?FiCBBqGVMS4qp+3}%!hn>iN-w7J% zNNgRQx{<#3!!hsoO@!C>iTSzg&09LgfIi3+ifpOl`RKMYV&Z#O z)BFp!+PKQC-=D9)LF5q&Hhi+FYTsv8rti=S1P9bJ$(BOg$aF?>mYecYZXV_t-)I1a zrcu;iZyNcL*xDe2aKme`x^@Zg>OVi~>=||f> zn7AKJ%7=Nsu;MZ@3U{ByiQm1TH=cRuY`DTVG!NqO=u+f%W2ZT%NQ=Vj_;N|Pje0iq zLOF9%pcqe#oFPnjt(%z*q~w* zYM;3Y*N2wDB?Qt&e5^lTUwu8;7!tXHij@~p`N-0qEd4y3tOD8SM2RH*r{ZzM_px?@ z2FGhwsy0tuL@LX^JhJ_&g5z(v;6~KHnJgcb?@D>E*)^x#wI6*bX8MdPX^V4?UoIz^ zTD1rB=yyMDJ2`KL8SI!!OJsLX6u}d2&)JP7?v$pEY8UfQMGj@ywvii6bdP=)r)+M z58T5?0k7|jN2K)=%O^7U+yQj_A>S|%2iU)NM{S1N?f?>5twDDm-><DZ;!3Lb&23w79JIOYjGX9|71*&g@dhEL$Y)dNKdU5hxT!V?;NoTGb70fiN4PE4~$#l0Z9A=C~eRv(KEF*DY-Z3}5XcJbX#MsD# zOfPep9m&Q>UJjeQ@2vZB=rI|GEDi$iY#hG(4d`~7s!1{|*#UIzI}TgIL<2?vJJeW5 z)UEO~hO-oHZ*)8A^bYw79vfBHmb!YyH0Oq3aD z#CVW#%Gk0q^cR;t$uu&zD&W;uq9_?fUuW@>Rj=_8#L{cc zynrpM_7IELwM&4rw%STiEl<44(ovgbIxOux0{>oky4~o7ne#ijR%_Su+1t=<7+{K# zidt9>uJ30ezEb0boWq-o&)#T9Ui5Wld%1FRL5h$3Nq>WU%wlav?Upu)ph3|}GT&47 zy6+y4zp1ii^j;DqmiWdxHciM|38i)Ch>al&BP}{`!nj4MfxN=p!qqR8q}D-g>59h{ z7N+0cYGj#yL!gcyDd|tqz@-q&F>;!3GT1h%GwD$2cfFL5S$=M*-+$x~qGw4M9sTS> zN>Np^YRE;W;8G|U&eO4U?OF!o6flpO>D+#FMF8+jYxL6D-iSqPiM6^`m3tB5)Gju8 z8=AKVlL5+Nsb1py2Vf0GbK+<}8Z<9f8QDk@dCwM*rUk3%FZDL~DNkKiQx~CH5*l*&G=m3?)G55gUKZR?m{3#vjmKskUzbkBGwG$ejBeR;J_n{f~vL4i5NtTWRs@~@}**|>V)5>aepqsDV1^1lBb z*D^w7=Zh2(yq&<0Z*U$X);4^X=fwR7RF2-rX6mNJLS`q*6i<1kpuZo+`}0ddsRwC& zU`vc7--~D{$nB99u6E$BO`7r!M(v~-0>7yD>b5N#$OT~T>*)J`}E_{yk%l)blh5SZ)_ zdiyy|;hn%|>ycGfx=wWMoeo5y{Rs)JcDZOV|CkzbpC zHf^I-$jn6|LE%K5_=NyV+gE01!Ij79iQeT(=3?~Md$Zi)gxG6CJOZnyG3`hrw+V=~ zR^(IoM}oU@s}GWxu89)_(R;9R|kXuNYn(v&UouYl;pIErJYWTuBTk1+1m z-bp|?V`zu5;Jdb!#`*@4=jBB?hg@_Tkv>G1Bt#N5T6@~(>(ZRI4EeNWR;|}JgrL9Q20@*;lpUwg5o3{A8|5C=XSHu0+myAabavV z{$ld{p&nl(&GY&j8+>l~!eZQv?eoB;NK;K6?_=y8%Mpa%;^P8TS%6>+2_qtQqPXZJ z)des;MbMxM!V`1clfK)5Ht#?F?(Z82Y9nPK zWn-4Ki~lurF~Kw7Cv&HKj3a|6?Ne74%{oMuq?Q{}(;;|L!-Y3TF#S{pJn%Ve(gL}d z7CwNh9Hn;0fuj3MKErn@w+T^3Oop5U1^3vbJ6GIzCLST2xKG0Ksao5zkwNB%=B2-O7OwM+&Fho5S)HM0 zaQ5NYJzizMn78zcpuuabu(nCd07s*yLlbv{>*&BD(w!l6z`Tp#`uH$tGElqe<@w#E z_4!{@&LtwK2E5<^z%VKRfc!sG&gKqZ@$Ag=Q6isfhQ*X$%xa zP>?6^#1)}aXY}l%g0*LEkW-h)RK)JFRO_`m9ywJ^cqU8VvNR@lh)?ZGs3hV5Zn@;u zkhgnt9RhOjg!nx@2_(l*SqW8GRN_qZfJ{zhc&GtI{$6h0Y)Tjm$WF>)Fofkc6AKZ0 zih^Es4g9_6*A(me<@%-sw!I zhEqnB)TFWl3;0L6ONDLl)je9Eyjf_(6xa9ZMr`4(P5B&U!4sIot)g>f1yw_3xfDR4 z&<}NDB_N60TGC}}n?7%{7Ww^JQWcHWOuQO(X{+`&TQ4|+cj};2kH(=5v#}!PC`uq2 zS)Uhnm@$G}r-q@DRBodZxIQ^a^ZHO|7p5wfWV(*hv6LQ@@zIB5ad)b{#m960-yY>o zJEQje@LKT!@tyJ00S1>mWeXG&(%CXAcu8bQ8Bb)21J}s4*==Jbq4WsQy4XVF4H{Ji-u@hyl<6;WQEY2+YYsBy&ugVb*dh`tc$nLIHX&K0b^_7d z8cOBk{_OIf(nDq|CAI;o)5#i5Zw>0!;{u4SfzPR^y~VMKJp{%BgP{?Y87&QiPt!}a z2OddUHuWu+`uhqyVItJiHH$DsZt;HDrO#vGBZ-c^HN?SR{FzG(6$sOh|BGCG_$XzV zGMaQL15U8WIo?beri0SIB`}=8W|4i4g&W^rWE!Vq}j`%u^QC64Fo$cLP z!V2n#{Ej(pm>JE|hT55Z2KoA<*D#sGdFH(6EEvxm@888oY?XQo>Qh&THYlh+%z&ks^GUCABkMd(Ml zBm+kA*L$_LPYw^C#|~M^jewt!ljHffxkGvK5nz=bF9Ca>K0x^bskX%3wq(l1cilG4 znOYI?F1jsd^QOOBN_+p_$o}u;kX(6q%y?gwU+M7w$3nyW&q7m_vF5z`KbZ6SNgxR+ zXBWm)#geyeob3b*T4yLrGa?B*yya?)Qfi~9c?UEm?P2a}!J7P%D4xy(QOt_wY4+ZM ze~E+-6Xu}3`1SREc(Tu}v6rv6ZQJxDH`1L*-Jp9NOW8UVY*SY{(Av$<8g2>3bAK@&s9NRZy8QQ1<%h>D&(@$#F9O!PtUd$E|nSb$>F3v1}o9sWsog5_EVPsV3 zzR|4G!0ZMs;lc-Jb$1#e@Z-Mlp4h9~%IG(*SUkm2$qmrYSfkBBtivKSo5flx34fQW zb*jv^5)4l^WU49%UjVKr=wPx{x3+?~Pcs^$YTY z{tz_r9Ev2k?vD}C;(Ks0T8e4E<0LN0Sq#Edojqq9d9o2Bmz@)(JBmYFyS?hWnla-v zn>WG5iLO4J&oE9;1?Q1j@61w^*Jxd6#rW>A)o@tf8a{fjx{F~8*8%LZgA~szRrbE- z>V@{2M1zOzg0qIBc#2`B7%J@>_z6D}M~XC8*xZQrjw@?%(?=kCu#MrZ&58G>E8)v# z%VHx5_2$vggY9CF#}V+a1yDeLf*K0=2c#b-sD+_nQO!sq7b6ocEdSU@lOkD^#(^v5(!!2Nv@^?Yb zpf~nzYl*I^#yl{Xz1|J2G~}nbpC#Z2QTNTVzg_#kYsAp{(Rh`;Y1F^`*Fs6P>TvN> zNB}?+>py^GX=Y^lKN!1FQ#N652&D^ZB!IY8NMZJxL>XyJ+s0!k0Rq*tKHg}*@t8zM z<%(^^)#I#5keD0W2_F2K9@=?if$4m&Z!`eL{ns;2`6vD?v^_uUpEPM?}Wwo%nSafOJ>Xqw0V;qJFVs2I;I z8K^2348~~FV}&LL<7w5=RC6&sQ@SLPsQNA`vBRjFO!9dh=E*YL#4M%eTD+LWShe6baf^8#?t z1r;RXMRt+ zaw^dXJn^8x?z_!qB?YZy7g>~1lsNYOE=BE7j1i6foAvu}v7d`wLjLe#*PFJEl7Ehl z}?ic2xAj@QJB)*Wrq1WT5Ui*;@h{*>~*^;k4Jujx@ikh6Bs#1&7V*HGmnrnsYN zB|$+mZFV@qHuFwK+jwi-oBcqKAZa#qeKQwDs9qkB3`gyH=tfKOms@LZ(!MH4D0`rS zNJ0l4O{{*@4te&a1irhe4v>q3HnN`s3J-rYs3l|tRLX)-JK(`L}m6Kz-VVbdXY>+?ME0EWq>Bkw_y z3QvXYc;fQ2d^PRjH_d(Rpk&nIJSvIs%XUHkyQ`~@>xW6fEx*quKaI|ss(uccj@fmc z*SdJk0{LSy#S)RMZJj?rbO`!L3qo2xo=%#wkjXdasGzQSVYr$tu^tlI2iz$Zc%H5; zro5VO#=xHD30^Wk2-@xl=hp6grv~MgUdc`gj_4{7BDeEk+=!73R)th8-oLb&KNsj+ z2%qDxG>IK#A-ENkv3Ppp@#W#EG~-Tbz9Ay%G`g$Vp2sR3pT9I4+qjHJ02h2yh3sq^ z3&(ywNhGnJ&KL75%@^x^O#-F)W(bw;fopdpY}H-EKf5A5*u~6#Q6nx{mJ=^C^5!xz zwz3oApch=HoNv8U*LW0^b+ERmMi|;F=`f>|Il;=yVUIrkg;$(o8yZ~%Z^htK!*bct z{ZMy(=>8Nz)ml7ZO7IpL`L81mx^M_VorWpYh~lWefxuF<2Rz~SE%xW@=5ulQpe?RD zY(7N2ES5TvtD%eEW3t_w>N~aVJ+3`2^*?X9^U4{cN^Uusa{S%jVVkjq0`@?bZ?{e{ zFrG-R10ez$=Y(K=@U2_dfpRrz+pO|XIq`1iNK73RJGRNgR5(3Ud`4cs=F=6m(XZvW^V6_BWUjH7wO=Z|Kw@IY_Q`Nzl|GHvBjM=vb ze#lFMRG#e~nlrZ?UiV7)cDX>u4N#$e6v^bbWrO&x^sS|R1-Q+OHU$B12Xi3gY*Ixq zh1LrZI~48Y7)D7qh#;?Q=MB2)C?<^hguv{|I98**WcWRS`MiOf0%x!CNyA_=ln_wf z_5M8V&d+3B)X7Cv7fv=_G1~+maSyKYR{v>C;Xe#d1m)k6Sma1I+2?DP#j+%*l@mu=uh%$(F*v6Lb{);K1_N>)F?K(+Nk}#4(LeQubf@7JYEPL zl@A-wH`JI@OeZ{r&^6Np&uKrSnoJa3rYVN+I$o6>fLPJJ^R5g5_Ja;a;?G5L^Eh58 zK98#hTPB}{H21vs{GY+6!K~1)_DDgYG!{?Y3>`wm324$PkdqEFR-eVBMqz=kQs2u! z8&P4-^f?&LQXu@-YMtoRO|`hw312_Nu}-UXV?fsHBcdXHfImcbR3tdyBb2k2ae)yCqi6y6Mg~q6+A- z`ZX&1vWcbJS5fu>e{J2*Z%a@#yKqkkcUn^#NV3iHhvSO5;nl=%f~H>;y~kHQ^&e;L zL>?T0^5z1i<-TtQ|H_p7?*g-G0E$u`0sxr%&zYhI}+Xx}L2H`UUy6OyeLOIStuCo?jT@E1_RVm;5UsL2|OKq>~}9o#K5j zsRh>-{E=ee5hDk8lX@mxU-b721l_eh@jew5I*W${2mSluW^ytb6)~+Kiw8`&*TIDh z13rUctugvU2C)u?ljdTN`?T>x*%63+i@(Zg@ z0dWkVpp`sQy=GEGZMvQ~$y?&l45GO1NkbVVZnM-^6kN1UAQKRK+{twZvXqi<^)#;l zT~cX6XmV$`O#9^?Vii7pLlmhd=w~K=JQ8Q;bYmVlJamp0zTIgWCBqV3{*9=a zxh0tM^7tJ|Y9kWqdAq#d{QHw>hQsFdUARiNCDkKiQ|&xP7{$TN7XzyV4g!G+*)C_Z zy!G?(P0e^{;RM^%hfDS2<@IlKk-0u1Y#3s+PSG$CJb1FsBilcAV|0HV>0S*S&H`3uK7-H9o*hUpYl#sI7dhbAYHgG=`UFiaf4MdOudJb z2ux=#;n3ut9ZLJQ(_u>@q01@9C$@CLX)0x&CZ-P(!H5`0g6n&(#38EKesg#)Ip_O$`lz~n7m4wcsEpRJa zON^U<;(@O$EQG|x$p=$Mv|>f7@>(vA5BgIq(Q~XKYUo-6q1H0wh)7T+VqEU33I_?6 zsJ0`ksk|*#Q8s;EgqnQdV>*UPRrFkRq;QnGHpd*4W)TccyEXG5rc_^q9r^}6m9Vin zaIw(h;Hq%A&dl4@E$X5<h z6i1O1AMuNF+L-C>kM`hCC-c43z|`5Ag=Q`NGq(rS?rKmy5D(k9XM*YhMJeMC{@64@ zzK#%DZ>8^DSaNbNR}0bCx`F^8?%Rqm2n_H?V*h=$@n2%>YO-9Fj>LLi;Nd+A`8cV1 zDm8q)tjb>-`_7cA(8;VKmKe!R&+QE7ETe<8Ww^p1qMlsV^3o>HV~Dv@0cw?wsMilR ztg0=`E7AKDN_8o*J#Y%viG=!B^Cz91G(jli3E{j-g7Q;ZDV=P7H(52 z&Tld}u{tm%GwYZ%9#rz!W=!+2BEo66Ocn`TKwCXD$3A0n8N-*nm5|>RtN2xW^?^DM zSX7j*cmeNS#?CKGSKS}a!w%;B3$_an#EtW zWrK&jykWclP@D_kl@IW`oBeG*7+kasAABsBQEy%O`771n9pS&(;`;5mNB`L3c3-ym z*XsN4zLkGW#Q*M#yIw&%?k>xn%jr=5Sm!taqY~ken=v9YI&Zmxb+LA26*`3r(l{?Q zXkQI$fBiPy0X9dGW*iqxOXMY|gi~tVEzxa2Xd%0gBP%AyLcNUpBUAV>`^y%Owxle^ zm}S`m5zQ=9a*ac^0fyG+!|V;{-lSSv?N8||kJhhWsEj$1xW$SJUXSw@lba3o1PexT zA&1c5^CK1xW}*M%i)T`Z>A2hC^nUr``a5Ps=n8;d#QvWNSqT#Kb^og`euz0IWt8X2 z8^?B)l0GNZxi|0%rAtHc<>#_fVt$lW(9l-=gBe<*mnKGZVAL5+vg!;M!n@vcrRuWBGgZLIjwT1|bg zGn=n7#~DzDO)084LG-0Tph&@Wexd?EN#J>Atmn3=Dk zOy@{&6G%`$^na=M?SQwyDZ92zox10^8fq=%xTxb( z+JWK0p9ftHx&oX8U{R)Q2rhTmdFjyBxNssWT|$2_){~KxP~%FP?s0g|Z7X!3IW&Fh zD!o>5yE(pId%hN~!=eA|Y{! z(#R3YmKtH`p)?Q`{yopQwB5nq*5+nVqd19e0Z<^)_4BUn!;w>vIFNIOvyjn@fMC2K zWxR?2j(I93^`<~bERY}|ebJ7YltJtIF-uWv99tKB`IbbMZuq=RY1NN^_R~xmBIcU? z>NUae-xRAfzPC#RXDuhQ~MpvNsh|ypU-istqT)I=aEB4#t zI+zK#pHKftDfwq?zwx)?2kl%9uWE4?Kl}#eN+MzkmwPGE=f(v-M-;IlwHN8ga@KRi6#bQ3 zx^*xVqYY#df=GWtob`Eox5GztaPX52Ysqe&KZQ~QH!-|rdhB=Px|G~m3;!F>;56E_ z)0!vRo$+v^f5oL4wurrn!gUjpefD$b_qSNfO7tT#jM=gaNe9XsvNx2(MXB$yXL`c? zs^LV<5`$0#_dA2q;H{GN7S|&9aeZ|rpMHwOz(C;#cm;tznjCDN4lj~}UxTgvSSbq4 z*gPOz*liF^u=1s}-0JQZe-xQsD_HJoL>T|#D>?QAACrY+`TRlIh}2X{)Na&D$M2^5 zBWPS-+`=SwrP=b0_xrRfm%6MZ1e>FXWEK3ZIrCr4LJl|3SPnP%uL&Nz*c6DXj@?@y zA}__4VsB1j2$-et72D?~UlsGaOi{5)B1mi_R+5UbqRgQ$s53DEtsH?sQLir~h}`xa zq=p_e;Sz-5ovTyb=vut^?trho$=;m}XM{xP3yY!4@_f`s27e`}tE{4Sh(+ZCgVUqDan71W}_!ptB*T&x;MIy2^n&`eVaG5ZLN_|tjLOy)Kl*-NVsVhWVWi6pkrqahYxZaF`h1HN-(b(cEo+fXFh~1 z!okWQT}7W(udWQA)c1QiLKierr%pK%FFH3lb%}Y-MzR*W_LP%_z@15Si$dZb@=&|Z z34h}VN=B`CAm7iT#v@=)d@pK?y; z(TIZ>1n|$q`1<8vjk|VcrdCFbR(3`fW-JVj_7+h}3X%x0xc}URAT1@P0sw&F0|1~b z&|k_4Fx9A2{dE9wQIQk@)J)-@e0_m&l+tnm01yZMnILJjh+nVsvq@S^Sj|)a+z-YO zIJofX#|W1_LeZ*JB$BKxMBi8zx2TQIp8p=2F5cA~>fZ#jD>5Gp{OJZ#t@MssDWy_5 zu!;mC*sI=yo3W@k*F9?tERq3Dr->Qsvhe$KIh>gI;j`uUDtoK%JY?Z{wfUXZ!k^XU z$YYw@qvncp++Xm+NA-cAC7u(KZv6K78tKi#w>U37?5G-0g88%s+ZZ{&zCLTq-qJ&o zNA%er>>nT@5l*Bc5ss8hCI9~cKtR90NfZ@-T0ou@dwJ{2TJ>>?Fd^l6l`v8Y69$<1sVi|)(iE}YPFzH3nid90yQHe)1QghqD-L- zXo3QpnwlD$;H!X;`YM3>1V}Gzx@;j-Dti?F5&8<9F|()~a$|2`b5);f$V5hg2Q)Av zD=Q;26F!-ul`;V?O--OuvrtG%!Iq*_$Cx0MB!teslmk!qrSl6G5#=O$;$KhzMRFM~uk}}WQWv*0VJG2V>>*Egt8&yS zR1XPOO@+G`0zrIiMC9Nl(V`O!`7{WPKL&6lLM4;d2;QXTjPpV++MvpiFuU1Gd8xmN zn>Z!=odA_(ZG}XWWp|1%_U#i5Mafb1n3U}z9jGg*n@A;vpqj;M#Ol7YtEQn9<#m&@ zwGIMuWCsfm(J>+O(+qK5xHPv87A7;RGA^;^S;yF?DvVOPLOn-xeorBEHL#Z-J*((A ztx0rMRn<6VpR0{_rl`Be3s^UFQ45PBBdm*Xi)=MD!l${hv8AEWYJ@NKfyR2Pv7x20 z!D?z~X=(^G!56eN1jOG^AJry7Fgh~p2CfS?1`6j+aCKuve;#UBzVnmu~;EBIJU{Z={$O6iS zB9|bwGRYO4h@5>3BAK>gV~)vh&ro5KlZC`lBt}HJaMeJPL?jFs6*Lr*eqzxT*%j5p z!=gFTekFTCn@^O5Lb54*z5HZcK^vho z1=tK%t1D=Lpb8pn0i3K{F?bq3WHvF9v~$oHr%oh3D0*DlUWAQuG53{CNZG6O#BkSVTv5~%QA|%a#=uR_E?u_epkjK^Blw%JRX?Rl*>YFTyYnT>#RKbT@4FQj}-ob4ZrgM6)3>ugMub8xq6)fmvnHly>zn+$$xw&s%`a zfT5tF0X~6-h8FlVHUz*W(%1xl@hmVNq*;vtLA)RKxaFoIV8GMH8?nZt2N3-@K563ss)W`i@cxm(M%{12zjBEM z8PF&pOLiq%GIb&cCH#@gJP`uj4T5+u>8H~i>mPzPOG!9+W zwtYqz2&HWV)wem5b`{AXsyLnF!eLAtIWh$Fwvj{6CVhF6bctZ4ac3{=q?(>1YG8QQ zEoPe>y3|K-!=M-{x#tA<*r(dAMxH)B>XeyJ%?-`LWh)hYa)L;Y8^BHOW7U1CL+O@D zx&LsuD*K{gbQESwHJ9zHzJOULf|06ll1C|D+!PTuQ4ivcU9r*Xn8 zF>5>v+RMU z$b@pwJiYRb*WL&-J}&y^MH8=??8<=dzZ-rq`IdPH-u;{O@qtlBK&stk0^I-j7`y;sMRbXWB??!6T&R#Z$h$_O6STYgIWteomk?@%68tE|NhA5~ABYn9iNt`B zT`O3x3O2$P={4Nt+$5-}Q*4i?Uf_)}MVBsPjitt@9#*4`sE6zceF3$}kb_aFIYNV2 z4P`ZLgm7z!1%%@APnf%jv``>;OFYDrTruc`bl_XdcQzRLc zywaAu_@1s2gnx5T-@kmk&pc9b#W~c6#GC}366?NVT6hqmsYE6?ds6Q3n|?O?@mJo| zqzYAXS*kXzd-cGsPocoUgZtk9=ev11U)u56$Ft_m*uQW8l3zadg@I>%{K-dO8S%BA zJ$pf(K4+YI-ndICatoF$vImGO7}*Qda_PA76CBLuG2%i>YmQV@URtHrcp+4%ntf=m zNb;4ckO~L^Mb+F5y8#CW$-<1DJtY5v|b+o#+y^ zqM~b4aZA)mE{RZ)c48fopMRh9^}=ntiY zB2{YbRqLecDU-oSHX_$+2ffhPk{n6uiRo&(MGoLs94@ z;RpRi@5mT&P5V?;vOS94+EY{rr@IFANBJmm*4TFgYAVQ1p`j~Dl^7h-(rP~NK}~if z0lWI+Jf&JodPoDGVHMNXKDxity5ZE*H#Q=$k1j-XU6DHIQBPz~%@Mk#AqzA?s%w$s zKbqAhs2zMD=?H}DhtTIjopB#Geg_wkM?&N=&zfcJ3#dWPJTvQ@uBtT|@)WBMDv{2@ zLj+||E7pM85oQgzR-?sJvu**$y3PbN;8 z^NY{_`hViFI6YTZ`N$svU%&m z2ktz4`0!0LZu#Ml{g-Rb5GW2uqM4G1sA33wX`#lLcl%5!b{3wcn}6l5vf8G71Bsp zI0T)d2OsUV*yJqcs-vDCQ{)7R4+t@Dk#U7otb>eKbYo2j(kd+;10>oYn4Vk?h_W~% ze<`*d68dB0`mB1yn7f1$nJ&nPWbhh|n^H_sQd(qCj3OoWf=w7xAeVAP8jMPiM4F2< zT0~OcROQA>PG#k2p+(OgC64hUI*s757km0eg(&H|sS(7hq&3kdKXQeo5^7Dig=k4i zpkruB>SB+58$gpAxrd{KLGxP$5+Q2*Cw|&@2sWBZqJ)&Dq->;&%QEh91i3dJyOXt0 z?5q$OW(JRGml5VZ_zO-S_;v6%h#88=<=?Ob5D8 zxu}ZK4wDbjVK)a-huj><^SHAjmvD!KAeGF_=1Fs`f~BgsWJcM4Kd|?ZBtX%OVmpsl z?%qARJ_e-f`s;S@KkDk>yMH#1I1Lz$T&MK$U;J``>r}6kx~}~3gekY(`Pu7#hEwDL z#AnpR-99+}^mA-*Ts;yfpQ3{dE4T0ezfX_3$Q&r}jZ?c%9^3~nO`<+csxV+mx%}We za)1`UHeB^ScU0-^bAI>N_e@eH{WZ4UKL4>}M-Lo0(5GMTv%h@ygh^Ky-}Iv`Tef`q z>8E$zJ1_s@uYY;?+5RqpPj-HM;&?sT{wZ=?{U@9+c+nA|++-dxKH_OX@d3-0H0aP> zFMM|Y<=U(v{j+a;IIIL;l+D+%?b2F<76Ik^qyTrhILHgL2wKN0G-^VLqCdnJ zxx&jM=sz@r=Y=^k1;1BqNaZmCOe8FeJ@dL3?*?2kw z9uXHGj1-YE;^IzR{-I`yAQDuGqDe#`Iofz=DWi6Y53S}>9e@{Pik%nmF?6{FgV2o* zQdm00iU|~yyGkC!ESM8p01Crj&^Zg*l@0%8W@qCEer9K7iG$QK+Ce}|l+V@6CWs!F zXUW1rR|E|xHA&fcWT?7)l!3MxTVpYT~IT+RkN>rtweP5X0fcr`hgp$-{QW;SeQ5bIM_+K>kN<1>sIQKC{5Q+a8ZzXKSKb_Q>NykU{D>|sbdmP^ zKK;v&zc77`s%8B18>;v2uFq_0c1oGh7>Yusp~sENU!3DO#7K2JorayE6Ew%?@RBfwx;Mi zdnZ=nmolI^7F46(q(0Ot*$vuEgyn@0{hty@XB3pSX*1Z0Ky|6!wR_r@DH|otF18l- z4Wq(VS;$=DPWGwONEFnZ;K6EhUbEr07+FHyJ}R@26OG^-KeMgDl{!GCtO{doRg%3X zk&`1vgakqX%#Ht;0MfX&hnc8^`a)Z7jDDmZk<&%n$uy&r16P_JiH9iPuF4HQ(I>&>oMD;i|F)$rM*D$2CMtHdPJICda;8pv-ty zA9VlCo&Sq%JN5=^e`N2jrp6OtIxQ=^>&g92ciG|w_V4w?@6WtJpLHL1qCfDIUVr?LQB&?N`{1d^&=4h})XDLOn+CvNs|7-=;5b;V z&~PO;tg8$PckhrqSzeSSuNb#-$HxbnKR)ou?|k1L^~gJd3&@WgCS=o~ zs}!yAK5K8?|61ptajN?1bS{`Tz0%w+d7LR-TvDCK<@Jxw=(?0`jItY-L}haEAPSIG z5Gf)sdIiuIO>;02&B71(`0zv9gvy8D^c7pPJ-J$te05nDj18rb2Upfg0d%69_2YrG zQG$48fQpDof;j0UTd9I-z=Y{*^%iouts#N-6Qt+6xFz|kpgy#VRwG) zSTNc3igl`HIYz4*e9C60#=LoLGclpDr=pR_DyFQQcF)NjJ}B~9n(-Vq9GyW$_$Z1- z9Gy1GQXlCgmXE;6@g_kL6GS_-1k%#*Y||74o#a+1pPr^OsR;EOsS$)V@peVLWHGu2 zP(P$An_?H07h8>Xqc6oAGshiiqWs|Y_E&n3Q?dRsLv`(K zr(1Axr0VE<5izNeAs(tN4d!6y?`@0#U70fjST&%KXF`GkenYG zJdDp#xjvv_qe$=th?M@A$Hmhm4uL2SHZp)nP{U%R%}5cytj!nzjH>K``BXI|_zp+l zc*L@{nMxVSNHf=Xal8Ne_BrkO=l)b*U;pyUFIQFlK6l6n2u|3yS@ryaj z9)ITj_ut>MXV3G0SRcU04e8QET}g3AbS;N3r+w?E5BlzSWwSAcB?VG!!noqnR~JPh z8btSkj*rS}yAzdPF~o$RB|TU^Sw)D{i0!1*&8Ij(S6I4o!zEJufG9PhO8TN{uUNE- z(bZO6zxB?LO%T;CVvNLQo^p>t^)HqJ7EC{J-%j`9>JdCKQ^a$k_Wpdh z8Hf1jE!X9~@2=^fJ3&r2Y@lwek|DNDr;pqrmY`8hR_!C+BXLvDB&uy)sSovEg`~YV zgxY(Sp3IOBHp*Ee$3M-u5KDT{g^LfgCITU;mAou^*Bz#m`;-@RQWHrH1AP%zwMnLB zl-26!ks}vRb*F9WYf~#@lt~6A=deBcG{tvm0YA;f)@2gWwTI{&@pPB*q1#(|WpOU! zaFA%t;$QyE_0eQYYC_u{nkzWo{)wmZDb~l1?2&t5QoxYf4^w_w-+j>Evid_^B)|05 zd$a;@9L}2gX@^X6OL3|Q1BocEK|SX0g2GLz4m5t!wQHBXd-wkCcfT9Z?~FmG<@V^( zH^bjE!=LSM$?~`OPw98sAO7(C?%lgjKI!EB^`GiBMA89ea^HqL z2KJir3Yyv8d8|*y7kX#HXV8*&tiY~Uf0TLU9RX|Zs6lH^m8^)umn->kuXGiO#qeW=oJ`(>WhFJnl*jG>RdD|m+V2nw^1CJn$J zhzJORBrf8#bmR5fUIT=infAhgJ;NHsf&qW<3tgsxxrl}3ogMF>A~t8`j3#QtYgR^C0EdsY#dQ)-hZTrxvRgY6X*5s^EEIu;V+?w=l> zq9#*g@uL!L?m(J|{oM72?zG`JVI}l5mVC8T%dOtk4iQpq*$ws>){C^}vQnn1QLmzp+!i12D?~U-m|LLdtP@l?8x$K2p5(Uvu4zaRx4ms8;VFl5ZlS z2BJVQbZKi3$u1; zM4^%oLgmR2r$iEBd3rJ{+8=64HO07J3nkGQ@{jJe6l0qqimd(bTd`rxCWfM?DO0p(%tIP&FTuDuJrj^<|+3r_$gI4`)9xWT3|o? zrJryd&Z?YP3X6B`Ze@4(gTSuvfrDNA-MVC`8ZD?jI}>&la?L1rOjV!8U8nT#2bQWa zmtLNK;iUsl9S%0V#^(B_=HnuO9(E{gIC$jq|1Q73p`l^JTYnq!)o%#{kLU-8qP9ML z?I&M+=0E?vWbEmF|9ijfGwRNT8+YDt-sVR#FTeeJzpGs_1_P$v`AzRpf!CU@9S-B+ z-QV{6YT(Ze*N(6@FYR(wV8iz7&b6#}A3x=5fxk4uL7(rv$NG-4{;%mL{a?85OW*$E z!i^u#gbc{2zV`cR8-9Cb|A6)ROaD3g>1l60cIBR>T?^-1^Z!!+AlxmmPbzFPr^qovlE)XDXCelq~KB*`7L&DBdv=3C3>`+!NB@%=Z zhM;Qm1- zVIq+<;>`Y%Tt*Nza|D^?<(Lpsg;k8)kLrtf-UOtYMo@7Zpv^q?C{UJ8IrhXqcBDTA zr7Oc5&LH)2A$e0Gn2HNsaf3^U8n~+8>X5rn=)672?74-9g%0!wH?5_>bG-CZlf>N- z5^lO*!99&t^?IC}+60YQonPZgbxDIF{!3Gz{%}ey1j6-6=yRdYxR0yf!TDWN+svR( zc^-6K4_qF1&$8y&(=ut&4277gVu-1_YwON^@_lMX1xNSqZai_!>@X^&Q&yMmJ^QLf zRvjHUpl8+NkI%aC`uG3!nXBxE*IudJ^j??|kUQd>(U**IwegJpJ)fI!{`c=KuU+*N zUNA+-6z%f~5mXr&FlSrv3j|c*E(DI4s`X91|JKx#YW!N};eyNq!AJ0Fs`gW85*rAC zdk>{r3zuFs>*w>H-@Z58mI|M~^XHr|4S{_V3Jb@5oe@g z5qL4+@#dD};%W7k3C_7%1fmEIKU(YF^?%lZK z`tzV=K&r-Cf8R0lJiPqBTR!L;x0V$*MhYzdj8WJt%`2nw%*!Wat?O%=P%wc;>n4den0%Z z?_aQd+MADE*|-fj zibwoOcyDA36Rrd{E0U|GpjOhiAlG>OB~}$$AndEEBp_;M1Sy|GmcXMTK!~ z5Yhy5qhxNLBw9vmq+`?@GiLfd#c8&Z3{7XSBsjk_bs_X@Y1e5q48k*twejtb>bBpD&p3$WPs%Xa^1cwHAV39K9g%IqgL zu@`(r5g@|R-FXTb8ZR&)lCX428x@RsIj;5G zQ_WPJPTfkC=z%5&tqhA(+mOQo`A>nijQ_yir{RqylIZ$wI z<|zjgl2;&`+vw|qBb)eP))<@(GUW8NZn){@TW-1alfCu&g73QEcr?>_$d%!YQ+ht} z_+vNUbp1d7wZ{ctIH*G-eflXq|2Xl&slP1W`iGSufk+kbCupA7PXTwU0!9UUV`Jz# zQXr35byK{3;h>APW>hE%qSSr3+>3~HAeum`@bX15GntvET{PzA*|)9vXm_|k^~Zm; z?waphw}1csE3O#(;g;GHha2~Na`@xA&;Pw`|Gz)l_pfcAzw`E{bI%)5S64UdzkdFw zr{59RDq-88cgTq=OX1Vo|FyUN-}@Us5mqI+u3q`1NH2 zgx#I2J4ji8B(2+1|F1pu+xIs7dtc+DV-^3E)nVSke8hA1e+vi@Ur%`PZ=takTt_!ND)rRB>vZuzvn_EqcKr)1>(c>QM->4Myb zB89M+zP%RS0eBIRxcGvT0-SE*uk!Sfv#+IDIfkU~OM2zV0|=LI`IIT{=`8(#^3+Gw6@# zP{P5EK%!6>@^z{e(d~Rxu!#3vigz?AZBge2r8%l3)HKi1caaAQDmRdF0Le%oJGe)B zWUxjk)!0zq*wD~e51;ym2K?Yp{M0w1O!!E5o^pO^(5NoT+yH(ksGX!WX(9C_t-gW2 zz+bFId{LP(zt)^86KzEGOgEPH+uB}T?c`EkHenwXF<6>AOJ?VJ$q{o;Mrg^1B_dg} zUP9IUNT_9dX`GOcPx>PD0FVQ>0>qhw#^U1K{F39jBoK#YM9KEZJ3LYHz0RMLZMkw?5Tn5Xo>u@4#w zi&a#vx|CBM3GyUrx1(-6y0}Fiwe#Yd<3?Xv9h{kW_Rs!a&P!#_v)PqpObrK_w1DkANbO{R~CGF_RQ>c zaz<}F>+jtgzC{1Y6s4dze6oc={G1Fjv5GAC(hKYDcFJij-A?Y^>m-BNAeQc!DtKMs zqF>J)IB?*9{nvl}Yx{>8U7CCK>;bzN!HzgSf6I|$2d#|b1qD|QA3prM)2fM&%m;T)M)2o)eBd%1t|9tI3?^E2jzN8KN=3L*L!X@vD6@B+B z|5f(B-R*RxJ8jrC-&;5J)>n6nRp$xp^1d#EbK7A$f*RNhB=xRZ^fW}Ym?1F&>LA{CDJYTnNCGND zm1PK#4idu18)sydBA|j55o1G!D&*k8=|u#k!h#XV$R1_m(SbN}RW|xq^|j_G(&eg2 zfM!(6qiWx%1g(am%fxxnr8w?Xte)m8g6SFYVMf=efFVg=XrZ8qDd)K{P_;QzhMX(s zqaLU#kj@U+vPY3YUSz)tW3VJq;t>r+eMHTmfaz#PL!bK4$|32$QG-B0eQ1PBNow5* zMJ9xGOdH>%E0HiYIG1w&>v2gP=V-oh4?t5)8?>0nq&Na0sg>j@-s*=bzi<>BW(yW*+S`t>}iN7pWjT1ie7f>$Z}BuG>j`;!gC z41S6&5rI~DQYc4RMc%?6#9bA;Wch(@opf?m&y%xHI;m^Vld@0hnSFB4E+_ZQ#v}0Q z3DgH@7G%imd2*MNPRa`X0SV?=_?ZbGkb`A=Wu4SB6NH0IkaKd+uHt9c-o1OAdd4Zk z&)d59z}H59orZ#KtA2Y&gJ~LE^n<%6-EjUTm*&H!SX;MlTT=db->fe-Hf0{EKeFf0 zCt2N&Ex30cz|_Bg|0jO?yAS{G`27Ew3wt-wzD~lO^x6LvpY^}j{A~FCo|6zE4USaIPqcsu~w)u_vK^LhhdL@P7>!^e6~uxkE!?KyZo{OrZj zQBQsMAHVx122QKaajg~}0O7;KEbvEC z8e}ShY7yTObcYN@qR*)7l=A@HUnob;5@VXtqN*1KDkd_jW8_d%`x)xD!qGOK>ZRAT zT^p^qLM`$|q9o!p@?Rr@MO8(4l=(o-Z?!nto=m$%N=M1#WF-PPe7&IDMIpnl_SMAA zSZNr*|H43swk68E23X5qWnsPIWck7GSO1C3>x2McgjxH-65)Q zdh4W1)HZQDeE`~LYWpI|$7HxTDcnan1j)s!TwxkIc}pWGlrW}ByAcJU3$lm|S`z}< zhlXBaMB%eCYP(e0h*ew(F$PnRWji99?6ovN<>({EqJ}YzHTa9}TD=7;N|W2VLT<^J z<)Z+~-6p7$c8{qzB(>9oXuR6RROFLhZ`pPB9FZL^!6Kf8laq?mjvUSxX)E)U44+qV z_B7r}B|W?KM5I7Pbw?y`g9$W^}4r19Z$TDk|9mUFgueri@B*z@QvTxS&WPB4UH+R#{%ND6Ump3ibL0oV=Lx7QFy#k7g)h3^Kmya6+z{*5hW8j@MzVc73!E&zwBN|!C(Bq3db zP3qYTqj)M)4qCA&QiEh%uyLoZGT|$0sNYCs!8wEgHQk6%Q5JRFzB!@zXzC_GrEo8I z@gigj_@KA<5wP$_k}7iWP&#$UZ22i-rS{ zy380IazGIQk{g!`9ze7MPE&Z6gIAF?SdMKuCH272^aI*Q`}(Wx^5rWEKx1Ldv;Co@ ztsE(04ilK3yh`Irw;i&tiPp&>B_|nO4X9gbS_G3S5vlGNuF7PnM=l9#zWuGr$e1_99KVrGaIwjn7sBy)p5|wtVNlZ zXSqPCtAHy{wy&{IcGJ4**EYTL_uV`Gt(z&b8LD}I_3lN#W6Q!M^mNa^y|7*zjAARt_hr4#}yzuK6!t;P{y|v-&v%l20 zZ{J_saqpqcr+4qsy=SkU*Dd`4@(W=!QR@k|MZLTg16|buFIbVdz}ecVbqvV?E%|7g zKt$Ap=@@})4sTD&+}aX`Gp`cycJ@bqEq zDl#hg3)lT7arGn{6}}D`{!))n=h$5&uEfUeJJCh4&XcuDF1p&rC2dp`QX|$ZvR6?X zMG}hxloV1#RpLLS5AMlG14WgUYpFz4ELVo5ie6-b|D$|zN2;J*V3ZX1jHd+5nrX-t zTFql~>gtt-EE@L5grx(I2C ztSGAqF7U=`twN0Yb3PL3fSAs9n)YCLrb_nSu#-gw)*>57Emo*`&BIZh{3Rkyq z(h-9&;80Up&qWxL__`cS$?Q+IU&H5#=FSm)>knPjG- zeOg6Oyts{FQsp@t((0i1gqCwDWJ^Mo^15()Tr<>F*0}7bs`N$1!H~odHHCX;qieDj z<+D!8w)%FpV6On3oEmzxBf3KlWWr(ChJztNHTIpbjyAZ}b(5+`fAm;O6D}^bdtU$6 z_!0knuWt9h_gDSze}De@=Rdn?@zA_8x}KEvw?F?Yx9^2rvU|WKCA^wKNgY0-%>&O+;kx(xp?}R*GwXmO=#9(HB;Nxgc>*ym zd7)Pf4zUw-Qs`q?s)#sBLZWz}&DdmPg+!CD=n038L(e4l@0J`&dcB$>Jp!H&>Tdwq_DXy10q6Nm{s2H2JF4!!PEGWuWDRvc-QPN~o zQ=vHof&dwsDBpyr-dCueZj+WO=`eH~%uEHv5HvtA02YJPo#YFJ)*q)Lx=%|oHn$umch+A7pf8tojEN+}_k9nM+9LZ5rJQ!9dYN_HF`9rFInKR?N`;r(- zA3Ay=t~nJ~p>Qg?N!1@0y|8o3r%(cJOb;A{aP8(3%}09-I@BCEv1^NO$bjL8j~(jK zt=maG`T(2*zkK?HIb+3(QPAHd>>P4+krza|1=eekxMr!flMFEOO02tGawxS#5gAg~ zP<3Z4_=cud(tcyGDti>!lnliPB;?(-zGp{$XyFz1Fc1PJl>4w zh1*eXX9c22MY4=IFSt}C{k5$U_99I0ei2KQ3Q?D&2CLDx-%^79M^q7EQzP8U-O?U~s4WP1t35%fUIc;@QEe#$J8U*`#=6R2yhJ$l| z3bGB$`De_Oppn1_(h7kHOLFMbo@d+#gUAzM`k<04X~G+UIRibK8> zWfbzt8LXu1I%uos1w>3l$B{XvlJ>5e2%6B=CbA8BU*8x2ql zT2X`pQ<5vWX6djYxth>5TJb4qWHP!YfA%;tfAAdrFkpJn;sBkqAP64@Z1l^^J3%Vjqc(<#h3DcFhzSBis**7GP zRG%Pu;y-l{tTuFZtrB8qb91QJONw%H5WR8@CnB#J4&r-y&6ng#ZZav1p3>$Sw513`h+L#G4-}() zi738@%s*&}vE?|&g&b^p*qO7=W3*wPstz^SkGhhGp{N+Ln~$Mr)nG}SoD@o6p!!bK zWNV`)g}hhM24L|;8uR2iH6&$W^Xe$%dzr+9+M*7cw~BK(;E;lCz9?I0>}4wHp|mxT z6zRt|%|hC&OL*!;T`yp8G+ikb4pLb_OaO;WNv()f8Mh4fwMAa2j7cG}u%Xnm(d2`4 zw zyEUan&lW)nnxj@!Auv)v>*1>~QB1{#h%hx)XuVvUy-So=nVM)JrK)X?M!)E{lO>tM z&t7Kra%e8)v{DPag8EU>d9AY2Ts2$kCzpHe4J4TI23>>A+G@@)`Z>N`CuOjDH$J>pN2b;~x}y(Z5X9idevT^v7EWt&KlksSvOxEJbX7oYLfE-8E5Bis+MJdP3YpUTI-MU_f&d#286%Ug$%m@jF zE7swZlt~$hRJpFL$O*uXBQAO3Qbzk)!xhn33WN`h>{XJf*cTt&ND(fiE~fIRT%yG_ z$+56#CM$W2T=La25*j&iAvLKFW|fy zxX6>@26E9vtdtQ*b5p&(st%;cDX}~V3mgSZyD&3)Td&A)Oj1|PU*+{&swl~kTpCb+ zHD@FR4&q~~S_jEdKSGQ+&%+)p%sG1rXM)nbD7sh$O?HW!gYFn>J+;+-Bv3nusW-{p zX>7721a+cf2L>#P7#SSKtVK*Q&0N#~uz_(l zgsucO`A21)vx8=%4I_Q`7?mzerSZ>|S&u~%WP1h~bl8(<8)H>YN@B^17Y8pep|4(i zCS4JTur|kXA$8{!zk>ysLgqXFl-F-MJ zjnEC=qbsgL4pA|aDs`KvEHi=%C^kY@Gb6xU-T<2fL9Wp97E?Xoh9&j-1kF-LBmAnb z`{r#hS%budj}}H5Cr&0a`)*dV18G?QYR}@H>UI+Wjk;Re0@HoUk0O6 zzD*HVCYN~9iyk9IYDo-HOa-wZ*oiL7M5pLxUB&NGRMJ_r-vV=^E zT_fqV*S%T}1S7Vu@p9o0arv@Gn6b>O`kf7NEp>F#TzG71Y@d*n%I-(W;?-TgXs1p( zvWLR)oMVQ@2{qM@BT{QM4og9*blkUXrI7Sqt)hgKUhF;jrBumg5{d_U@*(@)&&jEb zn+MgQ=6#}Zq11dpRV;}T=_ybYc+f!5%Q?YEc~w4pMu<^Fqf>)yPER#v%+VqljxnI^ z-m&|`i)zY2OB#vMY2!?ItMZC7>7l8bB->?-RaJ)OiX7E)&^X$qYZn<+ixr=Ajbs|S zPSWIUdFXgJ7=uPnFi^1$u$Za~7A{B?k_jb8E=@M5 z?n@Ib)MT!*W6g1q(|2UjNF|&rj-Jz&6G1u@jgYs2_U)(UQMNrrTTs*`OkS3qEmB>s z*E-2sY`)`oD$Xz?*;iy@R}_bCa~-l&(FIgtMavZ)(d|-e1MQxLWJgDa%N-Ep01-v( z#f756_3CI^s$`|+GNs2TG6nTi6h+}+N9rIgN8}2jt!IO6MX*P)b1^e%0s(8U*5a2^ z!*0FNDAiADv3E=jwQ?)6N4=sACMtHy5tmFuMnCaxtid+9l$28OAp}@cJ$M>RL?zpg z(y1ld!8hz78pK0PR_a`EB|3zWxCWRs+W2jEI?Xd>kkw1Bo-WR1I(U|AB4vq;sn87TCizC&Wmvixrh?!abWIF8yM=06*aVX#=~1c(m}9OQ`iq*K z{M#-p;WTd?9CGCR(+!bhR1a@gW*bOj7CWxl#zb?@IoT$!i5R4jUIq+q`WOpkLs#Z;YHhwtrA#E$Bsp{MqQMF$xcp&!%>bilrIH!jcOisM*I+=NQppvtSK z{@1u+m6ugl&+D62Dy~z|Raoh))R#H+l~8)tOWhh?)zXz9lyMDR>c=)5Ek&nC&2djsm@tExnl0*7Oia@>g_+_UqT6KwY!E)}Wzz!HbDE$y`a*Xx^@Cu! zx`Y}dE~26MnpVs)>O(Y2T?iZ(66ds(HbgDJNlcu#je0hOn%RcoOucnf98b{ijcf1_ z+V~e=VK%6q?H;g8Zzg?tL6tY|Y8QaezuNd|i*iASx6?5lG$8KXIPC#s_*)%q- z*Zs_hjHZydvW{c-%ULNFk{rlSvC{Vgjk)Wim=iB`5zz2)q-CvHcR#nF>H?(^1hZwItj-`=a+!R&ht|aYRt>!|2J@ zZ;STkp(JX=*1w`HcY)Wxb$(HV9mVKG{$Nh@NS89(JB|$e4iKkefFBfSoz^8QU-u1p z7DoEZ%03vB;COVw&ZvdTMm>@tK5l>z&eED>9z?3TNR**6Q!Lmz1XGYt1Gh(k75kG);OC*s^%$C0gy^cZJ{isE@UwhIUQ=Z+FaGP|(fS?>o$?_@aU z0w>IGP1;|GyrJ%r|KRcJl)p&+F{@?v^e`91c7koVXR|RRzw4}QUBcYrKX4~rS4m-Kfr^5l*b?2+!qNN1eZfavUCRh8$vJB?o@_5kULVS3h1$j_1%Utn zKtR(^wqM|#yAi75t1AvP+oR?8 z?n=Y|yT8&Ge+oV>D{YT|p1_pG6kXrfwatHCw-bg#F%2nDGmEEAA3fD7WI(DnVRGJGC)M=Pc|@Bch6iy*5V8l@9J zw0aO>4~2^?BXa1SS*qGZiEy6(g zD=P@x%jmt#wq15+=(YoZ48Ze6XKniza)>C4`Ey&b5~07$6Rp}p1qnSZ|8w(?h7kxq zjQZd$yT$L#a(a%~M^9L{qF>?Da(wxHo_o%#X>bCj1Yggpecw;ZClj->P#Q8_R?YRR zJr1*6yX}(JrL`2&|CNmL?izhEIvxl#>~(I9&JT*A@7?(YIbIFmXjS`We&*KG^`x1? z3AlY8p)_sWT=#2L!W%V4+Adu1x1DX3ff7L@ah}(eXs8}=M*2M5ZwIgndRN6dbV*;n z!8TF>tADnZ)wpUfg!@R`@&2|s-HFum)oEar1mB`il|Px|-k`9@OyA8xoN9sqYo|~7 z!i_)5a(d3WRtzIPPN&XG;nh8eQHlx48obq~rsNiO|7V=@LO_KIuGF|Yj~rbJtv^QU zAunX=WavH}MxuG&ucn9?H(&K42P(XGeLjBnZr%ERKKkmtpQyb)oDa=m%{(DQR z_9~KyCCOfmJy^ezk2j*cn)}vGF?TIu+JU?GdQ z>3F(_9|lThM#c!H>N!S^`6D>kb}U2j?qMq#t4c?WY9&@IZSzEFf6I){Jzz)2?b_ak z7tDUyLgB)-cH$+Y$RES|E$Y{9lbDDy!Yv+}*mtEkezw1<)u_&k6KW>Dj~PJjr}#sY z6S3W^s3<1qSNaX|_Nq{sQL|1;>b2rr+3`XIuD{vRS*L=~eb=i^zrv!j0@Y18EeV?+ z^?3x0+9ZOLE=-Kq%SZ7i>C~~Q@B38=Nr*pPs!*`03MnWrFHfyn zcOq}j4-&fbZ6B;3d|11`zbBIdqWeY+mkQ4>r!j(S%sRW{mru{bejiNUrdq5K zz{_X&Nw3C^lGHvXa`!E3TDnDZ=D1jcKm*i>v=%Sj8zTBZ!Iu|&Ix2)GC}?e-&u`+B zf-(BF)}r@Bsxjm0?Rc)zc&vU2{dCw}GUIO{-TB+?riY9%#vcLg=-!WmnTSSW7GVje zB)mb_&Be+O(u|h0yh`lo^3DUtDd!~1;~5GTr&2*P6Enn7LbWXIFpMK71j0ABzSmE_ z-5rv5km)CTAD7Sln{7YwsxEfCqJL(Jt_Px|tegMq2<9O=q+GJl;bRjjenT6{S+*Qv5SE~<&_ODR_hj|X3^Pg*{aYw_8 z0-%=&_sc%p+;S~6ISDn3NS@+|jO0cVGqE~{4)Ec(M%pgTGga0xqa~E8L@?))|FTe} zcnnoi(QayhR(wUwBQ%xA<3?0EzBS2c=V?`ym1g_hmcwj< zY(;UDPO6U=Y;86mFL70{^R3G{=1#XNkwV zTual}i$66wHI*|s+U?%Ybrxe~C}=cSPx|EfB8gO8n}N!e`}@B=8RVrTwkpvAasHAm zO?;b&5pam1xjgcthkAcQRP(SSp%vU~#qobfZ}2#M2%X>c%@)bGO{uUsN})7Npql!` z#FISRCDXzvV>)*lQ}4h}emg6Yp+ZdMlv64?t%h%%zQ1S_kW#8a1Skb(o$_xLtfWfy zLMTugEa@qbx@K`k>%+h0;p8$8%Zy2{>H2U5UG=fE621U#Po{XyE*BrC$@B4qCFJ=j?E<uBwPJntyn?TUJ*kxEob&3R#IcqdUn-jbKN^EJ$gZ zsxNODey$=!xo+3z5J%>pg_ywdit8n*8Q9N*_m3E=|3nGfkYjHYQT_$1BTk)HGQO{p zWOa%##4K2ufpI}J+l=Q@S4EUMxe6OwIHPv4BA#-K{re9`3j!E6!Plt&y+WnRxvr%d z!~J3#OPm0E#wcMva=Vk7US@0tuLg_jyeU_8|Ky)J-VOsBF;hiCYfh_SpORG}rrGWX zsjPu4@Oh-y9AD1glX8=JT5+f*w2$Kl}k_;_>huO4+n(E7r0Gr&H-PJ)7J z8IMkhKaCZ5;x1HYe#ekYFG)x1g(Rc@eeT!NZD0tcD?&21(y%(k+bA~DK9RCmEIs}d zK(l9!%WYoO>vXQG_2o;17TagBADb!ZZ zoV~&{zo#o~UFDn_S*X2nG(yN69}QI~I@<#^wii`@tB-gEl9Lt)=IziLt(w1U>W%s7 zqhP11PWQ3nyA`e%K65*|U~Ic0oC~VMay%kstD#EWOC7c8e84pZgGeNMO!PPej zngSd#7Gl5!qS&FNB!o3VsDWz= zqYv-z32-mO8_7sgYT>ZOgiu*&DrCW!d>Fa$=8raEEmAEP_JFTPGtjtbM9}|spLJ6F z#E5j?<8~Mw%HUadg)Ma8{c|8?3IY!vqaOdsyf1Q~;X)9I1wzL^gy^Ryz32?$vq9yd z*GOUSUul4i>H21jBb7qw+R{p`9+)I0Ra!REH2#slWOMF^*WD=tinQ) z1NqP_FG=D+vB9rXIFs*7Osx(@bdkay@qjR#hwyjewSjL7UXwi2u`!_o%fpGa6nv7b zY_2@ShgObg1rvX}4b7~mas+8>i=%`?#e@?87GyPaV9UO-POI9WOaU}ZD28UyFd>+> zS7~@N?@_>1MA9`)DSrYpO6g{fsEPmwr>8#n_yq%3u zNL|*O(psue?n3B((QUm1h343!7lw7YQU|m&+CKL0-tOBH??>ZjLi*@md?dr{=}mR@ zIX3Y4g0WazK(>XUJWvQKaCSItq5(=SZ1-K&<5cKYwo-gmq^(DR2!>)Bm~2}l(I+Z{ zt1;j9A-tlbQ&-SRFp+YBWxd?8H%Z=%A4G4W3dTT?0znZfC-rV5TYwsv%kpvQJhbd) z;}v#Lo1NeaB|c>MQpzV>*rVX68}7PQ5&#lyp+_tcB?@_0!iUdc%TtTaf3 zawUcpiRXzRqN)`9XbwzG*a)`jj3^@EW~M;L2K$5g0cC;4P)hgUw(GfV2ZL9P zqGEr4qz|w(A-@Q=v#ERP;NT!6Bm^2B)n-HUHQLsT0$&Ms9+c$w(knrd)#`ZI9V>WJ z<}x8)ldqs4^ghQv={NP^^N!!29cLoo|ErI!ok zhe98JS*4YdDu@<(hEgeAHPX{*sYy_`;Rd_9ZjkAv8#7>g}|Zwlk#x7n1Y}4J)=~Mm)Y=xU-}+6lXE8 zDCfmXQ|7A5O>~!~;}d{C_`zuC${8F;za`jIt1o!1HJeVWr!?!R1;t@SLC2?h)y#A} zAHx(wACJDLzSoh(h*)k0W_9v=5vv%GxQ8sO2IGChq1R}25C>4>VcaSKEAWo!CgkH1 zsYxtcyXd`QV6PGa{Z}F2??8QV@qrmVKQ5s%8g_L;Jt=xA?Jp_;v_Z;7Xw!?;2AOJI z8rREzm2ztcQ`a3&zn9Y3+i|#a<)BJ!yNIdxE;{}XV5UY4MnelqnFWbclqyq(?1WV1rS!O z;ObK!Nv#+_^$U-$sW8L=>^N#F#SjRj-`Q{48Gwg$`_+VgT!T~FImDpPT9y!U2WVEt zCZ)RWFcgJX=W%~R#OKma-E%dASVHu%*!kl%luyel$<77`s>T#3Eb|;YFVz9hg(C71gYuC&V`}djpKNvFUWgaaW z2?$P|z-RLJz{gFrTUymy>~Yo9Iu=KRk6wn0JvWD2JpW$^se^PB#Q@gsE%(2OSk(Nt z@#dOa&({F051y)oG|C{yD$!cw&g8$-?6Qf=|;R7MiC>{!-BLCB~GwyGEz zkldX(O0y#-O%WaNiLDkz#N)8HpQ0!DyFW}#w+ws>EvK?+s9Jiqjtff5cpUYi?^1*M z7D#@JZmAO3G!s;{#pt18VR>YxVc3yDup=DAf7eS&>>sVt2RC#ujQ#=fX8AWvFL~T| zCD?|$(FkP*uWDvtxUPZ=a6rc=P=XJ!O^kn2JDxAJYN{WsSycg0u}wUJ8=t&Wtm`q zG`DxThY|&^*SLs4OsHW|_YYOE>_|Vsu=_spp*C5Rpzc{H6Zsqc0fA?>O7*``s;7e9 zFG1$68_9BuVeN#&WSJgfM&%&JG1pqamWt|%)t<_~6(ypT#09kMx3hoys=N@pjqi57&Q2Kqkk}y$KEHZ^*E` z83T29NmK~}EA(B`8XT^7k{0*u-2f_$lZ(I+3uY@hpx&n4j_7+{Unm+ef4%RgKe%Jy z<@52%z_2{J831icqHVDqKtPohaLPR>BXBc(4FVh}+5eaf#`PD)Aa#NLpND&hb|tA; z^$2~h9}J(6Aca^!{W%}d=FDAhZ2c-IgxoA&8f$(eO{Hk@{@x%6kklB?mUa}QRhc=! z7*&G3=(JC$9392LmB(hvsH5id8eN7RyVWl__m0pl@CIG!H6YXaVDYIBM1Q&X!(xrk z{hD8>TqAQB{(QU58*{DnGjvdg8OI1b@f(3h5ypQLKxzid&jJkOTOz^$wnacXRAwl~ zQ5*tpGF?}pN;pP2*dF>1<%OhzuI#W?!hjnw?xSh zijcmPPe%M_j>0J4(3Mqz#p~&4VAK00*d3{kBYi`ZZV{;p`YlM`l!P~{;axH}(Hp?S z3VeQk7T8%GX{vMiq18l?zm7`Cy>+YMgP=Th)d%fYqkt`1{IfdC%VA2;+aqhX`mzlA zrH?NOTxBd@4YjZ2U_3lT-e7*dvWZx$@k^uF*46DV1n86in}9mHd+-}dN+v@f#+6em zSxm|PvEt{DYQt|?__3a4;`+;n2zRr{JahWYLI`c6cl~N9K!tXD++4HmLLsQ{t$N%t zN12mduCeI$g9P(D-F_T^LfcNn%Ss)4lT9=N{u2hi&sAXZQ9B|=kv$xkhX|d)?m6vyq{|Dh{f?-c(*2T_?fYummt1JTT=!m+wM& zWjW5yN{h{9KAC6R0auJdIuhAg#o#_DdVxK(AIPsnW?dGM$a;N?BJ=UO>3UXMthey^ zjuRuB>U}W&At8N8h}U#fum868tGs`008!wSvY?@ji~^T{@@{htHK)gYBdg$`%pwm^au~V- z79-nQY0%dsqI*?6yw5!TqZ}c=DqyJ^idk(y3+hWSu+D}mv)a&Y1p93?!9&Z3ul7TRqbr6X@86I@s} z7@&j+IZ`SDugHOd7r+PKyWe0sWyiSu&}$(xm(GG`O`{X%J3Z!W+KkobjupCxy16Cw z#VW1W0!~O`<8Kd`Y|4H&?5Qg5WL_~Ls0{jP6@EvB-^j@*oe8Ar1vH~H>t-*P%cz4$ z8_TMi-p*7R;6!pyq&J%sk~*tr>d<1mVRtCVdg6VV!Abv!ityI;un+upr496|LD)@M1hQqjQB7oi`mV3`Pvzq9-kUZ-}NBs>7} zoACmmn4p3U#;l4?O0%*OUBrx3PVWHq)h|p}jKH4OH^Z*wr zDK0ElC=pC>%opyA6H2!lsn9gAF9NgWNVGzVwEeYzZRGQ75OBAMC#l9V^3`1bn^|Lv@wzjFWeN1q%OzyPeR_zSmGAR4_JGj9kO+_UAP;x;37T1rBUrn@=g!@YBICJAt z&zV}3Z@XkT-H0XI7>F(@Kct|kDkh7tJk{C0va^-XPSbvCE7pjA@pMPY&)rsi-+Muo z!#9n~$6>l4Y59F|2P2u7!SRIvzJ_0EmN7o(L3T9Nkjy+7V*Gspla z++&Pxqw0!MakWxmzo4Lz$%<^G$R-UPF&2{JmdtR#zos;76;tbG!>?l11&R^ltcsNT z(u58RK{MoS)9WtGlZuScya6WF=!-;azl`sRJ)djmUR9$oX5`~S9y=cIC@hl{Ej;3P zgkPRsC6YBzTp6gxz6nk|ev}whjCMZWBwTHN(8zHML=>lL1Q>l3{!ZntmIYRRRsU9y zo+;3P4w0}Bg=fcsfNea`4rq9ZY>;At60d31vxQDy2h0NTJKLZ0Z(5yzLdPCljM}&- zKZNX_e~5}%5&ViK;_~i0!O5p&bTKS+9dC2)-2sX;0=~0|$p=iXBr{pob4KPxsUlos zs$2*M0|Qt$FYIjlpqz58EF?dlVGL>l)0fF z^E?&zqqVSPT8*Bp>bALhs0syJ7WuqH{yIA8-QUnq71xp(G^v5hjYf!?tA1mU$G1*- zQm5*dkxbd0mU`h|qH8N2A2$rLoRU1g+pe@Z7_zLR8!Sofh6K``QVxH{u^9uI(PCmE zyj5UE+OxBzU?TAmcT+*jKh8y|JRXARf70s0+Miyn-pdEUaW!C(3flUDD_6n(H_lT(CVreOy5nl;A5JC@5#OH8b-ceA3%=Q% zMIi;}B_U9x+zA5F1&jDiYT0T|EAXob9h%_-4EWM*DuFS)_vmET-|g#-#;)v=L1gUo zH{Z%48nl<0{MP+b7h@f_;YnfbiuY#qAOCa9sr#t-2xr|^V?mKN zQ%HAg%494GbV&c2cBDq*L%DBm2+sU4g~mv33u}B=930IB9txWi*B7OoUe6B8pjU8B zp4caO;~-21Aack;NIuiGABt1SFz63v0<$g%reV(}+qC_NhZwk)C+o!r;_888_kEVB zY1J>#be<9W)KGG`+UjvJlf?&0VO-6)Vf8#fe~g`^38VZx1Njl;+P09Fe5p{j&i1nm zFa&~@d9_BG+%uYc7bUi?wwanX4q6^P41EOKhl=kxw(xQNRV5%?RD7>KVB6+T#R`j% z%H@_nxlG1W7s}Nxm;X{UisQ!c8NfL7e_s(=U&*h4c}kKUbrV~pw$~=S0qJQLWR~Pg z+L%WJHE5BMY>qIQ-qQDPY7Cfe$BzL7#*wSET4RIJdAQiirC`8dO(F0{Gnz9dP4ACp z;IUo>Q&zoI{dve#21J>&X9tMjm@K-Wm%=KlYvw8C!Twnw2ihxVYJeHMY0~i>_ zCniur%dMqB^voK3RlikH_d)`L7kOEr(@cwuhv_p4p!$@84+@hKW>8HuE9&DByAl__ z8j6W-Yd6wJh^J_bj-S&JuKkj#C9?Qx17JsS%r@4tCml2>J9(eW@eo%wRslZW+<-u! zh=_<~rTrPxCkJ7-0CcRNE(yaTwqS&eX~^ol>5F85LZzntbnQ`$KZKRS!uiW-;2IRO zPL=Ooe9VlvjhLypAGlT+*^87$@teoeRR5z2#O7uqowA?)`DT|8Z=$0KaJ-gwxV=W{ z8y&8mcAWzwe*9up1s!qh`Vf6aTR{4(w!v$B6q5RCy0i?@SMg#wt-YV#1E(+rB0cUD z(+_*tf!GIMmAZfR-j10dukh?rA4UR5O?@3+y_R&5qd*Jg z3a3bWf*AIKVh96fB@?+_E3WJJ99Qqb+-t2vKd;*BFKARdqtX2sD^5BZwL`=1uc}SW zDBhd##Yyd9j*`BQYVN00mr7+SX)Jbnzp7TKLsFOj=gG~~*^E(JiW1j;+13x#r$u6+ z!Thc92MAeG8bQ^#wJ^cr=q(#m;|Xhzt+JH{D3@%3wk|V}50{M(4jm+ouMLm& zlfpdSfa!m$V=_uNO2=Nmj3M|+-c5Ntk4P7KFF|z zTgqWImTm(KB(Z~~0v_ZLuK_-fZNw>w;6V0loj?T#SaTxG^Kqo-+lz6c^fJcW#c$9X z#9Gls#3^>_ku3RJ1S?$f(P;X=nCRU`C=w=_IY6186 z?W={rI&}>&!$Oc0r&>;Q%X7dq!=_k^$GFNOKGkyPW5ZwP1Uo`u*#6yRC4-tlw_rLS z=!a04&$ZBAR|ollNO|xflauT!E@$ZQY5ei=xB@OS6)^IckYn_=vjidx$r zZHLM&1+8uZ6K#C>{ojxg2UNr=QrYV%pu!UXr)!&&nS5OKN)3)nC44SE9$OGyo5e(- z&}tJ1_0Ujy8lP`CTrCuHTzXC;$#1sxft=dLr=-(uuGRkF648UlHsyN-GAS;XiztnM z`S%l*HA@aCWr8=p==+u$4#nzm8H&5j{80&?5DLbcKJ;`gK~QP(D#bvz|_|!sM#`lrS?V22_;!GxVyH`X>lsSaN7#>1h`Bd zV+^y_Ehsdl`@1>1au)hQFfai}Q?{n_zEzipei9JMCDmpv0wRiViXsr9r1GsEN%-G! z5+Ekm^DxW^1^Ju3^*d}o&TaXe+FI=Yds@6;G*v5+9^QX&y!{E;Gn~L&gND4F7!fYo z+VC|i>@CRhSKn?+V-9${v_}erILjd(9z&i)cRTyroyb`$5zC>z= z?xlz9pB-9iGOC1938cu`xj9Fdle#>O0$kD^r<)noU85}EaD77q9ELXiR>U@)z&v(w zgNi}Rc{GG}f>zYsZ*vkpoxeuq?o1q=fxR<^CW8tFxgI-KNdMkl?`W;&r;`z@>Vm&j zWye{BGKR&d?vHy=kZv9jim0{6Nzjxt609vcKTAZ{c0NyRX?lZw=?ynS)f?hS#$*9p zmR4B3+ztop%!B^b36AGJhv0+C)g(l^mBoz})O*M$%fZNzma693X@P<*tL@@iO?wYf z^Qyxj=+(N}KeieQf~yvp{fsMwI)jhHYN;+qQ7&becni^7z1V#P54Jv7)hwC3cu?{( zNgN(MS~Wyp3ro8AA1%4mO45nwizhKMyhmemaDf5s7GNFG6gXN7&doQRrN|q1*5cex znP4Ktv!k*!?h_ShEjHec%CHGA%@%N9FrE_1;rC2}=LRpGLsOttq_g^8wUylnRYTc- zV0R-c<+KP1TWrop(|6qK9BOcD(%(ioPB$>)l;o#rJ6BtOCct*UdAt^AE98<07DPI(_T>#i#?{cSMPR?@?wse?pxOYkFK- z2eG4soBsU1YJHo@?vw34SfGs6Bt6vR$hQuZk7K*YMz_{tzVC4W1)ZNpIGn(8WoFv@ zl}Tb`nVv4G94HXeAS6obQv-;ZZNu!S+oW*(0uv+DafNy6bMqY5? zGaMvNX27=XfmmN>Z;;5yl_&o0tS9%d!EZG6nF?B7t`T;2PWx>um72Y`J(|u0g%~;V z!wYnT)JEDFQr?>TfkRGN`u3JQ z*qnkh4a7$R)P&JzBULBQR_b#n0+54g$cHa!V%HjQRA4N|ZW!;Rf?YAiH+t(QQ^5;A zX^OV_1&r!DlhCWiPP_D-{?%$!UpJ@T5jPzntXS!AXILhuM5$Io~? zETy@Sx$wV?kHYTYAuAXMw3QY+`KM}smm9`W(<&aeNK|H4(P>mwHIIKxAc>$Bo~Rmu zQ9Y=+6}fBrZgd>QR3G^Fh8Q!yT}#U_v)eAhq^8AVv;9~5wfJzPIE%E&v`A7ztrF*y zVbB=6{&gT~e~g}9KnkaMJlbZimeC(+W4NQ7^Ix60=SwVxrKUiUc zm&Y!B-P^b0g5&j>7n9Qw%k0;K#Y(N5Y>&}Aq4oLCbh%qSf4x+nH8;7>j1Rk`X}_KV z&h{?Ey3g$vxdL%(k57T?`w>O8{hXVBKbI!Po+}xykjb!v81Na4+Z`6_aB{)CQjM!WBvV8!?Jo<-KQsa5_W>-IurCdkKt~%g7a7lj^v0SV^EEN{FF7me&5fM(}}s7J;z4Q zuya(0Al>TP!~AS_&&$0c5m>dJ%&drr=$BLd0o)RiNXC=Ofd;B7;<_dvDLl?&;~}Dx z?WycKIE4P_U-ZD$4Nwjy(rIKd{vfwZV(Y+_7N-lP98BbC79$=F_Xp4jQ-t@nTqi0Min`$W%k_|HQda^beU`$9o@V}3|Zv6 zruzZeU2p7Fb8wT`50+mXDJf0bz8#mt@o_1U?zdYWv)@+ji0H!o+C{^CgCb>#n)krP zR6|MLWeA6sDV^8j!zW`1dAZTKZ)bbs<35=eNWSTM9Bkz+wyJ&J0GjEU1h8tg9Ui|) zMpmq}w6r9o_{*ZRIoNOzCz^|497Lw(hyHS)F-TLo{J;{ztOx%1@>w?}$Op#vX42}` zSNU|+E>AEq?XQ?AN@@MjJdrqKVpFyJbv>XhHxtP)QNdzE`^6NA)i~)4k*kB{u$wUA ziOIM*AscO6U04L8YTb?dm(zExChCX~id+&6C6-aiz&+#Q>twBgs}z(X*{Ddr^9e?`OVqDXor&l zofKAq(@~t^T);+GSbBQNvg^))R5lQf$$lbXQroz(b_R72a6qtoU_Xn4 zyVN`-D(mKR+hQz#%4Y09b^ca+m8i};vBs)?ux()D>Z4lBdww$sN=3W_Ah zv3k7`*VERbD{cNCA1yz3epH$WxShsms=81E3jkmXQ^mt#&k`Y+F7BoISLs?{Yi*_F z#+fXyhE=WiD);-J$TySUfTE@ZPhH;c)4E=D*yL1SS}HO%+{GoreR@lA@4_VmLr#1{ zS7WL~4LC-#qBgOSkvR~E^2hCAx1~aCrRniBV}@96CkgkDP%35bQUYm9vJFLe2#7nG z3_b9%bW{B2_8Q^4x=c5N<7|XLI8opfTk`tZZTJjt;0$SM0sTv4NSRAKheq3HfJC!O z68S*)WoTT_0vi(&(R*_k2MvBHv1dg~>2Q;$YijLy>2KJ^-ThEc%Fw!r+JCy-oUyYF zs{8P+It(4Y$;O9mPgQ7u+i|NgU&+qyA1PneZMy{a*}zv^3pOQ;JA_OD#XoDzk z3s7PC-N>zE9xUnr7}@VrX}id6WE$Y06ABg8gqrqt*?e9fN?_*}8IKC`Y<@y7AnhAt z*mpmrW3N-lRgXVEcO+sV3(whE!3egV|3Mwsm!wKdz{7}1W9Qc6=T86%{UVh)!u-3X z+Na=#&dx>q?PM@APVb_ORC)0|EDABd+r@fIuKN?nCbO;7Y9?SRc0~K=BQiNE7&|j3 zCr333bkA2vu?X>cZ!Gv)f&IWyif+7c-vi$!pOJjM6B%x}<*`F=?_Wtq#+Yao9nusz zzWB2ZTY?Ll7L1QAu$Oo?zXG+qlaS$Ekj=TQLQI~=9f7I zqQ1>&fohuW=6nU+9!c;4qNyGMA6=;2Uz(+CVDtmi#>FBnm^$Vi^?qd$wEGKJwhHG0 ze^0?7ap|4do~D13c1i77_b_Y9HbUvdYw(L#GB+)Pa&$ISR2&O{-G2AB^M*(v(WZBk z0im*AH-qQN?!$M=&|YuQ`0~9xG~r-h@rOfJ`o{Ilis{cMmAN>w?S(!8oP+UWHcyw{ zc>sq&m^eML$xQBi+6}J%)A2y5=EqG+aT@#ebEl*&Fm$4H>!sV^ILlmJVqr0nOTHzb zGvDH?F`Lv^b&$$DuNLTccTJt?eU^{M`yVP4a3D*Rlx@=!trVLVMy*=j1lO}af(II$ zv_lyM5t3i64msKRZTal+yoqA;_WfxH>`Wjog@*C){45b6-T4Mbmr6E@N^VQyz18-9 zA#|XkNItys9V%MrspncOl03bFREQUb9w6WG_i4EO*2P1T9VXD^wK+LQ-!dzqDW(@l zhDU32SWA+bbXx0WQkW8`)Hmn5ABl73U-CJRdeVs| zXx+~y3!2R@-&Lwbk;FcRodmFOq`BNw>4pr$3@=^`@SJQ4*#CR}K7Y2TCIj}cY2bN9TM* zQjhbcde5)}>gB_EP}#Hau+f;r{f-i2q*D|=i~6X6Pn=*IbQZ!BTOun3UThZQ*F5NB zQA_JLtDv#f8r=2Sc-5bOWDIc_q%dGHj3bJP_T6x|b_m{t6TqXN+tgIpkI>hF0@m$C zVY0r_iWfaQ@;Tt6Q}0d)|~Ma?RT!1lviP zQYk}ZU(=ZBY4gtPV_yM6B4^2eM8w4Gt^!NA{j;iC!N%l@>jrEO$r<5Um|@Ubq8ALU zI+6*>I8o{kme}95GX5gC+|Kt4|3~~J-x$dUwOl(`3=@sOu=m z9Y1Hm@FTqoIjw@;gmgnspW>xgwcDx;(C~eyZ+{QT3^eOpZRJoU%gn(zxclg<`Qt$TZttT?j&kPr6z`&hF4hGi>(4)K(b=t@6?VxsdLF#2ZD0^)x{ zFm~{OJ>Lxh(yJUaCv68*hWK3}X1m=fw!bsdh12awpHfb!E$J~&sFka+_gFNb8upm` zK}7k6@hbGwibabu0hOQ}u}o+^UuJW6H3kxeA&!-W|CIXKeI74;HiY^l1&d}!RDk?E z@V>+AAx{#9;uHQM~$B ztL5aom~htz8Y2+5;h)cWZYs!m0L%bjB}CHei)TS}6I8WHTAGH|tK>VYjOA`w9*7P- zXSHDd@Qd8(;K34Wi4Yp9=9Z=z7t=`XSB_i_cAg-$M?|Mfkm)%KBZBx{31R0zE`bykeM;zDKi#n*yB=iaIjj zZ$zx|y0`0&DAHGdchPI5{v!S4fhwYu|MEAvpoa5c!vFAr`kpxrRmijXo1FT26~>e& zD?n_KE{qaGD`xw8^qu%Php=j{h-G(nlJ*3P+etR^W zJ){gU9EgFO;18cgT1$zg`}*?F6Jw3oG9;N;0P1HbXO5@Me=7b2`JE2;->`AF5~2{| z)tAEt8XT}VaF#hg*i4rD9=x+NY_Swn~q_M&yF9ykN z_=X=%8S}A0;m%Mhn>Sd|#<8R&CfKd@v3Gj(NzqI^P1wG0>D*U8-x73wzW2We;;YWZ zh+V7a=y)85Zgyd81T%LY`99hgO)XQ&SSb{1ImS$!4DWt#Y}{t|VD6Osp{P?LmjHP% zT96_t4hxpQ2AC&;VX>B)y^T1jF3fl(sDI9vuIARX-Fn-B2-Y7g!nwfhJb@^x{kzP- zW;qidjinkpt75rQ*szOA__QQb^v>K`dL1ex`niS7OxX4L5rX{M;d1oL0fVR9a!JwC zl9lolY>iE5JZB&?S9G1#Vhx!3T27R6JvF+7#%({227FhjLSkT)L+AEVw{e7E4#K#E zi2Hid15ZZ_cuDytv7%3|#ByWs*2m!20=Asq@Y86wcdep?+0b)cKaXQCH@7~pLOq;~ zANANED&Ke#tCkLSW;J2C=UJShr@uiC4P61F7O{{!$in0iEv2jnxG`Fv>jQ0PUyU+W8;wR5)7WRuOUx6<#bYuBICv&rI{{`#Gt&-Z!Kqf5no|I3Y&L76w`}yWCI862q+*5Ey5dBW(JXR2+ax~ z=^nuuj}wm1$6cHke>h8-iX^eGiOv_<+r0;`k0YGoH>C9oV}k2c@K&6C$jzC^PL_{6 zg`O6~baLK~WqkiVCi(oFyd|*-ya=mBVzW1|HJ)XDW5UHPVL>>WxU&Wo4l8H4pXQ&D znkXtl4Xoh$tNio>XG_3oF>l5|?|s~6tuJr|N&!Ll>TGvX97;)1oDIoeKKU-6gkHzt zLy+9o-|ME{dZXfZ!ezA+MZ8l=JntmiXnL=JD?c(&Q?(*OU$SdE;5;Q@;lB7~BW1ea z^WKM`%4q;KSi#mSLaZNr*{XX@_~_kPZz0GqCP!Wbq<ko`riMaiF*p~uS>&>LFPDShC`6~G)QB9m;2K78j_xvH8<*X%X;W~BkTmf_l zrTe2m3u zKUTk4Z}VzGLN4#-?$^(CqX*|X=!Z#1g*OHSF#G`ru>OQY#Dge`Uu_S6U@I>S3>5;> z^h=@`sDakg*!t4}n#0&spKL1@(CceuNUOHVnUyzr@|-^!l=bWy{!2I`m0S=&Ria`9 z3YMF?5X6C6Zz3IJz&c(F9ox^qCa7F zO#5R@7mm7oo(obMg5)j!Kgzx;tg1EYT1rY%QaU6y-7Vd{*&FHZ76CyC>F$ya$xU~P zgmiaHx1cm4;J+}=Ip6m@|HXI3&Enl_y)oZ8=NNNLCUD)vKm*kyc=+O!eL0xn3_!jYkL#d!d( zn=&s}02;8KX8pVY;N#D3i}qCPV%N1E&hrHm!>920NSu#JnXBod8T18)?AsOeX(#2m z=%9O3x(d%d_lGHUoru0Cts2LL)pdruPvS7BW*G0h$d!#@=nHKdETr)jeo?Ctyl3Zt zLNn%#A_snYsU|DpnH85mVCkC=Yvj)Hf*E*hC8jwv7gtpscL4-3S=csGPsK6I*dsJO zpKqq0xAnZ_bjsCajs+GE+^!Bq-4^Dk#Rny z$@)7BlZ@Bc?8W<2%M?||-{^$g-{|K_ExwW-*zzLJ^4F-(Wyc+>|Kz9`fo1ZmcK^`# zZ8&5tYCntIA@@NEIjR|d;Sd=0Lbp2VxKyeyq;q~)sBl@6OJR0B= ziv)lkD&n?%P(VxdTLNbRX4|<`L^jSe^le0&U4UY|z-n@#*2Z@96(Pik0ISUp$d{#y(}4&lE5^j`BH06OiHN;-e+!{N%(a{ZQ0rV#=* z1L0RkOIq@YhN-1zJl5=IbBA6Z#)R%<_y&R-2@3fe)$(SV^gu3QRsG7k>IS%C?lvhsb)u%YO7^q8Muu&mh`hp{ z!}gfbjOu#H%(oUdr9Fr6^it3S*{#E_yoz>z+V`f#CYhW(Y=pb2PO z8=*^h<1w>45UcBhj9{>u~ zie<7muKfT#jGjm7oUKV%t>vpY4iy*^gh4@16BR*5B!*HWJ9#@?yQPX8eV&6ShcgFQ zF$+hUh&)}1D{z5peJ%s2p>BCH0zExZ(G6IF2ud%+CvtZwH4YaYxY9_;`D6{zA;WjP z%W(?O$((R~+7^)AN?i}0QA&iEr9^wJbg0{;ZyDRO24kHy^D{5&4#tMovk8{U)7Kpn zW$sd*$$Lr3Kqqrr)v%iK_Y9pWD|oqRGWFrz%UnvZ^`I#9dR}--J+gdXtDToygUF0* z)Y@%EWu6ve{u7LefX>B5#)rH1G{Sb{%21C4G}w!-n!#8NZGuYJ;Q|S2r+vj%8DSc~ zo#9=?x4`As9L|*L`25|ieQOt66bCh1QDUzeOx`;_!0~6P+_&DzSw_o-jDo(BXoh{} zv-F<2*eJ#gFLbOUXD1B}N@ur{U1X>f@n-g@*3f~FnZdk%-ij-0G9YhQxb-rZ$h^>t zR>*(kL=O*|8KW|6u=?-;*|xGcA(uMEI{#0HD^<#vF3n4XKgfeQ8!Z}<4>%;FS`Mpt!-^sg+j(Lm^8KkJn<$q^mwnAt)C;rKjYG)2y2|{GJc0Np zC$#M=^|}$Y&}M1#H%=IVrYRwa;Ht95MhQVdf5xr%ct)3_r9j^0*p8$ zw(rMFv4bUs_QYqy@W*U`f{fAlQ0tE2UD4Kidx{9=tL^M0asSiOEm3m8{nb z_tAGbz*|_pKkwl6V%3xLTq1jCkcam9V&^omG`giuk3A6EY0g_>&%Am0A?B0T+YK@q zm2W(Z1x^S$0T$X@169bq$hI4gGEc*>kSFET7&~AI?*U@)+bX;08Ilyrq0F$r7)Cv9 zPB7`A_IO^0|HIc7=+4eEG)kMXJ=8+aF!$1Y^PY!Ozfzl;eq$Md)Pq>Ez7CjXY*e`5 zA6R@OYRIfjo?+)}vOhbs+#wXf6>fBbLe9IZVyi=n=&}-bA0a+k?t3Zz!8!*9w}T}I z%vYG&tW9sw^Q33KEG;ed4~o&D5DNgJ7X!7qD5ug&u{w`WJauAW&2>y=H$cLrtP2)X z#bF%&hPK-PI}1nGJKWmfwN~JxdE`jses(mS#s&uC^cHTKL(d}tVKiRWYh{f6EvZd2b3c>20r(Lg{YJN2q}i5QO{$Gf(z0 z)z;mHFXZe!9dUT`NLSKW)1^+3DDiRsl^}xxzoe_IQ@+gmzOTVi+Jy%^1A+o1_bG4; z-&xpmEe}hi6&wRjw}D0U9Y}G>f^9Sl;&#CNLqi>_vb6sjL>A{%G7$=)DooYmsc5Xz zf_55Jzn#Mtlg)DK>^s{=BEO*B5a&b-^i_Kr>!hCWeYEj9f-kYq5xeP9X60@4rEP~e zdfTdzcz(bg46viD?9_YX`Ld<^4|SuRnO1Nh_#}v#O(J+S5{w%MWPu^6VL?eg!gBh_ zLpMg19BOWjyMD0fjVci%>qYzLC6dxCEDM;N)0oR<4Ko&oB+EE3pZ;>X8*iZ9H2QeB zc}*DY8C$p!q96r)IiPaId~g_Oddi@03-xxI?Vk8ysa>yewBGWpXo#zZXAp+YpXW6f^NnJ`Pi?I^?jO62kWX~`-4T)#t5f()Fsj0S)k{Z z*LPQUFDOV)zXxITy%Y7h(@;E8oIZM}2#$rG_mnQ+uJ{?(ISvho1NwnS+aB#;bf(k6 zPvR)Kpi(@0Y-FjUYMlK1b*XY#BvH|6IC`JrU2}y(IbJ~H$xTRb* zHb_ZrPexTnOxf>3b}lZinwpwh?=+b%MM~qPcEd8l4+OXdV;JN9#tCJ=6wCzOzZsQc z2)Z;Hj}dmtU*Mr!1nNG^B(g&Zt-IJgZSaa5KbTn%h+n+eR0xM1o`p#49=7M=cBb6B zgsh1BcXcH`Sx4o$01gM>{OApimz8#J(xJI%t#%rAH$o0mw+EFxeO0`vuc^FeAxu%kGFYKU={FO~ zpk5A8U0y2A5JFb1^AvpfK%nh;o>D^%ZQ*E?^ruXI5(i;S=>1S4W;8~q`W{LQZ5-_j zdk!=~D>8La#CNq9z*op?h!~6a+rM$n540j>=VL2$>Qt+IaDUGez&j{HZS2y>Wx^X- zpw^PGw@aJoD)>XNuvx}+zmx$OEz*QP)knMfN|mjMv~)I130@@I9(zbhd))>Y4AD$R@ zoZvwLMsoz47OydauLK6< zIU#c1A9y?*M6>MykMa9V1brd-Yvc}VdSyj{n(fG<(mV#%#IQ1+0do{84G64V5;9UM zOhbbVO_4mU`M?p}(92idg!e0MiV)MK#@;{4OiZi;=vV*?x*)s??H4VSe@yXx;m>s| zeF^DJQ=hn$anm4cVz*2|)t@}P_32hs6ue4BZyMSEj1orm3*vGnu>}yWg?zkMTRHB>7l=U`|Nni zn2{flI%C4|t~VoE3;?8{xME1s*#rGYuP)<>?->f@>y-Yd$p5J3xrhO(1%hX|2YrAK z{wJhhjD8RgOZZBy8S`)8FaZY#MEzrmr8K`k`9AK<6NU$b{svyDccOJ#kN`pxMDH6iZZl-z8j+TY!>37f#0hZF;Z5L!v7eu>6&-^FA0oHky1%ILqeZ%TO z3~UFe5$!~0`?A=;hF^H;nDze^mUs`9pRP6^bUR!?(&-*gQh_4Y4+{OJN`Efa8IJrF z*iq_Z?}p@|@x|x}H1Y0c^zBX1msQY@CDUiR=N1)(>_T3?R7A%vPHK;(k@Z#VsMrSD zq0&cD09NW%MZDpUkD{un{Ke;T)4z$)_CSGpVY;)z0iVbR+c}_nbr?QV6q%^EexAP9 zWJ^Y=$ggSH3l+OQ(mlhOgR`*9VmW+M}Q zT5;}^M}81_10(`i!8V;-Bt~R=c>tj60P(}^L#$wYQ2rpZ7tzcmDHj+nk(JGDm-$2I zAy7gM*ivrzJt^21&Gw|7I`cHNd4t3xNnLf*J3bKNM0K>b^{-zQdU41u_*5oLm;>=L`5LZ z#Kr|;=f_pow#Hsyurc&!OReF^>4Nc>N}Eq|CvXNN<=Bq8_-GtKCHd3ZS14q%@VhdI z*NI%V1}}yO>ik|#m+5P32xC=Cj39NHjo;g49_11MGZmf>U_|ptN};HIKoZyvTo;O|GV)7%5g=vb zqU5m@>wIjSN);Y4e&Q#`BnQ>R5!}hEVB_rjG(;;3uVS7!ob-jkaK6z_GFg!{h7+Ax z&vgs^orbiG&Q>Lb#3xG}L*~PmK#Y4*EOldf5XY2`9P!e?a9p+^(<}-udut0b*v*Z4Tv!#2+Q>TS`?umuyL;D>ZGK;p&=%0=nEr&g6gI z1y7tZLk|Dy!mQJ%Fxwu_*09a&egn`Fwr|@I+Mv@{RU1 zne7P^kcVKS1K&@AJg^q)+#&P|DJJM>Lm)YGZS{_ceDJIHtYvTcOH>;1pY!y>gZ`$G ztSk{H0wHx=N>n9;Yml2cL#1T|p(yg|$#I#Irn@4K>^5z3ew~#^p1VLyh`-mMk=43g zAZpa}i<8>3GNI0I#|ZaT1fuElk>+&Q(9X;Jj=>gW=KXq~QOjw3^*KZDuK+NVYhu^X zh~--x`Kgi)ih}KKx8h@SUJY&XTs)>oF6?tyJCIVnYg*Tw#-UM{dhU%dg=`E z_vTnoL#Yfy)g2l7ZR!F1m%TWQ!gHw{MCkScWsNw((5aWrTI!W#dXrG4?nhULS1)Ju;Uc($=+b(rg?2IuAP zH`DW`XZ3rIWbabV__fiZ?2*=);!$~7nV0nuvS<+V%be=5{93IVv=&~gOzRVP{L)Mg z;NDaU_=jsgAogr=cyop;K>x?hK%;8U0|g4ck1twZDukJjVI@CT(SBi1o^RW*bX>S4 zN}5OdY6h<)RV4x-yM6D_C)@}P|5}Ycgo>vWDFQb2CGd^3Xgd;xP8TGO0_ab|5yo44h=H+z@ zh%<56FK}z+q^0SmuKgsqT6A7r?I@552?&~68XJ2s4+!UIpU%$GE2ksoEEqp1{U>rD zrCjmY$v`J$U1}3~VmtKF=km6seq_#bhi|=zo-FKUC8H||(`)TgRG6!_8qizH6ne$_ zHVO04nCzGO@va`q(m%mL%d}nbz8(!FV2r9EO_JTiD;?rr)|?CPntY(QJ8XB<(>H@` z3Yy+qVUC_ur89QL3V}F-g1t$-o?b6Rvd)Wp6;~iJ;?s zE%-mnlt$g22UA|#9zgNkwNo&>x{Loh@{ysrbuSKmk#;hE1T z>{#k6?bp=A_zC?ia2{HWPPG{-k>j1a2bwDN|DOYA<&Rh}uylEttdpY2SlfMPLFeGe)F0!Bz2{~^4~+fom@NnkE2A6ib+ap`1T{8eX;)vAkVYPw_W^8a4E*MJ29}{TjktFf$x2_?*eB@ zZ4s&TJH{(!-j28ds5TS6=%bvM$VAyoh`(=Dz)|zUkxLeHwU$Mr7B$jyZXKVyLw&^X zu~gyBHkw7Uyp2>Yq!@5?+CS+ionXm^yt*jTp&h1lyQNm}iS3|me5Sxw7nK!`zEq%A^be2(@9RPhMMWrQ zh>=tiN8+plm7t)EOPh(nbX&kF*&(Bwkwx=*=(1(~8)NQIeGqzcSBP}CPQ@WY+Bw2k z;7*tjlKXD@<`XT_-<$S+)K4K|y-%S)sgms@iba6L9fvD`0~AHejv9=V^1a1?b3Ivw zBq3FUW|=>iA~pK>Q>bZo$Ha8cAyd_a(=p4OBfiiz0%*c6Dw>JR!T&}i>r#O4kw~V2 zKOK}}G@{Ly%l*8^Slliyti@R@k&DR@!@NIm4&;KCE3F{3Y-V7l4tO3XQ>NbKE33M- zX3ElOvb3XMsG7QNy}V2uzIJ@w4>q|S{x@0S|3pmpN+fd0EckIVt08{iR|m!JJR5~S zsoexE|3PO=q$xjS-G$fHz1Zqqbs}cOfBrp>z)y?imU8VHcaERhoLkX`OPlELWQ^fp z%h6vNrmbSANUHJ~r$LU&X@8Nn2YgT$CG(;z4&jlX5B!b9@;CvD18#aH_29$n239WE z)DdVV>h}a>*U$hqrY8D8;KzwIO0k`D+cXI*h2sB{a zICvU#0PFap5AQn?1;l(K&)^$x|Mjb5@a{F=kz@$|+JyKD#|JEK9&)h+Cr#XE!`7GQxD84Ft{Rx!7YOJpk+z#nm_C*g7`l_% zOVA}f!nNq@DC76Q8nF_3`>(`$2HO3@88F4hs{g!I6iOaVu^)C(_`slIA61cLdTqEG zKPr{#FEM~3XMWY-FQ8Yvg`~m9=qh_=trz^&4txA6T0o)u-`5CvD<&8RvH~mbZ69B$ zwEjAhts~X@1K|p?rSTR02=r8~giZC#E1)3B-HMX?V>~{8e~fcl{W|9LoViomi_q3E zdaFBKHFFNFBZ^3{6#gB37pMqkEFNG>W6Av$MG=08i|2zM2Ir6u52ipl26L@0I|s&= ziOhN34V;wCy|wWxc@&Up22Yq~L+DXiP;SGN8!-7{jq-oLS&UwaQdu7Ar0Oe=3QmNY zH6ucK*QS`Lb@886O5`!rcqETdB#|gMXe|q_WpB{^dzf*2qaNplq`}QxxhN`vlO(mEhj2D#MLq94ToE|c3Cja4&srVGUO--?{j)h#Ld0Ij zfY_;gXUhw?`8Ekq1!_)a)aKk-KWT<$5RUOMAWV0NSSD_VO#-JXpcdj`$+bC$fEb~A zAR`yM3a*_AoVV_fY*68!xaY(E-mqfGvVRRzt1l% zO+5P9(7uv{fQ-eGr6!v&Ko5wkNP~?A9CTsc z1zbR4B)Sp5OXo|%b-2r15U5WO@_zkDhxJ`G$|o5fje6ozBAOT?;1hAXD!ddoNV3@` z94YM#^}%HqdnK4^Iy^T~>gtPd>(<>dDbvU>OHAL~GBM%?7DjCmW7Bypuo9nwCQ)>= zUcn@_QIzUNi^$u@5*BvzJ*Xv$b7pDSA$t6`IC20LrHytT4_-Soc6Zt@6Zx=wrckmi zg#9*HTGpmx?#1L}gTOS_GKXgrr5zwk29Y)D8xrXH>Ry!b;kXzR_h5GXevE0s4E0vb z&!MvNts&a9qy8=+ATxLnR(4aZBh;YFv8i7s)AVR9TY+*YEnOG6Vp9?6qpwPVa{n_{ z5%(ZuW}C&y`lsPZks~CqTFf}cVa1CVlGP0+4lJ7@qGsf-y$=EM-DeP}uZDk%ps*(? zDRo?n{3zbO?kh=iODvystZ#db3{`{9YpE(%P;#;%cj}NaM}mZFW|d;nJ7aK%d4`Ru z&NfGzgqBuokR*3AF^_u)Hp<%(YGQ2Sx!0h*_LXTHebb_xqX!-2zUo+44B70xJ9Aar zT6JrYb8fON!XUC#zUx~Z(lFA(#G36E1n$#!WxT|RJ8FDE0bgjIECOF%1bn%rYn^Fn>bn5PmT}h6P0_}83; zt#XHS6KG#?PyZO>bQq;%VpK7h8{qNjMWUIw1V*)8(y3$cN+B4zSR_C{)gF zDbW|(J8ZyaFZ)y#J+tSjT9yq&*!-q z`S~mpWdM?x$6qG509=`rTi9|MVCeIV->v0(`FO0{73Q zq}jAbcI0GlzXZpvRhX(9jNg=`PR2HkC~SO+HkUNk7tdeFTMl0{bW#XUJHWE#ZK<|Vi3GhlcA>=Q z_%SaZqFG}zSIKQT#4f3UX8e==kawHCJh`|JSR1Y8FvG+@1muJiLNgzC*y~2hvL_oO z0(yJgPl4|QS!m;#859x8cewY%5DT9~uc;f%`@J5-%;NOR6UGYK-My)vrzT*sT-vy4R>zMyt+cb_|OKS|6wuWKbSYQm=Kr=tGQ-8mdDOgsCS1Ao9G6m=Zdz5{lPWb@}!}Bg~mSv=u$F`lhvg z9^2@VIT_2b$a}fMTiU!3i&uxZ$SnuI_JvZ_IM6gHA>tjq9kNG6`|h+NX&AdHHKawD z9)3=9T55fG5jWLJkKZ0gjvfqcFpuY<#tQX!3d4^rLo1U=pe+GXia5i^BQx7xNF0ZP zc$v&<>IDI3qrjbS&SRgUjQkLl;>pt2^#Wv0Fj=l`-ggx8v9?PuV!}FJ!W)09$=L4- z^^c+_z)yEokqD$7nhyl@Q43MqG}mbgAW<2FMFp$b^$qFKR1iaupuR0~NU&g==jkR9APoM;OHxnYgx{F0KPL%*2nO57IN{W9VDd0{ZsKFLy(% zT)3tqyrrab@=RS)%4T?i4k)8>h=A=+fcDZeb)^d{1pC9D0M1*Kha`HBpE&Bh!;#}c z^z`xk*@)$G@jq}&f(PAw`T|WeZpEmt{bUtoOWz}3uF3+UqB8}?&~;Le&we%YET1OQ zc8FDP*BD3rF(HpSdY-F)2!y8L<`qVlsvan|`yd$(`{yEQri@K$*EQV9kpC!g`6=IH zLHNlL5XaaGrO$lFYyU*8WqUix;_4&PT|Ftkdu4HzeBQl^_4V~N5b@63oS(}-ra%9> zB09wWyBpIzmI=d5e0K)VHdw^+k~Ti9*sPjU!?Uh|4Y$^x>hb$5PVKO#vMP<+PYZ(} z2@~qN%N@wJAk3#TjEOjnFOG6szBcW2eW;JxGcLyEnCr`+wSI7!^iMjbzcOMBKNZ>=}W>Sx(%7^@=XUu|s zjLwYc`e-zcpjT-g$S|Pql+Q0W-zA%Q)0 zA}r7=ygSaNz>Yl`yFl@=9%>wjdn@kaPf>++IV=x11#L=%Y+6iqbw&nI+-YLUlVF&$ zSw@EwZsq4s0~5c;_ezslPp`FG?L1uTS~={t-i3o*z&_yn%Bq>~Ge@9}eJLtCDmt|3 zMs3d(VuRmXyE|~Yjpr({nS*bNMQ{jgA8E#%?Ovk%?XLN%1OslA+rG1BSA^0~(Lym+ zqt0JPjxy!mWur@F4*%=+17SW3;0p=b&%W&XlM?~Fx&Om+-=0DHrMP&^kDoz{=h;f} ze|fSQh`#27Nrww{=mhU;tsMVLn&frQ0{T?ph%5ehR4fT|JGyr19Z|(1-6i_kApgU+ z1LHU8F%$&=$!u4N3o~C_7qg~B0|?>=-3CUDf^>lOC|_EBE_Db9;&g}M_eN2CbO7q) z>Rc$n=B=n|Z4WAYKuHx3l+dOshrg#Dmp8!ENjoh`kSa%sD*zlz$zYOZmqGHeisz3O zUTXfCF&DAbnh(fWWY1W}ibeI_N!YU*UaT0<_l3S5Iqs_4RHt>kf}QIuVj@pDpN+$@prxbXhN6 zl)1s{4U_1KPE-BJR8G04xVf_{XKK1$2Xo}#1+)@DoHCf*J9G530zjCsil#D6n@^kl zntSW#^*8ranCj|v&5{_Te^|MCA5?%^TuAyW}bDN{YAGvizFt#)35NB$M>Z0IY7@VSvZ z2wQ94MYVC1JlIu@Jee-LXBaqYMhU4#!DSUHVY#CN;h3B$e)C;`R>uASp)NG9@CSoX=lRhf7CR(p~H)e9(cC#|9s&1FL5tUVQT<&VD@9ql){wH4}HEN8nIC z@%@%qY@byis$ValEwj(e{|*4sI7#zFKfz)&BH6HiHlJ8EWpaufv%b?$VvB%U6@bz( zOb!#cN=%ZZ>p|?L#KrDpPU5jr$2JY!E-51C#QSnifGZj?y?~;q01}&KD>FjH2!}fMw6* zQQ`+YN7@sm@lU(`IW_)cD&hUNM7_h)~iJRX7utBQd@aMlcaRe7Xt20Yf;&AA*UE zY`4T5>7uzJ^{-YlN1sd8!xlJA#O)KQ8!F1UkBcm?LGDh8;CZbdH-PV1qxrL+_sgTE zqi*PA+3D9sD+}i^N!a+c6fy*Rz@gRgC3_)?KMnV4d-yCyD7p1ADIXr~`pqfXReP*` zG^+u@hb#VqzBe2{NqtNlcMQS**#)TRxx@kHZZ1C6QyKpJaKO$Sq6!MV-u3+R4XZ#*gW= z8br>&x7utZ6SR=}>h|BE1c4ed&}!{39qeBr=~Ve^ zao-EW=^5~|4C}t#FMM1KV?IL>e{~@)E^R+gmuNrieo~r(Ji8f7fgPbh17e~|(tDE7 zdcvT&^^^;6jwmwm?QSI|-?=Cf*pnOU!0wqJx&oa`E!MFCOV?8z(x4Mi+pz0-j?aJ4GZ7EiL)73T zb*nX6Ce4>xjM#RUh(#!Mm^UHIhu)4I&IB+i9~8AvMG?_&$X0Ta_Zbz`ZRl-FbzHT> zzcO0V@z=uWrZLjwwu=dRIra`#!^BurPq;JLuMb4-bs&96Q zt5YB?Vs-7+M{3wATO>pu!f}#cmTHJ2W9`Qfe?r7vp1w@zw#olcCZz52#csr6Fk^$8 zm5=MvtQ~6~N*)!wva)ip@WgMiARv#Te|vkc78KTZ9-MZ~_T*+jGuEJKe;(c8x+_QW zUBfX$rpSVo{kzLMzV80-!>8qMc11wW${UWweKRX6b*0;{W9KEO7&Wr)4yWh|#j8Y@ zT!st2SKOGmRN7=ad9oMA=Y!e^UMHh@KVwUoSd=FCWWN`THk;gTa8%w{?xqd`=hut) zYf^AsplC_TX}aEd@u}Z2(Wg^DWC0~)w+QZ3Pu|)7Z>4*+GlO`{W__;av)X2SDYZk2 zueK&S)$E021_;ohSw1UZWk<@O9ZyRVVb_<5XytQ{(Wpox0dpPaUX*KqAx=OioaCnw zi9jgQB8-bkn?9ZEKeP8bv*M=~jyz@@)fa8<0cn zQ>-aU-ApXziUTbXff`%_yuYwId;^yl0adTY=z0El4KXM{`)foe}fIatnSSty&b|T70>J#_{ z>yjtDg4derJg#ZO1s$Wt0+Io13I)(f`|qhEJriYR6ET%&w1_C@JaCm$spE92rajzj z{9N}HO##k7si0_xZ?}{@t`|IfaGIFSl&5vS)aLFg9dna!1^U>nkn{F=4fUIDX6tM$@Mn>R8}CaMd(;tM-9X6fR17z0g~AtXi8 zZt%WJs!q%1p~wA9z9T)o8l6zcFxBt5JF3Cg+p#bE;iBdwNCK~QdFvW|8PFoi3D#uF zSbwMxgd2%l=KqYQWM~Gfj7?rB>ufJI3AETnBH0kM8;eMS3-%p*YxRak@Um}&5LVG3 za?S9XwPP3U(@hyxLk~wpBT%u0iqp8c&_wfG7+gn|>fJxb=*Z^zg$K3M_H?JDBFFdz z-n_wiA6g1eK+){BVT-wg>$#ro8mYSJ3D}Yr+AkUX^l8{zGL5Z0R&^}fj%JJ_ey68_ z+p@nR7}G(gL4>Bt3Nk`hJ#^N3XWM*Y3e3}Lh_oG>7NaJ{llRf5Z ztl{ktg%4S}l7sx~?^_Y}X;fkiZy z>}GY?<_85pC}}AtSweaWd!GQ&t_&okjoev3Dwc1*gYbnF(Aa=766b+r(50K&7@e!h z7?zJmaGlea8_|YZz290f0KC3Gc+{-re(PdzmD^p6QOw#_;yi$+ZKjfK6gIFu(d zRAhT7I(B~0Pg*QHF~r1&VMqG8sCf44Wo3hB-ENMG%xItD_&DA+0e9^T${8TOab(zX zeYtmJ(+$YmGyx?~hl}yHxHkGl4}oSOua5M_ggtrw&76;PC&6Y8iIOrY;nyhOmDJqR z@MSGc)a;<8bq%6;GPMcDQPe%?pUaK5h3c855;R*H@|W>L>MjLD?|&|!8WOlDzU<-f zPGOu-p4M_H35bfUMbw1+Pb%XIYcdo9C~`&_dMKSL%9UR-kSg0;*4MVcXBeM2lMn-< z9+mR>dGdB-;z}?b)%4%PFFr3L&O#Gn`7|4!qnHoLex2+vk8pji;PzOX z{0}-IMDq>MP6@`OK*xjxGn|t+wpIa6I4OK?UI>*C>7V;Q#1M4*G6!_VwGDw{8BznH zv?6l&-!eKCEGNT;3Adz!H3Ia27rx#5*wd5rnB}UHk^WBr22_kZ_n~AOJcQE)%G-c& zkxn@kF&eo*I(`l391uhC6)^!s7|G%x#hP`Xuc&OK2JrDM9tJv3s+AP+?K0tv!pJ1y zd!xu^7nw)}oXx&|_A2|3FQKDEuX$ghdLPg>p)hM#n*nso$jHck{6&R{>gww2vpuet z+e0BIlhfetJdnw3P(M%v{ZZ8P%T{uR*Twv6df23@C>if2(X;hdpn(x*u>ztc>);>S zOELDY`wz(4n;QmwhyndIZj>IRt?h>T1@;PiX?4*fqnCagZsNBDCT|#w2vR<2_oV}> z5Kw}B8^43Pr9iN&83x6(n>a<*@2|a|dC8=|93cjnwy^3_!oQ0Y0|?ZePx`dm5j-l% zM@G31m7r($l7djA-Whxv48Pr%!iE-!tTKMIU~?+6d|_4{AZeGze({IH7B}jzfwM3$C+A`{2gYy%ETvSa1*c zO+oJgze<$R*9QO`xRl6sfI(>jPzE(z?5cjSNecx{0|GktSg@SW{TyijN-$B{DCF^M zn1+~v>Sq`#s1PLt(ETLG;#9hGhiMY>K|os86Tj#P>Il%QCCwheY7dD_E)5sghwJ_z z=q}WV6Q-x5d2Vt~zVhQ!V3xDsd)K*-13g*aR4)VoEwCIa$L%kVL_8D&uU5Cfe=xKR zK!mm-Ut;G=@0Bjb(MMf}+dcDS!@KDU@54bu^33E{7wx|OD$km*L~jX23-tbM`nNdP zD0y{1qJOYkAR3{c|H|M6J*M9W5*?o3R&#EDU;(t^pMA0KIR@ANLF<6}oVSFDeX8`d zP;AA_B*(P17k_sAJ=9(7|3HmV?qQ*cB6Qyc)Am0gI8dud34!}DiOK>LLhF|@{apuu z5={r+UVvC=GfqJZ?evzGVg3(I^?posN=jbxVET3!a1sa*dCKn9{x$1=_ym-QV+vdH zuy-2A*#AQFz=3{^7z5TeDz=%S5Gn}sfS&O?{~?3kuh{M;3d+~4!XFj!2=O=D;j^4S zJULpU*}n@`O8vGt_YYa|Abxu3(+)eM*Utnx!rnKH8)-+93qKWGdMAG!4}lN(Y8{k} z_Rw9jw6P`d--HHZ)Z1F9K<1s|i$q;rk-0}M`c;XBnk3gOeSIw^)dJHF<^2>%iwmhW ze<;hpev1v!Nf8O4@DgZz#t)SwGGa_7Ef}hD|ABTbrP?jEPvYRxg0dAp9`dsYzE6EY6;(O;0iEL?}TG-|+R zGBjn1wRUcV|5?-UKLZcY04cmU<>Jy>j~wTt%A3^Tx`*x9XskZIPk-V4{1*W!a)l^K z#ab;abZecLKt2M*?H;JY5gG}#92xy>qwl8!&{)@keR)uV=Z!$A6PfV!k+R6|oNEqY zMR_Z!h|RZ06ix)IL4C|9+QE=o55O@Lps?mHF!VuSeB%VcjBzFP4b75VDv zsr39y@Hu|6)|{EX9m>MYlNVlp*XS2lfC|3$kp7rK!MjpdO+o>w(7Gv|xm6>M+Q-Bh4{Mn-W&w0o1$DD&@01o(cKaGuf55BzOhPBi2&)R;CQ0L`$4 z)%-6{4|unG2@A;tG|e74^Q#BN@J#r0&dBw_ntFeTpufom)N};Sw9?+D3bH49-(oaucMeTjf7HU+@@XtG8 zzk7DDbGh_#)X?_QR?HKh&5M>Fx_Z#$<$ZVh-4^DZL2Y!n*Z?%;Fa;{shewX$s6lnU zPbzERxLX0AXzZIA6i~v@ma8WHJE9nk8e-nQ19EX^;mi@O5QEx}lt~{@4>LJ)1P_be#r~u7m=a$&J3OV41vLM;D z+ymfWRL_k+UH?PV${>TgWJZB3`E1p3r z#roGw2Z*&>e*c0M1Cql~j>M>a1y#tAsi4KnA}s&vwpY=xpXJ*=W9W0B`>{OUGIHYu zIox;smeA32pD@`cs}^lwj-0QpRniKP5}HAO&j(86+kV&>SehamQW*$USPS|hv)~Y^L`gPnTehjbuzdTs>Ly$Ly z>H?{TyW4zI`73QM!`D`Ja#b-%3AyRpmzyy{Hl-+38~1hge|EHW&|AY#h3|J%D+8u< zM!GX~idrHBxUF?QMqFEf2d)RpRz~2+e{B9U;Lz!mZGR1+*pyPjOO;Q@OGv z1jDv>LN{fLcR!FM8ym10-W=IGYDfDJ(nb)Q`uOD4U*IG}=2MQo3#Ld)EP?IuKYgLe zxSB8U@h!)20d~nhOO6fiZSI0okc?LY5m>UeYUf~q8jZujrg7)4o%%wY9E67Y5Fz)%EFZm?%VlMHV{!fX@taJ~lSsCOUjINn zO}B4{KSmk~B+aL%r!OvvCB+^;D$dpUd_9k>g__>reGxBPANOnp5Ef*DAAN?S^Kx>& zU-BPc5ICl&aH1flNSb2v0%|;@>Bn#f+gPXV+5%mr=8xY*>cbm8C=i{-w|s+gPJGXhHfVh4g#J2Dv#TSALo23CG0{U7bFJF8?qeS{BXccOn2Q1 zIF&{aqi}r`Wx)5Q*-oKyyE%W4J3#CC-%|v*G)bVFjV`qkJf+q8_k4J~d|3Ttw=>9f zCxLJj^Jh((%hX46K3C>vgs;LK#!iB;4I& zbEjkUH<#tiI=t31S-ChSD4+P$<2l#EOQp5S#~Q*$;lpayO@BU`&@+=OX3^4cf>bK} zHGX0?)T@`@R}p+Z8KRh|=9@MCd>H)!XqB2UVY8R{=6IVC`o5y=CVcl!_r!{)GP&dF z!HN$1>#kr|7T-a%0{g}dD&df<{j_2TmyEfGOtz!G^YpM$2DnYy*5ych9EAgMJ-i62 zB=WF2o*@>lP3Wxn&DnD(paOKogf&byDp&wktz`ZCF272uMaLLLzpNdOJ}@!=t`QFL zJz0=_rP<$HSSaE#%$QLAeaDY)HnE-j;i@WAzqTy$Xx8OcAw}aOO#O@YH1xaT>T)K$ z-py@QgOQIl3gjbi#L%8?Jvp$({ld7(yy{jY1*jLmowmFcD8h9e!*l)Mdd{6PEBTUz z8vOD2KP*Q%4n)sz&07ouh`&K?tmR0S=+ACTa#b7*TU%RI(OVbyLg%pEcy1np?|a2q zo3|h9x-A1^P#iTr*V#UIaB!%$*(3zfqsu3C{XLXch(LSEPlO;xk-X?DF3y_Or)??P z^JWzajFrNs1C!rYW$`z>udAniY-Oq3F3;YXej)7QrOAK&A45UpYlV%S2Vx?Thsr$k z%w?QR^7s;2Jd}|?@#V*=?chst;m8S}yVLSiQ9U7krIrO;gYy5NqwA7lfl8rXfUuEj z1c<8zogE~1+;1NVPDx4mMyY2(R}=uZvUe+RK<_ihI7#7s(#>w~dl9B-S^qGPIsUI9 z{C`|s1yEG)*9HVZ5NYX<5Lim-WF(}Yx>G%S;@P6$@kyiu&F;*`sJG7|R9pz9Y?gM9v$Xk}{vg zMq|QKMKqgA;G~`S_y2%#)l*0A&3$Tu-j>lzEv4(J{GK;MQ;ZDe_IQ~k`isr($QUFh zX7a^T{1U|;{Df?5tS%?J^R?ECPN;r#?cQ5bh*4sHtb$K#m{s%kZsVTw+c^Vt|Gn~- z?;Ygu+wU?2zHYWmC|q&9pZEmS?@fH-jZXTt1>vYDa0LzPWE4<|D%Ls~Jen`?>|&0p z3P>C3$bS!|KB5I8U233%QXKg~7&X)hn89`Dd;JzrFg0R0`;+pDC?~!Uj9TTqZg`$Q z&-C+eMBl?<(&{Vz=w@M5JC7T-40&h-XFLjmh9BFQ(xeisR zng`anhmvzjVLdph=~3+iI_(Gl-nN`d9~*QfuL=T+3W`9@5EYfT#~UMb{cG!53{$-7 z&^wY!I<&WU(`)|RLKzOeKBR$Ag{J08mR%7r?Z0PIWSK7e_eK?uc7EV1wc3#`EGpto ztoYxDiw1+w&cnqw!<@owv1=d%KyNF6DLHXQf%^(mTKIHgp2-7MX>fLfQ?^WiYU1J+<0;{P{-4blK*6-B2*7>AAiK}? z;=WoZ<0##5sa^|^TxZm4b^{<1tQFh==6?Jx5W7muAoH^ds;`u)zCbs@ipqI~+vmF8 z^ML#zqrZMBF#P8+{OrM)LBVKhD7z_Y7p2pYsklDi3^?-Vn0!U9`h0!D@;@_nF7pO$ZDR)VVo>_mzXJCEARj`MVXH0990V)hJ>?Gy z;M!i_YvJN=Qb0e=&5zm^^yknB)OBFgieg4vHi-}_2*G;ejXkbm9n#liW?VOE z{Cm#wSP@lU|K}Au0^oKIOq-Nu$W2Tiv2#_kb7^_Ug41aP>+eS5x*YqL7D>=g;dmQ| zKFf<#6+ru+Hv%E9JWi+d>05jk3@gYfcs@Lekd}{FW&G65K+bB@-~Q1UE|g#mVs6$^ z@$dG$p`y4qg08xX!498g_O7KT|64VL2Fvu#iSS`MWdFO~hKdx#>#vpob_5d{!K47Y zDUcCr(*@~NdXE3c?YeWssh%uhuyfT`u?NM2BzN|1wk4NrwSGqrz~@I4OZX5}GPsV{ zx8FQ2N{^O0nSRkq{oI&V9QfHh>8sKk*5B~PFBvGOgv*xR)j`rT(X#)47(qGZQti@5 z4v!UR-6w$8O#NW_>R-V@LF|yZH4##L1tA?!M&i=~5V>xh!GASSAr+hhpa?_^p;|U~ zBy-_yN-sypw%iBs{!vrJ6mXAWaI?V7QxN=~o}Ap^NR~vFGJdc`Jp=93w-lID_ zy#%BCUr{pxSmVSpwg_^`YGdbWtvzqf*H3rniKW^uBHQ}D*>{s zl0&~A9&AQM@v^{QiKeUY@01xI^Suj>m~*2_v}6q+EY(V?>FJ?$Ye|z%Gz}QeKN&Uk zmw+;_@`?Jr>~(1@dY@GcrA(;L`Jw)5=l3RfSBbZVJ=yX=RS8>EM#4PaTl8%6c`(F( zBx`AD8SKCEsK0b3*w+gW&kCec^69Q%pP-0dM;|D4!yMEUTueM@vsVY8#mUt6J`rAN zXLfHG{X8Svx*GFR`Uyefhsu9mpbD@LefyVeOQmn%1BYQwDlHz@06AHO+xa2TdDTrv z<*Cm622^DT9qfxRLNktARV#_emP!Fv#_QGMofKt{ff7v-+pmKSuN}yE!(j2{afQhtRJ}6i{nC-6+0_7SH5nGjg zn}yEw!O*7X(7)P7slca0n%2-2iBVh8AWc~t4P(n9qgiXxNoTy0Vtfn605anFAHzFB z0XJFj&>-tw%YhJFd|KLKmPDAz)-wNZ>ATapc9Mv#yfkDNQ7n+%aLX0(e+~2S7RuQ@ zcr;S0ip(=Zn5NFy8@%)vN|#r&P@IzFzk9?>k8l$k`#A7rMYuY>?S$0GlJQ>w0hWjs z;bskB5&&un*Nk7PCjS=F|KNsi;MT?I;o}(5bNNcG=m;tY5V+rG~ixvk`vFrL74{SFf}^;|6OOG z3(Ff|SinK2m2cIIMuRtDnH-_L#h<$$!KwPS*|mk6pfyLcltDe#Dx>O z5<96Lqwg$}iuCMeVI}gn*nw$Zw5CNQY!2;TUU`@}QEGYq_#XuYA()I|$sb7LXQov) zUPASaJ)Q(w0vV`Q{f;MD+hy1OwY7XxGB@tXE6tPhex=yZB+sV+t6paF7!xwL>P%q z#$zWazJ*>cN}zZhl$ zE3*L8$A4pPI=*}nCGaU51k>TD`{rY3vyTF0k@9GnHn*LlD7KRkM!ib-PV?DwkAkJ| zTs1YIy^=Di2%21-68hAzl999lpu2%cJCypAzA2KV#TxF@-0ORe5EGH~m|_OyT!=sp`YMQ?@m2$_r@Lq8(1p+M`u1ij_WhVr?Tx zd-&w~lzwJp6xrob6v)W<^ap_Sro#ej`tm6kgjt{KIX?J+N5FBma(=X4>e9q0EoIC( zowo%{9N7ju1`;Df1`*-82d-By3)y+PraItciGJq&N>>5G*-L3yK5&$2g(Cz7=sTXd% z7Y)R&&&iCNQ04hV&u!s-Jn>Z($<#E*jG?S;>cxnie(pPJ^w{Hj-(z2LXMad6Li!zG zg_EHW%+W73qk4QfR~ae5&4!?Wn%_OuP`Iczq%~m649+w|XX-umD-o34xG@)5*E3Y9 z+`r}EqFP@22XTA;gd%fdb2%?%+?gljL2Fh2!z67I?l*7`BlIJEkg-|W*eWVmf$G@% z@^euNyEAzTr79OE0`_1}|Dvt=LdV(aP`p1_GZ4C^S+8oPVde2!D~6>FrL}x3M^*td zsXj?S0V7@jl&t7`ufH_1wDl8uE1?rSgG=RE0-wBcfb0L|V{iDQ48&LNr@HPsTdzNu z=UyB;J0{PK(EA>saC==-M8(;pQu{!10$}8Ml(hEx_3QUJeY>t{7#H&ov$KY)f$-#x z^_J=b@vr0q;2E&3G1gt~HZIQFXs>i)f;`!fCfPZZJ8W@9xY2$uFeJ814Q+=uOfzogK<$>U+4k044pQVYdj4z&7}M0F+~Vcr zmxsDSPGeezhVF(h?uCQSU5sYyLCuJJH+(L~CecPgQ@ttt2i5JxZ7;(c#Fz*O@S2X} zAu!S!iY;4J*=o@M^w9|*T7hA^^WQW`mD)_(6tRkw@CgCAg*pPqF{8G2-4=(~f? z2NQ|WZNDc)YLsm{^X)pPM*k?o{6~|p0h;7yV~|}i&FM7FDbDuY-VO2J(A=I6C@6-k zVe~`KEy zP2N=$VrqINjqYo|V;&jD*6_?5$d6e5hlF-`0cChD3V^NjX?uc$2Z9G=olGCxyk8Uz z)Os<^Z2Q+GcX}e}_zX`23V-78YKa_}L@rJKWhG{oL0zQ*&K3z_JFt8l@X00AjHMp? zr1Cj+RvbRn^zA7`1yM*e`^t+)lSx-hmZ!h-nS0d!b5f#eNsdz~KnWDIKG;{lzyc!8 zxQf*u46KMPyJ$4(3K96PBK>!^hvMGCWo$&me2%%Q!_;?Xor=oFGq%c#uTe;@mO*a= zO#au{8LTMEPBgr12O|!2!tTah_&Nadt^G%VJRbb>3dUnx5DV#a>;`|d^7?b@|H=U# zB}MftGtDSsqF^gm%W+n|+oCQMk>?;wey>iiSc;i(abgr z`Gb*E;UN$bYP1%5DcNtL0%-FYpoDzNK|%cQZOkkF4IFr7K~2vLG&Ct3py2*+(&?&Z z>IR8@(E`ekOzl8V|Ky5|$rP)yXfNjEO1|7^MY2s)aST8`RP_qe6`11|$&{|Uq*XNY z{qT)=Q72BoKI`)C1EBcwkBeFy26uKktnAAPvb#0%zEmK{_gM~mIkhau(sISx0YL{y zEnMJvDq$4-(j5wT5Xn>GtukY`=&&X49OPP~a97z+T_~fiNWpY^NHB@WhtdzV(rXGT z#NgZ$xiTx1E31IfX6Dh3wjI0~dW_Xh@4h`@bJ^$qC!1D9ivW@*MVJ_Qg#_uVJuDv5 zmq0q(YZ4al5cIqW>?R9-A^fk=Q>j7mt?S^|i3H}4L{#%kpnS(1o{>6_aN4Hx9*e#jN4fSwtzQme)^ zXKobm=R!8;-WT!wmW|m1fuxem2e)&4 zIIa1C7T1okyO`mmqMTV^Oe{Qz;=6b4nTTBbPo50jdWE8FG+}iZED%4zU|$+o;(mXV zhC-;=K7fKq_U@&khPJQ-19Tpy zH&J|X0htG3!omN%yfIPAa-@4fsq$idAl}iElzvvrEJ}(Z1V&`1d;fhMJSIwtSJP5nZjS3y)tMcO zqKcqCRHj{kffk*Zb7gfE36J0AcqRQxi{IMKqrviv4j$)PR?cwc<6S~Lm@*m%2MRI* z1>DbTcmt&@n^jFQ{jFg$FZvbKI`d*U<^KY1!hN5qhqXSlt>+O(gRu zwWXc0(-2iEUkx?|zJ66IB&Z#+#_VNwnz&VbGgD(Zmz|vrsOw}w_gQ56 z1cVqK=gZ^uYT*L2p$9Gt(G^03&C$N*hb9T~(0ap5e0r9-ADai1OW*RWYj=3UnHFeY z6S>mRe%9W2ZVvUAnQ0Gp)~X>JGxpAI12+#EPk2-_O@=ACG-qd4GEIHtH?Z z^TjHFL>FsTgLruA?YAdAW@woY44=Y4_Y*Ul=lyakKG#UA=c7Z5@UJyGubvW;{aQd^ z8NoP;h8{Fld6hjH+TR(OZ6D)_M<&}g#tceZBB+O;$0(%o*8_x$<%Zo|-AC;7x1ocg zM+46VEG3l_d)3s_5~V|?b~NHYQ@v+LUwDZ&$KA>1I-hJCbN#JjlgwN73B2xey36=m z{!hp#UT{Az@G=F3p*wRZUuPjG967#gR_akj)~3CY?bmzy8fQC%T@TAnGQ9YMhY_)^`l=BN<6|c}<;2Q#a^BC3fwy}2OPK7V;jC|H`ECyuJh!56Bt!$nazk^8dJ%r>1vu{p zQE)-7zkhx{U8IWA$d3&Z*H_WJDt+Msh-pMnv}@t(B&-G34Shr-T| zK%Hz-7q${D@brTiRQ6T${O2xEqiltl36HqPU*S!^kKzxgL3-qEkOU4*_PFpp&-i@Z zdmUIbf28d_HAU2jDMAeI_vcj|oW{9aJ@3SG|9pd4EL>?0_@0-8V02LQ4xFi&eYFqN zvgk&t@uSK5%(pt@w;|Jib^=T>fG9dR+yt^Q`%>DmliR-I0Db+!X!f!+J)OdxheH_`Mre}9D=kb9lZ;fcsvX;*RDy~~n3#SZ9GovRc%iGC znzl59{DYb?If1U(r23HNg1xGd68HBzz@cbUBUI=&JiqFi8;=gpw;lOf6q*xANy=-t zk!0QM{C)blp2qe_<TS#ias_eLc^YCk8-FQ zB=c>w{@9T&F)zPc>2cwx!ak8|2v+5fow+bVHsBy|XlQ6asbsYyd3Y>wAJA;KlAC4$ zYVtUKsV>;*sIp3FTf9)`G?pQwC4GmbuWTKizv2` zgHIhJn$YrGBDt&&-aasz4|V8&zj|5{LjOGH7()2*R*i`=Pd-Jn+6-!IYr8#J>@FcX zR?ycMJt$3_gZpbJCO5ohUSKrjqyu+x zXQej^Xom%=p)K6p++u3;&J}f8u4s;*wn(!$Kh?BMRz7F&4q(HdWli-IZ0Fo%CYwZK zge*}k;ru-fuovKuh;%FnefwHHHh)yxjM_s$wVkHE9*D}T#_M<*Qzwdn3{#j4DftG} zEjWf$=H#3xjTKBIJqE2@QBeGSQ^XfwCM4;8uhA!k#mG35%UsONyFiXRmj~+c{-`z0 zf$Zc}y_q&7u4xs|wL%w_E_xC7)4jj{?&A%n-pehjDUNW5e-Xs`-Z!y#7uXV8ECoOpssXjEHS-uF4n^NA)57KPx0^jAf=$bxmx z87k?YNJ@TeMb8E|%L+uvvFtW>=$Y-?X2dcg0&l=5RHwPuI^Xu*yXD>E zGylk2Q`cy{?qgLvTWh`7{@TUm0%)v0IyxdIpnLV(aX_Gbc0M)qIqmjKDtQm%>Qe); z29WZAs$Gw0Ve2yLr}ZxSu4d1*y*@{UM^CH}55)AxnsExGYTfux7sA$@LNRVG$+pWm zR#WvBMqGFB{k*V$%Udf8d^u#3XNxs2@IIoH>_MZ~p_M+J6#$zfAt8CZc*-0hrIQe4 z8O63*6*wPtEslgCjMZ~dkZzci`#MlY0XK%MMUk>QG)>#q?I6m0`}ule(c3s3Z2M=W zboudL34dD^7I;;tOP`CWX~#G8MyIn<3&lNodU|^6Qb{!UL4X+91aIVPyB~5zUd5pJ z<3jtaj64xIQe{k{8{(VB(;jiLK-jd-8faqnyphd{t@-We(YB*?E9>ox^FqDS>s9c> zTVGKkZ(x_0S-q$9Jg@!8J<`SZDD$#zz6d^jnhW@k7y`$NP^tUM zxqOgKT!Rjl46EZ&G_1mBfm90e@kE^NEld&;k*V3@;r{aFOQ5O!8k!fF8CVjG%@J4j z%zUnjY>V--Q}|%4_DHP4yj&`%X>dD*huJW?p@ezXj$47w^VG^GEh#gwNQD}foRj!z zmp@LbeU*_J#4VIveED^C5e(sIlYYne9(<2tzwcY8oS|VJi03e$d}U;W zq=BanGU|P!KM82s)YpJ3byQ<#SoO>@n&;H)^4Qr;;H~X!K8n%4viIZJT%l5gPu@Qd z6T2!x-r-?nRMcFR>05YaeFUGNbgTwxbM$a*NpgP^4{pl?iCOx#=>wKXM-_)@(npbM zX2`8(+(egIr6{kedEGDUFh?x3s}z6udt2KmR+)xe5;AAAx+UTj9;rnH<2C2_QGE92X(TmG!% zxOm!wgQM4bHQ!y&lq7oZrx_YIN9aPGIwK{4C-)`O7DbhGoX&2Ug6@ zs2i=1vA`Hudw3XE2m48Sbx>zuEHK(&0jhqiOD76;+$*LpFL1J_i-`wSMsZ$` z^3xMyC)bq%A^DRynNBZXel_>GcY2(gqemsF#nz2Yoz*E?Y4)+nbZm+%*~ z)_V#VU5$&_Xjfg;+iZ0fsBRc&Q)wCLW0(n{YMXZ#IXCS@@`v$2@_ z3UGvofMMK6WW4{Ndi202hJ9JBK1OY*VY656Q#^yMF5wJk9p;VUsq|=XjFFV(^%u&^ zM$+r$(qnQP9oJ+F$TT4_Z0Q!v6xQ{(5!7S9VlVLYjGe|ye*^?KD;5MN2kn2$H= za`k`h)oe`{i?&mKDPo@mK17LcUOtYbYBrCgD%NGh7vUnm*%*EE{+(g?wGqK-&^u>v&Bob$)d6OM0kmEC`<-#0qIu zwjW{87^I=N88z-!Ncl)v!yX!%dy#%Jwp#a5r?>18mZL)mPqJf10$di?TgWecS7J&IozP z)=Xg<+{ZBIYnSM-zwN7;vH>h^8^bE0y7#$TNNdlXbm8(P}HKfy4|y zV<61aW@c6AHl6nKyLEczFG%rm6`z@ZTmpg0rS^7bu{KP*Y;#x0^UYpc*h)I7X&=@(^kHh=b?t_W6oJ>zSElJq(s2-eQ2@NW4xp_L+8t8Vh<6VXc~8?Cp~mZbGR z)@T=aT^&wk!UlKCWlL5EHa%-@laq_-xuzpGBT>bLyY{3;3~XcTyK2?EKc~iM6_Ja0 zSJk3E*TSa0(LtJ&S&NZY77I|1wP+NckBh9j%<+wW;Lsl#7q*V5zI>F7UpFq-RH-QV zK9wDlojj(SxIoQVA!?>Joe7$JS-_-4AnConEU>fD9oQVp49s=`*|O@P*7s^By~CqIwR z=gmClS66Gz86Q~+!w5wa?{*rD;xx#~Tp6M#?V4xAM0#s)Hj8|bg~3@|=AOb^#n+fo zpf}en;FMowuB|W^DA5q+eiBZD@V1!?X_L$RlTXP-<)^TLt=gAS9eGv<-}TSaN8|hD zJWII!nu_#l>p<56`JOA^!63Y=05!r6U$37UD{<$b??sW zvyTSXmvee>6`nGiaNQhD^%3oNnO{( zyhr-BW!GlG0~S1Ejw#(~7_ynwGEf`Rkg}`i3}bW@yt=46J4>iR0wp9FpNx&DFS8Au zUwasu4Atq-0Amu{J;^OKP3L$#xxJrqNu}J1#RYSbD0qNctc;`VVwsNKxBKaNIvb7d ze=;CGJ`#mMb=F_12;_aOW`B9`K}XsoCKi2bo}PwgbqCKB?;bF_g03~MyUJcnLc()M z{s(-O!@{>bUbK+=@$na{#mNcbx(Ln5DSO(8@5;r39)HR-nW@MLquAWsysI}H>i%A+ zU*9H#;sr*>j^}gSeLE!Jc)X#sOjf@sMOESHOkrQvkbcucQf)fX!9CYaFF-pJyR}W8 zmp!elg1nxaw=SQY@0OTR^a&b%uacXc=VE)6?8Fhn~bJx}BxfZpe7{8M*eQ&!Q zQx;WLTO%2U#A1Tws313twT0C0d5v@$0DkJtya3(KiUKcolFs!Qr40IEMxOAO;ryBk zA4`&ERtj5scTM?*4ZWE&-%yX^NI?Cc3h=yMXmZvg6{IS?8Nn*2e#@1F*^lkwNUd}- zNh||;y4n*#^(i+jn3CM%>}&Xw7bd-|!N9o6u&XG7>y;=4tCs5@>7sah9Mp5XuoRNs zkwJi18G8wqXw(Y3e9Zm<<@BiN@Aoe*XP

?d4lyDNoK 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 d0l*2_g;wn>@b<3 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Ğj?1V6F=S(3 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�dGHWQ;DH8NF2g3V8$Lwzv`0X)OLy{7iWZC$ZvEDZ?x- z3h!%F-YPB~wEgNmO4QYPo30d)E%UPU+}X%v>1l{N83nj4e%6_M^#3Jk!t9v@gVmnn zlHN4Y20Ydgumb#?+Gr!JFOYK#!({}^J~9o`3`|IOPJNpXe#3oC*n`EjoE811f7oiJ zK==bjdoX1_Q8oD2!xs*RBTSAi#-zz{>d_qAEXjZhGMWu}z1*4bw3=4N8@DYFIa!8R zZQFYE!SM9J(z+F8=5E){HAMI^;6nnuIK$Wt!Ek;_yTRbGRjpke=b;IfvsCY8~ zS{t0~A6E7gbR%B0;NA}+llMHG7H^>v{sp$?XbIk~Cn5%NspjJxsCvL7jvy8^8e8Ln zx(Z&(ii(EGlsOYS(r`s1!ML$ogS8#pu_61dReqBQ4f5GvBw3id=LVqZNS9w(UB6tT zS(IR^`2B4Ifv5x~>#Wr?a7U3TxI7=tGFf;1r9_5n4!^vg21=+7#Vkp@gZG#+pv~DU zIp-Ys5+(Jei>yb#qYg12f+!M-*O*Ye?nuGs@Bwb#NBaE1rz1w|sPT2CB-{t(WSyF-w&nVq0L)E$NT68=n!rSUMH3zxcQGv1m`p49c+$t5d_>_^t zq`RHjcN~Yz+}fQ*2`Z17b5sJssvdEFaJu>ZlEFXq@wZ19666fk%;KpyOATiF{V+R8U6S}nB9rjXBmcG> zAm={&2e=r|6CHyt<(m@G7Y41+x98FSB#K!7<2_r-h;>?0H;}G7HAtKi{l`xZppg-4m*m!+1})oEvwToE z@BE+9)Tx0DoY)hElQT?nGY?|_GnCd>xCVRJj{(_z#4$yCq5nwUUoLh7b=(DS$Cjnc z63>5?Y&6QhY~?b-_K*j5TILBXjaR&4ga?y z#z}R1(grhLokcTOu>NbS{q+VN5Xi;APrHhDLLu; zTh!nDW^@7LcQ?)P zjBA9`VSWXIFQF1S0o7Dd}Vt38~d$W$hTfJS+ z|KbP9_kZDs6%WqL-G7wXmw#2+iOtLNDE#GY1=R+nJV^^x;jz~?j}Wd zOR{sr z<&sc4E%f~Tp*-3)YWyYOxyQi=^+Ra}XC7Fm zB?yy1E@)vY)VoUyFJaj|#i)$5k_N@%;airNgkB~F)ey%ftF?I1-YK^A@R2N&LQW#`Gd&cW9R zh(d?a$^9-7o8Q6qlDSoWHAOPyZVY^r14t(HHw-EDm+4&c?Q4#_=68JBs;0-%9vRO0 ztRQ^u`bF!sd6OXpi89B^2mcDo=lZD#H5uKvnIysC${$rD}#YtM4e^hkhK+ebI z)Mn3kp;;Ds`N%8Wb}t%kIpd*1B_p9->}1oSKqxPb6IooC2O(F{6Ha05N)+fU?zq82 zhqb6Bl_LIrWZc(6@aJTVk?7gy3K<4yDBW4@kvCP30YeUaqFbG-a+CiEbrERnN#|!A zUT8$E2|Ctw`79I>bqaOS&xm7dMsg-v+=H_d&A*_;q;G!k1dEAUjixKH98N7CN$;0k zXkqU%YPGEFb6>)8i^f8MOSjLSfz6?dVh0?CfuXy;cBXvqCpH6gRP56 zT88pZ_@(W$6Y2h#Ps^+x0!;YDA#Z7S&gZ-}6YNz?E}-HeR$J=QPe$?C`o4_>y1Q$V z-(l_l0Up)e;*W+6yz^hImCC%Ww3u!t-m}TQ9Me18^N9!%uw=zw#l#lC? zQ;!#=MwKNMG!|AxRVJW;poyhk!=6MUiCWyD{{<(a#coI39NQLWK!A@78pKv`!N(O3g{L(ixtu$D&CTO;YTaPzr zPr>c8gwxzgy{%lLBom6Aj_bYZDF*yes!hid%*F#1Y|AMwS5NI#tu#91|%oBA`IkEefXZDvaD#M+2L7%iVQcQw` zbY(PH$R4%+k&pmLqVpBHRyV+p(yWDv_wrcEi(PK-?8nLb(56t`4Tb{s@A_G>|KS+j zSTz*6F2ul#@+;ySpZ4gy1@j209o^_Y7M0&4{%F8^759PY;ASFiRK0)MxgjHfeocDF z%b;(Xf0<~BUuEZV`p`TBNkxqNo9U>$u#3mP^9YvAg&6Dhw$6Uq*?7}F(;n)ju)%`l z*N3Sqr~lkVO&q}V2LiZqp{5Tjn**7cc;jil8566irncbVwC$4-TJJW?@K*7`gU()x z*%X;>Y_hiT!ZFxb2}-gB$3`mQXbtw67t9b-$4#~UDnmlV@r088{yga)$?dKI)X4MI zocVtVV*ONhPhqv%tQ2mhgvk#5Oo$kVGp5M!M=|Lbd-0&m7VG})D0*U;%d;#e2!QH7T4d3QpE5@;68-uZ$(gzz&2vg26?Z=C)4 z+3mBH6>|7+*HoX+Jkw0mKH0w3VGdlY2BSh_P5i!EL34t`g9^CTtkU)Q!@(fKTdCVh z`%<~~@~gVD?Y1aY_3ByQ=ho}U;jF%8cbZCh-mY8DX~`_rs675_D??HlFIA(KxoGtv zucVi%XOxdpqk)MlOMZM&%8;Fmly@p``>LZ16ehpj0P+KV?nCfSEXIWr|I^{MW+k#; z=gy+Z1pj%7HR|z%By`W|FDn;9&=};gI9W_}cEaBMrXq-5(Qe z-Nk;m4?LU9Qd+Big70sX2wwTplrl);F_{I5{f79O|iW9oX zUq=jEIZKw?XF3mS@{aegk{oWB9hYXxT>=>Y(s86`Wo2-_2Z2wN42C_%@E0lHtQL#|j zr`;(qZw5X$Q}}UCBs&?qT#NvrHov#dAjDeup0^k7u^XhsBi>E)q|5K$9C(V!6`Lm^ zCb=`*WrX}S* zYurM3#(gLPcQU<5cTipnZ!?(A!0>wK zH!PxS=T(FOiYm^`gS6Dnldohm)(_M);_(br$K(EQ$;R;qtbgt8(Qqhxy-hSMwe@EI z0_(+f436GNa?tH=JYVp1^*(*^%@6Gs@ZL@TS?A=)WHs#`#kn8U>*WNWXwqbZ(7UbM zQO?8a3Zuk$JDy%)2B|`jeF32A5j%TkbFg{E)L|vvPIn*20`7CG5o4saC7a;}3~N`~ z<4eA%h*GPt-ZQVQ*z?vHo#>;iJJ-^D07#}whK(4f9V&~Dn;t|AI7bpD`(n<= zobr7imdq^VnS6(v+~vf|wzY1ILQ#d1O$5Yg+N>%Ri;Qn!7#eTK8R+G5_{SxWd%78& zu;7XzGF-!Uo2fbNeWDqI4neU3!ctYl95-oAbZDH2Fdd-9$U z{6qFs6-?Oc`xW8DJu-Uu%Rf>KWahvFWl#TZNsdEOy|~)x+%O#Dy!_)A0kih;n7hJxp>_`3gubN>z9Y}2 zB|QugW)w0gc%?v3_ZeFEPqQwf*IYqOL^blzdILG<>n}?d*^i6wlyp1oR`x4(Htr@R zE*(aj>@F{Ml!P2txF%LxZSd`zG;4ZA3h}c+Q9h3aUXr7RYaIX{L-b+TR{{VkvLvE0 zTznv;ITVE}=10NaqJ(Z?LyAkx#jsdfSl_!duVV^M0r3p~2ax(Uf-HbMxrCgkq}VvL zEjq$Tq#w%=px`RnP0gbXcNhrQ_Yb!8H>k$WR!5A=fL10G ze5^5%;HsEN4@UEFm4)uc@5+QA(<&tN_0@o&KZR?kr`wrjnH2S;FMvKa4@#? zuPbY6Kh?qzm1+w}He`Y-)iT&f38{I?i7HXf8(Z*d zaX2y0gy*RYn}Z_;QA)z1ryf&WWbo-Ds^ z$_Yu|US_ro-g|7P=v3)7qt6qVhiLWGJq#5XU%)u+CyD;?l}G!tJ)e!w(5Wp~G~Rsy ze#~)b-$7r^ff{!)->IUkB-O`rlDqc<*BIQ(oQRYIc*ggVaCay`$d09w(pjmCxedR->Hn<@0HKq08w2 z6+`NIq1M3j2bCE5OLfy2st1?Z4tM#!dKmIUihz&tz0JsoJ_XxUA&K%V1UqnRJ%|wa z7b!6`Iil#l-+)nkYi*UjubmkNxNpcs^ajH}JmKw&*E>=8wQy~ z{m;5R3`H)<>kQ9!V)>TKFiMyji?-;K9k@ki?7nLhI{wHDO|5b*@=YFLdaY|}5acup z;9vza!2UdIs3D;(dRmH!CrylTtW^>)NRZ-p>wl+cx~Vw0k?EwkV@3#UHLzsc{B-l< zY-OwTW`Jwr`$jJTJzmkRDFv5#RTDLN+d}$Q_lK5mG~DFf9EL=fXg|K%iri&6N{<%C z!5^$UVVJ`0N_qdNmb&hU8oRK|rJFnH;TJ26*{ZSmVjI?lmNVJOW@&h3YsMlKK28&3 zH*0|GJMvnQh4-ByTRJz&m_szWW$8AR%IXhMLDtfenh>8rsp&^oYep!~&E5$E(WCb* zw6N#6woAVOqzLOlo=Z6|+EQLd&Z2j<#!96C`U2qmt3LSma$@8AdG_(Riai#XLzvfY zVs7%chw?6D+wbE$Wh^>B$z5aGN#jW4sOf|+p?2jXg9mGqAXU2tZ<0oc$N0Cilh{LF zrJ2ufB!qsV+ZG@$;c4#3jp5|`=kxvvreP`p@1L1_Gk z1m`2(0UD7p?5S%piR61JA#Iun*#1EAB@)3%$}hU?$M`%<6eXR=lqNW1KSxGNfbyUc zfQ8(C3k>~ueizf-`O8p-eW08*v#KZDmvl4NS z1CIg^5fq1<@J`d81g8{>Dx5i7;ArN~V+}KuYTW@Oo&*aFCAw{%F2OcHeXxz!owwP= zdMCcVx|Vq8%}=^$^M^|+I63@%s=a*nJ*3s)mjE7{^)6StHHV(nE{6?^9G@o7(ZaP< zJT&QDnPc9sLL4zX0u}dW4`*Or65O>?EPZ?@IAgd?-bCX0>aJWcGy?F2v;p7XvrxJr zvU2l6B7^%DFKQ^%3N_NZp5It4oKMQvDz7+%haWawe|q`e6m0#;&f6b2;xz~4E1dzD ziD7{0D+h{#m6oEgILQqO-^Hp` zN3d7j`A|sotldLEFNtICXBRjexZIFlm)snW=an=19z_IO6K|IZG6&An+*VTtm@*C8 z%!{-+2J=tpv8X>WNigrL3kxQ-%m6JFM9ZGT~x@Zv5#IZme0Imi+{ zeF~c01K`m+r{{UBK;q;-Sh_s+lDlesU#cr~EI!^wvUOaNyPX;)jPOc~xPz|MTS5s79jj-X!aT9#|p_pVPM%#d#!(#X3!Akl{8z6mBgY zU^>$1Go%hol#zjakReuHe9?S~*Fa`SP{LGbv-1;TzGM9Fy|}rT57h3LR6@_%U}y!DacQr*pOV5pd85mdRFf#7OL%ra zUh82*7nQ$8hH#}>hbRPi2UsMFpNkiK9=4P-`rejyCjRik8UrQoC;US+6=T zFSrr29POImV?Z0aTFJ4I0U{5uR=w(8dQsR^FC2aD)-FOsV9CyOJ*6_Ph_&mLCh-iN1Hi$qS)ll1b*~QpFY<6e2$1sD5I&>IQ6H{ z8CE34zGl_`^|IVa-xOcZqLJE+izsxcL+8^<~bnRgk zp69Jxujdz=zY7`a-AMQ^9mu>;iIO&%Zv}9NrYCN10p)V8!JiqC=xg}6y|ihEf>A{o zNU4_l_l*VA=Id^hDdY)d&ooU-%~hC#gSIt!ndmnego|=YFl}nNh%3@Qq#X56(wlPm zLh4_?Fax8eIY5$7_2~&kDd?XnHpH`;PZgldA3dMGqP0lwEb~=4)~`rRDJCD;ejAWu z1~dHBMD|dOZvR1cBxBenV>`O>@>{n$4+@oZ_ZZvS+rABWGewlsDT3km^mn%G_Oaxu4A6A(9fI>?v&P zs3@6}odlo~5WVFK%uEgCy;yVOUZGU&`s=>C1y8lF!xKgr5MF<>;@8TVwQS`Ss&B^n z62HZKnBVH$_}8WN9Tt=Oa$UK_^SD{T+IT77riOiy#a;TQI9ShymDD7##-WwmBNkQ) zHyXZQ-A4jvT`mo?>Wc!hHM}8K4=K?MISmPYh`c5x#!yg=gY05zfL7=i&q zS6Zw7v8pZ{*`$eMzUA~R;eqQ4Xa+$@r;Lkmx?nouo zIyqUux-W#wxZyK)EFs^fBJGt}ux*!5LR6p>&n-o(&&3AOi+W3$0dpZWaV(@hL+l02 zXWSaOGwH%ORCPmGski};ppZqg_ACZOELkk2l-;r_R$@2gtnEM*KUC_^yn8x&ejQgu z>Kt-IpwC+=+a6fsJ>vxio<|8Q_K_%KX1vuDH7TEHp1|x@uZy}3aRwGIg$*5Dba+LpFZNJW;#_yw7XSeogk8gzbUHvluZ98 zv$`*zsNePZ*}BxVtD3ONP7%wWys862kIaQg46|Mi3!IKs%9ef|%N}*GZUl-wGfM1a zdu|vT-7cQt>tep~W9iBwitLqP>FWvM;>R;op!hWy^~n_E&_Df| zQF?2($T{17;FrTISs_4V%NfAZa);};H#wJE=?(o&s!IlkM@>_y8O}S1yWDqat-jYs z3L(7SaCKc%?47bA1=Ka~0rXwY{4#*3S-b8p?>qMaM0|5tT8eOF-~+!H(zovA#?kN) zuQR|Ok^uQwQIV#Pe`eA%eiA7A(H5O!LwET*2HIi&GMPg2-bleXqt8gh;I%OlUjDtW||9xXYI!_p_+gBEdvo2_lbxg0A(^C`mOJau^(7 zwvudttH-1~N3tJu@}&latdZ0wLfb;ByA~NlJBT=(=vS=S_?*t2U(tYhohz4I)C~J$ z9Y;;~0&%wNGv)n@XXzfguVPOin+J}?YKPUub>O1oVKCVC_6CxHLxvTt z|EOOWnj*K;csU0J);kPx6BotYaL?(WHZ@c<-MD_rS_AFrL(l$}!u&q*5M$6)*`KuA(U$jN zAl1N9Ng-ZYmGQ98ebEJVcPEHOOSzHSTszA&U%F{sdi_n}!5|ZSLx(XH}%Qdd%j9m|7Gu z5L1%h-7p6)73Dr+FcLroHooGT2K3-4$U2e$vz zwdbQ-*nE|N9%de{RzLRHS#=T|=PCaACp}4>t~4#p?!(F&ikkXF(SuYnqH6K(NB`wG zh$sI|&d*9$Zclz^rBswyM$b=>yxV}|yeNQaAWz+Qx25Bn?NuBsMb8c!)3zz_nB%?u z4v3jzP0`j5FCh_IQ~&XZpKUyOkB*ChNtDgI>Qe7WRMpW`Bc>5Wo4ww97&(evY90ZH zzoE$Syfsd{Ob=~%$gj(%U1epqACl3v$uZ)*O-U6tP&zzIWmHk32L!khtvJzmMV=o0 zldTiQk80;v{fD`+UcGa6oa%*{6*%=A+T?)Gw(0IesQ>F}>vKkS$KyUk0U3BI)U$a= zq&#p22N~1~>CCQSu3Nbk@`Zxe*7@<~nyCbtwlxa7#0jdVAIM9x5SE;ej_1m@;=)6( zK$|_tFKPq{N4)Io?;Ln)aK!|pi&LI77^}bYE?PDJ8%AW{VY*QxsY8e_t zy-QuzW^~}greV6NZ|NQ&gy4&KnL=XYAdV8D%kDcAHlf>$4cjig;18vk1@dG)&Ju^| zY$B-%)+CQxU!xa@&9EPAI}ltRCxxp@GGPdBau%vJX#*kVWPfDQ{!Q}63c$=}gsj+8 zLkOtC9NT5a6srHay;Dh*bf}x3rTsFuj=N-qZ+=&J$yeNrP1-Koau`8M{GuG?l_F{+ z)y$TdFDxw<(Si31<;b=NE_h4p7{A%>c_dqS!`1WZ-iPq4mg41MHx*P? zNFJ?q6GlORIG#B%2PX8Oh)lxLZ;xNYT`nn3h+~Lji{tsrH&QE6R3@v#_y+WK3XNJE-MFanb%5)85@eJSnf%WLtMUwi_*wagkbo2ix4{)peNv&>BJGJ&7L}AJXdb z2A7HX3MjR5aj^`Kfw1^^HHznvdKBz`-HM;!bK7k`_`Fcjx>cAs&VSAyxld={`)Hyh z1j1nJw8YBp{PSsT5FpmT$6Z!1hZS|tU=A7k7r>i;3`~5ou)!$&`9smctIu<1gV%i? zUC0;&a!X;3eOY`I(gE)*QJUrcZgE``*h8LlpS7D1ayq!4U{f7oR&3_r327CM>4u?( z(&=%_^nGIcB>Fl1iYOZip(H2oz~7`%>SrDbiJ4m^JhUaJAsgls>JDKd`@%-o!}ez- zkq-;_-y>Q`r|cRWFdF*J2?vWYU3XI{pCS*aNve9f^0Q$-`g=a@^<^On|E?3#t`W*+ zB|E4m+d4`pU#O}aMOlRZ?zg19uj21Uyko$|6w-p24TT5<8W-g^ z1!e*55N}8fK6Gr=e&00^PnDrPu$=jb=WRGfbjTI8V9MT?`xc@`cl|9~n$)4uUZRz~ z#RRZZ-l*bn$9WOMOQ13Lk(trqlvff+kl^?bNK34meycg$d3-@`T< zU&VT@$)3~1ue0uew(wWy`|}BrYlR`EvQTPVoz&?2M8w)Z>j?E;0KR_v0?T#vF5{?Q zX72Yz<+UpSG;MD-LT{-HJQTO`2tL&MkrsAA9kpwltw)k?Xr3|Un?Ij#PonQD3lnfW z(ZPIWNWv0qvc49|bS(PX|L(H>cES7Y`N`>1eYWudi0>d1R!MIa zg0=lz?v^IKyD9gl-g!kh-6^x39Erh!deLVczsFRAwR*(! zr|fnYM%OW|@AdD!xm5Z{KJuVg98XM71bsH{79&>sRF32`@xI>tzD_M5_(!2v&lfqt zDcr^fjVM@B<7P&i1jSf&pu~-dJWdK1`e8zpVM@yor?$YrBNb6=x3{^9n9@-sa<~4# z!PWumgdoOU(etYpDAgB9sr@f9?|XmVu5G@wV|m2OtcxA45`Q6(9nl#1Rn(+2Twaz@ zx*N?9a0#~v(~j>W3t0AF5p?Ll%9ee%Kwb+&+-mwB*i^$stZJQH$CgrO%RqLdfGjKk zClM{k?v5AnQDUfg-9mH75iZa}+Ha`V8o6E>k}hGCS&Pc>O>HR?O8AanRk${Ow9n!u zsne2wb6IIoVI6|eErhRzqm@LK+_QtNLkd)d&b~podzRaRp7MQ{)4GTIz8Qp&)XM>H=21yD(uXb!7`7|;-%-^>e$wdsxN3i<2W&f zJDvu{Rt-Ip5CoDT)_uaJ2jUfHc=FV_`4lhWf?e1)IT4s_XD^*Z-}1Ov6KHns3# zEVuCYFGtiC#V>jnl@Y}SKeruK2E`i5@ljnVxXawK(CB zw^!#5dJ4}%Qw?g&@*H&KUC-kzY8W0;WX%-kN5s}0Pg#Z$bLG^o_#wuAuWT|L*(^n@ z$D`rGr&e2^%9n*1tg#bAKu#E-{k(}8=<540&1k*$J`(LTpo@^Vj@SS%lO8VLv>}p} zDy2rsu+<2q_UHRA>xWmOPQ(&!9FX}p6|JWMuv>^^lWHQyBpn}q$EfQ={_sm^3I)GS z;&XdE>C$kc!K#{vC?Zu34fLL3c))X4odgwNR!JZnyZMvX<~dm^3KSNoxUZ8l1{BJ3 zLDxPyWN2L+%vgU{58}sgRTP9ba-l}FLBVkfN<#o2pbO5NAg#L75ns4;dveSPOh-y0 zuqg`OL!0@t!eTkbJlM4r)Jj}6h!lxw6SUEsT7~5>^$xj<6y|j!Uwzv@!Q|$b@ot!{ zT`!6VV&^E~4b3?ptc|)*)VVdR7$U<-5G)c5nH@5A)3*27I}XB@G0R7ng&R3QhZW%-nR_O-Am^)P%LGEPj8w>)dXqpS=kU)N*rNb)NUn z_QvDLZ5XNOB=q%?m$o$ONJ;;mLQK|u<+0Yo8Ma--Id0xn%k_(QADhtoO3`ilZsv#5 z4>5Oh#*Ri`!IgbTgJ4d*jxzz82EHSeQuAq#BnUo+kZ&(E%R(iy!MfX2C@DKt!&)L- zFjsDC?Uq8oy+zE9#Lt6=XKbg`+ z2_w(7iPpG^*|7vn#aMafVX(donghr8*O&xBFY&Yib6t3K<1Gf)NpY4C_UQ$HJ%C?t z0*UIPw6xM%BKW5m$36S?v~*1g<{NcudVv{Hu&`ZA653y>t$m!r$G<(^p7FNHk#^|& zA5VP~S_hNUwoIOtBwBq&9bwo|q4KwSo2p>vc~zLQq4c{-`1cVy?^WSK4Z-vA{gzv# zM}f_^fDgbUa$vfzKPF`Er5_?P-%^Og4ngRvwc@_f9t!wEquZgK40z`gj2!fng|F@% z-vh5^M*DKAwfPE@q`odn>IF0D$~c%O;er=1Atbnw`0^9Wkc~B+&z*!PxF>expQ!Wx zusBz|NEFH29+Xr}2xl`Fv)}Jd12$gQf=6lKZ`>`^{XH={j0_}g)#6`?!q(A|tH(by z|L^O{=?e2I5rGBeZ=e6TuAJjPuPX<02jY~0g$BM=q^Qn-9~>uf4JTz=Qzusg2NN)1 zV_QQLV(GV|vy@B>jNR>rO!&aS-jQ0UXgF!e$?_Q4f*1_m{4ltI?B22m#wX}zXJBMy z;zVp{VrF5(PkPqcNlI*C%ulM$F2^KiCu(ADA?4vb^}@4IP$phlm3mD2l)SXnUR$EuNNmPe$sz3r6H$4ENbguLd?#O+NRov}N7?n&MZJiyAfQBsNV{Gf{Nd4EpoE(p|jiZx+jgg787(Xcx1A~QyF%LH@CpR-Y6DPA6vp6#|Fr+bY zu?vf{GP45Lghkm!{vB4#*2o!TV&nAhu*Ux#mg|2F`z8t?J0N8-69)?y6JrSnTM+Tz zBJ)`M?|EVWU*r2%SmXaaFC71CSVkZ-jBgG5KN|YqmjLN``}<#F3poBu@J(z0X?Fm` zTF|5q9SjWB|L;Z&OUGXj62GXrue}P~vQNlTz_|+zrLvKr62m2iVti^*(8i1t`XVm; z5f;6`ugfweIX<9dl0#E1U=}v|2WmC38m4{{#1xaCRPP81_^%-&VoVX1gyyu-?T+!q z=NIo6R>#xPEMC^34{G*j^*1YyH;a{*zOSA{5I9DOv}b4e1aSC2E4aRXu_tcb5_0MH zKaO~}hyhbWi)8%wOa1@8`#-#>ueHWwuL-FO>Ual{IEh3HT#!^D=q zx21MbUPJB`{I`8@Sp~LV0#qx>?YY%bII@1ni37TqWNVU-#ZcDWmx(#)fyr5)Qka}c zb=Kd1gZXLE;j>Hf)wuPSq7|MG%cp7nrsjPGN4s!Y52BI`ur*g+CFhhAY12f`_&`hH zB}SNnj_~J-{^uXA?%L{k0udlDeR6t!Yo2!s31#-TT!-bw*9i^KFk0C^MpqJQ5|9lDq2ac2i@sI7Pt*)xzilwXW&8~JLI;|0Y+{st4Y`r+ zbr+u+v1^Ax^@_<7ElE*y&k>(tqbX}_pAB}@dE|vp-(9lvr62>PrM_4Wh!G9WniZVr zV7N(cxS}0K*1*tPlKGko?_$Y%B(YG>T+s_MtHsFw(}s6W`_2xWyld@rVTLw}exyc=TTRac*%V~e;y z7ri@vIe<5%W8;f-Umx&X4}T;x>1@*?@bUe8gC8_1G`M|B&IfEk;Vp84z6*ht&K`=i zvmZ=D``XTI_zHe}p=8~3;#aVsQv14L%~H8}i+;6FVRGT0kJEE6D;8T>&kU^VJlk=f ztl9k&86NG2d{bd+G2rLNe2g!KdlK+fRL_lwKAG{`K{7&ZNFf0c( zyK(g1V1?B&U&xhPrvK18Zl+BKLe)#z!|Ji-J5Z$6mHjt-*C2`HA5*3nMcV016bNFM z?82KnHjK^*#eH9}8^OT@_*=gojY7T%Nf!Aak>5K>-Cr+ArhC^*blSlzorc1P?vzoq zb_?Pd72707}>D2;Ta*JK$I8Jt~#>s5l;>ksAuRC@dez6htQA^9IkaXufhVm zrV_C_+y&nW%f>tcEM^N`3$?}x8kAM=Pu7FetW1cti`Ouwr zbAHUXTZ>mdjMW?lPqysI@UL}vNa<-+1!^bft_!_?F61XUt}?zhj#+XQ8epv}?SE@( zMs$4*e7G&{EXB5m?BZbdp)_^+TJHFRwjGkgf+O!4;;I?tb{S@qWmA{_Gm^hsCbN@U z0a6|W*5Ivn_wqCOZs>46kvb$!`qo_s1N*mB^^S!Er2x=hVkgw&V?>vbY)RaQVodNtK6=*{XwUFw(c&nO?( zZgvuDBiUeoP;L&{(f1(@jcqMvqb*I4IP0FJs30k6CMtI4!iccI{9(=BCi+;npNQRY zubwslnOR0&kA#?in6dTB&^!t&%*cRfD$@?Q_Vv*5R4w58UUR;){Q|39$Vx*q&#EE`NkBe99MaSG zBJ|6~Tet9hF86G%@LIzjy+}dZ%?1pg9$x(j;%u!TWvP%bu>w(AA(yjQ)#~=yx}xed z+U#GnPxv%0GMY+-iSd72E@tn3V>iG1_F)iDO~=+?HNR~{=ibEZcrn~KmI71cS6BfQ z*KB6w`ZrKR$cDJ(13rx`b595{mnh=mhA}ph+p?Rl`w2lP*NyGRvI3;b$N7A2Ne%+> z(gc=>pHb-sN7qV+_aVJ>ljrfOQ;^y*qt`a>^%c}A+but3;by;ctENdt&a2Q^XYTHu zt!OE?MjGVk;w!9%vppgP%5NkpfyZPjKIRI*I4M!Hkl%O?%_ZKFkD&+&W5P@yzCL>4K@@W;9^#7d&WA z8yp^#)Xu~>1s-;L^OLt?TI#^C5rOGZ9AR}cs{5nZP=9S42~7AVw_j+s(22Gy2eE>T zWIS85X2bq&w;-ydM5`aaMdY^&3#n6|)``jy9(FZ)Lkb8 z)PV2&aLq^C3XAd#HU7LPyv(su42@-cjRx+c1 z+KGKK2OY|Z&pnaTfKPa%g zL9zIdWv*gaejLu9LAon{EI-E~s(c)blM4T3VI^&J=D|QM@&GI)4!WgX#lkNNC%2pH zoHMsuNcF^g0M+fWYMfO5Ff279r+ZJza0(i76hAx3@bHeZ80#l2QHSuY7}|4^km;a# zx$Ki>1{%jM9UgMR0|Vw=2N=bB$!88ZtPq4px&-W3m8-xp6HG&#kJrG8h>F|mI=O#(X-LCanf zwHs`l=%{VLfdAZxzahZu^$+xh9duHv%oK+umr8?s)@G}ZFg-on$ z6j8NWGfeNM4GWe`LIWK-@IoQ7Evln437<3AWqFMV{pTo%fcQ#KKkkv~%&hNpJY{t3 ze!qKj>iI*pb+LzeLXbc$on5x+V~u};W*}0^)U8IqL)Ge%7)KGI+^w5%m6@(V=aSa6 z-)%L`g~q72X7+w;+K3}hcPONt$a`MO41LrK7gLrvbq)m27jWEkgos(ck6QB9cx^`S z@Ju_RTX}noEFQ3aFk>w+%)r(v2}@%Q$#i+iLM_RmQj6)jKl*F8*ZerOV4-BoyvO%> z)#7{1Kd%3bO;UAcLMkfRzHr-HJilNz;Dvs@IK*eUV%l!@O~XSlHduf()0HgDDX(rF zCYPi>^Yyx8KmdIyWt+QVZ23mFgtNM{g6Y*ZCMjjJUa#kQp1-mHiSef7CLJ27kzuf@ zwo0vi0a9R~o5z%2x1wFv(hE1AB~9u?f&?{8bcm<3j(r=e^6=Aa-Qd$zxtazEbtuNe zb$EN$%-p0-qqbR$2$f5lGhtFX%~34k#og4|7>izl7=bdBhWxlr%=4vxf8$8vJE&iw zACNarMatsB#{2kl8rnfQWkUwSCaW$c+!jf4)xj%+@zLj@gnd2g0S!MSUf+WRYngKl zOmmmTRb8|XwiNtlObi2cAK4@Rbq?^U=dUM`g@4(- zKSl-B%xAa9H}k@$`3QFUKG*nLdpc6vc1!inMMlRy4T7PLE-nyeII!&^)#;K+V@xRV zICM#r3)3A}XyEm;B0WfmEG{6Q^Q`b))oapFC9B8B0JJJIV zJxGC0yp!p~8%EWRM)2(yF_kCxtz#Q6&WQvfb)PDnOvw6ASkm~7uy`#Pc#r5l^P5n* zu=jBl(9jljda-iZU_F62#bZ9;d7~Ijf;4ri!aEg$Y=ZNb(V7(NYrhLAABMvEV%hY< z(gVM|YGk7_8fNK4|1mlo3G4bgR@ARVI=I>1L5lkmSrC_NpGAG9mJtr~r4=~3RW5pA z7dijna^VA|L$Zc~bk2Lh-Sw^)*X>2D9KWCY=}p8eU;%RB^$u(UQ+uYWpsItWhr zB1Y0^$tbd*WS<(>MZnNd*2+*XT&SF7b5WXQHmjogiRjx^?mL71*$Yu3R;L!MQ_in^ zLU(`sn2*5hzRUKJY$7hqr!ZOL`OFh7P5AbtCK~?~=QlD8c`9y0d5)WYl0fRKmU!4$;yg}C%i|}CQ=_4jYa@EL4q+Qxfr^4WeS_ktX4Qd7vk@*}Yzq~`E+GXYx zYUF7i2CO}GRutEdTEcTK7_3={LDqcgbDf0+E5LEgvwA))r4Bh)Os8SyJ` zYn))?w1}(YhF8%O0)9wc1cG@|>!B&Cfmvb%aD=lpi0ezDMpko-CnV7*C~1b6JrCAHAu$) zfw2MjoPU!#|GMkWIr0j}1QzlQ38O8M2KkMkl@^y5s}Ru-P>^{C3IBEhc#(jy`k^9u z(}01!ef}eX%=VuNWbl0;c*oz3N$ukipa2KBqmrZuSj9O00pNhMlhSkq14HV2`+z6W zA_0dM(vbdZW6~@{YnAz=!AgI~cx${v$N`u0hQ(COu&jDYi}0ixX--|tEkD%y3JvGz zAZ^JAa|^qk;pjva>H5S~$X4eJ6Aa0!9<35>Tu{)FpF*ifIB={lh_grtVG#uN2Sy%O z3e0zGFx&I&G1HyZhxKgr{;5;Lc(?s=ocGrCEVKD+h4)PFV*HN07aD>X41)NpAmk?o zFQyE^wI1aUVBo-8?IJ@^2NL?P!=H~-;ezPZn=f`Hxa`e47{tdEhA%8siD4j1%hI~K zE9C9z93(nOV!(R}? zAwvog=fD!%?I~OJ-#y&wdDqxa3qJMs`d-eC~jzI@U`ZNJ$g4U3hcatFkPz#xUXh#)@d-4Cnl z`rQAPB1c6EebycHKJ?UkbpP$z;8V@#U-@mYLGZHn)yUQRD2cK9)a>!h{Uw~o*Aj}B z_^Ufaxa}sTuJ`HSG4n~_d2&XDz+J)#*{v%FL-nl4V_V;Rly87qYaWpz09<<`(4gX>FNxQS06voHXkRp@vLQ{XK3d^*GJu@hxKjQAnUbF zx7f^u0@n_eBfkscuLzK9&-ynFpShmFLR)S}jxRAN7kjnO2)^>`ty9@~J-#ge05`cV zn%7d-+=?kQHQxI!!=tUfNGr1ruhsWUP;K|mGSTYQJ=%9;K8NezI0XE?3lb8u0WN>E z*`TWc%qDmAhv{z_+3*!Da(Dt$*~-`GmSNnn^JS?wq!SRgZMkr=uw~rFj+ z{SmE4cG|?qZ;;h7hs3ZMh=5Z<4;;%=eG|>jR}q;C?~jCVx{KE^?LS+)DBC(v@fo*f=_h z1^GwE5!FbqO8Br@(m8|7_UQxKgip#JJRx3%=j`Atk9yl7Eb*71+xZRZmP)VToBD$- z(Lhq-uONsR@Zj4+w5Zif6ZnkItGWGU*@KML_G;XlnsDu;;98JXf-Bc3f2H$Sc`f28qzEG?3J zC#knp8)L7SF|hF+Acud-RK`@~7lT?9eS9hN8-~^VLE@0RNr`>h9bKiItKUv4yi!?$ zCOWUEj@&~iPI7!tc-P&+9dKD{6lyYbl}mTZbAZDf;<{DnxR)vK^w*f|XVFtW%wLo(>&ZYQ^rYhNI~;)lfF5zj?~zEUKs3cpQBf{8pFr zF@^L(rP^rNdWNRo>LGbPrkTOJ9gT$>`P(&lT<2WEJX&S8KC;S`)F#57p1=)*H*jJB zAFd(aV|zC`t^XGB*LRSgKIQ+>{bAFVQ?vXavE6NU&~Q<&en3Q6|y688FSf;2g59~lKRjXFt+)^k^b=INopFkG|O zy*F$>xaV%`d)56pTVHMqvq4|!V@#$+7gkPZLxcE}3Vr>Ee?+-^3*_P}%I%@r%j|8U zkLUg!Ne2_^LP4RVd6=BL-OSt@jt2VD6Qi9LLE^DZ%f)gaFhBKzlv~uDXc*BxZ z64O;JTiDST-C?>J@$ypq{y{=WNI=+ODwb3(`0GSg7D zXpfJR3l^dplHm&iW5=&eaMYzAt;}&`>LBgCu_#T+>K_^5dfewkBTScAAD;{zKg(-T zu=ejPH!e(E7t#f@Zprthk;Pm8K6GPNbf4)k99uGV=ig$;%aOxP(rk0HS5=*@F&Sy2 z-RN^sb4Of0>!$P9egzjXc&b4lDd&RI$$r1?B|6nbKvZ)*5!|3LH=2(%#2<&KiJv|@mdc;ZS5M&^Rf7aOvwy733-a;ysb;w;m?*B z8*brx>+~5V)Nc_4jl!bul&Nz|Hf5TaPG1$Za6rAOi0Ly=qX@LXPvG4&$;HM(!4gZ`=2q|u0#wIeR5y3q_6n)gAf1Zhfijy((_4Ile_9SmW zgZrFP6+bx>#5rNl0qX|_i%tzL?!R1slg=}`@MP!4!N&UB9lZJTSjG58%k9RJ9M?Yh zX>uE3&MC`L6+QS3b@;(Xb^htO$=-yd3h~r+Gg{%}SV?H2;!K>BSCghiSmBTkvEvfc zLd=um5+mVdbNtA5X~4F{U}u(u#DD}L@L|J1Y?)5c4<~dcnp$Xg;2D$2rq6P=kBU=r#^cO`PCxCU;&AEMv}U_}ku=NXzbYKtr!BK3FDZYieN2jc;lV}Wd4Uo3XS5l63GA;i=Muf0GHkKWec})CO^KG{! z7N^#>Sd@J@=#P#hmI;rg5S%9B#xqD0@)NQzjJ|lg??CX2c?Tm`jrzhny??&J;YNon zJ~ZSlD-G26c5jD{zT5RlLl&H`mk?an8qHyV05B+nf8;k7>&e%^`iG|IK_irL0!~8Q z#~Wp(n8fZe3RVr#hPU1u_K$XrXy> zrpXoYgA99FFBsg6IBXQpeJfd{k->E?K8kI3Gnuak-MX(K1ZzYY-KdNC&yoxjmW9n< zE}MgDy5x@P#b=PltUH-Zr>Bu4;lT&nicZ4`p-Z)##wC-E8 z9p&Q(nlx+#2~Lz`9xY9&mE$ecu9+D!!9ZA`VxQ*y90@Jm-ZnawgT&=fc3Q3XR?lIU zN)a|U7b2=tRyd}ehCb)$O=d~fC~6gMMaB8NyEb(Z5!XS+!iO6-$=CfWEr_sXZ=;kv zb|rMIM3OKk+7lI}Jg^&{urU;dzh|JX@=^NN-B!ge~?jZ^L0J<+%}0YU9xH= zM8_Dg14$#=JS91uUMNksNLm( zKs*W{WBEoGzr(WFqlc@h^=aW^v|J;}kop@=j5C@4pXM@Z=v-2Jd~AZMhtvI0yl}qE zM-hv}+}t*sM9RwTA~`GEE8#kJK5d7UtNJFD%CuyO5?E-w6HUiuDL%y4CvI`ojUv64 zk!lWckib?EJ)Ih42nAUlwMX?cXG+#P&gkmLWfv>BW5+MN*Z7SKo&6e9-sY}&Rvuw< z=?lX|hxrhDEiHEks>?As2gPTC-g87sRS*ysC>x7M7%o%+Y5OQ7YY8{okT-kp zQ!vGGfX6{4A7RmzHMrZj%T|~$p;D3Fg||9_z{Up0JW#b5O9ywzt94sG*)MAi{6du6 zZb;{8F1SBnE0W8I|4eB~W}&r4*|3y%=X_C7L4N!dgSn-Mit+vL8Af}R@@=2n6UtpZ zU)p|~h@~r6i><7Kq5bkgPWm?CcUUHu`Sa_4N+w<+X}?lI0#-VjRFm6Mz)!g}5mK6Y zdtQc`U8~)L)asZ=sjlRWhRW^aktTu@bJ{jXBa+6_F7H3cUx?aYm+iPjCZGz$L)96+ zq-N^4$sEXCV$p9U#Q)mA=A>(HezFy;w*EqLO*jJuIfG`0mav+rct%~38|7EpC5IGR z^!oJ6^Bqyt%SCT^cgguw7v=oAi{$e)Cq-(IqM9f+^c&uQAj@U4!@{Okfq4TQ4ncmU zLqqr_gez~%g?|s3U)L8mPz0_&poqD^Ax|M}^;H1gAom==B3QZ^`4RxifRY!Q-68J_ zKxo^}^#I(9WVz;jh}9d%?9>(SA&3?BA7vOz-2#A)RVK?R< zJ$jh2VE^ATFw*)wO+`mvDKJK54p@3xJ|GG1{Shr{s!}%ORr6T5R}$cWki|d%4}&W6 z9>DJlX!nV4Qqz1e?B-RsOz$#FNBWVw@X3&ayrL>$286^v4i)JBXdkK-8{~VNjk%$I^679in)4kp|U83Ygom zKNGCujBMhQuXf8%tV7@OBH7Vz4sX~U<1p~41p+9n{SeR-;wK)cN}n6fy@7~gXS0#54iFey!Q{P2XOIZbNx&|dlcG|u19D-HcM zZj+-b9k%~+=L9^RNvK~&>T!{*OWUBTH%abcb;XX3d;}Dk-QyHU9}+;z8Ib7Er`Bo+ z^MB>P=VPv=LycW{p6Oq}YytpgO&c&^jV~aoN+12YvS^qSVcz-z9U1`49o}tR{Pjlz zu9}eW0YSe0qrzXWI;aY4TOk@%GA~qrX+UZ;1@tCBmK|g^Am4zmo%Ea3S7x_pRDBSg zBlJ5N4klF6_q z8&S2P77q0}1J89JPnZP&ZjCTV+dM`08acD;gqyiZc^YsQ@arq^+N-bs?9$EnvZ$iD zuJYF1&Dt$Cg={|fX;l4=EVQ|7Vfi>VRVC`fss{NxC9}F7l6D7cCk7gR{)(ML3G=JH zrx3JD#JPv{ZgqQ6b8Y*V%nxP?$F_5wlu0VR^@^(g%NZI>MwJQVRSTotwI!QYt5)jo zi&%6{qRQlWc82y6QRV4`RtoQpw4A@vP#lINSheh<*E+Rj}TuN)O6wFvb+^(=d}o_MvBrT#+b(B7R6(~Sym#XAW5P?3O zCE#7H?_(gkbDKv|0usgTXi>oNwF-M0C)u{bd9@r-iI;(+#3c4n+Rsouc=<{t-=|CDi+>MJh>nX)8XD-&kK}Hs2rkYC4@o2Lu$=X4_ z%@tKrX5^`vnFu+a;D&qgYI&RVKcqh=O@f3opZ@4nY`EnCf?@})jcVAf%T5XOWv;+1 zDS2tbE#|lb<_v8=bI5MgcmbTHWU4k7h{3cDhwoBsBC z`zhM5T4f$TXg7zXtM&WG2I|8s2)NhH1yH|%`kM?0AtMs~db++8i2Y-aD#!x5tH7`k zxB>L{h!ijg*FO#XmPml<{2j2PFY7ip04r4J530}x!&-yn7+`K+Bmyc@It*&hBGhjg z7z>&|3;hbiGwg0&zdHW_3@>_cRPZYw5XR?`z&io&4mp1LT_80+=jx4>P-x`0D`~*c zKW#$*^!sQeN=$5^2JsN#tMzjp8UWop^e0BVueU-r)zHj&M#ndGpPTl zsWSs0x9f)8$?2j~^RG5wk;AY;bQ(BIZoPnv;l7o^^xQF6=3b;LI=3K$rGqPhOoA9w z=iIRS>(yA?1n!Dd>_1w@Lw!(_TUcn8k+Q_0nHKZC^m^vLryT?!2XYYb?_ARfH`(5X ze=z?7NEJDG2;MhJ!_SWt&@vHSJ#=rbVp6}Z@(8-)zZA{?c>~Z)Kn0d+y>|h`CL-G} zABD3oAhN>@{4EX^hzJP4nFJi16bPU*{x_Or2flhE|I7#s4B)uHsyFKV0aQUpIACdQ z)pAGH6g}p`w2PghQI%;)ShyG0XA|-uvV_qWPP`EhR6C-=8+J0=r75!PBM1-qT_{b* zg_{N+YDyM|GX;w@=7pWT>ni~+iLC8e*dtfm*a8YhF{TEq+|cGB;MV1=L* zXz_`yNa=Oz^_-g!haME}!h-1KC*NPDZnzn!Zs+e^p@FJuH;O*-GLz9u+>TYoCPpV^yu_0$=U?774p{D%2e^!g7 zLs{y^F+=vw8wFHT!5!~Z<4BE6Jf83NxyBbn^(`F2Dcyz2%KxC6I{N~ep<@~dJiJ6* zt6uF>H0&1efd+ILvSIh!Qr6MZ;+IZ`=CDzi z)+65RC>*NGCWfogc%o|*Y*~MyhyN1ygIhWGBF<|40rr6VhF}$cdZS~v%H+x)fHB1p z*EkgGQBF1v6tk9@N@$`d}vm zX4nlh&9b^qqVn~~(Cq|&?fGse{1`Br9}HE$p5&xyx{Yj!N7}S7;HE`Oh!APtUp#ht z9>zTChC9X!(m!5JlQ4Trp~c{7*gjc3lV3K~8$T&FhYsVfP(>TO^M8*ThJnyi2*9trpOsE$@4H-a7Q%zw9AzTN>=Y zjI8uO3SwXXZyXVTJ^%oMDk602UpJLqhQ6Z|T`H~8z>rEl!W-p@`xg`DNAv}(-uKS> z(>rv9Eu2>6CE`N3(_lEft6cxOq%O()=j(pzX3N$~jLd2c| z_Ik6DXI20rIuy7dtb!M|9&L^TNMJs_6ZZ#NY$OL!JUyZ5Sb#7`*XW?5c6J_!Tbk#u zlJO1T@a6m|k;+zP>yZ{I|JHTilRF8Fe?Edb2%mpC*lHN`rSj*k;qjb#0rRHUz&oq=9lXi_NL+d7QSAYjAg+n#-Im%_ulj7LjVwbYPI zEN6H<;`z)mi>|lW`h2f^8G8w#b58L+Nz;hzFSnjk8e&r<+>e4GT`=zR5_noB? z>jjxAfX*=DGDZ*^1?VN#8fu>onX2AFoWkwYYgC=K89BR4eB`(ks;z_aVW+1x)Ns zJ1RC_lQ5`Da5py!VAF?xb74b6=4|mRv)7&;+O3=#$)QapY2tI<&jV_6EhtM}HdT31 zpCGDf+MVd>?`0|n1_x6|TY0QaTe>kX+$uOUXh}(YEsQOw4MtcS4QE3NqfEKv=@x#P zkVk$;8gDgHonFZg-Y@)(aVq~O4;|{a^EW0HXi^3BEg?V{Yay2W&o?~v|H z3+5B)yd~6+8!izPDp03kuZG>CZ({_Pvr&TMe{R<)JhSg zFc3mikBsb;vNf|QFzB;lrlz%$6S{Y`sMA{NL7SUcFtkKcCv+jgY%}kg3t8F>#Pf_f zPfrx?K1f8$9W8NBzjLNNth3t3`fBUE)uZVkul=KVp2n0rcY$8Mt_kN+;Bi%5b>m-z zR$0{lq_c4Rd53q!D?7K9bDmR-APqZ|YcdrK01O4b+1&5vAW|USun;L}uBECp3W=C1fNO+^YXPLq zI)bPE4!8&OYT)q|S3mULS3xj!3hbr9rvcFx2Ns5x*kP2X0Hao z4FDR&_$_rk-#?xjef&`nB%?6wOBVOD&HYk7^KR_-NQz+NgCpmvoANIb$>6sHyYRq+ zOX8_OvQLt)d$EaIuSzpTzGh}-XRT))dasA`z`p!{b_K`(DuDbqdTe@QiP;5hyG*_E zWeck_cTeH@P`z^)-$ClLX$*S>Bm|Uf7bo*p9S-&T6~nG_&_yuwsg1eYc}03UM$Uay z!a83^y0%u$F+dlzif)L#0`{E7G^W~}l+{bFS|p@>6mjfxxjAx%ZgpX>4)v9>UX+Hx zMlzXA94U(MGHUCAiKhFap3eniN!0dUQ1-=lizSuDa;ZQ5E5-`2G{-YJH^W3X!?T#B z4Ft&2_xjevUzz_xt}GHJxzN?8GvmUjEm z3Y~F`ZYOUDn}oW6?QGBT6niJKG<$HWxXV92xO^p+Wv`Cgqu`N&EIuZ4M zHSW0Gvkuc+A{KOg3ojlD{E=^{FU1zrj5FcFj@QR$9((+RAmw?{{PLwhM#9csj(Xm- zc<51EaV-6!t30-uzB(heZPi8xfg~Eyu-ihYm-^Ei9)g?G_*O8hFJTEk)2%=0M#w3x zy0GoCy6fEC^s`)hXC1scgW_;I5WD&_6Ch1?T-G>tBVY_6tI0SrGzoR2I)~~Bb&A9Q zVT8et2AJvCp7h*!t)HsDQp(eYZY!5{%Vwiesa5O$(os_3r;y&%7L8XE4JIs+=AhDO zZO|c7&6qlMV$ajoLCf9evs+NSSAykFWOQ~KrDQfFqA6#xvr0&i=$Z>?xY0G0M=ku+ z!ugZ#7PEoT-K94{O6N`S=Hs zss7M4bz_PMb-Gk$QAY}f^=);^jX3R?eDRf{1s!#g>&?46?cugx)nwj$HVLBj%)==y z4dkD#KPFOMibpNP;+7$<%xQo|l`UD5Ne{C423KWTj9b|c^!A25&=7XvF!JbS?OQF@ zr)Obw1qa8o_^G!zED+BY2X^>MNUF#(%}?qhj_-cKRw+V4Mtq^uur+kn>h>tu4M!0$ z-1V@~NlYDaIV3ZW3^KFcN#?du8an1hO?Ym&k*_rD^SO2k_Ro+gOonW1?&-@cj$)xn zD4-bb4{+Zpvge3hMpmWQ8$xRH*}s0LRIjunedm|cz)EoK^Cu)LpIqVCushA-N+>rf z{-YK{5JO3OBY6|G4IU02cRMy-L339%=o9<6vn`NJ_%iMLm4+V;X`2n>YL-@zXq;u< z?qL9hyBlB=u>zdKkhMBHQkLr0t{l01e0=cY(SaW9#abWEIt?61A3zG&q}h4#C*&i1R4Da!`7|6qaIGLkH&zTT4yHoL?^pZ@db+JGsA!d zP9dGDkhXLB7jE`N2%RMT^vbE23++0(4f3$)&uuvMe0L;g-%b=e_xxOr*W6R(R$kUO ztq%P~onmQh#N9ADAud?%_=hCVe5ze5O5tZkp}y>KH8J zheegd5tv9g4o93^*4b3Rz~RDeOm}oy8~VIWSEK2p!4SWFgk4`c%QlZRl;wqC+=E^- zkJLkU-6fOCzenq( zHiYbZ*IQ|`*ps8$lcS7!DOmljU}yd2>2bdE#kt=($o&Hq6~sRj3l)_m?KkRq-ot6D z!GhE_UJUoMaT6cEX94MDRm%0;QE?&W0-F&XQp zeV`>~cRXQRL-=1FxP#Me%>{OS&&RbBEq_k#_i|j7-^^aP(3Q`6t+6tFA%@7|F0S2M^LqQg(P2{7MjkANwp|Hp(?bTDWzSl}87SaN;& z)*oQLsRA&g6a%>8YU^Ok=wxmJGWqA58Q77)`dCxO7EKbrQ~l5t`-I>rj36EsrauOe z9x}gA!5;rC#uaAb^oQvHT!*UN50w&{at|scTYeQ#oX}ygPkNmSebOEL^Te@7yHRZS zSCv8tqPG8!x3><8E9mw`fk4pU7F>c`@Zjz`NN@=58rXKa8HSss!^E%0@0!j!h#G|m)MgkU9yp=NbRFEO<_*LAOU+mi? zeQ*UNWsnerAJ&qT4`&Zu$@*9>mX9jB-Qen)?p(TqPb{Ht@ z@8|n#&fG$Kf$km`OcB^%Rlj^N2j}_G+k=pH)SgNow>4bdZKwuvC=l4fHEUF-yCwBJ zk9nSY;@I41y77q;mkskAclM@FcBKnfp3Q>(aBcO#evYmM+_3uw%KfZs*R{J3SH8!D zNuB@588e!SNNB9$+XgP!S`v8&f-;q%ifCq$VI$@im*zFt4__dLOjJsqL9j=MgAF$_ zx;eNraD7Vl_x5lA86CWY&V*83OQa$K2BTk#^Wzqw4tr;1ez?UvWuLCu=y%hV?&!}XdO*k(0TS5M4SB<)OA*;wkiohNu> zu0v=OtW}}&hV`fdpwG?FH9C04)KcLKWmM|Qs)Y)3V-u_QwnQLJW7n!%Na5;Fzi~TV04CQ_H$RST`I!jixQL(eS*K5RZCbuT)%9%?a0u8xvB^~84^WTJsv%VFz1)B zTr2)O`^i7ZXi_Vs*sjsc$8Hv=%i`Jl0|70891NsR4@GDj-y9C5V~fIG#3P7A7?<{j ziXrQ1gvW^yzp3GzYQ(8-csf|FCy3|GMUxxUrjR6$JEdd*N}yij4JuFnw*?X|rxT(;L?MK3>YUq-M?9zjKhi za1Kn&hzg3U5@OZePGQkf^PLhv!W`{E#(=(L@ztdt>k5EKWkl8V5LC9u-azk}3GGxckHW+yLECEvWPTghiuOIqG*^Tb9Q01l{OW`z{Z8&x7 z5E^o7Qvj9#b=_*k6qoH)e=~knohS378)+5;1fwq#p5xfk-FCagF(`=Q1f-UCImE16 z^%#E&7+gxFmS-&Fh%MuZ;(J*Wf>TA$SO6(ZNg@*SCz>49JsH{h6D4Zat^@t1!?<0< zaDldP&v{n-NdXeWSISmHG(eZtA5Q9XB~qV$3+D-zXwl||4O#36(%y13iUM5Xgrow6HcQE9YE0Yu`$L1fDZhz+V8evQzMd%0$xDPBp!LXoy}ZpaI(zWkXu@ylS3GP z0vQ%AT~@?5$|HXH0nVtdg=RNCntA9+I=e!;X3rZBWo~}9x#Hwbp5=tRkrcs-*W{Ml zsgGzT;PH2cdv&eL%2ubeyl8vLFNJA_Rya1f!Y=bRvzmxHqKZ|`2j$eRoeu{LCV!Of zEgaiUu090w>P@4O`3iF<;&VsQ_gb)krPuUIrx?Xc+}B3{YLL z492M%x}IYq42F-;4@MG20>r=EYPo6C$djO@Hd1#Kc9Ca5gUpYj;f#7qf?Q+KwK%x8_-)B&y=N4HIq^< zXu`)&LmL(c@Vki!Bo=1?h6a|R3F|lXzqOu9i2SOlsp-I*#5*c?HUEAfO@0!1vyWSU z$dWLlVH4Yjz;yIOj{IKM>{s)5msl;L`9scTn4cK;vGF$4T~4lz70npxlgv2Nb3x_` z0GyM-JjG<6%f|(FDmxl}MLMUc+A<3G5V{gi^N}UnCk#AT2wUWvc?f-2LULk~q3-J97CrvWHxs;0FdvOo}6v#V>Qw6AMrT z5ZQ$7+IY#n>Ckc$8_Q-$(}D@?6riE449jc#d{@)bp}MVS_Z5&a8K*|@@)m4+jp=Ws zvFTI+BTIEe^1Z2ThEb)2GY$S8DaZl?ZR^#DXnP-(@JIEe7~lT`t5{Sk9-c=u-M2b1uOibTPWy?(% zD)!sb6`C6S6quPA*=7r6sqHCSlmqHXar1|BAGIGz;{B@je2him z!AsiX`r?*L^i*AyoW8eZ0sZMhx(8j%?Mmwm%Up`r5`C|Xb zjy=aXA@I4)$%;MSxf=ho9PK=xBK)-&kJc3fWeS_-hA0{Ffu9MY#`B0tg6keBu_)?~ z_sS)$NQ`@TywIrvfQ5bCs<9jn$7UNjpa|#7CwMgx`Ymc7KXd%EDmKG2RcnI>A#qEQp09cZ7v+ zfAY!u{xI2jY2-4)t^(pCJ42chl)#qCFYFqJ>YQC#q8xoc^ zF%Dldy?_rQKU2#@!Utn>eVyfr*c`uPPwG-(gLJManK<-m|+kC$xn!f=$Os_O-nF!M_e39^rG#Ak@A1N@S$Bicze< z;5MeX5x)xuq>%w>S8t4ugbCIo3`t9+?%Fi!5^xD`$#*_ww4|pOS0c4_XHsw1#1$lyM`n5nSh;2I%5V(lZY~Y<2dNO&;Q$H6`>ZNf{va zSngGr%na2;6Dml$p|>1iqGR35prfpTQFj4(o!pE~g~6l=vznfvQP3D2g?h_jFtdKe zn}({P5{hv|s7Lw5u+M;((llNcwy*SH6OptwJ z7+(A~K-uYToZuo)ycdSjo)bqQZ@g&fzUQY_{$=~CAgH`)9rD-r|3<>&@uljMWqzF#w@RqAQu{2++s8rwi1AMb&aY5?n?3ug$CcTD_4~ z0A|YHgcP3D4xTHrt~$FMfKIDe!hP6#e!ZKvJA`}a4=M^nNt>tZ%jZQJ$+TC7lK{cm#OXM`TM(35w4cJcQ1_iW@yKu4fC$?3fiPtY zXjSJFcdpTavS%Qgq#7FMci?({DouXtI-#Ka!U>RZYJ#`qVhkYyK^G_RkY<|Luqg`U zi?aB+t)W+v=N2W^{oFUw5?g@vF>!u%%f>#uj&9}VW=B`^_}wOFltcXay3o(%2s)C6 zbvuDX3NhNWPogC*<+BG4lt_lp0Qg;zF)h?_f4jZ}_d2K#=q0MeM=9=3Z+TkAOv7cupJ!LPey*w9I(}hnlqBtWl{-7~^ z8D@uaXigTF^XXpa&PWi_;7xORP{}d=~Yig zQ=gKmXB;!@>&uRID4M5%=7!KY%O|rUd)qoQUcc+(hZM=3pO2(Z98X&GN+($#{A#U4 z)2E(>_QNM_mSN+5mPVq_{_t#5=H;}C;1%ObMvQpkQaT-*KIEWbAU57BY8PtK@v6TE zDtP9|dNIcLtK6K)#Tj1>Zp}~gIjCBXAhmWA_U`4q%sLMD4^d22V9WWU8tC*FFg#Ji zjeQ+?EsVu@;2uK?@k0rUeo|c(`DnZBT*KZ_WNujQhVJ=0ZAW~`N%XQGj z?|#Dd{&aG`U&IfRUGa@-Gp$yh+w>mCF&`bNo1*YrclTrG;y6b-q3@(YVQ_^Lqm)qA z{>MBrn_pY|T`2;c6Run(ykr~ZY|9nA52QC0<}Z5NQK`6cG!*!5u8wX}mxRW*FlQ5# zd}p}*LmYX^Wq|0k^4khM)dzx+x-lJz@V*_vY`Z^@#Yg@o`BiVT{j2SThx+$=*k>hA zk*4>L9_mkKXT&!Z7t0wJnI<0o&M9Bqp~@!E;f)8{>~KIinQyvkQR{@~j)JG!W}_#5 z)9keszqO|6#h@*~qjqGkn_{5^2Bl(9W?7ErG`W7EeNjl<1w>)t^Lr=+dS-XA*h$d_ znWJB42gv+1n0FL)zKTJ!vuXUwK$nrE(F@3k{qVv$A<7NI((c-r#8*nSmL|wncVA`d zK|H@#xb}rPPA73;- zV0`s0_d$_|68=Gl(xLLPEMHB&SVn-~G1*h`5%|In$s_`j#)=J~@IA3N;&&FHz8CZV zuivb{q419COSccSf;pJ+lJ!Upgu9oruMJxah2MqhHudVVxt*_0~m_U_%E zkOj4$q_JP)yti|{U+^I|jQBk+z|RF6X9H+H0EOpGl%JeG{FW*ZE7dx*5)V(PN%maYo-cEQHkkWG-yf(|KXho_W^}vt*VS))#z!bY$+7h%NdTR+JAb z?-=6s`VjQ0lG&}x@mV%uc^~G@RI2|sw97Gn`>zd!*XlIs8Qq6bVZuU^*c|~CHHDjz z%OJ8!qEw=%?));f2Cc~SB;r*$9R8Uz>k^Eo6+U`^LUfeTW>#B0L?(fAS~(@?RjEZ` zI{yMNv=CtxNtdK8NKWXtbsF(s6U&eu3Z z7XZrNtrhV;6X|u7bZuSatSe_~BecMDjooR`u;4}i@*1!e4)=P}Z&bxeU2pST^ZC!u zwRpg4K&y1~*a4}KT%>FL0p)G79+%mm`4mg zdq()pVdKQTXO^mRw*Ix3lYn8(mK+Egzi(%j6eRsw&bh2(;v8-thaWKnb+Hs&IA$pMr_{}BYyUq&tDKWsIeN> zUxxGun2ao?tMzZ%X82d4#1lN=0783&*G-^Ko9U|1Ll z3}ql#?E4b_wrK%rP7B!Cr_))zx6JIB)$vR5P}x6Q=Sd#@f&wia7Q{@_lXkC(DDH0E z-dYzuxfY%|PICRX2De5WffT>F%Tn+9p#**w+uJkhE)XGyfu7>mTFbAr+!4>u&QH!w zv+d4XgqMbE%bn9FunZ2Gtu0katpj!h(DNT1_enuK$r*HqjnC{wP_O=S4WzfXes4BU zcga&gcQ0!Si)swM13AvuMH#VNDlhDBZJEC!+o9inMU;vevKuL|vAEwanj@pwLxDDU z5He*aN5Sn2Mh;g+0ks~s%z<7rXFto$RhcTk3mm>$SV8l2x7Nc0QzOWtb+Eje17)CeaLz54H%1`!l)GnH&bx$kcX`t&zK+*TAhb z#m0(5IPddYz#vR?l+d2^dSLi*Ho~&!<0o8&U)}J8J#Vj`m&V2|MN8IHHB@wTV%5@L6l>TV1MfDwzaKQiF@HdJM7Wkb?wJt=Pi zYi^e3BC3O&e?oT*$!S1tqwmyeIbT5qfox*<%UuD_*;6OGofni6T4VceO`qydx2cS> z%RQci<~f^W?(kK}M*Z-OQSY{_T%YX3A`gX$SgUdK5NRLXFrek1uUMUUC!}s-BS!#$ zxL6e2H`R~&JHA_U|B;%l78?R4>jEU2byrrD_@u_%YQuV%p3aUcA(0=< zCuUy9Fl|8z6j@(SNn z_Yv^ZivXpTqvibIp)#!dGUR<>gri_@L@Bv%X@Z5_)F_!QrOIDhDRPV8mb_s>$g?Jx z^VXs)_IQb>)>Vs>6*kHPBRLhc%JpO|IM+(rrM9;P+V5gFg7}ux2zp*qu1j`CA*;te zmNTz&famh4D^fQ9dv>MQD>B!Y^@gVxD(%Tze3d!gcHy^Pt67$-m5z%M&tX4nLx6mD zx5Dcdshj5lH|8Lt?fnxw1@Qy*#+{3GvOztDrA>t~&x@p}3Ee?jw-_U6UNMlqk6oYx zcNjQ-=<^2}c7m)agiw~;^@+kYIYJP5i6+O*Q$Ws|e9)5eDYsr-!HrfAtR%OoOFhQE48pAVRy2=Q?2`_FSnNPXV6qoA)6|&;unC_2YXk_x$L<*dR&$dB~X-P z_5P<`!{NM*tymsY8dAN@9ZvX$2K%oAC*_-tfDfRJQ-tI^$=TmXXbMx7TM80!9ya1iJORtE+kF4s$0hQyfS zK76N=lFdX&r!an?=qO^v3q#tHix50{(96g%ke-mf0j(=$d8kYY20?(J6 zjQc1`cIlc8lIa}|$R>f>N`lFQ1GF4Wt;PO!R_TSy_D0{7SIhU5TC^gw(AjGp3+1cL z?jeh6!!WPI%^E0#&_GMGB$)`Nq>jesic#Ct`E~}y%_+BfX~k2xdEyG0&&2HZB%ImP zAu9@)x25q1fR^gb4+}9m0mNBDVuPpK6F!u{O(#wD5)hQ5&pexP8KOL^1{7%Ry+D6< zWs;NS@E8Xiu-Szs&@xnCU=68WeA)o7BK|R;xv!8-%t`M# z9C9n$9IY=0lytC2zt6B^7S$vlSR%Gp9K%uA4vksuLqhEzszWBN`!1H4A=M9ZIGMSy zDc=RVeG31=F>1Ofu=AgwPZI;9Kjii-`bQi85z${k8ORgZ5EEMFBTDE;Fq%q?+LZq; zkjDjYC|Z)wB}O)30Uzoniy_?l0PzW=Okp@O`NPEDgxJX@IabOSh6XAP_R`{kJt(_Pz zQS5Wsar&qz%qOmG4mGnqCTG?^Q~a#)jDx;s^x&21q)_Wp%FC>|E_D}jI8rj)7w344 zUG%zhe%YpbvWG$|>HoZSX*XTp>4+X*T(gszxLg<+S+Btg-NL7~rF;eQ*=czi+Av{E ze_I)J9C>zi7|6Mo`y>L(kF`@NP80o0oOOin19ajv35e#E5P7JZ@@HMH5CtzyO?M`?K7)(g z@^LyMzj*;K3t+~#3O%n3IQQyDx(5~JFaWXX(c_^xGv}?G%HJ{hVkVeAw!Tclf@!@Z zxTb#^*XO*io_4s*1}x_xN?PC&kG{X+IC4>3Z~4tyEiSq9JvS(R3q!TZo*LKi|4nOy_D} zLW$EyTN*=hwWSn?pp_Eto9HnQQ}S1$B~Vqi10^ymr4gHf=NxOk!_dz*w{OuW;hxGB z2tG>t{iy?A1iilD_{cElW`?LS+oYW+$Y0#&J&k`HUwiJ% zs^SNqJagmBYucE{$SpMByF22`O{EZhDDk8h^|}|ls0>=z43;AlVZrU={kZI>m+XLh zILr5Q%x%{-i@0YFj`fI(K`D|2f4VavlVL)uS+xf(Yr|{U*pP_?5glvZDD%eXse61@ zkEPX3JC(v6ylD1E41s3rbE_NQvD-HFE=B=$rv&7!#vYJ~f z*XG7tN{M^rQ~7G#vm5Ac!|kr}u9wce%&k)f4J+tWKAKyO{mdF3W^Cz&y|WLor>}OI zBUggQf~{A@)>jmcZL0INY>}`Ydyr=Hz!Ds{%aEUy>>6i#FO^hlF%yZme+BhZtr#bn zvYv{yPSNw8)b;C$?cLMWMq%5CyQgi~ zM@@@N-_y<6k1&lryts&uOBT_#80mjpzIgMXqh1jIKmCvrEJk$u^Cb_xqG&CDfW6x9gL zp{b_4Hk(;K#!e=pIyUg=4i)O`Say^v#%Gi69LtHmrEkS|N7d;y8`kQ`@F`t=* zC5N2|*_3pUwwa0LW?U5d&%Lu+FWeZ4Mj|7r_(}kh1N6D_~|9~*{DA!5MJ__;BzG$ zAP)I-U)W7^Dq9%@*3yw)=2r@9qCMuqe>N#CL5PYM>|V6Q6>!ZC*JovJfq90K;yYH%?pVf4UP31yqGp7wre6CI8+4o zKuPGiSH0dCP*jL#GsxTU8>9YE;=BGbYqnMj+?qn0suELYkko9@S39Q+ zCE5Xdg+IT6&HP13>`*!g;C16Q>_l(ac5ZW~GpE0{zgafD37z*<_Xu{SpI{v^QFNr( zYz5M(ncLxU>AG6XZo#zk9xpPfF}aa#8h4y9Fmh0RD_LHm37-eAAx+u(mK*z}!2`95 z^kLm8w}H_;X}sYa`t912*PY(cqKr#9czj#!fD}-Dp2(nk*0A&|s8v{ssy>qf*Covy ztADB&h+e4xk+C%MvO(&$@zSVe`ElHhxshkJD|5@DkGsDW2Yc1=#hrT^FxW)GYi%uO z?YQ|o_>z*$8ogb4lZkhvDZeu~+`i>knU~3tU{Ea{JJ&uF4d)b3;{&ZMi?%O{PS%C~ z@`s_g9LMyq&kCia0eD?^WU5fBleOaiSp~arO1+SaIP@)NM(3*H`zf7dvB{sakgnwxFy|o^HK~@f!to=S;D*P89LE$ zL--gX`y$XcTfgwBwW!NgaH_7^-bt3rqtqW17Ju2h-A=Wu*HChjEZYAq zj79y+Gsc+U#a;6#yV_xAw(^p`Uhq*a{-$Q)urU5)`l4y!5$Un4hzK8jT#Tp)4bfxV z?lGLV^~!>6jE;vG$2D*jb&Xi^;I82&s)x=IuikUv%rwzVc=__WXVkN%d$yuI7s{b9 zr2u|M)06dxV$tLLVgqeg6=@QPzE@tFjma#K2pPz*_Tk%<} z;L1JYc}Z%vcnwX^Ykz5vV(;GWfN!TZK#ynw62u^6k^_MJ6Dr~S2f2>tl4YRv(<)lNe;=Ksu@q-K|@QgL6cYfC!Tt<=NQ;hG@c zwRj>Mj&}oP|0T@s)`^A|2IMY}MX_mY$vEQKkZH8NaSyyt57;9T5zCinPH$@3?z zQ?U^|xyp(yN!$&N)>y_l$>q<05tgaCmq}G6?I#t}Ockuw{MQA~c~h_7##e3ojzc2T z(v$8Rc<;`7pN}Y6ZeGzATMD-J^53&B!|0HrWkPcTFUa@Bl{|4hXrzV;kTfKZkdIWC5O=`BaB>`jn&^8i zg??jrAe-jLr&~O=e0A{dtKiDa$epOcG^y$YCCPWNqBLaFMB69H0G&gdY`Gx5W~XvG z8{}%-+WZ(S4qap;_$Wg~5LUk7@;I7^ z)^s>_A84;86d>FgsP{+h6p!x~EKU{z8wnfM(sTE>Zv*)FbG2x0QRA~OBOe|7au@om z?KA#3A!Q2cCj9Oc<;#J1u7hgC72YiJV8b(5b-7;XF04CnFlgsnl&j36Wsle8TZ+<% zg{&}Rw@1*r2*(%!1ySt%`Q_(vT={a-JnBB>)@PB(WMnU7RB4ZFUBm|GJAccscQJXA zB-E2?WiX6LHWWfanDva0i^;(!4Na)UU((3&V9*z{BA?`|Ot)@JEN~Sp*d|Z-DxPW2 z=W+XRF%y`k_F>2Qqn6-TP^e;cuzRrN188x=3hOkj3@xO~wWti11`M zdywG}|(USqv? z*61MplcjI<3&0fIL+r7cKf|FT?Hm%|6Y7)}#x86E5s-G2|s4P!5BEz^wpgAwe zbl{6TXcL`2+7kZ~PQ1UN&;1_I7!LmH)m6Suj5IdlPibw-tx7=}HH5ADELSm^ncY#|9tr7nX8QCeYX^eJXu( z9Mwq^^Qd&(&?}ok*`_h;8|VbOKvr~5Ya5_zO{V*{q37_t`(jYMwP#!i2XCZ_iz{Z~hCAY343;J;xY zeWqOPYyheP??eO!@d@P1pPdL1s0S@TUBLU&AfhZSp?v=*kP{YbHfOM;tTe7?&P8F; zHs0twP{t{qRV>hAxCJ)Zi2XHr%67<3W2?V^V}yR7Q-Mjyy@~(FMb9dl1Q!hs+n6Ha zC5g-D2@?VcZnxej%i&kRUA2jpQow6)DTy;w5%zFc*NgbqzY3p8|qfOgb{KgfG__;% zjYVK)t{5iLO$JVa49)f-VU-?JD`d-#wry*$Dh+Pf-JR)Jsk&mo3%kCjpDScV>*2mx ztBmH+=?bMH05hYdJ}bZ=!LaY~I()v>>SZtYDFJ@3xXpr`yuKFbtBtPnM+Su4I{*N% zA}ub8*g~~=w@@M*a#c(R+yG3M2HAG)ZcF8&XJ-ApgmK~+H&)#98^Bp*jlxhw<9&Ue zk=&{`_aE)$t+j&gcui_~HNCaIWcf{A5s&?MZBbTdN|tAjBixZOGRA32VNkT(YV(-T0R^wam(1hYldf|kpkO)>V(D1KtSyq56H$aLs_o0CZeC^;Mz#(|? zl`|&^H-^Z={e2GQ+Or*3hA!vcyr*EQCMt)`qB7nsbHgAMzfU!oNx^tMFLR#;OQ_wc zpmMJwgsruG-3B)uK68F0g>!DDrVUD2f@0+10B_JS1?(%_O##%()ZUYONaE1FR291> zSlfC38vWo6?;)0@1YHJX{xLh))o-rdm`<*hnw2ip-*@{;(Z5>FiuzSEKS56>u>r;F zU&jMiBH@wdHM08&l01dZ2EH>i((>_?yiUaN482CB%N@zIlVa(-N_@D$pFi2&kx0M3 zBG#ASnk}bMN5W`xPCwqG;yrk8l7w?7QaReRpawx!RkDHJ59ZI z;;*%1o8PJK_}!47LgvZtVP;puuEKenX7fD|M@g3YFIzL#x3;!_d>m#hSS|delQ^&n zjPWBQH+^NR?>S0oo2N|U_9!fmL~18EC%3cx9aTzzfqEXF5soztOa$pT-CAnqe6bw6 z7x47e3rx9-%If^J4I&|k zHLt%jYEsb;|G4iC%(_E}-5>dX7!0CsAdl&K@FYkMzl-Akr^Y)YmR0T4aCBtHO4li( z+>P@6UEY8}QiA{;${Y&sWl$Qx!1vj-z+CUiH*&g|t$xCf&bYwI)KcRY+dOKpA`dXQ z%p!Y~*)+*Km^xj#4~`cKOEeUrPMDPldXaGAR7u7|!QZ*JMC`$7aM6~Pf5GAgmwUnx zH=V(FHav|6bm_#txoPPN7IaOkHb9`P4=!DKygO23669}FoHDS`1wH5)-ulgEGZ@lW zysEKaHD?ADcOjrCOK}Zb-<9W#WEx!j4T-t~NbL(k14$N_c~QQrtM8trPt8-PO?~5& zrqTh15nPJVo&`XNcEbcr|LEz^DiVfF=}SxFz0{M%mhpD=e0t`CSA)AXj@7Wi;N#eW zU-D<8*oleDvY&>=zEL!W50vfdBaLw})E{s8tsGKU?|nWI<*tiNWa_!Rx5CNC(g$; z4!<~V|JS$rjAq!>+%a2dDl=CeWB{uzP$*eY6>58Kz&SoeV|PIOa2@1M)lqYup!qow zc*t$IPh1RXZrfU#^C&Gd6MeaF&Cx&nb3gy4cXF1Qjvi`5bJ$mTun$Xf73cLJk48y@z$TCl$r3YEU>MT5^Sm1Hd^S8oAKDYY zLzgEkEwWM^9#0ItNl3xOz&Me^eK};kTXd;UDp!e{+qzhjF%|}Bx8~cUD=X~{t`XML z18hAC$PdLIa*OO^Eb7OiKy@UA>@uJfyhHmNuKfn$#(@|+l@2A8#qK2dZlZw>{g1`+ zgfv3o>M)G`oO8EV`8jwS^S0?d+Dd>-f`%5w%z`3qKtbEsUgCm&R%IBPZS@Y#m$<7~KW4Oq&+%lVU)(Z$!~*1T06 z@vA}{UC`q@U`to$lgI#h{z_&w;M{xRtX?^}Pn09N<_b5av+X&W1NT_pgQKotn1Y9ekWKdp!ud#GNobmRELs0=C=l^ zw8{_DnnQlHmi8aN9gWTxe8wbWPoWX13`Z^|sLr58N5bXw*Hqk}ppv+=3%MGe+0M&R zUVD-C8Mf(bz=k>B@Q66Ty*9C_xnwAC2#7t&g5rNAo=blFa}7*3iKnj0uHyMz z-l6Z@Dk=zs$smwCWvV-z3yZf>d^lBS9iJB(nhWT9RV15+Bn4e$12CN92VcQStcaZH z1xQM9axW@F`eq6N<&Hu`*=~co$scId_@8hd~4jN1t^l zC|tYaZ13jMCsZ3wK&OVQvi|wb048@vg4R6NB%IQ1NXWq}Z>L*_*G{o>B+M)E}S7 z?CoJ{A6`N8-P`;SWle+hB!RjhARTo$XbSaYx{k#wV7l=qAD5&LjxjFe+xkp8|KvQJ z%d`InJ)QjB*!?DuWq+6ldgDZ%2I*An6F+{Q`e{^$u#|QYt2#?J`X-l>?F!_Wjxqz4%})F`c%2@Na2wX(x#Qp!fnT`Tc7rV zILO`wS-QrquA*)LMGefsS+&Af#X!%PZc!?pY!e}?QCH3xH;)&j)p*K=Jb64&nHITq zbr0)LTSLVfv2!QNQ@G;f+)PcpwGtvYvUWtUT7)Tb`3c+eUL=D`H^iot_&d%g zQQI+qk2#l|3o)2eFWZmOu*Hf;fjCu4pN5fSy!o+Uh^Adw?-(W`ivvl^-sPD0dk}dT z{IAslvP^&dm$?BV-}Rhd|Dj?WNsyVDjpk?d531N9qWcM*)Z1wYcp+#CqD$s?iv}F& zZ#^3@NI}&79H4K8;s6HJ@;hnDfA*noqTJ_D{_Xw)=KV&3s0~I~vxYnrYbk?sE|;?) z3LW@~{2OEewLn%(iwKN|kpC&hiqF1^+sLH6A{ZlnKMX~g9kU0ZHVImiN^SIF{zm(Z zks+yleSx>)*Pp0#AoCa-Vm==Wiid)yisB~6BAsHHZ*lMAb*4=|sCUCxAM@dOc%DLk z|8W7zs{(^kR8F}zL+`Ymc}HLV4OmEZk@Z7R_SxQ-Qx~S|oHS4)F4;bx);%Wbs2_hn ztmcJ`-uz9PrYm5PQ!Y5a;@-Yvfk5>rF5der;z*I4GhZng%^38#0h%t*aBF zt|E8s{ky10(4ppif9urW=g8$l-c+<@#-fgIF6Q&`pTU?SlI25hR_~0{GVV$x`ybRN zSG!c5#uNV}(Yva`62`BJ72m2;tsz6Dxzrnir^1jq?6M@l$foUOEz{+X!#KBi8UZn2 ziYmeK{g`j|+nEU*XnvQF;$fug6;DS;rPHs`ZiNRbUTtgReT*>0$cGK75tOc*;4?F~ zMw7OWf;^a-7csdQi*u|wdjS=f*TR4WhdTMZ6)6KS#ki!qft(zV)Wo;Rw-?^HF=iJnAt zgh}(4N-~X(HGCwHdKksBjp^2Xy+H&Es;wlp9#~;Nh5tyBQIbX$2}Y^iu)>XZ8<9@F zig5Dy-1ep|W{V*ruX9zzqU>dW?hMN%M_83HNtU6K2W@!F^Id;(t~Qm!cxdF9zne-QWd2}CuKMURKeM$PWKtEbe}LoP&;_1ER( z-e3eWSOGV&&=z|H(=<=|HnWOcyX0Ho52eIldUMMZ0ZMHe$Z>yRgqUdSZU6!6zSvv0 z*2Sg96}vpFhYYlh*?Z4lYWp(DTUJ$*(P(cZHw6JLu=npcp1R1N?~u?d7^>Wsqq%Gl zCR78N>H4~ey(dCXgL(-73KSGomiF%IT3HW>;5rJAbEC`o^>`t4&ORKUH)k4i`&eozlJ ziwTH!tT~<-xCSd)$zN4fTyV*~rJqT7JXQ5kpiQq zrqxk>FKqq)RonU-ID+OUvDtX+D-hWHe?YU#?I}|rf!g0c|5bd&($K(Em%-Fh*VyoX z$g2mLCtwiFf5Gfd6o+k~;s38-_RHFTVD@7z*dOR3bEnC8kP!a37$^adWRPOsc#IgyIbB4+t_Su?buFZ+qP{xX>2rZY};sT+upI$_|4Pjod0>h>`!y8 z-t$$>nl&MvBl>y&xHczRgUZQlE2Qr^6N~HX^ZoC3)^#iC$2e1U;WxYd9hpt+lmjz$JkX)>%LJdl_loS*1L%3e=7oK$2B|vOX8n0zmb7 zYjJ#@6yRQ+0p!Q=G08F^n9$CX-w^$nQ+eQ@6Iv8bo-|ghW4aCod6Ub5ixvNZJQPgS zx0)=z^8DNu`rP*XygC>fIygu!n8qirZfyKxxTGYk{b;k zrl4?^NJD5w2QL`^#-#-(u-ar*&y`gXUvwH+SBm_|LSl~x1rqQO=? z!>H$@jTKPN!x24jQhhSeHs)qISyS`OaUe&kbJ|AlDe-)RpprC{?riG@Y#ImBqXGC- zhVX+f5@63V>y7d%=sT}sfphgSIks@a`)61tZ$}%=G9#@Y`^{Rx0yKCPJBLsRAvY>0 zdLp3JD5o8QD#sGiu@s)8N<|#jY&3fq+J@tt;AR_ca=ojMeteT%uCei(y7jx4a=f?Y zI92(@ok?s8H8e{SkWo%x@6u)+0ZLyV=;I+}OR$3Ul>_6U2gxXN@G2TOuprL1)mu1A z@E#$P$_0zahz0Xv9FHS&lnghli6|ERgjia~Jaap;6cgqiA<3XpQ=f(dI8 zYuuMmF8&bD`26lFd zA)AeEB|#qYY_xof3kDpNP#*y_h&wjYD<7D29NyT*qK1`C{I0Q9*U@KMCF6pceqG;6U+^cyy9863F`Zt6 zc+Py={Ia>#V%Xk{CY82;j^6Din)qetS|o=)cwuT=Q8O-i9lnfcf3*p#@t%H{J^H*jh#d)ZIxV2E7-Tom?$C0SbtDQaR^-k`fgjcoQ6k2hTz?oqG?SH4x?f~toLC-9 zX*cZZ0}By-TU1YPHVW42iZJPP zoaBzx4o#DFKL@>^^25NIt;=EnxYI~27ET9n_*aib_e-qAWHJ*jTrjvy3m$(DgX(9HMtiI`t|s5s?ms|gyYB`^_qz6Z2q#KnJq z-fzM?#BXTEbr4}8$UQ+pugGDh4iRvqAPP4?8zqm|28*0N6cx-fg83|})!oJ*TS27; z|Ba-tN^g5>)WjQ}@ob!dN#Hi9g>%yC6xWfFiN{v&mv$68Zq(;JSWW{GnE@4UfjndQ zz#PM0A8dhy^FBaAcS>)e=aO&4j&xX(++OSCFmbx$nk_I43MK#o)gszMX5%;KafXi zo^nVzznuKqGvaxK(SoVp`v ze$V~m1PVQl_AL+ox7W7;iBy*hTrrcjtQ9l$QI$X=2iTNd&bS1-q-}CySJy?IoeyS z9~w$8m$J|5_sP#0cDwQV6J96KKQwi?M5;S%cK2WKIq!f&<6Hv-KFB=Bp*RiJuj?_^ z@dv`{O{y1MuQOeVo}z{P-G}3oiC-V~3j#$nEL3UUor{Zk;%NE#y)OhgE{l-7yPhtx;$Z{{?g52Lx5uCD z{)d}gR$DPj6qYAeIAy-;^qDG-hDR7MriOg9X$r@A)n`HFP>>EkBU>Cbq9kf0jmHfb z)$b&-nK5h!0{f`AGQlb-@2I(`skzee0C15^?nU16#~gS(_>nO6^ZbjY(>G z9qPW1T%H8{-mW?}N~kO;V+tNU?PUHmqF7Xt3w9F(l?xr?Gs&WFfFA?i8l@Ul4$8_u zO3a`14L?;3Z~Sf`uihOigFyqjxCkIQ&9eelJTA$nbJn$da7CjT!=XgI>{p+0G%46H zMpxAue|Thrwh#b<1Ohe*)zW^{(iPI$wo57VxN#t}0Cc5JN-DqcyK`A-C03nSf;qLF z+O@4#RkORXgi8rh_c2h3`gU1pDME3#)cMCL2eRU-`)vak?N5QuK_f20j&9z*w*($N z6FDdD3E5Fmnm~j%u@OXUpk-OPT~&E8`#}2X>9^BU%EX+dM7=Pu`lO%`xf#Y%zx4$l zZUrsDu529zKAl1)wBf>X@kuheXxIYrY;vwYY8arRm$Qr+Vfm*@P;ST?cD31DDo$t1 zwHLhYIt=@P(22H_nfE{NxLt8M?6<39rzE;gMu~*zYg}vIIv$Hx=Umn=3nBbqMae!W ztTodw=N4l1ydLUNUig2So)egLzGc1P+hRzv5?8*)ag(ft3%9h02R193{!T4U+6Zf}*lS!rad8CdTuMmI=4;zsRv=Uu)23bnb(= z;?EB71PIQU4}yeSI1G}b@=h-AQ_^7sPDLn@9oC}t-UK#`*h!6Uue=>*`HnnqwLV0u z*}MJD@&2W{tLhH{;SYjjjtZf@>DN)LLCrhMl?Qd5VmgusXOZGK*RUt?06((joK_|o z8h^t?F{hp`cyy-m3#tKGEZJvO6Tqzcr@JWqHjapA#6MMQm zAmoZ7LT>m@5}PY>l}}quo9))q?_Da=<5W~dcI)M1lyn9z#EH< zNJZ%sHFt<3bVJ%Ny~$7E@6o(z0yTcYB8eaC4Tuhs-XV$T4uSU4NDjKh7`hcYxo@xgTP#MZ&ZAo_%9YK;QtxFS*ED{9S6hL*^=KjJel(G%D3{3g zUOxc?I|#&n3->byNDVg+tQ!nO?5Wzwlx`~H6EI1CjT;0KXsRylWmf13s)*9muu!qC z3Xjuj{_q|dz7=t*;{#$Drpm~(EY_ll3NGX*Z|U86y&}^7W7DVm{hYq| z-FNn~NG6Nl@BQs@g0taq84?eufzP%F(v zs%qPYqPT;R&hsLyt)n5W@lwcyC#57sAGZR&$;u2EoQ{%ODS`AigApHILp;|gv zTf62w9K4hxzBT1_KXMH_cac61f6Tjfp(yJfA1Ig$t>rNP_{ zQZG0wtGPYzO3`)p-z%tjrE8^}AzoYJIw7B&8N6m{?oIW6;UZEj#DDgFpG?gXAdhW_ zx=$fB&|-3`h&&cLqrL>T=2=KO%W@{!6}7FG7;>VfV^4}2VH(6%RZ*$*DIZ^yW?XIH3q(1gjS@SdlrVeU z%{Bud`qs$UZt**X`q;>3?=z5cR!ICzX=x32;`4}xyw)T;`o+T{6Z{)im6C>ro=o!T z>_9+vs2@zldIFNuJM-nb1zQrj5QRn=YKy?kBU7{VRE05>*^4!fnaWW4zVY6r(Z%H< z=f1I_hf;@ph~Dche&Fy{4*@n96JL}JB=VXXe{)a|Kkg~BCwNZe{P8SMlhdAFtjmN& zY(1WIX-S~+we=EIQMSi!up?zo)yY`>iC~L2<<6!z zuZxF++zds&v2;xg>nkqm+|Jd`@9M5+%baYpdwvq4CZSj;t5hhfhDOfORqOMG+oo051^?g4;+ISh@DZD`{Sa%Tdr&fnCp(8pFj9zLfc=7svMG&nV4jeBn{)cv$U`{t1u>e%%S4-?0gPXayd&pm|vQayr@V~7f$Ld>@i2hk-dOcMyRbT z`!+ey8RH0-f~DU)KbjrF`H3}1K)YWW`tVR?KtMnBtV57+o1IH2{h-I%+0E+3oigjB zPG&>|ssqn2;Z5{gX##e#kWS%GdC|G=9+t=WL^0A~Y3NbB65moC4~;W5uPI7ip%U}8 zokO8Di|2Fq8t7GT@{RiOJE z*FX7Dl#drp@m>1uP#WwZM`nnrBlsk_Y?hVs1F*eP7H%e_c2OLzSxQoV<@jWm95TMw6-I@@8e;f8Y&+_Q)g|DZ}rlj4f zja$VaZwkVxqxU#Zp-oR_#%}BlXt=CF7*=tV2jM#R-=a=26W4h=+uU*d@ zvDSRumNJkqM7!W`N2*^<*43&44vcWM1IzK8JLlRvKWZa8oCV8UKl zoLhnBU20pE`G6u{=4Zs{#!40L2Q_lwRYH4DCbmuLPse78B; zB(YADdWl)W-Pn@%lH}K2NBU(V>~cI`IE0M=zFIApnwP3O+U0b(NPxgKzi~J7g09u& zzzm9#5E|WL+5rAwc7Jhm7#!d2!_&PReJ~Ewn_tnNo`>duj{Mz|y&e^&XSW;+XjC)$ zt=(sHS)_r3=SEm^pYaIydL7NSTeOJ>mD|pcj=@T)K4i#rq}+I>u)eyT@nwh8%8kG^ zrk9Ca{cG2=+v^i}yW3@UI@kLNO%;E>*X~GS!}1HT=x~llpybI}N2cESCZK!$lV&rP z|8@Ua&+Ka3Rg`|E++Db)2=Vz=3&uQCXC~D~ig-v;?<~uMvAu?@TJL7#`RJZr?VJ|N zEmX873WmpTCUp)qrBJP6AI;|D1>5Y+H`>?yFcq)u_k(w_vX;kzjnIgk8FvfO(JXqi zwXel+3cs$(Yld7(43IWMwor)R_4-6>YhKM)|1BQQ0vLybpHM)t_{!r{WBtnzSG9_i7wzl9cpIBF* zg>V~Z$dM2fL4SGVF4h!?!&IO_4Rj!sL-@vpnGbf&Tfl9);qtig`0Cvvd&UY_nsw;` zA$S4&05iKkxw(;|UuU&Cd>|-W?=|#%&s-@V>n7J_ydQbZvO_O_4R1l?mDLdEUegJ! zrCAk<4e6}^(eypDy^Ga$U^-#2>L+Q0eRGYBwtEA2ylfL#OEfZlsI2imcE0Eq>&tt> zc)>#zjQ|FTU40(BI|YU=Z%xY&Rvd8>9bL2 z-O8Tqlf$E-Y$7EPnZUa-6|VtYg@_VNqdzWno$W?2ZMYq?s(;@87c^|P$B zg2C?hnTxV>=T~?4wTm*XDLdOP{pXYov8D>UYG!Yan)9owfpo$ zGLv36T9C-E4T+_(fhz~mnWrb}AQ&MA@k4PP!8}-zb;~Mk{X&~h8$zW&*yNF*K8-!nI8;m55im#Z3$MKs ze?xnLw*wvh5f=Kio}%57X3|QsK*YpVB(!AfEIi~AS&(m~?VQ>o z&UIX%rHzKC(arP=P1k9sRcS}n2w3_Ao{nMpRTBhNb~jcHt~UBaHgYj=%2 z1&GP6$-(Sl0ai!t-<=TePnRxZ{a*JF+q)x=r_q+kQ~@;>wQM;&DzqY*vJ$NZ$J4pZ z>z+m*UDthWBpd-@u4-Rpcc50GMxEF#?nqL+1_AF8ej6XUqMNEe7&>!ZAvbeUii1bW zc{z-Z?I_-?3@5$5a^(Cm-GPh9M>&V$%4@Qzzn9K*+*_rJ?ez0U{biT(wwHk)u4Jn& z#`)s0yv#1b%!XU=rxG*8TWV`6=iOz+Xm<6o%(y;LaHOSM&KFPcER$q+--I6DNxvbMw;O@S$k&zRQs<#K&em6-)p!8j)=hvW z_6AgMdEnDE$i}Cy2?YgqVe4E~-H0&3BlyHkqq)+ZTdwby_*u?F z!&X&NMUXA77we6rABFe|M{F#j*%jOxF0V%=F~yDqIYM#w7aK1Jn0jv)`AYSA9g#yd z*H>0ej^7ssv7yV0&oreO)kn+n_xFe~cR1ECy@Dg-P(&&a@jvK&pO>O1ny=sdhDA73 zS3ArGMGzqIbHOAkM$dy%Wmbu_gRSeVwNMx@=wDgRCuHcOP8ZfY?w^)3ta034Ipb^d z!7tZYy+u}wFspzz^?2Tw7x79<^?BWzH#vRfFXAcZArgB8hsl18``z@qGA0O}ZkGp2 zTRR$$Rot)b!cFRYF43IYoT(@@M)7`ekfyaRd?E<21BTk+X}Qbgfp;TYxcj1jE?{$q zb`VO`fF2qez90t2@G#9AJklIvn-JWD*wYk<;c>ioG$L7r+~5Sz4gnjs-S5ui!0C$Dv=i0CKyNad@$KRSZU2$Mu+4DH#YYbSZWRuZU!NPpTnPiTD|n#``VbkG;`8 z5xMFY2I3|)crB~IW|3TL?{Zwx8rQ2%sQaK7??u=(H$TI3b+`8#-zlC)2-=~mr=p3E zwO7_kCf}7|DY_1ZZ~(2_`%`XP=;|9kL8tX}i^kN};1)dLXfLL0YO0)?s-zkwll@Ps zcVfVLK;C2BT1-YN8fqM_&*a!}z=5}n{CC@aKIW){n$5AmKfWWls$KESenPQ`cv{ZN zxD~8}5y$%N09bUaXTWgQCIo4W%8(vWQmUDL1{Bm$K@rx+Tp)Fwi{O*&q;-Dw&k}@J z!oUF!zO3moHDYlo2rJGEr-RLM@spN#nsf-ugU5~_kWa+CN(DwMRgVUotaKNtzY9@O zpe|Kh38hI#x3M_J-%xMc$*0xxsr8$RJrU<-5}?31FqKu;ke4M?N}bx`D+BNi5X_v# zkLM(>JN9lOqS8O#7FWD>r4#aVF$Y|fQs)!>3H*ir%1FDe_&IFVhLL(Ri@B{Iq5WzW z8Wu*5O_WVs;rU_!wYO~^viEuDF4anIL5+Z)hpBSyj{D>4ym{+B5Ouf5*B0@GiGPz?NP|N*q@Y2I| z;(es5rqpRk)W_{ba@O5ND8>T^)O3(u*c0hwn0}LlsH=4Oe~laqT1eETKCT3vNQ|h^ zRdQa5X)RvZlVALJH1GVT@VT6i12ka2!1K`*In5t^yc6XPq2uKlS%B|W1dbSnH7BBi$k*1KgP<%I%J{#te1Z5h{Pu#s?k!Rl^H8CcfT<+;D6vQ)9?3?f za=PplV>>`Vip_L$Me3otdPB+FAU55PlMAfOQ=pk0#kEQL^JnRQhO=1BJL|`JCpUsT zUR@h*c~zd;fn|oJ;R}IX-NMD10rs-lak$cuk&&UHQ9G*p8{P5CSi4{EtLeCSF(8Zz zV`zV^)m{s)e#S#0L%O%%3XZcS6B$s)65cp1f}gJ%I17a_NHP;*CWlpFjFX(u3!OMx z$U$aEPP1Ypjux{<(Ap`hp%n#baNP+Rrc-P0()GdN1REhD)as(ZR_;JRl3G<^N`_ma zN(0#_GQFy%pz665_-@86n)nn!ze_|vO9*b5t=WrXZi$eK^cj*6=s|?w;mPh~F@Lg{ zaFnP&gH8i4RkQ+P$G54b=oAgsI45h@Pk=p*C({X$ zYr^25vP4j%DTHIW;)Q7srofVMSg-~`IQKq;eMMf#|{8SH7 zCilv`g1$}OR)@oW?lVTtH`Lm#sr<6iyJ$^Dh+TC;-a(L>T8Yi)N@RO0 zd#Js;rbM+JMYtQZP=V4lN#@pF?;QE6>kh+R9LH6&-=c{}W)1~Y*bMt7_vY;=T#J5| z7?&sQPNA~I4OBqO{=Nx8mT;XbO#w{0yPM;j=BR5ZGf>2j@PyvUAklx1D)E}IhF8qR zuU8t0R2jC47&WCC;4XFlq7ph##@J4P2#H#)htg;mV0||66{CYZqpE>*y|ES!(C8>O z9Cd`~_&1Xw&;tVMHf6E2q^(ms_84HgqNb2jS*D&$TPmj+zZd$$V#-*fg)_$FgVODy zS{IeKLiZZqtfeC>UzhbrvgHy&ik!mxT&i}dP9JJM!Mb7Btj~5}1wCI&8*#-kKhr6j zyd_+p?zNFi)&%dkV%9`WCl#158%nO4QpJozH35xK#KbdTFifkG$3Y=b_Y!*_l4@24 z%~Wz`M#Y4lN=lM*%30Jk1Ev6i1f6m;lG)x`Wq-bwWnbtsrK$Ds zc?FzLAs+nY2_O0DD$~_rg<*|x4t_#WJj%?|m|mESJumOK+Kk41X$4F(eG!Q`UG_2p z+GtC?Xsd2KetVzpg7TMzVo6u#K9*j^FVfSHUDeFo@s-rxSzrO_<1lEJzdGosh{Nt6 zBAuvN_cPlcAdZmeM6Rp>D4zOXodPB$w$2sP!r#^L-uOlKlCa7M#52iU`XVx+ZK zia6e1Wi|MKa9fcnhSiPFW;;@sg@{};-A^Msj(%e?@)pvdP>LZ#k%E`sO~EAA9B(Yt z)Z0e_bc#q8F=#3$)a8MZXfyRHp0jIz4RL}>FSO^x8=9@9N%kKO9?6cQjFQY1OeY(e zj40gG0$fNUQK!WqEue(b@#-kcD`v(}pYH0+qLPHvmbR2Rwm?h2wPHz=s(BB8Ek`d= z@wdxC`udOIybAV5WEoRIH+$HYS1eR|FcyvMR2cIrke|$(r}o@=aC8PKhc;`MTsRP5Wk$Yg{C6`TV4tGEL3|ZiPA7-=A;mlN88$I7E;RO>B5y8gBZqOq0 z|C@{f0VSNaMQ(ALd^O7+-*8g|7eE4ZQRO9U3tBecg5!PtXT?vNPdgXpFIv-61qMYU z{MYV%@(u!ajt$BUtLpUy%;$fE{sO;{Kzd=&65_xi{u2FN0UHzvAPxEdZ4&|u6sZCP z9LC|lANZuLnV)CBDRJ#oYMo$O;?eFbHV<*Y$#NV zW>Elg#icWBbQ^{GMNW%9UnUYEAABIXEO-eW zw$`Q6Z8cd;W{R%xQST7`vxYz%k-g(((ZrPAoy?4jP2tkfXNTn0$NS@asgg~h|BCB* zwAu-KIo<{nO-148xev3S$cLA%-`hR6XoE6p+cVd?%HX4nb(8(|yl>-RFX5)suiHwJ51Ov$x>y}KN`<2gO-?tXvFn3^p=lgsCu zzt5G_lIhd8a|5>6I(}<*R`XBZi2E&kWUX&_e`K1Dd;ngZnAW85<*#$n0F}914{T4U zh$N>!cbyD7EMildGp)ZrUweGLo%(n_trm(mg3O4lnfQVN|CI==or4wijEazI8w@-T zJ^1h69r!f>mEZ2UDcD*~svS|ktDt0=59ec3&+qlJka3}%AG)IN>*oXQcXM+5HF7gE zGxMX^W|`kzD?ZZ!5pbFTHVuV^8kQ&N-Q4ry6Y76H%v>5b{}o+(TXhYr@W8z6lw3Uf z1(jI}elmWNwe;iBviGT1pZ=nk%Xf=i-q|_F+VTqp9be52P^Dze&)$btwT)Zp4;9j& zh5WM*-S_8%kA-jOtXU%Q6A~6nvTeEsduUN%HdFfqYN4wcl?)mtmV4$C(+y@u1$8WdVL~hk@hk4K8)>2+| zzIOBQ;D(|5aO9?g-m)7(lS%(}#Q)(Cv@k^A=e=T*dVf6g+a&+x!Dg-Ld^eIlCOXXq*RvqrkyrZ`LI8s(M1^Krk z5xEbR`!w#0>HC>Y1t78ontiN4197~bEXT1k9T%CbTVh?5#ypcq;CmSpJOp2l z%QtRGXuNHoYLFTuZ;j2mDPyqHEpp0(CU)_JISOjhB?*N2$BqRwiTo;kPcp6-788?* z9@-$7nD~sQ+((U)w4?A8tSCi9MHLYD_pbjcFjF>NPDWz&nHkutO4HU`5cA&9X}pcF z5~cID&CU7U3!QR|!~Lh#jW}FD!ua><%ON#hwu*M(19pXSN&YqWRhhxu^pAvBlC_z3j#TfzJxc&;AYkfR=ZX-Jyw>;{h+(vo&f~ z<#w(z>W{fx8b7=V8y*1ZU0PZ0F>D+l zZR-ugD?4g3w3>caS`Ap5NR-P&zPiKD5Gc@{z-(V2J4N2jhHE1P_`t zQw}W*Xr9U*d^dR)*kgZjmgSdgq8n%&+6d2vrQuCZPWZCP5Kubco+Hj@zLt0Vo#P*$ zUJ$LgkRi8i3y*j+0*@Kim6l`1d7#<07BLvj|?eFYtL})r)Hs zFyknyl_X*@@^Rdd`ZKf}((RjdG(@)#9W->^`h5eRuUjUo=`qel3^5t^*0IJtlu&K2Hb~$X5}8CQgU^lry&Z$n}~l`hBAPFLVY5 zj;S5g6)=j{hV6^h29v}6?t`E@q=>7}uwPF8p6Du6Kv#I_Q8CZM_nE-G9}jV$hktr= zhIlRAv>$s?m=k=_)z2q6k1}WS#gvv4`DUzwjITL8?@H9pM$HC@iD?nvo$ls7IugC( z`Cw<%8WLK|YF8GQ=f@FEJ8zD)OyD9@O7jYwZs2S7)DoPZ@FHmB#R~Q<&mxFA^X#29 zNNK1dLGs}#T$H1W*L*Ov;}?%*T8WW8V^^{?f|749XOyDT{WMnvfJy7mZYqa0*tC)XcTVKp;mm!$Ia?qE%tP}ua8C7TsgXqnLba`AXH#0xAZ91 z@9fP)j-YyVN!m4)O|oD7Sng!eKrJt_zm}L7yvZBg37L(Btv-KbQJ@K6J9L8}jqh97 z!FQ~qlJo`a3zW21(17;L7+yV#)&k+mkKqb+V&^d#MckN}kbAK6@_Mftx8!mW#l zpG6Gmv$E6;eCu#UZgTiB&GV1o{|C|qf`gKck2*FK?Y!B=!eSE92Dr?u;9a&n_BQMJ zENaNyU_<`6n9fT^`sqF9e7M+Hi{>*-`|@nfnMH*Cf54LDYO@Hh0b!AEm0C< zsN0DsEHa>CAHNFq%iDjv0A{G+3|?+zfJxeymG8pw7wP50QQBK$NuQIJ4%D7?w^~h2 z-YIuBP^ikI&>6?!jLYL7|948DUm=#Yf&-N-0xr4&m34J>m6Z0Z&U@y^0EPa<@i2;L z@u6t}7W8+8!otGx${0SOzG~Cp|MdlIg8Xj{^Ui4d1Udy8w@{XDfSnn_T?(ZfCWZ zWgqe{yXxcZqVlVtpoYpxp>WtJsHll)Y0xnt*kJ!zq=y{>_hE#HND>Ys_2@(J;?D3R z7#dZzQd31i!9a6h^c7Mu-u^V%6r%`bAYv*zQA4*QM$BBLiiU0~GZ9a&VyyP(ci529 z2O{;3FoPnC`@Z^L`f3sq5<;J{!fZPJX#o;)au8$CvxA~_|2-Fha*`gXlmQF|dLoI_ zmHMOokwmo$b%(oS1*eVhDZ1elVSi7{Tx~Kk8B_I4y~0|1>U=|4c=H|A&(N0fPYc=R z8d{Mv%hd)8AP|U3A&bjs2r*MZQ{8d%8M9^MZc!WbaoWbw1VW}oF_5SLDj+H1qY)mTy_dYdmC8fDK zlQDeXKRZ-PMeFvz;g__XCWhllsyiQ64-XHo&RIPF#yLN=@mDgEK7>>UKc(OElC6r$ z!pTDU2xUTC+_owTvosmS4HNp>XyR?e@K?(90LjLr6%WPhK*#~bXLr{T6~>P{o9@PN z{e|24>1jefk-S$nNE0F1=1h3dMStTMow_lp-1d~H``nav(fO*h3!`n9D-;MHha7ol z%PHrr&P5i6XO9g4H3;aZIB661=mZCN#b0$kZsucE_`W^2f@Wg$)TyI5CP=b7Dv~jh zCnxp@6*D!FQDFcap=0DyVPF&0|0K5?KG0h1sJ5vz!7($EX<~wUzK>9CLzKC22QCVt zfr_fNW5=2DF?X766Gec{e->H%DnzosfjHSDPFlO$(BRPT@z1br3pcD;x5`o6u8~&p zeh~ElmPJEC;xyiuyLVwo9+drDO;`1gM17rLs4v_O(Z+-V{-7AAUY`|5R)+T?pT8+G# zRL*mxgMtYVW+Ll1QIqBA+$l8!>3xj25C3E*Bqkae8TnMlrtd`bR7F&h;ODTaI>)LD zWqW_CdK2jmL=VVCB^O7en}7P{*4ZiQ`Fd^9TSkBPX%GmFk>b>>mVjeA!NgyrG-i2? zJ;6nUAOixRh1gek7tsb-yL00s2d~QboFrtMr3ltZX*2e>cXOF!pX_MpBqufxnp`GW zeg}D#P-QFghWrkvNI0I)doxJ|KTsdnjmh#5pG#?`OsKWhbn=95b0RtC`L$s)%T%n( zv1eUU5V^@sV^C39&i9SVnql;zl488YZnf^oQg|~o1W^*zc77<5%hCk%Yw^qOdRu(< zcXSa<>SowuXb3?J&>sWq9&zn@uH|teU=hvuU`Dr_KL^d@ve;ws#c#iUCWAv!7qu-f zfy^DF+R<7Bsi)RI8A&de4~N9W=Z2rmk0x+Vle&SuczdmWH`mBFdka3N2-D^ZgLbhu2 z$XF*+rxic0li}{6mN@iMd&Qwn3G!LXQ9|@fk$Q*Nm>dAh|A!L=35o^m4q$p`T0U6E zR-q*-*Dn=<9EP3QqX_^WH<)9i-;o#J`kWFnM**ZPd9BAFbgt@I%vfjiMPI#zn=nAy=Adt(Zw}H?S471E2Y#J zFrsEe8rS`FI7bfp`UDpcf8(3GBZ;~s`Mltk!(CNFX`7@eSmDwa?OM%nV|Vi+#d zOK83|_B@M&>L&9lxo*>~q(rKZ+w39|FTWXB_4}_NuefiHx#%k$1+1``RSE*^{2oX_ z2iL{HpK7IzWDnJoXS;GK0)?a666I`vZ?qj1*n68CXRlfPeYtG#aD-0C)nvo{hZ-z= z^$GJ583Wb$(GlAIWJzqKH#*j1rB|Qbc2uxSgp<=Tj<$VtL%@`N&+UiS{&g%x(bU7T#{Qr6E3G1??eK*G&7h;btSvxw}%aRt-W0PZ0PLJ z@e3{|RLS*?qn|b$_VZE}D|NY+Eh&D9Lo_l9Sf4Jfyg-jfvDxhEUz~V8l>W2}f9Ig^ zAr5F!}+D<2bTeqjNR%bAjiiHVC#vwMfI@=`J~bf%bE$~sj?OG`@2t=-~R zNeLwhNq$?~DlC~i+JAwKJCXg7&~46>*F{&NX7#SuKYrH)Lvrt?R#w5vx<+Gc_1-Sfb7R#S_f3?vHk$`XgAuHxz zzp3 zHvBKB_ZL7S+U`&cvhGbFS$~V^&BJ`5kp5ZonVO7(fo}z|{pM;`R z8Lbw*;JA=WSCWeG(x@&;qA(41Ci&_n(?*&tIoXlPy@n@QdV~0dKICzx(6Gos==*=M zo&X^SlD#9CB}8OpAUYJ;C*zzRH~Z)`s<^zK^O+rg&H}9^AWDicMpYDNjWG$1)GVUz zvCy5YfALTRtOcV)%+WwZQ&A-)N!xm_)(Jh}n5Ix`Q~TPZ)JtFwShu$Jt0&UzjCj;@ z%0&|7WRI&9B#d~7NE4B@s*dBDQ~nRILVN{lSVX7F(<}WBV0rFsURZY?$g80{Te1kP zhK)ukP3Lksi6;@$^|@b|pPvs7hG2gqU()Jp;vCoSpL8tXkqv9mUg=XJH8B{UXW7oy zTDb+8+h`H&?^hCaqoEjDDymPs30$hq(elBTiLaFAD(el0^Ehp>36D>-k(f;po~$^7 z0>4C%eo065Eyd|qzP}b@HW4&Nb#RnfQB)T+YVL&Y6%AdFe4b<7dXupf1yCOmwWCKb z5SjY8S08NE%+&IN{deiRB{5mZ9SWf6w}fnb0;ler zr|sPILpuZ(JB1h}_@>~3v3-+of0*(!^PZ58vokYsad6fw&rsn1^*sKuKDl8%0Vk^^ zlUbWmQEyYZ`A5PlfmeynB=8b)PFJhO4+U$k@*3vz25$;ZH}Ww580c+gusgS!DHjD~ zs0L`TKKD6tnI9(k170z8HqU`Kgc}|G+P*sS8?-;PYMc*g_~F!bEdzJ|Yl#Hm|H<&g zEEm-8cDQgfX?n~~ykxK1JWE4JoyGq~_-G=&(?y*J0sAlgDh-(3881eI&Z8!~?XSH1 zn!5~MnqD=G3~!8vjS$06vS4JeN@M70wzWKV?^*>fESXx|9WE?j-&Z>T??UU?>w?ef-j}5-xs-~pM=B<4s+Khon&c_@H*0^?-mU61Az%q7buf5th)lbaTIsupIc1D% zH>tWw)%9hde)5Fn5 zM-?7@Dl7MnrM&?YWUr2Mu2Ix5$E%2SQ)Z{aK}O&$-#hQ3V>wC2r(7R}YIqXNkB*j! zj`4d?p=+bNUhB4Q-wyBp$Y?na;{L;I{}vS^2e)!_$^EsFNx;0y1vxAtibL6@MQsY7 z&Q#P@uH97bxyttQ3dhQd~lCFYfNa z-QC^YgB30A?(Pt@IPCQM);ecj`+u%UGV{#Ld+VEh<^}di_<&h&akR)=0BY-ME3ovu zoiU6A*O0YMC?C9;zC`$;;%V=t`)?H^jA#e*e8Wqw>No-3ao($OU!xwY*$Y;;-S)@? zoqY2!fkh8AMs-d2L@7W6@?c5+`~D#tYKv|`S|D1?I4wA3-|{Y?|KUyroceR{SwuID#R59*RW zjHSG{-9+!TNAdqMDEP-OjRNlvJc5%#N}b#9EJwuaDD5Gf?9=~@*Yf@{NcU9{Z1AehRVU+*iCM_RI|!)Pq<{-C>~f&a$aJX8v+nEQNqffN5FO%YA(_u*R2E zUgZhG6`d#8uv&n&;dZ+{J7rpr*>&Gr{qa=yZy z)|A*!*rojfTch^;K3U25z#*U4x#g$3)aR0_uPjGl#htN{SfiqsuS6mHY|+{$i3RCV zOX&-brswh0MIri*f-NQoqmki@$Zk@P?6tO&74w8Q3N})*KxxFHkFyHvZTmYW>0`H- zuWfG$uUwF_+=JVELmpn;M~uXS^=|}-t$B}gs-11rT1|3bx4EB1@g2F+>){HLTh2@y z3-%lTU5}cv=vO;lmD}z`F+tZx$^ppRKjE)Rb$=+f8l!);N4`i(Kza|M&G6}$VPP}G zxkI;po4}FB8UHdUC!_u%aeZ0BdDED9ZP)d@<8$tn#EV2%nN7DECD|SPj!^vlhuj`8 zcHqLqVM3GftKi^^X?^QwEi2-d40iVqWDWu>1~Uemqj$~YUE6{iY6v9XCuzT9Y{n-M z4X|^4Pqa3yToshVg{GXVeh1rB$9cCoE=+6;{wc>QTv&fnh^y+b+uoPZDZ)zf2E49X z)S;_C2)5t1`Ejwl`3>`Awpg$GTg&0#yVdEI`f1kOv!NIexojNa*jU~iPFc)XEY+Jv zyF~w5>)~2OFvg=S^sy3m8=6dP-Wj{15fw}=VA#dkNRyfO`7;(5I-@85Y= z{dBwX&M!FuBf`smaH05>U%BZA0qS`Up-n3&k)-2S@enurVN^hKA2PnX?socwepo{{ zV!jmVZ`c`#&P!~%SdD=D0jJqk**@-u+D)4sPV{B|;%Qc;Zrxgg zeyIOo2-N8LaP@CJ3)hTsFKTFy#jtqAk@%o-GJtpCWNs9#D;)NEToPdAUA-bj@V|-;0&uc>og@_!0WLD6RE2&D79T-;k~ha%I0sjwm-KAfS$j%=GM5e!CjS zS}=EAeHtmZQ1d2iH1SV0CdkUn_P9te$>V8G7t<*63)Z8vgLp636lC#x$Yt@R4$e>I zQ@i9v{Uc28rAFjaGn2Id+a~=zYJ`inmL-vAv%dre(b0V*$YK1@e+XqZq;%{{jIGU# zQm@qqF#?!p3jIHPCKl+g@X!u*yf!z6H}?Nh`Ntj^*H9It|1eTr02Ku*v|8h2HP+f( zT>Kfu2Pk{{gT$tptznS9muc{}&8P zlGQ9)A?C1E)X14e!bS<&76p5QY6ubVHV5O1EIP4Z2L{T0JT@41* zR%)?+uhkNRVWW}iN5aLHIofG0o~Zd;%gig?FUEF~Opv0Q1ToX)oikBQ%@(#6wPq4A zhk*mw(qf{=V4x!~cJh%z2_+ObN611zP)~PGVY=zZ+SnEKHlOgxF{B_S?@&kfE{+n{ zKm{lGFdYk|$goPp7Y;or&+36iEqe&XAB3`QC2l|DluEx)^rxuE!A7%AEG88IY6hvk z3ux0b%BE2~JtBwqo8nRJ(~sy)xug6rlJ+}gD`24V=ePQiM3;#8QI>}LG;CQ;JjO#$ z3OB2u+qNEwoCrGzN0eR|hjfz|<|`w_G^FhyWUMKq(tppc{-B9Y2z;rY`ICzGyuh<| zS((1;e$E%>y*2oVI;`8i8Wka&SNiOE1)_uo2Rn6?D8Kx&fE8%-7aBNUP@sLjGvlPleol3U# zv*W?#zKL~Lod)xFc53z~n2mLEj#AODI7q$C@Y>xg`!*QXD@{x(LB!y9F#irM6iyM7 zlJe^wck6hKgB<+aS&_^?ja7f~1Oku!{6xSmDm9~`GRh8ACAE@M^qkabXARHD!X)rdNh1@g^|&J*};7^ z^YUH9>An(Hu~ox>D?d)i%M>F(H^p}zvV{ssP2%r$C-}e51*`q<68|4*A`Q2j4EInD zD)UmdOzhoY!WNhRT9NaG@BhZ@#H)p>Ld z1O^-Ke~$MX9bF&r2*1KaBgf_a+AGxwp!10T+9^Mpecs-+R^dMvRU}r^Att%cV4x21 zteMhB4mpqz6LSm+Yq#Gu4t%@+qs`6j_qm`fI$QVo6-ITs=0?IApFDK5Gv8FM3!K~; zdshCY_8b{E)L-MrtJjC)gMO#@kqnuVRylcDZGRD!LJa3{U% zZ19HBEK+=zw-e^Pen1B2X!)lrYv!Sz)daM3I=WU@HN_3VW6sRniuYP*bFyHa-eVrU z1|DWsufv7OL9}nA(eSAD3Nah5Is)(M_Eg{W>SDoT!MF|<|-N&lC zW`yX{(nSn@6}Bd&L;EEtGsZu|ke3V^8yf{*Zm`DodxOzP<_e)?%M{n6in5c_?f>BT z&gz?$9N5ROs&Nu8@(GPu_6Y%xtKFfwH(=M%NTs9a%Ii4I<3Qz1j);WZnC#y2;N3f=CY%b7Se2UUZOocWFOOF z{^!FxVN5s!)J)4F?j1P{{>SmX_O@e%Wm`D-tKmft-$5^_y)tcH)JUFVy-Bj(W9Pk5 z4odg!ANsveqr@<6YY#i(zJ`$M1)tgbWi404dR-N@1dm(EpGC z(b26N6P_1`NBfx)i?!h-jgkLh6~?mY-i4D`{Uy#F_Lq*g$E+LX!xYCxB!sI_W6-GN z5tVR?2t8gTqc0Ls;lQuW%LkVFcT{ycFLIH1Yf*7EzvSN)=8By11*M%CJrJ)DNiKz7IydMBwMxh5rAr zeRfUfPeSgCx=`|uMf-^$^CH=sO7$7T3;TLV1B)KUbBszT z(DCtwBg&Lz9S`JKwGDU&oR2@xR-*A7VWSXkIZv?0=R`Bw~1VP+3J7Rlv3R?B1@=eT~qgll-jU5+JR&rzq0 zTDR$_2Tl?8_hE_UWn45!v>>=^1AqkcWv_p4k%L6##&81{X(bG%hg9UFcs zXtLDGM3*F?j=w_lh$bGOj(Z9^#VN8$3mmV>dys|j1``MgiIY-($tjWuzsC}*ua{4h z8Djpit)(_((zv)h9Hg2;AEFYZVziP%G|?s+K*tp;VWya&=3cwc%_v(q>69hSj-(9V zZ?Cb)!9%-ei#_SY{01VzUch1 z>35jye4(U_`?%Nw?2u|{>ib0cY=bqGuAJp|_rw$7>d(uSL|d=)OA=H|@6U!s%Y@pz z8mH~-4~B^DRJ0wQ@sl}}7WHZWjh$s{XD$}HUY)u@@Hb5@Xs|@(mM!H^pzidI)|Ylja#Wk+7@RZiyHd(L zhlBBY(+O(TwQy*AQI?1KIxFx)IAQ7GP6YEjCGbOeQ4|Iv)#I?e+{9tTJSiaJGfpwm zqtTyKM?6QP@Tp41QeRiRwybzc+LYIDeSP!_J*dIm)M^t^P0!mh1`_}WJkZ#))+qn| z_rvru3~VL4Sue7rI-JGp^8*waT+A*(vb*f=Mbc-L@EOnq*#~mR+g$EE%`XmY znWPL&vkKDM)}h4s(|OwjayL*EP6n@<1u>~^iJi;5aa<>W4>vQGQep=iV(aOwm_?~( zE=gB1xI+HNWD1lUrl}CeR*2beSE9hQ5IEKyqGaX!?vmzCg77eax)%i|euOMg#xn(icYlKoH;GJ-Q9H>$3b(=_2%75 zW9nDl6Vz#982`KMhprv({e1DUu<#&SF@VIlqIUA+#RpI6>b8x>slr%?Z&KB=M{^8& z51A&*wu%GgSx3n|m^l4xD0Ln&fu@aX;Q>0gaDo57!Z<1B>pVz?lvFHg{Vh+rV~^v6 zN$0gvqobIky+yeIuUJ}1SsTT;z7B=+j|5-c^-F8Av~ub16J-xK^WS4m)033#83Ja( zfY@&PvZ|vMui}>Qm=3AMYL6R;O&26{;Y#+CSgnF^&X{&UL%)8B9z6g5C^Mvpef|Bd z_80=M_dEZT#o6I=<0C`o@ro3e%Bsy^2P|AP_h#4Scd^CZbHfC4SF>@~w@|AL|NW-F z31}5hkRQ@wJ##EjJD+=lO9qWs{6zxwD|J~Lmf*bGJ_XkvpU&nP@8 zRc`WnlKp12(!OLg-ln8Kk+w7@8gQ9O`@La7bqstu$$_F|L<2L zAgWzNE83b%p8iRFw?t1*S$tQXxfFfY_fe+MJHCAL)WE{bX^3i^3E#GbS)1|Wv9rEC z3VO>t0VUd*uinE~bUzvZrec|(3=86Xf6J`lv`B;vJ|<(5Mw)aoRWVj-PU~sB8>msV zQSXvU%)<#2x8ku#njEkiAXW>2hSOh6c(eipWOMbZVL>>j6tg{#OHm}vN3Bu(R?}G2 zHF>ziVedkk`~PTy9b@5_u-Hr_XO^ZJRq8Aj-f?`y_ySG9kWhmdK(~vA(J#;pL>An+ zxA8Gbv`=Xgc(?nO!J5k8W{LvOsA{)0neTpc^UnxEt)-gt!P&-Zq-(NPWSAfwzpaSI zC+(!evX@XA&|H_#TQITvU_^Kf;(VxkB?CeCvfF0EF*#iK6~|((I}sd%io|&3y8CsU zxsU+@&D_!7LTA%YGH{c0Ea(b9fAIttng8&Q9Yu~K1(+z<)9wW6ovnAQeDJGOGyd%h z%i zKd6E^4&P+CwCztIpu&B(>ToccYB&IDyh+un(<@!!zB@F+cb~FF!l=*=ZrH00(fc_D z+kK@^3hIzX$E+A#v37_Cy4ob>6J&q6$J*2U3qkw)0Qa)Y1>3|vjxPKXh0M6>{~}!D znR5RMy#i@SdN|dH^HQ1N3KW115+&_`Q4l-`=l9k}V#RK$n)L_0{P&{Rh@WwArpV~8 zhGUcA!}My2Y^au}lAULarJC((X4Kn$x<?d{h++!_36hX)h0bJUDa~Szl^tLD4>2WOJ@0TEDwH*P^(3)!;Cd^K#-Hj|AIHq z{};RoVK5F^cj*d)IKm3FcO!n?j2@}qDTOXRc^MfDm=l53sFmCY#18~-#@oq~?q~j5 zv9vRxbo>R8n9=zuPbY5#-UfUE99a4zlwK}326u*soq=5@{Gu{>oWsMd%BmrgBF#_z z8m1cK-+RS+i@l#cm48}h`*YS(1ZB@i6-}=QJ_|np8((@WIS)z~0GANMU>#o32u{=G z6+ymP&1~Uu4K}LtH3nUql67?_>9XiB!bzDVpEC11vQn32-xy5nhD>O{ zP10$S1;gFr{j76WMxaDzcXlRuy{DVXx7S&-f-_qW+XGh97_3S~B9X_^gZAxr_4+NS zOLnemst_oFcyfYqkl}LxIj5;*nD44_*=aM?x=WTb)%j%r3WrHsdg|@h45t6Gl*M}z zk%H>a(6_Qgo~enAwYMx1&goxh?QI)WM6-38F5>p7>?zlAO5M(^3;b0suB^+uyx=h} zq8xRl@>HzP=HSQy;e6e-EPi;0lH5L$Q&Bsbn8Qy55f8&!V(G9yG`hxErM zAc%7_!-1T~IVTQ?cd+tA~R`R1Q zy8Uv1@a8Z>zza+Vn#)N%o{H71F+WLnCQ)MoB&}ngDv=RK-95(7{wZT5KcC~-Yj^+C{@d?!O~WNb1vG+nd_rQ^z!$N zLbr4EjjfF<=-2ys$Vk_r;JHoh3-jOEfC2Ruv=n7VG`JDJ?G`G4^T1c4-Z~pKC-Rf_ zX=e_(yZ#Wp)?a~BEjHNiI1ZZX;o8uDNJYWV##p>=hp&;~y|~H^%n|)v8N3~ODd>p$ z61rzSj-GF@Yr4y-19ECitAsunXIQ2-!BE=LUM7hV8bP70g(op;`jJ>tQgXi5Hj%>G zYpB@+Qq4b+a7l%x8(v%iE5gOBziEo3(4PF)+vm zHaBz2*dPCXB<+qP`Zg1MI;gpRlxGn{^t_REo82eg=lE>rabq_oMy$fV39!6d41iIL zVlii3n2zS!do>pwtR^n?QS*8;an`btS?FZ!I~F8d;$2jaFfckfJYQ@4Y5JGssiru0 zsvafK0A-0;Gd+R{Q04*{be5>Ofjtd8Vlowf4jWO$PxS!pmElN4QFqnp%hqsEMUb+e z7KmJsv2S!fVbU19?_;Qvc)hben__N|CWkMCKVzs$3y!U*|M-zfv{k5v?*+4i6d(}t zX`CJvE<6Hm?R$4!9M~L?R-v-&JuYAGC*z@=%2dlh!M1nsAN zrNt~@pHivY_5QxoNt*PxqL-6aIj#*Js4ud(a_}8&Kw5Cp)1+Le3-mo(NX1mQ6%_#bm)m3RFb6;sV=99 zI)D0kd3+|_0Sy@vxxP`{mdln1xW5Ty>i}cg(#4x>-o$w>Ubnk0MF%J|Yji|{{=S$z z{VhAwL_;Pd?<%HmrGscPfPu4kM|!G>$uTWV0=r2!`X8FC)s3cyA@gJh&!1NVEHLs`36x*Bls zR<5qnOvf$TuZDv@ViA~*{&L@f2fv^6%89)BQGW&xv(3M)fJFgF05EqM#1U}6-{)+@ zWHd+3mwliL96H)L)1jptiSPfx`yn>^w_0qmEoG`cM%VQH=hk-`WZc~C$%{G&-;AE5 zpt&GzR*+bG7$&Q+cn!H1^j-0XYfUO$t;#Zkt9t9Mt2mu;%{<+~!HT7G>10rT>O*uB zn@WZl%KAd_o4oQNH;3^^i=8I1CU&hiKY!k0z4_7fpaxg9WQOPd)n0xw%Uf?xpbx;+ zBfIKDGdum)-L$Fcr||4IfLDUGZspVT@XN2u{XFmuzuSGw9KlbC0juUoVH#Yz<3B81 zE~n)exYt0$u-nLEb&9W{c1IT!C-RqEz0-+>ysI)Mo}tfH=F%k87{zfXwn4XBWtG#1 z7B|ns#%{?!Tdl;(`@>Rq1Fb#85NVMcj(5v8l}hT~3=9VR>8&RO+z7!BWUhdX|Vp~G9l+8@h*dcQ_> zsH5-wBbO5WYoIUx5Y%BjUj8K|nN9=zBmJ8dxjelaLf57lG@emJ`E^*dRw>ot6wfk- zzG;dLRg5n_wH#Z+=sGS8kC7CqbhxZN!#VXAC}JhT6P3$_qzLb%R0E{zkR8uSuGcAFYC6?rDZ^Y&(-Ab z16|$k!0wL}M_$-%LmRZ;bTx#2JQe{yE7JykJmTTnn}P^3ylr&NA4aomXc>o|FS&>Y zp`<&ih zKsFDgD5}1`VZK=sW;)%=K}Gx$$9*~h2`l*yMvqqB@U7`WT9G*h#%G<25*o21(Mt9v zMJlv(5W=g;J4#|*syIVNK4UYbdtPuZK31M7+RAv5%_# zY4BI+yU#>ddQs$Dmxh)jh2snG#XroA@ilwmOr6#=#ej zzkNkm?@wvOwSpm^<{mF~EbV5y{pno8*YWHcnehy2JU?5i3!nez5smGydU4vW3?{v6 z`F!~ggy%b1<|@j8-TRuL0WP_a9<6dxWP2|E7BC)}U8%{Z8d9Z3xB5WHswB5*ayqIzE(oI`7oEr}vC5}CQrS1x zHkI(&OIPR_W*RK5qFqUZ)RlLJw2itT)rHCSyA+e|<8?awX{$SfdQ*~pg&6gZ-$2F) zRcaKfY1(03K1R9nof8B~;pD!is^L9^13jrI9uS=Y@V%+b3i!t8#I`G-WMD_Q%~9DF z)%}SS&|2(tLg{>_7s~_?bim8KAoy#Ef~Rjg?)POaJ; zrS-0v%ky_FEV;-^AO&pkcUG?Q?(45k%-7aVv2GnEdJzz$ilfg7|VR>%Gi$_KSC_YYiCQ9J)D3ZME z)TZ!Ze-QxGfE?OirsxMh;=V{NKTkQV?IFUg< zQ^2b&G;uXqAkzbcn1ng8UY~D#*x$<(EdMRR+p9oBr>mRod390aP5==!_h`5@&D6x* zooDqSxO0bz+iS|bBVTwZc6N~$=vDK5(r!KNpN*zl~K*SW;F);?xGjXUl0D!1y9sqIYSQv z7n%a6NfQ&Kxq5M0Z>hbOAnAX;iGTadd2_7lGtTXp(YXoxSm68d&G^R;22zC$%1kel zZ2u3vo@hI2LYP1Aec!DG=J`C=?lf;q(-0~80ooJSMH$!ynf@AXKVr&63PLZmrjy%Ikhbd)4AuRzw0BSQUj3h(%K7#%fr(|y(yg8e0=bi z``@K~G{FX@(dIGmjPot@3VNlbr`rb5!rrhWDvsqkr;rJ`U^)xtwhha$Oc4dGnX<7R zX6@``f*TU%*$T%feP&GuGgS`Nt%h?IxnqgKAY0ISSZ3QC4?-Vf&9(a{0tMxUL z$1fX!^rhyH7rpy|NLCGJ1BJ;-U&x{^gnUh8Tpi&$5ZU1tz4oJh3OrEP+~Ah-YUFHe zAPvZDXVW4sf|HCkxk%Ahb?`ExofI&BzsZGQPFb7RA4lH2_U9O88e?Nu#`#^e?d^qu zKxJ?5Ym!&Jw%s_r=Mw~;hsbvn07)UzL5!ANOfV8>*G-(&8raJR9J#&JvAhy|?#(vV z6T~NnmO@J6CP)%;)+Z*Jeh@KSeS^iIat90Ju3y}HxZ+Bhtz@73;-iyS zzMT_RuFj^ti(dJz#=GAbNIqwIR_FOeK-F{fqE8<*b8lT7PbI@A9gn{|boiMc&*?JS z_meVY6Ry?n{ObBxG=;9t{1`YHNz$dTLRzVHvh#6s_~6x3*WjZS)$8had9fGpd2(El z=D?~0=N-`1;N<*QgO6O_nu|oG#tzZUxJ zDB1oh)}SKH9{rWe_{StjwY3T$Eyv7wdB@LTsPft4z1y{^ni&6!<0$(Cz`~sGli3%p z^LkzRD%Y!{9iQO{LV@j!*O#3SS$E@qc)J$sY!1*=*}BcGi@NMU*E#o=kM9}3J2{lA z?&Y1VgO4a>%HQs%19;b)SY*{_nx0=Qytyws^<65ses|eVWvg*U8@=4sKSk(q57VSh zlc+nq(01?|=4k23j&G!^C#pTp8|*c|;k&&G=_Ksz+Oscr~32)}~!Q-nCzV*axYxXgZo zEWEku{d)IkLklt{AoEy{>`HpA9+lIa`EDoDp|zaoIn%R>b0Z-&^XbsT5UiwtZ8_Dt zkk^Cnc@D9hKdB1O;N>!SaUaQk-Q||FSmUu}49@Nv|Jru_P^^p3nz9iHaKEz#o@W*( z9Q333+AC%G+&y4tx!tt^F$#UPnH2j>R-|xdVZkipSB)Nlf)g__6wpj@WVF202;-<~ zep$d$C~PI8bP0~&fmC=0uz1%Dmp<6whLfyVi(aD(&X-fPoj-$n6NDTp z%i!MipMci;rnk1j&A->KZ>dDx*+v|FqsqS{4W*TMJ=a?_3!bhW<$mV-O@BT5(K}+S zFwevylfCAHL2?{^{wC}Gki-m215d6d!@y*}Hi6$-G=u(~?noGQK`LD!#ltVfgNdWI zU)Sp#i;0!s@<32ICdq013mOLSEX$Dd+qdcC8R@c8i#VOs>Ga~2lh@Ac+tQBv!e*1( zEWyo}Oui1Eo#PSH4zW~Dl<6Yw>(bfrOcpug#`X3j-Ru<(nrLm)#v;p92KAK(gWiO7 zmFk7rBW12iohKBAi?QXo^g|04!-qPhg4|*l7Z?6nM71T*mw{@^(9ddampt05?*AfM zFKk>Ue`NZcl$v){LpJC}^h)_m3EQm82W65<@o`3eCdjB3;L#W*7Gl?oD4EcyGSH^v zH!7$prEp8&QgVsr{YNc#u*ap6N_@cQmZA(T24VY%(_tSgttJw#;Jomw-==SHbiKOw zrm&K0Dc+RYWb4q#OJ#e$&ft5wKbu~kiv(LdX3xEpVZ1*%ITrJK*Z)n8dAyllzKvMH z-EesX%QSW+JVWe;)W>amS>=ysZ_kOTQ)bI;=vdw<@65!`oS?_1rjExo7KLtBS4*6c zQtgBPo>DqZMW<+kQMPOUgBD4V>_XL#z%hAAwB76N@lP3CUVj@V)Vjuizp6YvZcS%Q z;t_(ZmdvR5WBMEIr4z&dI2wln_r0c+jQiCL8joFqVzo)l_qHoOU}~Iv5onX?j7Xe9 z54YQTbUNb+;`z<=p`9+McfZCK*YOl(rLW2oZh%ilQifu0D}O4k zNU_nSD4LM(s@{fM=w!i~h}+%*o}{OdB|78lIiJ5`O*T97_g=CZ_38XCZQP-)2OFyv zTcKxjnJKsgg@{Zt&P}`)^I)elKH`z{D*gDPb_L<1K)Un;9B)$HN4?%pX4mag{;mZ4 zHuX1WRgj0XgSg=Rjpgi5+72(+Ja03c6`pS}{{2p6g<7T$3(rTnGHRBju_~_D%XcNe z?;w?He0%O?MvS`FtK}Fj%wG)P!b9ZP4%Eky8R}KYSSu8CVr7G8nQ0Ej_9A(R z3Jm(dm;r&I_*G4(H9ivxJGK$Jg>=|b7PQ9SVmWbC9B*fA{#-w8SNyhe%F39 zA(xyKQRr0-{s}A!F^0jJ3Cm8b7s`!WRS} zK@Av`qbB(V)22SH35_$AD}_D}vdh5QaC-+)@#b2YL&AKdunN}F84fakOffqV_!3HJ z7!-MK&&;6$5zpZ>iueFNWYkKh=^8z0sCvUrNL$1>Zix+x+ucM zzkL0)bm&Ca(A&V{sV_2xE%4lXdQQEqY!TXG%v-h6H@<@K zqrJUXMEF^}+U+KAsQS%qJopNdcsLV81O%qqp+4bQ+@a<>(6E3hwb_2Xi|04FN-iXM zzUci>h#AY~E%-H#QuxkM(t;ltzn4pNJ{0Mz@V*?WdEBj)vvdJZN5yPQe5ymDpg~N=gXjfJ zAv9*d)Ag&Nm%H2&jq|+k{{Bh!ybo3piAK^?Yi-j)v5_TExyiwFiQuS~w+e3n+rKHI z!Cpn^L{UlckV zmxpp2^1;of$w{%>OY-aI^{ygKu3F@VjuHP^rvX2Sjc3afi}aQ;?!QEGwI>s-b5=cE zizHWQX*Hy~yNxXl=?eVqk+Am>OygDOb_#!2?L>n2ds>2pkGfEY%QrLbSLsb4p7b0< zV2JC3k0b`;Ky}KK95fdjr&z*U&sREH&bTr6sP5c9IAn@WEjG_w>e>x5kZSU_lbxeZ zMXSlsY(f@O-9KReiD6c71B8p`!?&!Nqr3B{%ME+VsW zPIVSZaXhlIF)}NaA(2rFK)%M8&6C*Bgxp|1sedp{2vPEQQJD%!|C3>1Q8b#x z#k?kEP>NqzOo%H$xi?NKyIX!>lB7yh78ku>lPJFH?Rf7)Mys4FJ{(9H^HSM-fo#7>?F_ zqQ7cV$l;Np@+a~NEE}K$8zjYXZ0Q>jixTgSyJ>j-%_!*kfpJ?{AX_SG$DkCFSYk{` zFW^yArT6)Rz6JvW%9wiN-RcbAuNZHTtqw&OG0PPNMa7?}FKH?ETZg;ChE>v^Gc#ay4l{g!EJH*rsMTvt?la7>mBZ6tE@&X?Huo(&Q|FYm8gl5C-CdGH&n3qEm>Jn}H~sF3MYeu&Bj|?=1Q~=3LZSwz)t1%m zY`f>O+v>7K`kFz&r4w80{k(wU2A%4WT^Q9KKXIy~y6G9Z=}&*kc@%pl_BY50w#R?@ zl+3CQEjaFQBL~O1gotLQbC;+@3`!F4_oV6D+xEwBQ;0#_%O6Lm$#a?JNM1jo>_y7B zD5~InToE00=g{6hXwr==>~`Wr`QqrnzgiZT;_-NC^`8~87>g&=7;q=ll+)Xb#w*A^ z6k@#6){K)~C_I}m{<`+&Qf=iST0=ALe^~yUnH^Su-EtgjRsAAoB5OUKXzt$aJ_Ig0 z-5Zg<4^1$JOo6FLPV4IegYH3{)gz!F%3fS4% z3EVYZ`h6%s6Lc(*LltGSdthWAWEl5g^O--J`I7K_qWcD=7M`F@K_Slt1(dxymP>Vo zC>}0QDR?}UJy^x5y7ynhq~m%!1l6Lj>ePlvz|)mhqh|S?@qa4P77@;ZMdOo1f-`O% zYqUdQ!)(;QTT*VzZ`0B;YPA~;#TCjNiH;Ru1=Bws`+Y}S>kojPdfJTHT0zhDSXt}x zX}sy!JOnW?tn>aSeEFfBOtU+s8r@^gkWR`RTQO5ooGWg=K0$ z9~kfWEvua#n>zw_D9&}9)Bb9Fh0Px9gGzh%%0OjX7C3(s1i(U^q`Wi>@wvR~xUOa= z+o0L<2J_DyTPO6bB4H0!($kQ-$gV^YjoUMNJt2s?4t~5mz)4jrIiblS1u(;4v%oCR zFoXfnq~3C}>-)T%R(|`9!{-f;RPOCPdfLNIL&(_JKuj}4*ne7^I~rYH4@nh^*2=mq z0cVikt4JNf#U6NT-Ln@88`foGBwo9xB$SFtGR&7h^&}TQePk~ZM5I6%A-VZcYCY1g zhw&-X`^6%BX5W9Tw$|zTAii3mPY=3q+*SGcGr3Cp_EIM|fYcF%0~XsK$G9>44apHH z<^vpT(7RectUu9QV$R3&bkohB8SAt;$1|kJ&QN)#$^#p&Jbq)N2nYwU0;2iv0xCA$DJ963mj?$W01lyJS+tFl zgM$O^Ko)v;XDar0QsIhl+B`593ADzM}t(gFP@X;Dsro z=yMP2#l(a(f`Jj;z+T$Ng%9mwha}!)s?a*KHFYh!en{dCZro|1=ka(H-&j}b^fh$Ym(U66UJotSW3|qfB ztq%%^Jme!B!~RQ20ih?~U)r zs0V2bk&FRc+0sHs@m_WWS&J6hEhF@mH0O%`-_J5m2yU>iQe zI!c5AyO@tlivgsi>g{2$>`=25*wt?L`2p&Tv}A=R``&_%XbcjQ9Eg^eXJ({}r`+dD zNKG1#F0p56s?L$jzv_Ro14bFZ*CXvrd)ZYn5bQkU*VWy0>n0R)plj`{iZ65Mx^5li zYZiM

XvvyD~nm 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 DJjES0 literal 0 HcmV?d00001 diff --git a/ry-api/pom.xml b/ry-api/pom.xml new file mode 100644 index 0000000..8dfdedc --- /dev/null +++ b/ry-api/pom.xml @@ -0,0 +1,243 @@ + + + 4.0.0 + + com.ruoyi + ruoyi + 3.9.2 + + ruoyi + http://www.ruoyi.vip + 若依管理系统 + + + 3.9.2 + UTF-8 + UTF-8 + 17 + 4.0.6 + 4.0.1 + 1.2.28 + 8.1.1 + 2.3.3 + 4.1.0 + 2.0.64 + 7.3.0 + 2.22.0 + 4.1.2 + 2.3 + 0.9.1 + 2.3.1 + 3.0.3 + + + + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + com.alibaba + druid-spring-boot-4-starter + ${druid.version} + + + + + nl.basjes.parse.useragent + yauaa + ${yauaa.version} + + + + + com.github.pagehelper + pagehelper-spring-boot-starter + ${pagehelper.boot.version} + + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + ${mybatis-spring-boot.version} + + + + javax.xml.bind + jaxb-api + ${jaxb-api.version} + + + + + com.github.oshi + oshi-core + ${oshi.version} + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + ${springdoc.version} + + + + + commons-io + commons-io + ${commons.io.version} + + + + + org.apache.poi + poi-ooxml + ${poi.version} + + + + + org.apache.velocity + velocity-engine-core + ${velocity.version} + + + + + com.alibaba.fastjson2 + fastjson2 + ${fastjson.version} + + + + + io.jsonwebtoken + jjwt + ${jwt.version} + + + + + pro.fessional + kaptcha + ${kaptcha.version} + + + + + com.ruoyi + ruoyi-quartz + ${ruoyi.version} + + + + + com.ruoyi + ruoyi-generator + ${ruoyi.version} + + + + + com.ruoyi + ruoyi-framework + ${ruoyi.version} + + + + + com.ruoyi + ruoyi-system + ${ruoyi.version} + + + + + com.ruoyi + ruoyi-business + ${ruoyi.version} + + + + + com.ruoyi + ruoyi-common + ${ruoyi.version} + + + + + + + ruoyi-admin + ruoyi-framework + ruoyi-system + ruoyi-business + ruoyi-quartz + ruoyi-generator + ruoyi-common + + pom + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + true + ${java.version} + ${java.version} + ${project.build.sourceEncoding} + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + com.ruoyi.RuoYiApplication + + + + + + + + public + aliyun nexus + https://maven.aliyun.com/repository/public + + true + + + + + + + public + aliyun nexus + https://maven.aliyun.com/repository/public + + true + + + false + + + + + \ No newline at end of file diff --git a/ry-api/ruoyi-admin/pom.xml b/ry-api/ruoyi-admin/pom.xml new file mode 100644 index 0000000..42f8acb --- /dev/null +++ b/ry-api/ruoyi-admin/pom.xml @@ -0,0 +1,95 @@ + + + + ruoyi + com.ruoyi + 3.9.2 + + 4.0.0 + jar + ruoyi-admin + + + web服务入口 + + + + + + + org.springframework.boot + spring-boot-devtools + true + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + + + + + com.mysql + mysql-connector-j + + + + + com.ruoyi + ruoyi-framework + + + + + com.ruoyi + ruoyi-quartz + + + + + com.ruoyi + ruoyi-generator + + + + + com.ruoyi + ruoyi-business + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + true + com.ruoyi.RuoYiApplication + + + + + repackage + + + + + + org.apache.maven.plugins + maven-war-plugin + 3.1.0 + + false + ${project.artifactId} + + + + ${project.artifactId} + + + \ No newline at end of file diff --git a/ry-api/ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java new file mode 100644 index 0000000..d6f9167 --- /dev/null +++ b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java @@ -0,0 +1,30 @@ +package com.ruoyi; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration; + +/** + * 启动程序 + * + * @author ruoyi + */ +@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class }) +public class RuoYiApplication +{ + public static void main(String[] args) + { + // System.setProperty("spring.devtools.restart.enabled", "false"); + SpringApplication.run(RuoYiApplication.class, args); + System.out.println("(♥◠‿◠)ノ゙ 若依启动成功 ლ(´ڡ`ლ)゙ \n" + + " .-------. ____ __ \n" + + " | _ _ \\ \\ \\ / / \n" + + " | ( ' ) | \\ _. / ' \n" + + " |(_ o _) / _( )_ .' \n" + + " | (_,_).' __ ___(_ o _)' \n" + + " | |\\ \\ | || |(_,_)' \n" + + " | | \\ `' /| `-' / \n" + + " | | \\ / \\ / \n" + + " ''-' `'-' `-..-' "); + } +} diff --git a/ry-api/ruoyi-admin/src/main/java/com/ruoyi/RuoYiServletInitializer.java b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/RuoYiServletInitializer.java new file mode 100644 index 0000000..6de67dc --- /dev/null +++ b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/RuoYiServletInitializer.java @@ -0,0 +1,18 @@ +package com.ruoyi; + +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; + +/** + * web容器中进行部署 + * + * @author ruoyi + */ +public class RuoYiServletInitializer extends SpringBootServletInitializer +{ + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) + { + return application.sources(RuoYiApplication.class); + } +} diff --git a/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CaptchaController.java b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CaptchaController.java new file mode 100644 index 0000000..d6dcf06 --- /dev/null +++ b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CaptchaController.java @@ -0,0 +1,94 @@ +package com.ruoyi.web.controller.common; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import jakarta.annotation.Resource; +import javax.imageio.ImageIO; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.FastByteArrayOutputStream; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import com.google.code.kaptcha.Producer; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.utils.sign.Base64; +import com.ruoyi.common.utils.uuid.IdUtils; +import com.ruoyi.system.service.ISysConfigService; + +/** + * 验证码操作处理 + * + * @author ruoyi + */ +@RestController +public class CaptchaController +{ + @Resource(name = "captchaProducer") + private Producer captchaProducer; + + @Resource(name = "captchaProducerMath") + private Producer captchaProducerMath; + + @Autowired + private RedisCache redisCache; + + @Autowired + private ISysConfigService configService; + /** + * 生成验证码 + */ + @GetMapping("/captchaImage") + public AjaxResult getCode(HttpServletResponse response) throws IOException + { + AjaxResult ajax = AjaxResult.success(); + boolean captchaEnabled = configService.selectCaptchaEnabled(); + ajax.put("captchaEnabled", captchaEnabled); + if (!captchaEnabled) + { + return ajax; + } + + // 保存验证码信息 + String uuid = IdUtils.simpleUUID(); + String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid; + + String capStr = null, code = null; + BufferedImage image = null; + + // 生成验证码 + String captchaType = RuoYiConfig.getCaptchaType(); + if ("math".equals(captchaType)) + { + String capText = captchaProducerMath.createText(); + capStr = capText.substring(0, capText.lastIndexOf("@")); + code = capText.substring(capText.lastIndexOf("@") + 1); + image = captchaProducerMath.createImage(capStr); + } + else if ("char".equals(captchaType)) + { + capStr = code = captchaProducer.createText(); + image = captchaProducer.createImage(capStr); + } + + redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES); + // 转换流信息写出 + FastByteArrayOutputStream os = new FastByteArrayOutputStream(); + try + { + ImageIO.write(image, "jpg", os); + } + catch (IOException e) + { + return AjaxResult.error(e.getMessage()); + } + + ajax.put("uuid", uuid); + ajax.put("img", Base64.encode(os.toByteArray())); + return ajax; + } +} diff --git a/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java new file mode 100644 index 0000000..a2333d5 --- /dev/null +++ b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java @@ -0,0 +1,162 @@ +package com.ruoyi.web.controller.common; + +import java.util.ArrayList; +import java.util.List; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.file.FileUploadUtils; +import com.ruoyi.common.utils.file.FileUtils; +import com.ruoyi.framework.config.ServerConfig; + +/** + * 通用请求处理 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/common") +public class CommonController +{ + private static final Logger log = LoggerFactory.getLogger(CommonController.class); + + @Autowired + private ServerConfig serverConfig; + + private static final String FILE_DELIMITER = ","; + + /** + * 通用下载请求 + * + * @param fileName 文件名称 + * @param delete 是否删除 + */ + @GetMapping("/download") + public void fileDownload(String fileName, Boolean delete, HttpServletResponse response, HttpServletRequest request) + { + try + { + if (!FileUtils.checkAllowDownload(fileName)) + { + throw new Exception(StringUtils.format("文件名称({})非法,不允许下载。 ", fileName)); + } + String realFileName = System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1); + String filePath = RuoYiConfig.getDownloadPath() + fileName; + + response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); + FileUtils.setAttachmentResponseHeader(response, realFileName); + FileUtils.writeBytes(filePath, response.getOutputStream()); + if (delete) + { + FileUtils.deleteFile(filePath); + } + } + catch (Exception e) + { + log.error("下载文件失败", e); + } + } + + /** + * 通用上传请求(单个) + */ + @PostMapping("/upload") + public AjaxResult uploadFile(MultipartFile file) throws Exception + { + try + { + // 上传文件路径 + String filePath = RuoYiConfig.getUploadPath(); + // 上传并返回新文件名称 + String fileName = FileUploadUtils.upload(filePath, file); + String url = serverConfig.getUrl() + fileName; + AjaxResult ajax = AjaxResult.success(); + ajax.put("url", url); + ajax.put("fileName", fileName); + ajax.put("newFileName", FileUtils.getName(fileName)); + ajax.put("originalFilename", file.getOriginalFilename()); + return ajax; + } + catch (Exception e) + { + return AjaxResult.error(e.getMessage()); + } + } + + /** + * 通用上传请求(多个) + */ + @PostMapping("/uploads") + public AjaxResult uploadFiles(List files) throws Exception + { + try + { + // 上传文件路径 + String filePath = RuoYiConfig.getUploadPath(); + List urls = new ArrayList(); + List fileNames = new ArrayList(); + List newFileNames = new ArrayList(); + List originalFilenames = new ArrayList(); + for (MultipartFile file : files) + { + // 上传并返回新文件名称 + String fileName = FileUploadUtils.upload(filePath, file); + String url = serverConfig.getUrl() + fileName; + urls.add(url); + fileNames.add(fileName); + newFileNames.add(FileUtils.getName(fileName)); + originalFilenames.add(file.getOriginalFilename()); + } + AjaxResult ajax = AjaxResult.success(); + ajax.put("urls", StringUtils.join(urls, FILE_DELIMITER)); + ajax.put("fileNames", StringUtils.join(fileNames, FILE_DELIMITER)); + ajax.put("newFileNames", StringUtils.join(newFileNames, FILE_DELIMITER)); + ajax.put("originalFilenames", StringUtils.join(originalFilenames, FILE_DELIMITER)); + return ajax; + } + catch (Exception e) + { + return AjaxResult.error(e.getMessage()); + } + } + + /** + * 本地资源通用下载 + */ + @GetMapping("/download/resource") + public void resourceDownload(String resource, HttpServletRequest request, HttpServletResponse response) + throws Exception + { + try + { + if (!FileUtils.checkAllowDownload(resource)) + { + throw new Exception(StringUtils.format("资源文件({})非法,不允许下载。 ", resource)); + } + // 本地资源路径 + String localPath = RuoYiConfig.getProfile(); + // 数据库资源地址 + String downloadPath = localPath + FileUtils.stripPrefix(resource); + // 下载名称 + String downloadName = StringUtils.substringAfterLast(downloadPath, "/"); + response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); + FileUtils.setAttachmentResponseHeader(response, downloadName); + FileUtils.writeBytes(downloadPath, response.getOutputStream()); + } + catch (Exception e) + { + log.error("下载文件失败", e); + } + } +} diff --git a/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/OssController.java b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/OssController.java new file mode 100644 index 0000000..944838f --- /dev/null +++ b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/OssController.java @@ -0,0 +1,170 @@ +package com.ruoyi.web.controller.common; + +import java.nio.charset.StandardCharsets; +import java.io.InputStream; +import java.net.URI; +import java.net.URLConnection; +import java.util.Date; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.config.RuoYiConfig.OssProperties; +import com.ruoyi.common.core.domain.AjaxResult; + +/** + * 阿里云 OSS 签名服务 + * + * 用于前端 直传 OSS: 前端先 GET /common/oss/sign?dir=xxx + * 拿到 {host, dir, accessKeyId, policy, signature} 后直接 POST 到 OSS, + * 不再走后端中转. secretKey 不下发到前端. + * + * 配置项 (ruoyi.oss.*): + * enabled: 是否启用 OSS (false 时返回空, 前端 fallback 到 /common/upload 本地存储) + * endpoint: https://.oss-cn-beijing.aliyuncs.com + * bucket: bucket 名称 + * accessKeyId: AK + * accessKeySecret: SK (仅后端使用) + * dirPrefix: 上传目录前缀, 默认 "ry8080/" + * expireSeconds: 签名失效时间, 默认 3600 + */ +@RestController +@RequestMapping("/common/oss") +public class OssController +{ + @org.springframework.beans.factory.annotation.Autowired + private RuoYiConfig ruoyiConfig; + + @GetMapping("/sign") + public AjaxResult sign(String dir) + { + OssProperties props = ruoyiConfig.getOss(); + if (props == null || !props.isEnabled()) + { + return AjaxResult.error("OSS 未启用 (ruoyi.oss.enabled=false)"); + } + if (dir == null || dir.isEmpty()) + { + dir = props.getDirPrefix(); + } + // 不允许目录穿越 + if (dir.contains("..")) + { + return AjaxResult.error("非法目录"); + } + long expire = props.getExpireSeconds() * 1000L; + Date expiration = new Date(System.currentTimeMillis() + expire); + + // OSS PostObject policy 文档: https://help.aliyun.com/document_detail/31988.html + // expiration 格式: yyyy-MM-ddTHH:mm:ss.SSSZ (OSS 严格解析, 毫秒必须 3 位) + // 注: OSS conditions 是 AND 关系, 不支持 OR 嵌套; Content-Type 不在 policy 中限制, + // 由前端 FormData 字段 'Content-Type' 携带, OSS 按此存文件元数据 + String policyText = "{\"expiration\":\"" + toISO8601(expiration) + + "\",\"conditions\":[" + + "[\"content-length-range\",0,104857600]," + + "[\"starts-with\",\"$key\",\"" + dir + "\"]" + + "]}"; + String policyBase64 = java.util.Base64.getEncoder().encodeToString( + policyText.getBytes(StandardCharsets.UTF_8)); + String signature = hmacSha1Base64(props.getAccessKeySecret(), policyBase64); + + AjaxResult ajax = AjaxResult.success(); + ajax.put("host", props.getEndpoint()); + ajax.put("dir", dir); + ajax.put("accessKeyId", props.getAccessKeyId()); + ajax.put("policy", policyBase64); + ajax.put("signature", signature); + ajax.put("expire", expiration.getTime()); + return ajax; + } + + /** OSS 期望 ISO8601 (yyyy-MM-ddTHH:mm:ss.SSS'Z') GMT 时间 */ + private static String toISO8601(Date d) + { + java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); + sdf.setTimeZone(java.util.TimeZone.getTimeZone("GMT")); + return sdf.format(d); + } + + private static String hmacSha1Base64(String key, String data) + { + try + { + Mac mac = Mac.getInstance("HmacSHA1"); + mac.init(new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA1")); + byte[] bytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); + return java.util.Base64.getEncoder().encodeToString(bytes); + } + catch (Exception e) + { + throw new RuntimeException("OSS 签名失败", e); + } + } + + /** + * OSS 文件代理: OSS bucket 设置了 Content-Disposition: attachment, + * 直接 iframe/embed 会被强制下载. 此接口转发 OSS 字节流并重写 header, + * 让浏览器内嵌预览 (PDF/图片). + * 公开访问, 无需 token (SecurityConfig 已对 /common/oss/** 公开). + * + * @param url 完整 OSS URL (前端从 biz_project.invitationUrl 等字段拿到) + */ + @GetMapping("/proxy") + public void ossProxy(@RequestParam String url, HttpServletResponse response) throws Exception + { + // 安全: 仅允许代理 hwtossbamlorgcn bucket (防止 SSRF 攻击) + if (url == null || !url.contains("hwtossbamlorgcn.oss-cn-beijing.aliyuncs.com")) + { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + return; + } + URI uri = URI.create(url); + URLConnection conn = uri.toURL().openConnection(); + conn.setConnectTimeout(10000); + conn.setReadTimeout(30000); + + String origCt = conn.getContentType(); + String path = uri.getPath(); + String filename = path.substring(path.lastIndexOf('/') + 1); + + // 重写 Content-Type: OSS 可能返回 octet-stream, 按扩展名强制给正确 mime + String ct = origCt; + if (ct == null || ct.contains("octet-stream") || ct.isEmpty()) + { + String lower = filename.toLowerCase(); + if (lower.endsWith(".pdf")) ct = "application/pdf"; + else if (lower.endsWith(".png")) ct = "image/png"; + else if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) ct = "image/jpeg"; + else if (lower.endsWith(".gif")) ct = "image/gif"; + else if (lower.endsWith(".webp")) ct = "image/webp"; + else ct = origCt != null ? origCt : "application/octet-stream"; + } + + // 关键: 重写 Content-Disposition 为 inline (OSS 默认是 attachment) + response.setContentType(ct); + response.setHeader("Content-Disposition", "inline; filename=\"" + filename + "\""); + response.setHeader("Cache-Control", "public, max-age=3600"); + + try (InputStream in = conn.getInputStream()) + { + byte[] buf = new byte[8192]; + int n; + while ((n = in.read(buf)) > 0) + { + response.getOutputStream().write(buf, 0, n); + } + } + catch (java.io.FileNotFoundException e) + { + // OSS 返回 404 (文件不存在) - 返回 404 而非 500 + application/json + // 避免前端 embed 拿到 JSON 后误渲染 + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + response.setContentType("text/plain;charset=utf-8"); + response.getWriter().write("FILE_NOT_FOUND"); + } + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/CacheController.java b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/CacheController.java new file mode 100644 index 0000000..d17bd66 --- /dev/null +++ b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/CacheController.java @@ -0,0 +1,122 @@ +package com.ruoyi.web.controller.monitor; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.TreeSet; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisCallback; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.domain.SysCache; + +/** + * 缓存监控 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/monitor/cache") +public class CacheController +{ + @Autowired + private RedisTemplate redisTemplate; + + private final static List caches = new ArrayList(); + { + caches.add(new SysCache(CacheConstants.LOGIN_TOKEN_KEY, "用户信息")); + caches.add(new SysCache(CacheConstants.SYS_CONFIG_KEY, "配置信息")); + caches.add(new SysCache(CacheConstants.SYS_DICT_KEY, "数据字典")); + caches.add(new SysCache(CacheConstants.CAPTCHA_CODE_KEY, "验证码")); + caches.add(new SysCache(CacheConstants.REPEAT_SUBMIT_KEY, "防重提交")); + caches.add(new SysCache(CacheConstants.RATE_LIMIT_KEY, "限流处理")); + caches.add(new SysCache(CacheConstants.PWD_ERR_CNT_KEY, "密码错误次数")); + } + + @SuppressWarnings("deprecation") + @PreAuthorize("@ss.hasPermi('monitor:cache:list')") + @GetMapping() + public AjaxResult getInfo() throws Exception + { + Properties info = (Properties) redisTemplate.execute((RedisCallback) connection -> connection.info()); + Properties commandStats = (Properties) redisTemplate.execute((RedisCallback) connection -> connection.info("commandstats")); + Object dbSize = redisTemplate.execute((RedisCallback) connection -> connection.dbSize()); + + Map result = new HashMap<>(3); + result.put("info", info); + result.put("dbSize", dbSize); + + List> pieList = new ArrayList<>(); + commandStats.stringPropertyNames().forEach(key -> { + Map data = new HashMap<>(2); + String property = commandStats.getProperty(key); + data.put("name", StringUtils.removeStart(key, "cmdstat_")); + data.put("value", StringUtils.substringBetween(property, "calls=", ",usec")); + pieList.add(data); + }); + result.put("commandStats", pieList); + return AjaxResult.success(result); + } + + @PreAuthorize("@ss.hasPermi('monitor:cache:list')") + @GetMapping("/getNames") + public AjaxResult cache() + { + return AjaxResult.success(caches); + } + + @PreAuthorize("@ss.hasPermi('monitor:cache:list')") + @GetMapping("/getKeys/{cacheName}") + public AjaxResult getCacheKeys(@PathVariable String cacheName) + { + Set cacheKeys = redisTemplate.keys(cacheName + "*"); + return AjaxResult.success(new TreeSet<>(cacheKeys)); + } + + @PreAuthorize("@ss.hasPermi('monitor:cache:list')") + @GetMapping("/getValue/{cacheName}/{cacheKey}") + public AjaxResult getCacheValue(@PathVariable String cacheName, @PathVariable String cacheKey) + { + String cacheValue = redisTemplate.opsForValue().get(cacheKey); + SysCache sysCache = new SysCache(cacheName, cacheKey, cacheValue); + return AjaxResult.success(sysCache); + } + + @PreAuthorize("@ss.hasPermi('monitor:cache:list')") + @DeleteMapping("/clearCacheName/{cacheName}") + public AjaxResult clearCacheName(@PathVariable String cacheName) + { + Collection cacheKeys = redisTemplate.keys(cacheName + "*"); + redisTemplate.delete(cacheKeys); + return AjaxResult.success(); + } + + @PreAuthorize("@ss.hasPermi('monitor:cache:list')") + @DeleteMapping("/clearCacheKey/{cacheKey}") + public AjaxResult clearCacheKey(@PathVariable String cacheKey) + { + redisTemplate.delete(cacheKey); + return AjaxResult.success(); + } + + @PreAuthorize("@ss.hasPermi('monitor:cache:list')") + @DeleteMapping("/clearCacheAll") + public AjaxResult clearCacheAll() + { + Collection cacheKeys = redisTemplate.keys("*"); + redisTemplate.delete(cacheKeys); + return AjaxResult.success(); + } +} diff --git a/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/ServerController.java b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/ServerController.java new file mode 100644 index 0000000..cc805ad --- /dev/null +++ b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/ServerController.java @@ -0,0 +1,27 @@ +package com.ruoyi.web.controller.monitor; + +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.framework.web.domain.Server; + +/** + * 服务器监控 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/monitor/server") +public class ServerController +{ + @PreAuthorize("@ss.hasPermi('monitor:server:list')") + @GetMapping() + public AjaxResult getInfo() throws Exception + { + Server server = new Server(); + server.copyTo(); + return AjaxResult.success(server); + } +} diff --git a/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysLogininforController.java b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysLogininforController.java new file mode 100644 index 0000000..f6fac71 --- /dev/null +++ b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysLogininforController.java @@ -0,0 +1,82 @@ +package com.ruoyi.web.controller.monitor; + +import java.util.List; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.framework.web.service.SysPasswordService; +import com.ruoyi.system.domain.SysLogininfor; +import com.ruoyi.system.service.ISysLogininforService; + +/** + * 系统访问记录 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/monitor/logininfor") +public class SysLogininforController extends BaseController +{ + @Autowired + private ISysLogininforService logininforService; + + @Autowired + private SysPasswordService passwordService; + + @PreAuthorize("@ss.hasPermi('monitor:logininfor:list')") + @GetMapping("/list") + public TableDataInfo list(SysLogininfor logininfor) + { + startPage(); + List list = logininforService.selectLogininforList(logininfor); + return getDataTable(list); + } + + @Log(title = "登录日志", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('monitor:logininfor:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysLogininfor logininfor) + { + List list = logininforService.selectLogininforList(logininfor); + ExcelUtil util = new ExcelUtil(SysLogininfor.class); + util.exportExcel(response, list, "登录日志"); + } + + @PreAuthorize("@ss.hasPermi('monitor:logininfor:remove')") + @Log(title = "登录日志", businessType = BusinessType.DELETE) + @DeleteMapping("/{infoIds}") + public AjaxResult remove(@PathVariable Long[] infoIds) + { + return toAjax(logininforService.deleteLogininforByIds(infoIds)); + } + + @PreAuthorize("@ss.hasPermi('monitor:logininfor:remove')") + @Log(title = "登录日志", businessType = BusinessType.CLEAN) + @DeleteMapping("/clean") + public AjaxResult clean() + { + logininforService.cleanLogininfor(); + return success(); + } + + @PreAuthorize("@ss.hasPermi('monitor:logininfor:unlock')") + @Log(title = "账户解锁", businessType = BusinessType.OTHER) + @GetMapping("/unlock/{userName}") + public AjaxResult unlock(@PathVariable("userName") String userName) + { + passwordService.clearLoginRecordCache(userName); + return success(); + } +} diff --git a/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysOperlogController.java b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysOperlogController.java new file mode 100644 index 0000000..f056d65 --- /dev/null +++ b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysOperlogController.java @@ -0,0 +1,69 @@ +package com.ruoyi.web.controller.monitor; + +import java.util.List; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.system.domain.SysOperLog; +import com.ruoyi.system.service.ISysOperLogService; + +/** + * 操作日志记录 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/monitor/operlog") +public class SysOperlogController extends BaseController +{ + @Autowired + private ISysOperLogService operLogService; + + @PreAuthorize("@ss.hasPermi('monitor:operlog:list')") + @GetMapping("/list") + public TableDataInfo list(SysOperLog operLog) + { + startPage(); + List list = operLogService.selectOperLogList(operLog); + return getDataTable(list); + } + + @Log(title = "操作日志", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('monitor:operlog:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysOperLog operLog) + { + List list = operLogService.selectOperLogList(operLog); + ExcelUtil util = new ExcelUtil(SysOperLog.class); + util.exportExcel(response, list, "操作日志"); + } + + @Log(title = "操作日志", businessType = BusinessType.DELETE) + @PreAuthorize("@ss.hasPermi('monitor:operlog:remove')") + @DeleteMapping("/{operIds}") + public AjaxResult remove(@PathVariable Long[] operIds) + { + return toAjax(operLogService.deleteOperLogByIds(operIds)); + } + + @Log(title = "操作日志", businessType = BusinessType.CLEAN) + @PreAuthorize("@ss.hasPermi('monitor:operlog:remove')") + @DeleteMapping("/clean") + public AjaxResult clean() + { + operLogService.cleanOperLog(); + return success(); + } +} diff --git a/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysUserOnlineController.java b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysUserOnlineController.java new file mode 100644 index 0000000..a442863 --- /dev/null +++ b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysUserOnlineController.java @@ -0,0 +1,83 @@ +package com.ruoyi.web.controller.monitor; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.domain.SysUserOnline; +import com.ruoyi.system.service.ISysUserOnlineService; + +/** + * 在线用户监控 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/monitor/online") +public class SysUserOnlineController extends BaseController +{ + @Autowired + private ISysUserOnlineService userOnlineService; + + @Autowired + private RedisCache redisCache; + + @PreAuthorize("@ss.hasPermi('monitor:online:list')") + @GetMapping("/list") + public TableDataInfo list(String ipaddr, String userName) + { + Collection keys = redisCache.keys(CacheConstants.LOGIN_TOKEN_KEY + "*"); + List userOnlineList = new ArrayList(); + for (String key : keys) + { + LoginUser user = redisCache.getCacheObject(key); + if (StringUtils.isNotEmpty(ipaddr) && StringUtils.isNotEmpty(userName)) + { + userOnlineList.add(userOnlineService.selectOnlineByInfo(ipaddr, userName, user)); + } + else if (StringUtils.isNotEmpty(ipaddr)) + { + userOnlineList.add(userOnlineService.selectOnlineByIpaddr(ipaddr, user)); + } + else if (StringUtils.isNotEmpty(userName) && StringUtils.isNotNull(user.getUser())) + { + userOnlineList.add(userOnlineService.selectOnlineByUserName(userName, user)); + } + else + { + userOnlineList.add(userOnlineService.loginUserToUserOnline(user)); + } + } + Collections.reverse(userOnlineList); + userOnlineList.removeAll(Collections.singleton(null)); + return getDataTable(userOnlineList); + } + + /** + * 强退用户 + */ + @PreAuthorize("@ss.hasPermi('monitor:online:forceLogout')") + @Log(title = "在线用户", businessType = BusinessType.FORCE) + @DeleteMapping("/{tokenId}") + public AjaxResult forceLogout(@PathVariable String tokenId) + { + redisCache.deleteObject(CacheConstants.LOGIN_TOKEN_KEY + tokenId); + return success(); + } +} diff --git a/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysConfigController.java b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysConfigController.java new file mode 100644 index 0000000..91a854f --- /dev/null +++ b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysConfigController.java @@ -0,0 +1,133 @@ +package com.ruoyi.web.controller.system; + +import java.util.List; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.system.domain.SysConfig; +import com.ruoyi.system.service.ISysConfigService; + +/** + * 参数配置 信息操作处理 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/system/config") +public class SysConfigController extends BaseController +{ + @Autowired + private ISysConfigService configService; + + /** + * 获取参数配置列表 + */ + @PreAuthorize("@ss.hasPermi('system:config:list')") + @GetMapping("/list") + public TableDataInfo list(SysConfig config) + { + startPage(); + List list = configService.selectConfigList(config); + return getDataTable(list); + } + + @Log(title = "参数管理", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('system:config:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysConfig config) + { + List list = configService.selectConfigList(config); + ExcelUtil util = new ExcelUtil(SysConfig.class); + util.exportExcel(response, list, "参数数据"); + } + + /** + * 根据参数编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('system:config:query')") + @GetMapping(value = "/{configId}") + public AjaxResult getInfo(@PathVariable Long configId) + { + return success(configService.selectConfigById(configId)); + } + + /** + * 根据参数键名查询参数值 + */ + @GetMapping(value = "/configKey/{configKey}") + public AjaxResult getConfigKey(@PathVariable String configKey) + { + return success(configService.selectConfigByKey(configKey)); + } + + /** + * 新增参数配置 + */ + @PreAuthorize("@ss.hasPermi('system:config:add')") + @Log(title = "参数管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysConfig config) + { + if (!configService.checkConfigKeyUnique(config)) + { + return error("新增参数'" + config.getConfigName() + "'失败,参数键名已存在"); + } + config.setCreateBy(getUsername()); + return toAjax(configService.insertConfig(config)); + } + + /** + * 修改参数配置 + */ + @PreAuthorize("@ss.hasPermi('system:config:edit')") + @Log(title = "参数管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysConfig config) + { + if (!configService.checkConfigKeyUnique(config)) + { + return error("修改参数'" + config.getConfigName() + "'失败,参数键名已存在"); + } + config.setUpdateBy(getUsername()); + return toAjax(configService.updateConfig(config)); + } + + /** + * 删除参数配置 + */ + @PreAuthorize("@ss.hasPermi('system:config:remove')") + @Log(title = "参数管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{configIds}") + public AjaxResult remove(@PathVariable Long[] configIds) + { + configService.deleteConfigByIds(configIds); + return success(); + } + + /** + * 刷新参数缓存 + */ + @PreAuthorize("@ss.hasPermi('system:config:remove')") + @Log(title = "参数管理", businessType = BusinessType.CLEAN) + @DeleteMapping("/refreshCache") + public AjaxResult refreshCache() + { + configService.resetConfigCache(); + return success(); + } +} diff --git a/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDeptController.java b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDeptController.java new file mode 100644 index 0000000..343503c --- /dev/null +++ b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDeptController.java @@ -0,0 +1,147 @@ +package com.ruoyi.web.controller.system; + +import java.util.List; +import java.util.Map; +import org.apache.commons.lang3.ArrayUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.entity.SysDept; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.service.ISysDeptService; + +/** + * 部门信息 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/system/dept") +public class SysDeptController extends BaseController +{ + @Autowired + private ISysDeptService deptService; + + /** + * 获取部门列表 + */ + @PreAuthorize("@ss.hasPermi('system:dept:list')") + @GetMapping("/list") + public AjaxResult list(SysDept dept) + { + List depts = deptService.selectDeptList(dept); + return success(depts); + } + + /** + * 查询部门列表(排除节点) + */ + @PreAuthorize("@ss.hasPermi('system:dept:list')") + @GetMapping("/list/exclude/{deptId}") + public AjaxResult excludeChild(@PathVariable(value = "deptId", required = false) Long deptId) + { + List depts = deptService.selectDeptList(new SysDept()); + depts.removeIf(d -> d.getDeptId().intValue() == deptId || ArrayUtils.contains(StringUtils.split(d.getAncestors(), ","), deptId + "")); + return success(depts); + } + + /** + * 根据部门编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('system:dept:query')") + @GetMapping(value = "/{deptId}") + public AjaxResult getInfo(@PathVariable Long deptId) + { + deptService.checkDeptDataScope(deptId); + return success(deptService.selectDeptById(deptId)); + } + + /** + * 新增部门 + */ + @PreAuthorize("@ss.hasPermi('system:dept:add')") + @Log(title = "部门管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysDept dept) + { + if (!deptService.checkDeptNameUnique(dept)) + { + return error("新增部门'" + dept.getDeptName() + "'失败,部门名称已存在"); + } + dept.setCreateBy(getUsername()); + return toAjax(deptService.insertDept(dept)); + } + + /** + * 修改部门 + */ + @PreAuthorize("@ss.hasPermi('system:dept:edit')") + @Log(title = "部门管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysDept dept) + { + Long deptId = dept.getDeptId(); + deptService.checkDeptDataScope(deptId); + if (!deptService.checkDeptNameUnique(dept)) + { + return error("修改部门'" + dept.getDeptName() + "'失败,部门名称已存在"); + } + else if (dept.getParentId().equals(deptId)) + { + return error("修改部门'" + dept.getDeptName() + "'失败,上级部门不能是自己"); + } + else if (StringUtils.equals(UserConstants.DEPT_DISABLE, dept.getStatus()) && deptService.selectNormalChildrenDeptById(deptId) > 0) + { + return error("该部门包含未停用的子部门!"); + } + dept.setUpdateBy(getUsername()); + return toAjax(deptService.updateDept(dept)); + } + + /** + * 保存部门排序 + */ + @PreAuthorize("@ss.hasPermi('system:dept:edit')") + @Log(title = "保存部门排序", businessType = BusinessType.UPDATE) + @PutMapping("/updateSort") + public AjaxResult updateSort(@RequestBody Map params) + { + String[] deptIds = params.get("deptIds").split(","); + String[] orderNums = params.get("orderNums").split(","); + deptService.updateDeptSort(deptIds, orderNums); + return success(); + } + + /** + * 删除部门 + */ + @PreAuthorize("@ss.hasPermi('system:dept:remove')") + @Log(title = "部门管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{deptId}") + public AjaxResult remove(@PathVariable Long deptId) + { + if (deptService.hasChildByDeptId(deptId)) + { + return warn("存在下级部门,不允许删除"); + } + if (deptService.checkDeptExistUser(deptId)) + { + return warn("部门存在用户,不允许删除"); + } + deptService.checkDeptDataScope(deptId); + return toAjax(deptService.deleteDeptById(deptId)); + } +} diff --git a/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDictDataController.java b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDictDataController.java new file mode 100644 index 0000000..f65492b --- /dev/null +++ b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDictDataController.java @@ -0,0 +1,121 @@ +package com.ruoyi.web.controller.system; + +import java.util.ArrayList; +import java.util.List; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.entity.SysDictData; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.system.service.ISysDictDataService; +import com.ruoyi.system.service.ISysDictTypeService; + +/** + * 数据字典信息 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/system/dict/data") +public class SysDictDataController extends BaseController +{ + @Autowired + private ISysDictDataService dictDataService; + + @Autowired + private ISysDictTypeService dictTypeService; + + @PreAuthorize("@ss.hasPermi('system:dict:list')") + @GetMapping("/list") + public TableDataInfo list(SysDictData dictData) + { + startPage(); + List list = dictDataService.selectDictDataList(dictData); + return getDataTable(list); + } + + @Log(title = "字典数据", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('system:dict:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysDictData dictData) + { + List list = dictDataService.selectDictDataList(dictData); + ExcelUtil util = new ExcelUtil(SysDictData.class); + util.exportExcel(response, list, "字典数据"); + } + + /** + * 查询字典数据详细 + */ + @PreAuthorize("@ss.hasPermi('system:dict:query')") + @GetMapping(value = "/{dictCode}") + public AjaxResult getInfo(@PathVariable Long dictCode) + { + return success(dictDataService.selectDictDataById(dictCode)); + } + + /** + * 根据字典类型查询字典数据信息 + */ + @GetMapping(value = "/type/{dictType}") + public AjaxResult dictType(@PathVariable String dictType) + { + List data = dictTypeService.selectDictDataByType(dictType); + if (StringUtils.isNull(data)) + { + data = new ArrayList(); + } + return success(data); + } + + /** + * 新增字典类型 + */ + @PreAuthorize("@ss.hasPermi('system:dict:add')") + @Log(title = "字典数据", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysDictData dict) + { + dict.setCreateBy(getUsername()); + return toAjax(dictDataService.insertDictData(dict)); + } + + /** + * 修改保存字典类型 + */ + @PreAuthorize("@ss.hasPermi('system:dict:edit')") + @Log(title = "字典数据", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysDictData dict) + { + dict.setUpdateBy(getUsername()); + return toAjax(dictDataService.updateDictData(dict)); + } + + /** + * 删除字典类型 + */ + @PreAuthorize("@ss.hasPermi('system:dict:remove')") + @Log(title = "字典类型", businessType = BusinessType.DELETE) + @DeleteMapping("/{dictCodes}") + public AjaxResult remove(@PathVariable Long[] dictCodes) + { + dictDataService.deleteDictDataByIds(dictCodes); + return success(); + } +} diff --git a/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDictTypeController.java b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDictTypeController.java new file mode 100644 index 0000000..3ff314c --- /dev/null +++ b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDictTypeController.java @@ -0,0 +1,131 @@ +package com.ruoyi.web.controller.system; + +import java.util.List; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.entity.SysDictType; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.system.service.ISysDictTypeService; + +/** + * 数据字典信息 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/system/dict/type") +public class SysDictTypeController extends BaseController +{ + @Autowired + private ISysDictTypeService dictTypeService; + + @PreAuthorize("@ss.hasPermi('system:dict:list')") + @GetMapping("/list") + public TableDataInfo list(SysDictType dictType) + { + startPage(); + List list = dictTypeService.selectDictTypeList(dictType); + return getDataTable(list); + } + + @Log(title = "字典类型", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('system:dict:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysDictType dictType) + { + List list = dictTypeService.selectDictTypeList(dictType); + ExcelUtil util = new ExcelUtil(SysDictType.class); + util.exportExcel(response, list, "字典类型"); + } + + /** + * 查询字典类型详细 + */ + @PreAuthorize("@ss.hasPermi('system:dict:query')") + @GetMapping(value = "/{dictId}") + public AjaxResult getInfo(@PathVariable Long dictId) + { + return success(dictTypeService.selectDictTypeById(dictId)); + } + + /** + * 新增字典类型 + */ + @PreAuthorize("@ss.hasPermi('system:dict:add')") + @Log(title = "字典类型", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysDictType dict) + { + if (!dictTypeService.checkDictTypeUnique(dict)) + { + return error("新增字典'" + dict.getDictName() + "'失败,字典类型已存在"); + } + dict.setCreateBy(getUsername()); + return toAjax(dictTypeService.insertDictType(dict)); + } + + /** + * 修改字典类型 + */ + @PreAuthorize("@ss.hasPermi('system:dict:edit')") + @Log(title = "字典类型", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysDictType dict) + { + if (!dictTypeService.checkDictTypeUnique(dict)) + { + return error("修改字典'" + dict.getDictName() + "'失败,字典类型已存在"); + } + dict.setUpdateBy(getUsername()); + return toAjax(dictTypeService.updateDictType(dict)); + } + + /** + * 删除字典类型 + */ + @PreAuthorize("@ss.hasPermi('system:dict:remove')") + @Log(title = "字典类型", businessType = BusinessType.DELETE) + @DeleteMapping("/{dictIds}") + public AjaxResult remove(@PathVariable Long[] dictIds) + { + dictTypeService.deleteDictTypeByIds(dictIds); + return success(); + } + + /** + * 刷新字典缓存 + */ + @PreAuthorize("@ss.hasPermi('system:dict:remove')") + @Log(title = "字典类型", businessType = BusinessType.CLEAN) + @DeleteMapping("/refreshCache") + public AjaxResult refreshCache() + { + dictTypeService.resetDictCache(); + return success(); + } + + /** + * 获取字典选择框列表 + */ + @GetMapping("/optionselect") + public AjaxResult optionselect() + { + List dictTypes = dictTypeService.selectDictTypeAll(); + return success(dictTypes); + } +} diff --git a/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysIndexController.java b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysIndexController.java new file mode 100644 index 0000000..f2ce93c --- /dev/null +++ b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysIndexController.java @@ -0,0 +1,64 @@ +package com.ruoyi.web.controller.system; + +import java.util.Map; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.service.ISysUserService; + +/** + * 首页 + * + * @author ruoyi + */ +@RestController +public class SysIndexController +{ + /** 系统基础配置 */ + @Autowired + private RuoYiConfig ruoyiConfig; + + @Autowired + private ISysUserService userService; + + /** + * 访问首页,提示语 + */ + @RequestMapping("/") + public String index() + { + return StringUtils.format("欢迎使用{}后台管理框架,当前版本:v{},请通过前端地址访问。", ruoyiConfig.getName(), ruoyiConfig.getVersion()); + } + + /** + * 解锁屏幕 + */ + @PostMapping("/unlockscreen") + public AjaxResult unlockScreen(@RequestBody Map body) + { + String password = body.get("password"); + if (StringUtils.isEmpty(password)) + { + return AjaxResult.error("密码不能为空"); + } + String username = SecurityUtils.getUsername(); + SysUser user = userService.selectUserByUserName(username); + if (user == null) + { + return AjaxResult.error("服务器超时,请重新登录"); + } + if (!SecurityUtils.matchesPassword(password, user.getPassword())) + { + return AjaxResult.error("密码错误,请重新输入"); + } + + return AjaxResult.success("解锁成功"); + } +} diff --git a/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java new file mode 100644 index 0000000..7719c89 --- /dev/null +++ b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java @@ -0,0 +1,138 @@ +package com.ruoyi.web.controller.system; + +import java.util.Date; +import java.util.List; +import java.util.Set; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.entity.SysMenu; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.domain.model.LoginBody; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.web.service.SysLoginService; +import com.ruoyi.framework.web.service.SysPermissionService; +import com.ruoyi.framework.web.service.TokenService; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.system.service.ISysMenuService; + +/** + * 登录验证 + * + * @author ruoyi + */ +@RestController +public class SysLoginController +{ + @Autowired + private SysLoginService loginService; + + @Autowired + private ISysMenuService menuService; + + @Autowired + private SysPermissionService permissionService; + + @Autowired + private TokenService tokenService; + + @Autowired + private ISysConfigService configService; + + /** + * 登录方法 + * + * @param loginBody 登录信息 + * @return 结果 + */ + @PostMapping("/login") + public AjaxResult login(@RequestBody LoginBody loginBody) + { + AjaxResult ajax = AjaxResult.success(); + // 生成令牌 + String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(), + loginBody.getUuid()); + ajax.put(Constants.TOKEN, token); + return ajax; + } + + /** + * 获取用户信息 + * + * @return 用户信息 + */ + @GetMapping("getInfo") + public AjaxResult getInfo() + { + LoginUser loginUser = SecurityUtils.getLoginUser(); + SysUser user = loginUser.getUser(); + // 角色集合 + Set roles = permissionService.getRolePermission(user); + // 权限集合 + Set permissions = permissionService.getMenuPermission(user); + if (!loginUser.getPermissions().equals(permissions)) + { + loginUser.setPermissions(permissions); + tokenService.refreshToken(loginUser); + } + AjaxResult ajax = AjaxResult.success(); + ajax.put("user", user); + ajax.put("roles", roles); + ajax.put("permissions", permissions); + ajax.put("pwdChrtype", getSysAccountChrtype()); + ajax.put("isDefaultModifyPwd", initPasswordIsModify(user.getPwdUpdateDate())); + ajax.put("isPasswordExpired", passwordIsExpiration(user.getPwdUpdateDate())); + return ajax; + } + + /** + * 获取路由信息 + * + * @return 路由信息 + */ + @GetMapping("getRouters") + public AjaxResult getRouters() + { + Long userId = SecurityUtils.getUserId(); + List menus = menuService.selectMenuTreeByUserId(userId); + return AjaxResult.success(menuService.buildMenus(menus)); + } + + // 获取用户密码自定义配置规则 + public String getSysAccountChrtype() + { + return Convert.toStr(configService.selectConfigByKey("sys.account.chrtype"), "0"); + } + + // 检查初始密码是否提醒修改 + public boolean initPasswordIsModify(Date pwdUpdateDate) + { + Integer initPasswordModify = Convert.toInt(configService.selectConfigByKey("sys.account.initPasswordModify")); + return initPasswordModify != null && initPasswordModify == 1 && pwdUpdateDate == null; + } + + // 检查密码是否过期 + public boolean passwordIsExpiration(Date pwdUpdateDate) + { + Integer passwordValidateDays = Convert.toInt(configService.selectConfigByKey("sys.account.passwordValidateDays")); + if (passwordValidateDays != null && passwordValidateDays > 0) + { + if (StringUtils.isNull(pwdUpdateDate)) + { + // 如果从未修改过初始密码,直接提醒过期 + return true; + } + Date nowDate = DateUtils.getNowDate(); + return DateUtils.differentDaysByMillisecond(nowDate, pwdUpdateDate) > passwordValidateDays; + } + return false; + } +} diff --git a/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysMenuController.java b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysMenuController.java new file mode 100644 index 0000000..9d9fab6 --- /dev/null +++ b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysMenuController.java @@ -0,0 +1,165 @@ +package com.ruoyi.web.controller.system; + +import java.util.List; +import java.util.Map; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.entity.SysMenu; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.service.ISysMenuService; + +/** + * 菜单信息 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/system/menu") +public class SysMenuController extends BaseController +{ + @Autowired + private ISysMenuService menuService; + + /** + * 获取菜单列表 + */ + @PreAuthorize("@ss.hasPermi('system:menu:list')") + @GetMapping("/list") + public AjaxResult list(SysMenu menu) + { + List menus = menuService.selectMenuList(menu, getUserId()); + return success(menus); + } + + /** + * 根据菜单编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('system:menu:query')") + @GetMapping(value = "/{menuId}") + public AjaxResult getInfo(@PathVariable Long menuId) + { + return success(menuService.selectMenuById(menuId)); + } + + /** + * 获取菜单下拉树列表 + */ + @GetMapping("/treeselect") + public AjaxResult treeselect(SysMenu menu) + { + List menus = menuService.selectMenuList(menu, getUserId()); + return success(menuService.buildMenuTreeSelect(menus)); + } + + /** + * 加载对应角色菜单列表树 + */ + @GetMapping(value = "/roleMenuTreeselect/{roleId}") + public AjaxResult roleMenuTreeselect(@PathVariable("roleId") Long roleId) + { + List menus = menuService.selectMenuList(getUserId()); + AjaxResult ajax = AjaxResult.success(); + ajax.put("checkedKeys", menuService.selectMenuListByRoleId(roleId)); + ajax.put("menus", menuService.buildMenuTreeSelect(menus)); + return ajax; + } + + /** + * 新增菜单 + */ + @PreAuthorize("@ss.hasPermi('system:menu:add')") + @Log(title = "菜单管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysMenu menu) + { + if (!menuService.checkMenuNameUnique(menu)) + { + return error("新增菜单'" + menu.getMenuName() + "'失败,菜单名称已存在"); + } + else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath())) + { + return error("新增菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头"); + } + else if (!menuService.checkRouteConfigUnique(menu)) + { + return error("新增菜单'" + menu.getMenuName() + "'失败,路由名称或地址已存在"); + } + menu.setCreateBy(getUsername()); + return toAjax(menuService.insertMenu(menu)); + } + + /** + * 修改菜单 + */ + @PreAuthorize("@ss.hasPermi('system:menu:edit')") + @Log(title = "菜单管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysMenu menu) + { + if (!menuService.checkMenuNameUnique(menu)) + { + return error("修改菜单'" + menu.getMenuName() + "'失败,菜单名称已存在"); + } + else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath())) + { + return error("修改菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头"); + } + else if (menu.getMenuId().equals(menu.getParentId())) + { + return error("修改菜单'" + menu.getMenuName() + "'失败,上级菜单不能选择自己"); + } + else if (!menuService.checkRouteConfigUnique(menu)) + { + return error("修改菜单'" + menu.getMenuName() + "'失败,路由名称或地址已存在"); + } + menu.setUpdateBy(getUsername()); + return toAjax(menuService.updateMenu(menu)); + } + + /** + * 保存菜单排序 + */ + @PreAuthorize("@ss.hasPermi('system:menu:edit')") + @Log(title = "保存菜单排序", businessType = BusinessType.UPDATE) + @PutMapping("/updateSort") + public AjaxResult updateSort(@RequestBody Map params) + { + String[] menuIds = params.get("menuIds").split(","); + String[] orderNums = params.get("orderNums").split(","); + menuService.updateMenuSort(menuIds, orderNums); + return success(); + } + + /** + * 删除菜单 + */ + @PreAuthorize("@ss.hasPermi('system:menu:remove')") + @Log(title = "菜单管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{menuId}") + public AjaxResult remove(@PathVariable("menuId") Long menuId) + { + if (menuService.hasChildByMenuId(menuId)) + { + return warn("存在子菜单,不允许删除"); + } + if (menuService.checkMenuExistRole(menuId)) + { + return warn("菜单已分配,不允许删除"); + } + return toAjax(menuService.deleteMenuById(menuId)); + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysNoticeController.java b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysNoticeController.java new file mode 100644 index 0000000..84a3c73 --- /dev/null +++ b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysNoticeController.java @@ -0,0 +1,150 @@ +package com.ruoyi.web.controller.system; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.system.domain.SysNotice; +import com.ruoyi.system.service.ISysNoticeReadService; +import com.ruoyi.system.service.ISysNoticeService; + +/** + * 公告 信息操作处理 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/system/notice") +public class SysNoticeController extends BaseController +{ + @Autowired + private ISysNoticeService noticeService; + + @Autowired + private ISysNoticeReadService noticeReadService; + + /** + * 获取通知公告列表 + */ + @PreAuthorize("@ss.hasPermi('system:notice:list')") + @GetMapping("/list") + public TableDataInfo list(SysNotice notice) + { + startPage(); + List list = noticeService.selectNoticeList(notice); + return getDataTable(list); + } + + /** + * 根据通知公告编号获取详细信息 + */ + @GetMapping(value = "/{noticeId}") + public AjaxResult getInfo(@PathVariable Long noticeId) + { + return success(noticeService.selectNoticeById(noticeId)); + } + + /** + * 新增通知公告 + */ + @PreAuthorize("@ss.hasPermi('system:notice:add')") + @Log(title = "通知公告", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysNotice notice) + { + notice.setCreateBy(getUsername()); + return toAjax(noticeService.insertNotice(notice)); + } + + /** + * 修改通知公告 + */ + @PreAuthorize("@ss.hasPermi('system:notice:edit')") + @Log(title = "通知公告", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysNotice notice) + { + notice.setUpdateBy(getUsername()); + return toAjax(noticeService.updateNotice(notice)); + } + + /** + * 首页顶部公告列表(返回全部正常公告,带当前用户已读标记,最多5条) + */ + @GetMapping("/listTop") + @ResponseBody + public AjaxResult listTop() + { + Long userId = getUserId(); + List list = noticeReadService.selectNoticeListWithReadStatus(userId, 5); + long unreadCount = list.stream().filter(n -> !n.getIsRead()).count(); + AjaxResult result = AjaxResult.success(list); + result.put("unreadCount", unreadCount); + return result; + } + + /** + * 标记公告已读 + */ + @PostMapping("/markRead") + @ResponseBody + public AjaxResult markRead(Long noticeId) + { + Long userId = getUserId(); + noticeReadService.markRead(noticeId, userId); + return success(); + } + + /** + * 批量标记已读 + */ + @PostMapping("/markReadAll") + @ResponseBody + public AjaxResult markReadAll(String ids) + { + Long userId = getUserId(); + Long[] noticeIds = Convert.toLongArray(ids); + noticeReadService.markReadBatch(userId, noticeIds); + return success(); + } + + /** + * 已读用户列表数据 + */ + @PreAuthorize("@ss.hasPermi('system:notice:list')") + @GetMapping("/readUsers/list") + @ResponseBody + public TableDataInfo readUsersList(Long noticeId, String searchValue) + { + startPage(); + List list = noticeReadService.selectReadUsersByNoticeId(noticeId, searchValue); + return getDataTable(list); + } + + /** + * 删除通知公告 + */ + @PreAuthorize("@ss.hasPermi('system:notice:remove')") + @Log(title = "通知公告", businessType = BusinessType.DELETE) + @DeleteMapping("/{noticeIds}") + public AjaxResult remove(@PathVariable Long[] noticeIds) + { + noticeReadService.deleteByNoticeIds(noticeIds); + return toAjax(noticeService.deleteNoticeByIds(noticeIds)); + } +} diff --git a/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysPostController.java b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysPostController.java new file mode 100644 index 0000000..5b604cf --- /dev/null +++ b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysPostController.java @@ -0,0 +1,129 @@ +package com.ruoyi.web.controller.system; + +import java.util.List; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.system.domain.SysPost; +import com.ruoyi.system.service.ISysPostService; + +/** + * 岗位信息操作处理 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/system/post") +public class SysPostController extends BaseController +{ + @Autowired + private ISysPostService postService; + + /** + * 获取岗位列表 + */ + @PreAuthorize("@ss.hasPermi('system:post:list')") + @GetMapping("/list") + public TableDataInfo list(SysPost post) + { + startPage(); + List list = postService.selectPostList(post); + return getDataTable(list); + } + + @Log(title = "岗位管理", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('system:post:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysPost post) + { + List list = postService.selectPostList(post); + ExcelUtil util = new ExcelUtil(SysPost.class); + util.exportExcel(response, list, "岗位数据"); + } + + /** + * 根据岗位编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('system:post:query')") + @GetMapping(value = "/{postId}") + public AjaxResult getInfo(@PathVariable Long postId) + { + return success(postService.selectPostById(postId)); + } + + /** + * 新增岗位 + */ + @PreAuthorize("@ss.hasPermi('system:post:add')") + @Log(title = "岗位管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysPost post) + { + if (!postService.checkPostNameUnique(post)) + { + return error("新增岗位'" + post.getPostName() + "'失败,岗位名称已存在"); + } + else if (!postService.checkPostCodeUnique(post)) + { + return error("新增岗位'" + post.getPostName() + "'失败,岗位编码已存在"); + } + post.setCreateBy(getUsername()); + return toAjax(postService.insertPost(post)); + } + + /** + * 修改岗位 + */ + @PreAuthorize("@ss.hasPermi('system:post:edit')") + @Log(title = "岗位管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysPost post) + { + if (!postService.checkPostNameUnique(post)) + { + return error("修改岗位'" + post.getPostName() + "'失败,岗位名称已存在"); + } + else if (!postService.checkPostCodeUnique(post)) + { + return error("修改岗位'" + post.getPostName() + "'失败,岗位编码已存在"); + } + post.setUpdateBy(getUsername()); + return toAjax(postService.updatePost(post)); + } + + /** + * 删除岗位 + */ + @PreAuthorize("@ss.hasPermi('system:post:remove')") + @Log(title = "岗位管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{postIds}") + public AjaxResult remove(@PathVariable Long[] postIds) + { + return toAjax(postService.deletePostByIds(postIds)); + } + + /** + * 获取岗位选择框列表 + */ + @GetMapping("/optionselect") + public AjaxResult optionselect() + { + List posts = postService.selectPostAll(); + return success(posts); + } +} diff --git a/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java new file mode 100644 index 0000000..dd2926f --- /dev/null +++ b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java @@ -0,0 +1,216 @@ +package com.ruoyi.web.controller.system; + +import java.util.Map; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.file.FileUploadUtils; +import com.ruoyi.common.utils.file.FileUtils; +import com.ruoyi.common.utils.file.MimeTypeUtils; +import com.ruoyi.framework.web.service.TokenService; +import com.ruoyi.system.service.ISysUserService; +import com.ruoyi.business.dto.SmsValidForm; +import com.ruoyi.business.service.SysSmsService; + +/** + * 个人信息 业务处理 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/system/user/profile") +public class SysProfileController extends BaseController +{ + @Autowired + private ISysUserService userService; + + @Autowired + private TokenService tokenService; + + @Autowired + private SysSmsService smsService; + + /** + * 个人信息 + */ + @GetMapping + public AjaxResult profile() + { + LoginUser loginUser = getLoginUser(); + SysUser user = loginUser.getUser(); + AjaxResult ajax = AjaxResult.success(user); + ajax.put("roleGroup", userService.selectUserRoleGroup(loginUser.getUsername())); + ajax.put("postGroup", userService.selectUserPostGroup(loginUser.getUsername())); + return ajax; + } + + /** + * 修改用户 + */ + @Log(title = "个人信息", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult updateProfile(@RequestBody SysUser user) + { + LoginUser loginUser = getLoginUser(); + SysUser currentUser = loginUser.getUser(); + currentUser.setNickName(user.getNickName()); + currentUser.setEmail(user.getEmail()); + currentUser.setPhonenumber(user.getPhonenumber()); + currentUser.setSex(user.getSex()); + if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(currentUser)) + { + return error("修改用户'" + loginUser.getUsername() + "'失败,手机号码已存在"); + } + if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(currentUser)) + { + return error("修改用户'" + loginUser.getUsername() + "'失败,邮箱账号已存在"); + } + if (userService.updateUserProfile(currentUser) > 0) + { + // 更新缓存用户信息 + tokenService.setLoginUser(loginUser); + return success(); + } + return error("修改个人信息异常,请联系管理员"); + } + + /** + * 重置密码 + */ + @Log(title = "个人信息", businessType = BusinessType.UPDATE) + @PutMapping("/updatePwd") + public AjaxResult updatePwd(@RequestBody Map params) + { + String oldPassword = params.get("oldPassword"); + String newPassword = params.get("newPassword"); + LoginUser loginUser = getLoginUser(); + Long userId = loginUser.getUserId(); + SysUser user = userService.selectUserById(userId); + String password = user.getPassword(); + if (!SecurityUtils.matchesPassword(oldPassword, password)) + { + return error("修改密码失败,旧密码错误"); + } + if (SecurityUtils.matchesPassword(newPassword, password)) + { + return error("新密码不能与旧密码相同"); + } + newPassword = SecurityUtils.encryptPassword(newPassword); + if (userService.resetUserPwd(userId, newPassword) > 0) + { + // 更新缓存用户密码&密码最后更新时间 + loginUser.getUser().setPwdUpdateDate(DateUtils.getNowDate()); + loginUser.getUser().setPassword(newPassword); + tokenService.setLoginUser(loginUser); + return success(); + } + return error("修改密码异常,请联系管理员"); + } + + /** + * 修改手机号 (需短信验证码) + * 参数: { phone, smsCode, uuid } + */ + @Log(title = "个人信息", businessType = BusinessType.UPDATE) + @PostMapping("/changePhone") + public AjaxResult changePhone(@RequestBody SmsValidForm form) + { + if (StringUtils.isEmpty(form.getPhone()) || StringUtils.isEmpty(form.getSmsCode()) || StringUtils.isEmpty(form.getUuid())) + { + return error("参数不完整"); + } + // 1. 校验短信验证码 + try + { + smsService.verifyCode(form); + } + catch (RuntimeException e) + { + return error(e.getMessage()); + } + // 2. 更新当前用户手机号 + LoginUser loginUser = getLoginUser(); + SysUser currentUser = loginUser.getUser(); + currentUser.setPhonenumber(form.getPhone()); + if (!userService.checkPhoneUnique(currentUser)) + { + return error("手机号码已被占用"); + } + if (userService.updateUserProfile(currentUser) > 0) + { + // 同步缓存 + tokenService.setLoginUser(loginUser); + return success(); + } + return error("修改手机号失败"); + } + + /** + * 头像上传 (本地, RuoYi 默认) + */ + @Log(title = "用户头像", businessType = BusinessType.UPDATE) + @PostMapping("/avatar") + public AjaxResult avatar(@RequestParam("avatarfile") MultipartFile file) throws Exception + { + if (!file.isEmpty()) + { + LoginUser loginUser = getLoginUser(); + String avatar = FileUploadUtils.upload(RuoYiConfig.getAvatarPath(), file, MimeTypeUtils.IMAGE_EXTENSION, true); + if (userService.updateUserAvatar(loginUser.getUserId(), avatar)) + { + String oldAvatar = loginUser.getUser().getAvatar(); + if (StringUtils.isNotEmpty(oldAvatar)) + { + FileUtils.deleteFile(RuoYiConfig.getProfile() + FileUtils.stripPrefix(oldAvatar)); + } + AjaxResult ajax = AjaxResult.success(); + ajax.put("imgUrl", avatar); + // 更新缓存用户头像 + loginUser.getUser().setAvatar(avatar); + tokenService.setLoginUser(loginUser); + return ajax; + } + } + return error("上传图片异常,请联系管理员"); + } + + /** + * OSS 直传头像 (前端已上传到 OSS, 后端只负责写 sys_user.avatar 字段) + * 入参: { avatarUrl } + */ + @Log(title = "用户头像", businessType = BusinessType.UPDATE) + @PutMapping("/avatarUrl") + public AjaxResult updateAvatarUrl(@RequestBody Map body) + { + String avatarUrl = body.get("avatarUrl"); + if (StringUtils.isEmpty(avatarUrl)) + { + return error("avatarUrl 不能为空"); + } + LoginUser loginUser = getLoginUser(); + if (userService.updateUserAvatar(loginUser.getUserId(), avatarUrl)) + { + // 更新缓存用户头像 + loginUser.getUser().setAvatar(avatarUrl); + tokenService.setLoginUser(loginUser); + return success(); + } + return error("更新头像失败"); + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRegisterController.java b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRegisterController.java new file mode 100644 index 0000000..fe19249 --- /dev/null +++ b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRegisterController.java @@ -0,0 +1,38 @@ +package com.ruoyi.web.controller.system; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.model.RegisterBody; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.web.service.SysRegisterService; +import com.ruoyi.system.service.ISysConfigService; + +/** + * 注册验证 + * + * @author ruoyi + */ +@RestController +public class SysRegisterController extends BaseController +{ + @Autowired + private SysRegisterService registerService; + + @Autowired + private ISysConfigService configService; + + @PostMapping("/register") + public AjaxResult register(@RequestBody RegisterBody user) + { + if (!("true".equals(configService.selectConfigByKey("sys.account.registerUser")))) + { + return error("当前系统没有开启注册功能!"); + } + String msg = registerService.register(user); + return StringUtils.isEmpty(msg) ? success() : error(msg); + } +} diff --git a/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRoleController.java b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRoleController.java new file mode 100644 index 0000000..a5643a0 --- /dev/null +++ b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRoleController.java @@ -0,0 +1,254 @@ +package com.ruoyi.web.controller.system; + +import java.util.List; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.entity.SysDept; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.framework.web.service.SysPermissionService; +import com.ruoyi.framework.web.service.TokenService; +import com.ruoyi.system.domain.SysUserRole; +import com.ruoyi.system.service.ISysDeptService; +import com.ruoyi.system.service.ISysRoleService; +import com.ruoyi.system.service.ISysUserService; + +/** + * 角色信息 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/system/role") +public class SysRoleController extends BaseController +{ + @Autowired + private ISysRoleService roleService; + + @Autowired + private TokenService tokenService; + + @Autowired + private SysPermissionService permissionService; + + @Autowired + private ISysUserService userService; + + @Autowired + private ISysDeptService deptService; + + @PreAuthorize("@ss.hasPermi('system:role:list')") + @GetMapping("/list") + public TableDataInfo list(SysRole role) + { + startPage(); + List list = roleService.selectRoleList(role); + return getDataTable(list); + } + + @Log(title = "角色管理", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('system:role:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysRole role) + { + List list = roleService.selectRoleList(role); + ExcelUtil util = new ExcelUtil(SysRole.class); + util.exportExcel(response, list, "角色数据"); + } + + /** + * 根据角色编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('system:role:query')") + @GetMapping(value = "/{roleId}") + public AjaxResult getInfo(@PathVariable Long roleId) + { + roleService.checkRoleDataScope(roleId); + return success(roleService.selectRoleById(roleId)); + } + + /** + * 新增角色 + */ + @PreAuthorize("@ss.hasPermi('system:role:add')") + @Log(title = "角色管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysRole role) + { + if (!roleService.checkRoleNameUnique(role)) + { + return error("新增角色'" + role.getRoleName() + "'失败,角色名称已存在"); + } + else if (!roleService.checkRoleKeyUnique(role)) + { + return error("新增角色'" + role.getRoleName() + "'失败,角色权限已存在"); + } + role.setCreateBy(getUsername()); + return toAjax(roleService.insertRole(role)); + + } + + /** + * 修改保存角色 + */ + @PreAuthorize("@ss.hasPermi('system:role:edit')") + @Log(title = "角色管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysRole role) + { + roleService.checkRoleAllowed(role); + roleService.checkRoleDataScope(role.getRoleId()); + if (!roleService.checkRoleNameUnique(role)) + { + return error("修改角色'" + role.getRoleName() + "'失败,角色名称已存在"); + } + else if (!roleService.checkRoleKeyUnique(role)) + { + return error("修改角色'" + role.getRoleName() + "'失败,角色权限已存在"); + } + role.setUpdateBy(getUsername()); + + if (roleService.updateRole(role) > 0) + { + // 刷新所有持有该角色的在线用户权限 + tokenService.refreshPermissionByRoleId(role.getRoleId(), permissionService); + return success(); + } + return error("修改角色'" + role.getRoleName() + "'失败,请联系管理员"); + } + + /** + * 修改保存数据权限 + */ + @PreAuthorize("@ss.hasPermi('system:role:edit')") + @Log(title = "角色管理", businessType = BusinessType.UPDATE) + @PutMapping("/dataScope") + public AjaxResult dataScope(@RequestBody SysRole role) + { + roleService.checkRoleAllowed(role); + roleService.checkRoleDataScope(role.getRoleId()); + return toAjax(roleService.authDataScope(role)); + } + + /** + * 状态修改 + */ + @PreAuthorize("@ss.hasPermi('system:role:edit')") + @Log(title = "角色管理", businessType = BusinessType.UPDATE) + @PutMapping("/changeStatus") + public AjaxResult changeStatus(@RequestBody SysRole role) + { + roleService.checkRoleAllowed(role); + roleService.checkRoleDataScope(role.getRoleId()); + role.setUpdateBy(getUsername()); + return toAjax(roleService.updateRoleStatus(role)); + } + + /** + * 删除角色 + */ + @PreAuthorize("@ss.hasPermi('system:role:remove')") + @Log(title = "角色管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{roleIds}") + public AjaxResult remove(@PathVariable Long[] roleIds) + { + return toAjax(roleService.deleteRoleByIds(roleIds)); + } + + /** + * 获取角色选择框列表 + */ + @PreAuthorize("@ss.hasPermi('system:role:query')") + @GetMapping("/optionselect") + public AjaxResult optionselect() + { + return success(roleService.selectRoleAll()); + } + + /** + * 查询已分配用户角色列表 + */ + @PreAuthorize("@ss.hasPermi('system:role:list')") + @GetMapping("/authUser/allocatedList") + public TableDataInfo allocatedList(SysUser user) + { + startPage(); + List list = userService.selectAllocatedList(user); + return getDataTable(list); + } + + /** + * 查询未分配用户角色列表 + */ + @PreAuthorize("@ss.hasPermi('system:role:list')") + @GetMapping("/authUser/unallocatedList") + public TableDataInfo unallocatedList(SysUser user) + { + startPage(); + List list = userService.selectUnallocatedList(user); + return getDataTable(list); + } + + /** + * 取消授权用户 + */ + @PreAuthorize("@ss.hasPermi('system:role:edit')") + @Log(title = "角色管理", businessType = BusinessType.GRANT) + @PutMapping("/authUser/cancel") + public AjaxResult cancelAuthUser(@RequestBody SysUserRole userRole) + { + return toAjax(roleService.deleteAuthUser(userRole)); + } + + /** + * 批量取消授权用户 + */ + @PreAuthorize("@ss.hasPermi('system:role:edit')") + @Log(title = "角色管理", businessType = BusinessType.GRANT) + @PutMapping("/authUser/cancelAll") + public AjaxResult cancelAuthUserAll(Long roleId, Long[] userIds) + { + return toAjax(roleService.deleteAuthUsers(roleId, userIds)); + } + + /** + * 批量选择用户授权 + */ + @PreAuthorize("@ss.hasPermi('system:role:edit')") + @Log(title = "角色管理", businessType = BusinessType.GRANT) + @PutMapping("/authUser/selectAll") + public AjaxResult selectAuthUserAll(Long roleId, Long[] userIds) + { + roleService.checkRoleDataScope(roleId); + return toAjax(roleService.insertAuthUsers(roleId, userIds)); + } + + /** + * 获取对应角色部门树列表 + */ + @PreAuthorize("@ss.hasPermi('system:role:query')") + @GetMapping(value = "/deptTree/{roleId}") + public AjaxResult deptTree(@PathVariable("roleId") Long roleId) + { + AjaxResult ajax = AjaxResult.success(); + ajax.put("checkedKeys", deptService.selectDeptListByRoleId(roleId)); + ajax.put("depts", deptService.selectDeptTreeList(new SysDept())); + return ajax; + } +} diff --git a/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java new file mode 100644 index 0000000..c4bf05e --- /dev/null +++ b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java @@ -0,0 +1,256 @@ +package com.ruoyi.web.controller.system; + +import java.util.List; +import java.util.stream.Collectors; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.commons.lang3.ArrayUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.entity.SysDept; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.system.service.ISysDeptService; +import com.ruoyi.system.service.ISysPostService; +import com.ruoyi.system.service.ISysRoleService; +import com.ruoyi.system.service.ISysUserService; + +/** + * 用户信息 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/system/user") +public class SysUserController extends BaseController +{ + @Autowired + private ISysUserService userService; + + @Autowired + private ISysRoleService roleService; + + @Autowired + private ISysDeptService deptService; + + @Autowired + private ISysPostService postService; + + /** + * 获取用户列表 + */ + @PreAuthorize("@ss.hasPermi('system:user:list')") + @GetMapping("/list") + public TableDataInfo list(SysUser user) + { + startPage(); + List list = userService.selectUserList(user); + return getDataTable(list); + } + + @Log(title = "用户管理", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('system:user:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysUser user) + { + List list = userService.selectUserList(user); + ExcelUtil util = new ExcelUtil(SysUser.class); + util.exportExcel(response, list, "用户数据"); + } + + @Log(title = "用户管理", businessType = BusinessType.IMPORT) + @PreAuthorize("@ss.hasPermi('system:user:import')") + @PostMapping("/importData") + public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception + { + ExcelUtil util = new ExcelUtil(SysUser.class); + List userList = util.importExcel(file.getInputStream()); + String operName = getUsername(); + String message = userService.importUser(userList, updateSupport, operName); + return success(message); + } + + @PostMapping("/importTemplate") + public void importTemplate(HttpServletResponse response) + { + ExcelUtil util = new ExcelUtil(SysUser.class); + util.importTemplateExcel(response, "用户数据"); + } + + /** + * 根据用户编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('system:user:query')") + @GetMapping(value = { "/", "/{userId}" }) + public AjaxResult getInfo(@PathVariable(value = "userId", required = false) Long userId) + { + AjaxResult ajax = AjaxResult.success(); + if (StringUtils.isNotNull(userId)) + { + userService.checkUserDataScope(userId); + SysUser sysUser = userService.selectUserById(userId); + ajax.put(AjaxResult.DATA_TAG, sysUser); + ajax.put("postIds", postService.selectPostListByUserId(userId)); + ajax.put("roleIds", sysUser.getRoles().stream().map(SysRole::getRoleId).collect(Collectors.toList())); + } + List roles = roleService.selectRoleAll(); + ajax.put("roles", SecurityUtils.isAdmin(userId) ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList())); + ajax.put("posts", postService.selectPostAll()); + return ajax; + } + + /** + * 新增用户 + */ + @PreAuthorize("@ss.hasPermi('system:user:add')") + @Log(title = "用户管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysUser user) + { + deptService.checkDeptDataScope(user.getDeptId()); + roleService.checkRoleDataScope(user.getRoleIds()); + if (!userService.checkUserNameUnique(user)) + { + return error("新增用户'" + user.getUserName() + "'失败,登录账号已存在"); + } + else if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(user)) + { + return error("新增用户'" + user.getUserName() + "'失败,手机号码已存在"); + } + else if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(user)) + { + return error("新增用户'" + user.getUserName() + "'失败,邮箱账号已存在"); + } + user.setCreateBy(getUsername()); + user.setPassword(SecurityUtils.encryptPassword(user.getPassword())); + return toAjax(userService.insertUser(user)); + } + + /** + * 修改用户 + */ + @PreAuthorize("@ss.hasPermi('system:user:edit')") + @Log(title = "用户管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysUser user) + { + userService.checkUserAllowed(user); + userService.checkUserDataScope(user.getUserId()); + deptService.checkDeptDataScope(user.getDeptId()); + roleService.checkRoleDataScope(user.getRoleIds()); + if (!userService.checkUserNameUnique(user)) + { + return error("修改用户'" + user.getUserName() + "'失败,登录账号已存在"); + } + else if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(user)) + { + return error("修改用户'" + user.getUserName() + "'失败,手机号码已存在"); + } + else if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(user)) + { + return error("修改用户'" + user.getUserName() + "'失败,邮箱账号已存在"); + } + user.setUpdateBy(getUsername()); + return toAjax(userService.updateUser(user)); + } + + /** + * 删除用户 + */ + @PreAuthorize("@ss.hasPermi('system:user:remove')") + @Log(title = "用户管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{userIds}") + public AjaxResult remove(@PathVariable Long[] userIds) + { + if (ArrayUtils.contains(userIds, getUserId())) + { + return error("当前用户不能删除"); + } + return toAjax(userService.deleteUserByIds(userIds)); + } + + /** + * 重置密码 + */ + @PreAuthorize("@ss.hasPermi('system:user:resetPwd')") + @Log(title = "用户管理", businessType = BusinessType.UPDATE) + @PutMapping("/resetPwd") + public AjaxResult resetPwd(@RequestBody SysUser user) + { + userService.checkUserAllowed(user); + userService.checkUserDataScope(user.getUserId()); + user.setPassword(SecurityUtils.encryptPassword(user.getPassword())); + user.setUpdateBy(getUsername()); + return toAjax(userService.resetPwd(user)); + } + + /** + * 状态修改 + */ + @PreAuthorize("@ss.hasPermi('system:user:edit')") + @Log(title = "用户管理", businessType = BusinessType.UPDATE) + @PutMapping("/changeStatus") + public AjaxResult changeStatus(@RequestBody SysUser user) + { + userService.checkUserAllowed(user); + userService.checkUserDataScope(user.getUserId()); + user.setUpdateBy(getUsername()); + return toAjax(userService.updateUserStatus(user)); + } + + /** + * 根据用户编号获取授权角色 + */ + @PreAuthorize("@ss.hasPermi('system:user:query')") + @GetMapping("/authRole/{userId}") + public AjaxResult authRole(@PathVariable("userId") Long userId) + { + AjaxResult ajax = AjaxResult.success(); + SysUser user = userService.selectUserById(userId); + List roles = roleService.selectRolesByUserId(userId); + ajax.put("user", user); + ajax.put("roles", SecurityUtils.isAdmin(userId) ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList())); + return ajax; + } + + /** + * 用户授权角色 + */ + @PreAuthorize("@ss.hasPermi('system:user:edit')") + @Log(title = "用户管理", businessType = BusinessType.GRANT) + @PutMapping("/authRole") + public AjaxResult insertAuthRole(Long userId, Long[] roleIds) + { + userService.checkUserDataScope(userId); + roleService.checkRoleDataScope(roleIds); + userService.insertUserAuth(userId, roleIds); + return success(); + } + + /** + * 获取部门树列表 + */ + @PreAuthorize("@ss.hasPermi('system:user:list')") + @GetMapping("/deptTree") + public AjaxResult deptTree(SysDept dept) + { + return success(deptService.selectDeptTreeList(dept)); + } +} diff --git a/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/TestController.java b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/TestController.java new file mode 100644 index 0000000..586e1b8 --- /dev/null +++ b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/TestController.java @@ -0,0 +1,175 @@ +package com.ruoyi.web.controller.tool; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.StringUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.tags.Tag; + +/** + * swagger 用户测试方法 + * + * @author ruoyi + */ +@Tag(name = "用户信息管理") +@RestController +@RequestMapping("/test/user") +public class TestController extends BaseController +{ + private final static Map users = new LinkedHashMap(); + { + users.put(1, new UserEntity(1, "admin", "admin123", "15888888888")); + users.put(2, new UserEntity(2, "ry", "admin123", "15666666666")); + } + + @Operation(summary = "获取用户列表") + @GetMapping("/list") + public R> userList() + { + List userList = new ArrayList(users.values()); + return R.ok(userList); + } + + @Operation(summary = "获取用户详细") + @GetMapping("/{userId}") + public R getUser(@PathVariable(name = "userId") + Integer userId) + { + if (!users.isEmpty() && users.containsKey(userId)) + { + return R.ok(users.get(userId)); + } + else + { + return R.fail("用户不存在"); + } + } + + @Operation(summary = "新增用户") + @PostMapping("/save") + public R save(UserEntity user) + { + if (StringUtils.isNull(user) || StringUtils.isNull(user.getUserId())) + { + return R.fail("用户ID不能为空"); + } + users.put(user.getUserId(), user); + return R.ok(); + } + + @Operation(summary = "更新用户") + @PutMapping("/update") + public R update(@RequestBody + UserEntity user) + { + if (StringUtils.isNull(user) || StringUtils.isNull(user.getUserId())) + { + return R.fail("用户ID不能为空"); + } + if (users.isEmpty() || !users.containsKey(user.getUserId())) + { + return R.fail("用户不存在"); + } + users.remove(user.getUserId()); + users.put(user.getUserId(), user); + return R.ok(); + } + + @Operation(summary = "删除用户信息") + @DeleteMapping("/{userId}") + public R delete(@PathVariable(name = "userId") + Integer userId) + { + if (!users.isEmpty() && users.containsKey(userId)) + { + users.remove(userId); + return R.ok(); + } + else + { + return R.fail("用户不存在"); + } + } +} + +@Schema(description = "用户实体") +class UserEntity +{ + @Schema(title = "用户ID") + private Integer userId; + + @Schema(title = "用户名称") + private String username; + + @Schema(title = "用户密码") + private String password; + + @Schema(title = "用户手机") + private String mobile; + + public UserEntity() + { + + } + + public UserEntity(Integer userId, String username, String password, String mobile) + { + this.userId = userId; + this.username = username; + this.password = password; + this.mobile = mobile; + } + + public Integer getUserId() + { + return userId; + } + + public void setUserId(Integer userId) + { + this.userId = userId; + } + + public String getUsername() + { + return username; + } + + public void setUsername(String username) + { + this.username = username; + } + + public String getPassword() + { + return password; + } + + public void setPassword(String password) + { + this.password = password; + } + + public String getMobile() + { + return mobile; + } + + public void setMobile(String mobile) + { + this.mobile = mobile; + } +} diff --git a/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/core/config/SwaggerConfig.java b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/core/config/SwaggerConfig.java new file mode 100644 index 0000000..9ab7c24 --- /dev/null +++ b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/core/config/SwaggerConfig.java @@ -0,0 +1,64 @@ +package com.ruoyi.web.core.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import com.ruoyi.common.config.RuoYiConfig; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; + +/** + * Swagger2的接口配置 + * + * @author ruoyi + */ +@Configuration +public class SwaggerConfig +{ + /** 系统基础配置 */ + @Autowired + private RuoYiConfig ruoyiConfig; + + /** + * 自定义的 OpenAPI 对象 + */ + @Bean + public OpenAPI customOpenApi() + { + return new OpenAPI().components(new Components() + // 设置认证的请求头 + .addSecuritySchemes("apikey", securityScheme())) + .addSecurityItem(new SecurityRequirement().addList("apikey")) + .info(getApiInfo()); + } + + @Bean + public SecurityScheme securityScheme() + { + return new SecurityScheme() + .type(SecurityScheme.Type.APIKEY) + .name("Authorization") + .in(SecurityScheme.In.HEADER) + .scheme("Bearer"); + } + + /** + * 添加摘要信息 + */ + public Info getApiInfo() + { + return new Info() + // 设置标题 + .title("标题:若依管理系统_接口文档") + // 描述 + .description("描述:用于管理集团旗下公司的人员信息,具体包括XXX,XXX模块...") + // 作者信息 + .contact(new Contact().name(ruoyiConfig.getName())) + // 版本 + .version("版本号:" + ruoyiConfig.getVersion()); + } +} diff --git a/ry-api/ruoyi-admin/src/main/resources/META-INF/spring-devtools.properties b/ry-api/ruoyi-admin/src/main/resources/META-INF/spring-devtools.properties new file mode 100644 index 0000000..37e7b58 --- /dev/null +++ b/ry-api/ruoyi-admin/src/main/resources/META-INF/spring-devtools.properties @@ -0,0 +1 @@ +restart.include.json=/com.alibaba.fastjson2.*.jar \ No newline at end of file diff --git a/ry-api/ruoyi-admin/src/main/resources/application-druid.yml b/ry-api/ruoyi-admin/src/main/resources/application-druid.yml new file mode 100644 index 0000000..95480d3 --- /dev/null +++ b/ry-api/ruoyi-admin/src/main/resources/application-druid.yml @@ -0,0 +1,50 @@ +# 数据源配置 +spring: + datasource: + type: com.alibaba.druid.pool.DruidDataSource + driverClassName: com.mysql.cj.jdbc.Driver + druid: + master: + url: jdbc:mysql://127.0.0.1:3306/guoju0808?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 + username: root + password: cu2oh2co3 + slave: + enabled: false + url: + username: + password: + initialSize: 5 + minIdle: 10 + maxActive: 20 + maxWait: 60000 + connectTimeout: 30000 + socketTimeout: 60000 + timeBetweenEvictionRunsMillis: 60000 + minEvictableIdleTimeMillis: 300000 + maxEvictableIdleTimeMillis: 900000 + validationQuery: SELECT 1 FROM DUAL + testWhileIdle: true + testOnBorrow: false + testOnReturn: false + webStatFilter: + enabled: true + statViewServlet: + enabled: true + allow: + url-pattern: /druid/* + login-username: ruoyi + login-password: 123456 + filter: + stat: + enabled: true + log-slow-sql: true + slow-sql-millis: 1000 + merge-sql: true + wall: + config: + multi-statement-allow: true + +xss: + enabled: true + excludes: /system/notice + urlPatterns: /system/*,/monitor/*,/tool/* \ No newline at end of file diff --git a/ry-api/ruoyi-admin/src/main/resources/application.yml b/ry-api/ruoyi-admin/src/main/resources/application.yml new file mode 100644 index 0000000..aec7500 --- /dev/null +++ b/ry-api/ruoyi-admin/src/main/resources/application.yml @@ -0,0 +1,167 @@ +# 项目相关配置 +ruoyi: + # 名称 + name: RuoYi + # 版本 + version: 3.9.2 + # 版权年份 + copyrightYear: 2026 + # 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath) + profile: D:/ruoyi/uploadPath + # 获取ip地址开关 + addressEnabled: false + # 验证码类型 math 数字计算 char 字符验证 + captchaType: math + # OSS 配置 (前端直传签名, secretKey 仅后端使用) + oss: + enabled: true + endpoint: https://hwtossbamlorgcn.oss-cn-beijing.aliyuncs.com + bucket: hwtossbamlorgcn + accessKeyId: LTAI5tQKWWu96ihmg2VChQsY + accessKeySecret: gA0fTFa8arUYaYA5Us2ClPKVSM1Ypv + dirPrefix: ry8080/ + expireSeconds: 3600 + # 阿里云短信配置 (hwt-code ali.sms 模式) + # dev/prod 区分走代码: phone 以 "10" 开头视为 dev 测试 (固定码 1234), 其它走 aliyun 真发 + sms: + accessKeyId: LTAI5t7S88DmdTxHPzPtJTwG + accessKeySecret: UIkwjMpmlYjgX5IMLjPj8FQNPthdlR + signName: 北京仙仁掌医学科技发展 + template: SMS_321560247 + inviteTemplate: SMS_492460505 + endpoint: dysmsapi.aliyuncs.com + regionId: cn-hangzhou + +# 开发环境配置 +server: + # 服务器的HTTP端口,默认为8080 + port: 8080 + servlet: + # 应用的访问路径 + context-path: / + tomcat: + # tomcat的URI编码 + uri-encoding: UTF-8 + # 连接数满后的排队数,默认为100 + accept-count: 1000 + threads: + # tomcat最大线程数,默认为200 + max: 800 + # Tomcat启动初始化的线程数,默认值10 + min-spare: 100 + +# 日志配置 +logging: + level: + com.ruoyi: debug + org.springframework: debug + com.ruoyi.business: debug + +# 用户配置 +user: + password: + # 密码最大错误次数 + maxRetryCount: 5 + # 密码锁定时间(默认10分钟) + lockTime: 10 + +# Spring配置 +spring: + # 资源信息 + messages: + # 国际化资源文件路径 + basename: i18n/messages + profiles: + active: druid + # 文件上传 + servlet: + multipart: + # 单个文件大小 + max-file-size: 10MB + # 设置总上传的文件大小 + max-request-size: 20MB + jackson: + time-zone: GMT+8 + date-format: yyyy-MM-dd HH:mm:ss + # 服务模块 + devtools: + restart: + # 热部署开关 + enabled: true + data: + # redis 配置 + redis: + # 地址 + host: localhost + # 端口,默认为6379 + port: 6379 + # 数据库索引 + database: 0 + # 密码 + password: + # 连接超时时间 + timeout: 10s + lettuce: + pool: + # 连接池中的最小空闲连接 + min-idle: 0 + # 连接池中的最大空闲连接 + max-idle: 8 + # 连接池的最大数据库连接数 + max-active: 8 + # #连接池最大阻塞等待时间(使用负值表示没有限制) + max-wait: -1ms + +# token配置 +token: + # 令牌自定义标识 + header: Authorization + # 令牌密钥 + secret: abcdefghijklmnopqrstuvwxyz + # 令牌有效期(默认30分钟) + expireTime: 30 + +# MyBatis配置 +mybatis: + # 搜索指定包别名 + typeAliasesPackage: com.ruoyi.**.domain + # 配置mapper的扫描,找到所有的mapper.xml映射文件 + mapperLocations: classpath*:mapper/**/*Mapper.xml + # 加载全局的配置文件 + configLocation: classpath:mybatis/mybatis-config.xml + +# PageHelper分页插件 +pagehelper: + helperDialect: mysql + supportMethodsArguments: true + params: count=countSql + +# Springdoc配置 +springdoc: + api-docs: + path: /v3/api-docs + swagger-ui: + enabled: true + path: /swagger-ui.html + tags-sorter: alpha + group-configs: + - group: 'default' + display-name: '测试模块' + paths-to-match: '/**' + packages-to-scan: com.ruoyi.web.controller.tool + +# 防盗链配置 +referer: + # 防盗链开关 + enabled: false + # 允许的域名列表 + allowed-domains: localhost,127.0.0.1,ruoyi.vip,www.ruoyi.vip + +# 防止XSS攻击 +xss: + # 过滤开关 + enabled: true + # 排除链接(多个用逗号分隔) + excludes: /system/notice + # 匹配链接 + urlPatterns: /system/*,/monitor/*,/tool/* \ No newline at end of file diff --git a/ry-api/ruoyi-admin/src/main/resources/banner.txt b/ry-api/ruoyi-admin/src/main/resources/banner.txt new file mode 100644 index 0000000..0931cb8 --- /dev/null +++ b/ry-api/ruoyi-admin/src/main/resources/banner.txt @@ -0,0 +1,24 @@ +Application Version: ${ruoyi.version} +Spring Boot Version: ${spring-boot.version} +//////////////////////////////////////////////////////////////////// +// _ooOoo_ // +// o8888888o // +// 88" . "88 // +// (| ^_^ |) // +// O\ = /O // +// ____/`---'\____ // +// .' \\| |// `. // +// / \\||| : |||// \ // +// / _||||| -:- |||||- \ // +// | | \\\ - /// | | // +// | \_| ''\---/'' | | // +// \ .-\__ `-` ___/-. / // +// ___`. .' /--.--\ `. . ___ // +// ."" '< `.___\_<|>_/___.' >'"". // +// | | : `- \`.;`\ _ /`;.`/ - ` : | | // +// \ \ `-. \_ __\ /__ _/ .-` / / // +// ========`-.____`-.___\_____/___.-`____.-'======== // +// `=---=' // +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // +// 佛祖保佑 永不宕机 永无BUG // +//////////////////////////////////////////////////////////////////// \ No newline at end of file diff --git a/ry-api/ruoyi-admin/src/main/resources/i18n/messages.properties b/ry-api/ruoyi-admin/src/main/resources/i18n/messages.properties new file mode 100644 index 0000000..93de005 --- /dev/null +++ b/ry-api/ruoyi-admin/src/main/resources/i18n/messages.properties @@ -0,0 +1,38 @@ +#错误消息 +not.null=* 必须填写 +user.jcaptcha.error=验证码错误 +user.jcaptcha.expire=验证码已失效 +user.not.exists=用户不存在/密码错误 +user.password.not.match=用户不存在/密码错误 +user.password.retry.limit.count=密码输入错误{0}次 +user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分钟 +user.password.delete=对不起,您的账号已被删除 +user.blocked=用户已封禁,请联系管理员 +role.blocked=角色已封禁,请联系管理员 +login.blocked=很遗憾,访问IP已被列入系统黑名单 +user.logout.success=退出成功 + +length.not.valid=长度必须在{min}到{max}个字符之间 + +user.username.not.valid=* 2到20个汉字、字母、数字或下划线组成,且必须以非数字开头 +user.password.not.valid=* 5-50个字符 + +user.email.not.valid=邮箱格式错误 +user.mobile.phone.number.not.valid=手机号格式错误 +user.login.success=登录成功 +user.register.success=注册成功 +user.notfound=请重新登录 +user.forcelogout=管理员强制退出,请重新登录 +user.unknown.error=未知错误,请重新登录 + +##文件上传消息 +upload.exceed.maxSize=上传的文件大小超出限制的文件大小!
允许的文件最大大小是:{0}MB! +upload.filename.exceed.length=上传的文件名最长{0}个字符 + +##权限 +no.permission=您没有数据的权限,请联系管理员添加权限 [{0}] +no.create.permission=您没有创建数据的权限,请联系管理员添加权限 [{0}] +no.update.permission=您没有修改数据的权限,请联系管理员添加权限 [{0}] +no.delete.permission=您没有删除数据的权限,请联系管理员添加权限 [{0}] +no.export.permission=您没有导出数据的权限,请联系管理员添加权限 [{0}] +no.view.permission=您没有查看数据的权限,请联系管理员添加权限 [{0}] diff --git a/ry-api/ruoyi-admin/src/main/resources/logback.xml b/ry-api/ruoyi-admin/src/main/resources/logback.xml new file mode 100644 index 0000000..a360583 --- /dev/null +++ b/ry-api/ruoyi-admin/src/main/resources/logback.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + ${log.pattern} + + + + + + ${log.path}/sys-info.log + + + + ${log.path}/sys-info.%d{yyyy-MM-dd}.log + + 60 + + + ${log.pattern} + + + + INFO + + ACCEPT + + DENY + + + + + ${log.path}/sys-error.log + + + + ${log.path}/sys-error.%d{yyyy-MM-dd}.log + + 60 + + + ${log.pattern} + + + + ERROR + + ACCEPT + + DENY + + + + + + ${log.path}/sys-user.log + + + ${log.path}/sys-user.%d{yyyy-MM-dd}.log + + 60 + + + ${log.pattern} + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ry-api/ruoyi-admin/src/main/resources/mybatis/mybatis-config.xml b/ry-api/ruoyi-admin/src/main/resources/mybatis/mybatis-config.xml new file mode 100644 index 0000000..ac47c03 --- /dev/null +++ b/ry-api/ruoyi-admin/src/main/resources/mybatis/mybatis-config.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + diff --git a/ry-api/ruoyi-business/pom.xml b/ry-api/ruoyi-business/pom.xml new file mode 100644 index 0000000..519e7fe --- /dev/null +++ b/ry-api/ruoyi-business/pom.xml @@ -0,0 +1,40 @@ + + + + ruoyi + com.ruoyi + 3.9.2 + + 4.0.0 + + ruoyi-business + + + 业务模块 - 北京整合医学学会项目管理系统 + + + + + com.ruoyi + ruoyi-common + + + com.ruoyi + ruoyi-system + + + + com.aliyun + aliyun-java-sdk-dysmsapi + 2.2.1 + + + com.aliyun + aliyun-java-sdk-core + 4.6.4 + + + + diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizAuthController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizAuthController.java new file mode 100644 index 0000000..aa1078a --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizAuthController.java @@ -0,0 +1,130 @@ +package com.ruoyi.business.controller; + +import java.util.Map; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.business.domain.BizOrg; +import com.ruoyi.business.dto.SmsValidForm; +import com.ruoyi.business.service.IBizOrgService; +import com.ruoyi.business.service.SysSmsService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.system.service.ISysUserService; + +/** + * 业务登录 - 角色选择 + * registerExecutor 完整实现 (2026-08-15 重构, 原 registerSupplier): + * 1. 校验短信验证码 + * 2. 检查手机号/用户名是否已注册 + * 3. 加密密码 + * 4. 插入 sys_user (用户名=前端传入, role_type=executor, 主账号) + * 5. 插入 biz_org (executor 类型, business_nature/contact_phone 从表单) + * 注: 主账号不建 biz_person, 跟 sponsor 主账号保持一致 + */ +@RestController +@RequestMapping("/business/auth") +public class BizAuthController extends BaseController { + + @Autowired + private SysSmsService smsService; + + @Autowired + private ISysUserService userService; + + @Autowired + private IBizOrgService bizOrgService; + + @Autowired + private BCryptPasswordEncoder passwordEncoder; + + @PostMapping("/login") + public AjaxResult login(@RequestBody Map body) { + String username = (String) body.get("username"); + String password = (String) body.get("password"); + String roleType = (String) body.get("roleType"); + return success().put("roleType", roleType).put("token", "mock-token"); + } + + @PostMapping("/registerExecutor") + public AjaxResult registerExecutor(@RequestBody Map body) { + String username = (String) body.get("username"); + String unitName = (String) body.get("unitName"); + String businessNature = (String) body.get("businessNature"); + String phone = (String) body.get("phone"); + String code = (String) body.get("smsCode"); + String password = (String) body.get("password"); + String confirmPassword = (String) body.get("confirmPassword"); + String uuid = (String) body.get("uuid"); + + // 1. 基础校验 + if (username == null || username.length() < 4 || username.length() > 20) return error("用户名长度 4-20 位"); + if (!username.matches("^[A-Za-z0-9_]+$")) return error("用户名只能包含字母/数字/下划线"); + if (unitName == null || unitName.isEmpty()) return error("企业名称不能为空"); + if (businessNature == null || businessNature.isEmpty()) return error("企业性质不能为空"); + if (phone == null || !phone.matches("^1\\d{10}$")) return error("手机号格式错误"); + if (code == null || code.isEmpty()) return error("请输入短信验证码"); + if (password == null || password.length() < 6 || password.length() > 20) return error("密码长度 6-20 位"); + if (!password.equals(confirmPassword)) return error("两次密码输入不一致"); + + // 2. 校验短信验证码 + SmsValidForm smsForm = new SmsValidForm(); + smsForm.setPhone(phone); + smsForm.setSmsCode(code); + smsForm.setUuid(uuid); + try { + smsService.verifyCode(smsForm); + } catch (RuntimeException e) { + return error("验证码错误或已过期: " + e.getMessage()); + } + + // 3. 检查手机号/用户名是否已注册 + if (userService.isPhoneRegistered(phone)) { + return error("该手机号已注册, 请直接登录"); + } + SysUser nameCheck = new SysUser(); + nameCheck.setUserName(username); + if (!userService.checkUserNameUnique(nameCheck)) { + return error("用户名已被占用: " + username); + } + + // 4. 加密密码 + 插入 sys_user (用户名=userName 字段, 主账号) + SysUser user = new SysUser(); + user.setUserName(username); + user.setNickName(unitName); // 默认昵称用公司名, 用户后续可在账号页改 + user.setPhonenumber(phone); + user.setPassword(passwordEncoder.encode(password)); + user.setStatus("0"); + userService.insertUser(user); + Long userId = user.getUserId(); + // sys_user.role_type 字段需 = executor + userService.updateRoleType(userId, "executor"); + + // 5. 插入 biz_org (executor 类型, 主账号自己当 contact) + BizOrg org = new BizOrg(); + org.setOrgId(null); + org.setOrgName(unitName); + org.setOrgType("executor"); + org.setBusinessNature(businessNature); + org.setContactPhone(phone); + org.setContactName(unitName); // 联系人默认 = 公司名 + org.setStatus("0"); + bizOrgService.insert(org); + Long orgId = org.getOrgId(); + + // 注: 主账号不建 biz_person (与 sponsor 主账号保持一致, 见 sponsor/Account.vue) + // 后续主账号在 [账号信息] 看到 [所属公司] 时, 后端可通过 sys_user.role_type 关联 biz_org 取 org_name + // 子账号 (biz_person.unit_type='sponsor' + user_id 关联 sys_user.parent_user_id) 由 [人员管理] 创建 + + return success("注册成功, 请等待审核").put("userId", userId).put("orgId", orgId); + } + + @PostMapping("/registerSponsor") + public AjaxResult registerSponsor(@RequestBody Map body) { + return success(); + } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizDashboardController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizDashboardController.java new file mode 100644 index 0000000..4e09462 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizDashboardController.java @@ -0,0 +1,56 @@ +package com.ruoyi.business.controller; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.business.domain.BizProject; +import com.ruoyi.business.domain.BizMeeting; +import com.ruoyi.business.domain.BizExpert; +import com.ruoyi.business.service.IBizProjectService; +import com.ruoyi.business.service.IBizMeetingService; +import com.ruoyi.business.service.IBizExpertService; + +/** + * 工作台统计 + */ +@RestController +@RequestMapping("/business/dashboard") +public class BizDashboardController extends BaseController { + + @Autowired private IBizProjectService bizProjectService; + @Autowired private IBizMeetingService bizMeetingService; + @Autowired private IBizExpertService bizExpertService; + + @GetMapping("/manager") + public AjaxResult managerDashboard() { + Map map = new HashMap<>(); + List projects = bizProjectService.selectList(new BizProject()); + List meetings = bizMeetingService.selectList(new BizMeeting()); + List experts = bizExpertService.selectList(new BizExpert()); + map.put("totalProjects", projects.size()); + map.put("totalMeetings", meetings.size()); + map.put("totalExperts", experts.size()); + map.put("todoMeetings", meetings.stream().filter(m -> "未执行".equals(m.getCurrentStage())).count()); + map.put("doingMeetings", meetings.stream().filter(m -> "待监管".equals(m.getCurrentStage()) || "待整改".equals(m.getCurrentStage())).count()); + map.put("doneMeetings", meetings.stream().filter(m -> "已结算".equals(m.getCurrentStage()) || "已结题".equals(m.getCurrentStage())).count()); + return success(map); + } + + @GetMapping("/leader") + public AjaxResult leaderDashboard() { + Map map = new HashMap<>(); + List projects = bizProjectService.selectList(new BizProject()); + List meetings = bizMeetingService.selectList(new BizMeeting()); + map.put("totalProjects", projects.size()); + map.put("totalMeetings", meetings.size()); + map.put("finishedProjects", projects.stream().filter(p -> "1".equals(p.getIsFinished())).count()); + map.put("finishedMeetings", meetings.stream().filter(m -> "已结题".equals(m.getCurrentStage())).count()); + return success(map); + } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizDictController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizDictController.java new file mode 100644 index 0000000..4da8694 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizDictController.java @@ -0,0 +1,142 @@ +package com.ruoyi.business.controller; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.business.domain.BizDepartment; +import com.ruoyi.business.domain.BizDoctorTitle; +import com.ruoyi.business.service.IBizDepartmentService; +import com.ruoyi.business.service.IBizDoctorTitleService; + +/** + * 业务字典管理 (科室 + 医生职称) + * 仅管理员可维护; 其它角色可访问 /active 接口拉启用的列表 + */ +@RestController +@RequestMapping("/business/dict") +public class BizDictController extends BaseController +{ + @Autowired + private IBizDepartmentService departmentService; + @Autowired + private IBizDoctorTitleService doctorTitleService; + + // ============== 科室 ============== + + @PreAuthorize("@ss.hasRole('admin')") + @GetMapping("/department/list") + public TableDataInfo listDept(BizDepartment entity) + { + startPage(); + return getDataTable(departmentService.selectList(entity)); + } + + @GetMapping("/department/active") + public AjaxResult activeDept() + { + return success(departmentService.selectActive()); + } + + @GetMapping("/department/{deptId}") + public AjaxResult getDept(@PathVariable Long deptId) + { + return success(departmentService.getById(deptId)); + } + + @PreAuthorize("@ss.hasRole('admin')") + @Log(title = "科室字典", businessType = BusinessType.INSERT) + @PostMapping("/department") + public AjaxResult addDept(@RequestBody BizDepartment entity) + { + if (entity.getName() == null || entity.getName().trim().isEmpty()) { + return error("科室名称不能为空"); + } + if (entity.getStatus() == null || entity.getStatus().isEmpty()) entity.setStatus("0"); + entity.setCreateBy(SecurityUtils.getUsername()); + return toAjax(departmentService.insert(entity)); + } + + @PreAuthorize("@ss.hasRole('admin')") + @Log(title = "科室字典", businessType = BusinessType.UPDATE) + @PutMapping("/department") + public AjaxResult editDept(@RequestBody BizDepartment entity) + { + if (entity.getDeptId() == null) return error("ID 不能为空"); + if (entity.getName() == null || entity.getName().trim().isEmpty()) { + return error("科室名称不能为空"); + } + entity.setUpdateBy(SecurityUtils.getUsername()); + return toAjax(departmentService.updateByPrimaryKey(entity)); + } + + @PreAuthorize("@ss.hasRole('admin')") + @Log(title = "科室字典", businessType = BusinessType.DELETE) + @DeleteMapping("/department/{ids}") + public AjaxResult removeDept(@PathVariable Long[] ids) + { + return toAjax(departmentService.deleteByPrimaryKeys(ids)); + } + + // ============== 医生职称 ============== + + @PreAuthorize("@ss.hasRole('admin')") + @GetMapping("/title/list") + public TableDataInfo listTitle(BizDoctorTitle entity) + { + startPage(); + return getDataTable(doctorTitleService.selectList(entity)); + } + + @GetMapping("/title/active") + public AjaxResult activeTitle() + { + return success(doctorTitleService.selectActive()); + } + + @GetMapping("/title/{titleId}") + public AjaxResult getTitle(@PathVariable Long titleId) + { + return success(doctorTitleService.getById(titleId)); + } + + @PreAuthorize("@ss.hasRole('admin')") + @Log(title = "医生职称字典", businessType = BusinessType.INSERT) + @PostMapping("/title") + public AjaxResult addTitle(@RequestBody BizDoctorTitle entity) + { + if (entity.getName() == null || entity.getName().trim().isEmpty()) { + return error("职称名称不能为空"); + } + if (entity.getStatus() == null || entity.getStatus().isEmpty()) entity.setStatus("0"); + entity.setCreateBy(SecurityUtils.getUsername()); + return toAjax(doctorTitleService.insert(entity)); + } + + @PreAuthorize("@ss.hasRole('admin')") + @Log(title = "医生职称字典", businessType = BusinessType.UPDATE) + @PutMapping("/title") + public AjaxResult editTitle(@RequestBody BizDoctorTitle entity) + { + if (entity.getTitleId() == null) return error("ID 不能为空"); + if (entity.getName() == null || entity.getName().trim().isEmpty()) { + return error("职称名称不能为空"); + } + entity.setUpdateBy(SecurityUtils.getUsername()); + return toAjax(doctorTitleService.updateByPrimaryKey(entity)); + } + + @PreAuthorize("@ss.hasRole('admin')") + @Log(title = "医生职称字典", businessType = BusinessType.DELETE) + @DeleteMapping("/title/{ids}") + public AjaxResult removeTitle(@PathVariable Long[] ids) + { + return toAjax(doctorTitleService.deleteByPrimaryKeys(ids)); + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizExecutionIntentController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizExecutionIntentController.java new file mode 100644 index 0000000..ee72093 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizExecutionIntentController.java @@ -0,0 +1,122 @@ +package com.ruoyi.business.controller; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.system.mapper.SysUserMapper; +import com.ruoyi.business.domain.BizExecutionIntent; +import com.ruoyi.business.domain.BizProject; +import com.ruoyi.business.service.IBizExecutionIntentService; +import com.ruoyi.business.service.IBizProjectService; + +/** + * 执行意向Controller + */ +@RestController +@RequestMapping("/business/executionIntent") +public class BizExecutionIntentController extends BaseController +{ + @Autowired + private SysUserMapper sysUserMapper; + @Autowired + private IBizExecutionIntentService bizExecutionIntentService; + @Autowired + private IBizProjectService bizProjectService; + + /** + * 公开门户点击"立即报名" — 写入意向 (user_id = 当前登录用户) + * POST /business/executionIntent/signup + * body: { projectId } (后端按 id 查项目回填 no/name) + * 返回 { intentId, projectNo, projectName, onboardStatus } + */ + @Log(title = "执行意向", businessType = BusinessType.INSERT) + @PostMapping("/signup") + public AjaxResult signup(@RequestBody BizExecutionIntent bizExecutionIntent) + { + // 入参只接受 projectId (项目主键) + // BizExecutionIntent 没有 projectId 字段, 用 params 暂存 + Object pidRaw = bizExecutionIntent.getParams().get("projectId"); + Long projectId = null; + if (pidRaw != null) { + try { projectId = Long.parseLong(String.valueOf(pidRaw)); } catch (Exception e) { return error("项目ID格式错误"); } + } + if (projectId == null) return error("项目ID不能为空"); + BizProject proj = bizProjectService.getById(String.valueOf(projectId)); + if (proj == null) return error("项目不存在"); + // 查重: 当前用户是否已报过这个项目 + BizExecutionIntent q = new BizExecutionIntent(); + q.setUserId(SecurityUtils.getUserId()); + q.setProjectNo(proj.getProjectNo()); + List exists = bizExecutionIntentService.selectList(q); + if (exists != null && !exists.isEmpty()) return error("您已报名过该项目,无需重复报名"); + bizExecutionIntent.setProjectNo(proj.getProjectNo()); + bizExecutionIntent.setProjectName(proj.getProjectName()); + Long uid = SecurityUtils.getUserId(); + bizExecutionIntent.setUserId(uid); + // 默认按用户表信息补全 name/phone (意向表历史字段, 方便后续审核流使用) + SysUser u = sysUserMapper.selectUserById(uid); + if (u != null) { + if (bizExecutionIntent.getName() == null || bizExecutionIntent.getName().isEmpty()) + bizExecutionIntent.setName(u.getUserName()); + if (bizExecutionIntent.getPhone() == null || bizExecutionIntent.getPhone().isEmpty()) + bizExecutionIntent.setPhone(u.getPhonenumber()); + } + if (bizExecutionIntent.getOnboardStatus() == null) bizExecutionIntent.setOnboardStatus("未入库"); + bizExecutionIntent.setCreateBy(SecurityUtils.getUsername()); + int n = bizExecutionIntentService.insert(bizExecutionIntent); + if (n == 0) return error("报名失败"); + return success(bizExecutionIntent); + } + + /** 当前用户是否已报过该项目 */ + @GetMapping("/hasSigned") + public AjaxResult hasSigned(@RequestParam("projectId") Long projectId) + { + if (projectId == null) return success(false); + BizProject proj = bizProjectService.getById(String.valueOf(projectId)); + if (proj == null) return success(false); + BizExecutionIntent q = new BizExecutionIntent(); + q.setUserId(SecurityUtils.getUserId()); + q.setProjectNo(proj.getProjectNo()); + List exists = bizExecutionIntentService.selectList(q); + return success(exists != null && !exists.isEmpty()); + } + + @GetMapping("/list") + public TableDataInfo list(BizExecutionIntent bizExecutionIntent) + { + startPage(); + List list = bizExecutionIntentService.selectList(bizExecutionIntent); + return getDataTable(list); + } + @GetMapping("/{intentId}") + public AjaxResult getInfo(@PathVariable("intentId") String intentId) + { + return success(bizExecutionIntentService.getById(intentId)); + } + @Log(title = "执行意向", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody BizExecutionIntent bizExecutionIntent) + { + return toAjax(bizExecutionIntentService.insert(bizExecutionIntent)); + } + @Log(title = "执行意向", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody BizExecutionIntent bizExecutionIntent) + { + return toAjax(bizExecutionIntentService.updateByPrimaryKey(bizExecutionIntent)); + } + @Log(title = "执行意向", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable String[] ids) + { + return toAjax(bizExecutionIntentService.deleteByPrimaryKeys(ids)); + } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizExecutorController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizExecutorController.java new file mode 100644 index 0000000..b0a1226 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizExecutorController.java @@ -0,0 +1,74 @@ +package com.ruoyi.business.controller; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; + +/** + * 执行方用户查询 Controller + * 绕开 RuoYi 的 sys_user:list 权限(manager01 等普通用户被 data_scope 过滤看不到)。 + * 按 sys_user.role_type = 'executor' 过滤真正的执行方用户。 + */ +@RestController +@RequestMapping("/business/executor") +public class BizExecutorController extends BaseController +{ + @Autowired + private JdbcTemplate jdbcTemplate; + + /** + * 查询指定角色用户列表 (支持 executor / sponsor / manager / leader / doctor / admin) + * @param roleType 角色类型 (默认 'executor') + * @param userName 用户名模糊搜索 (可选) + * @param userId 精确匹配 (可选) + * @param pageSize 限制返回行数 (默认 50) + */ + @GetMapping("/list") + public AjaxResult list(@RequestParam(required = false, defaultValue = "executor") String roleType, + @RequestParam(required = false) String userName, + @RequestParam(required = false) Long userId, + @RequestParam(required = false, defaultValue = "50") Integer pageSize) + { + StringBuilder sql = new StringBuilder() + .append("SELECT user_id, dept_id, user_name, nick_name, email, phonenumber, role_type, status, create_time ") + .append("FROM sys_user WHERE del_flag = '0' AND status = '0' AND role_type = ?"); + List args = new ArrayList<>(); + args.add(roleType); + if (userId != null) { + sql.append(" AND user_id = ?"); + args.add(userId); + } + if (userName != null && !userName.isEmpty()) { + sql.append(" AND user_name LIKE ?"); + args.add("%" + userName + "%"); + } + sql.append(" ORDER BY user_id ASC LIMIT ?"); + args.add(pageSize); + + List> rows = jdbcTemplate.queryForList(sql.toString(), args.toArray()); + // 标准化为前端 el-select 友好的字段 + List> result = new ArrayList<>(); + for (Map r : rows) { + Map u = new HashMap<>(); + u.put("userId", r.get("user_id")); + u.put("deptId", r.get("dept_id")); + u.put("userName", r.get("user_name")); + u.put("nickName", r.get("nick_name")); + u.put("email", r.get("email")); + u.put("phonenumber", r.get("phonenumber")); + u.put("roleType", r.get("role_type")); + u.put("status", r.get("status")); + result.add(u); + } + return success(result); + } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizExpertController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizExpertController.java new file mode 100644 index 0000000..e926f80 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizExpertController.java @@ -0,0 +1,74 @@ +package com.ruoyi.business.controller; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.business.domain.BizExpert; +import com.ruoyi.business.service.IBizExpertService; + +/** + * 专家Controller + */ +@RestController +@RequestMapping("/business/expert") +public class BizExpertController extends BaseController +{ + @Autowired + private IBizExpertService bizExpertService; + @GetMapping("/list") + public TableDataInfo list(BizExpert bizExpert) + { + startPage(); + List list = bizExpertService.selectList(bizExpert); + return getDataTable(list); + } + @GetMapping("/{expertId}") + public AjaxResult getInfo(@PathVariable("expertId") String expertId) + { + return success(bizExpertService.getById(expertId)); + } + /** + * 查当前登录用户的专家信息 (按 userId 路由) + */ + @GetMapping("/profile") + public AjaxResult getProfile() + { + return success(bizExpertService.getByUserId(SecurityUtils.getUserId())); + } + @Log(title = "专家", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody BizExpert bizExpert) + { + return toAjax(bizExpertService.insert(bizExpert)); + } + @Log(title = "专家", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody BizExpert bizExpert) + { + return toAjax(bizExpertService.updateByPrimaryKey(bizExpert)); + } + /** + * 个人专家信息更新 (走 userId 路由, 后端强制注入当前登录用户) + * 不依赖前端传 expertId; 找不到 expert 行时自动 insert + */ + @Log(title = "专家个人信息", businessType = BusinessType.UPDATE) + @PutMapping("/profile") + public AjaxResult updateProfile(@RequestBody BizExpert bizExpert) + { + bizExpert.setUserId(SecurityUtils.getUserId()); + bizExpert.setCreateBy(SecurityUtils.getUsername()); + return toAjax(bizExpertService.updateProfileByUserId(bizExpert)); + } + @Log(title = "专家", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable String[] ids) + { + return toAjax(bizExpertService.deleteByPrimaryKeys(ids)); + } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizInvitationController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizInvitationController.java new file mode 100644 index 0000000..a6716f8 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizInvitationController.java @@ -0,0 +1,53 @@ +package com.ruoyi.business.controller; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.business.domain.BizInvitation; +import com.ruoyi.business.service.IBizInvitationService; + +/** + * 邀请函Controller + */ +@RestController +@RequestMapping("/business/invitation") +public class BizInvitationController extends BaseController +{ + @Autowired + private IBizInvitationService bizInvitationService; + @GetMapping("/list") + public TableDataInfo list(BizInvitation bizInvitation) + { + startPage(); + List list = bizInvitationService.selectList(bizInvitation); + return getDataTable(list); + } + @GetMapping("/{invitationId}") + public AjaxResult getInfo(@PathVariable("invitationId") String invitationId) + { + return success(bizInvitationService.getById(invitationId)); + } + @Log(title = "邀请函", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody BizInvitation bizInvitation) + { + return toAjax(bizInvitationService.insert(bizInvitation)); + } + @Log(title = "邀请函", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody BizInvitation bizInvitation) + { + return toAjax(bizInvitationService.updateByPrimaryKey(bizInvitation)); + } + @Log(title = "邀请函", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable String[] ids) + { + return toAjax(bizInvitationService.deleteByPrimaryKeys(ids)); + } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizLaborVoucherController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizLaborVoucherController.java new file mode 100644 index 0000000..61ec3f5 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizLaborVoucherController.java @@ -0,0 +1,53 @@ +package com.ruoyi.business.controller; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.business.domain.BizLaborVoucher; +import com.ruoyi.business.service.IBizLaborVoucherService; + +/** + * 劳务凭证Controller + */ +@RestController +@RequestMapping("/business/laborVoucher") +public class BizLaborVoucherController extends BaseController +{ + @Autowired + private IBizLaborVoucherService bizLaborVoucherService; + @GetMapping("/list") + public TableDataInfo list(BizLaborVoucher bizLaborVoucher) + { + startPage(); + List list = bizLaborVoucherService.selectList(bizLaborVoucher); + return getDataTable(list); + } + @GetMapping("/{voucherId}") + public AjaxResult getInfo(@PathVariable("voucherId") String voucherId) + { + return success(bizLaborVoucherService.getById(voucherId)); + } + @Log(title = "劳务凭证", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody BizLaborVoucher bizLaborVoucher) + { + return toAjax(bizLaborVoucherService.insert(bizLaborVoucher)); + } + @Log(title = "劳务凭证", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody BizLaborVoucher bizLaborVoucher) + { + return toAjax(bizLaborVoucherService.updateByPrimaryKey(bizLaborVoucher)); + } + @Log(title = "劳务凭证", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable String[] ids) + { + return toAjax(bizLaborVoucherService.deleteByPrimaryKeys(ids)); + } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingController.java new file mode 100644 index 0000000..50ea44f --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingController.java @@ -0,0 +1,53 @@ +package com.ruoyi.business.controller; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.business.domain.BizMeeting; +import com.ruoyi.business.service.IBizMeetingService; + +/** + * 会议Controller + */ +@RestController +@RequestMapping("/business/meeting") +public class BizMeetingController extends BaseController +{ + @Autowired + private IBizMeetingService bizMeetingService; + @GetMapping("/list") + public TableDataInfo list(BizMeeting bizMeeting) + { + startPage(); + List list = bizMeetingService.selectList(bizMeeting); + return getDataTable(list); + } + @GetMapping("/{meetingId}") + public AjaxResult getInfo(@PathVariable("meetingId") String meetingId) + { + return success(bizMeetingService.getById(meetingId)); + } + @Log(title = "会议", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody BizMeeting bizMeeting) + { + return toAjax(bizMeetingService.insert(bizMeeting)); + } + @Log(title = "会议", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody BizMeeting bizMeeting) + { + return toAjax(bizMeetingService.updateByPrimaryKey(bizMeeting)); + } + @Log(title = "会议", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable String[] ids) + { + return toAjax(bizMeetingService.deleteByPrimaryKeys(ids)); + } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingSettlementController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingSettlementController.java new file mode 100644 index 0000000..b2d3d9b --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingSettlementController.java @@ -0,0 +1,53 @@ +package com.ruoyi.business.controller; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.business.domain.BizMeetingSettlement; +import com.ruoyi.business.service.IBizMeetingSettlementService; + +/** + * 会议结算Controller + */ +@RestController +@RequestMapping("/business/meetingSettlement") +public class BizMeetingSettlementController extends BaseController +{ + @Autowired + private IBizMeetingSettlementService bizMeetingSettlementService; + @GetMapping("/list") + public TableDataInfo list(BizMeetingSettlement bizMeetingSettlement) + { + startPage(); + List list = bizMeetingSettlementService.selectList(bizMeetingSettlement); + return getDataTable(list); + } + @GetMapping("/{settlementId}") + public AjaxResult getInfo(@PathVariable("settlementId") String settlementId) + { + return success(bizMeetingSettlementService.getById(settlementId)); + } + @Log(title = "会议结算", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody BizMeetingSettlement bizMeetingSettlement) + { + return toAjax(bizMeetingSettlementService.insert(bizMeetingSettlement)); + } + @Log(title = "会议结算", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody BizMeetingSettlement bizMeetingSettlement) + { + return toAjax(bizMeetingSettlementService.updateByPrimaryKey(bizMeetingSettlement)); + } + @Log(title = "会议结算", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable String[] ids) + { + return toAjax(bizMeetingSettlementService.deleteByPrimaryKeys(ids)); + } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMessageController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMessageController.java new file mode 100644 index 0000000..d5d0355 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMessageController.java @@ -0,0 +1,128 @@ +package com.ruoyi.business.controller; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.business.domain.BizMessage; +import com.ruoyi.business.service.IBizMessageService; + +/** + * 个人消息Controller + */ +@RestController +@RequestMapping("/business/message") +public class BizMessageController extends BaseController +{ + @Autowired + private IBizMessageService bizMessageService; + + /** + * 列表 (管理员视角: 全量 + 过滤) + */ + @PreAuthorize("@ss.hasPermi('business:message:list')") + @GetMapping("/list") + public TableDataInfo list(BizMessage bizMessage) + { + startPage(); + List list = bizMessageService.selectList(bizMessage); + return getDataTable(list); + } + + /** + * 当前登录用户的最近消息 (含未读统计) + * GET /business/message/my?limit=50 + * 返回 {rows: [...], unread: 12} + */ + @GetMapping("/my") + public AjaxResult my(@RequestParam(value = "limit", required = false, defaultValue = "50") Integer limit) + { + Long uid = SecurityUtils.getUserId(); + List rows = bizMessageService.selectMyRecent(uid, limit); + int unread = 0; + for (BizMessage m : rows) { + if ("0".equals(m.getIsRead())) unread++; + } + Map data = new HashMap<>(); + data.put("rows", rows); + data.put("unread", unread); + data.put("total", rows.size()); + return success(data); + } + + /** + * 标记单条已读 (校验归属) + */ + @PutMapping("/read/{msgId}") + public AjaxResult markRead(@PathVariable Long msgId) + { + Long uid = SecurityUtils.getUserId(); + int n = bizMessageService.markRead(msgId, uid); + return n > 0 ? success() : error("消息不存在或无权操作"); + } + + /** + * 当前用户全部标记已读 + */ + @PutMapping("/readAll") + public AjaxResult markAllRead() + { + Long uid = SecurityUtils.getUserId(); + int n = bizMessageService.markAllRead(uid); + return success(n); + } + + /** + * 详情 (校验归属) + */ + @GetMapping("/{msgId}") + public AjaxResult getInfo(@PathVariable Long msgId) + { + BizMessage m = bizMessageService.getById(msgId); + if (m == null) return error("消息不存在"); + // 非管理员只能看自己的 + if (!SecurityUtils.isAdmin() && !m.getReceiverUserId().equals(SecurityUtils.getUserId())) { + return error("无权查看"); + } + // 自动标记已读 + if ("0".equals(m.getIsRead()) && m.getReceiverUserId().equals(SecurityUtils.getUserId())) { + bizMessageService.markRead(msgId, SecurityUtils.getUserId()); + m.setIsRead("1"); + } + return success(m); + } + + /** + * 发送消息给某用户 (管理员) + */ + @PreAuthorize("@ss.hasPermi('business:message:add')") + @Log(title = "个人消息", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody BizMessage bizMessage) + { + if (bizMessage.getReceiverUserId() == null) return error("接收人不能为空"); + if (bizMessage.getTitle() == null || bizMessage.getTitle().isEmpty()) return error("标题不能为空"); + bizMessage.setCreateBy(SecurityUtils.getUsername()); + return toAjax(bizMessageService.insert(bizMessage)); + } + + /** + * 批量删除 (管理员) + */ + @PreAuthorize("@ss.hasPermi('business:message:remove')") + @Log(title = "个人消息", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Long[] ids) + { + return toAjax(bizMessageService.deleteByPrimaryKeys(ids)); + } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizOrgController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizOrgController.java new file mode 100644 index 0000000..24434b4 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizOrgController.java @@ -0,0 +1,53 @@ +package com.ruoyi.business.controller; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.business.domain.BizOrg; +import com.ruoyi.business.service.IBizOrgService; + +/** + * 公司Controller (赞助方 + 执行方 共用) + * GET /business/org/list?orgType=sponsor|execution + */ +@RestController +@RequestMapping("/business/org") +public class BizOrgController extends BaseController { + @Autowired + private IBizOrgService bizOrgService; + + @GetMapping("/list") + public TableDataInfo list(BizOrg bizOrg) { + startPage(); + List list = bizOrgService.selectList(bizOrg); + return getDataTable(list); + } + + @GetMapping("/{orgId}") + public AjaxResult getInfo(@PathVariable("orgId") Long orgId) { + return success(bizOrgService.getById(orgId)); + } + + @Log(title = "公司管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody BizOrg bizOrg) { + return toAjax(bizOrgService.insert(bizOrg)); + } + + @Log(title = "公司管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody BizOrg bizOrg) { + return toAjax(bizOrgService.updateByPrimaryKey(bizOrg)); + } + + @Log(title = "公司管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{orgIds}") + public AjaxResult remove(@PathVariable Long[] orgIds) { + return toAjax(bizOrgService.deleteByPrimaryKeys(orgIds)); + } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPersonController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPersonController.java new file mode 100644 index 0000000..d3d2c72 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPersonController.java @@ -0,0 +1,172 @@ +package com.ruoyi.business.controller; + +import java.util.ArrayList; +import java.util.List; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.business.domain.BizOrg; +import com.ruoyi.business.domain.BizPerson; +import com.ruoyi.business.domain.BizPersonImportVO; +import com.ruoyi.business.mapper.BizOrgMapper; +import com.ruoyi.business.service.IBizPersonService; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.exception.ServiceException; + +/** + * 人员Controller + */ +@RestController +@RequestMapping("/business/person") +public class BizPersonController extends BaseController +{ + @Autowired + private IBizPersonService bizPersonService; + + @Autowired + private BizOrgMapper bizOrgMapper; + @GetMapping("/list") + public TableDataInfo list(BizPerson bizPerson) + { + startPage(); + List list = bizPersonService.selectList(bizPerson); + return getDataTable(list); + } + + /** + * sponsor 端专属人员列表 (走专属 mapper, 利用 biz_person.user_id -> sys_user.parent_user_id 做归属过滤) + * 公共 /list 方法不被 sponsor 专属逻辑污染 + */ + @GetMapping("/sponsorList") + public TableDataInfo sponsorList(BizPerson bizPerson) + { + Long mainUid = getUserId(); + bizPerson.getParams().put("sponsorOwnerUid", mainUid); + startPage(); + List list = bizPersonService.selectSponsorList(bizPerson); + return getDataTable(list); + } + @GetMapping("/{personId}") + public AjaxResult getInfo(@PathVariable("personId") String personId) + { + return success(bizPersonService.getById(personId)); + } + @Log(title = "人员", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody BizPerson bizPerson) + { + bizPerson.setCreateBy(getUsername()); + bizPerson.setUpdateBy(getUsername()); + // sponsor 端新建人员 = 创建 sys_user 子账号 (SUB, parent=当前登录主账号) + // service 内: 创建 sys_user + 创建 biz_person + 把 sys_user.user_id 绑到 biz_person.user_id + SysUser created = bizPersonService.insert(bizPerson, getUserId()); + AjaxResult ajax = success(created); + // SysUser.password 是 @JsonProperty WRITE_ONLY 不会序列化,这里额外 put 明文密码给前端 toast + ajax.put("password", bizPerson.getLoginPassword()); + return ajax; + } + @Log(title = "人员", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody BizPerson bizPerson) + { + bizPerson.setUpdateBy(getUsername()); + return toAjax(bizPersonService.updateByPrimaryKey(bizPerson)); + } + @Log(title = "人员", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable String[] ids) + { + return toAjax(bizPersonService.deleteByPrimaryKeys(ids)); + } + + // ========== sponsor 端 Excel 批量导入 ========== + + /** + * 下载 sponsor 端人员导入模板 (.xlsx) + * 模板列头按 BizPersonImportVO @Excel 注解自动生成 (中文: 姓名/手机号/工作单位/部门/职务/角色/状态) + */ + @GetMapping("/sponsorImportTemplate") + public void sponsorImportTemplate(HttpServletResponse response) + { + ExcelUtil util = new ExcelUtil<>(BizPersonImportVO.class); + util.importTemplateExcel(response, "人员导入"); + } + + /** + * sponsor 端批量导入人员 (上传 .xlsx) + * 每行创建一个 biz_person + 一个 sys_user 子账号 (parent_user_id=当前主账号, loginUsername=手机号, 默认密码 123456) + * 失败行记录在返回结果的 errorRows 字段, 不影响其他行 + */ + @Log(title = "人员批量导入", businessType = BusinessType.INSERT) + @PostMapping("/sponsorImport") + public AjaxResult sponsorImport(@RequestParam("file") MultipartFile file) throws Exception + { + if (file == null || file.isEmpty()) { + throw new ServiceException("请选择要上传的文件"); + } + ExcelUtil util = new ExcelUtil<>(BizPersonImportVO.class); + List rows = util.importExcel(file.getInputStream(), 0); + if (rows == null || rows.isEmpty()) { + throw new ServiceException("导入文件无有效数据"); + } + Long mainUid = getUserId(); + List> results = new ArrayList<>(); + int ok = 0; + for (int i = 0; i < rows.size(); i++) { + BizPersonImportVO row = rows.get(i); + java.util.Map r = new java.util.LinkedHashMap<>(); + r.put("rowNo", i + 2); // Excel 行号 (含标题行) + r.put("name", row.getName()); + r.put("phone", row.getPhone()); + try { + // 1. orgName → orgId (按 orgType='sponsor' 精确匹配, 查不到就报错) + String orgName = row.getOrgName(); + if (orgName == null || orgName.trim().isEmpty()) { + throw new ServiceException("所属公司不能为空"); + } + BizOrg orgQuery = new BizOrg(); + orgQuery.setOrgName(orgName.trim()); + orgQuery.setOrgType("sponsor"); + List matched = bizOrgMapper.selectList(orgQuery); + if (matched == null || matched.isEmpty()) { + throw new ServiceException("找不到所属公司: " + orgName + " (需先在 [公司管理] 录入 orgType=sponsor 的公司)"); + } + Long orgId = matched.get(0).getOrgId(); + + BizPerson p = new BizPerson(); + p.setName(row.getName()); + p.setPhone(row.getPhone()); + p.setOrgId(orgId); + p.setDepartment(row.getDepartment()); + p.setPosition(row.getPosition()); + p.setRole(row.getRole()); + p.setStatus("0"); // 默认正常 + p.setUnitType("sponsor"); + p.setLoginUsername(row.getPhone()); // 用户名 = 手机号 (要求唯一) + p.setLoginPassword("123456"); // 默认密码 + p.setCreateBy(getUsername()); + p.setUpdateBy(getUsername()); + bizPersonService.insert(p, mainUid); + ok++; + r.put("ok", true); + r.put("message", "成功"); + } catch (Exception e) { + r.put("ok", false); + r.put("message", e.getMessage()); + } + results.add(r); + } + AjaxResult ajax = success(); + ajax.put("total", rows.size()); + ajax.put("ok", ok); + ajax.put("results", results); + return ajax; + } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizProjectController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizProjectController.java new file mode 100644 index 0000000..8dd0ea8 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizProjectController.java @@ -0,0 +1,214 @@ +package com.ruoyi.business.controller; + +import java.util.ArrayList; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.business.domain.BizExecutionIntent; +import com.ruoyi.business.domain.BizProject; +import com.ruoyi.business.domain.BizProjectAssign; +import com.ruoyi.business.domain.BizProjectRating; +import com.ruoyi.business.service.IBizProjectService; +import com.ruoyi.business.service.IBizExecutionIntentService; +import com.ruoyi.business.domain.BizProjectSponsorAssign; +import com.ruoyi.business.service.IBizProjectAssignService; +import com.ruoyi.business.service.IBizProjectRatingService; +import com.ruoyi.business.service.IBizProjectSponsorAssignService; + +/** + * 项目Controller + * + * 评分说明: 评分公开 (无可见性), 由 rater_role 字段区分角色 (sponsor/executor/compliance) + * 一律走 biz_project_rating 中间表 (BizProjectRatingController 已提供完整 CRUD) + */ +@RestController +@RequestMapping("/business/project") +public class BizProjectController extends BaseController +{ + @Autowired + private IBizProjectService bizProjectService; + @Autowired + private IBizExecutionIntentService bizExecutionIntentService; + @Autowired + private IBizProjectAssignService bizProjectAssignService; + @Autowired + private IBizProjectRatingService bizProjectRatingService; + @Autowired + private IBizProjectSponsorAssignService bizProjectSponsorAssignService; + + /** + * 我报名的项目 (当前用户在 biz_execution_intent 里有意向的项目) + * GET /business/project/myProjects + */ + @GetMapping("/myProjects") + public TableDataInfo myProjects(BizProject bizProject) + { + Long uid = SecurityUtils.getUserId(); + BizExecutionIntent q = new BizExecutionIntent(); + q.setUserId(uid); + List intents = bizExecutionIntentService.selectList(q); + if (intents == null || intents.isEmpty()) { + TableDataInfo r = new TableDataInfo(); + r.setRows(new ArrayList<>()); + r.setTotal(0); + return r; + } + java.util.Set noSet = new java.util.LinkedHashSet<>(); + for (BizExecutionIntent it : intents) { + if (it.getProjectNo() != null && !it.getProjectNo().isEmpty()) noSet.add(it.getProjectNo()); + } + startPage(); + BizProject bp = new BizProject(); + bp.getParams().put("projectNos", new ArrayList<>(noSet)); + if (bizProject.getProjectNo() != null) bp.setProjectNo(bizProject.getProjectNo()); + if (bizProject.getProjectName() != null) bp.setProjectName(bizProject.getProjectName()); + List list = bizProjectService.selectList(bp); + return getDataTable(list); + } + + @GetMapping("/list") + public TableDataInfo list(BizProject bizProject) + { + startPage(); + List list = bizProjectService.selectList(bizProject); + return getDataTable(list); + } + @GetMapping("/{projectId}") + public AjaxResult getInfo(@PathVariable("projectId") String projectId) + { + return success(bizProjectService.getById(projectId)); + } + @Log(title = "项目", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody BizProject bizProject) + { + return toAjax(bizProjectService.insert(bizProject)); + } + @Log(title = "项目", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody BizProject bizProject) + { + return toAjax(bizProjectService.updateByPrimaryKey(bizProject)); + } + @Log(title = "项目", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable String[] ids) + { + return toAjax(bizProjectService.deleteByPrimaryKeys(ids)); + } + + // ========== 项目执行方分配 sub-resource ========== + @GetMapping("/{projectId}/assigns") + public AjaxResult getAssigns(@PathVariable("projectId") Long projectId) + { + return success(bizProjectAssignService.selectByProjectId(projectId)); + } + + @Log(title = "项目执行方分配", businessType = BusinessType.INSERT) + @PostMapping("/{projectId}/assigns") + public AjaxResult saveAssigns(@PathVariable("projectId") Long projectId, @RequestBody List assigns) + { + if (assigns == null) assigns = new ArrayList<>(); + bizProjectAssignService.deleteByProjectId(projectId); + for (BizProjectAssign a : assigns) { + a.setProjectId(projectId); + if (a.getStatus() == null) a.setStatus("0"); + bizProjectAssignService.insert(a); + } + return success(); + } + + @Log(title = "项目执行方分配", businessType = BusinessType.DELETE) + @DeleteMapping("/{projectId}/assigns") + public AjaxResult clearAssigns(@PathVariable("projectId") Long projectId) + { + return toAjax(bizProjectAssignService.deleteByProjectId(projectId)); + } + + /** + * 通用评分 upsert (任何角色: sponsor / executor / compliance) + * POST /business/project/rate + * 不做可见性校验 — 评分公开 + */ + @Log(title = "项目评分", businessType = BusinessType.UPDATE) + @PostMapping("/rate") + public AjaxResult rate(@RequestBody BizProjectRating body) + { + if (body.getProjectId() == null || body.getRaterId() == null || body.getRaterRole() == null) { + return error("projectId / raterId / raterRole 必填"); + } + body.setCreateBy(SecurityUtils.getUsername()); + body.setUpdateBy(SecurityUtils.getUsername()); + int rows = bizProjectRatingService.upsertRating(body); + return toAjax(rows); + } + + /** + * 查某项目的所有评分 (公开, 任何角色可读) + * GET /business/project/ratings?projectId=X + */ + @GetMapping("/ratings") + public AjaxResult ratings(@RequestParam("projectId") Long projectId) + { + BizProjectRating q = new BizProjectRating(); + q.setProjectId(projectId); + return success(bizProjectRatingService.selectList(q)); + } + + /** + * 赞助方分配监察员 (写 biz_project_sponsor_assign) + * POST /business/project/sponsorAssign + */ + @Log(title = "赞助方分配监察员", businessType = BusinessType.INSERT) + @PostMapping("/sponsorAssign") + public AjaxResult sponsorAssign(@RequestBody BizProjectSponsorAssign body) + { + body.setCreateBy(SecurityUtils.getUsername()); + body.setSponsorUserId(SecurityUtils.getUserId()); + int rows = bizProjectSponsorAssignService.insertAssign(body); + return toAjax(rows); + } + + /** + * 赞助方批量分配监察员 (多个项目, 同一个监察员 + 同一份说明) + * POST /business/project/sponsorAssignBatch + * body: List (每个 item.projectId / monitorUserId / assignDesc / assignPoints) + */ + @Log(title = "赞助方批量分配监察员", businessType = BusinessType.INSERT) + @PostMapping("/sponsorAssignBatch") + public AjaxResult sponsorAssignBatch(@RequestBody List bodies) + { + if (bodies == null || bodies.isEmpty()) { + return error("请提供至少一个分配记录"); + } + String loginName = SecurityUtils.getUsername(); + Long loginUid = SecurityUtils.getUserId(); + int ok = 0; + List errors = new ArrayList<>(); + for (int i = 0; i < bodies.size(); i++) { + BizProjectSponsorAssign body = bodies.get(i); + try { + if (body.getProjectId() == null || body.getMonitorUserId() == null) { + throw new IllegalArgumentException("projectId / monitorUserId 必填"); + } + body.setCreateBy(loginName); + body.setSponsorUserId(loginUid); + bizProjectSponsorAssignService.insertAssign(body); + ok++; + } catch (Exception e) { + errors.add("第" + (i + 1) + "条 (projectId=" + body.getProjectId() + "): " + e.getMessage()); + } + } + AjaxResult ajax = success(); + ajax.put("total", bodies.size()); + ajax.put("ok", ok); + ajax.put("errors", errors); + return ajax; + } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizProjectPlanController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizProjectPlanController.java new file mode 100644 index 0000000..3b62532 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizProjectPlanController.java @@ -0,0 +1,53 @@ +package com.ruoyi.business.controller; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.business.domain.BizProjectPlan; +import com.ruoyi.business.service.IBizProjectPlanService; + +/** + * 项目策划方案Controller + */ +@RestController +@RequestMapping("/business/projectPlan") +public class BizProjectPlanController extends BaseController +{ + @Autowired + private IBizProjectPlanService BizProjectPlanService; + @GetMapping("/list") + public TableDataInfo list(BizProjectPlan BizProjectPlan) + { + startPage(); + List list = BizProjectPlanService.selectList(BizProjectPlan); + return getDataTable(list); + } + @GetMapping("/{planId}") + public AjaxResult getInfo(@PathVariable("planId") String planId) + { + return success(BizProjectPlanService.getById(planId)); + } + @Log(title = "项目策划方案", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody BizProjectPlan BizProjectPlan) + { + return toAjax(BizProjectPlanService.insert(BizProjectPlan)); + } + @Log(title = "项目策划方案", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody BizProjectPlan BizProjectPlan) + { + return toAjax(BizProjectPlanService.updateByPrimaryKey(BizProjectPlan)); + } + @Log(title = "项目策划方案", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable String[] ids) + { + return toAjax(BizProjectPlanService.deleteByPrimaryKeys(ids)); + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizProjectRatingController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizProjectRatingController.java new file mode 100644 index 0000000..b467c17 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizProjectRatingController.java @@ -0,0 +1,71 @@ +package com.ruoyi.business.controller; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.business.domain.BizProjectRating; +import com.ruoyi.business.service.IBizProjectRatingService; + +/** + * 项目评分Controller + */ +@RestController +@RequestMapping("/business/projectRating") +public class BizProjectRatingController extends BaseController +{ + @Autowired + private IBizProjectRatingService bizProjectRatingService; + @GetMapping("/list") + public TableDataInfo list(BizProjectRating bizProjectRating) + { + startPage(); + List list = bizProjectRatingService.selectList(bizProjectRating); + return getDataTable(list); + } + @GetMapping("/{ratingId}") + public AjaxResult getInfo(@PathVariable("ratingId") String ratingId) + { + return success(bizProjectRatingService.getById(ratingId)); + } + @Log(title = "项目评分", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody BizProjectRating bizProjectRating) + { + return toAjax(bizProjectRatingService.insert(bizProjectRating)); + } + + /** + * 执行单位评分 upsert (按 project_id + rater_id + rater_role 唯一) + * POST /business/projectRating/rate + */ + @Log(title = "执行单位评分", businessType = BusinessType.UPDATE) + @PostMapping("/rate") + public AjaxResult rate(@RequestBody BizProjectRating body) + { + if (body.getProjectId() == null || body.getRaterId() == null || body.getRaterRole() == null) { + return error("projectId / raterId / raterRole 必填"); + } + body.setCreateBy(getUsername()); + body.setUpdateBy(getUsername()); + int rows = bizProjectRatingService.upsertRating(body); + return toAjax(rows); + } + + @Log(title = "项目评分", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody BizProjectRating bizProjectRating) + { + return toAjax(bizProjectRatingService.updateByPrimaryKey(bizProjectRating)); + } + @Log(title = "项目评分", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable String[] ids) + { + return toAjax(bizProjectRatingService.deleteByPrimaryKeys(ids)); + } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPublicController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPublicController.java new file mode 100644 index 0000000..f43fc1e --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPublicController.java @@ -0,0 +1,74 @@ +package com.ruoyi.business.controller; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.business.domain.BizProject; +import com.ruoyi.business.domain.BizSupportLetter; +import com.ruoyi.business.domain.BizInvitation; +import com.ruoyi.business.domain.BizProjectPlan; +import com.ruoyi.business.service.IBizProjectService; +import com.ruoyi.business.service.IBizSupportLetterService; +import com.ruoyi.business.service.IBizInvitationService; +import com.ruoyi.business.service.IBizProjectPlanService; + +/** + * 公开门户接口(无需登录) + */ +@RestController +@RequestMapping("/business/public") +public class BizPublicController extends BaseController { + + @Autowired private IBizProjectService projectService; + @Autowired private IBizSupportLetterService supportLetterService; + @Autowired private IBizInvitationService invitationService; + @Autowired private IBizProjectPlanService projectPlanService; + + @GetMapping("/index") + public AjaxResult index() { + Map map = new HashMap<>(); + BizProject publishedQuery = new BizProject(); + publishedQuery.setIsPublished("1"); + List announcements = projectService.selectList(publishedQuery); + List plans = projectPlanService.selectList(new BizProjectPlan()); + map.put("announcements", announcements); + map.put("plans", plans); + map.put("about", "北京整合医学学会(BAHIM)是一家专注于整合医学学术研究、项目合作与人才培养的省级学术组织。"); + return success(map); + } + + @GetMapping("/announcements") + public AjaxResult announcements() { + BizProject query = new BizProject(); + query.setIsPublished("1"); + List list = projectService.selectList(query); + return success(list); + } + + @GetMapping("/project/{projectId}") + public AjaxResult projectDetail(@PathVariable String projectId) { + return success(projectService.getById(projectId)); + } + + @GetMapping("/supportLetter/{annId}") + public AjaxResult supportLetterDetail(@PathVariable String annId) { + return success(supportLetterService.getById(annId)); + } + + @GetMapping("/invitation/{annId}") + public AjaxResult invitationDetail(@PathVariable String annId) { + return success(invitationService.getById(annId)); + } + + @GetMapping("/apply") + public AjaxResult apply(String annId) { + return success(); + } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPublicityController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPublicityController.java new file mode 100644 index 0000000..023e15e --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPublicityController.java @@ -0,0 +1,66 @@ +package com.ruoyi.business.controller; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.business.domain.BizPublicity; +import com.ruoyi.business.service.IBizPublicityService; +import com.ruoyi.common.utils.SecurityUtils; + +/** + * 项目公告Controller (与前端 bizList('publicity', ...) 对接) + */ +@RestController +@RequestMapping("/business/publicity") +public class BizPublicityController extends BaseController +{ + @Autowired + private IBizPublicityService bizPublicityService; + + @GetMapping("/list") + public TableDataInfo list(BizPublicity entity) + { + startPage(); + // 按 project_id 过滤 + entity.setStatus(null); + List list = bizPublicityService.selectList(entity); + return getDataTable(list); + } + + @GetMapping("/{id}") + public AjaxResult getInfo(@PathVariable("id") Long id) + { + return success(bizPublicityService.getById(id)); + } + + @Log(title = "项目公告", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody BizPublicity entity) + { + entity.setCreateBy(SecurityUtils.getUsername()); + entity.setUpdateBy(SecurityUtils.getUsername()); + if (entity.getStatus() == null) entity.setStatus("1"); + int rows = bizPublicityService.insert(entity); + return toAjax(rows); + } + + @Log(title = "项目公告", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody BizPublicity entity) + { + entity.setUpdateBy(SecurityUtils.getUsername()); + return toAjax(bizPublicityService.updateByPrimaryKey(entity)); + } + + @Log(title = "项目公告", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Long[] ids) + { + return toAjax(bizPublicityService.deleteByPrimaryKeys(ids)); + } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizRegisterController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizRegisterController.java new file mode 100644 index 0000000..e7a8e72 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizRegisterController.java @@ -0,0 +1,109 @@ +package com.ruoyi.business.controller; + +import java.util.Map; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.business.domain.BizExpert; +import com.ruoyi.business.dto.SmsValidForm; +import com.ruoyi.business.service.IBizExpertService; +import com.ruoyi.business.service.SysSmsService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.utils.uuid.UUID; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.system.service.ISysUserService; + +/** + * 专家注册 (完整实现) + * 流程: + * 1. 校验短信验证码 + * 2. 检查用户名(=手机号)是否已注册 + * 3. 加密密码 + * 4. 插入 sys_user (role_type=doctor, user_type=01) + * 5. 插入 biz_expert (user_id 关联) + */ +@RestController +@RequestMapping("/business/auth") +public class BizRegisterController extends BaseController { + + @Autowired + private SysSmsService smsService; + + @Autowired + private ISysUserService userService; + + @Autowired + private IBizExpertService expertService; + + @Autowired + private BCryptPasswordEncoder passwordEncoder; + + @PostMapping("/registerExpert") + public AjaxResult registerExpert(@RequestBody Map body) { + String realName = (String) body.get("realName"); + String workUnit = (String) body.get("workUnit"); + String department = (String) body.get("department"); + String doctorTitle = (String) body.get("doctorTitle"); + String phone = (String) body.get("phone"); + String code = (String) body.get("code"); + String password = (String) body.get("password"); + String uuid = (String) body.get("uuid"); + String licenseCertUrl = (String) body.get("licenseCertUrl"); + String titleCertUrl = (String) body.get("titleCertUrl"); + + if (realName == null || realName.isEmpty()) return error("姓名不能为空"); + if (workUnit == null || workUnit.isEmpty()) return error("工作单位不能为空"); + if (phone == null || !phone.matches("^1\\d{10}$")) return error("手机号格式错误"); + if (code == null || code.isEmpty()) return error("请输入短信验证码"); + if (password == null || password.length() < 6 || password.length() > 20) return error("密码长度 6-20 位"); + + // 1. 校验短信验证码 + SmsValidForm smsForm = new SmsValidForm(); + smsForm.setPhone(phone); + smsForm.setSmsCode(code); + smsForm.setUuid(uuid); + try { + smsService.verifyCode(smsForm); + } catch (RuntimeException e) { + return error("验证码错误或已过期: " + e.getMessage()); + } + + // 2. 用户名=手机号, 检查是否已注册 + if (userService.isPhoneRegistered(phone)) { + return error("该手机号已注册, 请直接登录"); + } + + // 3. 加密密码 + 4. 插入 sys_user (用户名=phone) + SysUser user = new SysUser(); + user.setUserName(phone); + user.setNickName(realName); + user.setPhonenumber(phone); + user.setPassword(passwordEncoder.encode(password)); + user.setStatus("0"); + userService.insertUser(user); + Long userId = user.getUserId(); + // sys_user.role_type 表字段 (DB 默认 executor, 专家需 = doctor), 用 mapper 更新 + userService.updateRoleType(userId, "doctor"); + + // 5. 插入 biz_expert + BizExpert expert = new BizExpert(); + expert.setExpertId(UUID.fastUUID().toString()); + expert.setUserId(userId); + expert.setName(realName); + expert.setPhone(phone); + expert.setWorkUnit(workUnit); + expert.setDepartment(department); + expert.setTitle(doctorTitle); + expert.setPracticeCertUrl(licenseCertUrl); + expert.setTitleCertUrl(titleCertUrl); + expert.setAuditStatus("0"); + expert.setStatus("0"); + expertService.insert(expert); + + return success("注册成功, 请等待审核").put("userId", userId); + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizRegisterSmsController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizRegisterSmsController.java new file mode 100644 index 0000000..a6eae5d --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizRegisterSmsController.java @@ -0,0 +1,40 @@ +package com.ruoyi.business.controller; + +import com.ruoyi.business.service.SysSmsService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.system.service.ISysUserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +/** + * 注册专用 - 发送短信验证码 + * 校验: 手机号未被注册过, 否则报错 + * 匿名访问 (前端 register-* 页面调用, 登录前) + */ +@RestController +@RequestMapping("/business/auth") +public class BizRegisterSmsController extends BaseController { + + @Autowired + private SysSmsService smsService; + + @Autowired + private com.ruoyi.system.service.ISysUserService userService; + + @PostMapping("/registerSendSms") + public AjaxResult registerSendSms(@RequestBody Map body) { + String phone = body == null ? null : body.get("phone"); + if (phone == null || !phone.matches("^1\\d{10}$")) { + return error("请输入正确的手机号"); + } + // 手机号已注册则报错 + if (userService.isPhoneRegistered(phone)) { + return error("该手机号已注册, 请直接登录"); + } + String uuid = smsService.sendCode(phone); + return AjaxResult.success("验证码已发送", uuid); + } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizResourceController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizResourceController.java new file mode 100644 index 0000000..8d5955e --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizResourceController.java @@ -0,0 +1,53 @@ +package com.ruoyi.business.controller; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.business.domain.BizResource; +import com.ruoyi.business.service.IBizResourceService; + +/** + * 资源Controller + */ +@RestController +@RequestMapping("/business/resource") +public class BizResourceController extends BaseController +{ + @Autowired + private IBizResourceService bizResourceService; + @GetMapping("/list") + public TableDataInfo list(BizResource bizResource) + { + startPage(); + List list = bizResourceService.selectList(bizResource); + return getDataTable(list); + } + @GetMapping("/{resourceId}") + public AjaxResult getInfo(@PathVariable("resourceId") String resourceId) + { + return success(bizResourceService.getById(resourceId)); + } + @Log(title = "资源", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody BizResource bizResource) + { + return toAjax(bizResourceService.insert(bizResource)); + } + @Log(title = "资源", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody BizResource bizResource) + { + return toAjax(bizResourceService.updateByPrimaryKey(bizResource)); + } + @Log(title = "资源", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable String[] ids) + { + return toAjax(bizResourceService.deleteByPrimaryKeys(ids)); + } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizSubmissionController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizSubmissionController.java new file mode 100644 index 0000000..ff1bca0 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizSubmissionController.java @@ -0,0 +1,79 @@ +package com.ruoyi.business.controller; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.business.domain.BizSubmission; +import com.ruoyi.business.enums.SubmissionStatus; +import com.ruoyi.business.service.IBizSubmissionService; + +/** + * 投稿Controller + */ +@RestController +@RequestMapping("/business/submission") +public class BizSubmissionController extends BaseController +{ + @Autowired + private IBizSubmissionService bizSubmissionService; + @GetMapping("/list") + public TableDataInfo list(BizSubmission bizSubmission) + { + // 后端兜底: 只查当前登录用户提交的; admin 跳过此限制 + if (!SecurityUtils.isAdmin()) { + bizSubmission.setSubmitterId(SecurityUtils.getUserId()); + } + startPage(); + List list = bizSubmissionService.selectList(bizSubmission); + return getDataTable(list); + } + @GetMapping("/{submissionId}") + public AjaxResult getInfo(@PathVariable("submissionId") String submissionId) + { + return success(bizSubmissionService.getById(submissionId)); + } + @Log(title = "投稿", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody BizSubmission bizSubmission) + { + // 后端兜底: 强制写入当前登录用户, 防止前端漏传/绕过导致列表过滤查不到 + bizSubmission.setSubmitterId(SecurityUtils.getUserId()); + bizSubmission.setSubmitterName(SecurityUtils.getUsername()); + bizSubmission.setCreateBy(getUsername()); + bizSubmission.setUpdateBy(getUsername()); + // 状态: 新建默认 DRAFT(待提交); 校验必须是英文 code 之一 + if (bizSubmission.getStatus() == null || bizSubmission.getStatus().isEmpty()) { + bizSubmission.setStatus(SubmissionStatus.DRAFT.getCode()); + } else if (!SubmissionStatus.isValid(bizSubmission.getStatus())) { + return AjaxResult.error("状态值非法: " + bizSubmission.getStatus()); + } + return toAjax(bizSubmissionService.insert(bizSubmission)); + } + @Log(title = "投稿", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody BizSubmission bizSubmission) + { + // 修改时也强制覆盖 submitterId/submitterName, 防止越权篡改为他人 + bizSubmission.setSubmitterId(SecurityUtils.getUserId()); + bizSubmission.setSubmitterName(SecurityUtils.getUsername()); + bizSubmission.setUpdateBy(getUsername()); + // 状态校验 (英文 code) + if (bizSubmission.getStatus() != null && !bizSubmission.getStatus().isEmpty() + && !SubmissionStatus.isValid(bizSubmission.getStatus())) { + return AjaxResult.error("状态值非法: " + bizSubmission.getStatus()); + } + return toAjax(bizSubmissionService.updateByPrimaryKey(bizSubmission)); + } + @Log(title = "投稿", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable String[] ids) + { + return toAjax(bizSubmissionService.deleteByPrimaryKeys(ids)); + } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizSupportIntentController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizSupportIntentController.java new file mode 100644 index 0000000..0512234 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizSupportIntentController.java @@ -0,0 +1,53 @@ +package com.ruoyi.business.controller; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.business.domain.BizSupportIntent; +import com.ruoyi.business.service.IBizSupportIntentService; + +/** + * 支持意向Controller + */ +@RestController +@RequestMapping("/business/supportIntent") +public class BizSupportIntentController extends BaseController +{ + @Autowired + private IBizSupportIntentService bizSupportIntentService; + @GetMapping("/list") + public TableDataInfo list(BizSupportIntent bizSupportIntent) + { + startPage(); + List list = bizSupportIntentService.selectList(bizSupportIntent); + return getDataTable(list); + } + @GetMapping("/{intentId}") + public AjaxResult getInfo(@PathVariable("intentId") String intentId) + { + return success(bizSupportIntentService.getById(intentId)); + } + @Log(title = "支持意向", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody BizSupportIntent bizSupportIntent) + { + return toAjax(bizSupportIntentService.insert(bizSupportIntent)); + } + @Log(title = "支持意向", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody BizSupportIntent bizSupportIntent) + { + return toAjax(bizSupportIntentService.updateByPrimaryKey(bizSupportIntent)); + } + @Log(title = "支持意向", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable String[] ids) + { + return toAjax(bizSupportIntentService.deleteByPrimaryKeys(ids)); + } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizSupportLetterController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizSupportLetterController.java new file mode 100644 index 0000000..2f8a281 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizSupportLetterController.java @@ -0,0 +1,53 @@ +package com.ruoyi.business.controller; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.business.domain.BizSupportLetter; +import com.ruoyi.business.service.IBizSupportLetterService; + +/** + * 支持函Controller + */ +@RestController +@RequestMapping("/business/supportLetter") +public class BizSupportLetterController extends BaseController +{ + @Autowired + private IBizSupportLetterService bizSupportLetterService; + @GetMapping("/list") + public TableDataInfo list(BizSupportLetter bizSupportLetter) + { + startPage(); + List list = bizSupportLetterService.selectList(bizSupportLetter); + return getDataTable(list); + } + @GetMapping("/{letterId}") + public AjaxResult getInfo(@PathVariable("letterId") String letterId) + { + return success(bizSupportLetterService.getById(letterId)); + } + @Log(title = "支持函", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody BizSupportLetter bizSupportLetter) + { + return toAjax(bizSupportLetterService.insert(bizSupportLetter)); + } + @Log(title = "支持函", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody BizSupportLetter bizSupportLetter) + { + return toAjax(bizSupportLetterService.updateByPrimaryKey(bizSupportLetter)); + } + @Log(title = "支持函", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable String[] ids) + { + return toAjax(bizSupportLetterService.deleteByPrimaryKeys(ids)); + } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/SmsController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/SmsController.java new file mode 100644 index 0000000..55754ed --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/SmsController.java @@ -0,0 +1,41 @@ +package com.ruoyi.business.controller; + +import com.ruoyi.business.dto.SmsValidForm; +import com.ruoyi.business.service.SysSmsService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +/** + * 短信验证码接口 + * 参考 hwt-code BizAuthApi.sendSmsOnLogin 模式 + */ +@RestController +@RequestMapping("/business/sms") +public class SmsController extends BaseController { + + @Autowired + private SysSmsService smsService; + + /** 发送验证码, 返回 uuid (前端 form 校验时回传) */ + @GetMapping("/send") + public AjaxResult send(@RequestParam(required = false) String phone) { + if (phone == null || phone.trim().isEmpty()) { + return error("手机号码不能为空"); + } + String uuid = smsService.sendCode(phone); + return AjaxResult.success("ok", uuid); + } + + /** 校验验证码 */ + @PostMapping("/verify") + public AjaxResult verify(@RequestBody SmsValidForm form) { + try { + smsService.verifyCode(form); + } catch (RuntimeException e) { + return error(e.getMessage()); + } + return success("ok"); + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizDepartment.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizDepartment.java new file mode 100644 index 0000000..76921b2 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizDepartment.java @@ -0,0 +1,35 @@ +package com.ruoyi.business.domain; + +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** 科室字典 */ +public class BizDepartment extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + @Excel(name = "ID") + private Long deptId; + + @Excel(name = "科室名称") + private String name; + + @Excel(name = "排序") + private Integer sort; + + /** 0=启用 1=停用 */ + @Excel(name = "状态", readConverterExp = "0=启用,1=停用") + private String status; + + public Long getDeptId() { return deptId; } + public void setDeptId(Long deptId) { this.deptId = deptId; } + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public Integer getSort() { return sort; } + public void setSort(Integer sort) { this.sort = sort; } + + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizDoctorTitle.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizDoctorTitle.java new file mode 100644 index 0000000..86f45b3 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizDoctorTitle.java @@ -0,0 +1,35 @@ +package com.ruoyi.business.domain; + +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** 医生职称字典 */ +public class BizDoctorTitle extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + @Excel(name = "ID") + private Long titleId; + + @Excel(name = "职称名称") + private String name; + + @Excel(name = "排序") + private Integer sort; + + /** 0=启用 1=停用 */ + @Excel(name = "状态", readConverterExp = "0=启用,1=停用") + private String status; + + public Long getTitleId() { return titleId; } + public void setTitleId(Long titleId) { this.titleId = titleId; } + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public Integer getSort() { return sort; } + public void setSort(Integer sort) { this.sort = sort; } + + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizExecutionIntent.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizExecutionIntent.java new file mode 100644 index 0000000..0316013 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizExecutionIntent.java @@ -0,0 +1,82 @@ +package com.ruoyi.business.domain; + +import java.math.BigDecimal; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** 执行意向对象 BizExecutionIntent */ +public class BizExecutionIntent extends BaseEntity { + private static final long serialVersionUID = 1L; + /** intentId */ + private String intentId; + /** 用户ID (关联 sys_user.user_id) */ + private Long userId; + /** project_no */ + @Excel(name = "project_no") + private String projectNo; + /** project_name */ + @Excel(name = "project_name") + private String projectName; + /** name */ + @Excel(name = "name") + private String name; + /** work_unit */ + @Excel(name = "work_unit") + private String workUnit; + /** department */ + @Excel(name = "department") + private String department; + /** position */ + @Excel(name = "position") + private String position; + /** phone */ + @Excel(name = "phone") + private String phone; + /** onboard_status */ + @Excel(name = "onboard_status") + private String onboardStatus; + /** create_by */ + @Excel(name = "create_by") + private String createBy; + /** create_time */ + @Excel(name = "create_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + /** update_by */ + @Excel(name = "update_by") + private String updateBy; + /** update_time */ + @Excel(name = "update_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + public String getIntentId() { return intentId; } + public void setIntentId(String intentId) { this.intentId = intentId; } + public Long getUserId() { return userId; } + public void setUserId(Long userId) { this.userId = userId; } + public String getProjectNo() { return projectNo; } + public void setProjectNo(String projectNo) { this.projectNo = projectNo; } + public String getProjectName() { return projectName; } + public void setProjectName(String projectName) { this.projectName = projectName; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public String getWorkUnit() { return workUnit; } + public void setWorkUnit(String workUnit) { this.workUnit = workUnit; } + public String getDepartment() { return department; } + public void setDepartment(String department) { this.department = department; } + public String getPosition() { return position; } + public void setPosition(String position) { this.position = position; } + public String getPhone() { return phone; } + public void setPhone(String phone) { this.phone = phone; } + public String getOnboardStatus() { return onboardStatus; } + public void setOnboardStatus(String onboardStatus) { this.onboardStatus = onboardStatus; } + public String getCreateBy() { return createBy; } + public void setCreateBy(String createBy) { this.createBy = createBy; } + public Date getCreateTime() { return createTime; } + public void setCreateTime(Date createTime) { this.createTime = createTime; } + public String getUpdateBy() { return updateBy; } + public void setUpdateBy(String updateBy) { this.updateBy = updateBy; } + public Date getUpdateTime() { return updateTime; } + public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; } +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizExpert.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizExpert.java new file mode 100644 index 0000000..e4b8e4d --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizExpert.java @@ -0,0 +1,137 @@ +package com.ruoyi.business.domain; + +import java.math.BigDecimal; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** 专家对象 BizExpert */ +public class BizExpert extends BaseEntity { + private static final long serialVersionUID = 1L; + /** expertId */ + private String expertId; + /** name */ + @Excel(name = "name") + private String name; + /** phone */ + @Excel(name = "phone") + private String phone; + /** work_unit */ + @Excel(name = "work_unit") + private String workUnit; + /** department */ + @Excel(name = "department") + private String department; + /** title */ + @Excel(name = "title") + private String title; + /** practice_cert_url */ + @Excel(name = "practice_cert_url") + private String practiceCertUrl; + /** title_cert_url */ + @Excel(name = "title_cert_url") + private String titleCertUrl; + /** audit_status */ + @Excel(name = "audit_status") + private String auditStatus; + /** audit_by */ + @Excel(name = "audit_by") + private String auditBy; + /** audit_time */ + @Excel(name = "audit_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date auditTime; + /** create_by */ + @Excel(name = "create_by") + private String createBy; + /** create_time */ + @Excel(name = "create_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + /** update_by */ + @Excel(name = "update_by") + private String updateBy; + /** update_time */ + @Excel(name = "update_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + /** 关联系统用户ID */ + private Long userId; + /** 地区 */ + private String region; + /** 证件号码 */ + private String idCard; + /** 银行卡号 */ + private String bankCard; + /** 银行名称 */ + private String bankName; + /** 开户行省份 */ + private String bankProvince; + /** 开户行城市 */ + private String bankCity; + /** 开户行地址 */ + private String bankAddress; + /** 身份证正面URL */ + private String idCardFrontUrl; + /** 身份证反面URL */ + private String idCardBackUrl; + /** 审核意见 */ + private String auditOpinion; + /** 状态 0正常 1禁用 */ + private String status; + public String getExpertId() { return expertId; } + public void setExpertId(String expertId) { this.expertId = expertId; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public String getPhone() { return phone; } + public void setPhone(String phone) { this.phone = phone; } + public String getWorkUnit() { return workUnit; } + public void setWorkUnit(String workUnit) { this.workUnit = workUnit; } + public String getDepartment() { return department; } + public void setDepartment(String department) { this.department = department; } + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + public String getPracticeCertUrl() { return practiceCertUrl; } + public void setPracticeCertUrl(String practiceCertUrl) { this.practiceCertUrl = practiceCertUrl; } + public String getTitleCertUrl() { return titleCertUrl; } + public void setTitleCertUrl(String titleCertUrl) { this.titleCertUrl = titleCertUrl; } + public String getAuditStatus() { return auditStatus; } + public void setAuditStatus(String auditStatus) { this.auditStatus = auditStatus; } + public String getAuditBy() { return auditBy; } + public void setAuditBy(String auditBy) { this.auditBy = auditBy; } + public Date getAuditTime() { return auditTime; } + public void setAuditTime(Date auditTime) { this.auditTime = auditTime; } + public String getCreateBy() { return createBy; } + public void setCreateBy(String createBy) { this.createBy = createBy; } + public Date getCreateTime() { return createTime; } + public void setCreateTime(Date createTime) { this.createTime = createTime; } + public String getUpdateBy() { return updateBy; } + public void setUpdateBy(String updateBy) { this.updateBy = updateBy; } + public Date getUpdateTime() { return updateTime; } + public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; } + public Long getUserId() { return userId; } + public void setUserId(Long userId) { this.userId = userId; } + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + public String getAuditOpinion() { return auditOpinion; } + public void setAuditOpinion(String auditOpinion) { this.auditOpinion = auditOpinion; } + public String getRegion() { return region; } + public void setRegion(String region) { this.region = region; } + public String getIdCard() { return idCard; } + public void setIdCard(String idCard) { this.idCard = idCard; } + public String getBankCard() { return bankCard; } + public void setBankCard(String bankCard) { this.bankCard = bankCard; } + public String getBankName() { return bankName; } + public void setBankName(String bankName) { this.bankName = bankName; } + public String getBankProvince() { return bankProvince; } + public void setBankProvince(String bankProvince) { this.bankProvince = bankProvince; } + public String getBankCity() { return bankCity; } + public void setBankCity(String bankCity) { this.bankCity = bankCity; } + public String getBankAddress() { return bankAddress; } + public void setBankAddress(String bankAddress) { this.bankAddress = bankAddress; } + public String getIdCardFrontUrl() { return idCardFrontUrl; } + public void setIdCardFrontUrl(String idCardFrontUrl) { this.idCardFrontUrl = idCardFrontUrl; } + public String getIdCardBackUrl() { return idCardBackUrl; } + public void setIdCardBackUrl(String idCardBackUrl) { this.idCardBackUrl = idCardBackUrl; } +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizInvitation.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizInvitation.java new file mode 100644 index 0000000..d9e1c3b --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizInvitation.java @@ -0,0 +1,78 @@ +package com.ruoyi.business.domain; + +import java.math.BigDecimal; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** 邀请函对象 BizInvitation */ +public class BizInvitation extends BaseEntity { + private static final long serialVersionUID = 1L; + /** annId */ + private String annId; + /** title */ + @Excel(name = "title") + private String title; + /** meeting_name */ + @Excel(name = "meeting_name") + private String meetingName; + /** expert_name */ + @Excel(name = "expert_name") + private String expertName; + /** content */ + @Excel(name = "content") + private String content; + /** issue_date */ + @Excel(name = "issue_date") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date issueDate; + /** create_by */ + @Excel(name = "create_by") + private String createBy; + /** create_time */ + @Excel(name = "create_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + /** update_by */ + @Excel(name = "update_by") + private String updateBy; + /** update_time */ + @Excel(name = "update_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + /** 邀请函ID */ + private Long inviteId; + /** 项目ID */ + private Long projectId; + /** 项目编号 */ + private String projectNo; + /** 邀请函模板URL */ + private String templateUrl; + /** 二维码URL */ + private String qrCodeUrl; + /** 分享URL */ + private String shareUrl; + /** 发布时间 */ + private String publishTime; + public String getAnnId() { return annId; } + public void setAnnId(String annId) { this.annId = annId; } + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + public String getMeetingName() { return meetingName; } + public void setMeetingName(String meetingName) { this.meetingName = meetingName; } + public String getExpertName() { return expertName; } + public void setExpertName(String expertName) { this.expertName = expertName; } + public String getContent() { return content; } + public void setContent(String content) { this.content = content; } + public Date getIssueDate() { return issueDate; } + public void setIssueDate(Date issueDate) { this.issueDate = issueDate; } + public String getCreateBy() { return createBy; } + public void setCreateBy(String createBy) { this.createBy = createBy; } + public Date getCreateTime() { return createTime; } + public void setCreateTime(Date createTime) { this.createTime = createTime; } + public String getUpdateBy() { return updateBy; } + public void setUpdateBy(String updateBy) { this.updateBy = updateBy; } + public Date getUpdateTime() { return updateTime; } + public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; } +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizLaborVoucher.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizLaborVoucher.java new file mode 100644 index 0000000..5d0561c --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizLaborVoucher.java @@ -0,0 +1,87 @@ +package com.ruoyi.business.domain; + +import java.math.BigDecimal; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** 劳务凭证对象 BizLaborVoucher */ +public class BizLaborVoucher extends BaseEntity { + private static final long serialVersionUID = 1L; + /** voucherId */ + private String voucherId; + /** voucher_no */ + @Excel(name = "voucher_no") + private String voucherNo; + /** meeting_name */ + @Excel(name = "meeting_name") + private String meetingName; + /** expert_name */ + @Excel(name = "expert_name") + private String expertName; + /** id_card */ + @Excel(name = "id_card") + private String idCard; + /** bank_account */ + @Excel(name = "bank_account") + private String bankAccount; + /** amount */ + @Excel(name = "amount") + private BigDecimal amount; + /** submit_date */ + @Excel(name = "submit_date") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date submitDate; + /** audit_status */ + @Excel(name = "audit_status") + private String auditStatus; + /** create_by */ + @Excel(name = "create_by") + private String createBy; + /** create_time */ + @Excel(name = "create_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + /** update_by */ + @Excel(name = "update_by") + private String updateBy; + /** update_time */ + @Excel(name = "update_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + /** 会议ID */ + private Long meetingId; + /** 审核意见 */ + private String auditOpinion; + /** 审核人 */ + private String auditBy; + /** 审核时间 */ + private String auditTime; + public String getVoucherId() { return voucherId; } + public void setVoucherId(String voucherId) { this.voucherId = voucherId; } + public String getVoucherNo() { return voucherNo; } + public void setVoucherNo(String voucherNo) { this.voucherNo = voucherNo; } + public String getMeetingName() { return meetingName; } + public void setMeetingName(String meetingName) { this.meetingName = meetingName; } + public String getExpertName() { return expertName; } + public void setExpertName(String expertName) { this.expertName = expertName; } + public String getIdCard() { return idCard; } + public void setIdCard(String idCard) { this.idCard = idCard; } + public String getBankAccount() { return bankAccount; } + public void setBankAccount(String bankAccount) { this.bankAccount = bankAccount; } + public BigDecimal getAmount() { return amount; } + public void setAmount(BigDecimal amount) { this.amount = amount; } + public Date getSubmitDate() { return submitDate; } + public void setSubmitDate(Date submitDate) { this.submitDate = submitDate; } + public String getAuditStatus() { return auditStatus; } + public void setAuditStatus(String auditStatus) { this.auditStatus = auditStatus; } + public String getCreateBy() { return createBy; } + public void setCreateBy(String createBy) { this.createBy = createBy; } + public Date getCreateTime() { return createTime; } + public void setCreateTime(Date createTime) { this.createTime = createTime; } + public String getUpdateBy() { return updateBy; } + public void setUpdateBy(String updateBy) { this.updateBy = updateBy; } + public Date getUpdateTime() { return updateTime; } + public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; } +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeeting.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeeting.java new file mode 100644 index 0000000..5f725f6 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeeting.java @@ -0,0 +1,103 @@ +package com.ruoyi.business.domain; + +import java.math.BigDecimal; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** 会议对象 BizMeeting */ +public class BizMeeting extends BaseEntity { + private static final long serialVersionUID = 1L; + /** meetingId */ + private String meetingId; + /** project_no */ + @Excel(name = "project_no") + private String projectNo; + /** business_id */ + @Excel(name = "business_id") + private String businessId; + /** project_form */ + @Excel(name = "project_form") + private String projectForm; + /** meeting_name */ + @Excel(name = "meeting_name") + private String meetingName; + /** start_time */ + @Excel(name = "start_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date startTime; + /** end_time */ + @Excel(name = "end_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date endTime; + /** total_periods */ + @Excel(name = "total_periods") + private Long totalPeriods; + /** period_no */ + @Excel(name = "period_no") + private Long periodNo; + /** current_stage */ + @Excel(name = "current_stage") + private String currentStage; + /** create_by */ + @Excel(name = "create_by") + private String createBy; + /** create_time */ + @Excel(name = "create_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + /** update_by */ + @Excel(name = "update_by") + private String updateBy; + /** update_time */ + @Excel(name = "update_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + /** 所属项目ID */ + private Long projectId; + /** 项目名称 */ + private String projectName; + /** 所属公司名称 (冗余字段, 由 biz_project.org_name 同步) */ + private String orgName; + /** 监察意见 */ + private String supervisionOpinion; + /** 监察人 */ + private String supervisionBy; + /** 监察时间 */ + private String supervisionTime; + /** 邀请函URL */ + private String invitationUrl; + /** 日程海报URL */ + private String scheduleUrl; + /** 签署劳务 0未签 1已签 */ + private String laborSigned; + public String getMeetingId() { return meetingId; } + public void setMeetingId(String meetingId) { this.meetingId = meetingId; } + public String getProjectNo() { return projectNo; } + public void setProjectNo(String projectNo) { this.projectNo = projectNo; } + public String getBusinessId() { return businessId; } + public void setBusinessId(String businessId) { this.businessId = businessId; } + public String getProjectForm() { return projectForm; } + public void setProjectForm(String projectForm) { this.projectForm = projectForm; } + public String getMeetingName() { return meetingName; } + public void setMeetingName(String meetingName) { this.meetingName = meetingName; } + public Date getStartTime() { return startTime; } + public void setStartTime(Date startTime) { this.startTime = startTime; } + public Date getEndTime() { return endTime; } + public void setEndTime(Date endTime) { this.endTime = endTime; } + public Long getTotalPeriods() { return totalPeriods; } + public void setTotalPeriods(Long totalPeriods) { this.totalPeriods = totalPeriods; } + public Long getPeriodNo() { return periodNo; } + public void setPeriodNo(Long periodNo) { this.periodNo = periodNo; } + public String getCurrentStage() { return currentStage; } + public void setCurrentStage(String currentStage) { this.currentStage = currentStage; } + public String getCreateBy() { return createBy; } + public void setCreateBy(String createBy) { this.createBy = createBy; } + public Date getCreateTime() { return createTime; } + public void setCreateTime(Date createTime) { this.createTime = createTime; } + public String getUpdateBy() { return updateBy; } + public void setUpdateBy(String updateBy) { this.updateBy = updateBy; } + public Date getUpdateTime() { return updateTime; } + public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; } +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeetingSettlement.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeetingSettlement.java new file mode 100644 index 0000000..ace4b15 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeetingSettlement.java @@ -0,0 +1,87 @@ +package com.ruoyi.business.domain; + +import java.math.BigDecimal; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** 会议结算明细对象 BizMeetingSettlement */ +public class BizMeetingSettlement extends BaseEntity { + private static final long serialVersionUID = 1L; + /** id */ + private String id; + /** project_no */ + @Excel(name = "project_no") + private String projectNo; + /** meeting_id */ + @Excel(name = "meeting_id") + private Long meetingId; + /** fee_type */ + @Excel(name = "fee_type") + private String feeType; + /** unit_price */ + @Excel(name = "unit_price") + private BigDecimal unitPrice; + /** qty */ + @Excel(name = "qty") + private Long qty; + /** subtotal */ + @Excel(name = "subtotal") + private BigDecimal subtotal; + /** remark */ + @Excel(name = "remark") + private String remark; + /** audit_status */ + @Excel(name = "audit_status") + private String auditStatus; + /** settlement_type */ + @Excel(name = "settlement_type") + private String settlementType; + /** create_by */ + @Excel(name = "create_by") + private String createBy; + /** create_time */ + @Excel(name = "create_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + /** update_by */ + @Excel(name = "update_by") + private String updateBy; + /** update_time */ + @Excel(name = "update_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + /** 会议名称 */ + private String projectName; + /** 执行单位 */ + private String execUnit; + public String getId() { return id; } + public void setId(String id) { this.id = id; } + public String getProjectNo() { return projectNo; } + public void setProjectNo(String projectNo) { this.projectNo = projectNo; } + public Long getMeetingId() { return meetingId; } + public void setMeetingId(Long meetingId) { this.meetingId = meetingId; } + public String getFeeType() { return feeType; } + public void setFeeType(String feeType) { this.feeType = feeType; } + public BigDecimal getUnitPrice() { return unitPrice; } + public void setUnitPrice(BigDecimal unitPrice) { this.unitPrice = unitPrice; } + public Long getQty() { return qty; } + public void setQty(Long qty) { this.qty = qty; } + public BigDecimal getSubtotal() { return subtotal; } + public void setSubtotal(BigDecimal subtotal) { this.subtotal = subtotal; } + public String getRemark() { return remark; } + public void setRemark(String remark) { this.remark = remark; } + public String getAuditStatus() { return auditStatus; } + public void setAuditStatus(String auditStatus) { this.auditStatus = auditStatus; } + public String getSettlementType() { return settlementType; } + public void setSettlementType(String settlementType) { this.settlementType = settlementType; } + public String getCreateBy() { return createBy; } + public void setCreateBy(String createBy) { this.createBy = createBy; } + public Date getCreateTime() { return createTime; } + public void setCreateTime(Date createTime) { this.createTime = createTime; } + public String getUpdateBy() { return updateBy; } + public void setUpdateBy(String updateBy) { this.updateBy = updateBy; } + public Date getUpdateTime() { return updateTime; } + public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; } +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMessage.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMessage.java new file mode 100644 index 0000000..632044a --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMessage.java @@ -0,0 +1,78 @@ +package com.ruoyi.business.domain; + +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** 个人通知消息对象 biz_message */ +public class BizMessage extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** msg_id */ + private Long msgId; + + /** 接收人 user_id */ + @Excel(name = "接收人") + private Long receiverUserId; + + /** 接收人姓名 (联表显示) */ + @Excel(name = "接收人") + private String receiverName; + + /** 消息类型 1通知 2待办 3系统 */ + @Excel(name = "类型") + private String msgType; + + /** 标题 */ + @Excel(name = "标题") + private String title; + + /** 内容 */ + private String content; + + /** 业务类型 auth/project/meeting/agreement */ + private String bizType; + + /** 关联业务ID */ + private Long bizId; + + /** 已读 0否 1是 */ + @Excel(name = "已读") + private String isRead; + + /** 已读时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date readTime; + + public Long getMsgId() { return msgId; } + public void setMsgId(Long msgId) { this.msgId = msgId; } + + public Long getReceiverUserId() { return receiverUserId; } + public void setReceiverUserId(Long receiverUserId) { this.receiverUserId = receiverUserId; } + + public String getReceiverName() { return receiverName; } + public void setReceiverName(String receiverName) { this.receiverName = receiverName; } + + public String getMsgType() { return msgType; } + public void setMsgType(String msgType) { this.msgType = msgType; } + + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + + public String getContent() { return content; } + public void setContent(String content) { this.content = content; } + + public String getBizType() { return bizType; } + public void setBizType(String bizType) { this.bizType = bizType; } + + public Long getBizId() { return bizId; } + public void setBizId(Long bizId) { this.bizId = bizId; } + + public String getIsRead() { return isRead; } + public void setIsRead(String isRead) { this.isRead = isRead; } + + public Date getReadTime() { return readTime; } + public void setReadTime(Date readTime) { this.readTime = readTime; } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizOrg.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizOrg.java new file mode 100644 index 0000000..89276ee --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizOrg.java @@ -0,0 +1,75 @@ +package com.ruoyi.business.domain; + +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 公司对象 biz_org + * 用途: 赞助方(sponsor) + 执行方(execution) 共用 + * 重构说明: 原 biz_support_unit + biz_execution_unit 合并, 通过 org_type 区分 + */ +public class BizOrg extends BaseEntity { + private static final long serialVersionUID = 1L; + /** org_id */ + private Long orgId; + /** org_name */ + @Excel(name = "org_name") + private String orgName; + /** org_type: sponsor赞助方 / execution执行方 */ + @Excel(name = "org_type") + private String orgType; + /** 企业性质 私营/国营/中外合资/外资/其他 */ + @Excel(name = "business_nature") + private String businessNature; + /** address */ + @Excel(name = "address") + private String address; + /** tax_no */ + @Excel(name = "tax_no") + private String taxNo; + /** contact_name */ + @Excel(name = "contact_name") + private String contactName; + /** contact_phone */ + @Excel(name = "contact_phone") + private String contactPhone; + /** intent_count (仅赞助方用) */ + @Excel(name = "intent_count") + private Integer intentCount; + /** status */ + @Excel(name = "status") + private String status; + /** create_time */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + /** update_time */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + public Long getOrgId() { return orgId; } + public void setOrgId(Long orgId) { this.orgId = orgId; } + public String getOrgName() { return orgName; } + public void setOrgName(String orgName) { this.orgName = orgName; } + public String getOrgType() { return orgType; } + public void setOrgType(String orgType) { this.orgType = orgType; } + public String getBusinessNature() { return businessNature; } + public void setBusinessNature(String businessNature) { this.businessNature = businessNature; } + public String getAddress() { return address; } + public void setAddress(String address) { this.address = address; } + public String getTaxNo() { return taxNo; } + public void setTaxNo(String taxNo) { this.taxNo = taxNo; } + public String getContactName() { return contactName; } + public void setContactName(String contactName) { this.contactName = contactName; } + public String getContactPhone() { return contactPhone; } + public void setContactPhone(String contactPhone) { this.contactPhone = contactPhone; } + public Integer getIntentCount() { return intentCount; } + public void setIntentCount(Integer intentCount) { this.intentCount = intentCount; } + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + public Date getCreateTime() { return createTime; } + public void setCreateTime(Date createTime) { this.createTime = createTime; } + public Date getUpdateTime() { return updateTime; } + public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizPerson.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizPerson.java new file mode 100644 index 0000000..f130758 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizPerson.java @@ -0,0 +1,108 @@ +package com.ruoyi.business.domain; + +import java.math.BigDecimal; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** 人员对象 BizPerson */ +public class BizPerson extends BaseEntity { + private static final long serialVersionUID = 1L; + /** personId */ + private String personId; + /** name */ + @Excel(name = "name") + private String name; + /** phone */ + @Excel(name = "phone") + private String phone; + /** 所属公司ID (FK: biz_org.org_id) */ + private Long orgId; + /** 所属公司名 (LEFT JOIN biz_org, 仅展示, 不入库) */ + private String orgName; + /** 所属公司类型 (LEFT JOIN biz_org, 仅展示, 不入库) */ + private String orgType; + /** department */ + @Excel(name = "department") + private String department; + /** position */ + @Excel(name = "position") + private String position; + /** role */ + @Excel(name = "role") + private String role; + /** status */ + @Excel(name = "status") + private String status; + /** 逻辑删除 0正常 1已删 */ + private String delFlag; + /** create_by */ + @Excel(name = "create_by") + private String createBy; + /** create_time */ + @Excel(name = "create_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + /** update_by */ + @Excel(name = "update_by") + private String updateBy; + /** update_time */ + @Excel(name = "update_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + /** 关联系统用户ID */ + private Long userId; + /** 所属单位类型 execution执行/sponsor支持 */ + private String unitType; + /** 账号类型 (来自 sys_user.account_type, MAIN=主账号 / SUB=子账号) - 仅展示用,不入库 */ + private String accountType; + /** 主账号ID (来自 sys_user.parent_user_id, 子账号指向其主账号) - 仅展示用 */ + private Long parentUserId; + /** 子账号登录账号 (前端传入, 用于创建 sys_user 子账号) - 非持久化字段 */ + private transient String loginUsername; + /** 子账号初始密码 (前端传入, 明文, 创建后不存储明文) - 非持久化字段 */ + private transient String loginPassword; + public String getPersonId() { return personId; } + public void setPersonId(String personId) { this.personId = personId; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public String getPhone() { return phone; } + public void setPhone(String phone) { this.phone = phone; } + public Long getOrgId() { return orgId; } + public void setOrgId(Long orgId) { this.orgId = orgId; } + public String getOrgName() { return orgName; } + public void setOrgName(String orgName) { this.orgName = orgName; } + public String getOrgType() { return orgType; } + public void setOrgType(String orgType) { this.orgType = orgType; } + public String getDepartment() { return department; } + public void setDepartment(String department) { this.department = department; } + public String getPosition() { return position; } + public void setPosition(String position) { this.position = position; } + public String getRole() { return role; } + public void setRole(String role) { this.role = role; } + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + public String getDelFlag() { return delFlag; } + public void setDelFlag(String delFlag) { this.delFlag = delFlag; } + public String getCreateBy() { return createBy; } + public void setCreateBy(String createBy) { this.createBy = createBy; } + public Date getCreateTime() { return createTime; } + public void setCreateTime(Date createTime) { this.createTime = createTime; } + public String getUpdateBy() { return updateBy; } + public void setUpdateBy(String updateBy) { this.updateBy = updateBy; } + public Date getUpdateTime() { return updateTime; } + public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; } + public Long getUserId() { return userId; } + public void setUserId(Long userId) { this.userId = userId; } + public String getUnitType() { return unitType; } + public void setUnitType(String unitType) { this.unitType = unitType; } + public String getAccountType() { return accountType; } + public void setAccountType(String accountType) { this.accountType = accountType; } + public Long getParentUserId() { return parentUserId; } + public void setParentUserId(Long parentUserId) { this.parentUserId = parentUserId; } + public String getLoginUsername() { return loginUsername; } + public void setLoginUsername(String loginUsername) { this.loginUsername = loginUsername; } + public String getLoginPassword() { return loginPassword; } + public void setLoginPassword(String loginPassword) { this.loginPassword = loginPassword; } +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizPersonImportVO.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizPersonImportVO.java new file mode 100644 index 0000000..d819fb5 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizPersonImportVO.java @@ -0,0 +1,51 @@ +package com.ruoyi.business.domain; + +import com.ruoyi.common.annotation.Excel; + +/** + * sponsor 端人员批量导入 VO (中文列头, 用 ruoyi ExcelUtil 解析) + * + *

区别于 BizPerson 实体: 这里只暴露用户能填写的业务字段, 不包含内部 id/审计字段, + * 列头按原型 sponsor-new-person.html: 姓名/手机号/所属公司/部门/职务/角色/状态. + * + *

导入后端会按以下规则创建 sys_user 子账号 (parent_user_id=当前登录主账号): + *

    + *
  • loginUsername = 手机号 (要求唯一)
  • + *
  • loginPassword = "123456" (默认密码, 前端 toast 提示用户)
  • + *
  • unitType = "sponsor"
  • + *
  • orgId = 业务层按 orgName 查 biz_org 取 org_id (TODO: SponsorPeople 那边 import 接口需补这步)
  • + *
+ */ +public class BizPersonImportVO +{ + @Excel(name = "姓名", sort = 1) + private String name; + + @Excel(name = "手机号", sort = 2) + private String phone; + + @Excel(name = "所属公司", sort = 3) + private String orgName; + + @Excel(name = "部门", sort = 4) + private String department; + + @Excel(name = "职务", sort = 5) + private String position; + + @Excel(name = "角色", sort = 6) + private String role; + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public String getPhone() { return phone; } + public void setPhone(String phone) { this.phone = phone; } + public String getOrgName() { return orgName; } + public void setOrgName(String orgName) { this.orgName = orgName; } + public String getDepartment() { return department; } + public void setDepartment(String department) { this.department = department; } + public String getPosition() { return position; } + public void setPosition(String position) { this.position = position; } + public String getRole() { return role; } + public void setRole(String role) { this.role = role; } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProject.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProject.java new file mode 100644 index 0000000..49b56d7 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProject.java @@ -0,0 +1,212 @@ +package com.ruoyi.business.domain; + +import java.math.BigDecimal; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** 项目对象 BizProject */ +public class BizProject extends BaseEntity { + private static final long serialVersionUID = 1L; + /** projectId */ + private String projectId; + /** project_no */ + @Excel(name = "project_no") + private String projectNo; + /** project_name */ + @Excel(name = "project_name") + private String projectName; + /** total_sessions */ + @Excel(name = "total_sessions") + private Long totalSessions; + /** done_sessions */ + @Excel(name = "done_sessions") + private Long doneSessions; + /** todo_sessions */ + @Excel(name = "todo_sessions") + private Long todoSessions; + /** total_amount */ + @Excel(name = "total_amount") + private BigDecimal totalAmount; + /** available_amount */ + @Excel(name = "available_amount") + private BigDecimal availableAmount; + /** paid_labor_amount */ + @Excel(name = "paid_labor_amount") + private BigDecimal paidLaborAmount; + /** paid_meeting_amount */ + @Excel(name = "paid_meeting_amount") + private BigDecimal paidMeetingAmount; + /** rating_score (平均分) */ + @Excel(name = "rating_score") + private BigDecimal ratingScore; + /** 评分维度: 履约质量 */ + private BigDecimal ratingQ1; + /** 评分维度: 时效响应 */ + private BigDecimal ratingQ2; + /** 评分维度: 配合度 */ + private BigDecimal ratingQ3; + /** 评分维度: 合规安全 */ + private BigDecimal ratingQ4; + /** org_name (赞助方/执行方, 由 org_type 区分) */ + @Excel(name = "org_name") + private String orgName; + /** org_type: sponsor 赞助方 / execution 执行方 */ + @Excel(name = "org_type") + private String orgType; + /** project_form */ + @Excel(name = "project_form") + private String projectForm; + /** is_finished */ + @Excel(name = "is_finished") + private String isFinished; + /** is_settled */ + @Excel(name = "is_settled") + private String isSettled; + /** org_id (赞助方/执行方, 由 org_type 区分) */ + @Excel(name = "org_id") + private Long orgId; + /** create_by */ + @Excel(name = "create_by") + private String createBy; + /** create_time */ + @Excel(name = "create_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + /** update_by */ + @Excel(name = "update_by") + private String updateBy; + /** update_time */ + @Excel(name = "update_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + /** 管理费及税金 */ + private BigDecimal manageFee; + /** 项目开始时间 */ + private String startTime; + /** 项目结束时间 */ + private String endTime; + /** 提交材料截止天数 */ + private Integer submitDeadlineDays; + /** 支持合同文件URL */ + private String supportContractUrl; + /** 执行合同文件URL */ + private String executeContractUrl; + /** 邀请函文件URL */ + private String invitationUrl; + /** 支持函文件URL */ + private String supportLetterUrl; + /** 已发布公告URL */ + private String publishUrl; + /** 通知文件URL */ + private String noticeUrl; + /** 日程文件URL */ + private String scheduleUrl; + /** 是否已发布公示 0否 1是 */ + private String isPublished; + /** 发布时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date publishTime; + /** 公告类型 (邀请函/支持函/通知/日程/公示) */ + private String announcementType; + + /* ============ 赞助方评分字段 (来自 biz_project_sponsor 中间表, 仅展示用) ============ */ + /** 当前 login 用户对该项目的平均分 */ + private BigDecimal sponsorRating; + private Integer sponsorQ1; + private Integer sponsorQ2; + private Integer sponsorQ3; + private Integer sponsorQ4; + private String sponsorRemark; + public String getProjectId() { return projectId; } + public void setProjectId(String projectId) { this.projectId = projectId; } + public String getProjectNo() { return projectNo; } + public void setProjectNo(String projectNo) { this.projectNo = projectNo; } + public String getProjectName() { return projectName; } + public void setProjectName(String projectName) { this.projectName = projectName; } + public Long getTotalSessions() { return totalSessions; } + public void setTotalSessions(Long totalSessions) { this.totalSessions = totalSessions; } + public Long getDoneSessions() { return doneSessions; } + public void setDoneSessions(Long doneSessions) { this.doneSessions = doneSessions; } + public Long getTodoSessions() { return todoSessions; } + public void setTodoSessions(Long todoSessions) { this.todoSessions = todoSessions; } + public BigDecimal getTotalAmount() { return totalAmount; } + public void setTotalAmount(BigDecimal totalAmount) { this.totalAmount = totalAmount; } + public BigDecimal getAvailableAmount() { return availableAmount; } + public void setAvailableAmount(BigDecimal availableAmount) { this.availableAmount = availableAmount; } + public BigDecimal getPaidLaborAmount() { return paidLaborAmount; } + public void setPaidLaborAmount(BigDecimal paidLaborAmount) { this.paidLaborAmount = paidLaborAmount; } + public BigDecimal getPaidMeetingAmount() { return paidMeetingAmount; } + public void setPaidMeetingAmount(BigDecimal paidMeetingAmount) { this.paidMeetingAmount = paidMeetingAmount; } + public BigDecimal getRatingScore() { return ratingScore; } + public void setRatingScore(BigDecimal ratingScore) { this.ratingScore = ratingScore; } + public BigDecimal getRatingQ1() { return ratingQ1; } + public void setRatingQ1(BigDecimal ratingQ1) { this.ratingQ1 = ratingQ1; } + public BigDecimal getRatingQ2() { return ratingQ2; } + public void setRatingQ2(BigDecimal ratingQ2) { this.ratingQ2 = ratingQ2; } + public BigDecimal getRatingQ3() { return ratingQ3; } + public void setRatingQ3(BigDecimal ratingQ3) { this.ratingQ3 = ratingQ3; } + public BigDecimal getRatingQ4() { return ratingQ4; } + public void setRatingQ4(BigDecimal ratingQ4) { this.ratingQ4 = ratingQ4; } + public String getOrgName() { return orgName; } + public void setOrgName(String orgName) { this.orgName = orgName; } + public String getOrgType() { return orgType; } + public void setOrgType(String orgType) { this.orgType = orgType; } + public String getProjectForm() { return projectForm; } + public void setProjectForm(String projectForm) { this.projectForm = projectForm; } + public String getIsFinished() { return isFinished; } + public void setIsFinished(String isFinished) { this.isFinished = isFinished; } + public String getIsSettled() { return isSettled; } + public void setIsSettled(String isSettled) { this.isSettled = isSettled; } + public Long getOrgId() { return orgId; } + public void setOrgId(Long orgId) { this.orgId = orgId; } + public String getCreateBy() { return createBy; } + public void setCreateBy(String createBy) { this.createBy = createBy; } + public Date getCreateTime() { return createTime; } + public void setCreateTime(Date createTime) { this.createTime = createTime; } + public String getUpdateBy() { return updateBy; } + public void setUpdateBy(String updateBy) { this.updateBy = updateBy; } + public Date getUpdateTime() { return updateTime; } + public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; } + public BigDecimal getManageFee() { return manageFee; } + public void setManageFee(BigDecimal manageFee) { this.manageFee = manageFee; } + public String getStartTime() { return startTime; } + public void setStartTime(String startTime) { this.startTime = startTime; } + public String getEndTime() { return endTime; } + public void setEndTime(String endTime) { this.endTime = endTime; } + public Integer getSubmitDeadlineDays() { return submitDeadlineDays; } + public void setSubmitDeadlineDays(Integer submitDeadlineDays) { this.submitDeadlineDays = submitDeadlineDays; } + public String getSupportContractUrl() { return supportContractUrl; } + public void setSupportContractUrl(String supportContractUrl) { this.supportContractUrl = supportContractUrl; } + public String getExecuteContractUrl() { return executeContractUrl; } + public void setExecuteContractUrl(String executeContractUrl) { this.executeContractUrl = executeContractUrl; } + public String getInvitationUrl() { return invitationUrl; } + public void setInvitationUrl(String invitationUrl) { this.invitationUrl = invitationUrl; } + public String getSupportLetterUrl() { return supportLetterUrl; } + public void setSupportLetterUrl(String supportLetterUrl) { this.supportLetterUrl = supportLetterUrl; } + public String getPublishUrl() { return publishUrl; } + public void setPublishUrl(String publishUrl) { this.publishUrl = publishUrl; } + public String getNoticeUrl() { return noticeUrl; } + public void setNoticeUrl(String noticeUrl) { this.noticeUrl = noticeUrl; } + public String getScheduleUrl() { return scheduleUrl; } + public void setScheduleUrl(String scheduleUrl) { this.scheduleUrl = scheduleUrl; } + public String getIsPublished() { return isPublished; } + public void setIsPublished(String isPublished) { this.isPublished = isPublished; } + public Date getPublishTime() { return publishTime; } + public void setPublishTime(Date publishTime) { this.publishTime = publishTime; } + public String getAnnouncementType() { return announcementType; } + public void setAnnouncementType(String announcementType) { this.announcementType = announcementType; } + public BigDecimal getSponsorRating() { return sponsorRating; } + public void setSponsorRating(BigDecimal sponsorRating) { this.sponsorRating = sponsorRating; } + public Integer getSponsorQ1() { return sponsorQ1; } + public void setSponsorQ1(Integer sponsorQ1) { this.sponsorQ1 = sponsorQ1; } + public Integer getSponsorQ2() { return sponsorQ2; } + public void setSponsorQ2(Integer sponsorQ2) { this.sponsorQ2 = sponsorQ2; } + public Integer getSponsorQ3() { return sponsorQ3; } + public void setSponsorQ3(Integer sponsorQ3) { this.sponsorQ3 = sponsorQ3; } + public Integer getSponsorQ4() { return sponsorQ4; } + public void setSponsorQ4(Integer sponsorQ4) { this.sponsorQ4 = sponsorQ4; } + public String getSponsorRemark() { return sponsorRemark; } + public void setSponsorRemark(String sponsorRemark) { this.sponsorRemark = sponsorRemark; } +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectAssign.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectAssign.java new file mode 100644 index 0000000..b5c496f --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectAssign.java @@ -0,0 +1,50 @@ +package com.ruoyi.business.domain; + +import java.math.BigDecimal; +import com.ruoyi.common.core.domain.BaseEntity; + +/** 项目执行方分配对象 BizProjectAssign */ +public class BizProjectAssign extends BaseEntity { + private static final long serialVersionUID = 1L; + /** assign_id */ + private String assignId; + /** project_id */ + private Long projectId; + /** 执行方用户ID (sys_user.user_id) */ + private Long execUserId; + /** 执行方用户名 (denormalized) */ + private String execUserName; + /** 执行方昵称 (denormalized) */ + private String execNickName; + /** 执行方所属机构 (denormalized) */ + private String execOrg; + /** 分配场次 */ + private Integer sessions; + /** 分配金额 */ + private BigDecimal amount; + /** 备注 */ + private String remark; + /** 状态 0正常 1已撤销 */ + private String status; + + public String getAssignId() { return assignId; } + public void setAssignId(String assignId) { this.assignId = assignId; } + public Long getProjectId() { return projectId; } + public void setProjectId(Long projectId) { this.projectId = projectId; } + public Long getExecUserId() { return execUserId; } + public void setExecUserId(Long execUserId) { this.execUserId = execUserId; } + public String getExecUserName() { return execUserName; } + public void setExecUserName(String execUserName) { this.execUserName = execUserName; } + public String getExecNickName() { return execNickName; } + public void setExecNickName(String execNickName) { this.execNickName = execNickName; } + public String getExecOrg() { return execOrg; } + public void setExecOrg(String execOrg) { this.execOrg = execOrg; } + public Integer getSessions() { return sessions; } + public void setSessions(Integer sessions) { this.sessions = sessions; } + public BigDecimal getAmount() { return amount; } + public void setAmount(BigDecimal amount) { this.amount = amount; } + public String getRemark() { return remark; } + public void setRemark(String remark) { this.remark = remark; } + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectPlan.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectPlan.java new file mode 100644 index 0000000..cece1ed --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectPlan.java @@ -0,0 +1,94 @@ +package com.ruoyi.business.domain; + +import java.math.BigDecimal; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** 项目策划方案对象 BizProjectPlan */ +public class BizProjectPlan extends BaseEntity { + private static final long serialVersionUID = 1L; + /** planId */ + private String planId; + /** plan_name */ + @Excel(name = "plan_name") + private String planName; + /** plan_direction */ + @Excel(name = "plan_direction") + private String planDirection; + /** plan_category */ + @Excel(name = "plan_category") + private String planCategory; + /** project_form */ + @Excel(name = "project_form") + private String projectForm; + /** design_file_url */ + @Excel(name = "design_file_url") + private String designFileUrl; + /** status */ + @Excel(name = "status") + private String status; + /** is_settled */ + @Excel(name = "is_settled") + private String isSettled; + /** project_no */ + @Excel(name = "project_no") + private String projectNo; + /** remark */ + @Excel(name = "remark") + private String remark; + /** is_finished */ + @Excel(name = "is_finished") + private String isFinished; + /** create_by */ + @Excel(name = "create_by") + private String createBy; + /** create_time */ + @Excel(name = "create_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + /** update_by */ + @Excel(name = "update_by") + private String updateBy; + /** update_time */ + @Excel(name = "update_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + /** 审核意见 */ + private String auditOpinion; + /** 审核人 */ + private String auditBy; + /** 审核时间 */ + private String auditTime; + public String getPlanId() { return planId; } + public void setPlanId(String planId) { this.planId = planId; } + public String getPlanName() { return planName; } + public void setPlanName(String planName) { this.planName = planName; } + public String getPlanDirection() { return planDirection; } + public void setPlanDirection(String planDirection) { this.planDirection = planDirection; } + public String getPlanCategory() { return planCategory; } + public void setPlanCategory(String planCategory) { this.planCategory = planCategory; } + public String getProjectForm() { return projectForm; } + public void setProjectForm(String projectForm) { this.projectForm = projectForm; } + public String getDesignFileUrl() { return designFileUrl; } + public void setDesignFileUrl(String designFileUrl) { this.designFileUrl = designFileUrl; } + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + public String getIsSettled() { return isSettled; } + public void setIsSettled(String isSettled) { this.isSettled = isSettled; } + public String getProjectNo() { return projectNo; } + public void setProjectNo(String projectNo) { this.projectNo = projectNo; } + public String getRemark() { return remark; } + public void setRemark(String remark) { this.remark = remark; } + public String getIsFinished() { return isFinished; } + public void setIsFinished(String isFinished) { this.isFinished = isFinished; } + public String getCreateBy() { return createBy; } + public void setCreateBy(String createBy) { this.createBy = createBy; } + public Date getCreateTime() { return createTime; } + public void setCreateTime(Date createTime) { this.createTime = createTime; } + public String getUpdateBy() { return updateBy; } + public void setUpdateBy(String updateBy) { this.updateBy = updateBy; } + public Date getUpdateTime() { return updateTime; } + public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; } +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectRating.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectRating.java new file mode 100644 index 0000000..881fd43 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectRating.java @@ -0,0 +1,100 @@ +package com.ruoyi.business.domain; + +import java.math.BigDecimal; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** 项目评分对象 BizProjectRating */ +public class BizProjectRating extends BaseEntity { + private static final long serialVersionUID = 1L; + /** ratingId */ + private String ratingId; + /** project_no */ + @Excel(name = "project_no") + private String projectNo; + /** project_name */ + @Excel(name = "project_name") + private String projectName; + /** rater_name */ + @Excel(name = "rater_name") + private String raterName; + /** rater_role */ + @Excel(name = "rater_role") + private String raterRole; + /** quality_score */ + @Excel(name = "quality_score") + private String qualityScore; + /** response_score */ + @Excel(name = "response_score") + private Long responseScore; + /** cooperation_score */ + @Excel(name = "cooperation_score") + private Long cooperationScore; + /** compliance_score */ + @Excel(name = "compliance_score") + private Long complianceScore; + /** total_score */ + @Excel(name = "total_score") + private BigDecimal totalScore; + /** remark */ + @Excel(name = "remark") + private String remark; + /** create_by */ + @Excel(name = "create_by") + private String createBy; + /** create_time */ + @Excel(name = "create_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + /** update_by */ + @Excel(name = "update_by") + private String updateBy; + /** update_time */ + @Excel(name = "update_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + /** 项目ID */ + private Long projectId; + /** 评分人ID */ + private Long raterId; + /** 评分时间 */ + private String ratingTime; + public String getRatingId() { return ratingId; } + public void setRatingId(String ratingId) { this.ratingId = ratingId; } + public String getProjectNo() { return projectNo; } + public void setProjectNo(String projectNo) { this.projectNo = projectNo; } + public String getProjectName() { return projectName; } + public void setProjectName(String projectName) { this.projectName = projectName; } + public String getRaterName() { return raterName; } + public void setRaterName(String raterName) { this.raterName = raterName; } + public String getRaterRole() { return raterRole; } + public void setRaterRole(String raterRole) { this.raterRole = raterRole; } + public String getQualityScore() { return qualityScore; } + public void setQualityScore(String qualityScore) { this.qualityScore = qualityScore; } + public Long getResponseScore() { return responseScore; } + public void setResponseScore(Long responseScore) { this.responseScore = responseScore; } + public Long getCooperationScore() { return cooperationScore; } + public void setCooperationScore(Long cooperationScore) { this.cooperationScore = cooperationScore; } + public Long getComplianceScore() { return complianceScore; } + public void setComplianceScore(Long complianceScore) { this.complianceScore = complianceScore; } + public BigDecimal getTotalScore() { return totalScore; } + public void setTotalScore(BigDecimal totalScore) { this.totalScore = totalScore; } + public String getRemark() { return remark; } + public void setRemark(String remark) { this.remark = remark; } + public String getCreateBy() { return createBy; } + public void setCreateBy(String createBy) { this.createBy = createBy; } + public Date getCreateTime() { return createTime; } + public void setCreateTime(Date createTime) { this.createTime = createTime; } + public String getUpdateBy() { return updateBy; } + public void setUpdateBy(String updateBy) { this.updateBy = updateBy; } + public Date getUpdateTime() { return updateTime; } + public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; } + public Long getProjectId() { return projectId; } + public void setProjectId(Long projectId) { this.projectId = projectId; } + public Long getRaterId() { return raterId; } + public void setRaterId(Long raterId) { this.raterId = raterId; } + public String getRatingTime() { return ratingTime; } + public void setRatingTime(String ratingTime) { this.ratingTime = ratingTime; } +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectSponsorAssign.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectSponsorAssign.java new file mode 100644 index 0000000..6e6863d --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectSponsorAssign.java @@ -0,0 +1,42 @@ +package com.ruoyi.business.domain; + +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.core.domain.BaseEntity; + +/** 项目-赞助方-监察员分配记录 (biz_project_sponsor_assign) */ +public class BizProjectSponsorAssign extends BaseEntity { + private static final long serialVersionUID = 1L; + private Long id; + private String projectId; + private Long sponsorUserId; + private Long monitorUserId; + private String assignDesc; + private String assignPoints; + + /** 关联展示字段 (非持久化) */ + private String sponsorUserName; + private String monitorUserName; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public String getProjectId() { return projectId; } + public void setProjectId(String projectId) { this.projectId = projectId; } + public Long getSponsorUserId() { return sponsorUserId; } + public void setSponsorUserId(Long sponsorUserId) { this.sponsorUserId = sponsorUserId; } + public Long getMonitorUserId() { return monitorUserId; } + public void setMonitorUserId(Long monitorUserId) { this.monitorUserId = monitorUserId; } + public String getAssignDesc() { return assignDesc; } + public void setAssignDesc(String assignDesc) { this.assignDesc = assignDesc; } + public String getAssignPoints() { return assignPoints; } + public void setAssignPoints(String assignPoints) { this.assignPoints = assignPoints; } + public String getSponsorUserName() { return sponsorUserName; } + public void setSponsorUserName(String sponsorUserName) { this.sponsorUserName = sponsorUserName; } + public String getMonitorUserName() { return monitorUserName; } + public void setMonitorUserName(String monitorUserName) { this.monitorUserName = monitorUserName; } + public Date getCreateTime() { return createTime; } + public void setCreateTime(Date createTime) { this.createTime = createTime; } +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizPublicity.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizPublicity.java new file mode 100644 index 0000000..7555948 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizPublicity.java @@ -0,0 +1,61 @@ +package com.ruoyi.business.domain; + +import java.util.Date; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** 项目公告对象 BizPublicity */ +public class BizPublicity extends BaseEntity +{ + private static final long serialVersionUID = 1L; + /** PK */ + private Long id; + /** 项目ID */ + @Excel(name = "project_id") + private Long projectId; + /** 项目编号 */ + @Excel(name = "project_no") + private String projectNo; + /** 项目名称 */ + @Excel(name = "project_name") + private String projectName; + /** 公告类型 (invitation/support/notice/agenda) */ + @Excel(name = "announce_type") + private String announceType; + /** 公告标题 */ + @Excel(name = "title") + private String title; + /** 公告文件URL */ + @Excel(name = "file_url") + private String fileUrl; + /** 备注 */ + @Excel(name = "remark") + private String remark; + /** 状态 0保存 1发布 */ + @Excel(name = "status") + private String status; + /** 发布时间 */ + @Excel(name = "rating_time") + private Date ratingTime; + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public Long getProjectId() { return projectId; } + public void setProjectId(Long projectId) { this.projectId = projectId; } + public String getProjectNo() { return projectNo; } + public void setProjectNo(String projectNo) { this.projectNo = projectNo; } + public String getProjectName() { return projectName; } + public void setProjectName(String projectName) { this.projectName = projectName; } + public String getAnnounceType() { return announceType; } + public void setAnnounceType(String announceType) { this.announceType = announceType; } + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + public String getFileUrl() { return fileUrl; } + public void setFileUrl(String fileUrl) { this.fileUrl = fileUrl; } + public String getRemark() { return remark; } + public void setRemark(String remark) { this.remark = remark; } + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + public Date getRatingTime() { return ratingTime; } + public void setRatingTime(Date ratingTime) { this.ratingTime = ratingTime; } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizResource.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizResource.java new file mode 100644 index 0000000..056a374 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizResource.java @@ -0,0 +1,69 @@ +package com.ruoyi.business.domain; + +import java.math.BigDecimal; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** 资料对象 BizResource */ +public class BizResource extends BaseEntity { + private static final long serialVersionUID = 1L; + /** resourceId */ + private String resourceId; + /** name */ + @Excel(name = "name") + private String name; + /** category */ + @Excel(name = "category") + private String category; + /** file_size */ + @Excel(name = "file_size") + private String fileSize; + /** uploader_name */ + @Excel(name = "uploader_name") + private String uploaderName; + /** upload_time */ + @Excel(name = "upload_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date uploadTime; + /** status */ + @Excel(name = "status") + private String status; + /** create_by */ + @Excel(name = "create_by") + private String createBy; + /** create_time */ + @Excel(name = "create_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + /** update_by */ + @Excel(name = "update_by") + private String updateBy; + /** update_time */ + @Excel(name = "update_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + public String getResourceId() { return resourceId; } + public void setResourceId(String resourceId) { this.resourceId = resourceId; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public String getCategory() { return category; } + public void setCategory(String category) { this.category = category; } + public String getFileSize() { return fileSize; } + public void setFileSize(String fileSize) { this.fileSize = fileSize; } + public String getUploaderName() { return uploaderName; } + public void setUploaderName(String uploaderName) { this.uploaderName = uploaderName; } + public Date getUploadTime() { return uploadTime; } + public void setUploadTime(Date uploadTime) { this.uploadTime = uploadTime; } + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + public String getCreateBy() { return createBy; } + public void setCreateBy(String createBy) { this.createBy = createBy; } + public Date getCreateTime() { return createTime; } + public void setCreateTime(Date createTime) { this.createTime = createTime; } + public String getUpdateBy() { return updateBy; } + public void setUpdateBy(String updateBy) { this.updateBy = updateBy; } + public Date getUpdateTime() { return updateTime; } + public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; } +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizSubmission.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizSubmission.java new file mode 100644 index 0000000..d6e5f5b --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizSubmission.java @@ -0,0 +1,86 @@ +package com.ruoyi.business.domain; + +import java.math.BigDecimal; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** 投稿对象 BizSubmission */ +public class BizSubmission extends BaseEntity { + private static final long serialVersionUID = 1L; + /** subId */ + private String subId; + /** title */ + @Excel(name = "title") + private String title; + /** direction */ + @Excel(name = "direction") + private String direction; + /** project_form */ + @Excel(name = "project_form") + private String projectForm; + /** design_file_url */ + @Excel(name = "design_file_url") + private String designFileUrl; + /** status */ + @Excel(name = "status") + private String status; + /** remark */ + @Excel(name = "remark") + private String remark; + /** create_by */ + @Excel(name = "create_by") + private String createBy; + /** create_time */ + @Excel(name = "create_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + /** update_by */ + @Excel(name = "update_by") + private String updateBy; + /** update_time */ + @Excel(name = "update_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + /** 投稿人用户ID */ + private Long submitterId; + /** 投稿人姓名 */ + private String submitterName; + /** 项目类别 学术会议类/专项科研类/调研征集类/慈善帮扶类/标准制定类/患者援助类/专业培训类 */ + private String projectCategory; + /** 审核意见 */ + private String auditOpinion; + /** 审核人 */ + private String auditBy; + /** 审核时间 */ + private String auditTime; + public String getSubId() { return subId; } + public void setSubId(String subId) { this.subId = subId; } + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + public String getDirection() { return direction; } + public void setDirection(String direction) { this.direction = direction; } + public String getProjectForm() { return projectForm; } + public void setProjectForm(String projectForm) { this.projectForm = projectForm; } + public String getProjectCategory() { return projectCategory; } + public void setProjectCategory(String projectCategory) { this.projectCategory = projectCategory; } + public String getDesignFileUrl() { return designFileUrl; } + public void setDesignFileUrl(String designFileUrl) { this.designFileUrl = designFileUrl; } + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + public String getRemark() { return remark; } + public void setRemark(String remark) { this.remark = remark; } + public Long getSubmitterId() { return submitterId; } + public void setSubmitterId(Long submitterId) { this.submitterId = submitterId; } + public String getSubmitterName() { return submitterName; } + public void setSubmitterName(String submitterName) { this.submitterName = submitterName; } + public String getCreateBy() { return createBy; } + public void setCreateBy(String createBy) { this.createBy = createBy; } + public Date getCreateTime() { return createTime; } + public void setCreateTime(Date createTime) { this.createTime = createTime; } + public String getUpdateBy() { return updateBy; } + public void setUpdateBy(String updateBy) { this.updateBy = updateBy; } + public Date getUpdateTime() { return updateTime; } + public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; } +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizSupportIntent.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizSupportIntent.java new file mode 100644 index 0000000..423b5ab --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizSupportIntent.java @@ -0,0 +1,78 @@ +package com.ruoyi.business.domain; + +import java.math.BigDecimal; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** 支持意向对象 BizSupportIntent */ +public class BizSupportIntent extends BaseEntity { + private static final long serialVersionUID = 1L; + /** intentId */ + private String intentId; + /** project_no */ + @Excel(name = "project_no") + private String projectNo; + /** project_name */ + @Excel(name = "project_name") + private String projectName; + /** name */ + @Excel(name = "name") + private String name; + /** work_unit */ + @Excel(name = "work_unit") + private String workUnit; + /** department */ + @Excel(name = "department") + private String department; + /** position */ + @Excel(name = "position") + private String position; + /** phone */ + @Excel(name = "phone") + private String phone; + /** account_status */ + @Excel(name = "account_status") + private String accountStatus; + /** create_by */ + @Excel(name = "create_by") + private String createBy; + /** create_time */ + @Excel(name = "create_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + /** update_by */ + @Excel(name = "update_by") + private String updateBy; + /** update_time */ + @Excel(name = "update_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + public String getIntentId() { return intentId; } + public void setIntentId(String intentId) { this.intentId = intentId; } + public String getProjectNo() { return projectNo; } + public void setProjectNo(String projectNo) { this.projectNo = projectNo; } + public String getProjectName() { return projectName; } + public void setProjectName(String projectName) { this.projectName = projectName; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public String getWorkUnit() { return workUnit; } + public void setWorkUnit(String workUnit) { this.workUnit = workUnit; } + public String getDepartment() { return department; } + public void setDepartment(String department) { this.department = department; } + public String getPosition() { return position; } + public void setPosition(String position) { this.position = position; } + public String getPhone() { return phone; } + public void setPhone(String phone) { this.phone = phone; } + public String getAccountStatus() { return accountStatus; } + public void setAccountStatus(String accountStatus) { this.accountStatus = accountStatus; } + public String getCreateBy() { return createBy; } + public void setCreateBy(String createBy) { this.createBy = createBy; } + public Date getCreateTime() { return createTime; } + public void setCreateTime(Date createTime) { this.createTime = createTime; } + public String getUpdateBy() { return updateBy; } + public void setUpdateBy(String updateBy) { this.updateBy = updateBy; } + public Date getUpdateTime() { return updateTime; } + public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; } +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizSupportLetter.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizSupportLetter.java new file mode 100644 index 0000000..c0f3be7 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizSupportLetter.java @@ -0,0 +1,76 @@ +package com.ruoyi.business.domain; + +import java.math.BigDecimal; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** 支持函对象 BizSupportLetter */ +public class BizSupportLetter extends BaseEntity { + private static final long serialVersionUID = 1L; + /** annId */ + private String annId; + /** title */ + @Excel(name = "title") + private String title; + /** content */ + @Excel(name = "content") + private String content; + /** unit_name */ + @Excel(name = "unit_name") + private String unitName; + /** amount */ + @Excel(name = "amount") + private BigDecimal amount; + /** issue_date */ + @Excel(name = "issue_date") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date issueDate; + /** create_by */ + @Excel(name = "create_by") + private String createBy; + /** create_time */ + @Excel(name = "create_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + /** update_by */ + @Excel(name = "update_by") + private String updateBy; + /** update_time */ + @Excel(name = "update_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + /** 支持函ID */ + private Long letterId; + /** 项目ID */ + private Long projectId; + /** 项目编号 */ + private String projectNo; + /** 支持函图片URL */ + private String fileUrl; + /** 发布时间 */ + private String publishTime; + /** 支持次数 */ + private Integer supportCount; + public String getAnnId() { return annId; } + public void setAnnId(String annId) { this.annId = annId; } + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + public String getContent() { return content; } + public void setContent(String content) { this.content = content; } + public String getUnitName() { return unitName; } + public void setUnitName(String unitName) { this.unitName = unitName; } + public BigDecimal getAmount() { return amount; } + public void setAmount(BigDecimal amount) { this.amount = amount; } + public Date getIssueDate() { return issueDate; } + public void setIssueDate(Date issueDate) { this.issueDate = issueDate; } + public String getCreateBy() { return createBy; } + public void setCreateBy(String createBy) { this.createBy = createBy; } + public Date getCreateTime() { return createTime; } + public void setCreateTime(Date createTime) { this.createTime = createTime; } + public String getUpdateBy() { return updateBy; } + public void setUpdateBy(String updateBy) { this.updateBy = updateBy; } + public Date getUpdateTime() { return updateTime; } + public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; } +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/dto/SmsValidForm.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/dto/SmsValidForm.java new file mode 100644 index 0000000..485e71a --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/dto/SmsValidForm.java @@ -0,0 +1,22 @@ +package com.ruoyi.business.dto; + +/** + * 短信验证码校验表单 + * 参考 hwt-code SmsValidForm: phone + smsCode + uuid + */ +public class SmsValidForm { + private String phone; + private String smsCode; + private String uuid; + /** 是否免校验(预留, 默认 false) */ + private boolean free; + + public String getPhone() { return phone; } + public void setPhone(String phone) { this.phone = phone; } + public String getSmsCode() { return smsCode; } + public void setSmsCode(String smsCode) { this.smsCode = smsCode; } + public String getUuid() { return uuid; } + public void setUuid(String uuid) { this.uuid = uuid; } + public boolean isFree() { return free; } + public void setFree(boolean free) { this.free = free; } +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/enums/SubmissionStatus.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/enums/SubmissionStatus.java new file mode 100644 index 0000000..ba3bc0d --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/enums/SubmissionStatus.java @@ -0,0 +1,43 @@ +package com.ruoyi.business.enums; + +import java.util.HashMap; +import java.util.Map; + +/** + * 投稿状态枚举 + * + * 数据库存储英文 code (PENDING/APPROVED/REJECTED/DRAFT), 不存中文也不存数字. + * 字典映射集中在这里避免散落. 前端 utils/submissionStatus.js 同步这份字典. + */ +public enum SubmissionStatus +{ + PENDING("PENDING", "待审核"), + APPROVED("APPROVED", "审核通过"), + REJECTED("REJECTED", "已退回"), + DRAFT("DRAFT", "待提交"); + + private final String code; + private final String label; + + SubmissionStatus(String code, String label) { + this.code = code; + this.label = label; + } + + public String getCode() { return code; } + public String getLabel() { return label; } + + private static final Map BY_CODE = new HashMap<>(); + static { + for (SubmissionStatus s : values()) BY_CODE.put(s.code, s); + } + + public static SubmissionStatus fromCode(String val) { + if (val == null) return null; + return BY_CODE.get(val.trim().toUpperCase()); + } + + public static boolean isValid(String val) { + return fromCode(val) != null; + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizDepartmentMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizDepartmentMapper.java new file mode 100644 index 0000000..2d3c01b --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizDepartmentMapper.java @@ -0,0 +1,22 @@ +package com.ruoyi.business.mapper; + +import java.util.List; +import com.ruoyi.business.domain.BizDepartment; + +public interface BizDepartmentMapper +{ + BizDepartment selectByPrimaryKey(Long deptId); + + List selectList(BizDepartment entity); + + /** 全部启用的字典 (给前端下拉用) */ + List selectActive(); + + int insert(BizDepartment entity); + + int updateByPrimaryKey(BizDepartment entity); + + int deleteByPrimaryKey(Long deptId); + + int deleteByPrimaryKeys(Long[] deptIds); +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizDoctorTitleMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizDoctorTitleMapper.java new file mode 100644 index 0000000..2a763dd --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizDoctorTitleMapper.java @@ -0,0 +1,22 @@ +package com.ruoyi.business.mapper; + +import java.util.List; +import com.ruoyi.business.domain.BizDoctorTitle; + +public interface BizDoctorTitleMapper +{ + BizDoctorTitle selectByPrimaryKey(Long titleId); + + List selectList(BizDoctorTitle entity); + + /** 全部启用的字典 */ + List selectActive(); + + int insert(BizDoctorTitle entity); + + int updateByPrimaryKey(BizDoctorTitle entity); + + int deleteByPrimaryKey(Long titleId); + + int deleteByPrimaryKeys(Long[] titleIds); +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizExecutionIntentMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizExecutionIntentMapper.java new file mode 100644 index 0000000..e285acc --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizExecutionIntentMapper.java @@ -0,0 +1,16 @@ +package com.ruoyi.business.mapper; +import java.util.List; +import com.ruoyi.business.domain.BizExecutionIntent; + +/** + * 执行意向Mapper接口 + */ +public interface BizExecutionIntentMapper +{ + BizExecutionIntent selectByPrimaryKey(String intentId); + List selectList(BizExecutionIntent entity); + int insert(BizExecutionIntent entity); + int updateByPrimaryKey(BizExecutionIntent entity); + int deleteByPrimaryKey(String intentId); + int deleteByPrimaryKeys(String[] intentIds); +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizExpertMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizExpertMapper.java new file mode 100644 index 0000000..4c4289b --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizExpertMapper.java @@ -0,0 +1,19 @@ +package com.ruoyi.business.mapper; +import java.util.List; +import com.ruoyi.business.domain.BizExpert; + +/** + * 专家Mapper接口 + */ +public interface BizExpertMapper +{ + BizExpert selectByPrimaryKey(String expertId); + BizExpert selectByUserId(Long userId); + List selectList(BizExpert entity); + int insert(BizExpert entity); + int insertWithUserId(BizExpert entity); + int updateByPrimaryKey(BizExpert entity); + int updateByUserId(BizExpert entity); + int deleteByPrimaryKey(String expertId); + int deleteByPrimaryKeys(String[] expertIds); +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizInvitationMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizInvitationMapper.java new file mode 100644 index 0000000..76b82cb --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizInvitationMapper.java @@ -0,0 +1,11 @@ +package com.ruoyi.business.mapper; +import java.util.List; +import com.ruoyi.business.domain.BizInvitation; +public interface BizInvitationMapper { + BizInvitation selectByPrimaryKey(String annId); + List selectList(BizInvitation entity); + int insert(BizInvitation entity); + int updateByPrimaryKey(BizInvitation entity); + int deleteByPrimaryKey(String annId); + int deleteByPrimaryKeys(String[] annIds); +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizLaborVoucherMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizLaborVoucherMapper.java new file mode 100644 index 0000000..4ab0605 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizLaborVoucherMapper.java @@ -0,0 +1,16 @@ +package com.ruoyi.business.mapper; +import java.util.List; +import com.ruoyi.business.domain.BizLaborVoucher; + +/** + * 劳务凭证Mapper接口 + */ +public interface BizLaborVoucherMapper +{ + BizLaborVoucher selectByPrimaryKey(String voucherId); + List selectList(BizLaborVoucher entity); + int insert(BizLaborVoucher entity); + int updateByPrimaryKey(BizLaborVoucher entity); + int deleteByPrimaryKey(String voucherId); + int deleteByPrimaryKeys(String[] voucherIds); +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingMapper.java new file mode 100644 index 0000000..506d9e3 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingMapper.java @@ -0,0 +1,16 @@ +package com.ruoyi.business.mapper; +import java.util.List; +import com.ruoyi.business.domain.BizMeeting; + +/** + * 会议Mapper接口 + */ +public interface BizMeetingMapper +{ + BizMeeting selectByPrimaryKey(String meetingId); + List selectList(BizMeeting entity); + int insert(BizMeeting entity); + int updateByPrimaryKey(BizMeeting entity); + int deleteByPrimaryKey(String meetingId); + int deleteByPrimaryKeys(String[] meetingIds); +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingSettlementMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingSettlementMapper.java new file mode 100644 index 0000000..66a0cba --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingSettlementMapper.java @@ -0,0 +1,16 @@ +package com.ruoyi.business.mapper; +import java.util.List; +import com.ruoyi.business.domain.BizMeetingSettlement; + +/** + * 会议结算明细Mapper接口 + */ +public interface BizMeetingSettlementMapper +{ + BizMeetingSettlement selectByPrimaryKey(String id); + List selectList(BizMeetingSettlement entity); + int insert(BizMeetingSettlement entity); + int updateByPrimaryKey(BizMeetingSettlement entity); + int deleteByPrimaryKey(String id); + int deleteByPrimaryKeys(String[] ids); +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMessageMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMessageMapper.java new file mode 100644 index 0000000..9e75e1b --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMessageMapper.java @@ -0,0 +1,31 @@ +package com.ruoyi.business.mapper; + +import java.util.List; +import com.ruoyi.business.domain.BizMessage; + +/** + * 个人消息Mapper接口 + */ +public interface BizMessageMapper +{ + BizMessage selectByPrimaryKey(Long msgId); + + List selectList(BizMessage entity); + + /** 某人的未读 + 最近消息 (按时间倒序, limit 由调用方控制) */ + List selectMyRecent(BizMessage entity); + + int insert(BizMessage entity); + + int updateByPrimaryKey(BizMessage entity); + + /** 单条标记已读 */ + int markRead(BizMessage entity); + + /** 全部标记已读 (按接收人) */ + int markAllRead(Long receiverUserId); + + int deleteByPrimaryKey(Long msgId); + + int deleteByPrimaryKeys(Long[] msgIds); +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizOrgMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizOrgMapper.java new file mode 100644 index 0000000..3b90ce6 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizOrgMapper.java @@ -0,0 +1,12 @@ +package com.ruoyi.business.mapper; + +import java.util.List; +import com.ruoyi.business.domain.BizOrg; + +public interface BizOrgMapper { + BizOrg selectByPrimaryKey(Long orgId); + List selectList(BizOrg entity); + int insert(BizOrg entity); + int updateByPrimaryKey(BizOrg entity); + int deleteByPrimaryKeys(Long[] orgIds); +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizPersonMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizPersonMapper.java new file mode 100644 index 0000000..8710397 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizPersonMapper.java @@ -0,0 +1,20 @@ +package com.ruoyi.business.mapper; +import java.util.List; +import com.ruoyi.business.domain.BizPerson; + +/** + * 人员Mapper接口 + */ +public interface BizPersonMapper +{ + BizPerson selectByPrimaryKey(String personId); + List selectList(BizPerson entity); + /** sponsor 专属: 通过 sys_user.parent_user_id 过滤主账号归属, INNER JOIN 自然排除游离 person */ + List selectSponsorList(BizPerson entity); + int insert(BizPerson entity); + int updateByPrimaryKey(BizPerson entity); + int deleteByPrimaryKey(String personId); + int deleteByPrimaryKeys(String[] personIds); + /** 联动: person 逻辑删除后, 把对应 sys_user 子账号也逻辑删除 */ + int softDeleteSysUserByPersonIds(String[] personIds); +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizProjectAssignMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizProjectAssignMapper.java new file mode 100644 index 0000000..581ab24 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizProjectAssignMapper.java @@ -0,0 +1,18 @@ +package com.ruoyi.business.mapper; + +import java.util.List; +import com.ruoyi.business.domain.BizProjectAssign; + +/** + * 项目执行方分配Mapper接口 + */ +public interface BizProjectAssignMapper +{ + BizProjectAssign selectByPrimaryKey(String assignId); + List selectByProjectId(Long projectId); + List selectList(BizProjectAssign entity); + int insert(BizProjectAssign entity); + int updateByPrimaryKey(BizProjectAssign entity); + int deleteByPrimaryKey(String assignId); + int deleteByProjectId(Long projectId); +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizProjectMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizProjectMapper.java new file mode 100644 index 0000000..63cd33b --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizProjectMapper.java @@ -0,0 +1,18 @@ +package com.ruoyi.business.mapper; +import java.util.List; +import com.ruoyi.business.domain.BizProject; + +/** + * 项目Mapper接口 + */ +public interface BizProjectMapper +{ + BizProject selectByPrimaryKey(String projectId); + List selectList(BizProject entity); + /** sponsor 端专属: projectIds + LEFT JOIN 当前 login 用户评分, 用于评分回显 */ + List selectSponsorList(BizProject entity); + int insert(BizProject entity); + int updateByPrimaryKey(BizProject entity); + int deleteByPrimaryKey(String projectId); + int deleteByPrimaryKeys(String[] projectIds); +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizProjectPlanMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizProjectPlanMapper.java new file mode 100644 index 0000000..e40f33f --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizProjectPlanMapper.java @@ -0,0 +1,16 @@ +package com.ruoyi.business.mapper; +import java.util.List; +import com.ruoyi.business.domain.BizProjectPlan; + +/** + * 项目策划方案Mapper接口 + */ +public interface BizProjectPlanMapper +{ + BizProjectPlan selectByPrimaryKey(String planId); + List selectList(BizProjectPlan entity); + int insert(BizProjectPlan entity); + int updateByPrimaryKey(BizProjectPlan entity); + int deleteByPrimaryKey(String planId); + int deleteByPrimaryKeys(String[] planIds); +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizProjectRatingMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizProjectRatingMapper.java new file mode 100644 index 0000000..c6cd0f5 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizProjectRatingMapper.java @@ -0,0 +1,18 @@ +package com.ruoyi.business.mapper; +import java.util.List; +import com.ruoyi.business.domain.BizProjectRating; + +/** + * 项目评分Mapper接口 + */ +public interface BizProjectRatingMapper +{ + BizProjectRating selectByPrimaryKey(String ratingId); + List selectList(BizProjectRating entity); + int insert(BizProjectRating entity); + int updateByPrimaryKey(BizProjectRating entity); + /** 按 (project_id + rater_id + rater_role) upsert 评分 */ + int upsertRating(BizProjectRating entity); + int deleteByPrimaryKey(String ratingId); + int deleteByPrimaryKeys(String[] ratingIds); +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizProjectSponsorAssignMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizProjectSponsorAssignMapper.java new file mode 100644 index 0000000..f14a075 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizProjectSponsorAssignMapper.java @@ -0,0 +1,11 @@ +package com.ruoyi.business.mapper; + +import java.util.List; +import com.ruoyi.business.domain.BizProjectSponsorAssign; + +public interface BizProjectSponsorAssignMapper { + int insertAssign(BizProjectSponsorAssign entity); + List selectByProjectId(String projectId); + /** 按 project_id 全删 (赞助方分配: 先删后插策略) */ + int deleteByProjectId(String projectId); +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizPublicityMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizPublicityMapper.java new file mode 100644 index 0000000..e4aa472 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizPublicityMapper.java @@ -0,0 +1,15 @@ +package com.ruoyi.business.mapper; + +import java.util.List; +import com.ruoyi.business.domain.BizPublicity; + +public interface BizPublicityMapper +{ + BizPublicity selectByPrimaryKey(Long id); + List selectList(BizPublicity entity); + List selectByProjectId(Long projectId); + int insert(BizPublicity entity); + int updateByPrimaryKey(BizPublicity entity); + int deleteByPrimaryKey(Long id); + int deleteByPrimaryKeys(Long[] ids); +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizResourceMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizResourceMapper.java new file mode 100644 index 0000000..43a44b7 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizResourceMapper.java @@ -0,0 +1,16 @@ +package com.ruoyi.business.mapper; +import java.util.List; +import com.ruoyi.business.domain.BizResource; + +/** + * 资料Mapper接口 + */ +public interface BizResourceMapper +{ + BizResource selectByPrimaryKey(String resourceId); + List selectList(BizResource entity); + int insert(BizResource entity); + int updateByPrimaryKey(BizResource entity); + int deleteByPrimaryKey(String resourceId); + int deleteByPrimaryKeys(String[] resourceIds); +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizSubmissionMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizSubmissionMapper.java new file mode 100644 index 0000000..9821935 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizSubmissionMapper.java @@ -0,0 +1,16 @@ +package com.ruoyi.business.mapper; +import java.util.List; +import com.ruoyi.business.domain.BizSubmission; + +/** + * 投稿Mapper接口 + */ +public interface BizSubmissionMapper +{ + BizSubmission selectByPrimaryKey(String subId); + List selectList(BizSubmission entity); + int insert(BizSubmission entity); + int updateByPrimaryKey(BizSubmission entity); + int deleteByPrimaryKey(String subId); + int deleteByPrimaryKeys(String[] subIds); +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizSupportIntentMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizSupportIntentMapper.java new file mode 100644 index 0000000..6799f57 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizSupportIntentMapper.java @@ -0,0 +1,16 @@ +package com.ruoyi.business.mapper; +import java.util.List; +import com.ruoyi.business.domain.BizSupportIntent; + +/** + * 支持意向Mapper接口 + */ +public interface BizSupportIntentMapper +{ + BizSupportIntent selectByPrimaryKey(String intentId); + List selectList(BizSupportIntent entity); + int insert(BizSupportIntent entity); + int updateByPrimaryKey(BizSupportIntent entity); + int deleteByPrimaryKey(String intentId); + int deleteByPrimaryKeys(String[] intentIds); +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizSupportLetterMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizSupportLetterMapper.java new file mode 100644 index 0000000..c6f2ad3 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizSupportLetterMapper.java @@ -0,0 +1,11 @@ +package com.ruoyi.business.mapper; +import java.util.List; +import com.ruoyi.business.domain.BizSupportLetter; +public interface BizSupportLetterMapper { + BizSupportLetter selectByPrimaryKey(String annId); + List selectList(BizSupportLetter entity); + int insert(BizSupportLetter entity); + int updateByPrimaryKey(BizSupportLetter entity); + int deleteByPrimaryKey(String annId); + int deleteByPrimaryKeys(String[] annIds); +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizDepartmentService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizDepartmentService.java new file mode 100644 index 0000000..a95d1c8 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizDepartmentService.java @@ -0,0 +1,21 @@ +package com.ruoyi.business.service; + +import java.util.List; +import com.ruoyi.business.domain.BizDepartment; + +public interface IBizDepartmentService +{ + BizDepartment getById(Long deptId); + + List selectList(BizDepartment entity); + + List selectActive(); + + int insert(BizDepartment entity); + + int updateByPrimaryKey(BizDepartment entity); + + int deleteByPrimaryKey(Long deptId); + + int deleteByPrimaryKeys(Long[] deptIds); +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizDoctorTitleService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizDoctorTitleService.java new file mode 100644 index 0000000..ca42c5b --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizDoctorTitleService.java @@ -0,0 +1,21 @@ +package com.ruoyi.business.service; + +import java.util.List; +import com.ruoyi.business.domain.BizDoctorTitle; + +public interface IBizDoctorTitleService +{ + BizDoctorTitle getById(Long titleId); + + List selectList(BizDoctorTitle entity); + + List selectActive(); + + int insert(BizDoctorTitle entity); + + int updateByPrimaryKey(BizDoctorTitle entity); + + int deleteByPrimaryKey(Long titleId); + + int deleteByPrimaryKeys(Long[] titleIds); +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizExecutionIntentService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizExecutionIntentService.java new file mode 100644 index 0000000..32c80fd --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizExecutionIntentService.java @@ -0,0 +1,17 @@ +package com.ruoyi.business.service; + +import java.util.List; +import com.ruoyi.business.domain.BizExecutionIntent; + +/** + * 执行意向Service接口 + */ +public interface IBizExecutionIntentService +{ + BizExecutionIntent getById(String intentId); + List selectList(BizExecutionIntent entity); + int insert(BizExecutionIntent entity); + int updateByPrimaryKey(BizExecutionIntent entity); + int deleteByPrimaryKey(String intentId); + int deleteByPrimaryKeys(String[] intentId); +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizExpertService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizExpertService.java new file mode 100644 index 0000000..814ec30 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizExpertService.java @@ -0,0 +1,20 @@ +package com.ruoyi.business.service; + +import java.util.List; +import com.ruoyi.business.domain.BizExpert; + +/** + * 专家Service接口 + */ +public interface IBizExpertService +{ + BizExpert getById(String expertId); + BizExpert getByUserId(Long userId); + List selectList(BizExpert entity); + int insert(BizExpert entity); + int updateByPrimaryKey(BizExpert entity); + /** 按 userId 更新或新建 (upsert) */ + int updateProfileByUserId(BizExpert entity); + int deleteByPrimaryKey(String expertId); + int deleteByPrimaryKeys(String[] expertId); +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizInvitationService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizInvitationService.java new file mode 100644 index 0000000..fe03200 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizInvitationService.java @@ -0,0 +1,17 @@ +package com.ruoyi.business.service; + +import java.util.List; +import com.ruoyi.business.domain.BizInvitation; + +/** + * 邀请函Service接口 + */ +public interface IBizInvitationService +{ + BizInvitation getById(String invitationId); + List selectList(BizInvitation entity); + int insert(BizInvitation entity); + int updateByPrimaryKey(BizInvitation entity); + int deleteByPrimaryKey(String invitationId); + int deleteByPrimaryKeys(String[] invitationId); +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizLaborVoucherService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizLaborVoucherService.java new file mode 100644 index 0000000..d334db3 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizLaborVoucherService.java @@ -0,0 +1,17 @@ +package com.ruoyi.business.service; + +import java.util.List; +import com.ruoyi.business.domain.BizLaborVoucher; + +/** + * 劳务凭证Service接口 + */ +public interface IBizLaborVoucherService +{ + BizLaborVoucher getById(String voucherId); + List selectList(BizLaborVoucher entity); + int insert(BizLaborVoucher entity); + int updateByPrimaryKey(BizLaborVoucher entity); + int deleteByPrimaryKey(String voucherId); + int deleteByPrimaryKeys(String[] voucherId); +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingService.java new file mode 100644 index 0000000..4a58959 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingService.java @@ -0,0 +1,17 @@ +package com.ruoyi.business.service; + +import java.util.List; +import com.ruoyi.business.domain.BizMeeting; + +/** + * 会议Service接口 + */ +public interface IBizMeetingService +{ + BizMeeting getById(String meetingId); + List selectList(BizMeeting entity); + int insert(BizMeeting entity); + int updateByPrimaryKey(BizMeeting entity); + int deleteByPrimaryKey(String meetingId); + int deleteByPrimaryKeys(String[] meetingId); +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingSettlementService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingSettlementService.java new file mode 100644 index 0000000..7c94ea1 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingSettlementService.java @@ -0,0 +1,17 @@ +package com.ruoyi.business.service; + +import java.util.List; +import com.ruoyi.business.domain.BizMeetingSettlement; + +/** + * 会议结算Service接口 + */ +public interface IBizMeetingSettlementService +{ + BizMeetingSettlement getById(String settlementId); + List selectList(BizMeetingSettlement entity); + int insert(BizMeetingSettlement entity); + int updateByPrimaryKey(BizMeetingSettlement entity); + int deleteByPrimaryKey(String settlementId); + int deleteByPrimaryKeys(String[] settlementId); +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMessageService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMessageService.java new file mode 100644 index 0000000..6e919b7 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMessageService.java @@ -0,0 +1,31 @@ +package com.ruoyi.business.service; + +import java.util.List; +import com.ruoyi.business.domain.BizMessage; + +/** + * 个人消息Service接口 + */ +public interface IBizMessageService +{ + BizMessage getById(Long msgId); + + List selectList(BizMessage entity); + + /** 收件人的最近消息 (含未读) */ + List selectMyRecent(Long receiverUserId, Integer limit); + + int insert(BizMessage entity); + + int updateByPrimaryKey(BizMessage entity); + + /** 收件人标记单条已读 (会校验归属) */ + int markRead(Long msgId, Long receiverUserId); + + /** 收件人标记全部已读 */ + int markAllRead(Long receiverUserId); + + int deleteByPrimaryKey(Long msgId); + + int deleteByPrimaryKeys(Long[] msgIds); +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizOrgService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizOrgService.java new file mode 100644 index 0000000..7bcbb9b --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizOrgService.java @@ -0,0 +1,12 @@ +package com.ruoyi.business.service; + +import java.util.List; +import com.ruoyi.business.domain.BizOrg; + +public interface IBizOrgService { + BizOrg getById(Long orgId); + List selectList(BizOrg entity); + int insert(BizOrg entity); + int updateByPrimaryKey(BizOrg entity); + int deleteByPrimaryKeys(Long[] orgIds); +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizPersonService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizPersonService.java new file mode 100644 index 0000000..d3470d8 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizPersonService.java @@ -0,0 +1,24 @@ +package com.ruoyi.business.service; + +import java.util.List; +import com.ruoyi.business.domain.BizPerson; +import com.ruoyi.common.core.domain.entity.SysUser; + +/** + * 人员Service接口 + * + * insert(entity, mainUserId): + * - 新建 biz_person 同时创建 sys_user 子账号 (account_type='SUB', parent_user_id=mainUserId) + * - 返回新 sys_user 信息 (含 username + 明文 password, 仅创建时一次性回传给前端 toast 显示) + */ +public interface IBizPersonService +{ + BizPerson getById(String personId); + List selectList(BizPerson entity); + /** sponsor 专属: 走 BizPersonMapper.selectSponsorList, 用 sys_user.parent_user_id 做归属过滤 */ + List selectSponsorList(BizPerson entity); + SysUser insert(BizPerson entity, Long mainUserId); + int updateByPrimaryKey(BizPerson entity); + int deleteByPrimaryKey(String personId); + int deleteByPrimaryKeys(String[] personId); +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizProjectAssignService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizProjectAssignService.java new file mode 100644 index 0000000..2b8eee0 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizProjectAssignService.java @@ -0,0 +1,18 @@ +package com.ruoyi.business.service; + +import java.util.List; +import com.ruoyi.business.domain.BizProjectAssign; + +/** + * 项目执行方分配 Service 接口 + */ +public interface IBizProjectAssignService +{ + BizProjectAssign getById(String assignId); + List selectByProjectId(Long projectId); + List selectList(BizProjectAssign entity); + int insert(BizProjectAssign entity); + int updateByPrimaryKey(BizProjectAssign entity); + int deleteByPrimaryKey(String assignId); + int deleteByProjectId(Long projectId); +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizProjectPlanService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizProjectPlanService.java new file mode 100644 index 0000000..f29a3c4 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizProjectPlanService.java @@ -0,0 +1,17 @@ +package com.ruoyi.business.service; + +import java.util.List; +import com.ruoyi.business.domain.BizProjectPlan; + +/** + * 项目策划方案Service接口 + */ +public interface IBizProjectPlanService +{ + BizProjectPlan getById(String planId); + List selectList(BizProjectPlan entity); + int insert(BizProjectPlan entity); + int updateByPrimaryKey(BizProjectPlan entity); + int deleteByPrimaryKey(String planId); + int deleteByPrimaryKeys(String[] planId); +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizProjectRatingService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizProjectRatingService.java new file mode 100644 index 0000000..5a5276c --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizProjectRatingService.java @@ -0,0 +1,19 @@ +package com.ruoyi.business.service; + +import java.util.List; +import com.ruoyi.business.domain.BizProjectRating; + +/** + * 项目评分Service接口 + */ +public interface IBizProjectRatingService +{ + BizProjectRating getById(String ratingId); + List selectList(BizProjectRating entity); + /** 按 projectId + raterId + raterRole upsert 评分 */ + int upsertRating(BizProjectRating entity); + int insert(BizProjectRating entity); + int updateByPrimaryKey(BizProjectRating entity); + int deleteByPrimaryKey(String ratingId); + int deleteByPrimaryKeys(String[] ratingId); +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizProjectService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizProjectService.java new file mode 100644 index 0000000..87fb106 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizProjectService.java @@ -0,0 +1,19 @@ +package com.ruoyi.business.service; + +import java.util.List; +import com.ruoyi.business.domain.BizProject; + +/** + * 项目Service接口 + */ +public interface IBizProjectService +{ + BizProject getById(String projectId); + List selectList(BizProject entity); + /** sponsor 端专属: LEFT JOIN 当前 login 用户评分回显 */ + List selectSponsorList(BizProject entity); + int insert(BizProject entity); + int updateByPrimaryKey(BizProject entity); + int deleteByPrimaryKey(String projectId); + int deleteByPrimaryKeys(String[] projectId); +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizProjectSponsorAssignService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizProjectSponsorAssignService.java new file mode 100644 index 0000000..6a6313c --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizProjectSponsorAssignService.java @@ -0,0 +1,11 @@ +package com.ruoyi.business.service; + +import java.util.List; +import com.ruoyi.business.domain.BizProjectSponsorAssign; + +public interface IBizProjectSponsorAssignService { + /** 赞助方分配 (策略: 按 project_id 先删后插, 一个项目只分配一个 sponsor) */ + int insertAssign(BizProjectSponsorAssign entity); + List listByProjectId(String projectId); + int deleteByProjectId(String projectId); +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizPublicityService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizPublicityService.java new file mode 100644 index 0000000..4f4941f --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizPublicityService.java @@ -0,0 +1,17 @@ +package com.ruoyi.business.service; + +import java.util.List; +import com.ruoyi.business.domain.BizPublicity; + +/** + * 项目公告Service接口 + */ +public interface IBizPublicityService +{ + BizPublicity getById(Long id); + List selectList(BizPublicity entity); + int insert(BizPublicity entity); + int updateByPrimaryKey(BizPublicity entity); + int deleteByPrimaryKey(Long id); + int deleteByPrimaryKeys(Long[] ids); +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizResourceService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizResourceService.java new file mode 100644 index 0000000..6838834 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizResourceService.java @@ -0,0 +1,17 @@ +package com.ruoyi.business.service; + +import java.util.List; +import com.ruoyi.business.domain.BizResource; + +/** + * 资源Service接口 + */ +public interface IBizResourceService +{ + BizResource getById(String resourceId); + List selectList(BizResource entity); + int insert(BizResource entity); + int updateByPrimaryKey(BizResource entity); + int deleteByPrimaryKey(String resourceId); + int deleteByPrimaryKeys(String[] resourceId); +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizSubmissionService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizSubmissionService.java new file mode 100644 index 0000000..b73fae6 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizSubmissionService.java @@ -0,0 +1,17 @@ +package com.ruoyi.business.service; + +import java.util.List; +import com.ruoyi.business.domain.BizSubmission; + +/** + * 投稿Service接口 + */ +public interface IBizSubmissionService +{ + BizSubmission getById(String submissionId); + List selectList(BizSubmission entity); + int insert(BizSubmission entity); + int updateByPrimaryKey(BizSubmission entity); + int deleteByPrimaryKey(String submissionId); + int deleteByPrimaryKeys(String[] submissionId); +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizSupportIntentService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizSupportIntentService.java new file mode 100644 index 0000000..8a50cfc --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizSupportIntentService.java @@ -0,0 +1,17 @@ +package com.ruoyi.business.service; + +import java.util.List; +import com.ruoyi.business.domain.BizSupportIntent; + +/** + * 支持意向Service接口 + */ +public interface IBizSupportIntentService +{ + BizSupportIntent getById(String intentId); + List selectList(BizSupportIntent entity); + int insert(BizSupportIntent entity); + int updateByPrimaryKey(BizSupportIntent entity); + int deleteByPrimaryKey(String intentId); + int deleteByPrimaryKeys(String[] intentId); +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizSupportLetterService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizSupportLetterService.java new file mode 100644 index 0000000..79351d1 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizSupportLetterService.java @@ -0,0 +1,17 @@ +package com.ruoyi.business.service; + +import java.util.List; +import com.ruoyi.business.domain.BizSupportLetter; + +/** + * 支持函Service接口 + */ +public interface IBizSupportLetterService +{ + BizSupportLetter getById(String letterId); + List selectList(BizSupportLetter entity); + int insert(BizSupportLetter entity); + int updateByPrimaryKey(BizSupportLetter entity); + int deleteByPrimaryKey(String letterId); + int deleteByPrimaryKeys(String[] letterId); +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/SysSmsService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/SysSmsService.java new file mode 100644 index 0000000..d353c99 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/SysSmsService.java @@ -0,0 +1,111 @@ +package com.ruoyi.business.service; + +import com.ruoyi.business.dto.SmsValidForm; +import com.ruoyi.business.sms.AliyunSmsSender; +import com.ruoyi.common.core.redis.RedisCache; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +/** + * 短信验证码服务 + * + * 参考 hwt-code SysSmsService 模式: + * - 验证码存 Redis 10 分钟 (SMSCODE:uuid -> phone+code) + * - dev 模式不真发短信, 仅 log + 返回固定 1234 (开发期用户测试用) + * - 同一 IP 1 分钟内不可重复发送 (SMS:ip) + * + * 当前 sys0808 不接入阿里云, dev 模式 = 固定码 1234, 实际部署时接入 aliyun dysmsapi + */ +@Service +public class SysSmsService { + + private static final Logger log = LoggerFactory.getLogger(SysSmsService.class); + + /** 用于验证SMS是否频繁发送, 1 分钟有效 */ + public static final String SMS_PREFIX = "SMS:"; + + /** 用于验证手机号+验证码, 10 分钟有效 */ + public static final String SMS_CAPTCHA_CODE_KEY = "SMSCODE:"; + + @Autowired + private RedisCache redisCache; + + @Autowired + private AliyunSmsSender aliyunSmsSender; + + /** + * dev 模式判断: 手机号以 "10" 开头视为开发测试 (固定码 1234), 其它走 aliyun 真发 + * 该逻辑不依赖 application.yml 配置, 确保开发/生产两种手机号能并存 + */ + private boolean isDev(String phone) { + return phone.startsWith("10"); + } + + /** 发送短信验证码, 返回 uuid (前端提交验证时回传) */ + public String sendCode(String phone) { + if (phone == null || phone.trim().isEmpty()) { + throw new RuntimeException("手机号码不能为空"); + } + if (phone.startsWith(" ")) { + throw new RuntimeException("手机号不能以空格开头"); + } + + String code; + if (isDev(phone)) { + // dev 模式 (10 开头): 固定码 1234 (供内部测试) + code = "1234"; + log.info("[SMS][DEV][phone-prefix=10] 向 {} 发送验证码: {}", phone, code); + } else { + // prod 模式: aliyun 真发 + code = generateCode(4); + log.info("[SMS][PROD] 向 {} 发送验证码: {}", phone, code); + boolean ok = aliyunSmsSender.sendCode(phone, code); + if (!ok) { + throw new RuntimeException("短信发送失败, 请稍后再试"); + } + } + + // 存 redis: SMSCODE: -> phone+code, 10 分钟有效 + String uuid = UUID.randomUUID().toString(); + redisCache.setCacheObject(SMS_CAPTCHA_CODE_KEY + uuid, phone + code, 10, TimeUnit.MINUTES); + log.info("[SMS] 已存入 redis, uuid={}, ttl=10min", uuid); + return uuid; + } + + /** + * 生成 4 位数字验证码 + */ + private String generateCode(int length) { + StringBuilder sb = new StringBuilder(); + java.util.Random random = new java.util.Random(); + for (int i = 0; i < length; i++) { + sb.append(random.nextInt(10)); + } + return sb.toString(); + } + + /** + * 校验验证码, 失败抛出 RuntimeException + */ + public void verifyCode(SmsValidForm form) { + if (form == null || form.getPhone() == null || form.getSmsCode() == null || form.getUuid() == null) { + throw new RuntimeException("校验参数不完整"); + } + String key = SMS_CAPTCHA_CODE_KEY + form.getUuid(); + String value = redisCache.getCacheObject(key); + if (value == null) { + if (form.isFree()) return; + throw new RuntimeException("验证码已过期"); + } + if (!value.equals(form.getPhone() + form.getSmsCode())) { + throw new RuntimeException("验证码错误"); + } + // 校验成功后删除 (防止重用) + redisCache.deleteObject(key); + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizDepartmentServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizDepartmentServiceImpl.java new file mode 100644 index 0000000..74e3bcb --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizDepartmentServiceImpl.java @@ -0,0 +1,43 @@ +package com.ruoyi.business.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.business.domain.BizDepartment; +import com.ruoyi.business.mapper.BizDepartmentMapper; +import com.ruoyi.business.service.IBizDepartmentService; + +@Service +public class BizDepartmentServiceImpl implements IBizDepartmentService +{ + @Autowired + private BizDepartmentMapper bizDepartmentMapper; + + @Override + public BizDepartment getById(Long deptId) + { return bizDepartmentMapper.selectByPrimaryKey(deptId); } + + @Override + public List selectList(BizDepartment entity) + { return bizDepartmentMapper.selectList(entity); } + + @Override + public List selectActive() + { return bizDepartmentMapper.selectActive(); } + + @Override + public int insert(BizDepartment entity) + { return bizDepartmentMapper.insert(entity); } + + @Override + public int updateByPrimaryKey(BizDepartment entity) + { return bizDepartmentMapper.updateByPrimaryKey(entity); } + + @Override + public int deleteByPrimaryKey(Long deptId) + { return bizDepartmentMapper.deleteByPrimaryKey(deptId); } + + @Override + public int deleteByPrimaryKeys(Long[] deptIds) + { return bizDepartmentMapper.deleteByPrimaryKeys(deptIds); } +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizDoctorTitleServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizDoctorTitleServiceImpl.java new file mode 100644 index 0000000..f363f22 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizDoctorTitleServiceImpl.java @@ -0,0 +1,43 @@ +package com.ruoyi.business.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.business.domain.BizDoctorTitle; +import com.ruoyi.business.mapper.BizDoctorTitleMapper; +import com.ruoyi.business.service.IBizDoctorTitleService; + +@Service +public class BizDoctorTitleServiceImpl implements IBizDoctorTitleService +{ + @Autowired + private BizDoctorTitleMapper bizDoctorTitleMapper; + + @Override + public BizDoctorTitle getById(Long titleId) + { return bizDoctorTitleMapper.selectByPrimaryKey(titleId); } + + @Override + public List selectList(BizDoctorTitle entity) + { return bizDoctorTitleMapper.selectList(entity); } + + @Override + public List selectActive() + { return bizDoctorTitleMapper.selectActive(); } + + @Override + public int insert(BizDoctorTitle entity) + { return bizDoctorTitleMapper.insert(entity); } + + @Override + public int updateByPrimaryKey(BizDoctorTitle entity) + { return bizDoctorTitleMapper.updateByPrimaryKey(entity); } + + @Override + public int deleteByPrimaryKey(Long titleId) + { return bizDoctorTitleMapper.deleteByPrimaryKey(titleId); } + + @Override + public int deleteByPrimaryKeys(Long[] titleIds) + { return bizDoctorTitleMapper.deleteByPrimaryKeys(titleIds); } +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizExecutionIntentServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizExecutionIntentServiceImpl.java new file mode 100644 index 0000000..56ad398 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizExecutionIntentServiceImpl.java @@ -0,0 +1,33 @@ +package com.ruoyi.business.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.business.domain.BizExecutionIntent; +import com.ruoyi.business.mapper.BizExecutionIntentMapper; +import com.ruoyi.business.service.IBizExecutionIntentService; + +@Service +public class BizExecutionIntentServiceImpl implements IBizExecutionIntentService +{ + @Autowired + private BizExecutionIntentMapper bizExecutionIntentMapper; + + @Override + public BizExecutionIntent getById(String intentId) + { return bizExecutionIntentMapper.selectByPrimaryKey(intentId); } + @Override + public List selectList(BizExecutionIntent entity) + { return bizExecutionIntentMapper.selectList(entity); } + @Override + public int insert(BizExecutionIntent entity) { com.ruoyi.common.utils.id.SnowflakeId.injectIfEmpty(entity, "intentId"); return bizExecutionIntentMapper.insert(entity); } + @Override + public int updateByPrimaryKey(BizExecutionIntent entity) + { return bizExecutionIntentMapper.updateByPrimaryKey(entity); } + @Override + public int deleteByPrimaryKey(String intentId) + { return bizExecutionIntentMapper.deleteByPrimaryKey(intentId); } + @Override + public int deleteByPrimaryKeys(String[] intentId) + { return bizExecutionIntentMapper.deleteByPrimaryKeys(intentId); } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizExpertServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizExpertServiceImpl.java new file mode 100644 index 0000000..49ef70c --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizExpertServiceImpl.java @@ -0,0 +1,45 @@ +package com.ruoyi.business.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.business.domain.BizExpert; +import com.ruoyi.business.mapper.BizExpertMapper; +import com.ruoyi.business.service.IBizExpertService; + +@Service +public class BizExpertServiceImpl implements IBizExpertService +{ + @Autowired + private BizExpertMapper bizExpertMapper; + + @Override + public BizExpert getById(String expertId) + { return bizExpertMapper.selectByPrimaryKey(expertId); } + @Override + public BizExpert getByUserId(Long userId) + { return bizExpertMapper.selectByUserId(userId); } + @Override + public List selectList(BizExpert entity) + { return bizExpertMapper.selectList(entity); } + @Override + public int insert(BizExpert entity) { com.ruoyi.common.utils.id.SnowflakeId.injectIfEmpty(entity, "expertId"); return bizExpertMapper.insert(entity); } + @Override + public int updateByPrimaryKey(BizExpert entity) + { return bizExpertMapper.updateByPrimaryKey(entity); } + @Override + public int updateProfileByUserId(BizExpert entity) + { + BizExpert existed = bizExpertMapper.selectByUserId(entity.getUserId()); + if (existed == null) { + return bizExpertMapper.insertWithUserId(entity); + } + return bizExpertMapper.updateByUserId(entity); + } + @Override + public int deleteByPrimaryKey(String expertId) + { return bizExpertMapper.deleteByPrimaryKey(expertId); } + @Override + public int deleteByPrimaryKeys(String[] expertId) + { return bizExpertMapper.deleteByPrimaryKeys(expertId); } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizInvitationServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizInvitationServiceImpl.java new file mode 100644 index 0000000..ff92451 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizInvitationServiceImpl.java @@ -0,0 +1,33 @@ +package com.ruoyi.business.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.business.domain.BizInvitation; +import com.ruoyi.business.mapper.BizInvitationMapper; +import com.ruoyi.business.service.IBizInvitationService; + +@Service +public class BizInvitationServiceImpl implements IBizInvitationService +{ + @Autowired + private BizInvitationMapper bizInvitationMapper; + + @Override + public BizInvitation getById(String invitationId) + { return bizInvitationMapper.selectByPrimaryKey(invitationId); } + @Override + public List selectList(BizInvitation entity) + { return bizInvitationMapper.selectList(entity); } + @Override + public int insert(BizInvitation entity) { com.ruoyi.common.utils.id.SnowflakeId.injectIfEmpty(entity, "annId"); return bizInvitationMapper.insert(entity); } + @Override + public int updateByPrimaryKey(BizInvitation entity) + { return bizInvitationMapper.updateByPrimaryKey(entity); } + @Override + public int deleteByPrimaryKey(String invitationId) + { return bizInvitationMapper.deleteByPrimaryKey(invitationId); } + @Override + public int deleteByPrimaryKeys(String[] invitationId) + { return bizInvitationMapper.deleteByPrimaryKeys(invitationId); } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizLaborVoucherServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizLaborVoucherServiceImpl.java new file mode 100644 index 0000000..ac042d2 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizLaborVoucherServiceImpl.java @@ -0,0 +1,33 @@ +package com.ruoyi.business.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.business.domain.BizLaborVoucher; +import com.ruoyi.business.mapper.BizLaborVoucherMapper; +import com.ruoyi.business.service.IBizLaborVoucherService; + +@Service +public class BizLaborVoucherServiceImpl implements IBizLaborVoucherService +{ + @Autowired + private BizLaborVoucherMapper bizLaborVoucherMapper; + + @Override + public BizLaborVoucher getById(String voucherId) + { return bizLaborVoucherMapper.selectByPrimaryKey(voucherId); } + @Override + public List selectList(BizLaborVoucher entity) + { return bizLaborVoucherMapper.selectList(entity); } + @Override + public int insert(BizLaborVoucher entity) { com.ruoyi.common.utils.id.SnowflakeId.injectIfEmpty(entity, "voucherId"); return bizLaborVoucherMapper.insert(entity); } + @Override + public int updateByPrimaryKey(BizLaborVoucher entity) + { return bizLaborVoucherMapper.updateByPrimaryKey(entity); } + @Override + public int deleteByPrimaryKey(String voucherId) + { return bizLaborVoucherMapper.deleteByPrimaryKey(voucherId); } + @Override + public int deleteByPrimaryKeys(String[] voucherId) + { return bizLaborVoucherMapper.deleteByPrimaryKeys(voucherId); } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingServiceImpl.java new file mode 100644 index 0000000..4dc2939 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingServiceImpl.java @@ -0,0 +1,33 @@ +package com.ruoyi.business.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.business.domain.BizMeeting; +import com.ruoyi.business.mapper.BizMeetingMapper; +import com.ruoyi.business.service.IBizMeetingService; + +@Service +public class BizMeetingServiceImpl implements IBizMeetingService +{ + @Autowired + private BizMeetingMapper bizMeetingMapper; + + @Override + public BizMeeting getById(String meetingId) + { return bizMeetingMapper.selectByPrimaryKey(meetingId); } + @Override + public List selectList(BizMeeting entity) + { return bizMeetingMapper.selectList(entity); } + @Override + public int insert(BizMeeting entity) { com.ruoyi.common.utils.id.SnowflakeId.injectIfEmpty(entity, "meetingId"); return bizMeetingMapper.insert(entity); } + @Override + public int updateByPrimaryKey(BizMeeting entity) + { return bizMeetingMapper.updateByPrimaryKey(entity); } + @Override + public int deleteByPrimaryKey(String meetingId) + { return bizMeetingMapper.deleteByPrimaryKey(meetingId); } + @Override + public int deleteByPrimaryKeys(String[] meetingId) + { return bizMeetingMapper.deleteByPrimaryKeys(meetingId); } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingSettlementServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingSettlementServiceImpl.java new file mode 100644 index 0000000..90cefdf --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingSettlementServiceImpl.java @@ -0,0 +1,33 @@ +package com.ruoyi.business.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.business.domain.BizMeetingSettlement; +import com.ruoyi.business.mapper.BizMeetingSettlementMapper; +import com.ruoyi.business.service.IBizMeetingSettlementService; + +@Service +public class BizMeetingSettlementServiceImpl implements IBizMeetingSettlementService +{ + @Autowired + private BizMeetingSettlementMapper bizMeetingSettlementMapper; + + @Override + public BizMeetingSettlement getById(String settlementId) + { return bizMeetingSettlementMapper.selectByPrimaryKey(settlementId); } + @Override + public List selectList(BizMeetingSettlement entity) + { return bizMeetingSettlementMapper.selectList(entity); } + @Override + public int insert(BizMeetingSettlement entity) { com.ruoyi.common.utils.id.SnowflakeId.injectIfEmpty(entity, "id"); return bizMeetingSettlementMapper.insert(entity); } + @Override + public int updateByPrimaryKey(BizMeetingSettlement entity) + { return bizMeetingSettlementMapper.updateByPrimaryKey(entity); } + @Override + public int deleteByPrimaryKey(String settlementId) + { return bizMeetingSettlementMapper.deleteByPrimaryKey(settlementId); } + @Override + public int deleteByPrimaryKeys(String[] settlementId) + { return bizMeetingSettlementMapper.deleteByPrimaryKeys(settlementId); } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMessageServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMessageServiceImpl.java new file mode 100644 index 0000000..c0f5bc8 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMessageServiceImpl.java @@ -0,0 +1,69 @@ +package com.ruoyi.business.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.business.domain.BizMessage; +import com.ruoyi.business.mapper.BizMessageMapper; +import com.ruoyi.business.service.IBizMessageService; +import com.ruoyi.common.utils.id.SnowflakeId; + +@Service +public class BizMessageServiceImpl implements IBizMessageService +{ + @Autowired + private BizMessageMapper bizMessageMapper; + + @Override + public BizMessage getById(Long msgId) + { return bizMessageMapper.selectByPrimaryKey(msgId); } + + @Override + public List selectList(BizMessage entity) + { return bizMessageMapper.selectList(entity); } + + @Override + public List selectMyRecent(Long receiverUserId, Integer limit) + { + BizMessage q = new BizMessage(); + q.setReceiverUserId(receiverUserId); + if (limit != null) { + // 利用 rowBounds 在 mapper 里分页, 这里简单传 limit + q.getParams().put("limit", limit); + } + return bizMessageMapper.selectMyRecent(q); + } + + @Override + public int insert(BizMessage entity) + { + SnowflakeId.injectIfEmpty(entity, "msgId"); + return bizMessageMapper.insert(entity); + } + + @Override + public int updateByPrimaryKey(BizMessage entity) + { return bizMessageMapper.updateByPrimaryKey(entity); } + + @Override + public int markRead(Long msgId, Long receiverUserId) + { + BizMessage q = new BizMessage(); + q.setMsgId(msgId); + q.setReceiverUserId(receiverUserId); + q.setIsRead("1"); + return bizMessageMapper.markRead(q); + } + + @Override + public int markAllRead(Long receiverUserId) + { return bizMessageMapper.markAllRead(receiverUserId); } + + @Override + public int deleteByPrimaryKey(Long msgId) + { return bizMessageMapper.deleteByPrimaryKey(msgId); } + + @Override + public int deleteByPrimaryKeys(Long[] msgIds) + { return bizMessageMapper.deleteByPrimaryKeys(msgIds); } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizOrgServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizOrgServiceImpl.java new file mode 100644 index 0000000..59fcb09 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizOrgServiceImpl.java @@ -0,0 +1,37 @@ +package com.ruoyi.business.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.business.domain.BizOrg; +import com.ruoyi.business.mapper.BizOrgMapper; +import com.ruoyi.business.service.IBizOrgService; +import com.ruoyi.common.utils.id.SnowflakeId; + +@Service +public class BizOrgServiceImpl implements IBizOrgService { + @Autowired + private BizOrgMapper bizOrgMapper; + + @Override + public BizOrg getById(Long orgId) { return bizOrgMapper.selectByPrimaryKey(orgId); } + + @Override + public List selectList(BizOrg entity) { return bizOrgMapper.selectList(entity); } + + @Override + public int insert(BizOrg entity) { + // 主键由 AUTO_INCREMENT 自增, 不需要 Snowflake + return bizOrgMapper.insert(entity); + } + + @Override + public int updateByPrimaryKey(BizOrg entity) { return bizOrgMapper.updateByPrimaryKey(entity); } + + @Override + public int deleteByPrimaryKeys(Long[] orgIds) { + int rows = 0; + for (Long id : orgIds) { rows += bizOrgMapper.deleteByPrimaryKeys(new Long[]{id}); } + return rows; + } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizPersonServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizPersonServiceImpl.java new file mode 100644 index 0000000..8a56c04 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizPersonServiceImpl.java @@ -0,0 +1,89 @@ +package com.ruoyi.business.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.business.domain.BizPerson; +import com.ruoyi.business.mapper.BizPersonMapper; +import com.ruoyi.business.service.IBizPersonService; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.system.mapper.SysUserMapper; + +@Service +public class BizPersonServiceImpl implements IBizPersonService +{ + @Autowired + private BizPersonMapper bizPersonMapper; + @Autowired + private SysUserMapper sysUserMapper; + + @Override + public BizPerson getById(String personId) + { return bizPersonMapper.selectByPrimaryKey(personId); } + + @Override + public List selectList(BizPerson entity) + { return bizPersonMapper.selectList(entity); } + + @Override + public List selectSponsorList(BizPerson entity) + { return bizPersonMapper.selectSponsorList(entity); } + + /** + * sponsor 端新建人员 = 同时创建 sys_user 子账号 (account_type='SUB', parent_user_id=主账号 user_id) + * 返回新 SysUser 含 username + 明文 password (前端 toast 显示后即丢) + */ + @Override + public SysUser insert(BizPerson entity, Long mainUserId) { + // 0. 校验 username 唯一 (UK 约束, 先查更友好) + SysUser exist = sysUserMapper.selectUserByUserName(entity.getLoginUsername()); + if (exist != null) { + throw new ServiceException("登录账号已存在"); + } + // 1. 创建 sys_user 子账号 + SysUser newUser = new SysUser(); + newUser.setUserName(entity.getLoginUsername()); + newUser.setNickName(entity.getName()); + newUser.setPhonenumber(entity.getPhone()); + newUser.setPassword(SecurityUtils.encryptPassword(entity.getLoginPassword())); + newUser.setAccountType("SUB"); + newUser.setParentUserId(mainUserId); + newUser.setStatus("0"); + newUser.setDelFlag("0"); + newUser.setCreateBy(SecurityUtils.getUsername()); + // mybatis useGeneratedKeys 自动回填 userId + sysUserMapper.insertUser(newUser); + + // 2. 创建 biz_person + user_id 绑定 + SnowflakeId + entity.setUserId(newUser.getUserId()); + com.ruoyi.common.utils.id.SnowflakeId.injectIfEmpty(entity, "personId"); + entity.setCreateBy(SecurityUtils.getUsername()); + bizPersonMapper.insert(entity); + + // 3. 把明文 password 临时塞回去 (仅本次返回,前端 toast 用完即丢) + newUser.setPassword(entity.getLoginPassword()); + return newUser; + } + + @Override + public int updateByPrimaryKey(BizPerson entity) + { return bizPersonMapper.updateByPrimaryKey(entity); } + + @Override + public int deleteByPrimaryKey(String personId) + { + int n = bizPersonMapper.deleteByPrimaryKey(personId); + if (n > 0) bizPersonMapper.softDeleteSysUserByPersonIds(new String[]{ personId }); + return n; + } + + @Override + public int deleteByPrimaryKeys(String[] personIds) + { + int n = bizPersonMapper.deleteByPrimaryKeys(personIds); + if (n > 0) bizPersonMapper.softDeleteSysUserByPersonIds(personIds); + return n; + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectAssignServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectAssignServiceImpl.java new file mode 100644 index 0000000..b130174 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectAssignServiceImpl.java @@ -0,0 +1,40 @@ +package com.ruoyi.business.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.business.domain.BizProjectAssign; +import com.ruoyi.business.mapper.BizProjectAssignMapper; +import com.ruoyi.business.service.IBizProjectAssignService; + +@Service +public class BizProjectAssignServiceImpl implements IBizProjectAssignService +{ + @Autowired + private BizProjectAssignMapper bizProjectAssignMapper; + + @Override + public BizProjectAssign getById(String assignId) { return bizProjectAssignMapper.selectByPrimaryKey(assignId); } + + @Override + public List selectByProjectId(Long projectId) { return bizProjectAssignMapper.selectByProjectId(projectId); } + + @Override + public List selectList(BizProjectAssign entity) { return bizProjectAssignMapper.selectList(entity); } + + @Override + public int insert(BizProjectAssign entity) { + com.ruoyi.common.utils.id.SnowflakeId.injectIfEmpty(entity, "assignId"); + if (entity.getStatus() == null) entity.setStatus("0"); + return bizProjectAssignMapper.insert(entity); + } + + @Override + public int updateByPrimaryKey(BizProjectAssign entity) { return bizProjectAssignMapper.updateByPrimaryKey(entity); } + + @Override + public int deleteByPrimaryKey(String assignId) { return bizProjectAssignMapper.deleteByPrimaryKey(assignId); } + + @Override + public int deleteByProjectId(Long projectId) { return bizProjectAssignMapper.deleteByProjectId(projectId); } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectPlanServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectPlanServiceImpl.java new file mode 100644 index 0000000..827e793 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectPlanServiceImpl.java @@ -0,0 +1,33 @@ +package com.ruoyi.business.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.business.domain.BizProjectPlan; +import com.ruoyi.business.mapper.BizProjectPlanMapper; +import com.ruoyi.business.service.IBizProjectPlanService; + +@Service +public class BizProjectPlanServiceImpl implements IBizProjectPlanService +{ + @Autowired + private BizProjectPlanMapper bizProjectPlanMapper; + + @Override + public BizProjectPlan getById(String planId) + { return bizProjectPlanMapper.selectByPrimaryKey(planId); } + @Override + public List selectList(BizProjectPlan entity) + { return bizProjectPlanMapper.selectList(entity); } + @Override + public int insert(BizProjectPlan entity) { com.ruoyi.common.utils.id.SnowflakeId.injectIfEmpty(entity, "planId"); return bizProjectPlanMapper.insert(entity); } + @Override + public int updateByPrimaryKey(BizProjectPlan entity) + { return bizProjectPlanMapper.updateByPrimaryKey(entity); } + @Override + public int deleteByPrimaryKey(String planId) + { return bizProjectPlanMapper.deleteByPrimaryKey(planId); } + @Override + public int deleteByPrimaryKeys(String[] planId) + { return bizProjectPlanMapper.deleteByPrimaryKeys(planId); } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectRatingServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectRatingServiceImpl.java new file mode 100644 index 0000000..e66394a --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectRatingServiceImpl.java @@ -0,0 +1,42 @@ +package com.ruoyi.business.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.business.domain.BizProjectRating; +import com.ruoyi.business.mapper.BizProjectRatingMapper; +import com.ruoyi.business.service.IBizProjectRatingService; + +@Service +public class BizProjectRatingServiceImpl implements IBizProjectRatingService { + @Autowired + private BizProjectRatingMapper bizProjectRatingMapper; + + @Override + public BizProjectRating getById(String ratingId) + { return bizProjectRatingMapper.selectByPrimaryKey(ratingId); } + + @Override + public List selectList(BizProjectRating entity) + { return bizProjectRatingMapper.selectList(entity); } + + @Override + public int insert(BizProjectRating entity) + { com.ruoyi.common.utils.id.SnowflakeId.injectIfEmpty(entity, "ratingId"); return bizProjectRatingMapper.insert(entity); } + + @Override + public int updateByPrimaryKey(BizProjectRating entity) + { return bizProjectRatingMapper.updateByPrimaryKey(entity); } + + @Override + public int upsertRating(BizProjectRating entity) + { return bizProjectRatingMapper.upsertRating(entity); } + + @Override + public int deleteByPrimaryKey(String ratingId) + { return bizProjectRatingMapper.deleteByPrimaryKey(ratingId); } + + @Override + public int deleteByPrimaryKeys(String[] ratingId) + { return bizProjectRatingMapper.deleteByPrimaryKeys(ratingId); } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectServiceImpl.java new file mode 100644 index 0000000..a909b83 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectServiceImpl.java @@ -0,0 +1,36 @@ +package com.ruoyi.business.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.business.domain.BizProject; +import com.ruoyi.business.mapper.BizProjectMapper; +import com.ruoyi.business.service.IBizProjectService; + +@Service +public class BizProjectServiceImpl implements IBizProjectService +{ + @Autowired + private BizProjectMapper bizProjectMapper; + + @Override + public BizProject getById(String projectId) + { return bizProjectMapper.selectByPrimaryKey(projectId); } + @Override + public List selectList(BizProject entity) + { return bizProjectMapper.selectList(entity); } + @Override + public List selectSponsorList(BizProject entity) + { return bizProjectMapper.selectSponsorList(entity); } + @Override + public int insert(BizProject entity) { com.ruoyi.common.utils.id.SnowflakeId.injectIfEmpty(entity, "projectId"); return bizProjectMapper.insert(entity); } + @Override + public int updateByPrimaryKey(BizProject entity) + { return bizProjectMapper.updateByPrimaryKey(entity); } + @Override + public int deleteByPrimaryKey(String projectId) + { return bizProjectMapper.deleteByPrimaryKey(projectId); } + @Override + public int deleteByPrimaryKeys(String[] projectId) + { return bizProjectMapper.deleteByPrimaryKeys(projectId); } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectSponsorAssignServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectSponsorAssignServiceImpl.java new file mode 100644 index 0000000..3985e11 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectSponsorAssignServiceImpl.java @@ -0,0 +1,31 @@ +package com.ruoyi.business.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.business.domain.BizProjectSponsorAssign; +import com.ruoyi.business.mapper.BizProjectSponsorAssignMapper; +import com.ruoyi.business.service.IBizProjectSponsorAssignService; + +@Service +public class BizProjectSponsorAssignServiceImpl implements IBizProjectSponsorAssignService { + @Autowired + private BizProjectSponsorAssignMapper mapper; + + @Override + public int insertAssign(BizProjectSponsorAssign entity) { + // 赞助方分配策略: 一个项目只分配一个 sponsor, 先按 project_id 删, 再插 + mapper.deleteByProjectId(entity.getProjectId()); + return mapper.insertAssign(entity); + } + + @Override + public List listByProjectId(String projectId) { + return mapper.selectByProjectId(projectId); + } + + @Override + public int deleteByProjectId(String projectId) { + return mapper.deleteByProjectId(projectId); + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizPublicityServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizPublicityServiceImpl.java new file mode 100644 index 0000000..2f5e11d --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizPublicityServiceImpl.java @@ -0,0 +1,39 @@ +package com.ruoyi.business.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.business.domain.BizPublicity; +import com.ruoyi.business.mapper.BizPublicityMapper; +import com.ruoyi.business.service.IBizPublicityService; + +@Service +public class BizPublicityServiceImpl implements IBizPublicityService +{ + @Autowired + private BizPublicityMapper bizPublicityMapper; + + @Override + public BizPublicity getById(Long id) + { return bizPublicityMapper.selectByPrimaryKey(id); } + + @Override + public List selectList(BizPublicity entity) + { return bizPublicityMapper.selectList(entity); } + + @Override + public int insert(BizPublicity entity) + { return bizPublicityMapper.insert(entity); } + + @Override + public int updateByPrimaryKey(BizPublicity entity) + { return bizPublicityMapper.updateByPrimaryKey(entity); } + + @Override + public int deleteByPrimaryKey(Long id) + { return bizPublicityMapper.deleteByPrimaryKey(id); } + + @Override + public int deleteByPrimaryKeys(Long[] ids) + { return bizPublicityMapper.deleteByPrimaryKeys(ids); } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizResourceServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizResourceServiceImpl.java new file mode 100644 index 0000000..4a2bcf2 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizResourceServiceImpl.java @@ -0,0 +1,33 @@ +package com.ruoyi.business.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.business.domain.BizResource; +import com.ruoyi.business.mapper.BizResourceMapper; +import com.ruoyi.business.service.IBizResourceService; + +@Service +public class BizResourceServiceImpl implements IBizResourceService +{ + @Autowired + private BizResourceMapper bizResourceMapper; + + @Override + public BizResource getById(String resourceId) + { return bizResourceMapper.selectByPrimaryKey(resourceId); } + @Override + public List selectList(BizResource entity) + { return bizResourceMapper.selectList(entity); } + @Override + public int insert(BizResource entity) { com.ruoyi.common.utils.id.SnowflakeId.injectIfEmpty(entity, "resourceId"); return bizResourceMapper.insert(entity); } + @Override + public int updateByPrimaryKey(BizResource entity) + { return bizResourceMapper.updateByPrimaryKey(entity); } + @Override + public int deleteByPrimaryKey(String resourceId) + { return bizResourceMapper.deleteByPrimaryKey(resourceId); } + @Override + public int deleteByPrimaryKeys(String[] resourceId) + { return bizResourceMapper.deleteByPrimaryKeys(resourceId); } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizSubmissionServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizSubmissionServiceImpl.java new file mode 100644 index 0000000..86ac9bc --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizSubmissionServiceImpl.java @@ -0,0 +1,33 @@ +package com.ruoyi.business.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.business.domain.BizSubmission; +import com.ruoyi.business.mapper.BizSubmissionMapper; +import com.ruoyi.business.service.IBizSubmissionService; + +@Service +public class BizSubmissionServiceImpl implements IBizSubmissionService +{ + @Autowired + private BizSubmissionMapper bizSubmissionMapper; + + @Override + public BizSubmission getById(String submissionId) + { return bizSubmissionMapper.selectByPrimaryKey(submissionId); } + @Override + public List selectList(BizSubmission entity) + { return bizSubmissionMapper.selectList(entity); } + @Override + public int insert(BizSubmission entity) { com.ruoyi.common.utils.id.SnowflakeId.injectIfEmpty(entity, "subId"); return bizSubmissionMapper.insert(entity); } + @Override + public int updateByPrimaryKey(BizSubmission entity) + { return bizSubmissionMapper.updateByPrimaryKey(entity); } + @Override + public int deleteByPrimaryKey(String submissionId) + { return bizSubmissionMapper.deleteByPrimaryKey(submissionId); } + @Override + public int deleteByPrimaryKeys(String[] submissionId) + { return bizSubmissionMapper.deleteByPrimaryKeys(submissionId); } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizSupportIntentServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizSupportIntentServiceImpl.java new file mode 100644 index 0000000..c74850b --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizSupportIntentServiceImpl.java @@ -0,0 +1,33 @@ +package com.ruoyi.business.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.business.domain.BizSupportIntent; +import com.ruoyi.business.mapper.BizSupportIntentMapper; +import com.ruoyi.business.service.IBizSupportIntentService; + +@Service +public class BizSupportIntentServiceImpl implements IBizSupportIntentService +{ + @Autowired + private BizSupportIntentMapper bizSupportIntentMapper; + + @Override + public BizSupportIntent getById(String intentId) + { return bizSupportIntentMapper.selectByPrimaryKey(intentId); } + @Override + public List selectList(BizSupportIntent entity) + { return bizSupportIntentMapper.selectList(entity); } + @Override + public int insert(BizSupportIntent entity) { com.ruoyi.common.utils.id.SnowflakeId.injectIfEmpty(entity, "intentId"); return bizSupportIntentMapper.insert(entity); } + @Override + public int updateByPrimaryKey(BizSupportIntent entity) + { return bizSupportIntentMapper.updateByPrimaryKey(entity); } + @Override + public int deleteByPrimaryKey(String intentId) + { return bizSupportIntentMapper.deleteByPrimaryKey(intentId); } + @Override + public int deleteByPrimaryKeys(String[] intentId) + { return bizSupportIntentMapper.deleteByPrimaryKeys(intentId); } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizSupportLetterServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizSupportLetterServiceImpl.java new file mode 100644 index 0000000..93b35c6 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizSupportLetterServiceImpl.java @@ -0,0 +1,33 @@ +package com.ruoyi.business.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.business.domain.BizSupportLetter; +import com.ruoyi.business.mapper.BizSupportLetterMapper; +import com.ruoyi.business.service.IBizSupportLetterService; + +@Service +public class BizSupportLetterServiceImpl implements IBizSupportLetterService +{ + @Autowired + private BizSupportLetterMapper bizSupportLetterMapper; + + @Override + public BizSupportLetter getById(String letterId) + { return bizSupportLetterMapper.selectByPrimaryKey(letterId); } + @Override + public List selectList(BizSupportLetter entity) + { return bizSupportLetterMapper.selectList(entity); } + @Override + public int insert(BizSupportLetter entity) { com.ruoyi.common.utils.id.SnowflakeId.injectIfEmpty(entity, "annId"); return bizSupportLetterMapper.insert(entity); } + @Override + public int updateByPrimaryKey(BizSupportLetter entity) + { return bizSupportLetterMapper.updateByPrimaryKey(entity); } + @Override + public int deleteByPrimaryKey(String letterId) + { return bizSupportLetterMapper.deleteByPrimaryKey(letterId); } + @Override + public int deleteByPrimaryKeys(String[] letterId) + { return bizSupportLetterMapper.deleteByPrimaryKeys(letterId); } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/sms/AliyunSmsSender.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/sms/AliyunSmsSender.java new file mode 100644 index 0000000..10c5ea7 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/sms/AliyunSmsSender.java @@ -0,0 +1,89 @@ +package com.ruoyi.business.sms; + +import com.aliyuncs.DefaultAcsClient; +import com.aliyuncs.IAcsClient; +import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest; +import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse; +import com.aliyuncs.profile.DefaultProfile; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +/** + * 阿里云短信发送器 + * 参考 hwt-code SysSmsService 模式: cn-hangzhou region + 强制 endpoint dysmsapi.aliyuncs.com + * + * 关键: 必须 addEndpoint(product, domain), 否则 SDK 自动加 dysmsapi-proxy.cn-.aliyuncs.com + * 该代理在服务器网络下不可达 (内网 IP 100.67.x), 而 dysmsapi.aliyuncs.com 可达 + */ +@Service +public class AliyunSmsSender { + + private static final Logger log = LoggerFactory.getLogger(AliyunSmsSender.class); + + private static final String PRODUCT = "Dysmsapi"; + private static final String DOMAIN = "dysmsapi.aliyuncs.com"; + + @Value("${ruoyi.sms.accessKeyId}") + private String accessKeyId; + + @Value("${ruoyi.sms.accessKeySecret}") + private String accessKeySecret; + + @Value("${ruoyi.sms.signName}") + private String signName; + + @Value("${ruoyi.sms.template}") + private String template; + + @Value("${ruoyi.sms.regionId:cn-hangzhou}") + private String regionId; + + private static volatile boolean endpointAdded = false; + private volatile IAcsClient client; + + private IAcsClient getClient() { + if (client == null) { + synchronized (this) { + if (client == null) { + DefaultProfile profile = DefaultProfile.getProfile(regionId, accessKeyId, accessKeySecret); + if (!endpointAdded) { + try { + DefaultProfile.addEndpoint(regionId, regionId, PRODUCT, DOMAIN); + endpointAdded = true; + } catch (com.aliyuncs.exceptions.ClientException e) { + log.warn("[SMS] addEndpoint failed: {}", e.getMessage()); + } + } + client = new DefaultAcsClient(profile); + } + } + } + return client; + } + + /** + * 真发送短信验证码, 成功返回 true + */ + public boolean sendCode(String phone, String code) { + try { + SendSmsRequest req = new SendSmsRequest(); + req.setPhoneNumbers(phone); + req.setSignName(signName); + req.setTemplateCode(template); + req.setTemplateParam("{\"code\":\"" + code + "\"}"); + SendSmsResponse resp = getClient().getAcsResponse(req); + if ("OK".equalsIgnoreCase(resp.getCode())) { + log.info("[SMS] aliyun 发送成功 phone={}, bizId={}", phone, resp.getBizId()); + return true; + } + log.error("[SMS] aliyun 发送失败 phone={}, code={}, msg={}, requestId={}", + phone, resp.getCode(), resp.getMessage(), resp.getRequestId()); + return false; + } catch (Exception e) { + log.error("[SMS] aliyun 异常 phone={}", phone, e); + return false; + } + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizDepartmentMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizDepartmentMapper.xml new file mode 100644 index 0000000..eceb552 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizDepartmentMapper.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + select d.dept_id, d.name, d.sort, d.status, + d.create_by, d.create_time, d.update_by, d.update_time + from biz_department d + + + + + + + + + + insert into biz_department + + name, + sort, + status, + create_by, + create_time, + update_time, + + + #{name}, + #{sort}, + #{status}, + #{createBy}, + sysdate(), + sysdate(), + + + + + update biz_department + + name = #{name}, + sort = #{sort}, + status = #{status}, + update_by = #{updateBy}, + update_time = sysdate(), + + where dept_id = #{deptId} + + + + delete from biz_department where dept_id = #{deptId} + + + + delete from biz_department where dept_id in + + #{deptId} + + + \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizDoctorTitleMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizDoctorTitleMapper.xml new file mode 100644 index 0000000..163fda6 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizDoctorTitleMapper.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + select t.title_id, t.name, t.sort, t.status, + t.create_by, t.create_time, t.update_by, t.update_time + from biz_doctor_title t + + + + + + + + + + insert into biz_doctor_title + + name, + sort, + status, + create_by, + create_time, + update_time, + + + #{name}, + #{sort}, + #{status}, + #{createBy}, + sysdate(), + sysdate(), + + + + + update biz_doctor_title + + name = #{name}, + sort = #{sort}, + status = #{status}, + update_by = #{updateBy}, + update_time = sysdate(), + + where title_id = #{titleId} + + + + delete from biz_doctor_title where title_id = #{titleId} + + + + delete from biz_doctor_title where title_id in + + #{titleId} + + + \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizExecutionIntentMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizExecutionIntentMapper.xml new file mode 100644 index 0000000..850454b --- /dev/null +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizExecutionIntentMapper.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + select intent_id, user_id, project_no, project_name, name, work_unit, department, position, phone, onboard_status, create_by, create_time, update_by, update_time + from biz_execution_intent + + + + + insert into biz_execution_intent + + intent_id, + user_id, + project_no, + project_name, + work_unit, + onboard_status, + name, + department, + position, + phone, + create_by, + create_time, + + + #{intentId}, + #{userId}, + #{projectNo}, + #{projectName}, + #{workUnit}, + #{onboardStatus}, + #{name}, + #{department}, + #{position}, + #{phone}, + #{createBy}, + sysdate(), + + + + update biz_execution_intent + + project_no = #{projectNo}, + project_name = #{projectName}, + work_unit = #{workUnit}, + onboard_status = #{onboardStatus}, + name = #{name}, + department = #{department}, + position = #{position}, + phone = #{phone}, + + where intent_id = #{intentId} + + + delete from biz_execution_intent where intent_id = #{intentId} + + + delete from biz_execution_intent where intent_id in + + #{intentId} + + + \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizExpertMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizExpertMapper.xml new file mode 100644 index 0000000..fae1603 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizExpertMapper.xml @@ -0,0 +1,218 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select expert_id, user_id, name, phone, region, id_card, work_unit, department, title, practice_cert_url, title_cert_url, bank_card, bank_name, bank_province, bank_city, bank_address, id_card_front_url, id_card_back_url, audit_status, audit_by, audit_time, create_by, create_time, update_by, update_time + from biz_expert + + + + + + insert into biz_expert + + expert_id, + user_id, + name, + phone, + region, + id_card, + work_unit, + department, + title, + id_card_front_url, + id_card_back_url, + practice_cert_url, + title_cert_url, + bank_card, + bank_name, + bank_province, + bank_city, + bank_address, + audit_status, + audit_by, + audit_time, + status, + create_by, + create_time, + update_by, + update_time, + + + #{expertId}, + #{userId}, + #{name}, + #{phone}, + #{region}, + #{idCard}, + #{workUnit}, + #{department}, + #{title}, + #{idCardFrontUrl}, + #{idCardBackUrl}, + #{practiceCertUrl}, + #{titleCertUrl}, + #{bankCard}, + #{bankName}, + #{bankProvince}, + #{bankCity}, + #{bankAddress}, + #{auditStatus}, + #{auditBy}, + #{auditTime}, + #{status}, + #{createBy}, + #{createTime}, + #{updateBy}, + #{updateTime}, + + + + update biz_expert + + name = #{name}, + phone = #{phone}, + work_unit = #{workUnit}, + department = #{department}, + title = #{title}, + region = #{region}, + id_card = #{idCard}, + bank_card = #{bankCard}, + bank_name = #{bankName}, + bank_province = #{bankProvince}, + bank_city = #{bankCity}, + bank_address = #{bankAddress}, + id_card_front_url = #{idCardFrontUrl}, + id_card_back_url = #{idCardBackUrl}, + practice_cert_url = #{practiceCertUrl}, + title_cert_url = #{titleCertUrl}, + audit_opinion = #{auditOpinion}, + audit_by = #{auditBy}, + audit_time = #{auditTime}, + status = #{status}, + + where user_id = #{userId} + + + insert into biz_expert + + expert_id, + user_id, + name, + phone, + work_unit, + department, + title, + region, + id_card, + bank_card, + bank_name, + bank_province, + bank_city, + bank_address, + id_card_front_url, + id_card_back_url, + practice_cert_url, + title_cert_url, + + + #{expertId}, + #{userId}, + #{name}, + #{phone}, + #{workUnit}, + #{department}, + #{title}, + #{region}, + #{idCard}, + #{bankCard}, + #{bankName}, + #{bankProvince}, + #{bankCity}, + #{bankAddress}, + #{idCardFrontUrl}, + #{idCardBackUrl}, + #{practiceCertUrl}, + #{titleCertUrl}, + + + + update biz_expert + + user_id = #{userId}, + work_unit = #{workUnit}, + region = #{region}, + id_card = #{idCard}, + bank_card = #{bankCard}, + bank_name = #{bankName}, + bank_province = #{bankProvince}, + bank_city = #{bankCity}, + bank_address = #{bankAddress}, + id_card_front_url = #{idCardFrontUrl}, + id_card_back_url = #{idCardBackUrl}, + practice_cert_url = #{practiceCertUrl}, + title_cert_url = #{titleCertUrl}, + audit_status = #{auditStatus}, + audit_opinion = #{auditOpinion}, + audit_by = #{auditBy}, + audit_time = #{auditTime}, + status = #{status}, + name = #{name}, + phone = #{phone}, + department = #{department}, + title = #{title}, + + where expert_id = #{expertId} + + + delete from biz_expert where expert_id = #{expertId} + + + delete from biz_expert where expert_id in + + #{expertId} + + + \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizInvitationMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizInvitationMapper.xml new file mode 100644 index 0000000..adcc9e4 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizInvitationMapper.xml @@ -0,0 +1,57 @@ + + + + + + + select invite_id as ann_id, title from biz_invitation + + + + insert into biz_invitation + + ann_id, + title, + + + #{annId}, + #{title}, + + + + update biz_invitation + + project_id = #{projectId}, + project_no = #{projectNo}, + template_url = #{templateUrl}, + qr_code_url = #{qrCodeUrl}, + share_url = #{shareUrl}, + publish_time = #{publishTime}, + remark = #{remark}, + title = #{title}, + + where invite_id = #{annId} + + + delete from biz_invitation where invite_id = #{annId} + + + delete from biz_invitation where invite_id in + + #{annId} + + + \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizLaborVoucherMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizLaborVoucherMapper.xml new file mode 100644 index 0000000..98f6674 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizLaborVoucherMapper.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + select voucher_id, voucher_no, meeting_name, expert_name, id_card, bank_account, amount, submit_date, audit_status, create_by, create_time, update_by, update_time + from biz_labor_voucher + + + + + insert into biz_labor_voucher + + voucher_id, + amount, + + + #{voucherId}, + #{amount}, + + + + update biz_labor_voucher + + voucher_no = #{voucherNo}, + meeting_id = #{meetingId}, + meeting_name = #{meetingName}, + expert_name = #{expertName}, + id_card = #{idCard}, + bank_account = #{bankAccount}, + submit_date = #{submitDate}, + audit_status = #{auditStatus}, + audit_opinion = #{auditOpinion}, + audit_by = #{auditBy}, + audit_time = #{auditTime}, + amount = #{amount}, + + where voucher_id = #{voucherId} + + + delete from biz_labor_voucher where voucher_id = #{voucherId} + + + delete from biz_labor_voucher where voucher_id in + + #{voucherId} + + + \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingMapper.xml new file mode 100644 index 0000000..83c0ddc --- /dev/null +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingMapper.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + select meeting_id, project_no, business_id, project_form, meeting_name, start_time, end_time, total_periods, period_no, current_stage, create_by, create_time, update_by, update_time + from biz_meeting + + + + + insert into biz_meeting + + meeting_id, + + + #{meetingId}, + + + + update biz_meeting + + business_id = #{businessId}, + project_id = #{projectId}, + project_no = #{projectNo}, + project_name = #{projectName}, + meeting_name = #{meetingName}, + period_no = #{periodNo}, + total_periods = #{totalPeriods}, + project_form = #{projectForm}, + start_time = #{startTime}, + end_time = #{endTime}, + org_name = #{orgName}, + current_stage = #{currentStage}, + supervision_opinion = #{supervisionOpinion}, + supervision_by = #{supervisionBy}, + supervision_time = #{supervisionTime}, + invitation_url = #{invitationUrl}, + schedule_url = #{scheduleUrl}, + labor_signed = #{laborSigned}, + + where meeting_id = #{meetingId} + + + delete from biz_meeting where meeting_id = #{meetingId} + + + delete from biz_meeting where meeting_id in + + #{meetingId} + + + \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingSettlementMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingSettlementMapper.xml new file mode 100644 index 0000000..754475a --- /dev/null +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingSettlementMapper.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + select id, project_no, meeting_id, fee_type, unit_price, qty, subtotal, remark, audit_status, settlement_type, create_by, create_time, update_by, update_time + from biz_meeting_settlement + + + + + insert into biz_meeting_settlement + + id, + qty, + subtotal, + remark, + + + #{id}, + #{qty}, + #{subtotal}, + #{remark}, + + + + update biz_meeting_settlement + + meeting_id = #{meetingId}, + project_no = #{projectNo}, + project_name = #{projectName}, + exec_unit = #{execUnit}, + fee_type = #{feeType}, + unit_price = #{unitPrice}, + audit_status = #{auditStatus}, + settlement_type = #{settlementType}, + qty = #{qty}, + subtotal = #{subtotal}, + remark = #{remark}, + + where id = #{id} + + + delete from biz_meeting_settlement where id = #{id} + + + delete from biz_meeting_settlement where id in + + #{id} + + + \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMessageMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMessageMapper.xml new file mode 100644 index 0000000..9001944 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMessageMapper.xml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + select m.msg_id, m.receiver_user_id, m.msg_type, m.title, m.content, m.biz_type, m.biz_id, + m.is_read, m.read_time, m.create_by, m.create_time, m.update_by, m.update_time, + u.user_name as receiver_name + from biz_message m + left join sys_user u on u.user_id = m.receiver_user_id + + + + + + + + + + insert into biz_message + + msg_id, + receiver_user_id, + msg_type, + title, + content, + biz_type, + biz_id, + is_read, + read_time, + create_by, + create_time, + update_time, + + + #{msgId}, + #{receiverUserId}, + #{msgType}, + #{title}, + #{content}, + #{bizType}, + #{bizId}, + #{isRead}, + #{readTime}, + #{createBy}, + #{createTime}, + sysdate(), + + + + + update biz_message + + receiver_user_id = #{receiverUserId}, + msg_type = #{msgType}, + title = #{title}, + content = #{content}, + biz_type = #{bizType}, + biz_id = #{bizId}, + is_read = #{isRead}, + read_time = #{readTime}, + update_by = #{updateBy}, + update_time = sysdate(), + + where msg_id = #{msgId} + + + + update biz_message + set is_read = '1', read_time = sysdate(), update_time = sysdate() + where msg_id = #{msgId} + and receiver_user_id = #{receiverUserId} + and is_read = '0' + + + + update biz_message + set is_read = '1', read_time = sysdate(), update_time = sysdate() + where receiver_user_id = #{receiverUserId} + and is_read = '0' + + + + delete from biz_message where msg_id = #{msgId} + + + + delete from biz_message where msg_id in + + #{msgId} + + + diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizOrgMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizOrgMapper.xml new file mode 100644 index 0000000..c7487fb --- /dev/null +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizOrgMapper.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + select org_id, org_name, org_type, business_nature, address, tax_no, status, create_time, + contact_name, contact_phone, intent_count, + create_by, update_by, update_time + from biz_org + + + + + + + + insert into biz_org + + org_name, + org_type, + business_nature, + address, + tax_no, + contact_name, + contact_phone, + intent_count, + status, + create_by, + create_time, + + + #{orgName}, + #{orgType}, + #{businessNature}, + #{address}, + #{taxNo}, + #{contactName}, + #{contactPhone}, + #{intentCount}, + #{status}, + #{createBy}, + sysdate(), + + + + + update biz_org + + org_name = #{orgName}, + org_type = #{orgType}, + business_nature = #{businessNature}, + address = #{address}, + tax_no = #{taxNo}, + contact_name = #{contactName}, + contact_phone = #{contactPhone}, + intent_count = #{intentCount}, + status = #{status}, + update_by = #{updateBy}, + update_time = sysdate(), + + where org_id = #{orgId} + + + + delete from biz_org where org_id in + + #{orgId} + + + diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizPersonMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizPersonMapper.xml new file mode 100644 index 0000000..e7dbe92 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizPersonMapper.xml @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + select person_id, name, phone, org_id, department, position, role, unit_type, status, del_flag, user_id, create_by, create_time, update_by, update_time + from biz_person + + + + + select p.person_id, p.name, p.phone, p.org_id, o.org_name, o.org_type, + p.department, p.position, p.role, p.unit_type, p.status, p.del_flag, p.user_id, + p.create_by, p.create_time, p.update_by, p.update_time, + u.account_type, u.parent_user_id, u.del_flag as user_del_flag + from biz_person p + left join biz_org o on p.org_id = o.org_id + left join sys_user u on p.user_id = u.user_id + + + + + + + + insert into biz_person + + person_id, + name, + phone, + org_id, + department, + position, + role, + unit_type, + status, + user_id, + create_by, + create_time, + update_by, + update_time, + + + #{personId}, + #{name}, + #{phone}, + #{orgId}, + #{department}, + #{position}, + #{role}, + #{unitType}, + #{status}, + #{userId}, + #{createBy}, + sysdate(), + #{updateBy}, + sysdate(), + + + + + update biz_person + + user_id = #{userId}, + org_id = #{orgId}, + unit_type = #{unitType}, + name = #{name}, + phone = #{phone}, + department = #{department}, + position = #{position}, + role = #{role}, + status = #{status}, + update_by = #{updateBy}, + update_time = sysdate(), + + where person_id = #{personId} + + + + + update biz_person set del_flag='1', update_time=sysdate() + where person_id = #{personId} and del_flag='0' + + + update biz_person set del_flag='1', update_time=sysdate() + where del_flag='0' and person_id in + + #{personId} + + + + + + update sys_user u inner join biz_person p on u.user_id = p.user_id + set u.del_flag='1' + where u.del_flag='0' and p.del_flag='1' and p.person_id in + + #{personId} + + + + + + + diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectAssignMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectAssignMapper.xml new file mode 100644 index 0000000..2169a51 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectAssignMapper.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + select assign_id, project_id, exec_user_id, exec_user_name, exec_nick_name, exec_org, + sessions, amount, remark, status, create_by, create_time, update_by, update_time + from biz_project_assign + + + + + + + + + + insert into biz_project_assign + + assign_id, + project_id, + exec_user_id, + exec_user_name, + exec_nick_name, + exec_org, + sessions, + amount, + remark, + status, + create_by, + create_time, + + + #{assignId}, + #{projectId}, + #{execUserId}, + #{execUserName}, + #{execNickName}, + #{execOrg}, + #{sessions}, + #{amount}, + #{remark}, + #{status}, + #{createBy}, + sysdate(), + + + + + update biz_project_assign + + exec_user_id = #{execUserId}, + exec_user_name = #{execUserName}, + exec_nick_name = #{execNickName}, + exec_org = #{execOrg}, + sessions = #{sessions}, + amount = #{amount}, + remark = #{remark}, + status = #{status}, + update_by = #{updateBy}, + update_time = sysdate(), + + where assign_id = #{assignId} + + + + delete from biz_project_assign where assign_id = #{assignId} + + + + delete from biz_project_assign where project_id = #{projectId} + + diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectMapper.xml new file mode 100644 index 0000000..af5e028 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectMapper.xml @@ -0,0 +1,256 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select project_id, project_no, project_name, total_sessions, done_sessions, todo_sessions, total_amount, available_amount, paid_labor_amount, paid_meeting_amount, rating_score, rating_q1, rating_q2, rating_q3, rating_q4, org_name, org_type, project_form, is_finished, is_settled, org_id, create_by, create_time, update_by, update_time, manage_fee, start_time, end_time, submit_deadline_days, support_contract_url, execute_contract_url, invitation_url, support_letter_url, publish_url, notice_url, schedule_url, is_published, publish_time, announcement_type + from biz_project + + + + + select p.project_id, p.project_no, p.project_name, p.total_sessions, p.done_sessions, p.todo_sessions, + p.total_amount, p.available_amount, p.paid_labor_amount, p.paid_meeting_amount, + p.org_name, p.org_type, p.project_form, p.is_finished, p.is_settled, + p.org_id, + p.create_by, p.create_time, p.update_by, p.update_time, p.manage_fee, + p.start_time, p.end_time, p.submit_deadline_days, + p.support_contract_url, p.execute_contract_url, p.invitation_url, p.support_letter_url, + p.publish_url, p.notice_url, p.schedule_url, p.is_published, p.publish_time, p.announcement_type, + bps.sponsor_rating as sponsor_rating, + bps.sponsor_q1 as sponsor_q1, + bps.sponsor_q2 as sponsor_q2, + bps.sponsor_q3 as sponsor_q3, + bps.sponsor_q4 as sponsor_q4, + bps.sponsor_remark as sponsor_remark + from biz_project p + left join biz_project_sponsor bps on bps.project_id = p.project_id and bps.user_id = #{params.loginUid} + + + + + + + + insert into biz_project + + project_id, + project_no, + project_name, + total_sessions, + done_sessions, + todo_sessions, + total_amount, + available_amount, + paid_labor_amount, + paid_meeting_amount, + rating_score, + rating_q1, + rating_q2, + rating_q3, + rating_q4, + org_name, + org_type, + project_form, + is_finished, + is_settled, + org_id, + create_by, + create_time, + update_by, + update_time, + manage_fee, + start_time, + end_time, + submit_deadline_days, + support_contract_url, + execute_contract_url, + invitation_url, + support_letter_url, + publish_url, + notice_url, + schedule_url, + is_published, + publish_time, + announcement_type, + + + #{projectId}, + #{projectNo}, + #{projectName}, + #{totalSessions}, + #{doneSessions}, + #{todoSessions}, + #{totalAmount}, + #{availableAmount}, + #{paidLaborAmount}, + #{paidMeetingAmount}, + #{ratingScore}, + #{ratingQ1}, + #{ratingQ2}, + #{ratingQ3}, + #{ratingQ4}, + #{orgName}, + #{orgType}, + #{projectForm}, + #{isFinished}, + #{isSettled}, + #{orgId}, + #{createBy}, + #{createTime}, + #{updateBy}, + #{updateTime}, + #{manageFee}, + #{startTime}, + #{endTime}, + #{submitDeadlineDays}, + #{supportContractUrl}, + #{executeContractUrl}, + #{invitationUrl}, + #{supportLetterUrl}, + #{publishUrl}, + #{noticeUrl}, + #{scheduleUrl}, + #{isPublished}, + #{publishTime}, + #{announcementType}, + + + + update biz_project + + manage_fee = #{manageFee}, + start_time = #{startTime}, + end_time = #{endTime}, + submit_deadline_days = #{submitDeadlineDays}, + support_contract_url = #{supportContractUrl}, + execute_contract_url = #{executeContractUrl}, + invitation_url = #{invitationUrl}, + support_letter_url = #{supportLetterUrl}, + publish_url = #{publishUrl}, + notice_url = #{noticeUrl}, + schedule_url = #{scheduleUrl}, + is_published = #{isPublished}, + publish_time = #{publishTime}, + announcement_type = #{announcementType}, + project_no = #{projectNo}, + project_name = #{projectName}, + total_sessions = #{totalSessions}, + done_sessions = #{doneSessions}, + todo_sessions = #{todoSessions}, + total_amount = #{totalAmount}, + available_amount = #{availableAmount}, + paid_labor_amount = #{paidLaborAmount}, + paid_meeting_amount = #{paidMeetingAmount}, + rating_score = #{ratingScore}, + rating_q1 = #{ratingQ1}, + rating_q2 = #{ratingQ2}, + rating_q3 = #{ratingQ3}, + rating_q4 = #{ratingQ4}, + org_name = #{orgName}, + org_type = #{orgType}, + project_form = #{projectForm}, + is_finished = #{isFinished}, + is_settled = #{isSettled}, + org_id = #{orgId}, + create_by = #{createBy}, + create_time = #{createTime}, + update_by = #{updateBy}, + update_time = #{updateTime}, + + where project_id = #{projectId} + + + delete from biz_project where project_id = #{projectId} + + + delete from biz_project where project_id in + + #{projectId} + + + \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectPlanMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectPlanMapper.xml new file mode 100644 index 0000000..5a9fc66 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectPlanMapper.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + select plan_id, plan_name, plan_direction, plan_category, project_form, design_file_url, status, is_settled, project_no, remark, create_by, create_time, update_by, update_time + from biz_project_plan + + + + + insert into biz_project_plan + + plan_id, + status, + remark, + + + #{planId}, + #{status}, + #{remark}, + + + + update biz_project_plan + + plan_name = #{planName}, + plan_direction = #{planDirection}, + plan_category = #{planCategory}, + project_form = #{projectForm}, + design_file_url = #{designFileUrl}, + is_settled = #{isSettled}, + project_no = #{projectNo}, + audit_opinion = #{auditOpinion}, + audit_by = #{auditBy}, + audit_time = #{auditTime}, + status = #{status}, + remark = #{remark}, + + where plan_id = #{planId} + + + delete from biz_project_plan where plan_id = #{planId} + + + delete from biz_project_plan where plan_id in + + #{planId} + + + \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectRatingMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectRatingMapper.xml new file mode 100644 index 0000000..7b54150 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectRatingMapper.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + select rating_id, project_no, project_name, rater_name, rater_role, quality_score, response_score, cooperation_score, compliance_score, total_score, remark, create_by, create_time, update_by, update_time + from biz_project_rating + + + + + insert into biz_project_rating + + rating_id, + remark, + + + #{ratingId}, + #{remark}, + + + + update biz_project_rating + + project_id = #{projectId}, + project_no = #{projectNo}, + project_name = #{projectName}, + rater_id = #{raterId}, + rater_name = #{raterName}, + rater_role = #{raterRole}, + quality_score = #{qualityScore}, + response_score = #{responseScore}, + cooperation_score = #{cooperationScore}, + compliance_score = #{complianceScore}, + total_score = #{totalScore}, + rating_time = #{ratingTime}, + remark = #{remark}, + + where rating_id = #{ratingId} + + + delete from biz_project_rating where rating_id = #{ratingId} + + + delete from biz_project_rating where rating_id in + + #{ratingId} + + + + + + INSERT INTO biz_project_rating + (project_id, project_no, project_name, rater_id, rater_name, rater_role, + quality_score, response_score, cooperation_score, compliance_score, total_score, + remark, create_by, create_time, update_by, update_time) + VALUES + (#{projectId}, #{projectNo}, #{projectName}, #{raterId}, #{raterName}, #{raterRole}, + #{qualityScore}, #{responseScore}, #{cooperationScore}, #{complianceScore}, #{totalScore}, + #{remark}, #{createBy}, sysdate(), #{updateBy}, sysdate()) + ON DUPLICATE KEY UPDATE + project_no = VALUES(project_no), + project_name = VALUES(project_name), + rater_name = VALUES(rater_name), + quality_score = VALUES(quality_score), + response_score = VALUES(response_score), + cooperation_score= VALUES(cooperation_score), + compliance_score = VALUES(compliance_score), + total_score = VALUES(total_score), + remark = VALUES(remark), + update_by = VALUES(update_by), + update_time = sysdate() + + \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectSponsorAssignMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectSponsorAssignMapper.xml new file mode 100644 index 0000000..7f37a31 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectSponsorAssignMapper.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + INSERT INTO biz_project_sponsor_assign + (project_id, sponsor_user_id, monitor_user_id, assign_desc, assign_points, create_by, create_time) + VALUES + (#{projectId}, #{sponsorUserId}, #{monitorUserId}, #{assignDesc}, #{assignPoints}, #{createBy}, sysdate()) + + + + + delete from biz_project_sponsor_assign where project_id = #{projectId} + + + + \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizPublicityMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizPublicityMapper.xml new file mode 100644 index 0000000..d7e936d --- /dev/null +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizPublicityMapper.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + select id, project_id, project_no, project_name, announce_type, title, file_url, remark, status, rating_time, create_by, create_time, update_by, update_time + from biz_publicity + + + + + + + + + + insert into biz_publicity + + project_id, + project_no, + project_name, + announce_type, + title, + file_url, + remark, + status, + rating_time, + create_by, + create_time, + update_by, + update_time, + + + #{projectId}, + #{projectNo}, + #{projectName}, + #{announceType}, + #{title}, + #{fileUrl}, + #{remark}, + #{status}, + #{ratingTime}, + #{createBy}, + sysdate(), + #{updateBy}, + sysdate(), + + + + + update biz_publicity + + project_id = #{projectId}, + project_no = #{projectNo}, + project_name = #{projectName}, + announce_type = #{announceType}, + title = #{title}, + file_url = #{fileUrl}, + remark = #{remark}, + status = #{status}, + rating_time = #{ratingTime}, + update_by = #{updateBy}, + update_time = sysdate(), + + where id = #{id} + + + + delete from biz_publicity where id = #{id} + + + + delete from biz_publicity where id in + + #{id} + + + diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizResourceMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizResourceMapper.xml new file mode 100644 index 0000000..73e22cd --- /dev/null +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizResourceMapper.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + select resource_id, name, category, file_size, uploader_name, upload_time, status, create_by, create_time, update_by, update_time + from biz_resource + + + + + insert into biz_resource + + resource_id, + name, + category, + status, + + + #{resourceId}, + #{name}, + #{category}, + #{status}, + + + + update biz_resource + + name = #{name}, + category = #{category}, + status = #{status}, + + where resource_id = #{resourceId} + + + delete from biz_resource where resource_id = #{resourceId} + + + delete from biz_resource where resource_id in + + #{resourceId} + + + \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizSubmissionMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizSubmissionMapper.xml new file mode 100644 index 0000000..0db7bbb --- /dev/null +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizSubmissionMapper.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + select sub_id, title, direction, project_form, project_category, design_file_url, submitter_id, submitter_name, status, remark, create_by, create_time, update_by, update_time + from biz_submission + + + + + insert into biz_submission + + sub_id, + title, + direction, + project_form, + project_category, + design_file_url, + submitter_id, + submitter_name, + status, + remark, + create_by, + create_time, + update_by, + update_time, + + + #{subId}, + #{title}, + #{direction}, + #{projectForm}, + #{projectCategory}, + #{designFileUrl}, + #{submitterId}, + #{submitterName}, + #{status}, + #{remark}, + #{createBy}, + sysdate(), + #{updateBy}, + sysdate(), + + + + update biz_submission + + submitter_id = #{submitterId}, + submitter_name = #{submitterName}, + project_form = #{projectForm}, + project_category = #{projectCategory}, + design_file_url = #{designFileUrl}, + audit_opinion = #{auditOpinion}, + audit_by = #{auditBy}, + audit_time = #{auditTime}, + title = #{title}, + direction = #{direction}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate(), + + where sub_id = #{subId} + + + delete from biz_submission where sub_id = #{subId} + + + delete from biz_submission where sub_id in + + #{subId} + + + \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizSupportIntentMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizSupportIntentMapper.xml new file mode 100644 index 0000000..dad35db --- /dev/null +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizSupportIntentMapper.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + select intent_id, project_no, project_name, name, work_unit, department, position, phone, account_status, create_by, create_time, update_by, update_time + from biz_support_intent + + + + + insert into biz_support_intent + + intent_id, + name, + department, + position, + phone, + + + #{intentId}, + #{name}, + #{department}, + #{position}, + #{phone}, + + + + update biz_support_intent + + project_no = #{projectNo}, + project_name = #{projectName}, + work_unit = #{workUnit}, + account_status = #{accountStatus}, + name = #{name}, + department = #{department}, + position = #{position}, + phone = #{phone}, + + where intent_id = #{intentId} + + + delete from biz_support_intent where intent_id = #{intentId} + + + delete from biz_support_intent where intent_id in + + #{intentId} + + + \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizSupportLetterMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizSupportLetterMapper.xml new file mode 100644 index 0000000..b011f85 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizSupportLetterMapper.xml @@ -0,0 +1,54 @@ + + + + + + + select letter_id as ann_id, title from biz_support_letter + + + + insert into biz_support_letter + + ann_id, + title, + + + #{annId}, + #{title}, + + + + update biz_support_letter + + project_id = #{projectId}, + project_no = #{projectNo}, + file_url = #{fileUrl}, + publish_time = #{publishTime}, + support_count = #{supportCount}, + title = #{title}, + + where letter_id = #{annId} + + + delete from biz_support_letter where letter_id = #{annId} + + + delete from biz_support_letter where letter_id in + + #{annId} + + + \ No newline at end of file diff --git a/ry-api/ruoyi-common/pom.xml b/ry-api/ruoyi-common/pom.xml new file mode 100644 index 0000000..d67fa45 --- /dev/null +++ b/ry-api/ruoyi-common/pom.xml @@ -0,0 +1,123 @@ + + + + ruoyi + com.ruoyi + 3.9.2 + + 4.0.0 + + ruoyi-common + + + common通用工具 + + + + + + + org.springframework + spring-context-support + + + + + org.springframework + spring-web + + + + + org.springframework.boot + spring-boot-starter-security + + + + + com.github.pagehelper + pagehelper-spring-boot-starter + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.apache.commons + commons-lang3 + + + + + tools.jackson.core + jackson-databind + + + + + com.alibaba.fastjson2 + fastjson2 + + + + + commons-io + commons-io + + + + + org.apache.poi + poi-ooxml + + + + + io.jsonwebtoken + jjwt + + + + + javax.xml.bind + jaxb-api + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + org.springframework.boot + spring-boot-starter-cache + + + + + org.apache.commons + commons-pool2 + + + + + nl.basjes.parse.useragent + yauaa + + + + + jakarta.servlet + jakarta.servlet-api + + + + + \ No newline at end of file diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Anonymous.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Anonymous.java new file mode 100644 index 0000000..1d6d4f4 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Anonymous.java @@ -0,0 +1,19 @@ +package com.ruoyi.common.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 匿名访问不鉴权注解 + * + * @author ruoyi + */ +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Anonymous +{ +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataScope.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataScope.java new file mode 100644 index 0000000..36163ae --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataScope.java @@ -0,0 +1,43 @@ +package com.ruoyi.common.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 数据权限过滤注解 + * + * @author ruoyi + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface DataScope +{ + /** + * 用户表的别名 + */ + public String userAlias() default ""; + + /** + * 部门表的别名 + */ + public String deptAlias() default ""; + + /** + * 用户字段名 + */ + public String userField() default "user_id"; + + /** + * 部门字段名 + */ + public String deptField() default "dept_id"; + + /** + * 权限字符(用于多个角色匹配符合要求的权限)默认根据权限注解@ss获取,多个权限用逗号分隔开来 + */ + public String permission() default ""; +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataSource.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataSource.java new file mode 100644 index 0000000..79cd191 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataSource.java @@ -0,0 +1,28 @@ +package com.ruoyi.common.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import com.ruoyi.common.enums.DataSourceType; + +/** + * 自定义多数据源切换注解 + * + * 优先级:先方法,后类,如果方法覆盖了类上的数据源类型,以方法的为准,否则以类上的为准 + * + * @author ruoyi + */ +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface DataSource +{ + /** + * 切换数据源名称 + */ + public DataSourceType value() default DataSourceType.MASTER; +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Excel.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Excel.java new file mode 100644 index 0000000..57c08b1 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Excel.java @@ -0,0 +1,198 @@ +package com.ruoyi.common.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.math.BigDecimal; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.IndexedColors; +import com.ruoyi.common.utils.poi.ExcelHandlerAdapter; + +/** + * 自定义导出Excel数据注解 + * + * @author ruoyi + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Excel +{ + /** + * 导出时在excel中排序 + */ + public int sort() default Integer.MAX_VALUE; + + /** + * 导出到Excel中的名字. + */ + public String name() default ""; + + /** + * 日期格式, 如: yyyy-MM-dd + */ + public String dateFormat() default ""; + + /** + * 如果是字典类型,请设置字典的type值 (如: sys_user_sex) + */ + public String dictType() default ""; + + /** + * 读取内容转表达式 (如: 0=男,1=女,2=未知) + */ + public String readConverterExp() default ""; + + /** + * 分隔符,读取字符串组内容 + */ + public String separator() default ","; + + /** + * BigDecimal 精度 默认:-1(默认不开启BigDecimal格式化) + */ + public int scale() default -1; + + /** + * BigDecimal 舍入规则 默认:BigDecimal.ROUND_HALF_EVEN + */ + @SuppressWarnings("deprecation") + public int roundingMode() default BigDecimal.ROUND_HALF_EVEN; + + /** + * 导出时在excel中每个列的高度 + */ + public double height() default 14; + + /** + * 导出时在excel中每个列的宽度 + */ + public double width() default 16; + + /** + * 文字后缀,如% 90 变成90% + */ + public String suffix() default ""; + + /** + * 当值为空时,字段的默认值 + */ + public String defaultValue() default ""; + + /** + * 提示信息 + */ + public String prompt() default ""; + + /** + * 是否允许内容换行 + */ + public boolean wrapText() default false; + + /** + * 设置只能选择不能输入的列内容. + */ + public String[] combo() default {}; + + /** + * 是否从字典读数据到combo,默认不读取,如读取需要设置dictType注解. + */ + public boolean comboReadDict() default false; + + /** + * 是否需要纵向合并单元格,应对需求:含有list集合单元格) + */ + public boolean needMerge() default false; + + /** + * 是否导出数据,应对需求:有时我们需要导出一份模板,这是标题需要但内容需要用户手工填写. + */ + public boolean isExport() default true; + + /** + * 另一个类中的属性名称,支持多级获取,以小数点隔开 + */ + public String targetAttr() default ""; + + /** + * 是否自动统计数据,在最后追加一行统计数据总和 + */ + public boolean isStatistics() default false; + + /** + * 导出类型(0数字 1字符串 2图片) + */ + public ColumnType cellType() default ColumnType.STRING; + + /** + * 导出列头背景颜色 + */ + public IndexedColors headerBackgroundColor() default IndexedColors.GREY_50_PERCENT; + + /** + * 导出列头字体颜色 + */ + public IndexedColors headerColor() default IndexedColors.WHITE; + + /** + * 导出单元格背景颜色 + */ + public IndexedColors backgroundColor() default IndexedColors.WHITE; + + /** + * 导出单元格字体颜色 + */ + public IndexedColors color() default IndexedColors.BLACK; + + /** + * 导出字段对齐方式 + */ + public HorizontalAlignment align() default HorizontalAlignment.CENTER; + + /** + * 自定义数据处理器 + */ + public Class handler() default ExcelHandlerAdapter.class; + + /** + * 自定义数据处理器参数 + */ + public String[] args() default {}; + + /** + * 字段类型(0:导出导入;1:仅导出;2:仅导入) + */ + Type type() default Type.ALL; + + public enum Type + { + ALL(0), EXPORT(1), IMPORT(2); + private final int value; + + Type(int value) + { + this.value = value; + } + + public int value() + { + return this.value; + } + } + + public enum ColumnType + { + NUMERIC(0), STRING(1), IMAGE(2), TEXT(3); + private final int value; + + ColumnType(int value) + { + this.value = value; + } + + public int value() + { + return this.value; + } + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Excels.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Excels.java new file mode 100644 index 0000000..1f1cc81 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Excels.java @@ -0,0 +1,18 @@ +package com.ruoyi.common.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Excel注解集 + * + * @author ruoyi + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Excels +{ + public Excel[] value(); +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Log.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Log.java new file mode 100644 index 0000000..1eb8e49 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Log.java @@ -0,0 +1,51 @@ +package com.ruoyi.common.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.enums.OperatorType; + +/** + * 自定义操作日志记录注解 + * + * @author ruoyi + * + */ +@Target({ ElementType.PARAMETER, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Log +{ + /** + * 模块 + */ + public String title() default ""; + + /** + * 功能 + */ + public BusinessType businessType() default BusinessType.OTHER; + + /** + * 操作人类别 + */ + public OperatorType operatorType() default OperatorType.MANAGE; + + /** + * 是否保存请求的参数 + */ + public boolean isSaveRequestData() default true; + + /** + * 是否保存响应的参数 + */ + public boolean isSaveResponseData() default true; + + /** + * 排除指定的请求参数 + */ + public String[] excludeParamNames() default {}; +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/annotation/RateLimiter.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/annotation/RateLimiter.java new file mode 100644 index 0000000..0f024c7 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/annotation/RateLimiter.java @@ -0,0 +1,40 @@ +package com.ruoyi.common.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.enums.LimitType; + +/** + * 限流注解 + * + * @author ruoyi + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RateLimiter +{ + /** + * 限流key + */ + public String key() default CacheConstants.RATE_LIMIT_KEY; + + /** + * 限流时间,单位秒 + */ + public int time() default 60; + + /** + * 限流次数 + */ + public int count() default 100; + + /** + * 限流类型 + */ + public LimitType limitType() default LimitType.DEFAULT; +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/annotation/RepeatSubmit.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/annotation/RepeatSubmit.java new file mode 100644 index 0000000..b769748 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/annotation/RepeatSubmit.java @@ -0,0 +1,31 @@ +package com.ruoyi.common.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 自定义注解防止表单重复提交 + * + * @author ruoyi + * + */ +@Inherited +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RepeatSubmit +{ + /** + * 间隔时间(ms),小于此时间视为重复提交 + */ + public int interval() default 5000; + + /** + * 提示消息 + */ + public String message() default "不允许重复提交,请稍候再试"; +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Sensitive.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Sensitive.java new file mode 100644 index 0000000..b33c961 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Sensitive.java @@ -0,0 +1,24 @@ +package com.ruoyi.common.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; +import tools.jackson.databind.annotation.JsonSerialize; +import com.ruoyi.common.config.serializer.SensitiveJsonSerializer; +import com.ruoyi.common.enums.DesensitizedType; + +/** + * 数据脱敏注解 + * + * @author ruoyi + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.FIELD, ElementType.METHOD }) +@JacksonAnnotationsInside +@JsonSerialize(using = SensitiveJsonSerializer.class) +public @interface Sensitive +{ + DesensitizedType desensitizedType(); +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/config/RuoYiConfig.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/config/RuoYiConfig.java new file mode 100644 index 0000000..260c945 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/config/RuoYiConfig.java @@ -0,0 +1,164 @@ +package com.ruoyi.common.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 读取项目相关配置 + * + * @author ruoyi + */ +@Component +@ConfigurationProperties(prefix = "ruoyi") +public class RuoYiConfig +{ + /** 项目名称 */ + private String name; + + /** 版本 */ + private String version; + + /** 版权年份 */ + private String copyrightYear; + + /** 上传路径 */ + private static String profile; + + /** 获取地址开关 */ + private static boolean addressEnabled; + + /** 验证码类型 */ + private static String captchaType; + + /** OSS 配置 (可选) */ + private OssProperties oss = new OssProperties(); + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public String getVersion() + { + return version; + } + + public void setVersion(String version) + { + this.version = version; + } + + public String getCopyrightYear() + { + return copyrightYear; + } + + public void setCopyrightYear(String copyrightYear) + { + this.copyrightYear = copyrightYear; + } + + public static String getProfile() + { + return profile; + } + + public void setProfile(String profile) + { + RuoYiConfig.profile = profile; + } + + public static boolean isAddressEnabled() + { + return addressEnabled; + } + + public void setAddressEnabled(boolean addressEnabled) + { + RuoYiConfig.addressEnabled = addressEnabled; + } + + public static String getCaptchaType() { + return captchaType; + } + + public void setCaptchaType(String captchaType) { + RuoYiConfig.captchaType = captchaType; + } + + /** + * 获取导入上传路径 + */ + public static String getImportPath() + { + return getProfile() + "/import"; + } + + /** + * 获取头像上传路径 + */ + public static String getAvatarPath() + { + return getProfile() + "/avatar"; + } + + /** + * 获取下载路径 + */ + public static String getDownloadPath() + { + return getProfile() + "/download/"; + } + + /** + * 获取上传路径 + */ + public static String getUploadPath() + { + return getProfile() + "/upload"; + } + + public OssProperties getOss() + { + return oss; + } + + public void setOss(OssProperties oss) + { + this.oss = oss; + } + + /** + * OSS 配置 (前端直传签名用) + */ + public static class OssProperties + { + private boolean enabled; + private String endpoint; + private String bucket; + private String accessKeyId; + private String accessKeySecret; + private String dirPrefix = "ry8080/"; + private int expireSeconds = 3600; + + public boolean isEnabled() { return enabled; } + public void setEnabled(boolean enabled) { this.enabled = enabled; } + public String getEndpoint() { return endpoint; } + public void setEndpoint(String endpoint) { this.endpoint = endpoint; } + public String getBucket() { return bucket; } + public void setBucket(String bucket) { this.bucket = bucket; } + public String getAccessKeyId() { return accessKeyId; } + public void setAccessKeyId(String accessKeyId) { this.accessKeyId = accessKeyId; } + public String getAccessKeySecret() { return accessKeySecret; } + public void setAccessKeySecret(String accessKeySecret) { this.accessKeySecret = accessKeySecret; } + public String getDirPrefix() { return dirPrefix; } + public void setDirPrefix(String dirPrefix) { this.dirPrefix = dirPrefix; } + public int getExpireSeconds() { return expireSeconds; } + public void setExpireSeconds(int expireSeconds) { this.expireSeconds = expireSeconds; } + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/config/serializer/SensitiveJsonSerializer.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/config/serializer/SensitiveJsonSerializer.java new file mode 100644 index 0000000..df676da --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/config/serializer/SensitiveJsonSerializer.java @@ -0,0 +1,77 @@ +package com.ruoyi.common.config.serializer; + +import java.util.Objects; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonGenerator; +import tools.jackson.databind.BeanProperty; +import tools.jackson.databind.DatabindException; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.ValueSerializer; +import tools.jackson.databind.ser.std.StdSerializer; +import com.ruoyi.common.annotation.Sensitive; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.enums.DesensitizedType; +import com.ruoyi.common.utils.SecurityUtils; + +/** + * 数据脱敏序列化过滤 + * + * @author ruoyi + */ +public class SensitiveJsonSerializer extends StdSerializer +{ + private final DesensitizedType desensitizedType; + + public SensitiveJsonSerializer() + { + super(String.class); + this.desensitizedType = null; + } + + public SensitiveJsonSerializer(DesensitizedType desensitizedType) + { + super(String.class); + this.desensitizedType = desensitizedType; + } + + @Override + public void serialize(String value, JsonGenerator gen, SerializationContext ctxt) throws JacksonException + { + if (desensitizedType != null && desensitization()) + { + gen.writeString(desensitizedType.desensitizer().apply(value)); + } + else + { + gen.writeString(value); + } + } + + @Override + public ValueSerializer createContextual(SerializationContext ctxt, BeanProperty property) throws DatabindException + { + Sensitive annotation = property.getAnnotation(Sensitive.class); + if (Objects.nonNull(annotation) && Objects.equals(String.class, property.getType().getRawClass())) + { + return new SensitiveJsonSerializer(annotation.desensitizedType()); + } + return ctxt.findValueSerializer(property.getType()); + } + + /** + * 是否需要脱敏处理 + */ + private boolean desensitization() + { + try + { + LoginUser securityUser = SecurityUtils.getLoginUser(); + // 管理员不脱敏 + return !securityUser.getUser().isAdmin(); + } + catch (Exception e) + { + return true; + } + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java new file mode 100644 index 0000000..0080343 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java @@ -0,0 +1,44 @@ +package com.ruoyi.common.constant; + +/** + * 缓存的key 常量 + * + * @author ruoyi + */ +public class CacheConstants +{ + /** + * 登录用户 redis key + */ + public static final String LOGIN_TOKEN_KEY = "login_tokens:"; + + /** + * 验证码 redis key + */ + public static final String CAPTCHA_CODE_KEY = "captcha_codes:"; + + /** + * 参数管理 cache key + */ + public static final String SYS_CONFIG_KEY = "sys_config:"; + + /** + * 字典管理 cache key + */ + public static final String SYS_DICT_KEY = "sys_dict:"; + + /** + * 防重提交 redis key + */ + public static final String REPEAT_SUBMIT_KEY = "repeat_submit:"; + + /** + * 限流 redis key + */ + public static final String RATE_LIMIT_KEY = "rate_limit:"; + + /** + * 登录账户密码错误次数 redis key + */ + public static final String PWD_ERR_CNT_KEY = "pwd_err_cnt:"; +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/constant/Constants.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/constant/Constants.java new file mode 100644 index 0000000..51d7f56 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/constant/Constants.java @@ -0,0 +1,204 @@ +package com.ruoyi.common.constant; + +import java.util.Locale; +import io.jsonwebtoken.Claims; + +/** + * 通用常量信息 + * + * @author ruoyi + */ +public class Constants +{ + /** + * UTF-8 字符集 + */ + public static final String UTF8 = "UTF-8"; + + /** + * GBK 字符集 + */ + public static final String GBK = "GBK"; + + /** + * 系统语言 + */ + public static final Locale DEFAULT_LOCALE = Locale.SIMPLIFIED_CHINESE; + + /** + * www主域 + */ + public static final String WWW = "www."; + + /** + * http请求 + */ + public static final String HTTP = "http://"; + + /** + * https请求 + */ + public static final String HTTPS = "https://"; + + /** + * 通用成功标识 + */ + public static final String SUCCESS = "0"; + + /** + * 通用失败标识 + */ + public static final String FAIL = "1"; + + /** + * 登录成功 + */ + public static final String LOGIN_SUCCESS = "Success"; + + /** + * 注销 + */ + public static final String LOGOUT = "Logout"; + + /** + * 注册 + */ + public static final String REGISTER = "Register"; + + /** + * 登录失败 + */ + public static final String LOGIN_FAIL = "Error"; + + /** + * 所有权限标识 + */ + public static final String ALL_PERMISSION = "*:*:*"; + + /** + * 管理员角色权限标识 + */ + public static final String SUPER_ADMIN = "admin"; + + /** + * 角色权限分隔符 + */ + public static final String ROLE_DELIMITER = ","; + + /** + * 权限标识分隔符 + */ + public static final String PERMISSION_DELIMITER = ","; + + /** + * 验证码有效期(分钟) + */ + public static final Integer CAPTCHA_EXPIRATION = 2; + + /** + * 令牌 + */ + public static final String TOKEN = "token"; + + /** + * 令牌前缀 + */ + public static final String TOKEN_PREFIX = "Bearer "; + + /** + * 令牌前缀 + */ + public static final String LOGIN_USER_KEY = "login_user_key"; + + /** + * 用户ID + */ + public static final String JWT_USERID = "userid"; + + /** + * 用户名称 + */ + public static final String JWT_USERNAME = Claims.SUBJECT; + + /** + * 用户头像 + */ + public static final String JWT_AVATAR = "avatar"; + + /** + * 创建时间 + */ + public static final String JWT_CREATED = "created"; + + /** + * 用户权限 + */ + public static final String JWT_AUTHORITIES = "authorities"; + + /** + * 资源映射路径 前缀 + */ + public static final String RESOURCE_PREFIX = "/profile"; + + /** + * RMI 远程方法调用 + */ + public static final String LOOKUP_RMI = "rmi:"; + + /** + * LDAP 远程方法调用 + */ + public static final String LOOKUP_LDAP = "ldap:"; + + /** + * LDAPS 远程方法调用 + */ + public static final String LOOKUP_LDAPS = "ldaps:"; + + /** + * 自动识别json对象白名单配置(仅允许解析的包名,范围越小越安全) + */ + public static final String[] JSON_WHITELIST_STR = { "com.ruoyi" }; + + /** + * 定时任务白名单配置(仅允许访问的包名,如其他需要可以自行添加) + */ + public static final String[] JOB_WHITELIST_STR = { "com.ruoyi.quartz.task" }; + + /** + * 定时任务违规的字符 + */ + public static final String[] JOB_ERROR_STR = { "java.net.URL", "javax.naming.InitialContext", "org.yaml.snakeyaml", + "org.springframework", "org.apache", "com.ruoyi.common.utils.file", "com.ruoyi.common.config", "com.ruoyi.generator" }; + + /** + * 部门相关常量 + */ + public static class Dept + { + /** + * 全部数据权限 + */ + public static final String DATA_SCOPE_ALL = "1"; + + /** + * 自定数据权限 + */ + public static final String DATA_SCOPE_CUSTOM = "2"; + + /** + * 部门数据权限 + */ + public static final String DATA_SCOPE_DEPT = "3"; + + /** + * 部门及以下数据权限 + */ + public static final String DATA_SCOPE_DEPT_AND_CHILD = "4"; + + /** + * 仅本人数据权限 + */ + public static final String DATA_SCOPE_SELF = "5"; + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/constant/GenConstants.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/constant/GenConstants.java new file mode 100644 index 0000000..ea1e117 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/constant/GenConstants.java @@ -0,0 +1,120 @@ +package com.ruoyi.common.constant; + +/** + * 代码生成通用常量 + * + * @author ruoyi + */ +public class GenConstants +{ + /** 单表(增删改查) */ + public static final String TPL_CRUD = "crud"; + + /** 树表(增删改查) */ + public static final String TPL_TREE = "tree"; + + /** 主子表(增删改查) */ + public static final String TPL_SUB = "sub"; + + /** 树编码字段 */ + public static final String TREE_CODE = "treeCode"; + + /** 树父编码字段 */ + public static final String TREE_PARENT_CODE = "treeParentCode"; + + /** 树名称字段 */ + public static final String TREE_NAME = "treeName"; + + /** 上级菜单ID字段 */ + public static final String PARENT_MENU_ID = "parentMenuId"; + + /** 上级菜单名称字段 */ + public static final String PARENT_MENU_NAME = "parentMenuName"; + + /** 生成详情页开关 */ + public static final String GEN_VIEW = "genView"; + + /** 数据库字符串类型 */ + public static final String[] COLUMNTYPE_STR = { "char", "varchar", "nvarchar", "varchar2" }; + + /** 数据库文本类型 */ + public static final String[] COLUMNTYPE_TEXT = { "tinytext", "text", "mediumtext", "longtext" }; + + /** 数据库时间类型 */ + public static final String[] COLUMNTYPE_TIME = { "datetime", "time", "date", "timestamp" }; + + /** 数据库数字类型 */ + public static final String[] COLUMNTYPE_NUMBER = { "tinyint", "smallint", "mediumint", "int", "number", "integer", + "bit", "bigint", "float", "double", "decimal" }; + + /** 页面不需要编辑字段 */ + public static final String[] COLUMNNAME_NOT_EDIT = { "id", "create_by", "create_time", "del_flag" }; + + /** 页面不需要显示的列表字段 */ + public static final String[] COLUMNNAME_NOT_LIST = { "id", "create_by", "create_time", "del_flag", "update_by", + "update_time" }; + + /** 页面不需要查询字段 */ + public static final String[] COLUMNNAME_NOT_QUERY = { "id", "create_by", "create_time", "del_flag", "update_by", + "update_time", "remark" }; + + /** Entity基类字段 */ + public static final String[] BASE_ENTITY = { "createBy", "createTime", "updateBy", "updateTime", "remark" }; + + /** Tree基类字段 */ + public static final String[] TREE_ENTITY = { "parentName", "parentId", "orderNum", "ancestors", "children" }; + + /** 文本框 */ + public static final String HTML_INPUT = "input"; + + /** 文本域 */ + public static final String HTML_TEXTAREA = "textarea"; + + /** 下拉框 */ + public static final String HTML_SELECT = "select"; + + /** 单选框 */ + public static final String HTML_RADIO = "radio"; + + /** 复选框 */ + public static final String HTML_CHECKBOX = "checkbox"; + + /** 日期控件 */ + public static final String HTML_DATETIME = "datetime"; + + /** 图片上传控件 */ + public static final String HTML_IMAGE_UPLOAD = "imageUpload"; + + /** 文件上传控件 */ + public static final String HTML_FILE_UPLOAD = "fileUpload"; + + /** 富文本控件 */ + public static final String HTML_EDITOR = "editor"; + + /** 字符串类型 */ + public static final String TYPE_STRING = "String"; + + /** 整型 */ + public static final String TYPE_INTEGER = "Integer"; + + /** 长整型 */ + public static final String TYPE_LONG = "Long"; + + /** 浮点型 */ + public static final String TYPE_DOUBLE = "Double"; + + /** 高精度计算类型 */ + public static final String TYPE_BIGDECIMAL = "BigDecimal"; + + /** 时间类型 */ + public static final String TYPE_DATE = "Date"; + + /** 模糊查询 */ + public static final String QUERY_LIKE = "LIKE"; + + /** 相等查询 */ + public static final String QUERY_EQ = "EQ"; + + /** 需要 */ + public static final String REQUIRE = "1"; +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/constant/HttpStatus.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/constant/HttpStatus.java new file mode 100644 index 0000000..a983c77 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/constant/HttpStatus.java @@ -0,0 +1,94 @@ +package com.ruoyi.common.constant; + +/** + * 返回状态码 + * + * @author ruoyi + */ +public class HttpStatus +{ + /** + * 操作成功 + */ + public static final int SUCCESS = 200; + + /** + * 对象创建成功 + */ + public static final int CREATED = 201; + + /** + * 请求已经被接受 + */ + public static final int ACCEPTED = 202; + + /** + * 操作已经执行成功,但是没有返回数据 + */ + public static final int NO_CONTENT = 204; + + /** + * 资源已被移除 + */ + public static final int MOVED_PERM = 301; + + /** + * 重定向 + */ + public static final int SEE_OTHER = 303; + + /** + * 资源没有被修改 + */ + public static final int NOT_MODIFIED = 304; + + /** + * 参数列表错误(缺少,格式不匹配) + */ + public static final int BAD_REQUEST = 400; + + /** + * 未授权 + */ + public static final int UNAUTHORIZED = 401; + + /** + * 访问受限,授权过期 + */ + public static final int FORBIDDEN = 403; + + /** + * 资源,服务未找到 + */ + public static final int NOT_FOUND = 404; + + /** + * 不允许的http方法 + */ + public static final int BAD_METHOD = 405; + + /** + * 资源冲突,或者资源被锁 + */ + public static final int CONFLICT = 409; + + /** + * 不支持的数据,媒体类型 + */ + public static final int UNSUPPORTED_TYPE = 415; + + /** + * 系统内部错误 + */ + public static final int ERROR = 500; + + /** + * 接口未实现 + */ + public static final int NOT_IMPLEMENTED = 501; + + /** + * 系统警告消息 + */ + public static final int WARN = 601; +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/constant/ScheduleConstants.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/constant/ScheduleConstants.java new file mode 100644 index 0000000..62ad815 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/constant/ScheduleConstants.java @@ -0,0 +1,50 @@ +package com.ruoyi.common.constant; + +/** + * 任务调度通用常量 + * + * @author ruoyi + */ +public class ScheduleConstants +{ + public static final String TASK_CLASS_NAME = "TASK_CLASS_NAME"; + + /** 执行目标key */ + public static final String TASK_PROPERTIES = "TASK_PROPERTIES"; + + /** 默认 */ + public static final String MISFIRE_DEFAULT = "0"; + + /** 立即触发执行 */ + public static final String MISFIRE_IGNORE_MISFIRES = "1"; + + /** 触发一次执行 */ + public static final String MISFIRE_FIRE_AND_PROCEED = "2"; + + /** 不触发立即执行 */ + public static final String MISFIRE_DO_NOTHING = "3"; + + public enum Status + { + /** + * 正常 + */ + NORMAL("0"), + /** + * 暂停 + */ + PAUSE("1"); + + private String value; + + private Status(String value) + { + this.value = value; + } + + public String getValue() + { + return value; + } + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/constant/UserConstants.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/constant/UserConstants.java new file mode 100644 index 0000000..8dc7faa --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/constant/UserConstants.java @@ -0,0 +1,81 @@ +package com.ruoyi.common.constant; + +/** + * 用户常量信息 + * + * @author ruoyi + */ +public class UserConstants +{ + /** + * 平台内系统用户的唯一标志 + */ + public static final String SYS_USER = "SYS_USER"; + + /** 正常状态 */ + public static final String NORMAL = "0"; + + /** 异常状态 */ + public static final String EXCEPTION = "1"; + + /** 用户封禁状态 */ + public static final String USER_DISABLE = "1"; + + /** 角色正常状态 */ + public static final String ROLE_NORMAL = "0"; + + /** 角色封禁状态 */ + public static final String ROLE_DISABLE = "1"; + + /** 部门正常状态 */ + public static final String DEPT_NORMAL = "0"; + + /** 部门停用状态 */ + public static final String DEPT_DISABLE = "1"; + + /** 字典正常状态 */ + public static final String DICT_NORMAL = "0"; + + /** 是否为系统默认(是) */ + public static final String YES = "Y"; + + /** 是否菜单外链(是) */ + public static final String YES_FRAME = "0"; + + /** 是否菜单外链(否) */ + public static final String NO_FRAME = "1"; + + /** 菜单类型(目录) */ + public static final String TYPE_DIR = "M"; + + /** 菜单类型(菜单) */ + public static final String TYPE_MENU = "C"; + + /** 菜单类型(按钮) */ + public static final String TYPE_BUTTON = "F"; + + /** Layout组件标识 */ + public final static String LAYOUT = "Layout"; + + /** ParentView组件标识 */ + public final static String PARENT_VIEW = "ParentView"; + + /** InnerLink组件标识 */ + public final static String INNER_LINK = "InnerLink"; + + /** 校验是否唯一的返回标识 */ + public final static boolean UNIQUE = true; + public final static boolean NOT_UNIQUE = false; + + /** + * 用户名长度限制 + */ + public static final int USERNAME_MIN_LENGTH = 2; + public static final int USERNAME_MAX_LENGTH = 20; + + /** + * 密码长度限制 + */ + public static final int PASSWORD_MIN_LENGTH = 5; + public static final int PASSWORD_MAX_LENGTH = 20; +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/controller/BaseController.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/controller/BaseController.java new file mode 100644 index 0000000..a685e06 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/controller/BaseController.java @@ -0,0 +1,202 @@ +package com.ruoyi.common.core.controller; + +import java.beans.PropertyEditorSupport; +import java.util.Date; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.InitBinder; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.ruoyi.common.constant.HttpStatus; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.core.page.PageDomain; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.core.page.TableSupport; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.PageUtils; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.sql.SqlUtil; + +/** + * web层通用数据处理 + * + * @author ruoyi + */ +public class BaseController +{ + protected final Logger logger = LoggerFactory.getLogger(this.getClass()); + + /** + * 将前台传递过来的日期格式的字符串,自动转化为Date类型 + */ + @InitBinder + public void initBinder(WebDataBinder binder) + { + // Date 类型转换 + binder.registerCustomEditor(Date.class, new PropertyEditorSupport() + { + @Override + public void setAsText(String text) + { + setValue(DateUtils.parseDate(text)); + } + }); + } + + /** + * 设置请求分页数据 + */ + protected void startPage() + { + PageUtils.startPage(); + } + + /** + * 设置请求排序数据 + */ + protected void startOrderBy() + { + PageDomain pageDomain = TableSupport.buildPageRequest(); + if (StringUtils.isNotEmpty(pageDomain.getOrderBy())) + { + String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy()); + PageHelper.orderBy(orderBy); + } + } + + /** + * 清理分页的线程变量 + */ + protected void clearPage() + { + PageUtils.clearPage(); + } + + /** + * 响应请求分页数据 + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + protected TableDataInfo getDataTable(List list) + { + TableDataInfo rspData = new TableDataInfo(); + rspData.setCode(HttpStatus.SUCCESS); + rspData.setMsg("查询成功"); + rspData.setRows(list); + rspData.setTotal(new PageInfo(list).getTotal()); + return rspData; + } + + /** + * 返回成功 + */ + public AjaxResult success() + { + return AjaxResult.success(); + } + + /** + * 返回失败消息 + */ + public AjaxResult error() + { + return AjaxResult.error(); + } + + /** + * 返回成功消息 + */ + public AjaxResult success(String message) + { + return AjaxResult.success(message); + } + + /** + * 返回成功消息 + */ + public AjaxResult success(Object data) + { + return AjaxResult.success(data); + } + + /** + * 返回失败消息 + */ + public AjaxResult error(String message) + { + return AjaxResult.error(message); + } + + /** + * 返回警告消息 + */ + public AjaxResult warn(String message) + { + return AjaxResult.warn(message); + } + + /** + * 响应返回结果 + * + * @param rows 影响行数 + * @return 操作结果 + */ + protected AjaxResult toAjax(int rows) + { + return rows > 0 ? AjaxResult.success() : AjaxResult.error(); + } + + /** + * 响应返回结果 + * + * @param result 结果 + * @return 操作结果 + */ + protected AjaxResult toAjax(boolean result) + { + return result ? success() : error(); + } + + /** + * 页面跳转 + */ + public String redirect(String url) + { + return StringUtils.format("redirect:{}", url); + } + + /** + * 获取用户缓存信息 + */ + public LoginUser getLoginUser() + { + return SecurityUtils.getLoginUser(); + } + + /** + * 获取登录用户id + */ + public Long getUserId() + { + return getLoginUser().getUserId(); + } + + /** + * 获取登录部门id + */ + public Long getDeptId() + { + return getLoginUser().getDeptId(); + } + + /** + * 获取登录用户名 + */ + public String getUsername() + { + return getLoginUser().getUsername(); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/AjaxResult.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/AjaxResult.java new file mode 100644 index 0000000..a7abfe4 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/AjaxResult.java @@ -0,0 +1,216 @@ +package com.ruoyi.common.core.domain; + +import java.util.HashMap; +import java.util.Objects; +import com.ruoyi.common.constant.HttpStatus; +import com.ruoyi.common.utils.StringUtils; + +/** + * 操作消息提醒 + * + * @author ruoyi + */ +public class AjaxResult extends HashMap +{ + private static final long serialVersionUID = 1L; + + /** 状态码 */ + public static final String CODE_TAG = "code"; + + /** 返回内容 */ + public static final String MSG_TAG = "msg"; + + /** 数据对象 */ + public static final String DATA_TAG = "data"; + + /** + * 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。 + */ + public AjaxResult() + { + } + + /** + * 初始化一个新创建的 AjaxResult 对象 + * + * @param code 状态码 + * @param msg 返回内容 + */ + public AjaxResult(int code, String msg) + { + super.put(CODE_TAG, code); + super.put(MSG_TAG, msg); + } + + /** + * 初始化一个新创建的 AjaxResult 对象 + * + * @param code 状态码 + * @param msg 返回内容 + * @param data 数据对象 + */ + public AjaxResult(int code, String msg, Object data) + { + super.put(CODE_TAG, code); + super.put(MSG_TAG, msg); + if (StringUtils.isNotNull(data)) + { + super.put(DATA_TAG, data); + } + } + + /** + * 返回成功消息 + * + * @return 成功消息 + */ + public static AjaxResult success() + { + return AjaxResult.success("操作成功"); + } + + /** + * 返回成功数据 + * + * @return 成功消息 + */ + public static AjaxResult success(Object data) + { + return AjaxResult.success("操作成功", data); + } + + /** + * 返回成功消息 + * + * @param msg 返回内容 + * @return 成功消息 + */ + public static AjaxResult success(String msg) + { + return AjaxResult.success(msg, null); + } + + /** + * 返回成功消息 + * + * @param msg 返回内容 + * @param data 数据对象 + * @return 成功消息 + */ + public static AjaxResult success(String msg, Object data) + { + return new AjaxResult(HttpStatus.SUCCESS, msg, data); + } + + /** + * 返回警告消息 + * + * @param msg 返回内容 + * @return 警告消息 + */ + public static AjaxResult warn(String msg) + { + return AjaxResult.warn(msg, null); + } + + /** + * 返回警告消息 + * + * @param msg 返回内容 + * @param data 数据对象 + * @return 警告消息 + */ + public static AjaxResult warn(String msg, Object data) + { + return new AjaxResult(HttpStatus.WARN, msg, data); + } + + /** + * 返回错误消息 + * + * @return 错误消息 + */ + public static AjaxResult error() + { + return AjaxResult.error("操作失败"); + } + + /** + * 返回错误消息 + * + * @param msg 返回内容 + * @return 错误消息 + */ + public static AjaxResult error(String msg) + { + return AjaxResult.error(msg, null); + } + + /** + * 返回错误消息 + * + * @param msg 返回内容 + * @param data 数据对象 + * @return 错误消息 + */ + public static AjaxResult error(String msg, Object data) + { + return new AjaxResult(HttpStatus.ERROR, msg, data); + } + + /** + * 返回错误消息 + * + * @param code 状态码 + * @param msg 返回内容 + * @return 错误消息 + */ + public static AjaxResult error(int code, String msg) + { + return new AjaxResult(code, msg, null); + } + + /** + * 是否为成功消息 + * + * @return 结果 + */ + public boolean isSuccess() + { + return Objects.equals(HttpStatus.SUCCESS, this.get(CODE_TAG)); + } + + /** + * 是否为警告消息 + * + * @return 结果 + */ + public boolean isWarn() + { + return Objects.equals(HttpStatus.WARN, this.get(CODE_TAG)); + } + + /** + * 是否为错误消息 + * + * @return 结果 + */ + public boolean isError() + { + return Objects.equals(HttpStatus.ERROR, this.get(CODE_TAG)); + } + + /** + * 方便链式调用 + * + * @param key 键 + * @param value 值 + * @return 数据对象 + */ + @Override + public AjaxResult put(String key, Object value) + { + super.put(key, value); + return this; + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/BaseEntity.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/BaseEntity.java new file mode 100644 index 0000000..15bf66b --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/BaseEntity.java @@ -0,0 +1,118 @@ +package com.ruoyi.common.core.domain; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Entity基类 + * + * @author ruoyi + */ +public class BaseEntity implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 搜索值 */ + @JsonIgnore + private String searchValue; + + /** 创建者 */ + private String createBy; + + /** 创建时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** 更新者 */ + private String updateBy; + + /** 更新时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + /** 备注 */ + private String remark; + + /** 请求参数 */ + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private Map params; + + public String getSearchValue() + { + return searchValue; + } + + public void setSearchValue(String searchValue) + { + this.searchValue = searchValue; + } + + public String getCreateBy() + { + return createBy; + } + + public void setCreateBy(String createBy) + { + this.createBy = createBy; + } + + public Date getCreateTime() + { + return createTime; + } + + public void setCreateTime(Date createTime) + { + this.createTime = createTime; + } + + public String getUpdateBy() + { + return updateBy; + } + + public void setUpdateBy(String updateBy) + { + this.updateBy = updateBy; + } + + public Date getUpdateTime() + { + return updateTime; + } + + public void setUpdateTime(Date updateTime) + { + this.updateTime = updateTime; + } + + public String getRemark() + { + return remark; + } + + public void setRemark(String remark) + { + this.remark = remark; + } + + public Map getParams() + { + if (params == null) + { + params = new HashMap<>(); + } + return params; + } + + public void setParams(Map params) + { + this.params = params; + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/R.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/R.java new file mode 100644 index 0000000..ef15802 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/R.java @@ -0,0 +1,115 @@ +package com.ruoyi.common.core.domain; + +import java.io.Serializable; +import com.ruoyi.common.constant.HttpStatus; + +/** + * 响应信息主体 + * + * @author ruoyi + */ +public class R implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 成功 */ + public static final int SUCCESS = HttpStatus.SUCCESS; + + /** 失败 */ + public static final int FAIL = HttpStatus.ERROR; + + private int code; + + private String msg; + + private T data; + + public static R ok() + { + return restResult(null, SUCCESS, "操作成功"); + } + + public static R ok(T data) + { + return restResult(data, SUCCESS, "操作成功"); + } + + public static R ok(T data, String msg) + { + return restResult(data, SUCCESS, msg); + } + + public static R fail() + { + return restResult(null, FAIL, "操作失败"); + } + + public static R fail(String msg) + { + return restResult(null, FAIL, msg); + } + + public static R fail(T data) + { + return restResult(data, FAIL, "操作失败"); + } + + public static R fail(T data, String msg) + { + return restResult(data, FAIL, msg); + } + + public static R fail(int code, String msg) + { + return restResult(null, code, msg); + } + + private static R restResult(T data, int code, String msg) + { + R apiResult = new R<>(); + apiResult.setCode(code); + apiResult.setData(data); + apiResult.setMsg(msg); + return apiResult; + } + + public int getCode() + { + return code; + } + + public void setCode(int code) + { + this.code = code; + } + + public String getMsg() + { + return msg; + } + + public void setMsg(String msg) + { + this.msg = msg; + } + + public T getData() + { + return data; + } + + public void setData(T data) + { + this.data = data; + } + + public static Boolean isError(R ret) + { + return !isSuccess(ret); + } + + public static Boolean isSuccess(R ret) + { + return R.SUCCESS == ret.getCode(); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/TreeEntity.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/TreeEntity.java new file mode 100644 index 0000000..a180a18 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/TreeEntity.java @@ -0,0 +1,79 @@ +package com.ruoyi.common.core.domain; + +import java.util.ArrayList; +import java.util.List; + +/** + * Tree基类 + * + * @author ruoyi + */ +public class TreeEntity extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 父菜单名称 */ + private String parentName; + + /** 父菜单ID */ + private Long parentId; + + /** 显示顺序 */ + private Integer orderNum; + + /** 祖级列表 */ + private String ancestors; + + /** 子部门 */ + private List children = new ArrayList<>(); + + public String getParentName() + { + return parentName; + } + + public void setParentName(String parentName) + { + this.parentName = parentName; + } + + public Long getParentId() + { + return parentId; + } + + public void setParentId(Long parentId) + { + this.parentId = parentId; + } + + public Integer getOrderNum() + { + return orderNum; + } + + public void setOrderNum(Integer orderNum) + { + this.orderNum = orderNum; + } + + public String getAncestors() + { + return ancestors; + } + + public void setAncestors(String ancestors) + { + this.ancestors = ancestors; + } + + public List getChildren() + { + return children; + } + + public void setChildren(List children) + { + this.children = children; + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/TreeSelect.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/TreeSelect.java new file mode 100644 index 0000000..ae25df2 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/TreeSelect.java @@ -0,0 +1,93 @@ +package com.ruoyi.common.core.domain; + +import java.io.Serializable; +import java.util.List; +import java.util.stream.Collectors; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.entity.SysDept; +import com.ruoyi.common.core.domain.entity.SysMenu; +import com.ruoyi.common.utils.StringUtils; + +/** + * Treeselect树结构实体类 + * + * @author ruoyi + */ +public class TreeSelect implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 节点ID */ + private Long id; + + /** 节点名称 */ + private String label; + + /** 节点禁用 */ + private boolean disabled = false; + + /** 子节点 */ + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private List children; + + public TreeSelect() + { + + } + + public TreeSelect(SysDept dept) + { + this.id = dept.getDeptId(); + this.label = dept.getDeptName(); + this.disabled = StringUtils.equals(UserConstants.DEPT_DISABLE, dept.getStatus()); + this.children = dept.getChildren().stream().map(TreeSelect::new).collect(Collectors.toList()); + } + + public TreeSelect(SysMenu menu) + { + this.id = menu.getMenuId(); + this.label = menu.getMenuName(); + this.children = menu.getChildren().stream().map(TreeSelect::new).collect(Collectors.toList()); + } + + public Long getId() + { + return id; + } + + public void setId(Long id) + { + this.id = id; + } + + public String getLabel() + { + return label; + } + + public void setLabel(String label) + { + this.label = label; + } + + public boolean isDisabled() + { + return disabled; + } + + public void setDisabled(boolean disabled) + { + this.disabled = disabled; + } + + public List getChildren() + { + return children; + } + + public void setChildren(List children) + { + this.children = children; + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDept.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDept.java new file mode 100644 index 0000000..37236db --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDept.java @@ -0,0 +1,203 @@ +package com.ruoyi.common.core.domain.entity; + +import java.util.ArrayList; +import java.util.List; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 部门表 sys_dept + * + * @author ruoyi + */ +public class SysDept extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 部门ID */ + private Long deptId; + + /** 父部门ID */ + private Long parentId; + + /** 祖级列表 */ + private String ancestors; + + /** 部门名称 */ + private String deptName; + + /** 显示顺序 */ + private Integer orderNum; + + /** 负责人 */ + private String leader; + + /** 联系电话 */ + private String phone; + + /** 邮箱 */ + private String email; + + /** 部门状态:0正常,1停用 */ + private String status; + + /** 删除标志(0代表存在 2代表删除) */ + private String delFlag; + + /** 父部门名称 */ + private String parentName; + + /** 子部门 */ + private List children = new ArrayList(); + + public Long getDeptId() + { + return deptId; + } + + public void setDeptId(Long deptId) + { + this.deptId = deptId; + } + + public Long getParentId() + { + return parentId; + } + + public void setParentId(Long parentId) + { + this.parentId = parentId; + } + + public String getAncestors() + { + return ancestors; + } + + public void setAncestors(String ancestors) + { + this.ancestors = ancestors; + } + + @NotBlank(message = "部门名称不能为空") + @Size(min = 0, max = 30, message = "部门名称长度不能超过30个字符") + public String getDeptName() + { + return deptName; + } + + public void setDeptName(String deptName) + { + this.deptName = deptName; + } + + @NotNull(message = "显示顺序不能为空") + public Integer getOrderNum() + { + return orderNum; + } + + public void setOrderNum(Integer orderNum) + { + this.orderNum = orderNum; + } + + public String getLeader() + { + return leader; + } + + public void setLeader(String leader) + { + this.leader = leader; + } + + @Size(min = 0, max = 11, message = "联系电话长度不能超过11个字符") + public String getPhone() + { + return phone; + } + + public void setPhone(String phone) + { + this.phone = phone; + } + + @Email(message = "邮箱格式不正确") + @Size(min = 0, max = 50, message = "邮箱长度不能超过50个字符") + public String getEmail() + { + return email; + } + + public void setEmail(String email) + { + this.email = email; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + public String getDelFlag() + { + return delFlag; + } + + public void setDelFlag(String delFlag) + { + this.delFlag = delFlag; + } + + public String getParentName() + { + return parentName; + } + + public void setParentName(String parentName) + { + this.parentName = parentName; + } + + public List getChildren() + { + return children; + } + + public void setChildren(List children) + { + this.children = children; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("deptId", getDeptId()) + .append("parentId", getParentId()) + .append("ancestors", getAncestors()) + .append("deptName", getDeptName()) + .append("orderNum", getOrderNum()) + .append("leader", getLeader()) + .append("phone", getPhone()) + .append("email", getEmail()) + .append("status", getStatus()) + .append("delFlag", getDelFlag()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .toString(); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictData.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictData.java new file mode 100644 index 0000000..6b035a6 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictData.java @@ -0,0 +1,176 @@ +package com.ruoyi.common.core.domain.entity; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.annotation.Excel.ColumnType; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 字典数据表 sys_dict_data + * + * @author ruoyi + */ +public class SysDictData extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 字典编码 */ + @Excel(name = "字典编码", cellType = ColumnType.NUMERIC) + private Long dictCode; + + /** 字典排序 */ + @Excel(name = "字典排序", cellType = ColumnType.NUMERIC) + private Long dictSort; + + /** 字典标签 */ + @Excel(name = "字典标签") + private String dictLabel; + + /** 字典键值 */ + @Excel(name = "字典键值") + private String dictValue; + + /** 字典类型 */ + @Excel(name = "字典类型") + private String dictType; + + /** 样式属性(其他样式扩展) */ + private String cssClass; + + /** 表格字典样式 */ + private String listClass; + + /** 是否默认(Y是 N否) */ + @Excel(name = "是否默认", readConverterExp = "Y=是,N=否") + private String isDefault; + + /** 状态(0正常 1停用) */ + @Excel(name = "状态", readConverterExp = "0=正常,1=停用") + private String status; + + public Long getDictCode() + { + return dictCode; + } + + public void setDictCode(Long dictCode) + { + this.dictCode = dictCode; + } + + public Long getDictSort() + { + return dictSort; + } + + public void setDictSort(Long dictSort) + { + this.dictSort = dictSort; + } + + @NotBlank(message = "字典标签不能为空") + @Size(min = 0, max = 100, message = "字典标签长度不能超过100个字符") + public String getDictLabel() + { + return dictLabel; + } + + public void setDictLabel(String dictLabel) + { + this.dictLabel = dictLabel; + } + + @NotBlank(message = "字典键值不能为空") + @Size(min = 0, max = 100, message = "字典键值长度不能超过100个字符") + public String getDictValue() + { + return dictValue; + } + + public void setDictValue(String dictValue) + { + this.dictValue = dictValue; + } + + @NotBlank(message = "字典类型不能为空") + @Size(min = 0, max = 100, message = "字典类型长度不能超过100个字符") + public String getDictType() + { + return dictType; + } + + public void setDictType(String dictType) + { + this.dictType = dictType; + } + + @Size(min = 0, max = 100, message = "样式属性长度不能超过100个字符") + public String getCssClass() + { + return cssClass; + } + + public void setCssClass(String cssClass) + { + this.cssClass = cssClass; + } + + public String getListClass() + { + return listClass; + } + + public void setListClass(String listClass) + { + this.listClass = listClass; + } + + public boolean getDefault() + { + return UserConstants.YES.equals(this.isDefault); + } + + public String getIsDefault() + { + return isDefault; + } + + public void setIsDefault(String isDefault) + { + this.isDefault = isDefault; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("dictCode", getDictCode()) + .append("dictSort", getDictSort()) + .append("dictLabel", getDictLabel()) + .append("dictValue", getDictValue()) + .append("dictType", getDictType()) + .append("cssClass", getCssClass()) + .append("listClass", getListClass()) + .append("isDefault", getIsDefault()) + .append("status", getStatus()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictType.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictType.java new file mode 100644 index 0000000..ac5d290 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictType.java @@ -0,0 +1,96 @@ +package com.ruoyi.common.core.domain.entity; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.annotation.Excel.ColumnType; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 字典类型表 sys_dict_type + * + * @author ruoyi + */ +public class SysDictType extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 字典主键 */ + @Excel(name = "字典主键", cellType = ColumnType.NUMERIC) + private Long dictId; + + /** 字典名称 */ + @Excel(name = "字典名称") + private String dictName; + + /** 字典类型 */ + @Excel(name = "字典类型") + private String dictType; + + /** 状态(0正常 1停用) */ + @Excel(name = "状态", readConverterExp = "0=正常,1=停用") + private String status; + + public Long getDictId() + { + return dictId; + } + + public void setDictId(Long dictId) + { + this.dictId = dictId; + } + + @NotBlank(message = "字典名称不能为空") + @Size(min = 0, max = 100, message = "字典类型名称长度不能超过100个字符") + public String getDictName() + { + return dictName; + } + + public void setDictName(String dictName) + { + this.dictName = dictName; + } + + @NotBlank(message = "字典类型不能为空") + @Size(min = 0, max = 100, message = "字典类型类型长度不能超过100个字符") + @Pattern(regexp = "^[a-z][a-z0-9_]*$", message = "字典类型必须以字母开头,且只能为(小写字母,数字,下滑线)") + public String getDictType() + { + return dictType; + } + + public void setDictType(String dictType) + { + this.dictType = dictType; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("dictId", getDictId()) + .append("dictName", getDictName()) + .append("dictType", getDictType()) + .append("status", getStatus()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysMenu.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysMenu.java new file mode 100644 index 0000000..d4ab762 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysMenu.java @@ -0,0 +1,274 @@ +package com.ruoyi.common.core.domain.entity; + +import java.util.ArrayList; +import java.util.List; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 菜单权限表 sys_menu + * + * @author ruoyi + */ +public class SysMenu extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 菜单ID */ + private Long menuId; + + /** 菜单名称 */ + private String menuName; + + /** 父菜单名称 */ + private String parentName; + + /** 父菜单ID */ + private Long parentId; + + /** 显示顺序 */ + private Integer orderNum; + + /** 路由地址 */ + private String path; + + /** 组件路径 */ + private String component; + + /** 路由参数 */ + private String query; + + /** 路由名称,默认和路由地址相同的驼峰格式(注意:因为vue3版本的router会删除名称相同路由,为避免名字的冲突,特殊情况可以自定义) */ + private String routeName; + + /** 是否为外链(0是 1否) */ + private String isFrame; + + /** 是否缓存(0缓存 1不缓存) */ + private String isCache; + + /** 类型(M目录 C菜单 F按钮) */ + private String menuType; + + /** 显示状态(0显示 1隐藏) */ + private String visible; + + /** 菜单状态(0正常 1停用) */ + private String status; + + /** 权限字符串 */ + private String perms; + + /** 菜单图标 */ + private String icon; + + /** 子菜单 */ + private List children = new ArrayList(); + + public Long getMenuId() + { + return menuId; + } + + public void setMenuId(Long menuId) + { + this.menuId = menuId; + } + + @NotBlank(message = "菜单名称不能为空") + @Size(min = 0, max = 50, message = "菜单名称长度不能超过50个字符") + public String getMenuName() + { + return menuName; + } + + public void setMenuName(String menuName) + { + this.menuName = menuName; + } + + public String getParentName() + { + return parentName; + } + + public void setParentName(String parentName) + { + this.parentName = parentName; + } + + public Long getParentId() + { + return parentId; + } + + public void setParentId(Long parentId) + { + this.parentId = parentId; + } + + @NotNull(message = "显示顺序不能为空") + public Integer getOrderNum() + { + return orderNum; + } + + public void setOrderNum(Integer orderNum) + { + this.orderNum = orderNum; + } + + @Size(min = 0, max = 200, message = "路由地址不能超过200个字符") + public String getPath() + { + return path; + } + + public void setPath(String path) + { + this.path = path; + } + + @Size(min = 0, max = 200, message = "组件路径不能超过255个字符") + public String getComponent() + { + return component; + } + + public void setComponent(String component) + { + this.component = component; + } + + public String getQuery() + { + return query; + } + + public void setQuery(String query) + { + this.query = query; + } + + public String getRouteName() + { + return routeName; + } + + public void setRouteName(String routeName) + { + this.routeName = routeName; + } + + public String getIsFrame() + { + return isFrame; + } + + public void setIsFrame(String isFrame) + { + this.isFrame = isFrame; + } + + public String getIsCache() + { + return isCache; + } + + public void setIsCache(String isCache) + { + this.isCache = isCache; + } + + @NotBlank(message = "菜单类型不能为空") + public String getMenuType() + { + return menuType; + } + + public void setMenuType(String menuType) + { + this.menuType = menuType; + } + + public String getVisible() + { + return visible; + } + + public void setVisible(String visible) + { + this.visible = visible; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + @Size(min = 0, max = 100, message = "权限标识长度不能超过100个字符") + public String getPerms() + { + return perms; + } + + public void setPerms(String perms) + { + this.perms = perms; + } + + public String getIcon() + { + return icon; + } + + public void setIcon(String icon) + { + this.icon = icon; + } + + public List getChildren() + { + return children; + } + + public void setChildren(List children) + { + this.children = children; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("menuId", getMenuId()) + .append("menuName", getMenuName()) + .append("parentId", getParentId()) + .append("orderNum", getOrderNum()) + .append("path", getPath()) + .append("component", getComponent()) + .append("query", getQuery()) + .append("routeName", getRouteName()) + .append("isFrame", getIsFrame()) + .append("IsCache", getIsCache()) + .append("menuType", getMenuType()) + .append("visible", getVisible()) + .append("status ", getStatus()) + .append("perms", getPerms()) + .append("icon", getIcon()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysRole.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysRole.java new file mode 100644 index 0000000..2f8ed14 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysRole.java @@ -0,0 +1,241 @@ +package com.ruoyi.common.core.domain.entity; + +import java.util.Set; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.annotation.Excel.ColumnType; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 角色表 sys_role + * + * @author ruoyi + */ +public class SysRole extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 角色ID */ + @Excel(name = "角色序号", cellType = ColumnType.NUMERIC) + private Long roleId; + + /** 角色名称 */ + @Excel(name = "角色名称") + private String roleName; + + /** 角色权限 */ + @Excel(name = "角色权限") + private String roleKey; + + /** 角色排序 */ + @Excel(name = "角色排序") + private Integer roleSort; + + /** 数据范围(1:所有数据权限;2:自定义数据权限;3:本部门数据权限;4:本部门及以下数据权限;5:仅本人数据权限) */ + @Excel(name = "数据范围", readConverterExp = "1=所有数据权限,2=自定义数据权限,3=本部门数据权限,4=本部门及以下数据权限,5=仅本人数据权限") + private String dataScope; + + /** 菜单树选择项是否关联显示( 0:父子不互相关联显示 1:父子互相关联显示) */ + private boolean menuCheckStrictly; + + /** 部门树选择项是否关联显示(0:父子不互相关联显示 1:父子互相关联显示 ) */ + private boolean deptCheckStrictly; + + /** 角色状态(0正常 1停用) */ + @Excel(name = "角色状态", readConverterExp = "0=正常,1=停用") + private String status; + + /** 删除标志(0代表存在 2代表删除) */ + private String delFlag; + + /** 用户是否存在此角色标识 默认不存在 */ + private boolean flag = false; + + /** 菜单组 */ + private Long[] menuIds; + + /** 部门组(数据权限) */ + private Long[] deptIds; + + /** 角色菜单权限 */ + private Set permissions; + + public SysRole() + { + + } + + public SysRole(Long roleId) + { + this.roleId = roleId; + } + + public Long getRoleId() + { + return roleId; + } + + public void setRoleId(Long roleId) + { + this.roleId = roleId; + } + + public boolean isAdmin() + { + return isAdmin(this.roleId); + } + + public static boolean isAdmin(Long roleId) + { + return roleId != null && 1L == roleId; + } + + @NotBlank(message = "角色名称不能为空") + @Size(min = 0, max = 30, message = "角色名称长度不能超过30个字符") + public String getRoleName() + { + return roleName; + } + + public void setRoleName(String roleName) + { + this.roleName = roleName; + } + + @NotBlank(message = "权限字符不能为空") + @Size(min = 0, max = 100, message = "权限字符长度不能超过100个字符") + public String getRoleKey() + { + return roleKey; + } + + public void setRoleKey(String roleKey) + { + this.roleKey = roleKey; + } + + @NotNull(message = "显示顺序不能为空") + public Integer getRoleSort() + { + return roleSort; + } + + public void setRoleSort(Integer roleSort) + { + this.roleSort = roleSort; + } + + public String getDataScope() + { + return dataScope; + } + + public void setDataScope(String dataScope) + { + this.dataScope = dataScope; + } + + public boolean isMenuCheckStrictly() + { + return menuCheckStrictly; + } + + public void setMenuCheckStrictly(boolean menuCheckStrictly) + { + this.menuCheckStrictly = menuCheckStrictly; + } + + public boolean isDeptCheckStrictly() + { + return deptCheckStrictly; + } + + public void setDeptCheckStrictly(boolean deptCheckStrictly) + { + this.deptCheckStrictly = deptCheckStrictly; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + public String getDelFlag() + { + return delFlag; + } + + public void setDelFlag(String delFlag) + { + this.delFlag = delFlag; + } + + public boolean isFlag() + { + return flag; + } + + public void setFlag(boolean flag) + { + this.flag = flag; + } + + public Long[] getMenuIds() + { + return menuIds; + } + + public void setMenuIds(Long[] menuIds) + { + this.menuIds = menuIds; + } + + public Long[] getDeptIds() + { + return deptIds; + } + + public void setDeptIds(Long[] deptIds) + { + this.deptIds = deptIds; + } + + public Set getPermissions() + { + return permissions; + } + + public void setPermissions(Set permissions) + { + this.permissions = permissions; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("roleId", getRoleId()) + .append("roleName", getRoleName()) + .append("roleKey", getRoleKey()) + .append("roleSort", getRoleSort()) + .append("dataScope", getDataScope()) + .append("menuCheckStrictly", isMenuCheckStrictly()) + .append("deptCheckStrictly", isDeptCheckStrictly()) + .append("status", getStatus()) + .append("delFlag", getDelFlag()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java new file mode 100644 index 0000000..b4418fa --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java @@ -0,0 +1,307 @@ +package com.ruoyi.common.core.domain.entity; +import java.util.Date; +import java.util.List; +import jakarta.validation.constraints.*; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.annotation.Excel.ColumnType; +import com.ruoyi.common.annotation.Excel.Type; +import com.ruoyi.common.annotation.Excels; +import com.ruoyi.common.core.domain.BaseEntity; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.xss.Xss; +/** +* 用户对象 sys_user +* +* @author ruoyi +*/ +public class SysUser extends BaseEntity +{ + private static final long serialVersionUID = 1L; + /** 用户ID */ + @Excel(name = "用户序号", type = Type.EXPORT, cellType = ColumnType.NUMERIC, prompt = "用户编号") + private Long userId; + /** 部门ID */ + @Excel(name = "部门编号", type = Type.IMPORT) + private Long deptId; + /** 用户账号 */ + @Excel(name = "登录名称") + private String userName; + /** 用户昵称 */ + @Excel(name = "用户名称") + private String nickName; + /** 用户邮箱 */ + @Excel(name = "用户邮箱") + private String email; + /** 手机号码 */ + @Excel(name = "手机号码", cellType = ColumnType.TEXT) + private String phonenumber; + /** 用户性别 */ + @Excel(name = "用户性别", readConverterExp = "0=男,1=女,2=未知") + private String sex; + /** 用户头像 */ + private String avatar; + + /** 密码 */ + private String password; + + /** 账号状态(0正常 1停用) */ + @Excel(name = "账号状态", readConverterExp = "0=正常,1=停用") + private String status; + + /** 删除标志(0代表存在 2代表删除) */ + private String delFlag; + + /** 账号类型 (MAIN=主账号 / SUB=子账号) */ + @Excel(name = "账号类型") + private String accountType; + + /** 主账号ID (子账号指向其主账号的 user_id, 主账号为 null) */ + @Excel(name = "主账号ID", type = Type.EXPORT) + private Long parentUserId; + + /** 最后登录IP */ + @Excel(name = "最后登录IP", type = Type.EXPORT) + private String loginIp; + /** 最后登录时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "最后登录时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss", type = Type.EXPORT) + private Date loginDate; + /** 密码最后更新时间 */ + private Date pwdUpdateDate; + /** 部门对象 */ + @Excels({ + @Excel(name = "部门名称", targetAttr = "deptName", type = Type.EXPORT), + @Excel(name = "部门负责人", targetAttr = "leader", type = Type.EXPORT) + }) + private SysDept dept; + /** 角色对象 */ + private List roles; + /** 角色组 */ + private Long[] roleIds; + /** 岗位组 */ + private Long[] postIds; + /** 角色ID */ + private Long roleId; + public SysUser() + { + } + public SysUser(Long userId) + { + this.userId = userId; + } + public Long getUserId() + { + return userId; + } + public void setUserId(Long userId) + { + this.userId = userId; + } + public boolean isAdmin() + { + return SecurityUtils.isAdmin(this.userId); + } + public Long getDeptId() + { + return deptId; + } + public void setDeptId(Long deptId) + { + this.deptId = deptId; + } + @Xss(message = "用户昵称不能包含脚本字符") + @Size(min = 0, max = 30, message = "用户昵称长度不能超过30个字符") + public String getNickName() + { + return nickName; + } + public void setNickName(String nickName) + { + this.nickName = nickName; + } + @Xss(message = "用户账号不能包含脚本字符") + @NotBlank(message = "用户账号不能为空") + @Size(min = 0, max = 30, message = "用户账号长度不能超过30个字符") + public String getUserName() + { + return userName; + } + public void setUserName(String userName) + { + this.userName = userName; + } + @Email(message = "邮箱格式不正确") + @Size(min = 0, max = 50, message = "邮箱长度不能超过50个字符") + public String getEmail() + { + return email; + } + public void setEmail(String email) + { + this.email = email; + } + @Size(min = 0, max = 11, message = "手机号码长度不能超过11个字符") + public String getPhonenumber() + { + return phonenumber; + } + public void setPhonenumber(String phonenumber) + { + this.phonenumber = phonenumber; + } + public String getSex() + { + return sex; + } + public void setSex(String sex) + { + this.sex = sex; + } + public String getAvatar() + { + return avatar; + } + public void setAvatar(String avatar) + { + this.avatar = avatar; + } + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + public String getPassword() + { + return password; + } + public void setPassword(String password) + { + this.password = password; + } + public String getStatus() + { + return status; + } + public void setStatus(String status) + { + this.status = status; + } + public String getAccountType() + { + return accountType; + } + + public void setAccountType(String accountType) + { + this.accountType = accountType; + } + + public Long getParentUserId() + { + return parentUserId; + } + + public void setParentUserId(Long parentUserId) + { + this.parentUserId = parentUserId; + } + + + public String getDelFlag() + { + return delFlag; + } + public void setDelFlag(String delFlag) + { + this.delFlag = delFlag; + } + public String getLoginIp() + { + return loginIp; + } + public void setLoginIp(String loginIp) + { + this.loginIp = loginIp; + } + public Date getLoginDate() + { + return loginDate; + } + public void setLoginDate(Date loginDate) + { + this.loginDate = loginDate; + } + public Date getPwdUpdateDate() + { + return pwdUpdateDate; + } + public void setPwdUpdateDate(Date pwdUpdateDate) + { + this.pwdUpdateDate = pwdUpdateDate; + } + public SysDept getDept() + { + return dept; + } + public void setDept(SysDept dept) + { + this.dept = dept; + } + public List getRoles() + { + return roles; + } + public void setRoles(List roles) + { + this.roles = roles; + } + public Long[] getRoleIds() + { + return roleIds; + } + public void setRoleIds(Long[] roleIds) + { + this.roleIds = roleIds; + } + public Long[] getPostIds() + { + return postIds; + } + public void setPostIds(Long[] postIds) + { + this.postIds = postIds; + } + public Long getRoleId() + { + return roleId; + } + public void setRoleId(Long roleId) + { + this.roleId = roleId; + } + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("userId", getUserId()) + .append("deptId", getDeptId()) + .append("userName", getUserName()) + .append("nickName", getNickName()) + .append("email", getEmail()) + .append("phonenumber", getPhonenumber()) + .append("sex", getSex()) + .append("avatar", getAvatar()) + .append("password", getPassword()) + .append("status", getStatus()) + .append("delFlag", getDelFlag()) + .append("loginIp", getLoginIp()) + .append("loginDate", getLoginDate()) + .append("pwdUpdateDate", getPwdUpdateDate()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .append("dept", getDept()) + .toString(); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginBody.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginBody.java new file mode 100644 index 0000000..b5bc8c8 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginBody.java @@ -0,0 +1,69 @@ +package com.ruoyi.common.core.domain.model; + +/** + * 用户登录对象 + * + * @author ruoyi + */ +public class LoginBody +{ + /** + * 用户名 + */ + private String username; + + /** + * 用户密码 + */ + private String password; + + /** + * 验证码 + */ + private String code; + + /** + * 唯一标识 + */ + private String uuid; + + public String getUsername() + { + return username; + } + + public void setUsername(String username) + { + this.username = username; + } + + public String getPassword() + { + return password; + } + + public void setPassword(String password) + { + this.password = password; + } + + public String getCode() + { + return code; + } + + public void setCode(String code) + { + this.code = code; + } + + public String getUuid() + { + return uuid; + } + + public void setUuid(String uuid) + { + this.uuid = uuid; + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUser.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUser.java new file mode 100644 index 0000000..e41ad56 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUser.java @@ -0,0 +1,278 @@ +package com.ruoyi.common.core.domain.model; + +import java.util.Collection; +import java.util.Collections; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import com.alibaba.fastjson2.annotation.JSONField; +import com.ruoyi.common.core.domain.entity.SysUser; + +/** + * 登录用户身份权限 + * + * @author ruoyi + */ +public class LoginUser implements UserDetails +{ + private static final long serialVersionUID = 1L; + + /** + * 用户ID + */ + private Long userId; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 用户唯一标识 + */ + private String token; + + /** + * 登录时间 + */ + private Long loginTime; + + /** + * 过期时间 + */ + private Long expireTime; + + /** + * 登录IP地址 + */ + private String ipaddr; + + /** + * 登录地点 + */ + private String loginLocation; + + /** + * 浏览器类型 + */ + private String browser; + + /** + * 操作系统 + */ + private String os; + + /** + * 权限列表 + */ + private Set permissions; + + /** + * 用户信息 + */ + private SysUser user; + + public LoginUser() + { + } + + public LoginUser(SysUser user, Set permissions) + { + this.user = user; + this.permissions = permissions; + } + + public LoginUser(Long userId, Long deptId, SysUser user, Set permissions) + { + this.userId = userId; + this.deptId = deptId; + this.user = user; + this.permissions = permissions; + } + + public Long getUserId() + { + return userId; + } + + public void setUserId(Long userId) + { + this.userId = userId; + } + + public Long getDeptId() + { + return deptId; + } + + public void setDeptId(Long deptId) + { + this.deptId = deptId; + } + + public String getToken() + { + return token; + } + + public void setToken(String token) + { + this.token = token; + } + + @JSONField(serialize = false) + @Override + public String getPassword() + { + return user.getPassword(); + } + + @Override + public String getUsername() + { + return user.getUserName(); + } + + /** + * 账户是否未过期,过期无法验证 + */ + @JSONField(serialize = false) + @Override + public boolean isAccountNonExpired() + { + return true; + } + + /** + * 指定用户是否解锁,锁定的用户无法进行身份验证 + * + * @return + */ + @JSONField(serialize = false) + @Override + public boolean isAccountNonLocked() + { + return true; + } + + /** + * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证 + * + * @return + */ + @JSONField(serialize = false) + @Override + public boolean isCredentialsNonExpired() + { + return true; + } + + /** + * 是否可用 ,禁用的用户不能身份验证 + * + * @return + */ + @JSONField(serialize = false) + @Override + public boolean isEnabled() + { + return true; + } + + public Long getLoginTime() + { + return loginTime; + } + + public void setLoginTime(Long loginTime) + { + this.loginTime = loginTime; + } + + public String getIpaddr() + { + return ipaddr; + } + + public void setIpaddr(String ipaddr) + { + this.ipaddr = ipaddr; + } + + public String getLoginLocation() + { + return loginLocation; + } + + public void setLoginLocation(String loginLocation) + { + this.loginLocation = loginLocation; + } + + public String getBrowser() + { + return browser; + } + + public void setBrowser(String browser) + { + this.browser = browser; + } + + public String getOs() + { + return os; + } + + public void setOs(String os) + { + this.os = os; + } + + public Long getExpireTime() + { + return expireTime; + } + + public void setExpireTime(Long expireTime) + { + this.expireTime = expireTime; + } + + public Set getPermissions() + { + return permissions; + } + + public void setPermissions(Set permissions) + { + this.permissions = permissions; + } + + public SysUser getUser() + { + return user; + } + + public void setUser(SysUser user) + { + this.user = user; + } + + @Override + @JSONField(serialize = false) + public Collection getAuthorities() + { + if (permissions == null || permissions.isEmpty()) + { + return Collections.emptyList(); + } + return permissions.stream() + .filter(Objects::nonNull) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/RegisterBody.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/RegisterBody.java new file mode 100644 index 0000000..868a1fc --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/RegisterBody.java @@ -0,0 +1,11 @@ +package com.ruoyi.common.core.domain.model; + +/** + * 用户注册对象 + * + * @author ruoyi + */ +public class RegisterBody extends LoginBody +{ + +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/page/PageDomain.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/page/PageDomain.java new file mode 100644 index 0000000..8966cb4 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/page/PageDomain.java @@ -0,0 +1,101 @@ +package com.ruoyi.common.core.page; + +import com.ruoyi.common.utils.StringUtils; + +/** + * 分页数据 + * + * @author ruoyi + */ +public class PageDomain +{ + /** 当前记录起始索引 */ + private Integer pageNum; + + /** 每页显示记录数 */ + private Integer pageSize; + + /** 排序列 */ + private String orderByColumn; + + /** 排序的方向desc或者asc */ + private String isAsc = "asc"; + + /** 分页参数合理化 */ + private Boolean reasonable = true; + + public String getOrderBy() + { + if (StringUtils.isEmpty(orderByColumn)) + { + return ""; + } + return StringUtils.toUnderScoreCase(orderByColumn) + " " + isAsc; + } + + public Integer getPageNum() + { + return pageNum; + } + + public void setPageNum(Integer pageNum) + { + this.pageNum = pageNum; + } + + public Integer getPageSize() + { + return pageSize; + } + + public void setPageSize(Integer pageSize) + { + this.pageSize = pageSize; + } + + public String getOrderByColumn() + { + return orderByColumn; + } + + public void setOrderByColumn(String orderByColumn) + { + this.orderByColumn = orderByColumn; + } + + public String getIsAsc() + { + return isAsc; + } + + public void setIsAsc(String isAsc) + { + if (StringUtils.isNotEmpty(isAsc)) + { + // 兼容前端排序类型 + if ("ascending".equals(isAsc)) + { + isAsc = "asc"; + } + else if ("descending".equals(isAsc)) + { + isAsc = "desc"; + } + this.isAsc = isAsc; + } + } + + public Boolean getReasonable() + { + if (StringUtils.isNull(reasonable)) + { + return Boolean.TRUE; + } + return reasonable; + } + + public void setReasonable(Boolean reasonable) + { + this.reasonable = reasonable; + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/page/TableDataInfo.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/page/TableDataInfo.java new file mode 100644 index 0000000..2fff93e --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/page/TableDataInfo.java @@ -0,0 +1,85 @@ +package com.ruoyi.common.core.page; + +import java.io.Serializable; +import java.util.List; + +/** + * 表格分页数据对象 + * + * @author ruoyi + */ +public class TableDataInfo implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 总记录数 */ + private long total; + + /** 列表数据 */ + private List rows; + + /** 消息状态码 */ + private int code; + + /** 消息内容 */ + private String msg; + + /** + * 表格数据对象 + */ + public TableDataInfo() + { + } + + /** + * 分页 + * + * @param list 列表数据 + * @param total 总记录数 + */ + public TableDataInfo(List list, long total) + { + this.rows = list; + this.total = total; + } + + public long getTotal() + { + return total; + } + + public void setTotal(long total) + { + this.total = total; + } + + public List getRows() + { + return rows; + } + + public void setRows(List rows) + { + this.rows = rows; + } + + public int getCode() + { + return code; + } + + public void setCode(int code) + { + this.code = code; + } + + public String getMsg() + { + return msg; + } + + public void setMsg(String msg) + { + this.msg = msg; + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/page/TableSupport.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/page/TableSupport.java new file mode 100644 index 0000000..a120c30 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/page/TableSupport.java @@ -0,0 +1,56 @@ +package com.ruoyi.common.core.page; + +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.utils.ServletUtils; + +/** + * 表格数据处理 + * + * @author ruoyi + */ +public class TableSupport +{ + /** + * 当前记录起始索引 + */ + public static final String PAGE_NUM = "pageNum"; + + /** + * 每页显示记录数 + */ + public static final String PAGE_SIZE = "pageSize"; + + /** + * 排序列 + */ + public static final String ORDER_BY_COLUMN = "orderByColumn"; + + /** + * 排序的方向 "desc" 或者 "asc". + */ + public static final String IS_ASC = "isAsc"; + + /** + * 分页参数合理化 + */ + public static final String REASONABLE = "reasonable"; + + /** + * 封装分页对象 + */ + public static PageDomain getPageDomain() + { + PageDomain pageDomain = new PageDomain(); + pageDomain.setPageNum(Convert.toInt(ServletUtils.getParameter(PAGE_NUM), 1)); + pageDomain.setPageSize(Convert.toInt(ServletUtils.getParameter(PAGE_SIZE), 10)); + pageDomain.setOrderByColumn(ServletUtils.getParameter(ORDER_BY_COLUMN)); + pageDomain.setIsAsc(ServletUtils.getParameter(IS_ASC)); + pageDomain.setReasonable(ServletUtils.getParameterToBool(REASONABLE)); + return pageDomain; + } + + public static PageDomain buildPageRequest() + { + return getPageDomain(); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/redis/RedisCache.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/redis/RedisCache.java new file mode 100644 index 0000000..44e80d8 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/redis/RedisCache.java @@ -0,0 +1,268 @@ +package com.ruoyi.common.core.redis; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.BoundSetOperations; +import org.springframework.data.redis.core.HashOperations; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Component; + +/** + * spring redis 工具类 + * + * @author ruoyi + **/ +@SuppressWarnings(value = { "unchecked", "rawtypes" }) +@Component +public class RedisCache +{ + @Autowired + public RedisTemplate redisTemplate; + + /** + * 缓存基本的对象,Integer、String、实体类等 + * + * @param key 缓存的键值 + * @param value 缓存的值 + */ + public void setCacheObject(final String key, final T value) + { + redisTemplate.opsForValue().set(key, value); + } + + /** + * 缓存基本的对象,Integer、String、实体类等 + * + * @param key 缓存的键值 + * @param value 缓存的值 + * @param timeout 时间 + * @param timeUnit 时间颗粒度 + */ + public void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) + { + redisTemplate.opsForValue().set(key, value, timeout, timeUnit); + } + + /** + * 设置有效时间 + * + * @param key Redis键 + * @param timeout 超时时间 + * @return true=设置成功;false=设置失败 + */ + public boolean expire(final String key, final long timeout) + { + return expire(key, timeout, TimeUnit.SECONDS); + } + + /** + * 设置有效时间 + * + * @param key Redis键 + * @param timeout 超时时间 + * @param unit 时间单位 + * @return true=设置成功;false=设置失败 + */ + public boolean expire(final String key, final long timeout, final TimeUnit unit) + { + return redisTemplate.expire(key, timeout, unit); + } + + /** + * 获取有效时间 + * + * @param key Redis键 + * @return 有效时间 + */ + public long getExpire(final String key) + { + return redisTemplate.getExpire(key); + } + + /** + * 判断 key是否存在 + * + * @param key 键 + * @return true 存在 false不存在 + */ + public Boolean hasKey(String key) + { + return redisTemplate.hasKey(key); + } + + /** + * 获得缓存的基本对象。 + * + * @param key 缓存键值 + * @return 缓存键值对应的数据 + */ + public T getCacheObject(final String key) + { + ValueOperations operation = redisTemplate.opsForValue(); + return operation.get(key); + } + + /** + * 删除单个对象 + * + * @param key + */ + public boolean deleteObject(final String key) + { + return redisTemplate.delete(key); + } + + /** + * 删除集合对象 + * + * @param collection 多个对象 + * @return + */ + public boolean deleteObject(final Collection collection) + { + return redisTemplate.delete(collection) > 0; + } + + /** + * 缓存List数据 + * + * @param key 缓存的键值 + * @param dataList 待缓存的List数据 + * @return 缓存的对象 + */ + public long setCacheList(final String key, final List dataList) + { + Long count = redisTemplate.opsForList().rightPushAll(key, dataList); + return count == null ? 0 : count; + } + + /** + * 获得缓存的list对象 + * + * @param key 缓存的键值 + * @return 缓存键值对应的数据 + */ + public List getCacheList(final String key) + { + return redisTemplate.opsForList().range(key, 0, -1); + } + + /** + * 缓存Set + * + * @param key 缓存键值 + * @param dataSet 缓存的数据 + * @return 缓存数据的对象 + */ + public BoundSetOperations setCacheSet(final String key, final Set dataSet) + { + BoundSetOperations setOperation = redisTemplate.boundSetOps(key); + Iterator it = dataSet.iterator(); + while (it.hasNext()) + { + setOperation.add(it.next()); + } + return setOperation; + } + + /** + * 获得缓存的set + * + * @param key + * @return + */ + public Set getCacheSet(final String key) + { + return redisTemplate.opsForSet().members(key); + } + + /** + * 缓存Map + * + * @param key + * @param dataMap + */ + public void setCacheMap(final String key, final Map dataMap) + { + if (dataMap != null) { + redisTemplate.opsForHash().putAll(key, dataMap); + } + } + + /** + * 获得缓存的Map + * + * @param key + * @return + */ + public Map getCacheMap(final String key) + { + return redisTemplate.opsForHash().entries(key); + } + + /** + * 往Hash中存入数据 + * + * @param key Redis键 + * @param hKey Hash键 + * @param value 值 + */ + public void setCacheMapValue(final String key, final String hKey, final T value) + { + redisTemplate.opsForHash().put(key, hKey, value); + } + + /** + * 获取Hash中的数据 + * + * @param key Redis键 + * @param hKey Hash键 + * @return Hash中的对象 + */ + public T getCacheMapValue(final String key, final String hKey) + { + HashOperations opsForHash = redisTemplate.opsForHash(); + return opsForHash.get(key, hKey); + } + + /** + * 获取多个Hash中的数据 + * + * @param key Redis键 + * @param hKeys Hash键集合 + * @return Hash对象集合 + */ + public List getMultiCacheMapValue(final String key, final Collection hKeys) + { + return redisTemplate.opsForHash().multiGet(key, hKeys); + } + + /** + * 删除Hash中的某条数据 + * + * @param key Redis键 + * @param hKey Hash键 + * @return 是否成功 + */ + public boolean deleteCacheMapValue(final String key, final String hKey) + { + return redisTemplate.opsForHash().delete(key, hKey) > 0; + } + + /** + * 获得缓存的基本对象列表 + * + * @param pattern 字符串前缀 + * @return 对象列表 + */ + public Collection keys(final String pattern) + { + return redisTemplate.keys(pattern); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/text/CharsetKit.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/text/CharsetKit.java new file mode 100644 index 0000000..84124aa --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/text/CharsetKit.java @@ -0,0 +1,86 @@ +package com.ruoyi.common.core.text; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import com.ruoyi.common.utils.StringUtils; + +/** + * 字符集工具类 + * + * @author ruoyi + */ +public class CharsetKit +{ + /** ISO-8859-1 */ + public static final String ISO_8859_1 = "ISO-8859-1"; + /** UTF-8 */ + public static final String UTF_8 = "UTF-8"; + /** GBK */ + public static final String GBK = "GBK"; + + /** ISO-8859-1 */ + public static final Charset CHARSET_ISO_8859_1 = Charset.forName(ISO_8859_1); + /** UTF-8 */ + public static final Charset CHARSET_UTF_8 = Charset.forName(UTF_8); + /** GBK */ + public static final Charset CHARSET_GBK = Charset.forName(GBK); + + /** + * 转换为Charset对象 + * + * @param charset 字符集,为空则返回默认字符集 + * @return Charset + */ + public static Charset charset(String charset) + { + return StringUtils.isEmpty(charset) ? Charset.defaultCharset() : Charset.forName(charset); + } + + /** + * 转换字符串的字符集编码 + * + * @param source 字符串 + * @param srcCharset 源字符集,默认ISO-8859-1 + * @param destCharset 目标字符集,默认UTF-8 + * @return 转换后的字符集 + */ + public static String convert(String source, String srcCharset, String destCharset) + { + return convert(source, Charset.forName(srcCharset), Charset.forName(destCharset)); + } + + /** + * 转换字符串的字符集编码 + * + * @param source 字符串 + * @param srcCharset 源字符集,默认ISO-8859-1 + * @param destCharset 目标字符集,默认UTF-8 + * @return 转换后的字符集 + */ + public static String convert(String source, Charset srcCharset, Charset destCharset) + { + if (null == srcCharset) + { + srcCharset = StandardCharsets.ISO_8859_1; + } + + if (null == destCharset) + { + destCharset = StandardCharsets.UTF_8; + } + + if (StringUtils.isEmpty(source) || srcCharset.equals(destCharset)) + { + return source; + } + return new String(source.getBytes(srcCharset), destCharset); + } + + /** + * @return 系统字符集编码 + */ + public static String systemCharset() + { + return Charset.defaultCharset().name(); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/text/Convert.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/text/Convert.java new file mode 100644 index 0000000..a87828a --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/text/Convert.java @@ -0,0 +1,1018 @@ +package com.ruoyi.common.core.text; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.RoundingMode; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.text.NumberFormat; +import java.util.Set; +import com.ruoyi.common.utils.StringUtils; + +/** + * 类型转换器 + * + * @author ruoyi + */ +public class Convert +{ + /** + * 转换为字符串
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static String toStr(Object value, String defaultValue) + { + if (null == value) + { + return defaultValue; + } + if (value instanceof String) + { + return (String) value; + } + return value.toString(); + } + + /** + * 转换为字符串
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static String toStr(Object value) + { + return toStr(value, null); + } + + /** + * 转换为字符
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Character toChar(Object value, Character defaultValue) + { + if (null == value) + { + return defaultValue; + } + if (value instanceof Character) + { + return (Character) value; + } + + final String valueStr = toStr(value, null); + return StringUtils.isEmpty(valueStr) ? defaultValue : valueStr.charAt(0); + } + + /** + * 转换为字符
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Character toChar(Object value) + { + return toChar(value, null); + } + + /** + * 转换为byte
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Byte toByte(Object value, Byte defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Byte) + { + return (Byte) value; + } + if (value instanceof Number) + { + return ((Number) value).byteValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Byte.parseByte(valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为byte
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Byte toByte(Object value) + { + return toByte(value, null); + } + + /** + * 转换为Short
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Short toShort(Object value, Short defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Short) + { + return (Short) value; + } + if (value instanceof Number) + { + return ((Number) value).shortValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Short.parseShort(valueStr.trim()); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为Short
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Short toShort(Object value) + { + return toShort(value, null); + } + + /** + * 转换为Number
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Number toNumber(Object value, Number defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Number) + { + return (Number) value; + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return NumberFormat.getInstance().parse(valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为Number
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Number toNumber(Object value) + { + return toNumber(value, null); + } + + /** + * 转换为int
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Integer toInt(Object value, Integer defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Integer) + { + return (Integer) value; + } + if (value instanceof Number) + { + return ((Number) value).intValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Integer.parseInt(valueStr.trim()); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为int
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Integer toInt(Object value) + { + return toInt(value, null); + } + + /** + * 转换为Integer数组
+ * + * @param str 被转换的值 + * @return 结果 + */ + public static Integer[] toIntArray(String str) + { + return toIntArray(",", str); + } + + /** + * 转换为Long数组
+ * + * @param str 被转换的值 + * @return 结果 + */ + public static Long[] toLongArray(String str) + { + return toLongArray(",", str); + } + + /** + * 转换为Integer数组
+ * + * @param split 分隔符 + * @param split 被转换的值 + * @return 结果 + */ + public static Integer[] toIntArray(String split, String str) + { + if (StringUtils.isEmpty(str)) + { + return new Integer[] {}; + } + String[] arr = str.split(split); + final Integer[] ints = new Integer[arr.length]; + for (int i = 0; i < arr.length; i++) + { + final Integer v = toInt(arr[i], 0); + ints[i] = v; + } + return ints; + } + + /** + * 转换为Long数组
+ * + * @param split 分隔符 + * @param str 被转换的值 + * @return 结果 + */ + public static Long[] toLongArray(String split, String str) + { + if (StringUtils.isEmpty(str)) + { + return new Long[] {}; + } + String[] arr = str.split(split); + final Long[] longs = new Long[arr.length]; + for (int i = 0; i < arr.length; i++) + { + final Long v = toLong(arr[i], null); + longs[i] = v; + } + return longs; + } + + /** + * 转换为String数组
+ * + * @param str 被转换的值 + * @return 结果 + */ + public static String[] toStrArray(String str) + { + if (StringUtils.isEmpty(str)) + { + return new String[] {}; + } + return toStrArray(",", str); + } + + /** + * 转换为String数组
+ * + * @param split 分隔符 + * @param split 被转换的值 + * @return 结果 + */ + public static String[] toStrArray(String split, String str) + { + return str.split(split); + } + + /** + * 转换为long
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Long toLong(Object value, Long defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Long) + { + return (Long) value; + } + if (value instanceof Number) + { + return ((Number) value).longValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + // 支持科学计数法 + return new BigDecimal(valueStr.trim()).longValue(); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为long
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Long toLong(Object value) + { + return toLong(value, null); + } + + /** + * 转换为double
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Double toDouble(Object value, Double defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Double) + { + return (Double) value; + } + if (value instanceof Number) + { + return ((Number) value).doubleValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + // 支持科学计数法 + return new BigDecimal(valueStr.trim()).doubleValue(); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为double
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Double toDouble(Object value) + { + return toDouble(value, null); + } + + /** + * 转换为Float
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Float toFloat(Object value, Float defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Float) + { + return (Float) value; + } + if (value instanceof Number) + { + return ((Number) value).floatValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Float.parseFloat(valueStr.trim()); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为Float
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Float toFloat(Object value) + { + return toFloat(value, null); + } + + /** + * 转换为boolean
+ * String支持的值为:true、false、yes、ok、no、1、0、是、否, 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Boolean toBool(Object value, Boolean defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Boolean) + { + return (Boolean) value; + } + String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + valueStr = valueStr.trim().toLowerCase(); + switch (valueStr) + { + case "true": + case "yes": + case "ok": + case "1": + case "是": + return true; + case "false": + case "no": + case "0": + case "否": + return false; + default: + return defaultValue; + } + } + + /** + * 转换为boolean
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Boolean toBool(Object value) + { + return toBool(value, null); + } + + /** + * 转换为Enum对象
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * + * @param clazz Enum的Class + * @param value 值 + * @param defaultValue 默认值 + * @return Enum + */ + public static > E toEnum(Class clazz, Object value, E defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (clazz.isAssignableFrom(value.getClass())) + { + @SuppressWarnings("unchecked") + E myE = (E) value; + return myE; + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Enum.valueOf(clazz, valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为Enum对象
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * + * @param clazz Enum的Class + * @param value 值 + * @return Enum + */ + public static > E toEnum(Class clazz, Object value) + { + return toEnum(clazz, value, null); + } + + /** + * 转换为BigInteger
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static BigInteger toBigInteger(Object value, BigInteger defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof BigInteger) + { + return (BigInteger) value; + } + if (value instanceof Long) + { + return BigInteger.valueOf((Long) value); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return new BigInteger(valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为BigInteger
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static BigInteger toBigInteger(Object value) + { + return toBigInteger(value, null); + } + + /** + * 转换为BigDecimal
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static BigDecimal toBigDecimal(Object value, BigDecimal defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof BigDecimal) + { + return (BigDecimal) value; + } + if (value instanceof Long) + { + return new BigDecimal((Long) value); + } + if (value instanceof Double) + { + return BigDecimal.valueOf((Double) value); + } + if (value instanceof Integer) + { + return new BigDecimal((Integer) value); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return new BigDecimal(valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为BigDecimal
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static BigDecimal toBigDecimal(Object value) + { + return toBigDecimal(value, null); + } + + /** + * 将对象转为字符串
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法 + * + * @param obj 对象 + * @return 字符串 + */ + public static String utf8Str(Object obj) + { + return str(obj, CharsetKit.CHARSET_UTF_8); + } + + /** + * 将对象转为字符串
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法 + * + * @param obj 对象 + * @param charsetName 字符集 + * @return 字符串 + */ + public static String str(Object obj, String charsetName) + { + return str(obj, Charset.forName(charsetName)); + } + + /** + * 将对象转为字符串
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法 + * + * @param obj 对象 + * @param charset 字符集 + * @return 字符串 + */ + public static String str(Object obj, Charset charset) + { + if (null == obj) + { + return null; + } + + if (obj instanceof String) + { + return (String) obj; + } + else if (obj instanceof byte[] || obj instanceof Byte[]) + { + if (obj instanceof byte[]) + { + return str((byte[]) obj, charset); + } + else + { + Byte[] bytes = (Byte[]) obj; + int length = bytes.length; + byte[] dest = new byte[length]; + for (int i = 0; i < length; i++) + { + dest[i] = bytes[i]; + } + return str(dest, charset); + } + } + else if (obj instanceof ByteBuffer) + { + return str((ByteBuffer) obj, charset); + } + return obj.toString(); + } + + /** + * 将byte数组转为字符串 + * + * @param bytes byte数组 + * @param charset 字符集 + * @return 字符串 + */ + public static String str(byte[] bytes, String charset) + { + return str(bytes, StringUtils.isEmpty(charset) ? Charset.defaultCharset() : Charset.forName(charset)); + } + + /** + * 解码字节码 + * + * @param data 字符串 + * @param charset 字符集,如果此字段为空,则解码的结果取决于平台 + * @return 解码后的字符串 + */ + public static String str(byte[] data, Charset charset) + { + if (data == null) + { + return null; + } + + if (null == charset) + { + return new String(data); + } + return new String(data, charset); + } + + /** + * 将编码的byteBuffer数据转换为字符串 + * + * @param data 数据 + * @param charset 字符集,如果为空使用当前系统字符集 + * @return 字符串 + */ + public static String str(ByteBuffer data, String charset) + { + if (data == null) + { + return null; + } + + return str(data, Charset.forName(charset)); + } + + /** + * 将编码的byteBuffer数据转换为字符串 + * + * @param data 数据 + * @param charset 字符集,如果为空使用当前系统字符集 + * @return 字符串 + */ + public static String str(ByteBuffer data, Charset charset) + { + if (null == charset) + { + charset = Charset.defaultCharset(); + } + return charset.decode(data).toString(); + } + + // ----------------------------------------------------------------------- 全角半角转换 + /** + * 半角转全角 + * + * @param input String. + * @return 全角字符串. + */ + public static String toSBC(String input) + { + return toSBC(input, null); + } + + /** + * 半角转全角 + * + * @param input String + * @param notConvertSet 不替换的字符集合 + * @return 全角字符串. + */ + public static String toSBC(String input, Set notConvertSet) + { + char[] c = input.toCharArray(); + for (int i = 0; i < c.length; i++) + { + if (null != notConvertSet && notConvertSet.contains(c[i])) + { + // 跳过不替换的字符 + continue; + } + + if (c[i] == ' ') + { + c[i] = '\u3000'; + } + else if (c[i] < '\177') + { + c[i] = (char) (c[i] + 65248); + + } + } + return new String(c); + } + + /** + * 全角转半角 + * + * @param input String. + * @return 半角字符串 + */ + public static String toDBC(String input) + { + return toDBC(input, null); + } + + /** + * 替换全角为半角 + * + * @param text 文本 + * @param notConvertSet 不替换的字符集合 + * @return 替换后的字符 + */ + public static String toDBC(String text, Set notConvertSet) + { + char[] c = text.toCharArray(); + for (int i = 0; i < c.length; i++) + { + if (null != notConvertSet && notConvertSet.contains(c[i])) + { + // 跳过不替换的字符 + continue; + } + + if (c[i] == '\u3000') + { + c[i] = ' '; + } + else if (c[i] > '\uFF00' && c[i] < '\uFF5F') + { + c[i] = (char) (c[i] - 65248); + } + } + return new String(c); + } + + /** + * 数字金额大写转换 先写个完整的然后将如零拾替换成零 + * + * @param n 数字 + * @return 中文大写数字 + */ + public static String digitUppercase(double n) + { + String[] fraction = { "角", "分" }; + String[] digit = { "零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖" }; + String[][] unit = { { "元", "万", "亿" }, { "", "拾", "佰", "仟" } }; + + String head = n < 0 ? "负" : ""; + n = Math.abs(n); + + String s = ""; + for (int i = 0; i < fraction.length; i++) + { + // 优化double计算精度丢失问题 + BigDecimal nNum = new BigDecimal(n); + BigDecimal decimal = new BigDecimal(10); + BigDecimal scale = nNum.multiply(decimal).setScale(2, RoundingMode.HALF_EVEN); + double d = scale.doubleValue(); + s += (digit[(int) (Math.floor(d * Math.pow(10, i)) % 10)] + fraction[i]).replaceAll("(零.)+", ""); + } + if (s.length() < 1) + { + s = "整"; + } + int integerPart = (int) Math.floor(n); + + for (int i = 0; i < unit[0].length && integerPart > 0; i++) + { + String p = ""; + for (int j = 0; j < unit[1].length && n > 0; j++) + { + p = digit[integerPart % 10] + unit[1][j] + p; + integerPart = integerPart / 10; + } + s = p.replaceAll("(零.)*零$", "").replaceAll("^$", "零") + unit[0][i] + s; + } + return head + s.replaceAll("(零.)*零元", "元").replaceFirst("(零.)+", "").replaceAll("(零.)+", "零").replaceAll("^整$", "零元整"); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/text/StrFormatter.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/text/StrFormatter.java new file mode 100644 index 0000000..c78ac77 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/text/StrFormatter.java @@ -0,0 +1,92 @@ +package com.ruoyi.common.core.text; + +import com.ruoyi.common.utils.StringUtils; + +/** + * 字符串格式化 + * + * @author ruoyi + */ +public class StrFormatter +{ + public static final String EMPTY_JSON = "{}"; + public static final char C_BACKSLASH = '\\'; + public static final char C_DELIM_START = '{'; + public static final char C_DELIM_END = '}'; + + /** + * 格式化字符串
+ * 此方法只是简单将占位符 {} 按照顺序替换为参数
+ * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可
+ * 例:
+ * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b
+ * 转义{}: format("this is \\{} for {}", "a", "b") -> this is \{} for a
+ * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b
+ * + * @param strPattern 字符串模板 + * @param argArray 参数列表 + * @return 结果 + */ + public static String format(final String strPattern, final Object... argArray) + { + if (StringUtils.isEmpty(strPattern) || StringUtils.isEmpty(argArray)) + { + return strPattern; + } + final int strPatternLength = strPattern.length(); + + // 初始化定义好的长度以获得更好的性能 + StringBuilder sbuf = new StringBuilder(strPatternLength + 50); + + int handledPosition = 0; + int delimIndex;// 占位符所在位置 + for (int argIndex = 0; argIndex < argArray.length; argIndex++) + { + delimIndex = strPattern.indexOf(EMPTY_JSON, handledPosition); + if (delimIndex == -1) + { + if (handledPosition == 0) + { + return strPattern; + } + else + { // 字符串模板剩余部分不再包含占位符,加入剩余部分后返回结果 + sbuf.append(strPattern, handledPosition, strPatternLength); + return sbuf.toString(); + } + } + else + { + if (delimIndex > 0 && strPattern.charAt(delimIndex - 1) == C_BACKSLASH) + { + if (delimIndex > 1 && strPattern.charAt(delimIndex - 2) == C_BACKSLASH) + { + // 转义符之前还有一个转义符,占位符依旧有效 + sbuf.append(strPattern, handledPosition, delimIndex - 1); + sbuf.append(Convert.utf8Str(argArray[argIndex])); + handledPosition = delimIndex + 2; + } + else + { + // 占位符被转义 + argIndex--; + sbuf.append(strPattern, handledPosition, delimIndex - 1); + sbuf.append(C_DELIM_START); + handledPosition = delimIndex + 1; + } + } + else + { + // 正常占位符 + sbuf.append(strPattern, handledPosition, delimIndex); + sbuf.append(Convert.utf8Str(argArray[argIndex])); + handledPosition = delimIndex + 2; + } + } + } + // 加入最后一个占位符后所有的字符 + sbuf.append(strPattern, handledPosition, strPattern.length()); + + return sbuf.toString(); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/BusinessStatus.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/BusinessStatus.java new file mode 100644 index 0000000..10b7306 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/BusinessStatus.java @@ -0,0 +1,20 @@ +package com.ruoyi.common.enums; + +/** + * 操作状态 + * + * @author ruoyi + * + */ +public enum BusinessStatus +{ + /** + * 成功 + */ + SUCCESS, + + /** + * 失败 + */ + FAIL, +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/BusinessType.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/BusinessType.java new file mode 100644 index 0000000..2e17c4a --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/BusinessType.java @@ -0,0 +1,59 @@ +package com.ruoyi.common.enums; + +/** + * 业务操作类型 + * + * @author ruoyi + */ +public enum BusinessType +{ + /** + * 其它 + */ + OTHER, + + /** + * 新增 + */ + INSERT, + + /** + * 修改 + */ + UPDATE, + + /** + * 删除 + */ + DELETE, + + /** + * 授权 + */ + GRANT, + + /** + * 导出 + */ + EXPORT, + + /** + * 导入 + */ + IMPORT, + + /** + * 强退 + */ + FORCE, + + /** + * 生成代码 + */ + GENCODE, + + /** + * 清空数据 + */ + CLEAN, +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/DataSourceType.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/DataSourceType.java new file mode 100644 index 0000000..0d945be --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/DataSourceType.java @@ -0,0 +1,19 @@ +package com.ruoyi.common.enums; + +/** + * 数据源 + * + * @author ruoyi + */ +public enum DataSourceType +{ + /** + * 主库 + */ + MASTER, + + /** + * 从库 + */ + SLAVE +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/DesensitizedType.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/DesensitizedType.java new file mode 100644 index 0000000..07f02ee --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/DesensitizedType.java @@ -0,0 +1,59 @@ +package com.ruoyi.common.enums; + +import java.util.function.Function; +import com.ruoyi.common.utils.DesensitizedUtil; + +/** + * 脱敏类型 + * + * @author ruoyi + */ +public enum DesensitizedType +{ + /** + * 姓名,第2位星号替换 + */ + USERNAME(s -> s.replaceAll("(\\S)\\S(\\S*)", "$1*$2")), + + /** + * 密码,全部字符都用*代替 + */ + PASSWORD(DesensitizedUtil::password), + + /** + * 身份证,中间10位星号替换 + */ + ID_CARD(s -> s.replaceAll("(\\d{4})\\d{10}(\\d{3}[Xx]|\\d{4})", "$1** **** ****$2")), + + /** + * 手机号,中间4位星号替换 + */ + PHONE(s -> s.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2")), + + /** + * 电子邮箱,仅显示第一个字母和@后面的地址显示,其他星号替换 + */ + EMAIL(s -> s.replaceAll("(^.)[^@]*(@.*$)", "$1****$2")), + + /** + * 银行卡号,保留最后4位,其他星号替换 + */ + BANK_CARD(s -> s.replaceAll("\\d{15}(\\d{3})", "**** **** **** **** $1")), + + /** + * 车牌号码,包含普通车辆、新能源车辆 + */ + CAR_LICENSE(DesensitizedUtil::carLicense); + + private final Function desensitizer; + + DesensitizedType(Function desensitizer) + { + this.desensitizer = desensitizer; + } + + public Function desensitizer() + { + return desensitizer; + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/HttpMethod.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/HttpMethod.java new file mode 100644 index 0000000..7357049 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/HttpMethod.java @@ -0,0 +1,36 @@ +package com.ruoyi.common.enums; + +import java.util.HashMap; +import java.util.Map; +import org.jspecify.annotations.Nullable; + +/** + * 请求方式 + * + * @author ruoyi + */ +public enum HttpMethod +{ + GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE; + + private static final Map mappings = new HashMap<>(16); + + static + { + for (HttpMethod httpMethod : values()) + { + mappings.put(httpMethod.name(), httpMethod); + } + } + + @Nullable + public static HttpMethod resolve(@Nullable String method) + { + return (method != null ? mappings.get(method) : null); + } + + public boolean matches(String method) + { + return (this == resolve(method)); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/LimitType.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/LimitType.java new file mode 100644 index 0000000..c609fd8 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/LimitType.java @@ -0,0 +1,20 @@ +package com.ruoyi.common.enums; + +/** + * 限流类型 + * + * @author ruoyi + */ + +public enum LimitType +{ + /** + * 默认策略全局限流 + */ + DEFAULT, + + /** + * 根据请求者IP进行限流 + */ + IP +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/OperatorType.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/OperatorType.java new file mode 100644 index 0000000..bdd143c --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/OperatorType.java @@ -0,0 +1,24 @@ +package com.ruoyi.common.enums; + +/** + * 操作人类别 + * + * @author ruoyi + */ +public enum OperatorType +{ + /** + * 其它 + */ + OTHER, + + /** + * 后台用户 + */ + MANAGE, + + /** + * 手机端用户 + */ + MOBILE +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/UserStatus.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/UserStatus.java new file mode 100644 index 0000000..d7ff44a --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/UserStatus.java @@ -0,0 +1,30 @@ +package com.ruoyi.common.enums; + +/** + * 用户状态 + * + * @author ruoyi + */ +public enum UserStatus +{ + OK("0", "正常"), DISABLE("1", "停用"), DELETED("2", "删除"); + + private final String code; + private final String info; + + UserStatus(String code, String info) + { + this.code = code; + this.info = info; + } + + public String getCode() + { + return code; + } + + public String getInfo() + { + return info; + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/DemoModeException.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/DemoModeException.java new file mode 100644 index 0000000..f6ad2ab --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/DemoModeException.java @@ -0,0 +1,15 @@ +package com.ruoyi.common.exception; + +/** + * 演示模式异常 + * + * @author ruoyi + */ +public class DemoModeException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + public DemoModeException() + { + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/GlobalException.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/GlobalException.java new file mode 100644 index 0000000..81a71b5 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/GlobalException.java @@ -0,0 +1,58 @@ +package com.ruoyi.common.exception; + +/** + * 全局异常 + * + * @author ruoyi + */ +public class GlobalException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + /** + * 错误提示 + */ + private String message; + + /** + * 错误明细,内部调试错误 + * + * 和 {@link CommonResult#getDetailMessage()} 一致的设计 + */ + private String detailMessage; + + /** + * 空构造方法,避免反序列化问题 + */ + public GlobalException() + { + } + + public GlobalException(String message) + { + this.message = message; + } + + public String getDetailMessage() + { + return detailMessage; + } + + public GlobalException setDetailMessage(String detailMessage) + { + this.detailMessage = detailMessage; + return this; + } + + @Override + public String getMessage() + { + return message; + } + + public GlobalException setMessage(String message) + { + this.message = message; + return this; + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/ServiceException.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/ServiceException.java new file mode 100644 index 0000000..fcc7ab6 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/ServiceException.java @@ -0,0 +1,74 @@ +package com.ruoyi.common.exception; + +/** + * 业务异常 + * + * @author ruoyi + */ +public final class ServiceException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + /** + * 错误码 + */ + private Integer code; + + /** + * 错误提示 + */ + private String message; + + /** + * 错误明细,内部调试错误 + * + * 和 {@link CommonResult#getDetailMessage()} 一致的设计 + */ + private String detailMessage; + + /** + * 空构造方法,避免反序列化问题 + */ + public ServiceException() + { + } + + public ServiceException(String message) + { + this.message = message; + } + + public ServiceException(String message, Integer code) + { + this.message = message; + this.code = code; + } + + public String getDetailMessage() + { + return detailMessage; + } + + @Override + public String getMessage() + { + return message; + } + + public Integer getCode() + { + return code; + } + + public ServiceException setMessage(String message) + { + this.message = message; + return this; + } + + public ServiceException setDetailMessage(String detailMessage) + { + this.detailMessage = detailMessage; + return this; + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/UtilException.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/UtilException.java new file mode 100644 index 0000000..980fa46 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/UtilException.java @@ -0,0 +1,26 @@ +package com.ruoyi.common.exception; + +/** + * 工具类异常 + * + * @author ruoyi + */ +public class UtilException extends RuntimeException +{ + private static final long serialVersionUID = 8247610319171014183L; + + public UtilException(Throwable e) + { + super(e.getMessage(), e); + } + + public UtilException(String message) + { + super(message); + } + + public UtilException(String message, Throwable throwable) + { + super(message, throwable); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/base/BaseException.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/base/BaseException.java new file mode 100644 index 0000000..b55d72e --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/base/BaseException.java @@ -0,0 +1,97 @@ +package com.ruoyi.common.exception.base; + +import com.ruoyi.common.utils.MessageUtils; +import com.ruoyi.common.utils.StringUtils; + +/** + * 基础异常 + * + * @author ruoyi + */ +public class BaseException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + /** + * 所属模块 + */ + private String module; + + /** + * 错误码 + */ + private String code; + + /** + * 错误码对应的参数 + */ + private Object[] args; + + /** + * 错误消息 + */ + private String defaultMessage; + + public BaseException(String module, String code, Object[] args, String defaultMessage) + { + this.module = module; + this.code = code; + this.args = args; + this.defaultMessage = defaultMessage; + } + + public BaseException(String module, String code, Object[] args) + { + this(module, code, args, null); + } + + public BaseException(String module, String defaultMessage) + { + this(module, null, null, defaultMessage); + } + + public BaseException(String code, Object[] args) + { + this(null, code, args, null); + } + + public BaseException(String defaultMessage) + { + this(null, null, null, defaultMessage); + } + + @Override + public String getMessage() + { + String message = null; + if (!StringUtils.isEmpty(code)) + { + message = MessageUtils.message(code, args); + } + if (message == null) + { + message = defaultMessage; + } + return message; + } + + public String getModule() + { + return module; + } + + public String getCode() + { + return code; + } + + public Object[] getArgs() + { + return args; + } + + public String getDefaultMessage() + { + return defaultMessage; + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileException.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileException.java new file mode 100644 index 0000000..871f09b --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileException.java @@ -0,0 +1,19 @@ +package com.ruoyi.common.exception.file; + +import com.ruoyi.common.exception.base.BaseException; + +/** + * 文件信息异常类 + * + * @author ruoyi + */ +public class FileException extends BaseException +{ + private static final long serialVersionUID = 1L; + + public FileException(String code, Object[] args) + { + super("file", code, args, null); + } + +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileNameLengthLimitExceededException.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileNameLengthLimitExceededException.java new file mode 100644 index 0000000..70e0ec9 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileNameLengthLimitExceededException.java @@ -0,0 +1,16 @@ +package com.ruoyi.common.exception.file; + +/** + * 文件名称超长限制异常类 + * + * @author ruoyi + */ +public class FileNameLengthLimitExceededException extends FileException +{ + private static final long serialVersionUID = 1L; + + public FileNameLengthLimitExceededException(int defaultFileNameLength) + { + super("upload.filename.exceed.length", new Object[] { defaultFileNameLength }); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileSizeLimitExceededException.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileSizeLimitExceededException.java new file mode 100644 index 0000000..ec6ab05 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileSizeLimitExceededException.java @@ -0,0 +1,16 @@ +package com.ruoyi.common.exception.file; + +/** + * 文件名大小限制异常类 + * + * @author ruoyi + */ +public class FileSizeLimitExceededException extends FileException +{ + private static final long serialVersionUID = 1L; + + public FileSizeLimitExceededException(long defaultMaxSize) + { + super("upload.exceed.maxSize", new Object[] { defaultMaxSize }); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileUploadException.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileUploadException.java new file mode 100644 index 0000000..f45e7ef --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileUploadException.java @@ -0,0 +1,61 @@ +package com.ruoyi.common.exception.file; + +import java.io.PrintStream; +import java.io.PrintWriter; + +/** + * 文件上传异常类 + * + * @author ruoyi + */ +public class FileUploadException extends Exception +{ + + private static final long serialVersionUID = 1L; + + private final Throwable cause; + + public FileUploadException() + { + this(null, null); + } + + public FileUploadException(final String msg) + { + this(msg, null); + } + + public FileUploadException(String msg, Throwable cause) + { + super(msg); + this.cause = cause; + } + + @Override + public void printStackTrace(PrintStream stream) + { + super.printStackTrace(stream); + if (cause != null) + { + stream.println("Caused by:"); + cause.printStackTrace(stream); + } + } + + @Override + public void printStackTrace(PrintWriter writer) + { + super.printStackTrace(writer); + if (cause != null) + { + writer.println("Caused by:"); + cause.printStackTrace(writer); + } + } + + @Override + public Throwable getCause() + { + return cause; + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/InvalidExtensionException.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/InvalidExtensionException.java new file mode 100644 index 0000000..bb8a53f --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/InvalidExtensionException.java @@ -0,0 +1,80 @@ +package com.ruoyi.common.exception.file; + +import java.util.Arrays; + +/** + * 文件上传无效扩展名异常类 + * + * @author ruoyi + */ +public class InvalidExtensionException extends FileUploadException +{ + private static final long serialVersionUID = 1L; + + private String[] allowedExtension; + private String extension; + private String filename; + + public InvalidExtensionException(String[] allowedExtension, String extension, String filename) + { + super("文件[" + filename + "]后缀[" + extension + "]不正确,请上传" + Arrays.toString(allowedExtension) + "格式"); + this.allowedExtension = allowedExtension; + this.extension = extension; + this.filename = filename; + } + + public String[] getAllowedExtension() + { + return allowedExtension; + } + + public String getExtension() + { + return extension; + } + + public String getFilename() + { + return filename; + } + + public static class InvalidImageExtensionException extends InvalidExtensionException + { + private static final long serialVersionUID = 1L; + + public InvalidImageExtensionException(String[] allowedExtension, String extension, String filename) + { + super(allowedExtension, extension, filename); + } + } + + public static class InvalidFlashExtensionException extends InvalidExtensionException + { + private static final long serialVersionUID = 1L; + + public InvalidFlashExtensionException(String[] allowedExtension, String extension, String filename) + { + super(allowedExtension, extension, filename); + } + } + + public static class InvalidMediaExtensionException extends InvalidExtensionException + { + private static final long serialVersionUID = 1L; + + public InvalidMediaExtensionException(String[] allowedExtension, String extension, String filename) + { + super(allowedExtension, extension, filename); + } + } + + public static class InvalidVideoExtensionException extends InvalidExtensionException + { + private static final long serialVersionUID = 1L; + + public InvalidVideoExtensionException(String[] allowedExtension, String extension, String filename) + { + super(allowedExtension, extension, filename); + } + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/job/TaskException.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/job/TaskException.java new file mode 100644 index 0000000..a567b40 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/job/TaskException.java @@ -0,0 +1,34 @@ +package com.ruoyi.common.exception.job; + +/** + * 计划策略异常 + * + * @author ruoyi + */ +public class TaskException extends Exception +{ + private static final long serialVersionUID = 1L; + + private Code code; + + public TaskException(String msg, Code code) + { + this(msg, code, null); + } + + public TaskException(String msg, Code code, Exception nestedEx) + { + super(msg, nestedEx); + this.code = code; + } + + public Code getCode() + { + return code; + } + + public enum Code + { + TASK_EXISTS, NO_TASK_EXISTS, TASK_ALREADY_STARTED, UNKNOWN, CONFIG_ERROR, TASK_NODE_NOT_AVAILABLE + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/BlackListException.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/BlackListException.java new file mode 100644 index 0000000..2bf5038 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/BlackListException.java @@ -0,0 +1,16 @@ +package com.ruoyi.common.exception.user; + +/** + * 黑名单IP异常类 + * + * @author ruoyi + */ +public class BlackListException extends UserException +{ + private static final long serialVersionUID = 1L; + + public BlackListException() + { + super("login.blocked", null); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/CaptchaException.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/CaptchaException.java new file mode 100644 index 0000000..389dbc7 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/CaptchaException.java @@ -0,0 +1,16 @@ +package com.ruoyi.common.exception.user; + +/** + * 验证码错误异常类 + * + * @author ruoyi + */ +public class CaptchaException extends UserException +{ + private static final long serialVersionUID = 1L; + + public CaptchaException() + { + super("user.jcaptcha.error", null); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/CaptchaExpireException.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/CaptchaExpireException.java new file mode 100644 index 0000000..85f9486 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/CaptchaExpireException.java @@ -0,0 +1,16 @@ +package com.ruoyi.common.exception.user; + +/** + * 验证码失效异常类 + * + * @author ruoyi + */ +public class CaptchaExpireException extends UserException +{ + private static final long serialVersionUID = 1L; + + public CaptchaExpireException() + { + super("user.jcaptcha.expire", null); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserException.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserException.java new file mode 100644 index 0000000..c292d70 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserException.java @@ -0,0 +1,18 @@ +package com.ruoyi.common.exception.user; + +import com.ruoyi.common.exception.base.BaseException; + +/** + * 用户信息异常类 + * + * @author ruoyi + */ +public class UserException extends BaseException +{ + private static final long serialVersionUID = 1L; + + public UserException(String code, Object[] args) + { + super("user", code, args, null); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserNotExistsException.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserNotExistsException.java new file mode 100644 index 0000000..eff8181 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserNotExistsException.java @@ -0,0 +1,16 @@ +package com.ruoyi.common.exception.user; + +/** + * 用户不存在异常类 + * + * @author ruoyi + */ +public class UserNotExistsException extends UserException +{ + private static final long serialVersionUID = 1L; + + public UserNotExistsException() + { + super("user.not.exists", null); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserPasswordNotMatchException.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserPasswordNotMatchException.java new file mode 100644 index 0000000..a7f3e5f --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserPasswordNotMatchException.java @@ -0,0 +1,16 @@ +package com.ruoyi.common.exception.user; + +/** + * 用户密码不正确或不符合规范异常类 + * + * @author ruoyi + */ +public class UserPasswordNotMatchException extends UserException +{ + private static final long serialVersionUID = 1L; + + public UserPasswordNotMatchException() + { + super("user.password.not.match", null); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserPasswordRetryLimitExceedException.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserPasswordRetryLimitExceedException.java new file mode 100644 index 0000000..c887cf1 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserPasswordRetryLimitExceedException.java @@ -0,0 +1,16 @@ +package com.ruoyi.common.exception.user; + +/** + * 用户错误最大次数异常类 + * + * @author ruoyi + */ +public class UserPasswordRetryLimitExceedException extends UserException +{ + private static final long serialVersionUID = 1L; + + public UserPasswordRetryLimitExceedException(int retryLimitCount, int lockTime) + { + super("user.password.retry.limit.exceed", new Object[] { retryLimitCount, lockTime }); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/filter/PropertyPreExcludeFilter.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/filter/PropertyPreExcludeFilter.java new file mode 100644 index 0000000..e1e431b --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/filter/PropertyPreExcludeFilter.java @@ -0,0 +1,24 @@ +package com.ruoyi.common.filter; + +import com.alibaba.fastjson2.filter.SimplePropertyPreFilter; + +/** + * 排除JSON敏感属性 + * + * @author ruoyi + */ +public class PropertyPreExcludeFilter extends SimplePropertyPreFilter +{ + public PropertyPreExcludeFilter() + { + } + + public PropertyPreExcludeFilter addExcludes(String... filters) + { + for (int i = 0; i < filters.length; i++) + { + this.getExcludes().add(filters[i]); + } + return this; + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/filter/RefererFilter.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/filter/RefererFilter.java new file mode 100644 index 0000000..ac9ed16 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/filter/RefererFilter.java @@ -0,0 +1,77 @@ +package com.ruoyi.common.filter; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * 防盗链过滤器 + * + * @author ruoyi + */ +public class RefererFilter implements Filter +{ + /** + * 允许的域名列表 + */ + public List allowedDomains; + + @Override + public void init(FilterConfig filterConfig) throws ServletException + { + String domains = filterConfig.getInitParameter("allowedDomains"); + this.allowedDomains = Arrays.asList(domains.split(",")); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException + { + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse resp = (HttpServletResponse) response; + + String referer = req.getHeader("Referer"); + + // 如果Referer为空,拒绝访问 + if (referer == null || referer.isEmpty()) + { + resp.sendError(HttpServletResponse.SC_FORBIDDEN, "Access denied: Referer header is required"); + return; + } + + // 检查Referer是否在允许的域名列表中 + boolean allowed = false; + for (String domain : allowedDomains) + { + if (referer.contains(domain)) + { + allowed = true; + break; + } + } + + // 根据检查结果决定是否放行 + if (allowed) + { + chain.doFilter(request, response); + } + else + { + resp.sendError(HttpServletResponse.SC_FORBIDDEN, "Access denied: Referer '" + referer + "' is not allowed"); + } + } + + @Override + public void destroy() + { + + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/filter/RepeatableFilter.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/filter/RepeatableFilter.java new file mode 100644 index 0000000..efeded9 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/filter/RepeatableFilter.java @@ -0,0 +1,52 @@ +package com.ruoyi.common.filter; + +import java.io.IOException; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.MediaType; +import com.ruoyi.common.utils.StringUtils; + +/** + * Repeatable 过滤器 + * + * @author ruoyi + */ +public class RepeatableFilter implements Filter +{ + @Override + public void init(FilterConfig filterConfig) throws ServletException + { + + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException + { + ServletRequest requestWrapper = null; + if (request instanceof HttpServletRequest + && StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE)) + { + requestWrapper = new RepeatedlyRequestWrapper((HttpServletRequest) request, response); + } + if (null == requestWrapper) + { + chain.doFilter(request, response); + } + else + { + chain.doFilter(requestWrapper, response); + } + } + + @Override + public void destroy() + { + + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/filter/RepeatedlyRequestWrapper.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/filter/RepeatedlyRequestWrapper.java new file mode 100644 index 0000000..5c6ee08 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/filter/RepeatedlyRequestWrapper.java @@ -0,0 +1,76 @@ +package com.ruoyi.common.filter; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import com.ruoyi.common.utils.http.HttpHelper; +import com.ruoyi.common.constant.Constants; + +/** + * 构建可重复读取inputStream的request + * + * @author ruoyi + */ +public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper +{ + private final byte[] body; + + public RepeatedlyRequestWrapper(HttpServletRequest request, ServletResponse response) throws IOException + { + super(request); + request.setCharacterEncoding(Constants.UTF8); + response.setCharacterEncoding(Constants.UTF8); + + body = HttpHelper.getBodyString(request).getBytes(Constants.UTF8); + } + + @Override + public BufferedReader getReader() throws IOException + { + return new BufferedReader(new InputStreamReader(getInputStream())); + } + + @Override + public ServletInputStream getInputStream() throws IOException + { + final ByteArrayInputStream bais = new ByteArrayInputStream(body); + return new ServletInputStream() + { + @Override + public int read() throws IOException + { + return bais.read(); + } + + @Override + public int available() throws IOException + { + return body.length; + } + + @Override + public boolean isFinished() + { + return false; + } + + @Override + public boolean isReady() + { + return false; + } + + @Override + public void setReadListener(ReadListener readListener) + { + + } + }; + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/filter/XssFilter.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/filter/XssFilter.java new file mode 100644 index 0000000..7438fc2 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/filter/XssFilter.java @@ -0,0 +1,75 @@ +package com.ruoyi.common.filter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.enums.HttpMethod; + +/** + * 防止XSS攻击的过滤器 + * + * @author ruoyi + */ +public class XssFilter implements Filter +{ + /** + * 排除链接 + */ + public List excludes = new ArrayList<>(); + + @Override + public void init(FilterConfig filterConfig) throws ServletException + { + String tempExcludes = filterConfig.getInitParameter("excludes"); + if (StringUtils.isNotEmpty(tempExcludes)) + { + String[] urls = tempExcludes.split(","); + for (String url : urls) + { + excludes.add(url); + } + } + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException + { + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse resp = (HttpServletResponse) response; + if (handleExcludeURL(req, resp)) + { + chain.doFilter(request, response); + return; + } + XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper((HttpServletRequest) request); + chain.doFilter(xssRequest, response); + } + + private boolean handleExcludeURL(HttpServletRequest request, HttpServletResponse response) + { + String url = request.getServletPath(); + String method = request.getMethod(); + // GET DELETE 不过滤 + if (method == null || HttpMethod.GET.matches(method) || HttpMethod.DELETE.matches(method)) + { + return true; + } + return StringUtils.matches(url, excludes); + } + + @Override + public void destroy() + { + + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/filter/XssHttpServletRequestWrapper.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/filter/XssHttpServletRequestWrapper.java new file mode 100644 index 0000000..bf7837b --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/filter/XssHttpServletRequestWrapper.java @@ -0,0 +1,111 @@ +package com.ruoyi.common.filter; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import org.apache.commons.io.IOUtils; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.html.EscapeUtil; + +/** + * XSS过滤处理 + * + * @author ruoyi + */ +public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper +{ + /** + * @param request + */ + public XssHttpServletRequestWrapper(HttpServletRequest request) + { + super(request); + } + + @Override + public String[] getParameterValues(String name) + { + String[] values = super.getParameterValues(name); + if (values != null) + { + int length = values.length; + String[] escapesValues = new String[length]; + for (int i = 0; i < length; i++) + { + // 防xss攻击和过滤前后空格 + escapesValues[i] = EscapeUtil.clean(values[i]).trim(); + } + return escapesValues; + } + return super.getParameterValues(name); + } + + @Override + public ServletInputStream getInputStream() throws IOException + { + // 非json类型,直接返回 + if (!isJsonRequest()) + { + return super.getInputStream(); + } + + // 为空,直接返回 + String json = IOUtils.toString(super.getInputStream(), "utf-8"); + if (StringUtils.isEmpty(json)) + { + return super.getInputStream(); + } + + // xss过滤 + json = EscapeUtil.clean(json).trim(); + byte[] jsonBytes = json.getBytes("utf-8"); + final ByteArrayInputStream bis = new ByteArrayInputStream(jsonBytes); + return new ServletInputStream() + { + @Override + public boolean isFinished() + { + return true; + } + + @Override + public boolean isReady() + { + return true; + } + + @Override + public int available() throws IOException + { + return jsonBytes.length; + } + + @Override + public void setReadListener(ReadListener readListener) + { + } + + @Override + public int read() throws IOException + { + return bis.read(); + } + }; + } + + /** + * 是否是Json请求 + * + * @param request + */ + public boolean isJsonRequest() + { + String header = super.getHeader(HttpHeaders.CONTENT_TYPE); + return StringUtils.startsWithIgnoreCase(header, MediaType.APPLICATION_JSON_VALUE); + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/Arith.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/Arith.java new file mode 100644 index 0000000..9f95c0f --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/Arith.java @@ -0,0 +1,113 @@ +package com.ruoyi.common.utils; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +/** + * 精确的浮点数运算 + * + * @author ruoyi + */ +public class Arith +{ + + /** 默认除法运算精度 */ + private static final int DEF_DIV_SCALE = 10; + + /** 这个类不能实例化 */ + private Arith() + { + } + + /** + * 提供精确的加法运算。 + * @param v1 被加数 + * @param v2 加数 + * @return 两个参数的和 + */ + public static double add(double v1, double v2) + { + BigDecimal b1 = new BigDecimal(Double.toString(v1)); + BigDecimal b2 = new BigDecimal(Double.toString(v2)); + return b1.add(b2).doubleValue(); + } + + /** + * 提供精确的减法运算。 + * @param v1 被减数 + * @param v2 减数 + * @return 两个参数的差 + */ + public static double sub(double v1, double v2) + { + BigDecimal b1 = new BigDecimal(Double.toString(v1)); + BigDecimal b2 = new BigDecimal(Double.toString(v2)); + return b1.subtract(b2).doubleValue(); + } + + /** + * 提供精确的乘法运算。 + * @param v1 被乘数 + * @param v2 乘数 + * @return 两个参数的积 + */ + public static double mul(double v1, double v2) + { + BigDecimal b1 = new BigDecimal(Double.toString(v1)); + BigDecimal b2 = new BigDecimal(Double.toString(v2)); + return b1.multiply(b2).doubleValue(); + } + + /** + * 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到 + * 小数点以后10位,以后的数字四舍五入。 + * @param v1 被除数 + * @param v2 除数 + * @return 两个参数的商 + */ + public static double div(double v1, double v2) + { + return div(v1, v2, DEF_DIV_SCALE); + } + + /** + * 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指 + * 定精度,以后的数字四舍五入。 + * @param v1 被除数 + * @param v2 除数 + * @param scale 表示表示需要精确到小数点以后几位。 + * @return 两个参数的商 + */ + public static double div(double v1, double v2, int scale) + { + if (scale < 0) + { + throw new IllegalArgumentException( + "The scale must be a positive integer or zero"); + } + BigDecimal b1 = new BigDecimal(Double.toString(v1)); + BigDecimal b2 = new BigDecimal(Double.toString(v2)); + if (b1.compareTo(BigDecimal.ZERO) == 0) + { + return BigDecimal.ZERO.doubleValue(); + } + return b1.divide(b2, scale, RoundingMode.HALF_UP).doubleValue(); + } + + /** + * 提供精确的小数位四舍五入处理。 + * @param v 需要四舍五入的数字 + * @param scale 小数点后保留几位 + * @return 四舍五入后的结果 + */ + public static double round(double v, int scale) + { + if (scale < 0) + { + throw new IllegalArgumentException( + "The scale must be a positive integer or zero"); + } + BigDecimal b = new BigDecimal(Double.toString(v)); + return b.divide(BigDecimal.ONE, scale, RoundingMode.HALF_UP).doubleValue(); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/DateUtils.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/DateUtils.java new file mode 100644 index 0000000..c1d4b55 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/DateUtils.java @@ -0,0 +1,192 @@ +package com.ruoyi.common.utils; + +import java.lang.management.ManagementFactory; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Date; +import org.apache.commons.lang3.time.DateFormatUtils; + +/** + * 时间工具类 + * + * @author ruoyi + */ +@SuppressWarnings("deprecation") +public class DateUtils extends org.apache.commons.lang3.time.DateUtils +{ + public static String YYYY = "yyyy"; + + public static String YYYY_MM = "yyyy-MM"; + + public static String YYYY_MM_DD = "yyyy-MM-dd"; + + public static String YYYYMMDDHHMMSS = "yyyyMMddHHmmss"; + + public static String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss"; + + private static String[] parsePatterns = { + "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-MM", + "yyyy/MM/dd", "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm", "yyyy/MM", + "yyyy.MM.dd", "yyyy.MM.dd HH:mm:ss", "yyyy.MM.dd HH:mm", "yyyy.MM"}; + + /** + * 获取当前Date型日期 + * + * @return Date() 当前日期 + */ + public static Date getNowDate() + { + return new Date(); + } + + /** + * 获取当前日期, 默认格式为yyyy-MM-dd + * + * @return String + */ + public static String getDate() + { + return dateTimeNow(YYYY_MM_DD); + } + + public static final String getTime() + { + return dateTimeNow(YYYY_MM_DD_HH_MM_SS); + } + + public static final String dateTimeNow() + { + return dateTimeNow(YYYYMMDDHHMMSS); + } + + public static final String dateTimeNow(final String format) + { + return parseDateToStr(format, new Date()); + } + + public static final String dateTime(final Date date) + { + return parseDateToStr(YYYY_MM_DD, date); + } + + public static final String parseDateToStr(final String format, final Date date) + { + return new SimpleDateFormat(format).format(date); + } + + public static final Date dateTime(final String format, final String ts) + { + try + { + return new SimpleDateFormat(format).parse(ts); + } + catch (ParseException e) + { + throw new RuntimeException(e); + } + } + + /** + * 日期路径 即年/月/日 如2018/08/08 + */ + public static final String datePath() + { + Date now = new Date(); + return DateFormatUtils.format(now, "yyyy/MM/dd"); + } + + /** + * 日期路径 即年/月/日 如20180808 + */ + public static final String dateTime() + { + Date now = new Date(); + return DateFormatUtils.format(now, "yyyyMMdd"); + } + + /** + * 日期型字符串转化为日期 格式 + */ + public static Date parseDate(Object str) + { + if (str == null) + { + return null; + } + try + { + return parseDate(str.toString(), parsePatterns); + } + catch (ParseException e) + { + return null; + } + } + + /** + * 获取服务器启动时间 + */ + public static Date getServerStartDate() + { + long time = ManagementFactory.getRuntimeMXBean().getStartTime(); + return new Date(time); + } + + /** + * 计算相差天数 + */ + public static int differentDaysByMillisecond(Date date1, Date date2) + { + return Math.abs((int) ((date2.getTime() - date1.getTime()) / (1000 * 3600 * 24))); + } + + /** + * 计算时间差 + * + * @param endDate 最后时间 + * @param startTime 开始时间 + * @return 时间差(天/小时/分钟) + */ + public static String timeDistance(Date endDate, Date startTime) + { + long nd = 1000 * 24 * 60 * 60; + long nh = 1000 * 60 * 60; + long nm = 1000 * 60; + // long ns = 1000; + // 获得两个时间的毫秒时间差异 + long diff = endDate.getTime() - startTime.getTime(); + // 计算差多少天 + long day = diff / nd; + // 计算差多少小时 + long hour = diff % nd / nh; + // 计算差多少分钟 + long min = diff % nd % nh / nm; + // 计算差多少秒//输出结果 + // long sec = diff % nd % nh % nm / ns; + return day + "天" + hour + "小时" + min + "分钟"; + } + + /** + * 增加 LocalDateTime ==> Date + */ + public static Date toDate(LocalDateTime temporalAccessor) + { + ZonedDateTime zdt = temporalAccessor.atZone(ZoneId.systemDefault()); + return Date.from(zdt.toInstant()); + } + + /** + * 增加 LocalDate ==> Date + */ + public static Date toDate(LocalDate temporalAccessor) + { + LocalDateTime localDateTime = LocalDateTime.of(temporalAccessor, LocalTime.of(0, 0, 0)); + ZonedDateTime zdt = localDateTime.atZone(ZoneId.systemDefault()); + return Date.from(zdt.toInstant()); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/DesensitizedUtil.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/DesensitizedUtil.java new file mode 100644 index 0000000..f8a4c02 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/DesensitizedUtil.java @@ -0,0 +1,49 @@ +package com.ruoyi.common.utils; + +/** + * 脱敏工具类 + * + * @author ruoyi + */ +public class DesensitizedUtil +{ + /** + * 密码的全部字符都用*代替,比如:****** + * + * @param password 密码 + * @return 脱敏后的密码 + */ + public static String password(String password) + { + if (StringUtils.isBlank(password)) + { + return StringUtils.EMPTY; + } + return StringUtils.repeat('*', password.length()); + } + + /** + * 车牌中间用*代替,如果是错误的车牌,不处理 + * + * @param carLicense 完整的车牌号 + * @return 脱敏后的车牌 + */ + public static String carLicense(String carLicense) + { + if (StringUtils.isBlank(carLicense)) + { + return StringUtils.EMPTY; + } + // 普通车牌 + if (carLicense.length() == 7) + { + carLicense = StringUtils.hide(carLicense, 3, 6); + } + else if (carLicense.length() == 8) + { + // 新能源车牌 + carLicense = StringUtils.hide(carLicense, 3, 7); + } + return carLicense; + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/DictUtils.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/DictUtils.java new file mode 100644 index 0000000..9d36bb7 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/DictUtils.java @@ -0,0 +1,217 @@ +package com.ruoyi.common.utils; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import com.alibaba.fastjson2.JSONArray; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.core.domain.entity.SysDictData; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.utils.spring.SpringUtils; + +/** + * 字典工具类 + * + * @author ruoyi + */ +public class DictUtils +{ + /** + * 分隔符 + */ + public static final String SEPARATOR = ","; + + /** + * 设置字典缓存 + * + * @param key 参数键 + * @param dictDatas 字典数据列表 + */ + public static void setDictCache(String key, List dictDatas) + { + SpringUtils.getBean(RedisCache.class).setCacheObject(getCacheKey(key), dictDatas); + } + + /** + * 获取字典缓存 + * + * @param key 参数键 + * @return dictDatas 字典数据列表 + */ + public static List getDictCache(String key) + { + JSONArray arrayCache = SpringUtils.getBean(RedisCache.class).getCacheObject(getCacheKey(key)); + if (StringUtils.isNotNull(arrayCache)) + { + return arrayCache.toList(SysDictData.class); + } + return null; + } + + /** + * 根据字典类型和字典值获取字典标签 + * + * @param dictType 字典类型 + * @param dictValue 字典值 + * @return 字典标签 + */ + public static String getDictLabel(String dictType, String dictValue) + { + if (StringUtils.isEmpty(dictValue)) + { + return StringUtils.EMPTY; + } + return getDictLabel(dictType, dictValue, SEPARATOR); + } + + /** + * 根据字典类型和字典标签获取字典值 + * + * @param dictType 字典类型 + * @param dictLabel 字典标签 + * @return 字典值 + */ + public static String getDictValue(String dictType, String dictLabel) + { + if (StringUtils.isEmpty(dictLabel)) + { + return StringUtils.EMPTY; + } + return getDictValue(dictType, dictLabel, SEPARATOR); + } + + /** + * 根据字典类型和字典值获取字典标签 + * + * @param dictType 字典类型 + * @param dictValue 字典值 + * @param separator 分隔符 + * @return 字典标签 + */ + public static String getDictLabel(String dictType, String dictValue, String separator) + { + List datas = getDictCache(dictType); + if (StringUtils.isNull(datas) || StringUtils.isEmpty(dictValue)) + { + return StringUtils.EMPTY; + } + Map dictMap = datas.stream().collect(HashMap::new, (map, dict) -> map.put(dict.getDictValue(), dict.getDictLabel()), Map::putAll); + if (!StringUtils.contains(dictValue, separator)) + { + return dictMap.getOrDefault(dictValue, StringUtils.EMPTY); + } + StringBuilder labelBuilder = new StringBuilder(); + for (String seperatedValue : dictValue.split(separator)) + { + if (dictMap.containsKey(seperatedValue)) + { + labelBuilder.append(dictMap.get(seperatedValue)).append(separator); + } + } + return StringUtils.removeEnd(labelBuilder.toString(), separator); + } + + /** + * 根据字典类型和字典标签获取字典值 + * + * @param dictType 字典类型 + * @param dictLabel 字典标签 + * @param separator 分隔符 + * @return 字典值 + */ + public static String getDictValue(String dictType, String dictLabel, String separator) + { + List datas = getDictCache(dictType); + if (StringUtils.isNull(datas) || StringUtils.isEmpty(dictLabel)) + { + return StringUtils.EMPTY; + } + Map dictMap = datas.stream().collect(HashMap::new, (map, dict) -> map.put(dict.getDictLabel(), dict.getDictValue()), Map::putAll); + if (!StringUtils.contains(dictLabel, separator)) + { + return dictMap.getOrDefault(dictLabel, StringUtils.EMPTY); + } + StringBuilder valueBuilder = new StringBuilder(); + for (String seperatedValue : dictLabel.split(separator)) + { + if (dictMap.containsKey(seperatedValue)) + { + valueBuilder.append(dictMap.get(seperatedValue)).append(separator); + } + } + return StringUtils.removeEnd(valueBuilder.toString(), separator); + } + + /** + * 根据字典类型获取字典所有值 + * + * @param dictType 字典类型 + * @return 字典值 + */ + public static String getDictValues(String dictType) + { + StringBuilder propertyString = new StringBuilder(); + List datas = getDictCache(dictType); + if (StringUtils.isNull(datas)) + { + return StringUtils.EMPTY; + } + for (SysDictData dict : datas) + { + propertyString.append(dict.getDictValue()).append(SEPARATOR); + } + return StringUtils.stripEnd(propertyString.toString(), SEPARATOR); + } + + /** + * 根据字典类型获取字典所有标签 + * + * @param dictType 字典类型 + * @return 字典值 + */ + public static String getDictLabels(String dictType) + { + StringBuilder propertyString = new StringBuilder(); + List datas = getDictCache(dictType); + if (StringUtils.isNull(datas)) + { + return StringUtils.EMPTY; + } + for (SysDictData dict : datas) + { + propertyString.append(dict.getDictLabel()).append(SEPARATOR); + } + return StringUtils.stripEnd(propertyString.toString(), SEPARATOR); + } + + /** + * 删除指定字典缓存 + * + * @param key 字典键 + */ + public static void removeDictCache(String key) + { + SpringUtils.getBean(RedisCache.class).deleteObject(getCacheKey(key)); + } + + /** + * 清空字典缓存 + */ + public static void clearDictCache() + { + Collection keys = SpringUtils.getBean(RedisCache.class).keys(CacheConstants.SYS_DICT_KEY + "*"); + SpringUtils.getBean(RedisCache.class).deleteObject(keys); + } + + /** + * 设置cache key + * + * @param configKey 参数键 + * @return 缓存键key + */ + public static String getCacheKey(String configKey) + { + return CacheConstants.SYS_DICT_KEY + configKey; + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/ExceptionUtil.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/ExceptionUtil.java new file mode 100644 index 0000000..214e4a0 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/ExceptionUtil.java @@ -0,0 +1,39 @@ +package com.ruoyi.common.utils; + +import java.io.PrintWriter; +import java.io.StringWriter; +import org.apache.commons.lang3.exception.ExceptionUtils; + +/** + * 错误信息处理类。 + * + * @author ruoyi + */ +public class ExceptionUtil +{ + /** + * 获取exception的详细错误信息。 + */ + public static String getExceptionMessage(Throwable e) + { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw, true)); + return sw.toString(); + } + + public static String getRootErrorMessage(Exception e) + { + Throwable root = ExceptionUtils.getRootCause(e); + root = (root == null ? e : root); + if (root == null) + { + return ""; + } + String msg = root.getMessage(); + if (msg == null) + { + return "null"; + } + return StringUtils.defaultString(msg); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/LogUtils.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/LogUtils.java new file mode 100644 index 0000000..0de30c6 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/LogUtils.java @@ -0,0 +1,18 @@ +package com.ruoyi.common.utils; + +/** + * 处理并记录日志文件 + * + * @author ruoyi + */ +public class LogUtils +{ + public static String getBlock(Object msg) + { + if (msg == null) + { + msg = ""; + } + return "[" + msg.toString() + "]"; + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/MessageUtils.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/MessageUtils.java new file mode 100644 index 0000000..7dac75a --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/MessageUtils.java @@ -0,0 +1,26 @@ +package com.ruoyi.common.utils; + +import org.springframework.context.MessageSource; +import org.springframework.context.i18n.LocaleContextHolder; +import com.ruoyi.common.utils.spring.SpringUtils; + +/** + * 获取i18n资源文件 + * + * @author ruoyi + */ +public class MessageUtils +{ + /** + * 根据消息键和参数 获取消息 委托给spring messageSource + * + * @param code 消息键 + * @param args 参数 + * @return 获取国际化翻译值 + */ + public static String message(String code, Object... args) + { + MessageSource messageSource = SpringUtils.getBean(MessageSource.class); + return messageSource.getMessage(code, args, LocaleContextHolder.getLocale()); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/PageUtils.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/PageUtils.java new file mode 100644 index 0000000..70e9b08 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/PageUtils.java @@ -0,0 +1,35 @@ +package com.ruoyi.common.utils; + +import com.github.pagehelper.PageHelper; +import com.ruoyi.common.core.page.PageDomain; +import com.ruoyi.common.core.page.TableSupport; +import com.ruoyi.common.utils.sql.SqlUtil; + +/** + * 分页工具类 + * + * @author ruoyi + */ +public class PageUtils extends PageHelper +{ + /** + * 设置请求分页数据 + */ + public static void startPage() + { + PageDomain pageDomain = TableSupport.buildPageRequest(); + Integer pageNum = pageDomain.getPageNum(); + Integer pageSize = pageDomain.getPageSize(); + String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy()); + Boolean reasonable = pageDomain.getReasonable(); + PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable); + } + + /** + * 清理分页的线程变量 + */ + public static void clearPage() + { + PageHelper.clearPage(); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/SecurityUtils.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/SecurityUtils.java new file mode 100644 index 0000000..8cd0a2f --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/SecurityUtils.java @@ -0,0 +1,188 @@ +package com.ruoyi.common.utils; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.util.PatternMatchUtils; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.constant.HttpStatus; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.exception.ServiceException; + +/** + * 安全服务工具类 + * + * @author ruoyi + */ +public class SecurityUtils +{ + + /** + * 用户ID + **/ + public static Long getUserId() + { + try + { + return getLoginUser().getUserId(); + } + catch (Exception e) + { + throw new ServiceException("获取用户ID异常", HttpStatus.UNAUTHORIZED); + } + } + + /** + * 获取部门ID + **/ + public static Long getDeptId() + { + try + { + return getLoginUser().getDeptId(); + } + catch (Exception e) + { + throw new ServiceException("获取部门ID异常", HttpStatus.UNAUTHORIZED); + } + } + + /** + * 获取用户账户 + **/ + public static String getUsername() + { + try + { + return getLoginUser().getUsername(); + } + catch (Exception e) + { + throw new ServiceException("获取用户账户异常", HttpStatus.UNAUTHORIZED); + } + } + + /** + * 获取用户 + **/ + public static LoginUser getLoginUser() + { + try + { + return (LoginUser) getAuthentication().getPrincipal(); + } + catch (Exception e) + { + throw new ServiceException("获取用户信息异常", HttpStatus.UNAUTHORIZED); + } + } + + /** + * 获取Authentication + */ + public static Authentication getAuthentication() + { + return SecurityContextHolder.getContext().getAuthentication(); + } + + /** + * 生成BCryptPasswordEncoder密码 + * + * @param password 密码 + * @return 加密字符串 + */ + public static String encryptPassword(String password) + { + BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + return passwordEncoder.encode(password); + } + + /** + * 判断密码是否相同 + * + * @param rawPassword 真实密码 + * @param encodedPassword 加密后字符 + * @return 结果 + */ + public static boolean matchesPassword(String rawPassword, String encodedPassword) + { + BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + return passwordEncoder.matches(rawPassword, encodedPassword); + } + + /** + * 是否为管理员 + * + * @return 结果 + */ + public static boolean isAdmin() + { + return isAdmin(getUserId()); + } + + /** + * 是否为管理员 + * + * @param userId 用户ID + * @return 结果 + */ + public static boolean isAdmin(Long userId) + { + return userId != null && 1L == userId; + } + + /** + * 验证用户是否具备某权限 + * + * @param permission 权限字符串 + * @return 用户是否具备某权限 + */ + public static boolean hasPermi(String permission) + { + return hasPermi(getLoginUser().getPermissions(), permission); + } + + /** + * 判断是否包含权限 + * + * @param authorities 权限列表 + * @param permission 权限字符串 + * @return 用户是否具备某权限 + */ + public static boolean hasPermi(Collection authorities, String permission) + { + return authorities.stream().filter(StringUtils::hasText) + .anyMatch(x -> Constants.ALL_PERMISSION.equals(x) || PatternMatchUtils.simpleMatch(x, permission)); + } + + /** + * 验证用户是否拥有某个角色 + * + * @param role 角色标识 + * @return 用户是否具备某角色 + */ + public static boolean hasRole(String role) + { + List roleList = getLoginUser().getUser().getRoles(); + Collection roles = roleList.stream().map(SysRole::getRoleKey).collect(Collectors.toSet()); + return hasRole(roles, role); + } + + /** + * 判断是否包含角色 + * + * @param roles 角色列表 + * @param role 角色 + * @return 用户是否具备某角色权限 + */ + public static boolean hasRole(Collection roles, String role) + { + return roles.stream().filter(StringUtils::hasText) + .anyMatch(x -> Constants.SUPER_ADMIN.equals(x) || PatternMatchUtils.simpleMatch(x, role)); + } + +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/ServletUtils.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/ServletUtils.java new file mode 100644 index 0000000..5635db7 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/ServletUtils.java @@ -0,0 +1,218 @@ +package com.ruoyi.common.utils; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.core.text.Convert; + +/** + * 客户端工具类 + * + * @author ruoyi + */ +public class ServletUtils +{ + /** + * 获取String参数 + */ + public static String getParameter(String name) + { + return getRequest().getParameter(name); + } + + /** + * 获取String参数 + */ + public static String getParameter(String name, String defaultValue) + { + return Convert.toStr(getRequest().getParameter(name), defaultValue); + } + + /** + * 获取Integer参数 + */ + public static Integer getParameterToInt(String name) + { + return Convert.toInt(getRequest().getParameter(name)); + } + + /** + * 获取Integer参数 + */ + public static Integer getParameterToInt(String name, Integer defaultValue) + { + return Convert.toInt(getRequest().getParameter(name), defaultValue); + } + + /** + * 获取Boolean参数 + */ + public static Boolean getParameterToBool(String name) + { + return Convert.toBool(getRequest().getParameter(name)); + } + + /** + * 获取Boolean参数 + */ + public static Boolean getParameterToBool(String name, Boolean defaultValue) + { + return Convert.toBool(getRequest().getParameter(name), defaultValue); + } + + /** + * 获得所有请求参数 + * + * @param request 请求对象{@link ServletRequest} + * @return Map + */ + public static Map getParams(ServletRequest request) + { + final Map map = request.getParameterMap(); + return Collections.unmodifiableMap(map); + } + + /** + * 获得所有请求参数 + * + * @param request 请求对象{@link ServletRequest} + * @return Map + */ + public static Map getParamMap(ServletRequest request) + { + Map params = new HashMap<>(); + for (Map.Entry entry : getParams(request).entrySet()) + { + params.put(entry.getKey(), StringUtils.join(entry.getValue(), ",")); + } + return params; + } + + /** + * 获取request + */ + public static HttpServletRequest getRequest() + { + return getRequestAttributes().getRequest(); + } + + /** + * 获取response + */ + public static HttpServletResponse getResponse() + { + return getRequestAttributes().getResponse(); + } + + /** + * 获取session + */ + public static HttpSession getSession() + { + return getRequest().getSession(); + } + + public static ServletRequestAttributes getRequestAttributes() + { + RequestAttributes attributes = RequestContextHolder.getRequestAttributes(); + return (ServletRequestAttributes) attributes; + } + + /** + * 将字符串渲染到客户端 + * + * @param response 渲染对象 + * @param string 待渲染的字符串 + */ + public static void renderString(HttpServletResponse response, String string) + { + try + { + response.setStatus(200); + response.setContentType("application/json"); + response.setCharacterEncoding("utf-8"); + response.getWriter().print(string); + } + catch (IOException e) + { + e.printStackTrace(); + } + } + + /** + * 是否是Ajax异步请求 + * + * @param request + */ + public static boolean isAjaxRequest(HttpServletRequest request) + { + String accept = request.getHeader("accept"); + if (accept != null && accept.contains("application/json")) + { + return true; + } + + String xRequestedWith = request.getHeader("X-Requested-With"); + if (xRequestedWith != null && xRequestedWith.contains("XMLHttpRequest")) + { + return true; + } + + String uri = request.getRequestURI(); + if (StringUtils.inStringIgnoreCase(uri, ".json", ".xml")) + { + return true; + } + + String ajax = request.getParameter("__ajax"); + return StringUtils.inStringIgnoreCase(ajax, "json", "xml"); + } + + /** + * 内容编码 + * + * @param str 内容 + * @return 编码后的内容 + */ + public static String urlEncode(String str) + { + try + { + return URLEncoder.encode(str, Constants.UTF8); + } + catch (UnsupportedEncodingException e) + { + return StringUtils.EMPTY; + } + } + + /** + * 内容解码 + * + * @param str 内容 + * @return 解码后的内容 + */ + public static String urlDecode(String str) + { + try + { + return URLDecoder.decode(str, Constants.UTF8); + } + catch (UnsupportedEncodingException e) + { + return StringUtils.EMPTY; + } + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/StringUtils.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/StringUtils.java new file mode 100644 index 0000000..f2770a6 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/StringUtils.java @@ -0,0 +1,921 @@ +package com.ruoyi.common.utils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.commons.lang3.Strings; +import org.springframework.util.AntPathMatcher; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.core.text.StrFormatter; + +/** + * 字符串工具类 + * + * @author ruoyi + */ +@SuppressWarnings("deprecation") +public class StringUtils extends org.apache.commons.lang3.StringUtils +{ + /** 空字符串 */ + private static final String NULLSTR = ""; + + /** 下划线 */ + private static final char SEPARATOR = '_'; + + /** 星号 */ + private static final char ASTERISK = '*'; + + /** + * 获取参数不为空值 + * + * @param value defaultValue 要判断的value + * @return value 返回值 + */ + public static T nvl(T value, T defaultValue) + { + return value != null ? value : defaultValue; + } + + /** + * * 判断一个Collection是否为空, 包含List,Set,Queue + * + * @param coll 要判断的Collection + * @return true:为空 false:非空 + */ + public static boolean isEmpty(Collection coll) + { + return isNull(coll) || coll.isEmpty(); + } + + /** + * * 判断一个Collection是否非空,包含List,Set,Queue + * + * @param coll 要判断的Collection + * @return true:非空 false:空 + */ + public static boolean isNotEmpty(Collection coll) + { + return !isEmpty(coll); + } + + /** + * * 判断一个对象数组是否为空 + * + * @param objects 要判断的对象数组 + ** @return true:为空 false:非空 + */ + public static boolean isEmpty(Object[] objects) + { + return isNull(objects) || (objects.length == 0); + } + + /** + * * 判断一个对象数组是否非空 + * + * @param objects 要判断的对象数组 + * @return true:非空 false:空 + */ + public static boolean isNotEmpty(Object[] objects) + { + return !isEmpty(objects); + } + + /** + * * 判断一个Map是否为空 + * + * @param map 要判断的Map + * @return true:为空 false:非空 + */ + public static boolean isEmpty(Map map) + { + return isNull(map) || map.isEmpty(); + } + + /** + * * 判断一个Map是否为空 + * + * @param map 要判断的Map + * @return true:非空 false:空 + */ + public static boolean isNotEmpty(Map map) + { + return !isEmpty(map); + } + + /** + * * 判断一个字符串是否为空串 + * + * @param str String + * @return true:为空 false:非空 + */ + public static boolean isEmpty(String str) + { + return isNull(str) || NULLSTR.equals(str.trim()); + } + + /** + * * 判断一个字符串是否为非空串 + * + * @param str String + * @return true:非空串 false:空串 + */ + public static boolean isNotEmpty(String str) + { + return !isEmpty(str); + } + + /** + * * 判断一个对象是否为空 + * + * @param object Object + * @return true:为空 false:非空 + */ + public static boolean isNull(Object object) + { + return object == null; + } + + /** + * * 判断一个对象是否非空 + * + * @param object Object + * @return true:非空 false:空 + */ + public static boolean isNotNull(Object object) + { + return !isNull(object); + } + + /** + * * 判断一个对象是否是数组类型(Java基本型别的数组) + * + * @param object 对象 + * @return true:是数组 false:不是数组 + */ + public static boolean isArray(Object object) + { + return isNotNull(object) && object.getClass().isArray(); + } + + /** + * 去空格 + */ + public static String trim(String str) + { + return (str == null ? "" : str.trim()); + } + + /** + * 替换指定字符串的指定区间内字符为"*" + * + * @param str 字符串 + * @param startInclude 开始位置(包含) + * @param endExclude 结束位置(不包含) + * @return 替换后的字符串 + */ + public static String hide(CharSequence str, int startInclude, int endExclude) + { + if (isEmpty(str)) + { + return NULLSTR; + } + final int strLength = str.length(); + if (startInclude > strLength) + { + return NULLSTR; + } + if (endExclude > strLength) + { + endExclude = strLength; + } + if (startInclude > endExclude) + { + // 如果起始位置大于结束位置,不替换 + return NULLSTR; + } + final char[] chars = new char[strLength]; + for (int i = 0; i < strLength; i++) + { + if (i >= startInclude && i < endExclude) + { + chars[i] = ASTERISK; + } + else + { + chars[i] = str.charAt(i); + } + } + return new String(chars); + } + + /** + * 截取字符串 + * + * @param str 字符串 + * @param start 开始 + * @return 结果 + */ + public static String substring(final String str, int start) + { + if (str == null) + { + return NULLSTR; + } + + if (start < 0) + { + start = str.length() + start; + } + + if (start < 0) + { + start = 0; + } + if (start > str.length()) + { + return NULLSTR; + } + + return str.substring(start); + } + + /** + * 截取字符串 + * + * @param str 字符串 + * @param start 开始 + * @param end 结束 + * @return 结果 + */ + public static String substring(final String str, int start, int end) + { + if (str == null) + { + return NULLSTR; + } + + if (end < 0) + { + end = str.length() + end; + } + if (start < 0) + { + start = str.length() + start; + } + + if (end > str.length()) + { + end = str.length(); + } + + if (start > end) + { + return NULLSTR; + } + + if (start < 0) + { + start = 0; + } + if (end < 0) + { + end = 0; + } + + return str.substring(start, end); + } + + /** + * 在字符串中查找第一个出现的 `open` 和最后一个出现的 `close` 之间的子字符串 + * + * @param str 要截取的字符串 + * @param open 起始字符串 + * @param close 结束字符串 + * @return 截取结果 + */ + public static String substringBetweenLast(final String str, final String open, final String close) + { + if (isEmpty(str) || isEmpty(open) || isEmpty(close)) + { + return NULLSTR; + } + final int start = str.indexOf(open); + if (start != INDEX_NOT_FOUND) + { + final int end = str.lastIndexOf(close); + if (end != INDEX_NOT_FOUND) + { + return str.substring(start + open.length(), end); + } + } + return NULLSTR; + } + + /** + * 判断是否为空,并且不是空白字符 + * + * @param str 要判断的value + * @return 结果 + */ + public static boolean hasText(String str) + { + return (str != null && !str.isEmpty() && containsText(str)); + } + + private static boolean containsText(CharSequence str) + { + int strLen = str.length(); + for (int i = 0; i < strLen; i++) + { + if (!Character.isWhitespace(str.charAt(i))) + { + return true; + } + } + return false; + } + + /** + * 格式化文本, {} 表示占位符
+ * 此方法只是简单将占位符 {} 按照顺序替换为参数
+ * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可
+ * 例:
+ * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b
+ * 转义{}: format("this is \\{} for {}", "a", "b") -> this is \{} for a
+ * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b
+ * + * @param template 文本模板,被替换的部分用 {} 表示 + * @param params 参数值 + * @return 格式化后的文本 + */ + public static String format(String template, Object... params) + { + if (isEmpty(params) || isEmpty(template)) + { + return template; + } + return StrFormatter.format(template, params); + } + + /** + * 是否为http(s)://开头 + * + * @param link 链接 + * @return 结果 + */ + public static boolean ishttp(String link) + { + return StringUtils.startsWithAny(link, Constants.HTTP, Constants.HTTPS); + } + + /** + * 字符串转set + * + * @param str 字符串 + * @param sep 分隔符 + * @return set集合 + */ + public static final Set str2Set(String str, String sep) + { + return new HashSet(str2List(str, sep, true, false)); + } + + /** + * 字符串转list + * + * @param str 字符串 + * @param sep 分隔符 + * @return list集合 + */ + public static final List str2List(String str, String sep) + { + return str2List(str, sep, true, false); + } + + /** + * 字符串转list + * + * @param str 字符串 + * @param sep 分隔符 + * @param filterBlank 过滤纯空白 + * @param trim 去掉首尾空白 + * @return list集合 + */ + public static final List str2List(String str, String sep, boolean filterBlank, boolean trim) + { + List list = new ArrayList(); + if (StringUtils.isEmpty(str)) + { + return list; + } + + // 过滤空白字符串 + if (filterBlank && StringUtils.isBlank(str)) + { + return list; + } + String[] split = str.split(sep); + for (String string : split) + { + if (filterBlank && StringUtils.isBlank(string)) + { + continue; + } + if (trim) + { + string = string.trim(); + } + list.add(string); + } + + return list; + } + + /** + * 检查子字符串是否存在 + * + * @param seq 检查的字符串 + * @param searchSeq 查找的字符串 + * @return 结果 + */ + public static boolean contains(final CharSequence seq, final CharSequence searchSeq) + { + return Strings.CS.contains(seq, searchSeq); + } + + /** + * 判断给定的collection列表中是否包含数组array 判断给定的数组array中是否包含给定的元素value + * + * @param collection 给定的集合 + * @param array 给定的数组 + * @return 结果 + */ + public static boolean containsAny(Collection collection, String... array) + { + if (isEmpty(collection) || isEmpty(array)) + { + return false; + } + else + { + for (String str : array) + { + if (collection.contains(str)) + { + return true; + } + } + return false; + } + } + + /** + * 判断是否包含给定数组中的任意一个。 + * + * @param cs 要判断的字符串 + * @param searchCharSequences 要判断的数组 + * @return 结果 + */ + public static boolean containsAny(final CharSequence cs, final CharSequence... searchCharSequences) + { + return Strings.CS.containsAny(cs, searchCharSequences); + } + + /** + * 查找指定字符串是否包含指定字符串列表中的任意一个字符串同时串忽略大小写 + * + * @param cs 指定字符串 + * @param searchCharSequences 需要检查的字符串数组 + * @return 是否包含任意一个字符串 + */ + public static boolean containsAnyIgnoreCase(CharSequence cs, CharSequence... searchCharSequences) + { + if (isEmpty(cs) || isEmpty(searchCharSequences)) + { + return false; + } + for (CharSequence testStr : searchCharSequences) + { + if (containsIgnoreCase(cs, testStr)) + { + return true; + } + } + return false; + } + + /** + * 检查是否包含要搜索的字符串,忽略大小写 + * + * @param str 要检查的字符串 + * @param searchStr 要查找的字符串 + * @return 如果包含要搜索的字符串(忽略大小写)则返回true,如果不包含或返回false + */ + public static boolean containsIgnoreCase(final CharSequence str, final CharSequence searchStr) + { + return Strings.CI.contains(str, searchStr); + } + + /** + * 检查字符串是否以任意前缀开始 + * + * @param sequence 要检查的字符串 + * @param searchStrings 区分大小写的字符串前缀数组 + * @return 结果 + */ + public static boolean startsWithAny(final CharSequence sequence, final CharSequence... searchStrings) + { + return Strings.CS.startsWithAny(sequence, searchStrings); + } + + /** + * 不区分大小写地检查一个字符串是否以指定前缀开头。 + * + * @param str 待检查的字符串 + * @param 要查找的前缀 + * @return 结果 + */ + public static boolean startsWithIgnoreCase(final CharSequence str, final CharSequence prefix) + { + return Strings.CI.startsWith(str, prefix); + } + + /** + * 比较两个字符串是否相同 + * + * @param cs1 第一个字符串 + * @param cs2 第二个字符串 + * @return 如果给定对象与字符串相等,则返回 true;否则返回 false + */ + public static boolean equals(final CharSequence cs1, final CharSequence cs2) + { + return Strings.CS.equals(cs1, cs2); + } + + /** + * 替换字符串中所有匹配的字符 + * + * @param text 要搜索和替换的文本 + * @param searchString 要搜索的字符串 + * @param replacement 用于替换的字符串 + * @return 处理完所有替换后的文本 + */ + public static String replace(final String text, final String searchString, final String replacement) + { + return Strings.CS.replace(text, searchString, replacement); + } + + /** + * 仅当子字符串位于源字符串末尾时才将其移除,否则返回源字符串。 + * @param str 要搜索的源字符串 + * @param remove 要搜索并移除的字符串 + * @return 如果找到并移除了字符串,则返回移除后的子字符串 + */ + public static String removeEnd(final String str, final String remove) + { + return Strings.CS.removeEnd(str, remove); + } + + /** + * 查找字符串首次出现位置的索引 + * + * @param seq 要检查的字符串 + * @param searchSeq 要查找的字符串 + * @return 返回指定字符在字符串中第一次出现处的索引,如果此字符串中没有这样的字符,则返回 -1 + */ + public static int indexOf(final CharSequence seq, final CharSequence searchSeq) + { + return Strings.CS.indexOf(seq, searchSeq); + } + + /** + * 检查字符串是否以指定的后缀结尾 + * + * @param str 要检查的字符 + * @param suffix 要检查的后缀 + * @return 若参数与该字符串末尾相符 true;否则 false + */ + public static boolean endsWith(final CharSequence str, final CharSequence suffix) + { + return Strings.CS.endsWith(str, suffix); + } + + /** + * 将给定的字符串与数组进行比较 + * + * @param string 要比较的字符串 + * @param searchStrings 字符串数组 + * @return 如果字符串等于(区分大小写){@code searchStrings}中的任意其他元素,则返回true;如果{@code searchStrings}为null或不包含匹配项,则返回false + */ + public static boolean equalsAny(final CharSequence string, final CharSequence... searchStrings) + { + return Strings.CS.equalsAny(string, searchStrings); + } + + /** + * 检查一个字符串是否以任意提供的区分大小写的后缀结尾。 + * + * @param sequence 要检查的字符串 + * @param searchStrings 要查找的区分大小写的字符串数组 + * @return 如果输入参数{@code sequence}为null且未提供任何{@code searchStrings},或者输入{@code sequence}以任意提供的区分大小写的{@code searchStrings}结尾,则返回{@code true}。 + */ + public static boolean endsWithAny(final CharSequence sequence, final CharSequence... searchStrings) + { + return Strings.CS.endsWithAny(sequence, searchStrings); + } + + /** + * 不区分大小写地检查字符序列是否以指定的后缀结尾 + * + * @param str 要检查的字符序列 + * @param suffix 要查找的后缀 + * @return 如果字符序列以该后缀结尾(不区分大小写),或两者均为{@code null},则返回{@code true} + */ + public static boolean endsWithIgnoreCase(final CharSequence str, final CharSequence suffix) + { + return Strings.CI.endsWith(str, suffix); + } + + /** + * 指定范围内查找字符串,忽略大小写 + * + * @param str 要检查的字符串 + * @param searchStr 要查找的字符串 + * @return 搜索字符串的第一个索引,如果未找到匹配项则返回 -1 + */ + public static int indexOfIgnoreCase(final CharSequence str, final CharSequence searchStr) + { + return Strings.CI.indexOf(str, searchStr); + } + + /** + * Compares given {@code string} to a CharSequences vararg of {@code searchStrings}, + * returning {@code true} if the {@code string} is equal to any of the {@code searchStrings}, ignoring case. + * + * @param string to compare, may be {@code null}. + * @param searchStrings a vararg of strings, may be {@code null}. + * @return {@code true} if the string is equal (case-insensitive) to any other element of {@code searchStrings}; + */ + public static boolean equalsAnyIgnoreCase(final CharSequence string, final CharSequence... searchStrings) + { + return Strings.CI.equalsAny(string, searchStrings); + } + + /** + * 驼峰转下划线命名 + */ + public static String toUnderScoreCase(String str) + { + if (str == null) + { + return null; + } + StringBuilder sb = new StringBuilder(); + // 前置字符是否大写 + boolean preCharIsUpperCase = true; + // 当前字符是否大写 + boolean curreCharIsUpperCase = true; + // 下一字符是否大写 + boolean nexteCharIsUpperCase = true; + for (int i = 0; i < str.length(); i++) + { + char c = str.charAt(i); + if (i > 0) + { + preCharIsUpperCase = Character.isUpperCase(str.charAt(i - 1)); + } + else + { + preCharIsUpperCase = false; + } + + curreCharIsUpperCase = Character.isUpperCase(c); + + if (i < (str.length() - 1)) + { + nexteCharIsUpperCase = Character.isUpperCase(str.charAt(i + 1)); + } + + if (preCharIsUpperCase && curreCharIsUpperCase && !nexteCharIsUpperCase) + { + sb.append(SEPARATOR); + } + else if ((i != 0 && !preCharIsUpperCase) && curreCharIsUpperCase) + { + sb.append(SEPARATOR); + } + sb.append(Character.toLowerCase(c)); + } + + return sb.toString(); + } + + /** + * 是否包含字符串 + * + * @param str 验证字符串 + * @param strs 字符串组 + * @return 包含返回true + */ + public static boolean inStringIgnoreCase(String str, String... strs) + { + if (str != null && strs != null) + { + for (String s : strs) + { + if (str.equalsIgnoreCase(trim(s))) + { + return true; + } + } + } + return false; + } + + /** + * 删除最后一个字符串 + * + * @param str 输入字符串 + * @param spit 以什么类型结尾的 + * @return 截取后的字符串 + */ + public static String lastStringDel(String str, String spit) + { + if (!StringUtils.isEmpty(str) && str.endsWith(spit)) + { + return str.subSequence(0, str.length() - 1).toString(); + } + return str; + } + + /** + * 将下划线大写方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。 例如:HELLO_WORLD->HelloWorld + * + * @param name 转换前的下划线大写方式命名的字符串 + * @return 转换后的驼峰式命名的字符串 + */ + public static String convertToCamelCase(String name) + { + StringBuilder result = new StringBuilder(); + // 快速检查 + if (name == null || name.isEmpty()) + { + // 没必要转换 + return ""; + } + else if (!name.contains("_")) + { + // 不含下划线,仅将首字母大写 + return name.substring(0, 1).toUpperCase() + name.substring(1); + } + // 用下划线将原始字符串分割 + String[] camels = name.split("_"); + for (String camel : camels) + { + // 跳过原始字符串中开头、结尾的下换线或双重下划线 + if (camel.isEmpty()) + { + continue; + } + // 首字母大写 + result.append(camel.substring(0, 1).toUpperCase()); + result.append(camel.substring(1).toLowerCase()); + } + return result.toString(); + } + + /** + * 驼峰式命名法 + * 例如:user_name->userName + */ + public static String toCamelCase(String s) + { + if (s == null) + { + return null; + } + if (s.indexOf(SEPARATOR) == -1) + { + return s; + } + s = s.toLowerCase(); + StringBuilder sb = new StringBuilder(s.length()); + boolean upperCase = false; + for (int i = 0; i < s.length(); i++) + { + char c = s.charAt(i); + + if (c == SEPARATOR) + { + upperCase = true; + } + else if (upperCase) + { + sb.append(Character.toUpperCase(c)); + upperCase = false; + } + else + { + sb.append(c); + } + } + return sb.toString(); + } + + /** + * 查找指定字符串是否匹配指定字符串列表中的任意一个字符串 + * + * @param str 指定字符串 + * @param strs 需要检查的字符串数组 + * @return 是否匹配 + */ + public static boolean matches(String str, List strs) + { + if (isEmpty(str) || isEmpty(strs)) + { + return false; + } + for (String pattern : strs) + { + if (isMatch(pattern, str)) + { + return true; + } + } + return false; + } + + /** + * 判断url是否与规则配置: + * ? 表示单个字符; + * * 表示一层路径内的任意字符串,不可跨层级; + * ** 表示任意层路径; + * + * @param pattern 匹配规则 + * @param url 需要匹配的url + * @return + */ + public static boolean isMatch(String pattern, String url) + { + AntPathMatcher matcher = new AntPathMatcher(); + return matcher.match(pattern, url); + } + + @SuppressWarnings("unchecked") + public static T cast(Object obj) + { + return (T) obj; + } + + /** + * 数字左边补齐0,使之达到指定长度。注意,如果数字转换为字符串后,长度大于size,则只保留 最后size个字符。 + * + * @param num 数字对象 + * @param size 字符串指定长度 + * @return 返回数字的字符串格式,该字符串为指定长度。 + */ + public static final String padl(final Number num, final int size) + { + return padl(num.toString(), size, '0'); + } + + /** + * 字符串左补齐。如果原始字符串s长度大于size,则只保留最后size个字符。 + * + * @param s 原始字符串 + * @param size 字符串指定长度 + * @param c 用于补齐的字符 + * @return 返回指定长度的字符串,由原字符串左补齐或截取得到。 + */ + public static final String padl(final String s, final int size, final char c) + { + final StringBuilder sb = new StringBuilder(size); + if (s != null) + { + final int len = s.length(); + if (s.length() <= size) + { + for (int i = size - len; i > 0; i--) + { + sb.append(c); + } + sb.append(s); + } + else + { + return s.substring(len - size, len); + } + } + else + { + for (int i = size; i > 0; i--) + { + sb.append(c); + } + } + return sb.toString(); + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/Threads.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/Threads.java new file mode 100644 index 0000000..71fe6d5 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/Threads.java @@ -0,0 +1,99 @@ +package com.ruoyi.common.utils; + +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 线程相关工具类. + * + * @author ruoyi + */ +public class Threads +{ + private static final Logger logger = LoggerFactory.getLogger(Threads.class); + + /** + * sleep等待,单位为毫秒 + */ + public static void sleep(long milliseconds) + { + try + { + Thread.sleep(milliseconds); + } + catch (InterruptedException e) + { + return; + } + } + + /** + * 停止线程池 + * 先使用shutdown, 停止接收新任务并尝试完成所有已存在任务. + * 如果超时, 则调用shutdownNow, 取消在workQueue中Pending的任务,并中断所有阻塞函数. + * 如果仍然超時,則強制退出. + * 另对在shutdown时线程本身被调用中断做了处理. + */ + public static void shutdownAndAwaitTermination(ExecutorService pool) + { + if (pool != null && !pool.isShutdown()) + { + pool.shutdown(); + try + { + if (!pool.awaitTermination(120, TimeUnit.SECONDS)) + { + pool.shutdownNow(); + if (!pool.awaitTermination(120, TimeUnit.SECONDS)) + { + logger.info("Pool did not terminate"); + } + } + } + catch (InterruptedException ie) + { + pool.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + } + + /** + * 打印线程异常信息 + */ + public static void printException(Runnable r, Throwable t) + { + if (t == null && r instanceof Future) + { + try + { + Future future = (Future) r; + if (future.isDone()) + { + future.get(); + } + } + catch (CancellationException ce) + { + t = ce; + } + catch (ExecutionException ee) + { + t = ee.getCause(); + } + catch (InterruptedException ie) + { + Thread.currentThread().interrupt(); + } + } + if (t != null) + { + logger.error(t.getMessage(), t); + } + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/bean/BeanUtils.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/bean/BeanUtils.java new file mode 100644 index 0000000..4463662 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/bean/BeanUtils.java @@ -0,0 +1,110 @@ +package com.ruoyi.common.utils.bean; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Bean 工具类 + * + * @author ruoyi + */ +public class BeanUtils extends org.springframework.beans.BeanUtils +{ + /** Bean方法名中属性名开始的下标 */ + private static final int BEAN_METHOD_PROP_INDEX = 3; + + /** * 匹配getter方法的正则表达式 */ + private static final Pattern GET_PATTERN = Pattern.compile("get(\\p{javaUpperCase}\\w*)"); + + /** * 匹配setter方法的正则表达式 */ + private static final Pattern SET_PATTERN = Pattern.compile("set(\\p{javaUpperCase}\\w*)"); + + /** + * Bean属性复制工具方法。 + * + * @param dest 目标对象 + * @param src 源对象 + */ + public static void copyBeanProp(Object dest, Object src) + { + try + { + copyProperties(src, dest); + } + catch (Exception e) + { + e.printStackTrace(); + } + } + + /** + * 获取对象的setter方法。 + * + * @param obj 对象 + * @return 对象的setter方法列表 + */ + public static List getSetterMethods(Object obj) + { + // setter方法列表 + List setterMethods = new ArrayList(); + + // 获取所有方法 + Method[] methods = obj.getClass().getMethods(); + + // 查找setter方法 + + for (Method method : methods) + { + Matcher m = SET_PATTERN.matcher(method.getName()); + if (m.matches() && (method.getParameterTypes().length == 1)) + { + setterMethods.add(method); + } + } + // 返回setter方法列表 + return setterMethods; + } + + /** + * 获取对象的getter方法。 + * + * @param obj 对象 + * @return 对象的getter方法列表 + */ + + public static List getGetterMethods(Object obj) + { + // getter方法列表 + List getterMethods = new ArrayList(); + // 获取所有方法 + Method[] methods = obj.getClass().getMethods(); + // 查找getter方法 + for (Method method : methods) + { + Matcher m = GET_PATTERN.matcher(method.getName()); + if (m.matches() && (method.getParameterTypes().length == 0)) + { + getterMethods.add(method); + } + } + // 返回getter方法列表 + return getterMethods; + } + + /** + * 检查Bean方法名中的属性名是否相等。
+ * 如getName()和setName()属性名一样,getName()和setAge()属性名不一样。 + * + * @param m1 方法名1 + * @param m2 方法名2 + * @return 属性名一样返回true,否则返回false + */ + + public static boolean isMethodPropEquals(String m1, String m2) + { + return m1.substring(BEAN_METHOD_PROP_INDEX).equals(m2.substring(BEAN_METHOD_PROP_INDEX)); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/bean/BeanValidators.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/bean/BeanValidators.java new file mode 100644 index 0000000..898aca5 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/bean/BeanValidators.java @@ -0,0 +1,24 @@ +package com.ruoyi.common.utils.bean; + +import java.util.Set; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.Validator; + +/** + * bean对象属性验证 + * + * @author ruoyi + */ +public class BeanValidators +{ + public static void validateWithException(Validator validator, Object object, Class... groups) + throws ConstraintViolationException + { + Set> constraintViolations = validator.validate(object, groups); + if (!constraintViolations.isEmpty()) + { + throw new ConstraintViolationException(constraintViolations); + } + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileTypeUtils.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileTypeUtils.java new file mode 100644 index 0000000..68130b9 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileTypeUtils.java @@ -0,0 +1,76 @@ +package com.ruoyi.common.utils.file; + +import java.io.File; +import org.apache.commons.lang3.StringUtils; + +/** + * 文件类型工具类 + * + * @author ruoyi + */ +public class FileTypeUtils +{ + /** + * 获取文件类型 + *

+ * 例如: ruoyi.txt, 返回: txt + * + * @param file 文件名 + * @return 后缀(不含".") + */ + public static String getFileType(File file) + { + if (null == file) + { + return StringUtils.EMPTY; + } + return getFileType(file.getName()); + } + + /** + * 获取文件类型 + *

+ * 例如: ruoyi.txt, 返回: txt + * + * @param fileName 文件名 + * @return 后缀(不含".") + */ + public static String getFileType(String fileName) + { + int separatorIndex = fileName.lastIndexOf("."); + if (separatorIndex < 0) + { + return ""; + } + return fileName.substring(separatorIndex + 1).toLowerCase(); + } + + /** + * 获取文件类型 + * + * @param photoByte 文件字节码 + * @return 后缀(不含".") + */ + public static String getFileExtendName(byte[] photoByte) + { + String strFileExtendName = "JPG"; + if ((photoByte[0] == 71) && (photoByte[1] == 73) && (photoByte[2] == 70) && (photoByte[3] == 56) + && ((photoByte[4] == 55) || (photoByte[4] == 57)) && (photoByte[5] == 97)) + { + strFileExtendName = "GIF"; + } + else if ((photoByte[6] == 74) && (photoByte[7] == 70) && (photoByte[8] == 73) && (photoByte[9] == 70)) + { + strFileExtendName = "JPG"; + } + else if ((photoByte[0] == 66) && (photoByte[1] == 77)) + { + strFileExtendName = "BMP"; + } + else if ((photoByte[1] == 80) && (photoByte[2] == 78) && (photoByte[3] == 71)) + { + strFileExtendName = "PNG"; + } + return strFileExtendName; + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUploadUtils.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUploadUtils.java new file mode 100644 index 0000000..cc1b6f4 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUploadUtils.java @@ -0,0 +1,260 @@ +package com.ruoyi.common.utils.file; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.Objects; +import org.apache.commons.io.FilenameUtils; +import org.springframework.web.multipart.MultipartFile; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.exception.file.FileNameLengthLimitExceededException; +import com.ruoyi.common.exception.file.FileSizeLimitExceededException; +import com.ruoyi.common.exception.file.InvalidExtensionException; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.uuid.IdUtils; +import com.ruoyi.common.utils.uuid.Seq; + +/** + * 文件上传工具类 + * + * @author ruoyi + */ +public class FileUploadUtils +{ + /** + * 默认大小 50M + */ + public static final long DEFAULT_MAX_SIZE = 50 * 1024 * 1024L; + + /** + * 默认的文件名最大长度 100 + */ + public static final int DEFAULT_FILE_NAME_LENGTH = 100; + + /** + * 默认上传的地址 + */ + private static String defaultBaseDir = RuoYiConfig.getProfile(); + + public static void setDefaultBaseDir(String defaultBaseDir) + { + FileUploadUtils.defaultBaseDir = defaultBaseDir; + } + + public static String getDefaultBaseDir() + { + return defaultBaseDir; + } + + /** + * 以默认配置进行文件上传 + * + * @param file 上传的文件 + * @return 文件名称 + * @throws Exception + */ + public static final String upload(MultipartFile file) throws IOException + { + try + { + return upload(getDefaultBaseDir(), file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION); + } + catch (Exception e) + { + throw new IOException(e.getMessage(), e); + } + } + + /** + * 根据文件路径上传 + * + * @param baseDir 相对应用的基目录 + * @param file 上传的文件 + * @return 文件名称 + * @throws IOException + */ + public static final String upload(String baseDir, MultipartFile file) throws IOException + { + try + { + return upload(baseDir, file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION); + } + catch (Exception e) + { + throw new IOException(e.getMessage(), e); + } + } + + /** + * 文件上传 + * + * @param baseDir 相对应用的基目录 + * @param file 上传的文件 + * @param allowedExtension 上传文件类型 + * @return 返回上传成功的文件名 + * @throws FileSizeLimitExceededException 如果超出最大大小 + * @throws FileNameLengthLimitExceededException 文件名太长 + * @throws IOException 比如读写文件出错时 + * @throws InvalidExtensionException 文件校验异常 + */ + public static final String upload(String baseDir, MultipartFile file, String[] allowedExtension) + throws FileSizeLimitExceededException, IOException, FileNameLengthLimitExceededException, + InvalidExtensionException + { + return upload(baseDir, file, allowedExtension, false); + } + + /** + * 文件上传 + * + * @param baseDir 相对应用的基目录 + * @param file 上传的文件 + * @param useCustomNaming 系统自定义文件名 + * @param allowedExtension 上传文件类型 + * @return 返回上传成功的文件名 + * @throws FileSizeLimitExceededException 如果超出最大大小 + * @throws FileNameLengthLimitExceededException 文件名太长 + * @throws IOException 比如读写文件出错时 + * @throws InvalidExtensionException 文件校验异常 + */ + public static final String upload(String baseDir, MultipartFile file, String[] allowedExtension, boolean useCustomNaming) + throws FileSizeLimitExceededException, IOException, FileNameLengthLimitExceededException, + InvalidExtensionException + { + int fileNameLength = Objects.requireNonNull(file.getOriginalFilename()).length(); + if (fileNameLength > FileUploadUtils.DEFAULT_FILE_NAME_LENGTH) + { + throw new FileNameLengthLimitExceededException(FileUploadUtils.DEFAULT_FILE_NAME_LENGTH); + } + + assertAllowed(file, allowedExtension); + + String fileName = useCustomNaming ? uuidFilename(file) : extractFilename(file); + + String absPath = getAbsoluteFile(baseDir, fileName).getAbsolutePath(); + file.transferTo(Paths.get(absPath)); + return getPathFileName(baseDir, fileName); + } + + /** + * 编码文件名(日期格式目录 + 原文件名 + 序列值 + 后缀) + */ + public static final String extractFilename(MultipartFile file) + { + return StringUtils.format("{}/{}_{}.{}", DateUtils.datePath(), FilenameUtils.getBaseName(file.getOriginalFilename()), Seq.getId(Seq.uploadSeqType), getExtension(file)); + } + + /** + * 编编码文件名(日期格式目录 + UUID + 后缀) + */ + public static final String uuidFilename(MultipartFile file) + { + return StringUtils.format("{}/{}.{}", DateUtils.datePath(), IdUtils.fastSimpleUUID(), getExtension(file)); + } + + public static final File getAbsoluteFile(String uploadDir, String fileName) throws IOException + { + File desc = new File(uploadDir + File.separator + fileName); + + if (!desc.exists()) + { + if (!desc.getParentFile().exists()) + { + desc.getParentFile().mkdirs(); + } + } + return desc; + } + + public static final String getPathFileName(String uploadDir, String fileName) throws IOException + { + int dirLastIndex = RuoYiConfig.getProfile().length() + 1; + String currentDir = StringUtils.substring(uploadDir, dirLastIndex); + return Constants.RESOURCE_PREFIX + "/" + currentDir + "/" + fileName; + } + + /** + * 文件大小校验 + * + * @param file 上传的文件 + * @return + * @throws FileSizeLimitExceededException 如果超出最大大小 + * @throws InvalidExtensionException + */ + public static final void assertAllowed(MultipartFile file, String[] allowedExtension) + throws FileSizeLimitExceededException, InvalidExtensionException + { + long size = file.getSize(); + if (size > DEFAULT_MAX_SIZE) + { + throw new FileSizeLimitExceededException(DEFAULT_MAX_SIZE / 1024 / 1024); + } + + String fileName = file.getOriginalFilename(); + String extension = getExtension(file); + if (allowedExtension != null && !isAllowedExtension(extension, allowedExtension)) + { + if (allowedExtension == MimeTypeUtils.IMAGE_EXTENSION) + { + throw new InvalidExtensionException.InvalidImageExtensionException(allowedExtension, extension, + fileName); + } + else if (allowedExtension == MimeTypeUtils.FLASH_EXTENSION) + { + throw new InvalidExtensionException.InvalidFlashExtensionException(allowedExtension, extension, + fileName); + } + else if (allowedExtension == MimeTypeUtils.MEDIA_EXTENSION) + { + throw new InvalidExtensionException.InvalidMediaExtensionException(allowedExtension, extension, + fileName); + } + else if (allowedExtension == MimeTypeUtils.VIDEO_EXTENSION) + { + throw new InvalidExtensionException.InvalidVideoExtensionException(allowedExtension, extension, + fileName); + } + else + { + throw new InvalidExtensionException(allowedExtension, extension, fileName); + } + } + } + + /** + * 判断MIME类型是否是允许的MIME类型 + * + * @param extension + * @param allowedExtension + * @return + */ + public static final boolean isAllowedExtension(String extension, String[] allowedExtension) + { + for (String str : allowedExtension) + { + if (str.equalsIgnoreCase(extension)) + { + return true; + } + } + return false; + } + + /** + * 获取文件名的后缀 + * + * @param file 表单文件 + * @return 后缀名 + */ + public static final String getExtension(MultipartFile file) + { + String extension = FilenameUtils.getExtension(file.getOriginalFilename()); + if (StringUtils.isEmpty(extension)) + { + extension = MimeTypeUtils.getExtension(Objects.requireNonNull(file.getContentType())); + } + return extension; + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUtils.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUtils.java new file mode 100644 index 0000000..9f16a3d --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUtils.java @@ -0,0 +1,303 @@ +package com.ruoyi.common.utils.file; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.ArrayUtils; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.uuid.IdUtils; + +/** + * 文件处理工具类 + * + * @author ruoyi + */ +public class FileUtils +{ + public static String FILENAME_PATTERN = "[a-zA-Z0-9_\\-\\|\\.\\u4e00-\\u9fa5]+"; + + /** + * 输出指定文件的byte数组 + * + * @param filePath 文件路径 + * @param os 输出流 + * @return + */ + public static void writeBytes(String filePath, OutputStream os) throws IOException + { + FileInputStream fis = null; + try + { + File file = new File(filePath); + if (!file.exists()) + { + throw new FileNotFoundException(filePath); + } + fis = new FileInputStream(file); + byte[] b = new byte[1024]; + int length; + while ((length = fis.read(b)) > 0) + { + os.write(b, 0, length); + } + } + catch (IOException e) + { + throw e; + } + finally + { + IOUtils.close(os); + IOUtils.close(fis); + } + } + + /** + * 写数据到文件中 + * + * @param data 数据 + * @return 目标文件 + * @throws IOException IO异常 + */ + public static String writeImportBytes(byte[] data) throws IOException + { + return writeBytes(data, RuoYiConfig.getImportPath()); + } + + /** + * 写数据到文件中 + * + * @param data 数据 + * @param uploadDir 目标文件 + * @return 目标文件 + * @throws IOException IO异常 + */ + public static String writeBytes(byte[] data, String uploadDir) throws IOException + { + FileOutputStream fos = null; + String pathName = ""; + try + { + String extension = getFileExtendName(data); + pathName = DateUtils.datePath() + "/" + IdUtils.fastUUID() + "." + extension; + File file = FileUploadUtils.getAbsoluteFile(uploadDir, pathName); + fos = new FileOutputStream(file); + fos.write(data); + } + finally + { + IOUtils.close(fos); + } + return FileUploadUtils.getPathFileName(uploadDir, pathName); + } + + /** + * 移除路径中的请求前缀片段 + * + * @param filePath 文件路径 + * @return 移除后的文件路径 + */ + public static String stripPrefix(String filePath) + { + return StringUtils.substringAfter(filePath, Constants.RESOURCE_PREFIX); + } + + /** + * 删除文件 + * + * @param filePath 文件 + * @return + */ + public static boolean deleteFile(String filePath) + { + boolean flag = false; + File file = new File(filePath); + // 路径为文件且不为空则进行删除 + if (file.isFile() && file.exists()) + { + flag = file.delete(); + } + return flag; + } + + /** + * 文件名称验证 + * + * @param filename 文件名称 + * @return true 正常 false 非法 + */ + public static boolean isValidFilename(String filename) + { + return filename.matches(FILENAME_PATTERN); + } + + /** + * 检查文件是否可下载 + * + * @param resource 需要下载的文件 + * @return true 正常 false 非法 + */ + public static boolean checkAllowDownload(String resource) + { + // 禁止目录上跳级别 + if (StringUtils.contains(resource, "..")) + { + return false; + } + + // 检查允许下载的文件规则 + if (ArrayUtils.contains(MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION, FileTypeUtils.getFileType(resource))) + { + return true; + } + + // 不在允许下载的文件规则 + return false; + } + + /** + * 下载文件名重新编码 + * + * @param request 请求对象 + * @param fileName 文件名 + * @return 编码后的文件名 + */ + public static String setFileDownloadHeader(HttpServletRequest request, String fileName) throws UnsupportedEncodingException + { + final String agent = request.getHeader("USER-AGENT"); + String filename = fileName; + if (agent.contains("MSIE")) + { + // IE浏览器 + filename = URLEncoder.encode(filename, "utf-8"); + filename = filename.replace("+", " "); + } + else if (agent.contains("Firefox")) + { + // 火狐浏览器 + filename = new String(fileName.getBytes(), "ISO8859-1"); + } + else if (agent.contains("Chrome")) + { + // google浏览器 + filename = URLEncoder.encode(filename, "utf-8"); + } + else + { + // 其它浏览器 + filename = URLEncoder.encode(filename, "utf-8"); + } + return filename; + } + + /** + * 下载文件名重新编码 + * + * @param response 响应对象 + * @param realFileName 真实文件名 + */ + public static void setAttachmentResponseHeader(HttpServletResponse response, String realFileName) throws UnsupportedEncodingException + { + String percentEncodedFileName = percentEncode(realFileName); + + StringBuilder contentDispositionValue = new StringBuilder(); + contentDispositionValue.append("attachment; filename=") + .append(percentEncodedFileName) + .append(";") + .append("filename*=") + .append("utf-8''") + .append(percentEncodedFileName); + + response.addHeader("Access-Control-Expose-Headers", "Content-Disposition,download-filename"); + response.setHeader("Content-disposition", contentDispositionValue.toString()); + response.setHeader("download-filename", percentEncodedFileName); + } + + /** + * 百分号编码工具方法 + * + * @param s 需要百分号编码的字符串 + * @return 百分号编码后的字符串 + */ + public static String percentEncode(String s) throws UnsupportedEncodingException + { + String encode = URLEncoder.encode(s, StandardCharsets.UTF_8.toString()); + return encode.replaceAll("\\+", "%20"); + } + + /** + * 获取图像后缀 + * + * @param photoByte 图像数据 + * @return 后缀名 + */ + public static String getFileExtendName(byte[] photoByte) + { + String strFileExtendName = "jpg"; + if ((photoByte[0] == 71) && (photoByte[1] == 73) && (photoByte[2] == 70) && (photoByte[3] == 56) + && ((photoByte[4] == 55) || (photoByte[4] == 57)) && (photoByte[5] == 97)) + { + strFileExtendName = "gif"; + } + else if ((photoByte[6] == 74) && (photoByte[7] == 70) && (photoByte[8] == 73) && (photoByte[9] == 70)) + { + strFileExtendName = "jpg"; + } + else if ((photoByte[0] == 66) && (photoByte[1] == 77)) + { + strFileExtendName = "bmp"; + } + else if ((photoByte[1] == 80) && (photoByte[2] == 78) && (photoByte[3] == 71)) + { + strFileExtendName = "png"; + } + return strFileExtendName; + } + + /** + * 获取文件名称 /profile/upload/2022/04/16/ruoyi.png -- ruoyi.png + * + * @param fileName 路径名称 + * @return 没有文件路径的名称 + */ + public static String getName(String fileName) + { + if (fileName == null) + { + return null; + } + int lastUnixPos = fileName.lastIndexOf('/'); + int lastWindowsPos = fileName.lastIndexOf('\\'); + int index = Math.max(lastUnixPos, lastWindowsPos); + return fileName.substring(index + 1); + } + + /** + * 获取不带后缀文件名称 /profile/upload/2022/04/16/ruoyi.png -- ruoyi + * + * @param fileName 路径名称 + * @return 没有文件路径和后缀的名称 + */ + public static String getNameNotSuffix(String fileName) + { + if (fileName == null) + { + return null; + } + String baseName = FilenameUtils.getBaseName(fileName); + return baseName; + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/ImageUtils.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/ImageUtils.java new file mode 100644 index 0000000..432dfda --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/ImageUtils.java @@ -0,0 +1,98 @@ +package com.ruoyi.common.utils.file; + +import java.io.ByteArrayInputStream; +import java.io.FileInputStream; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; +import java.util.Arrays; +import org.apache.poi.util.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.utils.StringUtils; + +/** + * 图片处理工具类 + * + * @author ruoyi + */ +public class ImageUtils +{ + private static final Logger log = LoggerFactory.getLogger(ImageUtils.class); + + public static byte[] getImage(String imagePath) + { + InputStream is = getFile(imagePath); + try + { + return IOUtils.toByteArray(is); + } + catch (Exception e) + { + log.error("图片加载异常 {}", e); + return null; + } + finally + { + IOUtils.closeQuietly(is); + } + } + + public static InputStream getFile(String imagePath) + { + try + { + byte[] result = readFile(imagePath); + result = Arrays.copyOf(result, result.length); + return new ByteArrayInputStream(result); + } + catch (Exception e) + { + log.error("获取图片异常 {}", e); + } + return null; + } + + /** + * 读取文件为字节数据 + * + * @param url 地址 + * @return 字节数据 + */ + public static byte[] readFile(String url) + { + InputStream in = null; + try + { + if (url.startsWith("http")) + { + // 网络地址 + URL urlObj = new URL(url); + URLConnection urlConnection = urlObj.openConnection(); + urlConnection.setConnectTimeout(30 * 1000); + urlConnection.setReadTimeout(60 * 1000); + urlConnection.setDoInput(true); + in = urlConnection.getInputStream(); + } + else + { + // 本机地址 + String localPath = RuoYiConfig.getProfile(); + String downloadPath = localPath + StringUtils.substringAfter(url, Constants.RESOURCE_PREFIX); + in = new FileInputStream(downloadPath); + } + return IOUtils.toByteArray(in); + } + catch (Exception e) + { + log.error("获取文件路径异常 {}", e); + return null; + } + finally + { + IOUtils.closeQuietly(in); + } + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/MimeTypeUtils.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/MimeTypeUtils.java new file mode 100644 index 0000000..756ba5c --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/MimeTypeUtils.java @@ -0,0 +1,59 @@ +package com.ruoyi.common.utils.file; + +/** + * 媒体类型工具类 + * + * @author ruoyi + */ +public class MimeTypeUtils +{ + public static final String IMAGE_PNG = "image/png"; + + public static final String IMAGE_JPG = "image/jpg"; + + public static final String IMAGE_JPEG = "image/jpeg"; + + public static final String IMAGE_BMP = "image/bmp"; + + public static final String IMAGE_GIF = "image/gif"; + + public static final String[] IMAGE_EXTENSION = { "bmp", "gif", "jpg", "jpeg", "png" }; + + public static final String[] FLASH_EXTENSION = { "swf", "flv" }; + + public static final String[] MEDIA_EXTENSION = { "swf", "flv", "mp3", "wav", "wma", "wmv", "mid", "avi", "mpg", + "asf", "rm", "rmvb" }; + + public static final String[] VIDEO_EXTENSION = { "mp4", "avi", "rmvb" }; + + public static final String[] DEFAULT_ALLOWED_EXTENSION = { + // 图片 + "bmp", "gif", "jpg", "jpeg", "png", + // word excel powerpoint + "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", + // 压缩文件 + "rar", "zip", "gz", "bz2", + // 视频格式 + "mp4", "avi", "rmvb", + // pdf + "pdf" }; + + public static String getExtension(String prefix) + { + switch (prefix) + { + case IMAGE_PNG: + return "png"; + case IMAGE_JPG: + return "jpg"; + case IMAGE_JPEG: + return "jpeg"; + case IMAGE_BMP: + return "bmp"; + case IMAGE_GIF: + return "gif"; + default: + return ""; + } + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/html/EscapeUtil.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/html/EscapeUtil.java new file mode 100644 index 0000000..f52e83e --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/html/EscapeUtil.java @@ -0,0 +1,167 @@ +package com.ruoyi.common.utils.html; + +import com.ruoyi.common.utils.StringUtils; + +/** + * 转义和反转义工具类 + * + * @author ruoyi + */ +public class EscapeUtil +{ + public static final String RE_HTML_MARK = "(<[^<]*?>)|(<[\\s]*?/[^<]*?>)|(<[^<]*?/[\\s]*?>)"; + + private static final char[][] TEXT = new char[64][]; + + static + { + for (int i = 0; i < 64; i++) + { + TEXT[i] = new char[] { (char) i }; + } + + // special HTML characters + TEXT['\''] = "'".toCharArray(); // 单引号 + TEXT['"'] = """.toCharArray(); // 双引号 + TEXT['&'] = "&".toCharArray(); // &符 + TEXT['<'] = "<".toCharArray(); // 小于号 + TEXT['>'] = ">".toCharArray(); // 大于号 + } + + /** + * 转义文本中的HTML字符为安全的字符 + * + * @param text 被转义的文本 + * @return 转义后的文本 + */ + public static String escape(String text) + { + return encode(text); + } + + /** + * 还原被转义的HTML特殊字符 + * + * @param content 包含转义符的HTML内容 + * @return 转换后的字符串 + */ + public static String unescape(String content) + { + return decode(content); + } + + /** + * 清除所有HTML标签,但是不删除标签内的内容 + * + * @param content 文本 + * @return 清除标签后的文本 + */ + public static String clean(String content) + { + return new HTMLFilter().filter(content); + } + + /** + * Escape编码 + * + * @param text 被编码的文本 + * @return 编码后的字符 + */ + private static String encode(String text) + { + if (StringUtils.isEmpty(text)) + { + return StringUtils.EMPTY; + } + + final StringBuilder tmp = new StringBuilder(text.length() * 6); + char c; + for (int i = 0; i < text.length(); i++) + { + c = text.charAt(i); + if (c < 256) + { + tmp.append("%"); + if (c < 16) + { + tmp.append("0"); + } + tmp.append(Integer.toString(c, 16)); + } + else + { + tmp.append("%u"); + if (c <= 0xfff) + { + // issue#I49JU8@Gitee + tmp.append("0"); + } + tmp.append(Integer.toString(c, 16)); + } + } + return tmp.toString(); + } + + /** + * Escape解码 + * + * @param content 被转义的内容 + * @return 解码后的字符串 + */ + public static String decode(String content) + { + if (StringUtils.isEmpty(content)) + { + return content; + } + + StringBuilder tmp = new StringBuilder(content.length()); + int lastPos = 0, pos = 0; + char ch; + while (lastPos < content.length()) + { + pos = content.indexOf("%", lastPos); + if (pos == lastPos) + { + if (content.charAt(pos + 1) == 'u') + { + ch = (char) Integer.parseInt(content.substring(pos + 2, pos + 6), 16); + tmp.append(ch); + lastPos = pos + 6; + } + else + { + ch = (char) Integer.parseInt(content.substring(pos + 1, pos + 3), 16); + tmp.append(ch); + lastPos = pos + 3; + } + } + else + { + if (pos == -1) + { + tmp.append(content.substring(lastPos)); + lastPos = content.length(); + } + else + { + tmp.append(content.substring(lastPos, pos)); + lastPos = pos; + } + } + } + return tmp.toString(); + } + + public static void main(String[] args) + { + String html = ""; + String escape = EscapeUtil.escape(html); + // String html = "ipt>alert(\"XSS\")ipt>"; + // String html = "<123"; + // String html = "123>"; + System.out.println("clean: " + EscapeUtil.clean(html)); + System.out.println("escape: " + escape); + System.out.println("unescape: " + EscapeUtil.unescape(escape)); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/html/HTMLFilter.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/html/HTMLFilter.java new file mode 100644 index 0000000..ebff3fd --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/html/HTMLFilter.java @@ -0,0 +1,570 @@ +package com.ruoyi.common.utils.html; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * HTML过滤器,用于去除XSS漏洞隐患。 + * + * @author ruoyi + */ +public final class HTMLFilter +{ + /** + * regex flag union representing /si modifiers in php + **/ + private static final int REGEX_FLAGS_SI = Pattern.CASE_INSENSITIVE | Pattern.DOTALL; + private static final Pattern P_COMMENTS = Pattern.compile("", Pattern.DOTALL); + private static final Pattern P_COMMENT = Pattern.compile("^!--(.*)--$", REGEX_FLAGS_SI); + private static final Pattern P_TAGS = Pattern.compile("<(.*?)>", Pattern.DOTALL); + private static final Pattern P_END_TAG = Pattern.compile("^/([a-z0-9]+)", REGEX_FLAGS_SI); + private static final Pattern P_START_TAG = Pattern.compile("^([a-z0-9]+)(.*?)(/?)$", REGEX_FLAGS_SI); + private static final Pattern P_QUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)=([\"'])(.*?)\\2", REGEX_FLAGS_SI); + private static final Pattern P_UNQUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)(=)([^\"\\s']+)", REGEX_FLAGS_SI); + private static final Pattern P_PROTOCOL = Pattern.compile("^([^:]+):", REGEX_FLAGS_SI); + private static final Pattern P_ENTITY = Pattern.compile("&#(\\d+);?"); + private static final Pattern P_ENTITY_UNICODE = Pattern.compile("&#x([0-9a-f]+);?"); + private static final Pattern P_ENCODE = Pattern.compile("%([0-9a-f]{2});?"); + private static final Pattern P_VALID_ENTITIES = Pattern.compile("&([^&;]*)(?=(;|&|$))"); + private static final Pattern P_VALID_QUOTES = Pattern.compile("(>|^)([^<]+?)(<|$)", Pattern.DOTALL); + private static final Pattern P_END_ARROW = Pattern.compile("^>"); + private static final Pattern P_BODY_TO_END = Pattern.compile("<([^>]*?)(?=<|$)"); + private static final Pattern P_XML_CONTENT = Pattern.compile("(^|>)([^<]*?)(?=>)"); + private static final Pattern P_STRAY_LEFT_ARROW = Pattern.compile("<([^>]*?)(?=<|$)"); + private static final Pattern P_STRAY_RIGHT_ARROW = Pattern.compile("(^|>)([^<]*?)(?=>)"); + private static final Pattern P_AMP = Pattern.compile("&"); + private static final Pattern P_QUOTE = Pattern.compile("\""); + private static final Pattern P_LEFT_ARROW = Pattern.compile("<"); + private static final Pattern P_RIGHT_ARROW = Pattern.compile(">"); + private static final Pattern P_BOTH_ARROWS = Pattern.compile("<>"); + + // @xxx could grow large... maybe use sesat's ReferenceMap + private static final ConcurrentMap P_REMOVE_PAIR_BLANKS = new ConcurrentHashMap<>(); + private static final ConcurrentMap P_REMOVE_SELF_BLANKS = new ConcurrentHashMap<>(); + + /** + * set of allowed html elements, along with allowed attributes for each element + **/ + private final Map> vAllowed; + /** + * counts of open tags for each (allowable) html element + **/ + private final Map vTagCounts = new HashMap<>(); + + /** + * html elements which must always be self-closing (e.g. "") + **/ + private final String[] vSelfClosingTags; + /** + * html elements which must always have separate opening and closing tags (e.g. "") + **/ + private final String[] vNeedClosingTags; + /** + * set of disallowed html elements + **/ + private final String[] vDisallowed; + /** + * attributes which should be checked for valid protocols + **/ + private final String[] vProtocolAtts; + /** + * allowed protocols + **/ + private final String[] vAllowedProtocols; + /** + * tags which should be removed if they contain no content (e.g. "" or "") + **/ + private final String[] vRemoveBlanks; + /** + * entities allowed within html markup + **/ + private final String[] vAllowedEntities; + /** + * flag determining whether comments are allowed in input String. + */ + private final boolean stripComment; + private final boolean encodeQuotes; + /** + * flag determining whether to try to make tags when presented with "unbalanced" angle brackets (e.g. "" + * becomes " text "). If set to false, unbalanced angle brackets will be html escaped. + */ + private final boolean alwaysMakeTags; + + /** + * Default constructor. + */ + public HTMLFilter() + { + vAllowed = new HashMap<>(); + + final ArrayList a_atts = new ArrayList<>(); + a_atts.add("href"); + a_atts.add("target"); + vAllowed.put("a", a_atts); + + final ArrayList img_atts = new ArrayList<>(); + img_atts.add("src"); + img_atts.add("width"); + img_atts.add("height"); + img_atts.add("alt"); + vAllowed.put("img", img_atts); + + final ArrayList no_atts = new ArrayList<>(); + vAllowed.put("b", no_atts); + vAllowed.put("strong", no_atts); + vAllowed.put("i", no_atts); + vAllowed.put("em", no_atts); + + vSelfClosingTags = new String[] { "img" }; + vNeedClosingTags = new String[] { "a", "b", "strong", "i", "em" }; + vDisallowed = new String[] {}; + vAllowedProtocols = new String[] { "http", "mailto", "https" }; // no ftp. + vProtocolAtts = new String[] { "src", "href" }; + vRemoveBlanks = new String[] { "a", "b", "strong", "i", "em" }; + vAllowedEntities = new String[] { "amp", "gt", "lt", "quot" }; + stripComment = true; + encodeQuotes = true; + alwaysMakeTags = false; + } + + /** + * Map-parameter configurable constructor. + * + * @param conf map containing configuration. keys match field names. + */ + @SuppressWarnings("unchecked") + public HTMLFilter(final Map conf) + { + + assert conf.containsKey("vAllowed") : "configuration requires vAllowed"; + assert conf.containsKey("vSelfClosingTags") : "configuration requires vSelfClosingTags"; + assert conf.containsKey("vNeedClosingTags") : "configuration requires vNeedClosingTags"; + assert conf.containsKey("vDisallowed") : "configuration requires vDisallowed"; + assert conf.containsKey("vAllowedProtocols") : "configuration requires vAllowedProtocols"; + assert conf.containsKey("vProtocolAtts") : "configuration requires vProtocolAtts"; + assert conf.containsKey("vRemoveBlanks") : "configuration requires vRemoveBlanks"; + assert conf.containsKey("vAllowedEntities") : "configuration requires vAllowedEntities"; + + vAllowed = Collections.unmodifiableMap((HashMap>) conf.get("vAllowed")); + vSelfClosingTags = (String[]) conf.get("vSelfClosingTags"); + vNeedClosingTags = (String[]) conf.get("vNeedClosingTags"); + vDisallowed = (String[]) conf.get("vDisallowed"); + vAllowedProtocols = (String[]) conf.get("vAllowedProtocols"); + vProtocolAtts = (String[]) conf.get("vProtocolAtts"); + vRemoveBlanks = (String[]) conf.get("vRemoveBlanks"); + vAllowedEntities = (String[]) conf.get("vAllowedEntities"); + stripComment = conf.containsKey("stripComment") ? (Boolean) conf.get("stripComment") : true; + encodeQuotes = conf.containsKey("encodeQuotes") ? (Boolean) conf.get("encodeQuotes") : true; + alwaysMakeTags = conf.containsKey("alwaysMakeTags") ? (Boolean) conf.get("alwaysMakeTags") : true; + } + + private void reset() + { + vTagCounts.clear(); + } + + // --------------------------------------------------------------- + // my versions of some PHP library functions + public static String chr(final int decimal) + { + return String.valueOf((char) decimal); + } + + public static String htmlSpecialChars(final String s) + { + String result = s; + result = regexReplace(P_AMP, "&", result); + result = regexReplace(P_QUOTE, """, result); + result = regexReplace(P_LEFT_ARROW, "<", result); + result = regexReplace(P_RIGHT_ARROW, ">", result); + return result; + } + + // --------------------------------------------------------------- + + /** + * given a user submitted input String, filter out any invalid or restricted html. + * + * @param input text (i.e. submitted by a user) than may contain html + * @return "clean" version of input, with only valid, whitelisted html elements allowed + */ + public String filter(final String input) + { + reset(); + String s = input; + + s = escapeComments(s); + + s = balanceHTML(s); + + s = checkTags(s); + + s = processRemoveBlanks(s); + + // s = validateEntities(s); + + return s; + } + + public boolean isAlwaysMakeTags() + { + return alwaysMakeTags; + } + + public boolean isStripComments() + { + return stripComment; + } + + private String escapeComments(final String s) + { + final Matcher m = P_COMMENTS.matcher(s); + final StringBuffer buf = new StringBuffer(); + if (m.find()) + { + final String match = m.group(1); // (.*?) + m.appendReplacement(buf, Matcher.quoteReplacement("")); + } + m.appendTail(buf); + + return buf.toString(); + } + + private String balanceHTML(String s) + { + if (alwaysMakeTags) + { + // + // try and form html + // + s = regexReplace(P_END_ARROW, "", s); + // 不追加结束标签 + s = regexReplace(P_BODY_TO_END, "<$1>", s); + s = regexReplace(P_XML_CONTENT, "$1<$2", s); + + } + else + { + // + // escape stray brackets + // + s = regexReplace(P_STRAY_LEFT_ARROW, "<$1", s); + s = regexReplace(P_STRAY_RIGHT_ARROW, "$1$2><", s); + + // + // the last regexp causes '<>' entities to appear + // (we need to do a lookahead assertion so that the last bracket can + // be used in the next pass of the regexp) + // + s = regexReplace(P_BOTH_ARROWS, "", s); + } + + return s; + } + + private String checkTags(String s) + { + Matcher m = P_TAGS.matcher(s); + + final StringBuffer buf = new StringBuffer(); + while (m.find()) + { + String replaceStr = m.group(1); + replaceStr = processTag(replaceStr); + m.appendReplacement(buf, Matcher.quoteReplacement(replaceStr)); + } + m.appendTail(buf); + + // these get tallied in processTag + // (remember to reset before subsequent calls to filter method) + final StringBuilder sBuilder = new StringBuilder(buf.toString()); + for (String key : vTagCounts.keySet()) + { + for (int ii = 0; ii < vTagCounts.get(key); ii++) + { + sBuilder.append(""); + } + } + s = sBuilder.toString(); + + return s; + } + + private String processRemoveBlanks(final String s) + { + String result = s; + for (String tag : vRemoveBlanks) + { + if (!P_REMOVE_PAIR_BLANKS.containsKey(tag)) + { + P_REMOVE_PAIR_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?>")); + } + result = regexReplace(P_REMOVE_PAIR_BLANKS.get(tag), "", result); + if (!P_REMOVE_SELF_BLANKS.containsKey(tag)) + { + P_REMOVE_SELF_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?/>")); + } + result = regexReplace(P_REMOVE_SELF_BLANKS.get(tag), "", result); + } + + return result; + } + + private static String regexReplace(final Pattern regex_pattern, final String replacement, final String s) + { + Matcher m = regex_pattern.matcher(s); + return m.replaceAll(replacement); + } + + private String processTag(final String s) + { + // ending tags + Matcher m = P_END_TAG.matcher(s); + if (m.find()) + { + final String name = m.group(1).toLowerCase(); + if (allowed(name)) + { + if (!inArray(name, vSelfClosingTags)) + { + if (vTagCounts.containsKey(name)) + { + vTagCounts.put(name, vTagCounts.get(name) - 1); + return ""; + } + } + } + } + + // starting tags + m = P_START_TAG.matcher(s); + if (m.find()) + { + final String name = m.group(1).toLowerCase(); + final String body = m.group(2); + String ending = m.group(3); + + // debug( "in a starting tag, name='" + name + "'; body='" + body + "'; ending='" + ending + "'" ); + if (allowed(name)) + { + final StringBuilder params = new StringBuilder(); + + final Matcher m2 = P_QUOTED_ATTRIBUTES.matcher(body); + final Matcher m3 = P_UNQUOTED_ATTRIBUTES.matcher(body); + final List paramNames = new ArrayList<>(); + final List paramValues = new ArrayList<>(); + while (m2.find()) + { + paramNames.add(m2.group(1)); // ([a-z0-9]+) + paramValues.add(m2.group(3)); // (.*?) + } + while (m3.find()) + { + paramNames.add(m3.group(1)); // ([a-z0-9]+) + paramValues.add(m3.group(3)); // ([^\"\\s']+) + } + + String paramName, paramValue; + for (int ii = 0; ii < paramNames.size(); ii++) + { + paramName = paramNames.get(ii).toLowerCase(); + paramValue = paramValues.get(ii); + + // debug( "paramName='" + paramName + "'" ); + // debug( "paramValue='" + paramValue + "'" ); + // debug( "allowed? " + vAllowed.get( name ).contains( paramName ) ); + + if (allowedAttribute(name, paramName)) + { + if (inArray(paramName, vProtocolAtts)) + { + paramValue = processParamProtocol(paramValue); + } + params.append(' ').append(paramName).append("=\\\"").append(paramValue).append("\\\""); + } + } + + if (inArray(name, vSelfClosingTags)) + { + ending = " /"; + } + + if (inArray(name, vNeedClosingTags)) + { + ending = ""; + } + + if (ending == null || ending.length() < 1) + { + if (vTagCounts.containsKey(name)) + { + vTagCounts.put(name, vTagCounts.get(name) + 1); + } + else + { + vTagCounts.put(name, 1); + } + } + else + { + ending = " /"; + } + return "<" + name + params + ending + ">"; + } + else + { + return ""; + } + } + + // comments + m = P_COMMENT.matcher(s); + if (!stripComment && m.find()) + { + return "<" + m.group() + ">"; + } + + return ""; + } + + private String processParamProtocol(String s) + { + s = decodeEntities(s); + final Matcher m = P_PROTOCOL.matcher(s); + if (m.find()) + { + final String protocol = m.group(1); + if (!inArray(protocol, vAllowedProtocols)) + { + // bad protocol, turn into local anchor link instead + s = "#" + s.substring(protocol.length() + 1); + if (s.startsWith("#//")) + { + s = "#" + s.substring(3); + } + } + } + + return s; + } + + private String decodeEntities(String s) + { + StringBuffer buf = new StringBuffer(); + + Matcher m = P_ENTITY.matcher(s); + while (m.find()) + { + final String match = m.group(1); + final int decimal = Integer.decode(match).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + buf = new StringBuffer(); + m = P_ENTITY_UNICODE.matcher(s); + while (m.find()) + { + final String match = m.group(1); + final int decimal = Integer.valueOf(match, 16).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + buf = new StringBuffer(); + m = P_ENCODE.matcher(s); + while (m.find()) + { + final String match = m.group(1); + final int decimal = Integer.valueOf(match, 16).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + s = validateEntities(s); + return s; + } + + private String validateEntities(final String s) + { + StringBuffer buf = new StringBuffer(); + + // validate entities throughout the string + Matcher m = P_VALID_ENTITIES.matcher(s); + while (m.find()) + { + final String one = m.group(1); // ([^&;]*) + final String two = m.group(2); // (?=(;|&|$)) + m.appendReplacement(buf, Matcher.quoteReplacement(checkEntity(one, two))); + } + m.appendTail(buf); + + return encodeQuotes(buf.toString()); + } + + private String encodeQuotes(final String s) + { + if (encodeQuotes) + { + StringBuffer buf = new StringBuffer(); + Matcher m = P_VALID_QUOTES.matcher(s); + while (m.find()) + { + final String one = m.group(1); // (>|^) + final String two = m.group(2); // ([^<]+?) + final String three = m.group(3); // (<|$) + // 不替换双引号为",防止json格式无效 regexReplace(P_QUOTE, """, two) + m.appendReplacement(buf, Matcher.quoteReplacement(one + two + three)); + } + m.appendTail(buf); + return buf.toString(); + } + else + { + return s; + } + } + + private String checkEntity(final String preamble, final String term) + { + + return ";".equals(term) && isValidEntity(preamble) ? '&' + preamble : "&" + preamble; + } + + private boolean isValidEntity(final String entity) + { + return inArray(entity, vAllowedEntities); + } + + private static boolean inArray(final String s, final String[] array) + { + for (String item : array) + { + if (item != null && item.equals(s)) + { + return true; + } + } + return false; + } + + private boolean allowed(final String name) + { + return (vAllowed.isEmpty() || vAllowed.containsKey(name)) && !inArray(name, vDisallowed); + } + + private boolean allowedAttribute(final String name, final String paramName) + { + return allowed(name) && (vAllowed.isEmpty() || vAllowed.get(name).contains(paramName)); + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpHelper.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpHelper.java new file mode 100644 index 0000000..401f25a --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpHelper.java @@ -0,0 +1,55 @@ +package com.ruoyi.common.utils.http; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import jakarta.servlet.ServletRequest; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 通用http工具封装 + * + * @author ruoyi + */ +public class HttpHelper +{ + private static final Logger LOGGER = LoggerFactory.getLogger(HttpHelper.class); + + public static String getBodyString(ServletRequest request) + { + StringBuilder sb = new StringBuilder(); + BufferedReader reader = null; + try (InputStream inputStream = request.getInputStream()) + { + reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); + String line = ""; + while ((line = reader.readLine()) != null) + { + sb.append(line); + } + } + catch (IOException e) + { + LOGGER.warn("getBodyString出现问题!"); + } + finally + { + if (reader != null) + { + try + { + reader.close(); + } + catch (IOException e) + { + LOGGER.error(ExceptionUtils.getMessage(e)); + } + } + } + return sb.toString(); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpUtils.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpUtils.java new file mode 100644 index 0000000..534d21c --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpUtils.java @@ -0,0 +1,293 @@ +package com.ruoyi.common.utils.http; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.ConnectException; +import java.net.SocketTimeoutException; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.utils.StringUtils; +import org.springframework.http.MediaType; + +/** + * 通用http发送方法 + * + * @author ruoyi + */ +public class HttpUtils +{ + private static final Logger log = LoggerFactory.getLogger(HttpUtils.class); + + /** + * 向指定 URL 发送GET方法的请求 + * + * @param url 发送请求的 URL + * @return 所代表远程资源的响应结果 + */ + public static String sendGet(String url) + { + return sendGet(url, StringUtils.EMPTY); + } + + /** + * 向指定 URL 发送GET方法的请求 + * + * @param url 发送请求的 URL + * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。 + * @return 所代表远程资源的响应结果 + */ + public static String sendGet(String url, String param) + { + return sendGet(url, param, Constants.UTF8); + } + + /** + * 向指定 URL 发送GET方法的请求 + * + * @param url 发送请求的 URL + * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。 + * @param contentType 编码类型 + * @return 所代表远程资源的响应结果 + */ + public static String sendGet(String url, String param, String contentType) + { + StringBuilder result = new StringBuilder(); + BufferedReader in = null; + try + { + String urlNameString = StringUtils.isNotBlank(param) ? url + "?" + param : url; + log.info("sendGet - {}", urlNameString); + URL realUrl = new URL(urlNameString); + URLConnection connection = realUrl.openConnection(); + connection.setRequestProperty("accept", "*/*"); + connection.setRequestProperty("connection", "Keep-Alive"); + connection.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"); + connection.connect(); + in = new BufferedReader(new InputStreamReader(connection.getInputStream(), contentType)); + String line; + while ((line = in.readLine()) != null) + { + result.append(line); + } + log.info("recv - {}", result); + } + catch (ConnectException e) + { + log.error("调用HttpUtils.sendGet ConnectException, url=" + url + ",param=" + param, e); + } + catch (SocketTimeoutException e) + { + log.error("调用HttpUtils.sendGet SocketTimeoutException, url=" + url + ",param=" + param, e); + } + catch (IOException e) + { + log.error("调用HttpUtils.sendGet IOException, url=" + url + ",param=" + param, e); + } + catch (Exception e) + { + log.error("调用HttpsUtil.sendGet Exception, url=" + url + ",param=" + param, e); + } + finally + { + try + { + if (in != null) + { + in.close(); + } + } + catch (Exception ex) + { + log.error("调用in.close Exception, url=" + url + ",param=" + param, ex); + } + } + return result.toString(); + } + + /** + * 向指定 URL 发送POST方法的请求 + * + * @param url 发送请求的 URL + * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。 + * @return 所代表远程资源的响应结果 + */ + public static String sendPost(String url, String param) + { + return sendPost(url, param, MediaType.APPLICATION_FORM_URLENCODED_VALUE); + } + + /** + * 向指定 URL 发送POST方法的请求 + * + * @param url 发送请求的 URL + * @param param 请求参数 + * @param contentType 内容类型 + * @return 所代表远程资源的响应结果 + */ + public static String sendPost(String url, String param, String contentType) + { + PrintWriter out = null; + BufferedReader in = null; + StringBuilder result = new StringBuilder(); + try + { + log.info("sendPost - {}", url); + URL realUrl = new URL(url); + URLConnection conn = realUrl.openConnection(); + conn.setRequestProperty("accept", "*/*"); + conn.setRequestProperty("connection", "Keep-Alive"); + conn.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"); + conn.setRequestProperty("Accept-Charset", "utf-8"); + conn.setRequestProperty("Content-Type", contentType); + conn.setDoOutput(true); + conn.setDoInput(true); + out = new PrintWriter(conn.getOutputStream()); + out.print(param); + out.flush(); + in = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8)); + String line; + while ((line = in.readLine()) != null) + { + result.append(line); + } + log.info("recv - {}", result); + } + catch (ConnectException e) + { + log.error("调用HttpUtils.sendPost ConnectException, url=" + url + ",param=" + param, e); + } + catch (SocketTimeoutException e) + { + log.error("调用HttpUtils.sendPost SocketTimeoutException, url=" + url + ",param=" + param, e); + } + catch (IOException e) + { + log.error("调用HttpUtils.sendPost IOException, url=" + url + ",param=" + param, e); + } + catch (Exception e) + { + log.error("调用HttpsUtil.sendPost Exception, url=" + url + ",param=" + param, e); + } + finally + { + try + { + if (out != null) + { + out.close(); + } + if (in != null) + { + in.close(); + } + } + catch (IOException ex) + { + log.error("调用in.close Exception, url=" + url + ",param=" + param, ex); + } + } + return result.toString(); + } + + public static String sendSSLPost(String url, String param) + { + return sendSSLPost(url, param, MediaType.APPLICATION_FORM_URLENCODED_VALUE); + } + + public static String sendSSLPost(String url, String param, String contentType) + { + StringBuilder result = new StringBuilder(); + String urlNameString = url + "?" + param; + try + { + log.info("sendSSLPost - {}", urlNameString); + SSLContext sc = SSLContext.getInstance("SSL"); + sc.init(null, new TrustManager[] { new TrustAnyTrustManager() }, new java.security.SecureRandom()); + URL console = new URL(urlNameString); + HttpsURLConnection conn = (HttpsURLConnection) console.openConnection(); + conn.setRequestProperty("accept", "*/*"); + conn.setRequestProperty("connection", "Keep-Alive"); + conn.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"); + conn.setRequestProperty("Accept-Charset", "utf-8"); + conn.setRequestProperty("Content-Type", contentType); + conn.setDoOutput(true); + conn.setDoInput(true); + + conn.setSSLSocketFactory(sc.getSocketFactory()); + conn.setHostnameVerifier(new TrustAnyHostnameVerifier()); + conn.connect(); + InputStream is = conn.getInputStream(); + BufferedReader br = new BufferedReader(new InputStreamReader(is)); + String ret = ""; + while ((ret = br.readLine()) != null) + { + if (ret != null && !"".equals(ret.trim())) + { + result.append(new String(ret.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8)); + } + } + log.info("recv - {}", result); + conn.disconnect(); + br.close(); + } + catch (ConnectException e) + { + log.error("调用HttpUtils.sendSSLPost ConnectException, url=" + url + ",param=" + param, e); + } + catch (SocketTimeoutException e) + { + log.error("调用HttpUtils.sendSSLPost SocketTimeoutException, url=" + url + ",param=" + param, e); + } + catch (IOException e) + { + log.error("调用HttpUtils.sendSSLPost IOException, url=" + url + ",param=" + param, e); + } + catch (Exception e) + { + log.error("调用HttpsUtil.sendSSLPost Exception, url=" + url + ",param=" + param, e); + } + return result.toString(); + } + + private static class TrustAnyTrustManager implements X509TrustManager + { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) + { + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) + { + } + + @Override + public X509Certificate[] getAcceptedIssuers() + { + return new X509Certificate[] {}; + } + } + + private static class TrustAnyHostnameVerifier implements HostnameVerifier + { + @Override + public boolean verify(String hostname, SSLSession session) + { + return true; + } + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/UserAgentUtils.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/UserAgentUtils.java new file mode 100644 index 0000000..a133be2 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/UserAgentUtils.java @@ -0,0 +1,254 @@ +package com.ruoyi.common.utils.http; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import com.ruoyi.common.utils.StringUtils; +import nl.basjes.parse.useragent.UserAgent; +import nl.basjes.parse.useragent.UserAgentAnalyzer; + +/** + * UserAgent解析工具类 + * + * @author ruoyi + */ +public class UserAgentUtils +{ + public static final String UNKNOWN = ""; + + // 浏览器正则表达式模式 + private static final Pattern CHROME_PATTERN = Pattern.compile("Chrome/(\\d+)(?:\\.\\d+)*"); + private static final Pattern FIREFOX_PATTERN = Pattern.compile("Firefox/(\\d+)(?:\\.\\d+)*"); + private static final Pattern EDGE_PATTERN = Pattern.compile("Edg(?:e)?/(\\d+)(?:\\.\\d+)*"); + private static final Pattern SAFARI_PATTERN = Pattern.compile("Version/(\\d+)(?:\\.\\d+)*"); + private static final Pattern OPERA_PATTERN = Pattern.compile("Opera/(\\d+)(?:\\.\\d+)*"); + private static final Pattern IE_PATTERN = Pattern.compile("(?:MSIE |Trident/.*rv:)(\\d+)(?:\\.\\d+)*"); + private static final Pattern SAMSUNG_PATTERN = Pattern.compile("SamsungBrowser/(\\d+)(?:\\.\\d+)*"); + private static final Pattern UC_PATTERN = Pattern.compile("UCBrowser/(\\d+)(?:\\.\\d+)*"); + private static final Pattern QQ_PATTERN = Pattern.compile("QQBrowser/(\\d+)(?:\\.\\d+)*"); + private static final Pattern WECHAT_PATTERN = Pattern.compile("MicroMessenger/(\\d+)(?:\\.\\d+)*"); + private static final Pattern BAIDU_PATTERN = Pattern.compile("baidubrowser/(\\d+)(?:\\.\\d+)*"); + + // 操作系统正则表达式模式 + private static final Pattern WINDOWS_PATTERN = Pattern.compile("Windows NT (\\d+\\.\\d+)"); + private static final Pattern MACOS_PATTERN = Pattern.compile("Mac OS X (\\d+[_\\d]*)"); + private static final Pattern ANDROID_PATTERN = Pattern.compile("Android (\\d+)(?:\\.\\d+)*"); + private static final Pattern IOS_PATTERN = Pattern.compile("OS[\\s_](\\d+)(?:_\\d+)*"); + private static final Pattern LINUX_PATTERN = Pattern.compile("Linux"); + private static final Pattern CHROMEOS_PATTERN = Pattern.compile("CrOS"); + + private static final UserAgentAnalyzer userAgentAnalyzer = UserAgentAnalyzer + .newBuilder().hideMatcherLoadStats() + .withCache(5000) + .showMinimalVersion() + .withField(UserAgent.AGENT_NAME_VERSION) + .withField(UserAgent.OPERATING_SYSTEM_NAME_VERSION) + .build(); + + /** + * 获取客户端浏览器 + */ + public static String getBrowser(String userAgent) + { + UserAgent.ImmutableUserAgent iua = userAgentAnalyzer.parse(userAgent); + String agentNameVersion = iua.get(UserAgent.AGENT_NAME_VERSION).getValue(); + if (StringUtils.isBlank(agentNameVersion) || agentNameVersion.contains("??")) + { + return formatBrowser(userAgent); + } + return agentNameVersion; + } + + /** + * 获取客户端操作系统 + */ + public static String getOperatingSystem(String userAgent) + { + UserAgent.ImmutableUserAgent iua = userAgentAnalyzer.parse(userAgent); + String operatingSystemNameVersion = iua.get(UserAgent.OPERATING_SYSTEM_NAME_VERSION).getValue(); + if (StringUtils.isBlank(operatingSystemNameVersion) || operatingSystemNameVersion.contains("??")) + { + return formatOperatingSystem(userAgent); + } + return operatingSystemNameVersion; + } + + /** + * 全面浏览器检测 + */ + private static String formatBrowser(String browser) + { + // Chrome系列浏览器 + Matcher chromeMatcher = CHROME_PATTERN.matcher(browser); + if (chromeMatcher.find() && (browser.contains("Chrome") || browser.contains("CriOS"))) + { + return "Chrome" + chromeMatcher.group(1); + } + // Firefox + Matcher firefoxMatcher = FIREFOX_PATTERN.matcher(browser); + if (firefoxMatcher.find()) + { + return "Firefox" + firefoxMatcher.group(1); + } + // Edge浏览器 + Matcher edgeMatcher = EDGE_PATTERN.matcher(browser); + if (edgeMatcher.find()) + { + return "Edge" + edgeMatcher.group(1); + } + // Safari浏览器(需排除Chrome) + Matcher safariMatcher = SAFARI_PATTERN.matcher(browser); + if (safariMatcher.find() && !browser.contains("Chrome")) + { + return "Safari" + safariMatcher.group(1); + } + // 微信内置浏览器 + Matcher wechatMatcher = WECHAT_PATTERN.matcher(browser); + if (wechatMatcher.find()) + { + return "WeChat" + wechatMatcher.group(1); + } + // UC浏览器 + Matcher ucMatcher = UC_PATTERN.matcher(browser); + if (ucMatcher.find()) + { + return "UC Browser" + ucMatcher.group(1); + } + // QQ浏览器 + Matcher qqMatcher = QQ_PATTERN.matcher(browser); + if (qqMatcher.find()) + { + return "QQ Browser" + qqMatcher.group(1); + } + // 百度浏览器 + Matcher baiduMatcher = BAIDU_PATTERN.matcher(browser); + if (baiduMatcher.find()) + { + return "Baidu Browser" + baiduMatcher.group(1); + } + // Samsung浏览器 + Matcher samsungMatcher = SAMSUNG_PATTERN.matcher(browser); + if (samsungMatcher.find()) + { + return "Samsung Browser" + samsungMatcher.group(1); + } + // Opera浏览器 + Matcher operaMatcher = OPERA_PATTERN.matcher(browser); + if (operaMatcher.find()) + { + return "Opera" + operaMatcher.group(1); + } + // IE浏览器 + Matcher ieMatcher = IE_PATTERN.matcher(browser); + if (ieMatcher.find()) + { + return "Internet Explorer" + ieMatcher.group(1); + } + return UNKNOWN; + } + + /** + * 检测操作系统 + */ + private static String formatOperatingSystem(String operatingSystem) + { + // Windows系统 + Matcher windowsMatcher = WINDOWS_PATTERN.matcher(operatingSystem); + if (windowsMatcher.find()) + { + return "Windows" + getWindowsVersionDisplay(windowsMatcher.group(1)); + } + // macOS系统 + Matcher macMatcher = MACOS_PATTERN.matcher(operatingSystem); + if (macMatcher.find()) + { + String version = macMatcher.group(1).replace("_", "."); + return "macOS" + extractMajorVersion(version); + } + // Android系统 + Matcher androidMatcher = ANDROID_PATTERN.matcher(operatingSystem); + if (androidMatcher.find()) + { + return "Android" + extractMajorVersion(androidMatcher.group(1)); + } + // iOS系统 + Matcher iosMatcher = IOS_PATTERN.matcher(operatingSystem); + if (iosMatcher.find() && (operatingSystem.contains("iPhone") || operatingSystem.contains("iPad"))) + { + return "iOS" + extractMajorVersion(iosMatcher.group(1)); + } + // Linux系统 + if (LINUX_PATTERN.matcher(operatingSystem).find() && !operatingSystem.contains("Android")) + { + return "Linux"; + } + // Chrome OS + if (CHROMEOS_PATTERN.matcher(operatingSystem).find()) + { + return "Chrome OS"; + } + return UNKNOWN; + } + + /** + * 提取优化的主版本号 + */ + private static String extractMajorVersion(String fullVersion) + { + if (StringUtils.isEmpty(fullVersion)) + { + return StringUtils.EMPTY; + } + try + { + // 清理版本号中的非数字字符 + String cleanVersion = fullVersion.replaceAll("[^0-9.]", ""); + String[] parts = cleanVersion.split("\\."); + if (parts.length > 0) + { + String firstPart = parts[0]; + if (firstPart.matches("\\d+")) + { + int version = Integer.parseInt(firstPart); + + // 处理三位数版本号(如142 -> 14) + if (version >= 100) + { + return String.valueOf(version / 10); + } + return firstPart; + } + } + } + catch (NumberFormatException e) + { + // 版本号解析失败,返回原始值 + } + return fullVersion; + } + + /** + * Windows版本号显示优化 + */ + private static String getWindowsVersionDisplay(String version) + { + switch (version) + { + case "10.0": + return "10"; + case "6.3": + return "8.1"; + case "6.2": + return "8"; + case "6.1": + return "7"; + case "6.0": + return "Vista"; + case "5.1": + return "XP"; + case "5.0": + return "2000"; + default: + return extractMajorVersion(version); + } + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/id/IdGenerator.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/id/IdGenerator.java new file mode 100644 index 0000000..7d7984f --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/id/IdGenerator.java @@ -0,0 +1,83 @@ +package com.ruoyi.common.utils.id; + +/** + * compressed id generator, result id not greater than 53bits before 2318-06-04. + * 移植自 hwt-code (IdGenerator.java) - 雪花 ID 压缩版. + * 53-bit 容量: 2^53-1 = 9007199254740991, JS Number 安全. + */ +public class IdGenerator { + + private static IdGenerator instance = new IdGenerator(0); + + public static IdGenerator initDefaultInstance(int machineId) { + instance = new IdGenerator(machineId); + return instance; + } + + public static IdGenerator getInstance() { + return instance; + } + + /** 生成下一个 Long ID */ + public static long generateId() { + return instance.nextId(); + } + + /** 业务短 UUID (32位) */ + public static String uuid() { + return java.util.UUID.randomUUID().toString().replaceAll("-", ""); + } + + // total bits=53(max 2^53-1: 9007199254740992-1) + private final static long MACHINE_BIT = 5; // max 31 + private final static long SEQUENCE_BIT = 8; // 256/10ms + + private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT); + private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT); + + private final static long MACHINE_LEFT = SEQUENCE_BIT; + private final static long TIMESTMP_LEFT = MACHINE_BIT + SEQUENCE_BIT; + + private long machineId; + private long sequence = 0L; + private long lastStmp = -1L; + + private IdGenerator(long machineId) { + if (machineId > MAX_MACHINE_NUM || machineId < 0) { + throw new IllegalArgumentException( + "machineId can't be greater than " + MAX_MACHINE_NUM + " or less than 0"); + } + this.machineId = machineId; + } + + public synchronized long nextId() { + long currStmp = getTimestamp(); + if (currStmp < lastStmp) { + throw new RuntimeException("Clock moved backwards. Refusing to generate id"); + } + if (currStmp == lastStmp) { + sequence = (sequence + 1) & MAX_SEQUENCE; + if (sequence == 0L) { + currStmp = getNextTimestamp(); + } + } else { + sequence = 0L; + } + lastStmp = currStmp; + return currStmp << TIMESTMP_LEFT | machineId << MACHINE_LEFT | sequence; + } + + private long getNextTimestamp() { + long mill = getTimestamp(); + while (mill <= lastStmp) mill = getTimestamp(); + return mill; + } + + private long getTimestamp() { + return System.currentTimeMillis() / 10; // per 10ms + } + + public static java.util.Date parseIdTimestamp(long id) { + return new java.util.Date((id >>> TIMESTMP_LEFT) * 10); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/id/SnowflakeId.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/id/SnowflakeId.java new file mode 100644 index 0000000..726f1ea --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/id/SnowflakeId.java @@ -0,0 +1,36 @@ +package com.ruoyi.common.utils.id; + +import java.lang.reflect.Method; + +/** + * 雪花 ID 注入工具 - 业务 entity insert 前若主键为空则自动填雪花 ID. + * 约定: 主键 getter/setter 命名 must be getXxxId()/setXxxId(String) (BizXxxId 风格). + */ +public class SnowflakeId { + + /** + * 主键为空时, 雪花 ID 注入 id field (支持 String 类型主键). + * 通过反射调用 setXxxId(String.valueOf(IdGenerator.generateId())). + * + * @param entity 业务对象 + * @param idFieldName 主键字段名, 如 "projectId" / "annId" / "id" (注意 BizMeetingSettlement 是 "id") + */ + public static void injectIfEmpty(Object entity, String idFieldName) { + if (entity == null) return; + try { + String getter = "get" + Character.toUpperCase(idFieldName.charAt(0)) + idFieldName.substring(1); + String setter = "set" + Character.toUpperCase(idFieldName.charAt(0)) + idFieldName.substring(1); + Method getM = entity.getClass().getMethod(getter); + Object current = getM.invoke(entity); + if (current == null || (current instanceof String && ((String) current).isEmpty())) { + String snowId = String.valueOf(IdGenerator.generateId()); + Method setM = entity.getClass().getMethod(setter, String.class); + setM.invoke(entity, snowId); + } + } catch (NoSuchMethodException e) { + // 字段可能叫别的, 跳过 + } catch (Exception e) { + throw new RuntimeException("SnowflakeId inject failed: " + entity.getClass().getSimpleName() + "." + idFieldName, e); + } + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/ip/AddressUtils.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/ip/AddressUtils.java new file mode 100644 index 0000000..9158cf6 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/ip/AddressUtils.java @@ -0,0 +1,56 @@ +package com.ruoyi.common.utils.ip; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.http.HttpUtils; + +/** + * 获取地址类 + * + * @author ruoyi + */ +public class AddressUtils +{ + private static final Logger log = LoggerFactory.getLogger(AddressUtils.class); + + // IP地址查询 + public static final String IP_URL = "https://whois.pconline.com.cn/ipJson.jsp"; + + // 未知地址 + public static final String UNKNOWN = "XX XX"; + + public static String getRealAddressByIP(String ip) + { + // 内网不查询 + if (IpUtils.internalIp(ip)) + { + return "内网IP"; + } + if (RuoYiConfig.isAddressEnabled()) + { + try + { + String rspStr = HttpUtils.sendGet(IP_URL, "ip=" + ip + "&json=true", Constants.GBK); + if (StringUtils.isEmpty(rspStr)) + { + log.error("获取地理位置异常 {}", ip); + return UNKNOWN; + } + JSONObject obj = JSON.parseObject(rspStr); + String region = obj.getString("pro"); + String city = obj.getString("city"); + return String.format("%s %s", region, city); + } + catch (Exception e) + { + log.error("获取地理位置异常 {}", ip); + } + } + return UNKNOWN; + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/ip/IpUtils.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/ip/IpUtils.java new file mode 100644 index 0000000..a376416 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/ip/IpUtils.java @@ -0,0 +1,382 @@ +package com.ruoyi.common.utils.ip; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import jakarta.servlet.http.HttpServletRequest; +import com.ruoyi.common.utils.ServletUtils; +import com.ruoyi.common.utils.StringUtils; + +/** + * 获取IP方法 + * + * @author ruoyi + */ +public class IpUtils +{ + public final static String REGX_0_255 = "(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]\\d|\\d)"; + // 匹配 ip + public final static String REGX_IP = "((" + REGX_0_255 + "\\.){3}" + REGX_0_255 + ")"; + public final static String REGX_IP_WILDCARD = "(((\\*\\.){3}\\*)|(" + REGX_0_255 + "(\\.\\*){3})|(" + REGX_0_255 + "\\." + REGX_0_255 + ")(\\.\\*){2}" + "|((" + REGX_0_255 + "\\.){3}\\*))"; + // 匹配网段 + public final static String REGX_IP_SEG = "(" + REGX_IP + "\\-" + REGX_IP + ")"; + + /** + * 获取客户端IP + * + * @return IP地址 + */ + public static String getIpAddr() + { + return getIpAddr(ServletUtils.getRequest()); + } + + /** + * 获取客户端IP + * + * @param request 请求对象 + * @return IP地址 + */ + public static String getIpAddr(HttpServletRequest request) + { + if (request == null) + { + return "unknown"; + } + String ip = request.getHeader("x-forwarded-for"); + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) + { + ip = request.getHeader("Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) + { + ip = request.getHeader("X-Forwarded-For"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) + { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) + { + ip = request.getHeader("X-Real-IP"); + } + + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) + { + ip = request.getRemoteAddr(); + } + + return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : getMultistageReverseProxyIp(ip); + } + + /** + * 检查是否为内部IP地址 + * + * @param ip IP地址 + * @return 结果 + */ + public static boolean internalIp(String ip) + { + byte[] addr = textToNumericFormatV4(ip); + return internalIp(addr) || "127.0.0.1".equals(ip); + } + + /** + * 检查是否为内部IP地址 + * + * @param addr byte地址 + * @return 结果 + */ + private static boolean internalIp(byte[] addr) + { + if (StringUtils.isNull(addr) || addr.length < 2) + { + return true; + } + final byte b0 = addr[0]; + final byte b1 = addr[1]; + // 10.x.x.x/8 + final byte SECTION_1 = 0x0A; + // 172.16.x.x/12 + final byte SECTION_2 = (byte) 0xAC; + final byte SECTION_3 = (byte) 0x10; + final byte SECTION_4 = (byte) 0x1F; + // 192.168.x.x/16 + final byte SECTION_5 = (byte) 0xC0; + final byte SECTION_6 = (byte) 0xA8; + switch (b0) + { + case SECTION_1: + return true; + case SECTION_2: + if (b1 >= SECTION_3 && b1 <= SECTION_4) + { + return true; + } + case SECTION_5: + switch (b1) + { + case SECTION_6: + return true; + } + default: + return false; + } + } + + /** + * 将IPv4地址转换成字节 + * + * @param text IPv4地址 + * @return byte 字节 + */ + public static byte[] textToNumericFormatV4(String text) + { + if (text.length() == 0) + { + return null; + } + + byte[] bytes = new byte[4]; + String[] elements = text.split("\\.", -1); + try + { + long l; + int i; + switch (elements.length) + { + case 1: + l = Long.parseLong(elements[0]); + if ((l < 0L) || (l > 4294967295L)) + { + return null; + } + bytes[0] = (byte) (int) (l >> 24 & 0xFF); + bytes[1] = (byte) (int) ((l & 0xFFFFFF) >> 16 & 0xFF); + bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF); + bytes[3] = (byte) (int) (l & 0xFF); + break; + case 2: + l = Integer.parseInt(elements[0]); + if ((l < 0L) || (l > 255L)) + { + return null; + } + bytes[0] = (byte) (int) (l & 0xFF); + l = Integer.parseInt(elements[1]); + if ((l < 0L) || (l > 16777215L)) + { + return null; + } + bytes[1] = (byte) (int) (l >> 16 & 0xFF); + bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF); + bytes[3] = (byte) (int) (l & 0xFF); + break; + case 3: + for (i = 0; i < 2; ++i) + { + l = Integer.parseInt(elements[i]); + if ((l < 0L) || (l > 255L)) + { + return null; + } + bytes[i] = (byte) (int) (l & 0xFF); + } + l = Integer.parseInt(elements[2]); + if ((l < 0L) || (l > 65535L)) + { + return null; + } + bytes[2] = (byte) (int) (l >> 8 & 0xFF); + bytes[3] = (byte) (int) (l & 0xFF); + break; + case 4: + for (i = 0; i < 4; ++i) + { + l = Integer.parseInt(elements[i]); + if ((l < 0L) || (l > 255L)) + { + return null; + } + bytes[i] = (byte) (int) (l & 0xFF); + } + break; + default: + return null; + } + } + catch (NumberFormatException e) + { + return null; + } + return bytes; + } + + /** + * 获取IP地址 + * + * @return 本地IP地址 + */ + public static String getHostIp() + { + try + { + return InetAddress.getLocalHost().getHostAddress(); + } + catch (UnknownHostException e) + { + } + return "127.0.0.1"; + } + + /** + * 获取主机名 + * + * @return 本地主机名 + */ + public static String getHostName() + { + try + { + return InetAddress.getLocalHost().getHostName(); + } + catch (UnknownHostException e) + { + } + return "未知"; + } + + /** + * 从多级反向代理中获得第一个非unknown IP地址 + * + * @param ip 获得的IP地址 + * @return 第一个非unknown IP地址 + */ + public static String getMultistageReverseProxyIp(String ip) + { + // 多级反向代理检测 + if (ip != null && ip.indexOf(",") > 0) + { + final String[] ips = ip.trim().split(","); + for (String subIp : ips) + { + if (false == isUnknown(subIp)) + { + ip = subIp; + break; + } + } + } + return StringUtils.substring(ip, 0, 255); + } + + /** + * 检测给定字符串是否为未知,多用于检测HTTP请求相关 + * + * @param checkString 被检测的字符串 + * @return 是否未知 + */ + public static boolean isUnknown(String checkString) + { + return StringUtils.isBlank(checkString) || "unknown".equalsIgnoreCase(checkString); + } + + /** + * 是否为IP + */ + public static boolean isIP(String ip) + { + return StringUtils.isNotBlank(ip) && ip.matches(REGX_IP); + } + + /** + * 是否为IP,或 *为间隔的通配符地址 + */ + public static boolean isIpWildCard(String ip) + { + return StringUtils.isNotBlank(ip) && ip.matches(REGX_IP_WILDCARD); + } + + /** + * 检测参数是否在ip通配符里 + */ + public static boolean ipIsInWildCardNoCheck(String ipWildCard, String ip) + { + String[] s1 = ipWildCard.split("\\."); + String[] s2 = ip.split("\\."); + boolean isMatchedSeg = true; + for (int i = 0; i < s1.length && !s1[i].equals("*"); i++) + { + if (!s1[i].equals(s2[i])) + { + isMatchedSeg = false; + break; + } + } + return isMatchedSeg; + } + + /** + * 是否为特定格式如:“10.10.10.1-10.10.10.99”的ip段字符串 + */ + public static boolean isIPSegment(String ipSeg) + { + return StringUtils.isNotBlank(ipSeg) && ipSeg.matches(REGX_IP_SEG); + } + + /** + * 判断ip是否在指定网段中 + */ + public static boolean ipIsInNetNoCheck(String iparea, String ip) + { + int idx = iparea.indexOf('-'); + String[] sips = iparea.substring(0, idx).split("\\."); + String[] sipe = iparea.substring(idx + 1).split("\\."); + String[] sipt = ip.split("\\."); + long ips = 0L, ipe = 0L, ipt = 0L; + for (int i = 0; i < 4; ++i) + { + ips = ips << 8 | Integer.parseInt(sips[i]); + ipe = ipe << 8 | Integer.parseInt(sipe[i]); + ipt = ipt << 8 | Integer.parseInt(sipt[i]); + } + if (ips > ipe) + { + long t = ips; + ips = ipe; + ipe = t; + } + return ips <= ipt && ipt <= ipe; + } + + /** + * 校验ip是否符合过滤串规则 + * + * @param filter 过滤IP列表,支持后缀'*'通配,支持网段如:`10.10.10.1-10.10.10.99` + * @param ip 校验IP地址 + * @return boolean 结果 + */ + public static boolean isMatchedIp(String filter, String ip) + { + if (StringUtils.isEmpty(filter) || StringUtils.isEmpty(ip)) + { + return false; + } + String[] ips = filter.split(";"); + for (String iStr : ips) + { + if (isIP(iStr) && iStr.equals(ip)) + { + return true; + } + else if (isIpWildCard(iStr) && ipIsInWildCardNoCheck(iStr, ip)) + { + return true; + } + else if (isIPSegment(iStr) && ipIsInNetNoCheck(iStr, ip)) + { + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelHandlerAdapter.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelHandlerAdapter.java new file mode 100644 index 0000000..ccab288 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelHandlerAdapter.java @@ -0,0 +1,24 @@ +package com.ruoyi.common.utils.poi; + +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.Workbook; + +/** + * Excel数据格式处理适配器 + * + * @author ruoyi + */ +public interface ExcelHandlerAdapter +{ + /** + * 格式化 + * + * @param value 单元格数据值 + * @param args excel注解args参数组 + * @param cell 单元格对象 + * @param wb 工作簿对象 + * + * @return 处理后的值 + */ + Object format(Object value, String[] args, Cell cell, Workbook wb); +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelSheet.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelSheet.java new file mode 100644 index 0000000..20ca22c --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelSheet.java @@ -0,0 +1,85 @@ +package com.ruoyi.common.utils.poi; + +import java.util.ArrayList; +import java.util.List; + +/** + * 多 Sheet 导出时的数据信息 + * + * 使用示例: + *

+ *   List> sheets = new ArrayList<>();
+ *   sheets.add(new ExcelSheet<>("参数数据", configList, Config.class, "参数信息"));
+ *   sheets.add(new ExcelSheet<>("岗位数据", postList, Post.class, "岗位信息"));
+ *   return ExcelUtil.exportMultiSheet(sheets);
+ * 
+ * + * @author ruoyi + */ +public class ExcelSheet +{ + /** Sheet 名称 */ + private String sheetName; + + /** 导出数据集合 */ + private List list; + + /** 数据对应的实体 Class */ + private Class clazz; + + /** Sheet 顶部大标题(可为空) */ + private String title; + + public ExcelSheet(String sheetName, List list, Class clazz) + { + this(sheetName, list, clazz, ""); + } + + public ExcelSheet(String sheetName, List list, Class clazz, String title) + { + this.sheetName = sheetName; + this.list = list != null ? list : new ArrayList<>(); + this.clazz = clazz; + this.title = title != null ? title : ""; + } + + public String getSheetName() + { + return sheetName; + } + + public List getList() + { + return list; + } + + public Class getClazz() + { + return clazz; + } + + public String getTitle() + { + return title; + } + + public void setSheetName(String sheetName) + { + this.sheetName = sheetName; + } + + public void setList(List list) + { + this.list = list; + } + + public void setClazz(Class clazz) + { + this.clazz = clazz; + } + + public void setTitle(String title) + { + this.title = title; + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java new file mode 100644 index 0000000..aecc0d8 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java @@ -0,0 +1,2067 @@ +package com.ruoyi.common.utils.poi; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.math.BigDecimal; +import java.text.DecimalFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.RegExUtils; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.poi.hssf.usermodel.HSSFClientAnchor; +import org.apache.poi.hssf.usermodel.HSSFPicture; +import org.apache.poi.hssf.usermodel.HSSFPictureData; +import org.apache.poi.hssf.usermodel.HSSFShape; +import org.apache.poi.hssf.usermodel.HSSFSheet; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ooxml.POIXMLDocumentPart; +import org.apache.poi.ss.usermodel.BorderStyle; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.ClientAnchor; +import org.apache.poi.ss.usermodel.DataFormat; +import org.apache.poi.ss.usermodel.DataValidation; +import org.apache.poi.ss.usermodel.DataValidationConstraint; +import org.apache.poi.ss.usermodel.DataValidationHelper; +import org.apache.poi.ss.usermodel.DateUtil; +import org.apache.poi.ss.usermodel.Drawing; +import org.apache.poi.ss.usermodel.FillPatternType; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.IndexedColors; +import org.apache.poi.ss.usermodel.Name; +import org.apache.poi.ss.usermodel.PictureData; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.VerticalAlignment; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; +import org.apache.poi.ss.util.CellRangeAddress; +import org.apache.poi.ss.util.CellRangeAddressList; +import org.apache.poi.util.IOUtils; +import org.apache.poi.xssf.streaming.SXSSFWorkbook; +import org.apache.poi.xssf.usermodel.XSSFClientAnchor; +import org.apache.poi.xssf.usermodel.XSSFDataValidation; +import org.apache.poi.xssf.usermodel.XSSFDrawing; +import org.apache.poi.xssf.usermodel.XSSFPicture; +import org.apache.poi.xssf.usermodel.XSSFShape; +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.openxmlformats.schemas.drawingml.x2006.spreadsheetDrawing.CTMarker; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.annotation.Excel.ColumnType; +import com.ruoyi.common.annotation.Excel.Type; +import com.ruoyi.common.annotation.Excels; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.exception.UtilException; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.DictUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.file.FileTypeUtils; +import com.ruoyi.common.utils.file.FileUtils; +import com.ruoyi.common.utils.file.ImageUtils; +import com.ruoyi.common.utils.reflect.ReflectUtils; + +/** + * Excel相关处理 + * + * @author ruoyi + */ +public class ExcelUtil +{ + private static final Logger log = LoggerFactory.getLogger(ExcelUtil.class); + + public static final String SEPARATOR = ","; + + public static final String FORMULA_REGEX_STR = "=|-|\\+|@"; + + public static final String[] FORMULA_STR = { "=", "-", "+", "@" }; + + /** + * 用于dictType属性数据存储,避免重复查缓存 + */ + public Map sysDictMap = new HashMap(); + + /** + * 单元格样式缓存 + */ + private Map cellStyleCache = new HashMap(); + + /** + * Excel sheet最大行数,默认65536 + */ + public static final int sheetSize = 65536; + + /** + * 工作表名称 + */ + private String sheetName; + + /** + * 导出类型(EXPORT:导出数据;IMPORT:导入模板) + */ + private Type type; + + /** + * 工作薄对象 + */ + private Workbook wb; + + /** + * 工作表对象 + */ + private Sheet sheet; + + /** + * 样式列表 + */ + private Map styles; + + /** + * 导入导出数据列表 + */ + private List list; + + /** + * 注解列表 + */ + private List fields; + + /** + * 当前行号 + */ + private int rownum; + + /** + * 标题 + */ + private String title; + + /** + * 最大高度 + */ + private short maxHeight; + + /** + * 合并后最后行数 + */ + private int subMergedLastRowNum = 0; + + /** + * 合并后开始行数 + */ + private int subMergedFirstRowNum = 1; + + /** + * 对象的子列表方法 + */ + private Map subMethods; + + /** + * 对象的子列表属性 + */ + private Map> subFieldsMap; + + /** + * 统计列表 + */ + private Map statistics = new HashMap(); + + /** + * 实体对象 + */ + public Class clazz; + + /** + * 需要显示列属性 + */ + public String[] includeFields; + + /** + * 需要排除列属性 + */ + public String[] excludeFields; + + public ExcelUtil(Class clazz) + { + this.clazz = clazz; + } + + /** + * 仅在Excel中显示列属性 + * + * @param fields 列属性名 示例[单个"name"/多个"id","name"] + */ + public void showColumn(String... fields) + { + this.includeFields = fields; + } + + /** + * 隐藏Excel中列属性 + * + * @param fields 列属性名 示例[单个"name"/多个"id","name"] + */ + public void hideColumn(String... fields) + { + this.excludeFields = fields; + } + + public void init(List list, String sheetName, String title, Type type) + { + if (list == null) + { + list = new ArrayList(); + } + this.list = list; + this.sheetName = sheetName; + this.type = type; + this.title = title; + createExcelField(); + createWorkbook(); + createTitle(); + createSubHead(); + } + + /** + * 创建excel第一行标题 + */ + public void createTitle() + { + if (StringUtils.isNotEmpty(title)) + { + int titleLastCol = this.fields.size() - 1; + if (isSubList()) + { + for (List currentSubFields : subFieldsMap.values()) + { + titleLastCol = titleLastCol + currentSubFields.size() - 1; + } + } + Row titleRow = sheet.createRow(rownum == 0 ? rownum++ : 0); + titleRow.setHeightInPoints(30); + Cell titleCell = titleRow.createCell(0); + titleCell.setCellStyle(styles.get("title")); + titleCell.setCellValue(title); + sheet.addMergedRegion(new CellRangeAddress(titleRow.getRowNum(), titleRow.getRowNum(), 0, titleLastCol)); + } + } + + /** + * 创建对象的子列表名称 + */ + public void createSubHead() + { + if (isSubList()) + { + Row subRow = sheet.createRow(rownum); + int column = 0; + for (Object[] objects : fields) + { + Field field = (Field) objects[0]; + Excel attr = (Excel) objects[1]; + CellStyle cellStyle = styles.get(StringUtils.format("header_{}_{}", attr.headerColor(), attr.headerBackgroundColor())); + if (Collection.class.isAssignableFrom(field.getType())) + { + Cell cell = subRow.createCell(column); + cell.setCellValue(attr.name()); + cell.setCellStyle(cellStyle); + int subFieldSize = subFieldsMap != null ? subFieldsMap.get(field.getName()).size() : 0; + if (subFieldSize > 1) + { + CellRangeAddress cellAddress = new CellRangeAddress(rownum, rownum, column, column + subFieldSize - 1); + sheet.addMergedRegion(cellAddress); + } + column += subFieldSize; + } + else + { + Cell cell = subRow.createCell(column++); + cell.setCellValue(attr.name()); + cell.setCellStyle(cellStyle); + } + } + rownum++; + } + } + + /** + * 对excel表单默认第一个索引名转换成list + * + * @param is 输入流 + * @return 转换后集合 + */ + public List importExcel(InputStream is) + { + return importExcel(is, 0); + } + + /** + * 对excel表单默认第一个索引名转换成list + * + * @param is 输入流 + * @param titleNum 标题占用行数 + * @return 转换后集合 + */ + public List importExcel(InputStream is, int titleNum) + { + List list = null; + try + { + list = importExcel(StringUtils.EMPTY, is, titleNum); + } + catch (Exception e) + { + log.error("导入Excel异常{}", e.getMessage()); + throw new UtilException(e.getMessage()); + } + finally + { + IOUtils.closeQuietly(is); + } + return list; + } + + /** + * 对excel表单指定表格索引名转换成list + * + * @param sheetName 表格索引名 + * @param titleNum 标题占用行数 + * @param is 输入流 + * @return 转换后集合 + */ + public List importExcel(String sheetName, InputStream is, int titleNum) throws Exception + { + this.type = Type.IMPORT; + this.wb = WorkbookFactory.create(is); + List list = new ArrayList(); + // 如果指定sheet名,则取指定sheet中的内容 否则默认指向第1个sheet + Sheet sheet = StringUtils.isNotEmpty(sheetName) ? wb.getSheet(sheetName) : wb.getSheetAt(0); + if (sheet == null) + { + throw new IOException("文件sheet不存在"); + } + boolean isXSSFWorkbook = !(wb instanceof HSSFWorkbook); + Map> pictures = null; + if (isXSSFWorkbook) + { + pictures = getSheetPictures07((XSSFSheet) sheet, (XSSFWorkbook) wb); + } + else + { + pictures = getSheetPictures03((HSSFSheet) sheet, (HSSFWorkbook) wb); + } + // 获取最后一个非空行的行下标,比如总行数为n,则返回的为n-1 + int rows = sheet.getLastRowNum(); + if (rows > 0) + { + // 定义一个map用于存放excel列的序号和field. + Map cellMap = new HashMap(); + // 获取表头 + Row heard = sheet.getRow(titleNum); + if (heard == null) + { + throw new UtilException("文件标题行为空,请检查Excel文件格式"); + } + for (int i = 0; i < heard.getLastCellNum(); i++) + { + Cell cell = heard.getCell(i); + if (StringUtils.isNotNull(cell)) + { + String value = this.getCellValue(heard, i).toString(); + cellMap.put(value, i); + } + } + // 有数据时才处理 得到类的所有field. + List fields = this.getFields(); + Map fieldsMap = new HashMap(); + for (Object[] objects : fields) + { + Excel attr = (Excel) objects[1]; + Integer column = cellMap.get(attr.name()); + if (column != null) + { + fieldsMap.put(column, objects); + } + } + for (int i = titleNum + 1; i <= rows; i++) + { + // 从第2行开始取数据,默认第一行是表头. + Row row = sheet.getRow(i); + // 判断当前行是否是空行 + if (isRowEmpty(row)) + { + continue; + } + T entity = null; + for (Map.Entry entry : fieldsMap.entrySet()) + { + Object val = this.getCellValue(row, entry.getKey()); + + // 如果不存在实例则新建. + entity = (entity == null ? clazz.getDeclaredConstructor().newInstance() : entity); + // 从map中得到对应列的field. + Field field = (Field) entry.getValue()[0]; + Excel attr = (Excel) entry.getValue()[1]; + // 取得类型,并根据对象类型设置值. + Class fieldType = field.getType(); + if (String.class == fieldType) + { + String s = Convert.toStr(val); + if (s.matches("^\\d+\\.0$")) + { + val = StringUtils.substringBefore(s, ".0"); + } + else + { + String dateFormat = field.getAnnotation(Excel.class).dateFormat(); + if (StringUtils.isNotEmpty(dateFormat)) + { + val = parseDateToStr(dateFormat, val); + } + else + { + val = Convert.toStr(val); + } + } + } + else if ((Integer.TYPE == fieldType || Integer.class == fieldType) && StringUtils.isNumeric(Convert.toStr(val))) + { + val = Convert.toInt(val); + } + else if ((Long.TYPE == fieldType || Long.class == fieldType) && StringUtils.isNumeric(Convert.toStr(val))) + { + val = Convert.toLong(val); + } + else if (Double.TYPE == fieldType || Double.class == fieldType) + { + val = Convert.toDouble(val); + } + else if (Float.TYPE == fieldType || Float.class == fieldType) + { + val = Convert.toFloat(val); + } + else if (BigDecimal.class == fieldType) + { + val = Convert.toBigDecimal(val); + } + else if (Date.class == fieldType) + { + if (val instanceof String) + { + val = DateUtils.parseDate(val); + } + else if (val instanceof Double) + { + val = DateUtil.getJavaDate((Double) val); + } + } + else if (Boolean.TYPE == fieldType || Boolean.class == fieldType) + { + val = Convert.toBool(val, false); + } + if (StringUtils.isNotNull(fieldType)) + { + String propertyName = field.getName(); + if (StringUtils.isNotEmpty(attr.targetAttr())) + { + propertyName = field.getName() + "." + attr.targetAttr(); + } + if (StringUtils.isNotEmpty(attr.readConverterExp())) + { + val = reverseByExp(Convert.toStr(val), attr.readConverterExp(), attr.separator()); + } + else if (StringUtils.isNotEmpty(attr.dictType())) + { + if (!sysDictMap.containsKey(attr.dictType() + val)) + { + String dictValue = reverseDictByExp(Convert.toStr(val), attr.dictType(), attr.separator()); + sysDictMap.put(attr.dictType() + val, dictValue); + } + val = sysDictMap.get(attr.dictType() + val); + } + else if (!attr.handler().equals(ExcelHandlerAdapter.class)) + { + val = dataFormatHandlerAdapter(val, attr, null); + } + else if (ColumnType.IMAGE == attr.cellType() && StringUtils.isNotEmpty(pictures)) + { + StringBuilder propertyString = new StringBuilder(); + List images = pictures.get(row.getRowNum() + "_" + entry.getKey()); + for (PictureData picture : images) + { + byte[] data = picture.getData(); + String fileName = FileUtils.writeImportBytes(data); + propertyString.append(fileName).append(SEPARATOR); + } + val = StringUtils.stripEnd(propertyString.toString(), SEPARATOR); + } + ReflectUtils.invokeSetter(entity, propertyName, val); + } + } + list.add(entity); + } + } + return list; + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param list 导出数据集合 + * @param sheetName 工作表的名称 + * @return 结果 + */ + public AjaxResult exportExcel(List list, String sheetName) + { + return exportExcel(list, sheetName, StringUtils.EMPTY); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param list 导出数据集合 + * @param sheetName 工作表的名称 + * @param title 标题 + * @return 结果 + */ + public AjaxResult exportExcel(List list, String sheetName, String title) + { + this.init(list, sheetName, title, Type.EXPORT); + return exportExcel(); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param response 返回数据 + * @param list 导出数据集合 + * @param sheetName 工作表的名称 + * @return 结果 + */ + public void exportExcel(HttpServletResponse response, List list, String sheetName) + { + exportExcel(response, list, sheetName, StringUtils.EMPTY); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param response 返回数据 + * @param list 导出数据集合 + * @param sheetName 工作表的名称 + * @param title 标题 + * @return 结果 + */ + public void exportExcel(HttpServletResponse response, List list, String sheetName, String title) + { + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setCharacterEncoding("utf-8"); + this.init(list, sheetName, title, Type.EXPORT); + exportExcel(response); + } + + /** + * 多 Sheet 导出 —— 将多个不同类型的数据集合写入同一 Excel,直接输出到 HttpServletResponse + * + * @param response HTTP 响应 + * @param sheets Sheet 描述列表 + */ + public static void exportMultiSheet(HttpServletResponse response, List> sheets) + { + if (sheets == null || sheets.isEmpty()) + { + return; + } + SXSSFWorkbook wb = buildWorkbook(sheets); + try + { + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setCharacterEncoding("utf-8"); + wb.write(response.getOutputStream()); + } + catch (Exception e) + { + log.error("多Sheet导出Excel异常{}", e.getMessage()); + } + finally + { + IOUtils.closeQuietly(wb); + } + } + + /** + * 多 Sheet 导出 —— 将多个不同类型的数据集合写入同一 Excel,生成文件并返回下载地址 + * + * @param sheets Sheet 描述列表 + * @return AjaxResult(含文件下载地址) + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static AjaxResult exportMultiSheet(List> sheets) + { + if (sheets == null || sheets.isEmpty()) + { + return AjaxResult.error("导出数据不能为空"); + } + SXSSFWorkbook wb = buildWorkbook(sheets); + OutputStream out = null; + try + { + ExcelUtil firstUtil = new ExcelUtil(sheets.get(0).getClazz()); + String filename = firstUtil.encodingFilename(sheets.get(0).getSheetName()); + out = new FileOutputStream(firstUtil.getAbsoluteFile(filename)); + wb.write(out); + return AjaxResult.success(filename); + } + catch (Exception e) + { + log.error("多Sheet导出Excel异常{}", e.getMessage()); + throw new UtilException("导出Excel失败,请联系网站管理员!"); + } + finally + { + IOUtils.closeQuietly(wb); + IOUtils.closeQuietly(out); + } + } + + /** + * 构建多 Sheet Workbook —— 创建 SXSSFWorkbook 并将所有 Sheet 数据写入 + * + * @param sheets Sheet 描述列表 + * @return 已写入所有 Sheet 数据的 SXSSFWorkbook + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + private static SXSSFWorkbook buildWorkbook(List> sheets) + { + SXSSFWorkbook wb = new SXSSFWorkbook(500); + for (ExcelSheet excelSheet : sheets) + { + ExcelUtil util = new ExcelUtil(excelSheet.getClazz()); + util.initWithWorkbook(wb, excelSheet.getList(), excelSheet.getSheetName(), excelSheet.getTitle()); + util.writeSheet(); + } + return wb; + } + + /** + * 使用外部传入的 Workbook 初始化(多 Sheet 导出专用) + * 与 init() 的区别:不新建 Workbook,而是在已有 wb 上追加新 Sheet + * + * @param wb 已有工作簿 + * @param list 数据集合 + * @param sheetName Sheet 名称 + * @param title 大标题(可为空) + */ + public void initWithWorkbook(SXSSFWorkbook wb, List list, String sheetName, String title) + { + if (list == null) + { + list = new ArrayList(); + } + this.list = list; + this.sheetName = sheetName; + this.title = title != null ? title : ""; + this.type = Type.EXPORT; + this.rownum = 0; + this.wb = wb; + this.sheet = wb.createSheet(sheetName); + createExcelField(); + this.styles = createStyles(wb); + createTitle(); + createSubHead(); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param sheetName 工作表的名称 + * @return 结果 + */ + public AjaxResult importTemplateExcel(String sheetName) + { + return importTemplateExcel(sheetName, StringUtils.EMPTY); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param sheetName 工作表的名称 + * @param title 标题 + * @return 结果 + */ + public AjaxResult importTemplateExcel(String sheetName, String title) + { + this.init(null, sheetName, title, Type.IMPORT); + return exportExcel(); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param sheetName 工作表的名称 + * @return 结果 + */ + public void importTemplateExcel(HttpServletResponse response, String sheetName) + { + importTemplateExcel(response, sheetName, StringUtils.EMPTY); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param sheetName 工作表的名称 + * @param title 标题 + * @return 结果 + */ + public void importTemplateExcel(HttpServletResponse response, String sheetName, String title) + { + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setCharacterEncoding("utf-8"); + this.init(null, sheetName, title, Type.IMPORT); + exportExcel(response); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @return 结果 + */ + public void exportExcel(HttpServletResponse response) + { + try + { + writeSheet(); + wb.write(response.getOutputStream()); + } + catch (Exception e) + { + log.error("导出Excel异常{}", e.getMessage()); + } + finally + { + IOUtils.closeQuietly(wb); + } + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @return 结果 + */ + public AjaxResult exportExcel() + { + OutputStream out = null; + try + { + writeSheet(); + String filename = encodingFilename(sheetName); + out = new FileOutputStream(getAbsoluteFile(filename)); + wb.write(out); + return AjaxResult.success(filename); + } + catch (Exception e) + { + log.error("导出Excel异常{}", e.getMessage()); + throw new UtilException("导出Excel失败,请联系网站管理员!"); + } + finally + { + IOUtils.closeQuietly(wb); + IOUtils.closeQuietly(out); + } + } + + /** + * 创建写入数据到Sheet + */ + public void writeSheet() + { + // 取出一共有多少个sheet. + int sheetNo = Math.max(1, (int) Math.ceil(list.size() * 1.0 / sheetSize)); + for (int index = 0; index < sheetNo; index++) + { + createSheet(sheetNo, index); + + // 产生一行 + Row row = sheet.createRow(rownum); + int column = 0; + // 写入各个字段的列头名称 + for (Object[] os : fields) + { + Field field = (Field) os[0]; + Excel excel = (Excel) os[1]; + if (Collection.class.isAssignableFrom(field.getType())) + { + List currentSubFields = subFieldsMap.get(field.getName()); + for (Field subField : currentSubFields) + { + Excel subExcel = subField.getAnnotation(Excel.class); + this.createHeadCell(subExcel, row, column++); + } + } + else + { + this.createHeadCell(excel, row, column++); + } + } + if (Type.EXPORT.equals(type)) + { + fillExcelData(index); + addStatisticsRow(); + } + } + } + + /** + * 填充excel数据 + * + * @param index 序号 + */ + @SuppressWarnings("unchecked") + public void fillExcelData(int index) + { + int startNo = index * sheetSize; + int endNo = Math.min(startNo + sheetSize, list.size()); + int currentRowNum = rownum + 1; // 从标题行后开始 + + for (int i = startNo; i < endNo; i++) + { + Row row = sheet.createRow(currentRowNum); + T vo = (T) list.get(i); + int column = 0; + int maxSubListSize = getCurrentMaxSubListSize(vo); + for (Object[] os : fields) + { + Field field = (Field) os[0]; + Excel excel = (Excel) os[1]; + if (Collection.class.isAssignableFrom(field.getType())) + { + try + { + Collection subList = (Collection) getTargetValue(vo, field, excel); + List currentSubFields = subFieldsMap.get(field.getName()); + if (subList != null && !subList.isEmpty()) + { + int subIndex = 0; + for (Object subVo : subList) + { + Row subRow = sheet.getRow(currentRowNum + subIndex); + if (subRow == null) + { + subRow = sheet.createRow(currentRowNum + subIndex); + } + + int subColumn = column; + for (Field subField : currentSubFields) + { + Excel subExcel = subField.getAnnotation(Excel.class); + addCell(subExcel, subRow, (T) subVo, subField, subColumn++); + } + subIndex++; + } + } + column += currentSubFields.size(); + } + catch (Exception e) + { + log.error("填充集合数据失败", e); + } + } + else + { + // 创建单元格并设置值 + addCell(excel, row, vo, field, column); + if (maxSubListSize > 1 && excel.needMerge()) + { + sheet.addMergedRegion(new CellRangeAddress(currentRowNum, currentRowNum + maxSubListSize - 1, column, column)); + } + column++; + } + } + currentRowNum += maxSubListSize; + } + } + + /** + * 获取子列表最大数 + */ + private int getCurrentMaxSubListSize(T vo) + { + int maxSubListSize = 1; + for (Object[] os : fields) + { + Field field = (Field) os[0]; + if (Collection.class.isAssignableFrom(field.getType())) + { + try + { + Collection subList = (Collection) getTargetValue(vo, field, (Excel) os[1]); + if (subList != null && !subList.isEmpty()) + { + maxSubListSize = Math.max(maxSubListSize, subList.size()); + } + } + catch (Exception e) + { + log.error("获取集合大小失败", e); + } + } + } + return maxSubListSize; + } + + /** + * 创建表格样式 + * + * @param wb 工作薄对象 + * @return 样式列表 + */ + private Map createStyles(Workbook wb) + { + // 写入各条记录,每条记录对应excel表中的一行 + Map styles = new HashMap(); + CellStyle style = wb.createCellStyle(); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + Font titleFont = wb.createFont(); + titleFont.setFontName("Arial"); + titleFont.setFontHeightInPoints((short) 16); + titleFont.setBold(true); + style.setFont(titleFont); + DataFormat dataFormat = wb.createDataFormat(); + style.setDataFormat(dataFormat.getFormat("@")); + styles.put("title", style); + + style = wb.createCellStyle(); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setBorderRight(BorderStyle.THIN); + style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderLeft(BorderStyle.THIN); + style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderTop(BorderStyle.THIN); + style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderBottom(BorderStyle.THIN); + style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + Font dataFont = wb.createFont(); + dataFont.setFontName("Arial"); + dataFont.setFontHeightInPoints((short) 10); + style.setFont(dataFont); + styles.put("data", style); + + style = wb.createCellStyle(); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setDataFormat(dataFormat.getFormat("######0.00")); + Font totalFont = wb.createFont(); + totalFont.setFontName("Arial"); + totalFont.setFontHeightInPoints((short) 10); + style.setFont(totalFont); + styles.put("total", style); + + styles.putAll(annotationHeaderStyles(wb, styles)); + + styles.putAll(annotationDataStyles(wb)); + + return styles; + } + + /** + * 根据Excel注解创建表格头样式 + * + * @param wb 工作薄对象 + * @return 自定义样式列表 + */ + private Map annotationHeaderStyles(Workbook wb, Map styles) + { + Map headerStyles = new HashMap(); + for (Object[] os : fields) + { + Excel excel = (Excel) os[1]; + String key = StringUtils.format("header_{}_{}", excel.headerColor(), excel.headerBackgroundColor()); + if (!headerStyles.containsKey(key)) + { + CellStyle style = wb.createCellStyle(); + style.cloneStyleFrom(styles.get("data")); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setFillForegroundColor(excel.headerBackgroundColor().index); + style.setFillPattern(FillPatternType.SOLID_FOREGROUND); + Font headerFont = wb.createFont(); + headerFont.setFontName("Arial"); + headerFont.setFontHeightInPoints((short) 10); + headerFont.setBold(true); + headerFont.setColor(excel.headerColor().index); + style.setFont(headerFont); + // 设置表格头单元格文本形式 + DataFormat dataFormat = wb.createDataFormat(); + style.setDataFormat(dataFormat.getFormat("@")); + headerStyles.put(key, style); + } + } + return headerStyles; + } + + /** + * 根据Excel注解创建表格列样式 + * + * @param wb 工作薄对象 + * @return 自定义样式列表 + */ + private Map annotationDataStyles(Workbook wb) + { + Map styles = new HashMap(); + for (Object[] os : fields) + { + Field field = (Field) os[0]; + Excel excel = (Excel) os[1]; + if (Collection.class.isAssignableFrom(field.getType())) + { + ParameterizedType pt = (ParameterizedType) field.getGenericType(); + Class subClass = (Class) pt.getActualTypeArguments()[0]; + List subFields = FieldUtils.getFieldsListWithAnnotation(subClass, Excel.class); + for (Field subField : subFields) + { + Excel subExcel = subField.getAnnotation(Excel.class); + annotationDataStyles(styles, subField, subExcel); + } + } + else + { + annotationDataStyles(styles, field, excel); + } + } + return styles; + } + + /** + * 根据Excel注解创建表格列样式 + * + * @param styles 自定义样式列表 + * @param field 属性列信息 + * @param excel 注解信息 + */ + public void annotationDataStyles(Map styles, Field field, Excel excel) + { + String key = StringUtils.format("data_{}_{}_{}_{}_{}", excel.align(), excel.color(), excel.backgroundColor(), excel.cellType(), excel.wrapText()); + if (!styles.containsKey(key)) + { + CellStyle style = wb.createCellStyle(); + style.setAlignment(excel.align()); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setBorderRight(BorderStyle.THIN); + style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderLeft(BorderStyle.THIN); + style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderTop(BorderStyle.THIN); + style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderBottom(BorderStyle.THIN); + style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setFillPattern(FillPatternType.SOLID_FOREGROUND); + style.setFillForegroundColor(excel.backgroundColor().getIndex()); + style.setWrapText(excel.wrapText()); + Font dataFont = wb.createFont(); + dataFont.setFontName("Arial"); + dataFont.setFontHeightInPoints((short) 10); + dataFont.setColor(excel.color().index); + style.setFont(dataFont); + if (ColumnType.TEXT == excel.cellType()) + { + DataFormat dataFormat = wb.createDataFormat(); + style.setDataFormat(dataFormat.getFormat("@")); + } + styles.put(key, style); + } + } + + /** + * 创建单元格 + */ + public Cell createHeadCell(Excel attr, Row row, int column) + { + // 创建列 + Cell cell = row.createCell(column); + // 写入列信息 + cell.setCellValue(attr.name()); + setDataValidation(attr, row, column); + cell.setCellStyle(styles.get(StringUtils.format("header_{}_{}", attr.headerColor(), attr.headerBackgroundColor()))); + if (isSubList()) + { + // 填充默认样式,防止合并单元格样式失效 + sheet.setDefaultColumnStyle(column, styles.get(StringUtils.format("data_{}_{}_{}_{}_{}", attr.align(), attr.color(), attr.backgroundColor(), attr.cellType(), attr.wrapText()))); + if (attr.needMerge()) + { + sheet.addMergedRegion(new CellRangeAddress(rownum - 1, rownum, column, column)); + } + } + return cell; + } + + /** + * 设置单元格信息 + * + * @param value 单元格值 + * @param attr 注解相关 + * @param cell 单元格信息 + */ + public void setCellVo(Object value, Excel attr, Cell cell) + { + if (ColumnType.STRING == attr.cellType() || ColumnType.TEXT == attr.cellType()) + { + String cellValue = Convert.toStr(value); + // 对于任何以表达式触发字符 =-+@开头的单元格,直接使用tab字符作为前缀,防止CSV注入。 + if (StringUtils.startsWithAny(cellValue, FORMULA_STR)) + { + cellValue = RegExUtils.replaceFirst(cellValue, FORMULA_REGEX_STR, "\t$0"); + } + if (value instanceof Collection && StringUtils.equals("[]", cellValue)) + { + cellValue = StringUtils.EMPTY; + } + cell.setCellValue(StringUtils.isNull(cellValue) ? attr.defaultValue() : cellValue + attr.suffix()); + } + else if (ColumnType.NUMERIC == attr.cellType()) + { + if (StringUtils.isNotNull(value)) + { + cell.setCellValue(StringUtils.contains(Convert.toStr(value), ".") ? Convert.toDouble(value) : Convert.toInt(value)); + } + } + else if (ColumnType.IMAGE == attr.cellType()) + { + ClientAnchor anchor = new XSSFClientAnchor(0, 0, 0, 0, (short) cell.getColumnIndex(), cell.getRow().getRowNum(), (short) (cell.getColumnIndex() + 1), cell.getRow().getRowNum() + 1); + String propertyValue = Convert.toStr(value); + if (StringUtils.isNotEmpty(propertyValue)) + { + List imagePaths = StringUtils.str2List(propertyValue, SEPARATOR); + for (String imagePath : imagePaths) + { + byte[] data = ImageUtils.getImage(imagePath); + getDrawingPatriarch(cell.getSheet()).createPicture(anchor, cell.getSheet().getWorkbook().addPicture(data, getImageType(data))); + } + } + } + } + + /** + * 获取画布 + */ + public static Drawing getDrawingPatriarch(Sheet sheet) + { + if (sheet.getDrawingPatriarch() == null) + { + sheet.createDrawingPatriarch(); + } + return sheet.getDrawingPatriarch(); + } + + /** + * 获取图片类型,设置图片插入类型 + */ + public int getImageType(byte[] value) + { + String type = FileTypeUtils.getFileExtendName(value); + if ("JPG".equalsIgnoreCase(type)) + { + return Workbook.PICTURE_TYPE_JPEG; + } + else if ("PNG".equalsIgnoreCase(type)) + { + return Workbook.PICTURE_TYPE_PNG; + } + return Workbook.PICTURE_TYPE_JPEG; + } + + /** + * 创建表格样式 + */ + public void setDataValidation(Excel attr, Row row, int column) + { + if (attr.name().indexOf("注:") >= 0) + { + sheet.setColumnWidth(column, 6000); + } + else + { + // 设置列宽 + sheet.setColumnWidth(column, (int) ((attr.width() + 0.72) * 256)); + } + if (StringUtils.isNotEmpty(attr.prompt()) || attr.combo().length > 0 || attr.comboReadDict()) + { + String[] comboArray = attr.combo(); + if (attr.comboReadDict()) + { + if (!sysDictMap.containsKey("combo_" + attr.dictType())) + { + String labels = DictUtils.getDictLabels(attr.dictType()); + sysDictMap.put("combo_" + attr.dictType(), labels); + } + String val = sysDictMap.get("combo_" + attr.dictType()); + comboArray = StringUtils.split(val, DictUtils.SEPARATOR); + } + if (comboArray.length > 15 || StringUtils.join(comboArray).length() > 255) + { + // 如果下拉数大于15或字符串长度大于255,则使用一个新sheet存储,避免生成的模板下拉值获取不到 + setXSSFValidationWithHidden(sheet, comboArray, attr.prompt(), 1, 100, column, column); + } + else + { + // 提示信息或只能选择不能输入的列内容. + setPromptOrValidation(sheet, comboArray, attr.prompt(), 1, 100, column, column); + } + } + } + + /** + * 添加单元格 + */ + @SuppressWarnings("deprecation") + public Cell addCell(Excel attr, Row row, T vo, Field field, int column) + { + Cell cell = null; + try + { + // 设置行高 + row.setHeight(maxHeight); + // 根据Excel中设置情况决定是否导出,有些情况需要保持为空,希望用户填写这一列. + if (attr.isExport()) + { + // 创建cell + cell = row.createCell(column); + if (isSubListValue(vo) && getListCellValue(vo) > 1 && attr.needMerge()) + { + if (subMergedLastRowNum >= subMergedFirstRowNum) + { + sheet.addMergedRegion(new CellRangeAddress(subMergedFirstRowNum, subMergedLastRowNum, column, column)); + } + } + cell.setCellStyle(styles.get(StringUtils.format("data_{}_{}_{}_{}_{}", attr.align(), attr.color(), attr.backgroundColor(), attr.cellType(), attr.wrapText()))); + + // 用于读取对象中的属性 + Object value = getTargetValue(vo, field, attr); + String dateFormat = attr.dateFormat(); + String readConverterExp = attr.readConverterExp(); + String separator = attr.separator(); + String dictType = attr.dictType(); + if (StringUtils.isNotEmpty(dateFormat) && StringUtils.isNotNull(value)) + { + cell.setCellStyle(createCellStyle(cell.getCellStyle(), dateFormat)); + cell.setCellValue(parseDateToStr(dateFormat, value)); + } + else if (StringUtils.isNotEmpty(readConverterExp) && StringUtils.isNotNull(value)) + { + cell.setCellValue(convertByExp(Convert.toStr(value), readConverterExp, separator)); + } + else if (StringUtils.isNotEmpty(dictType) && StringUtils.isNotNull(value)) + { + if (!sysDictMap.containsKey(dictType + value)) + { + String lable = convertDictByExp(Convert.toStr(value), dictType, separator); + sysDictMap.put(dictType + value, lable); + } + cell.setCellValue(sysDictMap.get(dictType + value)); + } + else if (value instanceof BigDecimal && -1 != attr.scale()) + { + cell.setCellValue((((BigDecimal) value).setScale(attr.scale(), attr.roundingMode())).doubleValue()); + } + else if (!attr.handler().equals(ExcelHandlerAdapter.class)) + { + cell.setCellValue(dataFormatHandlerAdapter(value, attr, cell)); + } + else + { + // 设置列类型 + setCellVo(value, attr, cell); + } + addStatisticsData(column, Convert.toStr(value), attr); + } + } + catch (Exception e) + { + log.error("导出Excel失败{}", e); + } + return cell; + } + + /** + * 使用自定义格式,同时避免样式污染 + * + * @param cellStyle 从此样式复制 + * @param format 格式匹配的字符串 + * @return 格式化后CellStyle对象 + */ + private CellStyle createCellStyle(CellStyle cellStyle, String format) + { + String key = cellStyle.getIndex() + "|" + format; + CellStyle cached = cellStyleCache.get(key); + if (cached != null) + { + return cached; + } + CellStyle style = wb.createCellStyle(); + style.cloneStyleFrom(cellStyle); + style.setDataFormat(wb.getCreationHelper().createDataFormat().getFormat(format)); + cellStyleCache.put(key, style); + return style; + } + + /** + * 设置 POI XSSFSheet 单元格提示或选择框 + * + * @param sheet 表单 + * @param textlist 下拉框显示的内容 + * @param promptContent 提示内容 + * @param firstRow 开始行 + * @param endRow 结束行 + * @param firstCol 开始列 + * @param endCol 结束列 + */ + public void setPromptOrValidation(Sheet sheet, String[] textlist, String promptContent, int firstRow, int endRow, + int firstCol, int endCol) + { + DataValidationHelper helper = sheet.getDataValidationHelper(); + DataValidationConstraint constraint = textlist.length > 0 ? helper.createExplicitListConstraint(textlist) : helper.createCustomConstraint("DD1"); + CellRangeAddressList regions = new CellRangeAddressList(firstRow, endRow, firstCol, endCol); + DataValidation dataValidation = helper.createValidation(constraint, regions); + if (StringUtils.isNotEmpty(promptContent)) + { + // 如果设置了提示信息则鼠标放上去提示 + dataValidation.createPromptBox("", promptContent); + dataValidation.setShowPromptBox(true); + } + // 处理Excel兼容性问题 + if (dataValidation instanceof XSSFDataValidation) + { + dataValidation.setSuppressDropDownArrow(true); + dataValidation.setShowErrorBox(true); + } + else + { + dataValidation.setSuppressDropDownArrow(false); + } + sheet.addValidationData(dataValidation); + } + + /** + * 设置某些列的值只能输入预制的数据,显示下拉框(兼容超出一定数量的下拉框). + * + * @param sheet 要设置的sheet. + * @param textlist 下拉框显示的内容 + * @param promptContent 提示内容 + * @param firstRow 开始行 + * @param endRow 结束行 + * @param firstCol 开始列 + * @param endCol 结束列 + */ + public void setXSSFValidationWithHidden(Sheet sheet, String[] textlist, String promptContent, int firstRow, int endRow, int firstCol, int endCol) + { + String hideSheetName = "combo_" + firstCol + "_" + endCol; + Sheet hideSheet = null; + String hideSheetDataName = hideSheetName + "_data"; + Name name = wb.getName(hideSheetDataName); + if (name != null) + { + // 名称已存在,尝试从名称的引用中找到sheet名称 + String refersToFormula = name.getRefersToFormula(); + if (StringUtils.isNotEmpty(refersToFormula) && refersToFormula.contains("!")) + { + String sheetNameFromFormula = refersToFormula.substring(0, refersToFormula.indexOf("!")); + hideSheet = wb.getSheet(sheetNameFromFormula); + } + } + + if (hideSheet == null) + { + hideSheet = wb.createSheet(hideSheetName); // 用于存储 下拉菜单数据 + for (int i = 0; i < textlist.length; i++) + { + hideSheet.createRow(i).createCell(0).setCellValue(textlist[i]); + } + // 创建名称,可被其他单元格引用 + name = wb.createName(); + name.setNameName(hideSheetDataName); + name.setRefersToFormula(hideSheetName + "!$A$1:$A$" + textlist.length); + } + + DataValidationHelper helper = sheet.getDataValidationHelper(); + // 加载下拉列表内容 + DataValidationConstraint constraint = helper.createFormulaListConstraint(hideSheetDataName); + // 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列 + CellRangeAddressList regions = new CellRangeAddressList(firstRow, endRow, firstCol, endCol); + // 数据有效性对象 + DataValidation dataValidation = helper.createValidation(constraint, regions); + if (StringUtils.isNotEmpty(promptContent)) + { + // 如果设置了提示信息则鼠标放上去提示 + dataValidation.createPromptBox("", promptContent); + dataValidation.setShowPromptBox(true); + } + // 处理Excel兼容性问题 + if (dataValidation instanceof XSSFDataValidation) + { + dataValidation.setSuppressDropDownArrow(true); + dataValidation.setShowErrorBox(true); + } + else + { + dataValidation.setSuppressDropDownArrow(false); + } + + sheet.addValidationData(dataValidation); + // 设置hiddenSheet隐藏 + wb.setSheetHidden(wb.getSheetIndex(hideSheet), true); + } + + /** + * 解析导出值 0=男,1=女,2=未知 + * + * @param propertyValue 参数值 + * @param converterExp 翻译注解 + * @param separator 分隔符 + * @return 解析后值 + */ + public static String convertByExp(String propertyValue, String converterExp, String separator) + { + StringBuilder propertyString = new StringBuilder(); + String[] convertSource = converterExp.split(SEPARATOR); + for (String item : convertSource) + { + String[] itemArray = item.split("="); + if (StringUtils.containsAny(propertyValue, separator)) + { + for (String value : propertyValue.split(separator)) + { + if (itemArray[0].equals(value)) + { + propertyString.append(itemArray[1] + separator); + break; + } + } + } + else + { + if (itemArray[0].equals(propertyValue)) + { + return itemArray[1]; + } + } + } + return StringUtils.stripEnd(propertyString.toString(), separator); + } + + /** + * 反向解析值 男=0,女=1,未知=2 + * + * @param propertyValue 参数值 + * @param converterExp 翻译注解 + * @param separator 分隔符 + * @return 解析后值 + */ + public static String reverseByExp(String propertyValue, String converterExp, String separator) + { + StringBuilder propertyString = new StringBuilder(); + String[] convertSource = converterExp.split(SEPARATOR); + for (String item : convertSource) + { + String[] itemArray = item.split("="); + if (StringUtils.containsAny(propertyValue, separator)) + { + for (String value : propertyValue.split(separator)) + { + if (itemArray[1].equals(value)) + { + propertyString.append(itemArray[0] + separator); + break; + } + } + } + else + { + if (itemArray[1].equals(propertyValue)) + { + return itemArray[0]; + } + } + } + return StringUtils.stripEnd(propertyString.toString(), separator); + } + + /** + * 解析字典值 + * + * @param dictValue 字典值 + * @param dictType 字典类型 + * @param separator 分隔符 + * @return 字典标签 + */ + public static String convertDictByExp(String dictValue, String dictType, String separator) + { + return DictUtils.getDictLabel(dictType, dictValue, separator); + } + + /** + * 反向解析值字典值 + * + * @param dictLabel 字典标签 + * @param dictType 字典类型 + * @param separator 分隔符 + * @return 字典值 + */ + public static String reverseDictByExp(String dictLabel, String dictType, String separator) + { + return DictUtils.getDictValue(dictType, dictLabel, separator); + } + + /** + * 数据处理器 + * + * @param value 数据值 + * @param excel 数据注解 + * @return + */ + public String dataFormatHandlerAdapter(Object value, Excel excel, Cell cell) + { + try + { + Object instance = excel.handler().getDeclaredConstructor().newInstance(); + Method formatMethod = excel.handler().getMethod("format", new Class[] { Object.class, String[].class, Cell.class, Workbook.class }); + value = formatMethod.invoke(instance, value, excel.args(), cell, this.wb); + } + catch (Exception e) + { + log.error("不能格式化数据 " + excel.handler(), e.getMessage()); + } + return Convert.toStr(value); + } + + /** + * 合计统计信息 + */ + private void addStatisticsData(Integer index, String text, Excel entity) + { + if (entity != null && entity.isStatistics()) + { + Double temp = 0D; + if (!statistics.containsKey(index)) + { + statistics.put(index, temp); + } + try + { + temp = Double.valueOf(text); + } + catch (NumberFormatException e) + { + } + statistics.put(index, statistics.get(index) + temp); + } + } + + /** + * 创建统计行 + */ + public void addStatisticsRow() + { + if (statistics.size() > 0) + { + Row row = sheet.createRow(sheet.getLastRowNum() + 1); + Set keys = statistics.keySet(); + Cell cell = row.createCell(0); + cell.setCellStyle(styles.get("total")); + cell.setCellValue("合计"); + + for (Integer key : keys) + { + cell = row.createCell(key); + cell.setCellStyle(styles.get("total")); + cell.setCellValue(statistics.get(key)); + } + statistics.clear(); + } + } + + /** + * 编码文件名 + */ + public String encodingFilename(String filename) + { + return UUID.randomUUID() + "_" + filename + ".xlsx"; + } + + /** + * 获取下载路径 + * + * @param filename 文件名称 + */ + public String getAbsoluteFile(String filename) + { + String downloadPath = RuoYiConfig.getDownloadPath() + filename; + File desc = new File(downloadPath); + if (!desc.getParentFile().exists()) + { + desc.getParentFile().mkdirs(); + } + return downloadPath; + } + + /** + * 获取bean中的属性值 + * + * @param vo 实体对象 + * @param field 字段 + * @param excel 注解 + * @return 最终的属性值 + * @throws Exception + */ + private Object getTargetValue(T vo, Field field, Excel excel) throws Exception + { + field.setAccessible(true); + Object o = field.get(vo); + if (StringUtils.isNotEmpty(excel.targetAttr())) + { + String target = excel.targetAttr(); + if (target.contains(".")) + { + String[] targets = target.split("[.]"); + for (String name : targets) + { + o = getValue(o, name); + } + } + else + { + o = getValue(o, target); + } + } + return o; + } + + /** + * 以类的属性的get方法方法形式获取值 + * + * @param o + * @param name + * @return value + * @throws Exception + */ + private Object getValue(Object o, String name) throws Exception + { + if (StringUtils.isNotNull(o) && StringUtils.isNotEmpty(name)) + { + Class clazz = o.getClass(); + Field field = clazz.getDeclaredField(name); + field.setAccessible(true); + o = field.get(o); + } + return o; + } + + /** + * 得到所有定义字段 + */ + private void createExcelField() + { + this.fields = getFields(); + this.fields = this.fields.stream().sorted(Comparator.comparing(objects -> ((Excel) objects[1]).sort())).collect(Collectors.toList()); + this.maxHeight = getRowHeight(); + } + + /** + * 获取字段注解信息 + */ + public List getFields() + { + List fields = new ArrayList(); + List tempFields = new ArrayList<>(); + subFieldsMap = new HashMap<>(); + subMethods = new HashMap<>(); + tempFields.addAll(Arrays.asList(clazz.getSuperclass().getDeclaredFields())); + tempFields.addAll(Arrays.asList(clazz.getDeclaredFields())); + if (StringUtils.isNotEmpty(includeFields)) + { + for (Field field : tempFields) + { + if (ArrayUtils.contains(this.includeFields, field.getName()) || field.isAnnotationPresent(Excels.class)) + { + addField(fields, field); + } + } + } + else if (StringUtils.isNotEmpty(excludeFields)) + { + for (Field field : tempFields) + { + if (!ArrayUtils.contains(this.excludeFields, field.getName())) + { + addField(fields, field); + } + } + } + else + { + for (Field field : tempFields) + { + addField(fields, field); + } + } + return fields; + } + + /** + * 添加字段信息 + */ + public void addField(List fields, Field field) + { + // 单注解 + if (field.isAnnotationPresent(Excel.class)) + { + Excel attr = field.getAnnotation(Excel.class); + if (attr != null && (attr.type() == Type.ALL || attr.type() == type)) + { + fields.add(new Object[] { field, attr }); + } + if (Collection.class.isAssignableFrom(field.getType())) + { + String fieldName = field.getName(); + subMethods.put(fieldName, getSubMethod(fieldName, clazz)); + ParameterizedType pt = (ParameterizedType) field.getGenericType(); + Class subClass = (Class) pt.getActualTypeArguments()[0]; + subFieldsMap.put(fieldName, FieldUtils.getFieldsListWithAnnotation(subClass, Excel.class)); + } + } + + // 多注解 + if (field.isAnnotationPresent(Excels.class)) + { + Excels attrs = field.getAnnotation(Excels.class); + Excel[] excels = attrs.value(); + for (Excel attr : excels) + { + if (StringUtils.isNotEmpty(includeFields)) + { + if (ArrayUtils.contains(this.includeFields, field.getName() + "." + attr.targetAttr()) + && (attr != null && (attr.type() == Type.ALL || attr.type() == type))) + { + fields.add(new Object[] { field, attr }); + } + } + else + { + if (!ArrayUtils.contains(this.excludeFields, field.getName() + "." + attr.targetAttr()) + && (attr != null && (attr.type() == Type.ALL || attr.type() == type))) + { + fields.add(new Object[] { field, attr }); + } + } + } + } + } + + /** + * 根据注解获取最大行高 + */ + public short getRowHeight() + { + double maxHeight = 0; + for (Object[] os : this.fields) + { + Excel excel = (Excel) os[1]; + maxHeight = Math.max(maxHeight, excel.height()); + } + return (short) (maxHeight * 20); + } + + /** + * 创建一个工作簿 + */ + public void createWorkbook() + { + this.wb = new SXSSFWorkbook(500); + this.sheet = wb.createSheet(); + wb.setSheetName(0, sheetName); + this.styles = createStyles(wb); + } + + /** + * 创建工作表 + * + * @param sheetNo sheet数量 + * @param index 序号 + */ + public void createSheet(int sheetNo, int index) + { + // 设置工作表的名称. + if (sheetNo > 1 && index > 0) + { + this.sheet = wb.createSheet(); + this.createTitle(); + int actualIndex = wb.getSheetIndex(this.sheet); + wb.setSheetName(actualIndex, sheetName + index); + } + } + + /** + * 获取单元格值 + * + * @param row 获取的行 + * @param column 获取单元格列号 + * @return 单元格值 + */ + public Object getCellValue(Row row, int column) + { + if (row == null) + { + return row; + } + Object val = ""; + try + { + Cell cell = row.getCell(column); + if (StringUtils.isNotNull(cell)) + { + if (cell.getCellType() == CellType.NUMERIC || cell.getCellType() == CellType.FORMULA) + { + val = cell.getNumericCellValue(); + if (DateUtil.isCellDateFormatted(cell)) + { + val = DateUtil.getJavaDate((Double) val); // POI Excel 日期格式转换 + } + else + { + if ((Double) val % 1 != 0) + { + val = new BigDecimal(val.toString()); + } + else + { + val = new DecimalFormat("0").format(val); + } + } + } + else if (cell.getCellType() == CellType.STRING) + { + val = cell.getStringCellValue(); + } + else if (cell.getCellType() == CellType.BOOLEAN) + { + val = cell.getBooleanCellValue(); + } + else if (cell.getCellType() == CellType.ERROR) + { + val = cell.getErrorCellValue(); + } + + } + } + catch (Exception e) + { + return val; + } + return val; + } + + /** + * 判断是否是空行 + * + * @param row 判断的行 + * @return + */ + private boolean isRowEmpty(Row row) + { + if (row == null) + { + return true; + } + for (int i = row.getFirstCellNum(); i < row.getLastCellNum(); i++) + { + Cell cell = row.getCell(i); + if (cell != null && cell.getCellType() != CellType.BLANK) + { + return false; + } + } + return true; + } + + /** + * 获取Excel2003图片 + * + * @param sheet 当前sheet对象 + * @param workbook 工作簿对象 + * @return Map key:图片单元格索引(1_1)String,value:图片流PictureData + */ + public static Map> getSheetPictures03(HSSFSheet sheet, HSSFWorkbook workbook) + { + Map> sheetIndexPicMap = new HashMap<>(); + List pictures = workbook.getAllPictures(); + if (!pictures.isEmpty() && sheet.getDrawingPatriarch() != null) + { + for (HSSFShape shape : sheet.getDrawingPatriarch().getChildren()) + { + if (shape instanceof HSSFPicture) + { + HSSFPicture pic = (HSSFPicture) shape; + HSSFClientAnchor anchor = (HSSFClientAnchor) pic.getAnchor(); + String picIndex = anchor.getRow1() + "_" + anchor.getCol1(); + sheetIndexPicMap.computeIfAbsent(picIndex, k -> new ArrayList<>()).add(pic.getPictureData()); + } + } + } + return sheetIndexPicMap; + } + + /** + * 获取Excel2007图片 + * + * @param sheet 当前sheet对象 + * @param workbook 工作簿对象 + * @return Map key:图片单元格索引(1_1)String,value:图片流PictureData + */ + public static Map> getSheetPictures07(XSSFSheet sheet, XSSFWorkbook workbook) + { + Map> sheetIndexPicMap = new HashMap<>(); + for (POIXMLDocumentPart dr : sheet.getRelations()) + { + if (dr instanceof XSSFDrawing) + { + XSSFDrawing drawing = (XSSFDrawing) dr; + for (XSSFShape shape : drawing.getShapes()) + { + if (shape instanceof XSSFPicture) + { + XSSFPicture pic = (XSSFPicture) shape; + XSSFClientAnchor anchor = pic.getPreferredSize(); + CTMarker ctMarker = anchor.getFrom(); + String picIndex = ctMarker.getRow() + "_" + ctMarker.getCol(); + sheetIndexPicMap.computeIfAbsent(picIndex, k -> new ArrayList<>()).add(pic.getPictureData()); + } + } + } + } + return sheetIndexPicMap; + } + + /** + * 格式化不同类型的日期对象 + * + * @param dateFormat 日期格式 + * @param val 被格式化的日期对象 + * @return 格式化后的日期字符 + */ + public String parseDateToStr(String dateFormat, Object val) + { + if (val == null) + { + return ""; + } + String str; + if (val instanceof Date) + { + str = DateUtils.parseDateToStr(dateFormat, (Date) val); + } + else if (val instanceof LocalDateTime) + { + str = DateUtils.parseDateToStr(dateFormat, DateUtils.toDate((LocalDateTime) val)); + } + else if (val instanceof LocalDate) + { + str = DateUtils.parseDateToStr(dateFormat, DateUtils.toDate((LocalDate) val)); + } + else + { + str = val.toString(); + } + return str; + } + + /** + * 是否有对象的子列表 + */ + public boolean isSubList() + { + return !StringUtils.isEmpty(subFieldsMap); + } + + /** + * 是否有对象的子列表,集合不为空 + */ + public boolean isSubListValue(T vo) + { + return !StringUtils.isEmpty(subFieldsMap) && getListCellValue(vo) > 0; + } + + /** + * 获取集合的值 + */ + public int getListCellValue(Object obj) + { + Collection value; + int max = 0; + try + { + for (String s : subMethods.keySet()) + { + value = (Collection) subMethods.get(s).invoke(obj); + if (value.size() > max) + { + max = value.size(); + } + } + } + catch (Exception e) + { + return 0; + } + return max; + } + + /** + * 获取对象的子列表方法 + * + * @param name 名称 + * @param pojoClass 类对象 + * @return 子列表方法 + */ + public Method getSubMethod(String name, Class pojoClass) + { + StringBuffer getMethodName = new StringBuffer("get"); + getMethodName.append(name.substring(0, 1).toUpperCase()); + getMethodName.append(name.substring(1)); + Method method = null; + try + { + method = pojoClass.getMethod(getMethodName.toString(), new Class[] {}); + } + catch (Exception e) + { + log.error("获取对象异常{}", e.getMessage()); + } + return method; + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/reflect/ReflectUtils.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/reflect/ReflectUtils.java new file mode 100644 index 0000000..628b4c2 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/reflect/ReflectUtils.java @@ -0,0 +1,412 @@ +package com.ruoyi.common.utils.reflect; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Date; +import org.apache.commons.lang3.Validate; +import org.apache.poi.ss.usermodel.DateUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; + +/** + * 反射工具类. 提供调用getter/setter方法, 访问私有变量, 调用私有方法, 获取泛型类型Class, 被AOP过的真实类等工具函数. + * + * @author ruoyi + */ +@SuppressWarnings("rawtypes") +public class ReflectUtils +{ + private static final String SETTER_PREFIX = "set"; + + private static final String GETTER_PREFIX = "get"; + + private static final String CGLIB_CLASS_SEPARATOR = "$$"; + + private static Logger logger = LoggerFactory.getLogger(ReflectUtils.class); + + /** + * 调用Getter方法. + * 支持多级,如:对象名.对象名.方法 + */ + @SuppressWarnings("unchecked") + public static E invokeGetter(Object obj, String propertyName) + { + Object object = obj; + for (String name : StringUtils.split(propertyName, ".")) + { + String getterMethodName = GETTER_PREFIX + StringUtils.capitalize(name); + object = invokeMethod(object, getterMethodName, new Class[] {}, new Object[] {}); + } + return (E) object; + } + + /** + * 调用Setter方法, 仅匹配方法名。 + * 支持多级,如:对象名.对象名.方法 + */ + public static void invokeSetter(Object obj, String propertyName, E value) + { + Object object = obj; + String[] names = StringUtils.split(propertyName, "."); + for (int i = 0; i < names.length; i++) + { + if (i < names.length - 1) + { + String getterMethodName = GETTER_PREFIX + StringUtils.capitalize(names[i]); + object = invokeMethod(object, getterMethodName, new Class[] {}, new Object[] {}); + } + else + { + String setterMethodName = SETTER_PREFIX + StringUtils.capitalize(names[i]); + invokeMethodByName(object, setterMethodName, new Object[] { value }); + } + } + } + + /** + * 直接读取对象属性值, 无视private/protected修饰符, 不经过getter函数. + */ + @SuppressWarnings("unchecked") + public static E getFieldValue(final Object obj, final String fieldName) + { + Field field = getAccessibleField(obj, fieldName); + if (field == null) + { + logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + fieldName + "] 字段 "); + return null; + } + E result = null; + try + { + result = (E) field.get(obj); + } + catch (IllegalAccessException e) + { + logger.error("不可能抛出的异常{}", e.getMessage()); + } + return result; + } + + /** + * 直接设置对象属性值, 无视private/protected修饰符, 不经过setter函数. + */ + public static void setFieldValue(final Object obj, final String fieldName, final E value) + { + Field field = getAccessibleField(obj, fieldName); + if (field == null) + { + // throw new IllegalArgumentException("在 [" + obj.getClass() + "] 中,没有找到 [" + fieldName + "] 字段 "); + logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + fieldName + "] 字段 "); + return; + } + try + { + field.set(obj, value); + } + catch (IllegalAccessException e) + { + logger.error("不可能抛出的异常: {}", e.getMessage()); + } + } + + /** + * 直接调用对象方法, 无视private/protected修饰符. + * 用于一次性调用的情况,否则应使用getAccessibleMethod()函数获得Method后反复调用. + * 同时匹配方法名+参数类型, + */ + @SuppressWarnings("unchecked") + public static E invokeMethod(final Object obj, final String methodName, final Class[] parameterTypes, + final Object[] args) + { + if (obj == null || methodName == null) + { + return null; + } + Method method = getAccessibleMethod(obj, methodName, parameterTypes); + if (method == null) + { + logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + methodName + "] 方法 "); + return null; + } + try + { + return (E) method.invoke(obj, args); + } + catch (Exception e) + { + String msg = "method: " + method + ", obj: " + obj + ", args: " + args + ""; + throw convertReflectionExceptionToUnchecked(msg, e); + } + } + + /** + * 直接调用对象方法, 无视private/protected修饰符, + * 用于一次性调用的情况,否则应使用getAccessibleMethodByName()函数获得Method后反复调用. + * 只匹配函数名,如果有多个同名函数调用第一个。 + */ + @SuppressWarnings("unchecked") + public static E invokeMethodByName(final Object obj, final String methodName, final Object[] args) + { + Method method = getAccessibleMethodByName(obj, methodName, args.length); + if (method == null) + { + // 如果为空不报错,直接返回空。 + logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + methodName + "] 方法 "); + return null; + } + try + { + // 类型转换(将参数数据类型转换为目标方法参数类型) + Class[] cs = method.getParameterTypes(); + for (int i = 0; i < cs.length; i++) + { + if (args[i] != null && !args[i].getClass().equals(cs[i])) + { + if (cs[i] == String.class) + { + args[i] = Convert.toStr(args[i]); + if (StringUtils.endsWith((String) args[i], ".0")) + { + args[i] = StringUtils.substringBefore((String) args[i], ".0"); + } + } + else if (cs[i] == Integer.class) + { + args[i] = Convert.toInt(args[i]); + } + else if (cs[i] == Long.class) + { + args[i] = Convert.toLong(args[i]); + } + else if (cs[i] == Double.class) + { + args[i] = Convert.toDouble(args[i]); + } + else if (cs[i] == Float.class) + { + args[i] = Convert.toFloat(args[i]); + } + else if (cs[i] == Date.class) + { + if (args[i] instanceof String) + { + args[i] = DateUtils.parseDate(args[i]); + } + else + { + args[i] = DateUtil.getJavaDate((Double) args[i]); + } + } + else if (cs[i] == boolean.class || cs[i] == Boolean.class) + { + args[i] = Convert.toBool(args[i]); + } + } + } + return (E) method.invoke(obj, args); + } + catch (Exception e) + { + String msg = "method: " + method + ", obj: " + obj + ", args: " + args + ""; + throw convertReflectionExceptionToUnchecked(msg, e); + } + } + + /** + * 循环向上转型, 获取对象的DeclaredField, 并强制设置为可访问. + * 如向上转型到Object仍无法找到, 返回null. + */ + public static Field getAccessibleField(final Object obj, final String fieldName) + { + // 为空不报错。直接返回 null + if (obj == null) + { + return null; + } + Validate.notBlank(fieldName, "fieldName can't be blank"); + for (Class superClass = obj.getClass(); superClass != Object.class; superClass = superClass.getSuperclass()) + { + try + { + Field field = superClass.getDeclaredField(fieldName); + makeAccessible(field); + return field; + } + catch (NoSuchFieldException e) + { + continue; + } + } + return null; + } + + /** + * 循环向上转型, 获取对象的DeclaredMethod,并强制设置为可访问. + * 如向上转型到Object仍无法找到, 返回null. + * 匹配函数名+参数类型。 + * 用于方法需要被多次调用的情况. 先使用本函数先取得Method,然后调用Method.invoke(Object obj, Object... args) + */ + public static Method getAccessibleMethod(final Object obj, final String methodName, + final Class... parameterTypes) + { + // 为空不报错。直接返回 null + if (obj == null) + { + return null; + } + Validate.notBlank(methodName, "methodName can't be blank"); + for (Class searchType = obj.getClass(); searchType != Object.class; searchType = searchType.getSuperclass()) + { + try + { + Method method = searchType.getDeclaredMethod(methodName, parameterTypes); + makeAccessible(method); + return method; + } + catch (NoSuchMethodException e) + { + continue; + } + } + return null; + } + + /** + * 循环向上转型, 获取对象的DeclaredMethod,并强制设置为可访问. + * 如向上转型到Object仍无法找到, 返回null. + * 只匹配函数名。 + * 用于方法需要被多次调用的情况. 先使用本函数先取得Method,然后调用Method.invoke(Object obj, Object... args) + */ + public static Method getAccessibleMethodByName(final Object obj, final String methodName, int argsNum) + { + // 为空不报错。直接返回 null + if (obj == null) + { + return null; + } + Validate.notBlank(methodName, "methodName can't be blank"); + for (Class searchType = obj.getClass(); searchType != Object.class; searchType = searchType.getSuperclass()) + { + Method[] methods = searchType.getDeclaredMethods(); + for (Method method : methods) + { + if (method.getName().equals(methodName) && method.getParameterTypes().length == argsNum) + { + makeAccessible(method); + return method; + } + } + } + return null; + } + + /** + * 改变private/protected的方法为public,尽量不调用实际改动的语句,避免JDK的SecurityManager抱怨。 + */ + @SuppressWarnings("deprecation") + public static void makeAccessible(Method method) + { + if ((!Modifier.isPublic(method.getModifiers()) || !Modifier.isPublic(method.getDeclaringClass().getModifiers())) + && !method.isAccessible()) + { + method.setAccessible(true); + } + } + + /** + * 改变private/protected的成员变量为public,尽量不调用实际改动的语句,避免JDK的SecurityManager抱怨。 + */ + @SuppressWarnings("deprecation") + public static void makeAccessible(Field field) + { + if ((!Modifier.isPublic(field.getModifiers()) || !Modifier.isPublic(field.getDeclaringClass().getModifiers()) + || Modifier.isFinal(field.getModifiers())) && !field.isAccessible()) + { + field.setAccessible(true); + } + } + + /** + * 通过反射, 获得Class定义中声明的泛型参数的类型, 注意泛型必须定义在父类处 + * 如无法找到, 返回Object.class. + */ + @SuppressWarnings("unchecked") + public static Class getClassGenricType(final Class clazz) + { + return getClassGenricType(clazz, 0); + } + + /** + * 通过反射, 获得Class定义中声明的父类的泛型参数的类型. + * 如无法找到, 返回Object.class. + */ + public static Class getClassGenricType(final Class clazz, final int index) + { + Type genType = clazz.getGenericSuperclass(); + + if (!(genType instanceof ParameterizedType)) + { + logger.debug(clazz.getSimpleName() + "'s superclass not ParameterizedType"); + return Object.class; + } + + Type[] params = ((ParameterizedType) genType).getActualTypeArguments(); + + if (index >= params.length || index < 0) + { + logger.debug("Index: " + index + ", Size of " + clazz.getSimpleName() + "'s Parameterized Type: " + + params.length); + return Object.class; + } + if (!(params[index] instanceof Class)) + { + logger.debug(clazz.getSimpleName() + " not set the actual class on superclass generic parameter"); + return Object.class; + } + + return (Class) params[index]; + } + + public static Class getUserClass(Object instance) + { + if (instance == null) + { + throw new RuntimeException("Instance must not be null"); + } + Class clazz = instance.getClass(); + if (clazz != null && clazz.getName().contains(CGLIB_CLASS_SEPARATOR)) + { + Class superClass = clazz.getSuperclass(); + if (superClass != null && !Object.class.equals(superClass)) + { + return superClass; + } + } + return clazz; + + } + + /** + * 将反射时的checked exception转换为unchecked exception. + */ + public static RuntimeException convertReflectionExceptionToUnchecked(String msg, Exception e) + { + if (e instanceof IllegalAccessException || e instanceof IllegalArgumentException + || e instanceof NoSuchMethodException) + { + return new IllegalArgumentException(msg, e); + } + else if (e instanceof InvocationTargetException) + { + return new RuntimeException(msg, ((InvocationTargetException) e).getTargetException()); + } + return new RuntimeException(msg, e); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/Base64.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/Base64.java new file mode 100644 index 0000000..ca1cd92 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/Base64.java @@ -0,0 +1,291 @@ +package com.ruoyi.common.utils.sign; + +/** + * Base64工具类 + * + * @author ruoyi + */ +public final class Base64 +{ + static private final int BASELENGTH = 128; + static private final int LOOKUPLENGTH = 64; + static private final int TWENTYFOURBITGROUP = 24; + static private final int EIGHTBIT = 8; + static private final int SIXTEENBIT = 16; + static private final int FOURBYTE = 4; + static private final int SIGN = -128; + static private final char PAD = '='; + static final private byte[] base64Alphabet = new byte[BASELENGTH]; + static final private char[] lookUpBase64Alphabet = new char[LOOKUPLENGTH]; + + static + { + for (int i = 0; i < BASELENGTH; ++i) + { + base64Alphabet[i] = -1; + } + for (int i = 'Z'; i >= 'A'; i--) + { + base64Alphabet[i] = (byte) (i - 'A'); + } + for (int i = 'z'; i >= 'a'; i--) + { + base64Alphabet[i] = (byte) (i - 'a' + 26); + } + + for (int i = '9'; i >= '0'; i--) + { + base64Alphabet[i] = (byte) (i - '0' + 52); + } + + base64Alphabet['+'] = 62; + base64Alphabet['/'] = 63; + + for (int i = 0; i <= 25; i++) + { + lookUpBase64Alphabet[i] = (char) ('A' + i); + } + + for (int i = 26, j = 0; i <= 51; i++, j++) + { + lookUpBase64Alphabet[i] = (char) ('a' + j); + } + + for (int i = 52, j = 0; i <= 61; i++, j++) + { + lookUpBase64Alphabet[i] = (char) ('0' + j); + } + lookUpBase64Alphabet[62] = (char) '+'; + lookUpBase64Alphabet[63] = (char) '/'; + } + + private static boolean isWhiteSpace(char octect) + { + return (octect == 0x20 || octect == 0xd || octect == 0xa || octect == 0x9); + } + + private static boolean isPad(char octect) + { + return (octect == PAD); + } + + private static boolean isData(char octect) + { + return (octect < BASELENGTH && base64Alphabet[octect] != -1); + } + + /** + * Encodes hex octects into Base64 + * + * @param binaryData Array containing binaryData + * @return Encoded Base64 array + */ + public static String encode(byte[] binaryData) + { + if (binaryData == null) + { + return null; + } + + int lengthDataBits = binaryData.length * EIGHTBIT; + if (lengthDataBits == 0) + { + return ""; + } + + int fewerThan24bits = lengthDataBits % TWENTYFOURBITGROUP; + int numberTriplets = lengthDataBits / TWENTYFOURBITGROUP; + int numberQuartet = fewerThan24bits != 0 ? numberTriplets + 1 : numberTriplets; + char encodedData[] = null; + + encodedData = new char[numberQuartet * 4]; + + byte k = 0, l = 0, b1 = 0, b2 = 0, b3 = 0; + + int encodedIndex = 0; + int dataIndex = 0; + + for (int i = 0; i < numberTriplets; i++) + { + b1 = binaryData[dataIndex++]; + b2 = binaryData[dataIndex++]; + b3 = binaryData[dataIndex++]; + + l = (byte) (b2 & 0x0f); + k = (byte) (b1 & 0x03); + + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0); + byte val3 = ((b3 & SIGN) == 0) ? (byte) (b3 >> 6) : (byte) ((b3) >> 6 ^ 0xfc); + + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[(l << 2) | val3]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[b3 & 0x3f]; + } + + // form integral number of 6-bit groups + if (fewerThan24bits == EIGHTBIT) + { + b1 = binaryData[dataIndex]; + k = (byte) (b1 & 0x03); + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[k << 4]; + encodedData[encodedIndex++] = PAD; + encodedData[encodedIndex++] = PAD; + } + else if (fewerThan24bits == SIXTEENBIT) + { + b1 = binaryData[dataIndex]; + b2 = binaryData[dataIndex + 1]; + l = (byte) (b2 & 0x0f); + k = (byte) (b1 & 0x03); + + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0); + + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[l << 2]; + encodedData[encodedIndex++] = PAD; + } + return new String(encodedData); + } + + /** + * Decodes Base64 data into octects + * + * @param encoded string containing Base64 data + * @return Array containind decoded data. + */ + public static byte[] decode(String encoded) + { + if (encoded == null) + { + return null; + } + + char[] base64Data = encoded.toCharArray(); + // remove white spaces + int len = removeWhiteSpace(base64Data); + + if (len % FOURBYTE != 0) + { + return null;// should be divisible by four + } + + int numberQuadruple = (len / FOURBYTE); + + if (numberQuadruple == 0) + { + return new byte[0]; + } + + byte decodedData[] = null; + byte b1 = 0, b2 = 0, b3 = 0, b4 = 0; + char d1 = 0, d2 = 0, d3 = 0, d4 = 0; + + int i = 0; + int encodedIndex = 0; + int dataIndex = 0; + decodedData = new byte[(numberQuadruple) * 3]; + + for (; i < numberQuadruple - 1; i++) + { + + if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++])) + || !isData((d3 = base64Data[dataIndex++])) || !isData((d4 = base64Data[dataIndex++]))) + { + return null; + } // if found "no data" just return null + + b1 = base64Alphabet[d1]; + b2 = base64Alphabet[d2]; + b3 = base64Alphabet[d3]; + b4 = base64Alphabet[d4]; + + decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + decodedData[encodedIndex++] = (byte) (b3 << 6 | b4); + } + + if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++]))) + { + return null;// if found "no data" just return null + } + + b1 = base64Alphabet[d1]; + b2 = base64Alphabet[d2]; + + d3 = base64Data[dataIndex++]; + d4 = base64Data[dataIndex++]; + if (!isData((d3)) || !isData((d4))) + {// Check if they are PAD characters + if (isPad(d3) && isPad(d4)) + { + if ((b2 & 0xf) != 0)// last 4 bits should be zero + { + return null; + } + byte[] tmp = new byte[i * 3 + 1]; + System.arraycopy(decodedData, 0, tmp, 0, i * 3); + tmp[encodedIndex] = (byte) (b1 << 2 | b2 >> 4); + return tmp; + } + else if (!isPad(d3) && isPad(d4)) + { + b3 = base64Alphabet[d3]; + if ((b3 & 0x3) != 0)// last 2 bits should be zero + { + return null; + } + byte[] tmp = new byte[i * 3 + 2]; + System.arraycopy(decodedData, 0, tmp, 0, i * 3); + tmp[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + tmp[encodedIndex] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + return tmp; + } + else + { + return null; + } + } + else + { // No PAD e.g 3cQl + b3 = base64Alphabet[d3]; + b4 = base64Alphabet[d4]; + decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + decodedData[encodedIndex++] = (byte) (b3 << 6 | b4); + + } + return decodedData; + } + + /** + * remove WhiteSpace from MIME containing encoded Base64 data. + * + * @param data the byte array of base64 data (with WS) + * @return the new length + */ + private static int removeWhiteSpace(char[] data) + { + if (data == null) + { + return 0; + } + + // count characters that's not whitespace + int newSize = 0; + int len = data.length; + for (int i = 0; i < len; i++) + { + if (!isWhiteSpace(data[i])) + { + data[newSize++] = data[i]; + } + } + return newSize; + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/Md5Utils.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/Md5Utils.java new file mode 100644 index 0000000..c1c58db --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/Md5Utils.java @@ -0,0 +1,67 @@ +package com.ruoyi.common.utils.sign; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Md5加密方法 + * + * @author ruoyi + */ +public class Md5Utils +{ + private static final Logger log = LoggerFactory.getLogger(Md5Utils.class); + + private static byte[] md5(String s) + { + MessageDigest algorithm; + try + { + algorithm = MessageDigest.getInstance("MD5"); + algorithm.reset(); + algorithm.update(s.getBytes("UTF-8")); + byte[] messageDigest = algorithm.digest(); + return messageDigest; + } + catch (Exception e) + { + log.error("MD5 Error...", e); + } + return null; + } + + private static final String toHex(byte hash[]) + { + if (hash == null) + { + return null; + } + StringBuffer buf = new StringBuffer(hash.length * 2); + int i; + + for (i = 0; i < hash.length; i++) + { + if ((hash[i] & 0xff) < 0x10) + { + buf.append("0"); + } + buf.append(Long.toString(hash[i] & 0xff, 16)); + } + return buf.toString(); + } + + public static String hash(String s) + { + try + { + return new String(toHex(md5(s)).getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8); + } + catch (Exception e) + { + log.error("not supported charset...{}", e); + return s; + } + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/spring/SpringUtils.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/spring/SpringUtils.java new file mode 100644 index 0000000..4e3f603 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/spring/SpringUtils.java @@ -0,0 +1,164 @@ +package com.ruoyi.common.utils.spring; + +import org.springframework.aop.framework.Advised; +import org.springframework.aop.framework.AopContext; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; +import com.ruoyi.common.utils.StringUtils; + +/** + * spring工具类 方便在非spring管理环境中获取bean + * + * @author ruoyi + */ +@Component +public final class SpringUtils implements BeanFactoryPostProcessor, ApplicationContextAware +{ + /** Spring应用上下文环境 */ + private static ConfigurableListableBeanFactory beanFactory; + + private static ApplicationContext applicationContext; + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException + { + SpringUtils.beanFactory = beanFactory; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException + { + SpringUtils.applicationContext = applicationContext; + } + + /** + * 获取对象 + * + * @param name + * @return Object 一个以所给名字注册的bean的实例 + * @throws org.springframework.beans.BeansException + * + */ + @SuppressWarnings("unchecked") + public static T getBean(String name) throws BeansException + { + return (T) beanFactory.getBean(name); + } + + /** + * 获取类型为requiredType的对象 + * + * @param clz + * @return + * @throws org.springframework.beans.BeansException + * + */ + public static T getBean(Class clz) throws BeansException + { + T result = (T) beanFactory.getBean(clz); + return result; + } + + /** + * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true + * + * @param name + * @return boolean + */ + public static boolean containsBean(String name) + { + return beanFactory.containsBean(name); + } + + /** + * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException) + * + * @param name + * @return boolean + * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException + * + */ + public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException + { + return beanFactory.isSingleton(name); + } + + /** + * @param name + * @return Class 注册对象的类型 + * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException + * + */ + public static Class getType(String name) throws NoSuchBeanDefinitionException + { + return beanFactory.getType(name); + } + + /** + * 如果给定的bean名字在bean定义中有别名,则返回这些别名 + * + * @param name + * @return + * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException + * + */ + public static String[] getAliases(String name) throws NoSuchBeanDefinitionException + { + return beanFactory.getAliases(name); + } + + /** + * 获取aop代理对象 + * + * @param invoker + * @return + */ + @SuppressWarnings("unchecked") + public static T getAopProxy(T invoker) + { + Object proxy = AopContext.currentProxy(); + if (((Advised) proxy).getTargetSource().getTargetClass() == invoker.getClass()) + { + return (T) proxy; + } + return invoker; + } + + /** + * 获取当前的环境配置,无配置返回null + * + * @return 当前的环境配置 + */ + public static String[] getActiveProfiles() + { + return applicationContext.getEnvironment().getActiveProfiles(); + } + + /** + * 获取当前的环境配置,当有多个环境配置时,只获取第一个 + * + * @return 当前的环境配置 + */ + public static String getActiveProfile() + { + final String[] activeProfiles = getActiveProfiles(); + return StringUtils.isNotEmpty(activeProfiles) ? activeProfiles[0] : null; + } + + /** + * 获取配置文件中的值 + * + * @param key 配置文件的key + * @return 当前的配置文件的值 + * + */ + public static String getRequiredProperty(String key) + { + return applicationContext.getEnvironment().getRequiredProperty(key); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/sql/SqlUtil.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/sql/SqlUtil.java new file mode 100644 index 0000000..48dba8b --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/sql/SqlUtil.java @@ -0,0 +1,71 @@ +package com.ruoyi.common.utils.sql; + +import com.ruoyi.common.exception.UtilException; +import com.ruoyi.common.utils.StringUtils; + +/** + * sql操作工具类 + * + * @author ruoyi + */ +public class SqlUtil +{ + /** + * 定义常用的 sql关键字 + */ + public static String SQL_REGEX = "\u000B|%0A|and |extractvalue|updatexml|sleep|information_schema|exec |insert |select |delete |update |drop |count |chr |mid |master |truncate |char |declare |or |union |like |+|/*|user()"; + + /** + * 仅支持字母、数字、下划线、空格、逗号、小数点(支持多个字段排序) + */ + public static String SQL_PATTERN = "[a-zA-Z0-9_\\ \\,\\.]+"; + + /** + * 限制orderBy最大长度 + */ + private static final int ORDER_BY_MAX_LENGTH = 500; + + /** + * 检查字符,防止注入绕过 + */ + public static String escapeOrderBySql(String value) + { + if (StringUtils.isNotEmpty(value) && !isValidOrderBySql(value)) + { + throw new UtilException("参数不符合规范,不能进行查询"); + } + if (StringUtils.length(value) > ORDER_BY_MAX_LENGTH) + { + throw new UtilException("参数已超过最大限制,不能进行查询"); + } + return value; + } + + /** + * 验证 order by 语法是否符合规范 + */ + public static boolean isValidOrderBySql(String value) + { + return value.matches(SQL_PATTERN); + } + + /** + * SQL关键字检查 + */ + public static void filterKeyword(String value) + { + if (StringUtils.isEmpty(value)) + { + return; + } + String normalizedValue = value.replaceAll("\\p{Z}|\\s", ""); + String[] sqlKeywords = StringUtils.split(SQL_REGEX, "\\|"); + for (String sqlKeyword : sqlKeywords) + { + if (StringUtils.indexOfIgnoreCase(normalizedValue, sqlKeyword) > -1) + { + throw new UtilException("请求参数包含敏感关键词'" + sqlKeyword + "',可能存在安全风险"); + } + } + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/IdUtils.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/IdUtils.java new file mode 100644 index 0000000..2c84427 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/IdUtils.java @@ -0,0 +1,49 @@ +package com.ruoyi.common.utils.uuid; + +/** + * ID生成器工具类 + * + * @author ruoyi + */ +public class IdUtils +{ + /** + * 获取随机UUID + * + * @return 随机UUID + */ + public static String randomUUID() + { + return UUID.randomUUID().toString(); + } + + /** + * 简化的UUID,去掉了横线 + * + * @return 简化的UUID,去掉了横线 + */ + public static String simpleUUID() + { + return UUID.randomUUID().toString(true); + } + + /** + * 获取随机UUID,使用性能更好的ThreadLocalRandom生成UUID + * + * @return 随机UUID + */ + public static String fastUUID() + { + return UUID.fastUUID().toString(); + } + + /** + * 简化的UUID,去掉了横线,使用性能更好的ThreadLocalRandom生成UUID + * + * @return 简化的UUID,去掉了横线 + */ + public static String fastSimpleUUID() + { + return UUID.fastUUID().toString(true); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/Seq.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/Seq.java new file mode 100644 index 0000000..bf99611 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/Seq.java @@ -0,0 +1,86 @@ +package com.ruoyi.common.utils.uuid; + +import java.util.concurrent.atomic.AtomicInteger; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; + +/** + * @author ruoyi 序列生成类 + */ +public class Seq +{ + // 通用序列类型 + public static final String commSeqType = "COMMON"; + + // 上传序列类型 + public static final String uploadSeqType = "UPLOAD"; + + // 通用接口序列数 + private static AtomicInteger commSeq = new AtomicInteger(1); + + // 上传接口序列数 + private static AtomicInteger uploadSeq = new AtomicInteger(1); + + // 机器标识 + private static final String machineCode = "A"; + + /** + * 获取通用序列号 + * + * @return 序列值 + */ + public static String getId() + { + return getId(commSeqType); + } + + /** + * 默认16位序列号 yyMMddHHmmss + 一位机器标识 + 3长度循环递增字符串 + * + * @return 序列值 + */ + public static String getId(String type) + { + AtomicInteger atomicInt = commSeq; + if (uploadSeqType.equals(type)) + { + atomicInt = uploadSeq; + } + return getId(atomicInt, 3); + } + + /** + * 通用接口序列号 yyMMddHHmmss + 一位机器标识 + length长度循环递增字符串 + * + * @param atomicInt 序列数 + * @param length 数值长度 + * @return 序列值 + */ + public static String getId(AtomicInteger atomicInt, int length) + { + String result = DateUtils.dateTimeNow(); + result += machineCode; + result += getSeq(atomicInt, length); + return result; + } + + /** + * 序列循环递增字符串[1, 10 的 (length)幂次方), 用0左补齐length位数 + * + * @return 序列值 + */ + private synchronized static String getSeq(AtomicInteger atomicInt, int length) + { + // 先取值再+1 + int value = atomicInt.getAndIncrement(); + + // 如果更新后值>=10 的 (length)幂次方则重置为1 + int maxSeq = (int) Math.pow(10, length); + if (atomicInt.get() >= maxSeq) + { + atomicInt.set(1); + } + // 转字符串,用0左补齐 + return StringUtils.padl(value, length); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/UUID.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/UUID.java new file mode 100644 index 0000000..a5585d6 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/UUID.java @@ -0,0 +1,484 @@ +package com.ruoyi.common.utils.uuid; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; +import com.ruoyi.common.exception.UtilException; + +/** + * 提供通用唯一识别码(universally unique identifier)(UUID)实现 + * + * @author ruoyi + */ +public final class UUID implements java.io.Serializable, Comparable +{ + private static final long serialVersionUID = -1185015143654744140L; + + /** + * SecureRandom 的单例 + * + */ + private static class Holder + { + static final SecureRandom numberGenerator = getSecureRandom(); + } + + /** 此UUID的最高64有效位 */ + private final long mostSigBits; + + /** 此UUID的最低64有效位 */ + private final long leastSigBits; + + /** + * 私有构造 + * + * @param data 数据 + */ + private UUID(byte[] data) + { + long msb = 0; + long lsb = 0; + assert data.length == 16 : "data must be 16 bytes in length"; + for (int i = 0; i < 8; i++) + { + msb = (msb << 8) | (data[i] & 0xff); + } + for (int i = 8; i < 16; i++) + { + lsb = (lsb << 8) | (data[i] & 0xff); + } + this.mostSigBits = msb; + this.leastSigBits = lsb; + } + + /** + * 使用指定的数据构造新的 UUID。 + * + * @param mostSigBits 用于 {@code UUID} 的最高有效 64 位 + * @param leastSigBits 用于 {@code UUID} 的最低有效 64 位 + */ + public UUID(long mostSigBits, long leastSigBits) + { + this.mostSigBits = mostSigBits; + this.leastSigBits = leastSigBits; + } + + /** + * 获取类型 4(伪随机生成的)UUID 的静态工厂。 + * + * @return 随机生成的 {@code UUID} + */ + public static UUID fastUUID() + { + return randomUUID(false); + } + + /** + * 获取类型 4(伪随机生成的)UUID 的静态工厂。 使用加密的强伪随机数生成器生成该 UUID。 + * + * @return 随机生成的 {@code UUID} + */ + public static UUID randomUUID() + { + return randomUUID(true); + } + + /** + * 获取类型 4(伪随机生成的)UUID 的静态工厂。 使用加密的强伪随机数生成器生成该 UUID。 + * + * @param isSecure 是否使用{@link SecureRandom}如果是可以获得更安全的随机码,否则可以得到更好的性能 + * @return 随机生成的 {@code UUID} + */ + public static UUID randomUUID(boolean isSecure) + { + final Random ng = isSecure ? Holder.numberGenerator : getRandom(); + + byte[] randomBytes = new byte[16]; + ng.nextBytes(randomBytes); + randomBytes[6] &= 0x0f; /* clear version */ + randomBytes[6] |= 0x40; /* set to version 4 */ + randomBytes[8] &= 0x3f; /* clear variant */ + randomBytes[8] |= 0x80; /* set to IETF variant */ + return new UUID(randomBytes); + } + + /** + * 根据指定的字节数组获取类型 3(基于名称的)UUID 的静态工厂。 + * + * @param name 用于构造 UUID 的字节数组。 + * + * @return 根据指定数组生成的 {@code UUID} + */ + public static UUID nameUUIDFromBytes(byte[] name) + { + MessageDigest md; + try + { + md = MessageDigest.getInstance("MD5"); + } + catch (NoSuchAlgorithmException nsae) + { + throw new InternalError("MD5 not supported"); + } + byte[] md5Bytes = md.digest(name); + md5Bytes[6] &= 0x0f; /* clear version */ + md5Bytes[6] |= 0x30; /* set to version 3 */ + md5Bytes[8] &= 0x3f; /* clear variant */ + md5Bytes[8] |= 0x80; /* set to IETF variant */ + return new UUID(md5Bytes); + } + + /** + * 根据 {@link #toString()} 方法中描述的字符串标准表示形式创建{@code UUID}。 + * + * @param name 指定 {@code UUID} 字符串 + * @return 具有指定值的 {@code UUID} + * @throws IllegalArgumentException 如果 name 与 {@link #toString} 中描述的字符串表示形式不符抛出此异常 + * + */ + public static UUID fromString(String name) + { + String[] components = name.split("-"); + if (components.length != 5) + { + throw new IllegalArgumentException("Invalid UUID string: " + name); + } + for (int i = 0; i < 5; i++) + { + components[i] = "0x" + components[i]; + } + + long mostSigBits = Long.decode(components[0]).longValue(); + mostSigBits <<= 16; + mostSigBits |= Long.decode(components[1]).longValue(); + mostSigBits <<= 16; + mostSigBits |= Long.decode(components[2]).longValue(); + + long leastSigBits = Long.decode(components[3]).longValue(); + leastSigBits <<= 48; + leastSigBits |= Long.decode(components[4]).longValue(); + + return new UUID(mostSigBits, leastSigBits); + } + + /** + * 返回此 UUID 的 128 位值中的最低有效 64 位。 + * + * @return 此 UUID 的 128 位值中的最低有效 64 位。 + */ + public long getLeastSignificantBits() + { + return leastSigBits; + } + + /** + * 返回此 UUID 的 128 位值中的最高有效 64 位。 + * + * @return 此 UUID 的 128 位值中最高有效 64 位。 + */ + public long getMostSignificantBits() + { + return mostSigBits; + } + + /** + * 与此 {@code UUID} 相关联的版本号. 版本号描述此 {@code UUID} 是如何生成的。 + *

+ * 版本号具有以下含意: + *

    + *
  • 1 基于时间的 UUID + *
  • 2 DCE 安全 UUID + *
  • 3 基于名称的 UUID + *
  • 4 随机生成的 UUID + *
+ * + * @return 此 {@code UUID} 的版本号 + */ + public int version() + { + // Version is bits masked by 0x000000000000F000 in MS long + return (int) ((mostSigBits >> 12) & 0x0f); + } + + /** + * 与此 {@code UUID} 相关联的变体号。变体号描述 {@code UUID} 的布局。 + *

+ * 变体号具有以下含意: + *

    + *
  • 0 为 NCS 向后兼容保留 + *
  • 2 IETF RFC 4122(Leach-Salz), 用于此类 + *
  • 6 保留,微软向后兼容 + *
  • 7 保留供以后定义使用 + *
+ * + * @return 此 {@code UUID} 相关联的变体号 + */ + public int variant() + { + // This field is composed of a varying number of bits. + // 0 - - Reserved for NCS backward compatibility + // 1 0 - The IETF aka Leach-Salz variant (used by this class) + // 1 1 0 Reserved, Microsoft backward compatibility + // 1 1 1 Reserved for future definition. + return (int) ((leastSigBits >>> (64 - (leastSigBits >>> 62))) & (leastSigBits >> 63)); + } + + /** + * 与此 UUID 相关联的时间戳值。 + * + *

+ * 60 位的时间戳值根据此 {@code UUID} 的 time_low、time_mid 和 time_hi 字段构造。
+ * 所得到的时间戳以 100 毫微秒为单位,从 UTC(通用协调时间) 1582 年 10 月 15 日零时开始。 + * + *

+ * 时间戳值仅在在基于时间的 UUID(其 version 类型为 1)中才有意义。
+ * 如果此 {@code UUID} 不是基于时间的 UUID,则此方法抛出 UnsupportedOperationException。 + * + * @throws UnsupportedOperationException 如果此 {@code UUID} 不是 version 为 1 的 UUID。 + */ + public long timestamp() throws UnsupportedOperationException + { + checkTimeBase(); + return (mostSigBits & 0x0FFFL) << 48// + | ((mostSigBits >> 16) & 0x0FFFFL) << 32// + | mostSigBits >>> 32; + } + + /** + * 与此 UUID 相关联的时钟序列值。 + * + *

+ * 14 位的时钟序列值根据此 UUID 的 clock_seq 字段构造。clock_seq 字段用于保证在基于时间的 UUID 中的时间唯一性。 + *

+ * {@code clockSequence} 值仅在基于时间的 UUID(其 version 类型为 1)中才有意义。 如果此 UUID 不是基于时间的 UUID,则此方法抛出 + * UnsupportedOperationException。 + * + * @return 此 {@code UUID} 的时钟序列 + * + * @throws UnsupportedOperationException 如果此 UUID 的 version 不为 1 + */ + public int clockSequence() throws UnsupportedOperationException + { + checkTimeBase(); + return (int) ((leastSigBits & 0x3FFF000000000000L) >>> 48); + } + + /** + * 与此 UUID 相关的节点值。 + * + *

+ * 48 位的节点值根据此 UUID 的 node 字段构造。此字段旨在用于保存机器的 IEEE 802 地址,该地址用于生成此 UUID 以保证空间唯一性。 + *

+ * 节点值仅在基于时间的 UUID(其 version 类型为 1)中才有意义。
+ * 如果此 UUID 不是基于时间的 UUID,则此方法抛出 UnsupportedOperationException。 + * + * @return 此 {@code UUID} 的节点值 + * + * @throws UnsupportedOperationException 如果此 UUID 的 version 不为 1 + */ + public long node() throws UnsupportedOperationException + { + checkTimeBase(); + return leastSigBits & 0x0000FFFFFFFFFFFFL; + } + + /** + * 返回此{@code UUID} 的字符串表现形式。 + * + *

+ * UUID 的字符串表示形式由此 BNF 描述: + * + *

+     * {@code
+     * UUID                   = ----
+     * time_low               = 4*
+     * time_mid               = 2*
+     * time_high_and_version  = 2*
+     * variant_and_sequence   = 2*
+     * node                   = 6*
+     * hexOctet               = 
+     * hexDigit               = [0-9a-fA-F]
+     * }
+     * 
+ * + * + * + * @return 此{@code UUID} 的字符串表现形式 + * @see #toString(boolean) + */ + @Override + public String toString() + { + return toString(false); + } + + /** + * 返回此{@code UUID} 的字符串表现形式。 + * + *

+ * UUID 的字符串表示形式由此 BNF 描述: + * + *

+     * {@code
+     * UUID                   = ----
+     * time_low               = 4*
+     * time_mid               = 2*
+     * time_high_and_version  = 2*
+     * variant_and_sequence   = 2*
+     * node                   = 6*
+     * hexOctet               = 
+     * hexDigit               = [0-9a-fA-F]
+     * }
+     * 
+ * + * + * + * @param isSimple 是否简单模式,简单模式为不带'-'的UUID字符串 + * @return 此{@code UUID} 的字符串表现形式 + */ + public String toString(boolean isSimple) + { + final StringBuilder builder = new StringBuilder(isSimple ? 32 : 36); + // time_low + builder.append(digits(mostSigBits >> 32, 8)); + if (!isSimple) + { + builder.append('-'); + } + // time_mid + builder.append(digits(mostSigBits >> 16, 4)); + if (!isSimple) + { + builder.append('-'); + } + // time_high_and_version + builder.append(digits(mostSigBits, 4)); + if (!isSimple) + { + builder.append('-'); + } + // variant_and_sequence + builder.append(digits(leastSigBits >> 48, 4)); + if (!isSimple) + { + builder.append('-'); + } + // node + builder.append(digits(leastSigBits, 12)); + + return builder.toString(); + } + + /** + * 返回此 UUID 的哈希码。 + * + * @return UUID 的哈希码值。 + */ + @Override + public int hashCode() + { + long hilo = mostSigBits ^ leastSigBits; + return ((int) (hilo >> 32)) ^ (int) hilo; + } + + /** + * 将此对象与指定对象比较。 + *

+ * 当且仅当参数不为 {@code null}、而是一个 UUID 对象、具有与此 UUID 相同的 varriant、包含相同的值(每一位均相同)时,结果才为 {@code true}。 + * + * @param obj 要与之比较的对象 + * + * @return 如果对象相同,则返回 {@code true};否则返回 {@code false} + */ + @Override + public boolean equals(Object obj) + { + if ((null == obj) || (obj.getClass() != UUID.class)) + { + return false; + } + UUID id = (UUID) obj; + return (mostSigBits == id.mostSigBits && leastSigBits == id.leastSigBits); + } + + // Comparison Operations + + /** + * 将此 UUID 与指定的 UUID 比较。 + * + *

+ * 如果两个 UUID 不同,且第一个 UUID 的最高有效字段大于第二个 UUID 的对应字段,则第一个 UUID 大于第二个 UUID。 + * + * @param val 与此 UUID 比较的 UUID + * + * @return 在此 UUID 小于、等于或大于 val 时,分别返回 -1、0 或 1。 + * + */ + @Override + public int compareTo(UUID val) + { + // The ordering is intentionally set up so that the UUIDs + // can simply be numerically compared as two numbers + return (this.mostSigBits < val.mostSigBits ? -1 : // + (this.mostSigBits > val.mostSigBits ? 1 : // + (this.leastSigBits < val.leastSigBits ? -1 : // + (this.leastSigBits > val.leastSigBits ? 1 : // + 0)))); + } + + // ------------------------------------------------------------------------------------------------------------------- + // Private method start + /** + * 返回指定数字对应的hex值 + * + * @param val 值 + * @param digits 位 + * @return 值 + */ + private static String digits(long val, int digits) + { + long hi = 1L << (digits * 4); + return Long.toHexString(hi | (val & (hi - 1))).substring(1); + } + + /** + * 检查是否为time-based版本UUID + */ + private void checkTimeBase() + { + if (version() != 1) + { + throw new UnsupportedOperationException("Not a time-based UUID"); + } + } + + /** + * 获取{@link SecureRandom},类提供加密的强随机数生成器 (RNG) + * + * @return {@link SecureRandom} + */ + public static SecureRandom getSecureRandom() + { + try + { + return SecureRandom.getInstance("SHA1PRNG"); + } + catch (NoSuchAlgorithmException e) + { + throw new UtilException(e); + } + } + + /** + * 获取随机数生成器对象
+ * ThreadLocalRandom是JDK 7之后提供并发产生随机数,能够解决多个线程发生的竞争争夺。 + * + * @return {@link ThreadLocalRandom} + */ + public static ThreadLocalRandom getRandom() + { + return ThreadLocalRandom.current(); + } +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/xss/Xss.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/xss/Xss.java new file mode 100644 index 0000000..cb4b408 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/xss/Xss.java @@ -0,0 +1,27 @@ +package com.ruoyi.common.xss; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 自定义xss校验注解 + * + * @author ruoyi + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(value = { ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER }) +@Constraint(validatedBy = { XssValidator.class }) +public @interface Xss +{ + String message() + + default "不允许任何脚本运行"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/xss/XssValidator.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/xss/XssValidator.java new file mode 100644 index 0000000..7c8e4c0 --- /dev/null +++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/xss/XssValidator.java @@ -0,0 +1,39 @@ +package com.ruoyi.common.xss; + +import com.ruoyi.common.utils.StringUtils; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 自定义xss校验注解实现 + * + * @author ruoyi + */ +public class XssValidator implements ConstraintValidator +{ + private static final String HTML_PATTERN = "<(\\S*?)[^>]*>.*?|<.*? />"; + + @Override + public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) + { + if (StringUtils.isBlank(value)) + { + return true; + } + return !containsHtml(value); + } + + public static boolean containsHtml(String value) + { + StringBuilder sHtml = new StringBuilder(); + Pattern pattern = Pattern.compile(HTML_PATTERN); + Matcher matcher = pattern.matcher(value); + while (matcher.find()) + { + sHtml.append(matcher.group()); + } + return pattern.matcher(sHtml).matches(); + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-framework/pom.xml b/ry-api/ruoyi-framework/pom.xml new file mode 100644 index 0000000..328c03c --- /dev/null +++ b/ry-api/ruoyi-framework/pom.xml @@ -0,0 +1,64 @@ + + + + ruoyi + com.ruoyi + 3.9.2 + + 4.0.0 + + ruoyi-framework + + + framework框架核心 + + + + + + + org.springframework.boot + spring-boot-starter-webmvc + + + + + org.springframework.boot + spring-boot-starter-aspectj + + + + + com.alibaba + druid-spring-boot-4-starter + + + + + pro.fessional + kaptcha + + + servlet-api + javax.servlet + + + + + + + com.github.oshi + oshi-core + + + + + com.ruoyi + ruoyi-system + + + + + \ No newline at end of file diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataScopeAspect.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataScopeAspect.java new file mode 100644 index 0000000..53cf56c --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataScopeAspect.java @@ -0,0 +1,160 @@ +package com.ruoyi.framework.aspectj; + +import java.util.ArrayList; +import java.util.List; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.springframework.stereotype.Component; +import com.ruoyi.common.annotation.DataScope; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.BaseEntity; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.security.context.PermissionContextHolder; + +/** + * 数据过滤处理 + * + * @author ruoyi + */ +@Aspect +@Component +public class DataScopeAspect +{ + /** + * 数据权限过滤关键字 + */ + public static final String DATA_SCOPE = "dataScope"; + + @Before("@annotation(controllerDataScope)") + public void doBefore(JoinPoint point, DataScope controllerDataScope) throws Throwable + { + clearDataScope(point); + handleDataScope(point, controllerDataScope); + } + + protected void handleDataScope(final JoinPoint joinPoint, DataScope controllerDataScope) + { + // 获取当前的用户 + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (StringUtils.isNotNull(loginUser)) + { + SysUser currentUser = loginUser.getUser(); + // 如果是超级管理员,则不过滤数据 + if (StringUtils.isNotNull(currentUser) && !currentUser.isAdmin()) + { + String permission = StringUtils.defaultIfEmpty(controllerDataScope.permission(), PermissionContextHolder.getContext()); + dataScopeFilter(joinPoint, currentUser, controllerDataScope.userAlias(), controllerDataScope.deptAlias(), controllerDataScope.userField(), controllerDataScope.deptField(), permission); + } + } + } + + /** + * 数据范围过滤 + * + * @param joinPoint 切点 + * @param user 用户 + * @param deptAlias 部门别名 + * @param userAlias 用户别名 + * @param permission 权限字符 + */ + public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String userAlias, String deptAlias, String userField, String deptField, String permission) + { + StringBuilder sqlString = new StringBuilder(); + List conditions = new ArrayList(); + List scopeCustomIds = new ArrayList(); + user.getRoles().forEach(role -> { + if (Constants.Dept.DATA_SCOPE_CUSTOM.equals(role.getDataScope()) && StringUtils.equals(role.getStatus(), UserConstants.ROLE_NORMAL) && (StringUtils.isEmpty(permission) || StringUtils.containsAny(role.getPermissions(), Convert.toStrArray(permission)))) + { + scopeCustomIds.add(Convert.toStr(role.getRoleId())); + } + }); + + for (SysRole role : user.getRoles()) + { + String dataScope = role.getDataScope(); + if (conditions.contains(dataScope) || StringUtils.equals(role.getStatus(), UserConstants.ROLE_DISABLE)) + { + continue; + } + if (StringUtils.isNotEmpty(permission) && !StringUtils.containsAny(role.getPermissions(), Convert.toStrArray(permission))) + { + continue; + } + if (Constants.Dept.DATA_SCOPE_ALL.equals(dataScope)) + { + sqlString = new StringBuilder(); + conditions.add(dataScope); + break; + } + else if (Constants.Dept.DATA_SCOPE_CUSTOM.equals(dataScope)) + { + if (scopeCustomIds.size() > 1) + { + // 多个自定数据权限使用in查询,避免多次拼接。 + sqlString.append(StringUtils.format(" OR {}.{} IN ( SELECT dept_id FROM sys_role_dept WHERE role_id in ({}) ) ", deptAlias, deptField, String.join(",", scopeCustomIds))); + } + else + { + sqlString.append(StringUtils.format(" OR {}.{} IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias, deptField, role.getRoleId())); + } + } + else if (Constants.Dept.DATA_SCOPE_DEPT.equals(dataScope)) + { + sqlString.append(StringUtils.format(" OR {}.{} = {} ", deptAlias, deptField, user.getDeptId())); + } + else if (Constants.Dept.DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope)) + { + sqlString.append(StringUtils.format(" OR {}.{} IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )", deptAlias, deptField, user.getDeptId(), user.getDeptId())); + } + else if (Constants.Dept.DATA_SCOPE_SELF.equals(dataScope)) + { + if (StringUtils.isNotBlank(userAlias)) + { + sqlString.append(StringUtils.format(" OR {}.{} = {} ", userAlias, userField, user.getUserId())); + } + else + { + // 数据权限为仅本人且没有userAlias别名不查询任何数据 + sqlString.append(StringUtils.format(" OR {}.{} = 0 ", deptAlias, deptField)); + } + } + conditions.add(dataScope); + } + + // 角色都不包含传递过来的权限字符,这个时候sqlString也会为空,所以要限制一下,不查询任何数据 + if (StringUtils.isEmpty(conditions)) + { + sqlString.append(StringUtils.format(" OR {}.{} = 0 ", deptAlias, deptField)); + } + + if (StringUtils.isNotBlank(sqlString.toString())) + { + Object params = joinPoint.getArgs()[0]; + if (StringUtils.isNotNull(params) && params instanceof BaseEntity) + { + BaseEntity baseEntity = (BaseEntity) params; + baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")"); + } + } + } + + /** + * 拼接权限sql前先清空params.dataScope参数防止注入 + */ + private void clearDataScope(final JoinPoint joinPoint) + { + Object params = joinPoint.getArgs()[0]; + if (StringUtils.isNotNull(params) && params instanceof BaseEntity) + { + BaseEntity baseEntity = (BaseEntity) params; + baseEntity.getParams().put(DATA_SCOPE, ""); + } + } +} diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataSourceAspect.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataSourceAspect.java new file mode 100644 index 0000000..8c2c9f4 --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataSourceAspect.java @@ -0,0 +1,72 @@ +package com.ruoyi.framework.aspectj; + +import java.util.Objects; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import com.ruoyi.common.annotation.DataSource; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.datasource.DynamicDataSourceContextHolder; + +/** + * 多数据源处理 + * + * @author ruoyi + */ +@Aspect +@Order(1) +@Component +public class DataSourceAspect +{ + protected Logger logger = LoggerFactory.getLogger(getClass()); + + @Pointcut("@annotation(com.ruoyi.common.annotation.DataSource)" + + "|| @within(com.ruoyi.common.annotation.DataSource)") + public void dsPointCut() + { + + } + + @Around("dsPointCut()") + public Object around(ProceedingJoinPoint point) throws Throwable + { + DataSource dataSource = getDataSource(point); + + if (StringUtils.isNotNull(dataSource)) + { + DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name()); + } + + try + { + return point.proceed(); + } + finally + { + // 销毁数据源 在执行方法之后 + DynamicDataSourceContextHolder.clearDataSourceType(); + } + } + + /** + * 获取需要切换的数据源 + */ + public DataSource getDataSource(ProceedingJoinPoint point) + { + MethodSignature signature = (MethodSignature) point.getSignature(); + DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class); + if (Objects.nonNull(dataSource)) + { + return dataSource; + } + + return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class); + } +} diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/LogAspect.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/LogAspect.java new file mode 100644 index 0000000..c6204fd --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/LogAspect.java @@ -0,0 +1,264 @@ +package com.ruoyi.framework.aspectj; + +import java.util.Collection; +import java.util.Map; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.commons.lang3.ArrayUtils; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.NamedThreadLocal; +import org.springframework.stereotype.Component; +import org.springframework.validation.BindingResult; +import org.springframework.web.multipart.MultipartFile; +import com.alibaba.fastjson2.JSON; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.enums.BusinessStatus; +import com.ruoyi.common.enums.HttpMethod; +import com.ruoyi.common.filter.PropertyPreExcludeFilter; +import com.ruoyi.common.utils.ExceptionUtil; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.ServletUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.ip.IpUtils; +import com.ruoyi.framework.manager.AsyncManager; +import com.ruoyi.framework.manager.factory.AsyncFactory; +import com.ruoyi.system.domain.SysOperLog; + +/** + * 操作日志记录处理 + * + * @author ruoyi + */ +@Aspect +@Component +public class LogAspect +{ + private static final Logger log = LoggerFactory.getLogger(LogAspect.class); + + /** 排除敏感属性字段 */ + public static final String[] EXCLUDE_PROPERTIES = { "password", "oldPassword", "newPassword", "confirmPassword" }; + + /** 计算操作消耗时间 */ + private static final ThreadLocal TIME_THREADLOCAL = new NamedThreadLocal("Cost Time"); + + /** 参数最大长度限制 */ + private static final int PARAM_MAX_LENGTH = 2000; + + /** + * 处理请求前执行 + */ + @Before(value = "@annotation(controllerLog)") + public void doBefore(JoinPoint joinPoint, Log controllerLog) + { + TIME_THREADLOCAL.set(System.currentTimeMillis()); + } + + /** + * 处理完请求后执行 + * + * @param joinPoint 切点 + */ + @AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult") + public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) + { + handleLog(joinPoint, controllerLog, null, jsonResult); + } + + /** + * 拦截异常操作 + * + * @param joinPoint 切点 + * @param e 异常 + */ + @AfterThrowing(value = "@annotation(controllerLog)", throwing = "e") + public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e) + { + handleLog(joinPoint, controllerLog, e, null); + } + + protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) + { + try + { + // 获取当前的用户 + LoginUser loginUser = SecurityUtils.getLoginUser(); + + // *========数据库日志=========*// + SysOperLog operLog = new SysOperLog(); + operLog.setStatus(BusinessStatus.SUCCESS.ordinal()); + // 请求的地址 + String ip = IpUtils.getIpAddr(); + operLog.setOperIp(ip); + operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255)); + if (loginUser != null) + { + operLog.setOperName(loginUser.getUsername()); + SysUser currentUser = loginUser.getUser(); + if (StringUtils.isNotNull(currentUser) && StringUtils.isNotNull(currentUser.getDept())) + { + operLog.setDeptName(currentUser.getDept().getDeptName()); + } + } + + if (e != null) + { + operLog.setStatus(BusinessStatus.FAIL.ordinal()); + operLog.setErrorMsg(StringUtils.substring(Convert.toStr(e.getMessage(), ExceptionUtil.getExceptionMessage(e)), 0, 2000)); + } + // 设置方法名称 + String className = joinPoint.getTarget().getClass().getName(); + String methodName = joinPoint.getSignature().getName(); + operLog.setMethod(className + "." + methodName + "()"); + // 设置请求方式 + operLog.setRequestMethod(ServletUtils.getRequest().getMethod()); + // 处理设置注解上的参数 + getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult); + // 设置消耗时间 + operLog.setCostTime(System.currentTimeMillis() - TIME_THREADLOCAL.get()); + // 保存数据库 + AsyncManager.me().execute(AsyncFactory.recordOper(operLog)); + } + catch (Exception exp) + { + // 记录本地异常日志 + log.error("异常信息:{}", exp.getMessage()); + exp.printStackTrace(); + } + finally + { + TIME_THREADLOCAL.remove(); + } + } + + /** + * 获取注解中对方法的描述信息 用于Controller层注解 + * + * @param log 日志 + * @param operLog 操作日志 + * @throws Exception + */ + public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLog operLog, Object jsonResult) throws Exception + { + // 设置action动作 + operLog.setBusinessType(log.businessType().ordinal()); + // 设置标题 + operLog.setTitle(log.title()); + // 设置操作人类别 + operLog.setOperatorType(log.operatorType().ordinal()); + // 是否需要保存request,参数和值 + if (log.isSaveRequestData()) + { + // 获取参数的信息,传入到数据库中。 + setRequestValue(joinPoint, operLog, log.excludeParamNames()); + } + // 是否需要保存response,参数和值 + if (log.isSaveResponseData() && StringUtils.isNotNull(jsonResult)) + { + operLog.setJsonResult(StringUtils.substring(JSON.toJSONString(jsonResult), 0, 2000)); + } + } + + /** + * 获取请求的参数,放到log中 + * + * @param operLog 操作日志 + * @throws Exception 异常 + */ + private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog, String[] excludeParamNames) throws Exception + { + String requestMethod = operLog.getRequestMethod(); + Map paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest()); + if (StringUtils.isEmpty(paramsMap) && StringUtils.equalsAny(requestMethod, HttpMethod.PUT.name(), HttpMethod.POST.name(), HttpMethod.DELETE.name())) + { + String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames); + operLog.setOperParam(params); + } + else + { + operLog.setOperParam(StringUtils.substring(JSON.toJSONString(paramsMap, excludePropertyPreFilter(excludeParamNames)), 0, PARAM_MAX_LENGTH)); + } + } + + /** + * 参数拼装 + */ + private String argsArrayToString(Object[] paramsArray, String[] excludeParamNames) + { + StringBuilder params = new StringBuilder(); + if (paramsArray != null && paramsArray.length > 0) + { + for (Object o : paramsArray) + { + if (StringUtils.isNotNull(o) && !isFilterObject(o)) + { + try + { + String jsonObj = JSON.toJSONString(o, excludePropertyPreFilter(excludeParamNames)); + params.append(jsonObj).append(" "); + if (params.length() >= PARAM_MAX_LENGTH) + { + return StringUtils.substring(params.toString(), 0, PARAM_MAX_LENGTH); + } + } + catch (Exception e) + { + log.error("请求参数拼装异常 msg:{}, 参数:{}", e.getMessage(), paramsArray, e); + } + } + } + } + return params.toString(); + } + + /** + * 忽略敏感属性 + */ + public PropertyPreExcludeFilter excludePropertyPreFilter(String[] excludeParamNames) + { + return new PropertyPreExcludeFilter().addExcludes(ArrayUtils.addAll(EXCLUDE_PROPERTIES, excludeParamNames)); + } + + /** + * 判断是否需要过滤的对象。 + * + * @param o 对象信息。 + * @return 如果是需要过滤的对象,则返回true;否则返回false。 + */ + @SuppressWarnings("rawtypes") + public boolean isFilterObject(final Object o) + { + Class clazz = o.getClass(); + if (clazz.isArray()) + { + return clazz.getComponentType().isAssignableFrom(MultipartFile.class); + } + else if (Collection.class.isAssignableFrom(clazz)) + { + Collection collection = (Collection) o; + for (Object value : collection) + { + return value instanceof MultipartFile; + } + } + else if (Map.class.isAssignableFrom(clazz)) + { + Map map = (Map) o; + for (Object value : map.entrySet()) + { + Map.Entry entry = (Map.Entry) value; + return entry.getValue() instanceof MultipartFile; + } + } + return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse + || o instanceof BindingResult; + } +} diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RateLimiterAspect.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RateLimiterAspect.java new file mode 100644 index 0000000..b720bc1 --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RateLimiterAspect.java @@ -0,0 +1,89 @@ +package com.ruoyi.framework.aspectj; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.script.RedisScript; +import org.springframework.stereotype.Component; +import com.ruoyi.common.annotation.RateLimiter; +import com.ruoyi.common.enums.LimitType; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.ip.IpUtils; + +/** + * 限流处理 + * + * @author ruoyi + */ +@Aspect +@Component +public class RateLimiterAspect +{ + private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class); + + private RedisTemplate redisTemplate; + + private RedisScript limitScript; + + @Autowired + public void setRedisTemplate1(RedisTemplate redisTemplate) + { + this.redisTemplate = redisTemplate; + } + + @Autowired + public void setLimitScript(RedisScript limitScript) + { + this.limitScript = limitScript; + } + + @Before("@annotation(rateLimiter)") + public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable + { + int time = rateLimiter.time(); + int count = rateLimiter.count(); + + String combineKey = getCombineKey(rateLimiter, point); + List keys = Collections.singletonList(combineKey); + try + { + Long number = redisTemplate.execute(limitScript, keys, count, time); + if (StringUtils.isNull(number) || number.intValue() > count) + { + throw new ServiceException("访问过于频繁,请稍候再试"); + } + log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), combineKey); + } + catch (ServiceException e) + { + throw e; + } + catch (Exception e) + { + throw new RuntimeException("服务器限流异常,请稍候再试"); + } + } + + public String getCombineKey(RateLimiter rateLimiter, JoinPoint point) + { + StringBuffer stringBuffer = new StringBuffer(rateLimiter.key()); + if (rateLimiter.limitType() == LimitType.IP) + { + stringBuffer.append(IpUtils.getIpAddr()).append("-"); + } + MethodSignature signature = (MethodSignature) point.getSignature(); + Method method = signature.getMethod(); + Class targetClass = method.getDeclaringClass(); + stringBuffer.append(targetClass.getName()).append("-").append(method.getName()); + return stringBuffer.toString(); + } +} diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ApplicationConfig.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ApplicationConfig.java new file mode 100644 index 0000000..26451c6 --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ApplicationConfig.java @@ -0,0 +1,19 @@ +package com.ruoyi.framework.config; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; + +/** + * 程序注解配置 + * + * @author ruoyi + */ +@Configuration +// 表示通过aop框架暴露该代理对象,AopContext能够访问 +@EnableAspectJAutoProxy(exposeProxy = true) +// 指定要扫描的Mapper类的包的路径 +@MapperScan("com.ruoyi.**.mapper") +public class ApplicationConfig +{ +} diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/CaptchaConfig.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/CaptchaConfig.java new file mode 100644 index 0000000..43e78ae --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/CaptchaConfig.java @@ -0,0 +1,83 @@ +package com.ruoyi.framework.config; + +import java.util.Properties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import com.google.code.kaptcha.impl.DefaultKaptcha; +import com.google.code.kaptcha.util.Config; +import static com.google.code.kaptcha.Constants.*; + +/** + * 验证码配置 + * + * @author ruoyi + */ +@Configuration +public class CaptchaConfig +{ + @Bean(name = "captchaProducer") + public DefaultKaptcha getKaptchaBean() + { + DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); + Properties properties = new Properties(); + // 是否有边框 默认为true 我们可以自己设置yes,no + properties.setProperty(KAPTCHA_BORDER, "yes"); + // 验证码文本字符颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "black"); + // 验证码图片宽度 默认为200 + properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160"); + // 验证码图片高度 默认为50 + properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60"); + // 验证码文本字符大小 默认为40 + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "38"); + // KAPTCHA_SESSION_KEY + properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCode"); + // 验证码文本字符长度 默认为5 + properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4"); + // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize) + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier"); + // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy + properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy"); + Config config = new Config(properties); + defaultKaptcha.setConfig(config); + return defaultKaptcha; + } + + @Bean(name = "captchaProducerMath") + public DefaultKaptcha getKaptchaBeanMath() + { + DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); + Properties properties = new Properties(); + // 是否有边框 默认为true 我们可以自己设置yes,no + properties.setProperty(KAPTCHA_BORDER, "yes"); + // 边框颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_BORDER_COLOR, "105,179,90"); + // 验证码文本字符颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "blue"); + // 验证码图片宽度 默认为200 + properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160"); + // 验证码图片高度 默认为50 + properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60"); + // 验证码文本字符大小 默认为40 + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "35"); + // KAPTCHA_SESSION_KEY + properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCodeMath"); + // 验证码文本生成器 + properties.setProperty(KAPTCHA_TEXTPRODUCER_IMPL, "com.ruoyi.framework.config.KaptchaTextCreator"); + // 验证码文本字符间距 默认为2 + properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_SPACE, "3"); + // 验证码文本字符长度 默认为5 + properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "6"); + // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize) + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier"); + // 验证码噪点颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_NOISE_COLOR, "white"); + // 干扰实现类 + properties.setProperty(KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise"); + // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy + properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy"); + Config config = new Config(properties); + defaultKaptcha.setConfig(config); + return defaultKaptcha; + } +} diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/DruidConfig.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/DruidConfig.java new file mode 100644 index 0000000..d99a931 --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/DruidConfig.java @@ -0,0 +1,126 @@ +package com.ruoyi.framework.config; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import javax.sql.DataSource; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import com.alibaba.druid.pool.DruidDataSource; +import com.alibaba.druid.spring.boot4.autoconfigure.DruidDataSourceBuilder; +import com.alibaba.druid.spring.boot4.autoconfigure.properties.DruidStatProperties; +import com.alibaba.druid.util.Utils; +import com.ruoyi.common.enums.DataSourceType; +import com.ruoyi.common.utils.spring.SpringUtils; +import com.ruoyi.framework.config.properties.DruidProperties; +import com.ruoyi.framework.datasource.DynamicDataSource; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; + +/** + * druid 配置多数据源 + * + * @author ruoyi + */ +@Configuration +public class DruidConfig +{ + @Bean + @ConfigurationProperties("spring.datasource.druid.master") + public DataSource masterDataSource(DruidProperties druidProperties) + { + DruidDataSource dataSource = DruidDataSourceBuilder.create().build(); + return druidProperties.dataSource(dataSource); + } + + @Bean + @ConfigurationProperties("spring.datasource.druid.slave") + @ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true") + public DataSource slaveDataSource(DruidProperties druidProperties) + { + DruidDataSource dataSource = DruidDataSourceBuilder.create().build(); + return druidProperties.dataSource(dataSource); + } + + @Bean(name = "dynamicDataSource") + @Primary + public DynamicDataSource dataSource(DataSource masterDataSource) + { + Map targetDataSources = new HashMap<>(); + targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource); + setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource"); + return new DynamicDataSource(masterDataSource, targetDataSources); + } + + /** + * 设置数据源 + * + * @param targetDataSources 备选数据源集合 + * @param sourceName 数据源名称 + * @param beanName bean名称 + */ + public void setDataSource(Map targetDataSources, String sourceName, String beanName) + { + try + { + DataSource dataSource = SpringUtils.getBean(beanName); + targetDataSources.put(sourceName, dataSource); + } + catch (Exception e) + { + } + } + + /** + * 去除监控页面底部的广告 + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Bean + @ConditionalOnProperty(name = "spring.datasource.druid.statViewServlet.enabled", havingValue = "true") + public FilterRegistrationBean removeDruidFilterRegistrationBean(DruidStatProperties properties) + { + // 获取web监控页面的参数 + DruidStatProperties.StatViewServlet config = properties.getStatViewServlet(); + // 提取common.js的配置路径 + String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*"; + String commonJsPattern = pattern.replaceAll("\\*", "js/common.js"); + final String filePath = "support/http/resources/js/common.js"; + // 创建filter进行过滤 + Filter filter = new Filter() + { + @Override + public void init(jakarta.servlet.FilterConfig filterConfig) throws ServletException + { + } + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException + { + chain.doFilter(request, response); + // 重置缓冲区,响应头不会被重置 + response.resetBuffer(); + // 获取common.js + String text = Utils.readFromResource(filePath); + // 正则替换banner, 除去底部的广告信息 + text = text.replaceAll("
", ""); + text = text.replaceAll("powered.*?shrek.wang", ""); + response.getWriter().write(text); + } + @Override + public void destroy() + { + } + }; + FilterRegistrationBean registrationBean = new FilterRegistrationBean(); + registrationBean.setFilter(filter); + registrationBean.addUrlPatterns(commonJsPattern); + return registrationBean; + } +} diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FastJson2JsonRedisSerializer.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FastJson2JsonRedisSerializer.java new file mode 100644 index 0000000..4adbb7f --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FastJson2JsonRedisSerializer.java @@ -0,0 +1,52 @@ +package com.ruoyi.framework.config; + +import java.nio.charset.Charset; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.SerializationException; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONReader; +import com.alibaba.fastjson2.JSONWriter; +import com.alibaba.fastjson2.filter.Filter; +import com.ruoyi.common.constant.Constants; + +/** + * Redis使用FastJson序列化 + * + * @author ruoyi + */ +public class FastJson2JsonRedisSerializer implements RedisSerializer +{ + public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); + + static final Filter AUTO_TYPE_FILTER = JSONReader.autoTypeFilter(Constants.JSON_WHITELIST_STR); + + private Class clazz; + + public FastJson2JsonRedisSerializer(Class clazz) + { + super(); + this.clazz = clazz; + } + + @Override + public byte[] serialize(T t) throws SerializationException + { + if (t == null) + { + return new byte[0]; + } + return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET); + } + + @Override + public T deserialize(byte[] bytes) throws SerializationException + { + if (bytes == null || bytes.length <= 0) + { + return null; + } + String str = new String(bytes, DEFAULT_CHARSET); + + return JSON.parseObject(str, clazz, AUTO_TYPE_FILTER); + } +} diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FilterConfig.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FilterConfig.java new file mode 100644 index 0000000..e4189b3 --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FilterConfig.java @@ -0,0 +1,80 @@ +package com.ruoyi.framework.config; + +import java.util.HashMap; +import java.util.Map; +import jakarta.servlet.DispatcherType; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.filter.RefererFilter; +import com.ruoyi.common.filter.RepeatableFilter; +import com.ruoyi.common.filter.XssFilter; +import com.ruoyi.common.utils.StringUtils; + +/** + * Filter配置 + * + * @author ruoyi + */ +@Configuration +public class FilterConfig +{ + @Value("${xss.excludes}") + private String excludes; + + @Value("${xss.urlPatterns}") + private String urlPatterns; + + @Value("${referer.allowed-domains}") + private String allowedDomains; + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Bean + @ConditionalOnProperty(value = "xss.enabled", havingValue = "true") + public FilterRegistrationBean xssFilterRegistration() + { + FilterRegistrationBean registration = new FilterRegistrationBean(); + registration.setDispatcherTypes(DispatcherType.REQUEST); + registration.setFilter(new XssFilter()); + registration.addUrlPatterns(StringUtils.split(urlPatterns, ",")); + registration.setName("xssFilter"); + registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE); + Map initParameters = new HashMap(); + initParameters.put("excludes", excludes); + registration.setInitParameters(initParameters); + return registration; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Bean + @ConditionalOnProperty(value = "referer.enabled", havingValue = "true") + public FilterRegistrationBean refererFilterRegistration() + { + FilterRegistrationBean registration = new FilterRegistrationBean(); + registration.setDispatcherTypes(DispatcherType.REQUEST); + registration.setFilter(new RefererFilter()); + registration.addUrlPatterns(Constants.RESOURCE_PREFIX + "/*"); + registration.setName("refererFilter"); + registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE); + Map initParameters = new HashMap(); + initParameters.put("allowedDomains", allowedDomains); + registration.setInitParameters(initParameters); + return registration; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Bean + public FilterRegistrationBean someFilterRegistration() + { + FilterRegistrationBean registration = new FilterRegistrationBean(); + registration.setFilter(new RepeatableFilter()); + registration.addUrlPatterns("/*"); + registration.setName("repeatableFilter"); + registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE); + return registration; + } + +} diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/I18nConfig.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/I18nConfig.java new file mode 100644 index 0000000..163fd01 --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/I18nConfig.java @@ -0,0 +1,43 @@ +package com.ruoyi.framework.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.LocaleResolver; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; +import org.springframework.web.servlet.i18n.SessionLocaleResolver; +import com.ruoyi.common.constant.Constants; + +/** + * 资源文件配置加载 + * + * @author ruoyi + */ +@Configuration +public class I18nConfig implements WebMvcConfigurer +{ + @Bean + public LocaleResolver localeResolver() + { + SessionLocaleResolver slr = new SessionLocaleResolver(); + // 默认语言 + slr.setDefaultLocale(Constants.DEFAULT_LOCALE); + return slr; + } + + @Bean + public LocaleChangeInterceptor localeChangeInterceptor() + { + LocaleChangeInterceptor lci = new LocaleChangeInterceptor(); + // 参数名 + lci.setParamName("lang"); + return lci; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) + { + registry.addInterceptor(localeChangeInterceptor()); + } +} diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/KaptchaTextCreator.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/KaptchaTextCreator.java new file mode 100644 index 0000000..7f8e1d5 --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/KaptchaTextCreator.java @@ -0,0 +1,68 @@ +package com.ruoyi.framework.config; + +import java.util.Random; +import com.google.code.kaptcha.text.impl.DefaultTextCreator; + +/** + * 验证码文本生成器 + * + * @author ruoyi + */ +public class KaptchaTextCreator extends DefaultTextCreator +{ + private static final String[] CNUMBERS = "0,1,2,3,4,5,6,7,8,9,10".split(","); + + @Override + public String getText() + { + Integer result = 0; + Random random = new Random(); + int x = random.nextInt(10); + int y = random.nextInt(10); + StringBuilder suChinese = new StringBuilder(); + int randomoperands = random.nextInt(3); + if (randomoperands == 0) + { + result = x * y; + suChinese.append(CNUMBERS[x]); + suChinese.append("*"); + suChinese.append(CNUMBERS[y]); + } + else if (randomoperands == 1) + { + if ((x != 0) && y % x == 0) + { + result = y / x; + suChinese.append(CNUMBERS[y]); + suChinese.append("/"); + suChinese.append(CNUMBERS[x]); + } + else + { + result = x + y; + suChinese.append(CNUMBERS[x]); + suChinese.append("+"); + suChinese.append(CNUMBERS[y]); + } + } + else + { + if (x >= y) + { + result = x - y; + suChinese.append(CNUMBERS[x]); + suChinese.append("-"); + suChinese.append(CNUMBERS[y]); + } + else + { + result = y - x; + suChinese.append(CNUMBERS[y]); + suChinese.append("-"); + suChinese.append(CNUMBERS[x]); + } + } + suChinese.append("=?@" + result); + return suChinese.toString(); + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/MyBatisConfig.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/MyBatisConfig.java new file mode 100644 index 0000000..057c941 --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/MyBatisConfig.java @@ -0,0 +1,132 @@ +package com.ruoyi.framework.config; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import javax.sql.DataSource; +import org.apache.ibatis.io.VFS; +import org.apache.ibatis.session.SqlSessionFactory; +import org.mybatis.spring.SqlSessionFactoryBean; +import org.mybatis.spring.boot.autoconfigure.SpringBootVFS; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.core.type.classreading.CachingMetadataReaderFactory; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.util.ClassUtils; +import com.ruoyi.common.utils.StringUtils; + +/** + * Mybatis支持*匹配扫描包 + * + * @author ruoyi + */ +@Configuration +public class MyBatisConfig +{ + @Autowired + private Environment env; + + static final String DEFAULT_RESOURCE_PATTERN = "**/*.class"; + + public static String setTypeAliasesPackage(String typeAliasesPackage) + { + ResourcePatternResolver resolver = (ResourcePatternResolver) new PathMatchingResourcePatternResolver(); + MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resolver); + List allResult = new ArrayList(); + try + { + for (String aliasesPackage : typeAliasesPackage.split(",")) + { + List result = new ArrayList(); + aliasesPackage = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + + ClassUtils.convertClassNameToResourcePath(aliasesPackage.trim()) + "/" + DEFAULT_RESOURCE_PATTERN; + Resource[] resources = resolver.getResources(aliasesPackage); + if (resources != null && resources.length > 0) + { + MetadataReader metadataReader = null; + for (Resource resource : resources) + { + if (resource.isReadable()) + { + metadataReader = metadataReaderFactory.getMetadataReader(resource); + try + { + result.add(Class.forName(metadataReader.getClassMetadata().getClassName()).getPackage().getName()); + } + catch (ClassNotFoundException e) + { + e.printStackTrace(); + } + } + } + } + if (result.size() > 0) + { + HashSet hashResult = new HashSet(result); + allResult.addAll(hashResult); + } + } + if (allResult.size() > 0) + { + typeAliasesPackage = String.join(",", (String[]) allResult.toArray(new String[0])); + } + else + { + throw new RuntimeException("mybatis typeAliasesPackage 路径扫描错误,参数typeAliasesPackage:" + typeAliasesPackage + "未找到任何包"); + } + } + catch (IOException e) + { + e.printStackTrace(); + } + return typeAliasesPackage; + } + + public Resource[] resolveMapperLocations(String[] mapperLocations) + { + ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver(); + List resources = new ArrayList(); + if (mapperLocations != null) + { + for (String mapperLocation : mapperLocations) + { + try + { + Resource[] mappers = resourceResolver.getResources(mapperLocation); + resources.addAll(Arrays.asList(mappers)); + } + catch (IOException e) + { + // ignore + } + } + } + return resources.toArray(new Resource[resources.size()]); + } + + @Bean + public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception + { + String typeAliasesPackage = env.getProperty("mybatis.typeAliasesPackage"); + String mapperLocations = env.getProperty("mybatis.mapperLocations"); + String configLocation = env.getProperty("mybatis.configLocation"); + typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage); + VFS.addImplClass(SpringBootVFS.class); + + final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean(); + sessionFactory.setDataSource(dataSource); + sessionFactory.setTypeAliasesPackage(typeAliasesPackage); + sessionFactory.setMapperLocations(resolveMapperLocations(StringUtils.split(mapperLocations, ","))); + sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation)); + return sessionFactory.getObject(); + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/RedisConfig.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/RedisConfig.java new file mode 100644 index 0000000..3453237 --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/RedisConfig.java @@ -0,0 +1,70 @@ +package com.ruoyi.framework.config; + +import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.script.DefaultRedisScript; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +/** + * redis配置 + * + * @author ruoyi + */ +@SuppressWarnings("deprecation") +@Configuration +@EnableCaching +public class RedisConfig extends CachingConfigurerSupport +{ + @Bean + @SuppressWarnings(value = { "unchecked", "rawtypes" }) + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) + { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + + FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class); + + // 使用StringRedisSerializer来序列化和反序列化redis的key值 + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(serializer); + + // Hash的key也采用StringRedisSerializer的序列化方式 + template.setHashKeySerializer(new StringRedisSerializer()); + template.setHashValueSerializer(serializer); + + template.afterPropertiesSet(); + return template; + } + + @Bean + public DefaultRedisScript limitScript() + { + DefaultRedisScript redisScript = new DefaultRedisScript<>(); + redisScript.setScriptText(limitScriptText()); + redisScript.setResultType(Long.class); + return redisScript; + } + + /** + * 限流脚本 + */ + private String limitScriptText() + { + return "local key = KEYS[1]\n" + + "local count = tonumber(ARGV[1])\n" + + "local time = tonumber(ARGV[2])\n" + + "local current = redis.call('get', key);\n" + + "if current and tonumber(current) > count then\n" + + " return tonumber(current);\n" + + "end\n" + + "current = redis.call('incr', key)\n" + + "if tonumber(current) == 1 then\n" + + " redis.call('expire', key, time)\n" + + "end\n" + + "return tonumber(current);"; + } +} diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ResourcesConfig.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ResourcesConfig.java new file mode 100644 index 0000000..0f48b11 --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ResourcesConfig.java @@ -0,0 +1,72 @@ +package com.ruoyi.framework.config; + +import java.util.concurrent.TimeUnit; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.CacheControl; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.framework.interceptor.RepeatSubmitInterceptor; + +/** + * 通用配置 + * + * @author ruoyi + */ +@Configuration +public class ResourcesConfig implements WebMvcConfigurer +{ + @Autowired + private RepeatSubmitInterceptor repeatSubmitInterceptor; + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) + { + /** 本地文件上传路径 */ + registry.addResourceHandler(Constants.RESOURCE_PREFIX + "/**") + .addResourceLocations("file:" + RuoYiConfig.getProfile() + "/"); + + /** swagger配置 */ + registry.addResourceHandler("/swagger-ui/**") + .addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/") + .setCacheControl(CacheControl.maxAge(5, TimeUnit.HOURS).cachePublic()); + } + + /** + * 自定义拦截规则 + */ + @Override + public void addInterceptors(InterceptorRegistry registry) + { + registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**"); + } + + /** + * 跨域配置 + */ + @Bean + public CorsFilter corsFilter() + { + CorsConfiguration config = new CorsConfiguration(); + // 设置访问源地址 + config.addAllowedOriginPattern("*"); + // 设置访问源请求头 + config.addAllowedHeader("*"); + // 设置访问源请求方法 + config.addAllowedMethod("*"); + // 有效期 1800秒 + config.setMaxAge(1800L); + // 添加映射路径,拦截一切请求 + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + // 返回新的CorsFilter + return new CorsFilter(source); + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java new file mode 100644 index 0000000..b1231b2 --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java @@ -0,0 +1,86 @@ +package com.ruoyi.framework.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.logout.LogoutFilter; +import org.springframework.web.filter.CorsFilter; +import com.ruoyi.framework.config.properties.PermitAllUrlProperties; +import com.ruoyi.framework.security.filter.JwtAuthenticationTokenFilter; +import com.ruoyi.framework.security.handle.AuthenticationEntryPointImpl; +import com.ruoyi.framework.security.handle.LogoutSuccessHandlerImpl; + +/** + * spring security配置 + * + * @author ruoyi + */ +@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true) +@Configuration +public class SecurityConfig +{ + @Autowired + private AuthenticationEntryPointImpl unauthorizedHandler; + @Autowired + private LogoutSuccessHandlerImpl logoutSuccessHandler; + @Autowired + private JwtAuthenticationTokenFilter authenticationTokenFilter; + @Autowired + private CorsFilter corsFilter; + @Autowired + private PermitAllUrlProperties permitAllUrl; + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception + { + return authenticationConfiguration.getAuthenticationManager(); + } + + @Bean + protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception + { + return httpSecurity + .csrf(csrf -> csrf.disable()) + .headers((headersCustomizer) -> { + headersCustomizer.cacheControl(cache -> cache.disable()).frameOptions(options -> options.sameOrigin()); + }) + .exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler)) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests((requests) -> { + permitAllUrl.getUrls().forEach(url -> requests.requestMatchers(url).permitAll()); + requests.requestMatchers("/login", "/register", "/captchaImage").permitAll() + // OSS 文件代理 (PDF/图片内嵌预览, 重写 Content-Disposition 为 inline) + .requestMatchers(HttpMethod.GET, "/common/oss/proxy").permitAll() + // 业务公开门户与注册入口可匿名访问; 字典 active/单查公开, 写操作需登录+权限 + .requestMatchers("/business/public/**", "/business/auth/**", "/business/sms/**").permitAll() + // 字典 active (只读) + 按 ID 查 公开给前端拉下拉数据 + .requestMatchers(HttpMethod.GET, "/business/dict/department/active", "/business/dict/title/active", + "/business/dict/department/*", "/business/dict/title/*").permitAll() + // 项目公示/计划详情/评审公示等只读接口可匿名访问 + .requestMatchers(HttpMethod.GET, "/business/*/list", "/business/*/getInfo/**", "/business/*/getById/**").permitAll() + .requestMatchers(HttpMethod.GET, "/", "/*.html", "/**.html", "/**.css", "/**.js", "/profile/**").permitAll() + .requestMatchers("/swagger-ui.html", "/v3/api-docs/**", "/swagger-ui/**", "/druid/**").permitAll() + .anyRequest().authenticated(); + }) + .logout(logout -> logout.logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler)) + .addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class) + .addFilterBefore(corsFilter, LogoutFilter.class) + .build(); + } + + @Bean + public BCryptPasswordEncoder bCryptPasswordEncoder() + { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ServerConfig.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ServerConfig.java new file mode 100644 index 0000000..cd8fb09 --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ServerConfig.java @@ -0,0 +1,32 @@ +package com.ruoyi.framework.config; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.stereotype.Component; +import com.ruoyi.common.utils.ServletUtils; + +/** + * 服务相关配置 + * + * @author ruoyi + */ +@Component +public class ServerConfig +{ + /** + * 获取完整的请求路径,包括:域名,端口,上下文访问路径 + * + * @return 服务地址 + */ + public String getUrl() + { + HttpServletRequest request = ServletUtils.getRequest(); + return getDomain(request); + } + + public static String getDomain(HttpServletRequest request) + { + StringBuffer url = request.getRequestURL(); + String contextPath = request.getServletContext().getContextPath(); + return url.delete(url.length() - request.getRequestURI().length(), url.length()).append(contextPath).toString(); + } +} diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ThreadPoolConfig.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ThreadPoolConfig.java new file mode 100644 index 0000000..09ce6bd --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ThreadPoolConfig.java @@ -0,0 +1,63 @@ +package com.ruoyi.framework.config; + +import com.ruoyi.common.utils.Threads; +import org.apache.commons.lang3.concurrent.BasicThreadFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadPoolExecutor; + +/** + * 线程池配置 + * + * @author ruoyi + **/ +@Configuration +public class ThreadPoolConfig +{ + // 核心线程池大小 + private int corePoolSize = 50; + + // 最大可创建的线程数 + private int maxPoolSize = 200; + + // 队列最大长度 + private int queueCapacity = 1000; + + // 线程池维护线程所允许的空闲时间 + private int keepAliveSeconds = 300; + + @Bean(name = "threadPoolTaskExecutor") + public ThreadPoolTaskExecutor threadPoolTaskExecutor() + { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setMaxPoolSize(maxPoolSize); + executor.setCorePoolSize(corePoolSize); + executor.setQueueCapacity(queueCapacity); + executor.setKeepAliveSeconds(keepAliveSeconds); + // 线程池对拒绝任务(无线程可用)的处理策略 + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + return executor; + } + + /** + * 执行周期性或定时任务 + */ + @Bean(name = "scheduledExecutorService") + protected ScheduledExecutorService scheduledExecutorService() + { + return new ScheduledThreadPoolExecutor(corePoolSize, + BasicThreadFactory.builder().namingPattern("schedule-pool-%d").daemon(true).build(), + new ThreadPoolExecutor.CallerRunsPolicy()) + { + @Override + protected void afterExecute(Runnable r, Throwable t) + { + super.afterExecute(r, t); + Threads.printException(r, t); + } + }; + } +} diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/DruidProperties.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/DruidProperties.java new file mode 100644 index 0000000..c8a5c8a --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/DruidProperties.java @@ -0,0 +1,89 @@ +package com.ruoyi.framework.config.properties; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import com.alibaba.druid.pool.DruidDataSource; + +/** + * druid 配置属性 + * + * @author ruoyi + */ +@Configuration +public class DruidProperties +{ + @Value("${spring.datasource.druid.initialSize}") + private int initialSize; + + @Value("${spring.datasource.druid.minIdle}") + private int minIdle; + + @Value("${spring.datasource.druid.maxActive}") + private int maxActive; + + @Value("${spring.datasource.druid.maxWait}") + private int maxWait; + + @Value("${spring.datasource.druid.connectTimeout}") + private int connectTimeout; + + @Value("${spring.datasource.druid.socketTimeout}") + private int socketTimeout; + + @Value("${spring.datasource.druid.timeBetweenEvictionRunsMillis}") + private int timeBetweenEvictionRunsMillis; + + @Value("${spring.datasource.druid.minEvictableIdleTimeMillis}") + private int minEvictableIdleTimeMillis; + + @Value("${spring.datasource.druid.maxEvictableIdleTimeMillis}") + private int maxEvictableIdleTimeMillis; + + @Value("${spring.datasource.druid.validationQuery}") + private String validationQuery; + + @Value("${spring.datasource.druid.testWhileIdle}") + private boolean testWhileIdle; + + @Value("${spring.datasource.druid.testOnBorrow}") + private boolean testOnBorrow; + + @Value("${spring.datasource.druid.testOnReturn}") + private boolean testOnReturn; + + public DruidDataSource dataSource(DruidDataSource datasource) + { + /** 配置初始化大小、最小、最大 */ + datasource.setInitialSize(initialSize); + datasource.setMaxActive(maxActive); + datasource.setMinIdle(minIdle); + + /** 配置获取连接等待超时的时间 */ + datasource.setMaxWait(maxWait); + + /** 配置驱动连接超时时间,检测数据库建立连接的超时时间,单位是毫秒 */ + datasource.setConnectTimeout(connectTimeout); + + /** 配置网络超时时间,等待数据库操作完成的网络超时时间,单位是毫秒 */ + datasource.setSocketTimeout(socketTimeout); + + /** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */ + datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis); + + /** 配置一个连接在池中最小、最大生存的时间,单位是毫秒 */ + datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis); + datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis); + + /** + * 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。 + */ + datasource.setValidationQuery(validationQuery); + /** 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 */ + datasource.setTestWhileIdle(testWhileIdle); + /** 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */ + datasource.setTestOnBorrow(testOnBorrow); + /** 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */ + datasource.setTestOnReturn(testOnReturn); + return datasource; + } +} diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/PermitAllUrlProperties.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/PermitAllUrlProperties.java new file mode 100644 index 0000000..a7d0de2 --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/PermitAllUrlProperties.java @@ -0,0 +1,74 @@ +package com.ruoyi.framework.config.properties; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.regex.Pattern; +import org.apache.commons.lang3.RegExUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import com.ruoyi.common.annotation.Anonymous; + +/** + * 设置Anonymous注解允许匿名访问的url + * + * @author ruoyi + */ +@Configuration +public class PermitAllUrlProperties implements InitializingBean, ApplicationContextAware +{ + private static final Pattern PATTERN = Pattern.compile("\\{(.*?)\\}"); + + private ApplicationContext applicationContext; + + private List urls = new ArrayList<>(); + + public String ASTERISK = "*"; + + @SuppressWarnings("deprecation") + @Override + public void afterPropertiesSet() + { + RequestMappingHandlerMapping mapping = applicationContext.getBean("requestMappingHandlerMapping", RequestMappingHandlerMapping.class); + Map map = mapping.getHandlerMethods(); + + map.keySet().forEach(info -> { + HandlerMethod handlerMethod = map.get(info); + + // 获取方法上边的注解 替代path variable 为 * + Anonymous method = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Anonymous.class); + Optional.ofNullable(method).ifPresent(anonymous -> Objects.requireNonNull(info.getPathPatternsCondition().getPatternValues()) + .forEach(url -> urls.add(RegExUtils.replaceAll(url, PATTERN, ASTERISK)))); + + // 获取类上边的注解, 替代path variable 为 * + Anonymous controller = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), Anonymous.class); + Optional.ofNullable(controller).ifPresent(anonymous -> Objects.requireNonNull(info.getPathPatternsCondition().getPatternValues()) + .forEach(url -> urls.add(RegExUtils.replaceAll(url, PATTERN, ASTERISK)))); + }); + } + + @Override + public void setApplicationContext(ApplicationContext context) throws BeansException + { + this.applicationContext = context; + } + + public List getUrls() + { + return urls; + } + + public void setUrls(List urls) + { + this.urls = urls; + } +} diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSource.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSource.java new file mode 100644 index 0000000..e70b8cf --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSource.java @@ -0,0 +1,26 @@ +package com.ruoyi.framework.datasource; + +import java.util.Map; +import javax.sql.DataSource; +import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; + +/** + * 动态数据源 + * + * @author ruoyi + */ +public class DynamicDataSource extends AbstractRoutingDataSource +{ + public DynamicDataSource(DataSource defaultTargetDataSource, Map targetDataSources) + { + super.setDefaultTargetDataSource(defaultTargetDataSource); + super.setTargetDataSources(targetDataSources); + super.afterPropertiesSet(); + } + + @Override + protected Object determineCurrentLookupKey() + { + return DynamicDataSourceContextHolder.getDataSourceType(); + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSourceContextHolder.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSourceContextHolder.java new file mode 100644 index 0000000..9770af6 --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSourceContextHolder.java @@ -0,0 +1,45 @@ +package com.ruoyi.framework.datasource; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 数据源切换处理 + * + * @author ruoyi + */ +public class DynamicDataSourceContextHolder +{ + public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class); + + /** + * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本, + * 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。 + */ + private static final ThreadLocal CONTEXT_HOLDER = new ThreadLocal<>(); + + /** + * 设置数据源的变量 + */ + public static void setDataSourceType(String dsType) + { + log.info("切换到{}数据源", dsType); + CONTEXT_HOLDER.set(dsType); + } + + /** + * 获得数据源的变量 + */ + public static String getDataSourceType() + { + return CONTEXT_HOLDER.get(); + } + + /** + * 清空数据源变量 + */ + public static void clearDataSourceType() + { + CONTEXT_HOLDER.remove(); + } +} diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/RepeatSubmitInterceptor.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/RepeatSubmitInterceptor.java new file mode 100644 index 0000000..4e8d20f --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/RepeatSubmitInterceptor.java @@ -0,0 +1,56 @@ +package com.ruoyi.framework.interceptor; + +import java.lang.reflect.Method; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; +import com.alibaba.fastjson2.JSON; +import com.ruoyi.common.annotation.RepeatSubmit; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.utils.ServletUtils; + +/** + * 防止重复提交拦截器 + * + * @author ruoyi + */ +@Component +public abstract class RepeatSubmitInterceptor implements HandlerInterceptor +{ + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception + { + if (handler instanceof HandlerMethod) + { + HandlerMethod handlerMethod = (HandlerMethod) handler; + Method method = handlerMethod.getMethod(); + RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class); + if (annotation != null) + { + if (this.isRepeatSubmit(request, annotation)) + { + AjaxResult ajaxResult = AjaxResult.error(annotation.message()); + ServletUtils.renderString(response, JSON.toJSONString(ajaxResult)); + return false; + } + } + return true; + } + else + { + return true; + } + } + + /** + * 验证是否重复提交由子类实现具体的防重复提交的规则 + * + * @param request 请求信息 + * @param annotation 防重复注解参数 + * @return 结果 + * @throws Exception + */ + public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation); +} diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/impl/SameUrlDataInterceptor.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/impl/SameUrlDataInterceptor.java new file mode 100644 index 0000000..c93df50 --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/impl/SameUrlDataInterceptor.java @@ -0,0 +1,110 @@ +package com.ruoyi.framework.interceptor.impl; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import com.alibaba.fastjson2.JSON; +import com.ruoyi.common.annotation.RepeatSubmit; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.filter.RepeatedlyRequestWrapper; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.http.HttpHelper; +import com.ruoyi.framework.interceptor.RepeatSubmitInterceptor; + +/** + * 判断请求url和数据是否和上一次相同, + * 如果和上次相同,则是重复提交表单。 有效时间为10秒内。 + * + * @author ruoyi + */ +@Component +public class SameUrlDataInterceptor extends RepeatSubmitInterceptor +{ + public final String REPEAT_PARAMS = "repeatParams"; + + public final String REPEAT_TIME = "repeatTime"; + + // 令牌自定义标识 + @Value("${token.header}") + private String header; + + @Autowired + private RedisCache redisCache; + + @SuppressWarnings("unchecked") + @Override + public boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation) + { + String nowParams = ""; + if (request instanceof RepeatedlyRequestWrapper) + { + RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request; + nowParams = HttpHelper.getBodyString(repeatedlyRequest); + } + + // body参数为空,获取Parameter的数据 + if (StringUtils.isEmpty(nowParams)) + { + nowParams = JSON.toJSONString(request.getParameterMap()); + } + Map nowDataMap = new HashMap(); + nowDataMap.put(REPEAT_PARAMS, nowParams); + nowDataMap.put(REPEAT_TIME, System.currentTimeMillis()); + + // 请求地址(作为存放cache的key值) + String url = request.getRequestURI(); + + // 唯一值(没有消息头则使用请求地址) + String submitKey = StringUtils.trimToEmpty(request.getHeader(header)); + + // 唯一标识(指定key + url + 消息头) + String cacheRepeatKey = CacheConstants.REPEAT_SUBMIT_KEY + url + submitKey; + + Object sessionObj = redisCache.getCacheObject(cacheRepeatKey); + if (sessionObj != null) + { + Map sessionMap = (Map) sessionObj; + if (sessionMap.containsKey(url)) + { + Map preDataMap = (Map) sessionMap.get(url); + if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap, annotation.interval())) + { + return true; + } + } + } + Map cacheMap = new HashMap(); + cacheMap.put(url, nowDataMap); + redisCache.setCacheObject(cacheRepeatKey, cacheMap, annotation.interval(), TimeUnit.MILLISECONDS); + return false; + } + + /** + * 判断参数是否相同 + */ + private boolean compareParams(Map nowMap, Map preMap) + { + String nowParams = (String) nowMap.get(REPEAT_PARAMS); + String preParams = (String) preMap.get(REPEAT_PARAMS); + return nowParams.equals(preParams); + } + + /** + * 判断两次间隔时间 + */ + private boolean compareTime(Map nowMap, Map preMap, int interval) + { + long time1 = (Long) nowMap.get(REPEAT_TIME); + long time2 = (Long) preMap.get(REPEAT_TIME); + if ((time1 - time2) < interval) + { + return true; + } + return false; + } +} diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/AsyncManager.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/AsyncManager.java new file mode 100644 index 0000000..7387a02 --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/AsyncManager.java @@ -0,0 +1,55 @@ +package com.ruoyi.framework.manager; + +import java.util.TimerTask; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import com.ruoyi.common.utils.Threads; +import com.ruoyi.common.utils.spring.SpringUtils; + +/** + * 异步任务管理器 + * + * @author ruoyi + */ +public class AsyncManager +{ + /** + * 操作延迟10毫秒 + */ + private final int OPERATE_DELAY_TIME = 10; + + /** + * 异步操作任务调度线程池 + */ + private ScheduledExecutorService executor = SpringUtils.getBean("scheduledExecutorService"); + + /** + * 单例模式 + */ + private AsyncManager(){} + + private static AsyncManager me = new AsyncManager(); + + public static AsyncManager me() + { + return me; + } + + /** + * 执行任务 + * + * @param task 任务 + */ + public void execute(TimerTask task) + { + executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS); + } + + /** + * 停止任务线程池 + */ + public void shutdown() + { + Threads.shutdownAndAwaitTermination(executor); + } +} diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/ShutdownManager.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/ShutdownManager.java new file mode 100644 index 0000000..095b865 --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/ShutdownManager.java @@ -0,0 +1,39 @@ +package com.ruoyi.framework.manager; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import jakarta.annotation.PreDestroy; + +/** + * 确保应用退出时能关闭后台线程 + * + * @author ruoyi + */ +@Component +public class ShutdownManager +{ + private static final Logger logger = LoggerFactory.getLogger("sys-user"); + + @PreDestroy + public void destroy() + { + shutdownAsyncManager(); + } + + /** + * 停止异步执行任务 + */ + private void shutdownAsyncManager() + { + try + { + logger.info("====关闭后台任务任务线程池===="); + AsyncManager.me().shutdown(); + } + catch (Exception e) + { + logger.error(e.getMessage(), e); + } + } +} diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/factory/AsyncFactory.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/factory/AsyncFactory.java new file mode 100644 index 0000000..b20ad92 --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/factory/AsyncFactory.java @@ -0,0 +1,102 @@ +package com.ruoyi.framework.manager.factory; + +import java.util.TimerTask; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.utils.LogUtils; +import com.ruoyi.common.utils.ServletUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.http.UserAgentUtils; +import com.ruoyi.common.utils.ip.AddressUtils; +import com.ruoyi.common.utils.ip.IpUtils; +import com.ruoyi.common.utils.spring.SpringUtils; +import com.ruoyi.system.domain.SysLogininfor; +import com.ruoyi.system.domain.SysOperLog; +import com.ruoyi.system.service.ISysLogininforService; +import com.ruoyi.system.service.ISysOperLogService; + +/** + * 异步工厂(产生任务用) + * + * @author ruoyi + */ +public class AsyncFactory +{ + private static final Logger sys_user_logger = LoggerFactory.getLogger("sys-user"); + + /** + * 记录登录信息 + * + * @param username 用户名 + * @param status 状态 + * @param message 消息 + * @param args 列表 + * @return 任务task + */ + public static TimerTask recordLogininfor(final String username, final String status, final String message, + final Object... args) + { + final String userAgent = ServletUtils.getRequest().getHeader("User-Agent"); + final String ip = IpUtils.getIpAddr(); + return new TimerTask() + { + @Override + public void run() + { + String address = AddressUtils.getRealAddressByIP(ip); + StringBuilder s = new StringBuilder(); + s.append(LogUtils.getBlock(ip)); + s.append(address); + s.append(LogUtils.getBlock(username)); + s.append(LogUtils.getBlock(status)); + s.append(LogUtils.getBlock(message)); + // 打印信息到日志 + sys_user_logger.info(s.toString(), args); + // 获取客户端操作系统 + String os = UserAgentUtils.getOperatingSystem(userAgent); + // 获取客户端浏览器 + String browser = UserAgentUtils.getBrowser(userAgent); + // 封装对象 + SysLogininfor logininfor = new SysLogininfor(); + logininfor.setUserName(username); + logininfor.setIpaddr(ip); + logininfor.setLoginLocation(address); + logininfor.setBrowser(browser); + logininfor.setOs(os); + logininfor.setMsg(message); + // 日志状态 + if (StringUtils.equalsAny(status, Constants.LOGIN_SUCCESS, Constants.LOGOUT, Constants.REGISTER)) + { + logininfor.setStatus(Constants.SUCCESS); + } + else if (Constants.LOGIN_FAIL.equals(status)) + { + logininfor.setStatus(Constants.FAIL); + } + // 插入数据 + SpringUtils.getBean(ISysLogininforService.class).insertLogininfor(logininfor); + } + }; + } + + /** + * 操作日志记录 + * + * @param operLog 操作日志信息 + * @return 任务task + */ + public static TimerTask recordOper(final SysOperLog operLog) + { + return new TimerTask() + { + @Override + public void run() + { + // 远程查询操作地点 + operLog.setOperLocation(AddressUtils.getRealAddressByIP(operLog.getOperIp())); + SpringUtils.getBean(ISysOperLogService.class).insertOperlog(operLog); + } + }; + } +} diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/AuthenticationContextHolder.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/AuthenticationContextHolder.java new file mode 100644 index 0000000..6c776ce --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/AuthenticationContextHolder.java @@ -0,0 +1,28 @@ +package com.ruoyi.framework.security.context; + +import org.springframework.security.core.Authentication; + +/** + * 身份验证信息 + * + * @author ruoyi + */ +public class AuthenticationContextHolder +{ + private static final ThreadLocal contextHolder = new ThreadLocal<>(); + + public static Authentication getContext() + { + return contextHolder.get(); + } + + public static void setContext(Authentication context) + { + contextHolder.set(context); + } + + public static void clearContext() + { + contextHolder.remove(); + } +} diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/PermissionContextHolder.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/PermissionContextHolder.java new file mode 100644 index 0000000..5472f3d --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/PermissionContextHolder.java @@ -0,0 +1,27 @@ +package com.ruoyi.framework.security.context; + +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import com.ruoyi.common.core.text.Convert; + +/** + * 权限信息 + * + * @author ruoyi + */ +public class PermissionContextHolder +{ + private static final String PERMISSION_CONTEXT_ATTRIBUTES = "PERMISSION_CONTEXT"; + + public static void setContext(String permission) + { + RequestContextHolder.currentRequestAttributes().setAttribute(PERMISSION_CONTEXT_ATTRIBUTES, permission, + RequestAttributes.SCOPE_REQUEST); + } + + public static String getContext() + { + return Convert.toStr(RequestContextHolder.currentRequestAttributes().getAttribute(PERMISSION_CONTEXT_ATTRIBUTES, + RequestAttributes.SCOPE_REQUEST)); + } +} diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.java new file mode 100644 index 0000000..9d2f494 --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.java @@ -0,0 +1,44 @@ +package com.ruoyi.framework.security.filter; + +import java.io.IOException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +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.web.filter.OncePerRequestFilter; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.web.service.TokenService; + +/** + * token过滤器 验证token有效性 + * + * @author ruoyi + */ +@Component +public class JwtAuthenticationTokenFilter extends OncePerRequestFilter +{ + @Autowired + private TokenService tokenService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException + { + LoginUser loginUser = tokenService.getLoginUser(request); + if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())) + { + tokenService.verifyToken(loginUser); + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); + authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authenticationToken); + } + chain.doFilter(request, response); + } +} diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/AuthenticationEntryPointImpl.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/AuthenticationEntryPointImpl.java new file mode 100644 index 0000000..e1789f8 --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/AuthenticationEntryPointImpl.java @@ -0,0 +1,34 @@ +package com.ruoyi.framework.security.handle; + +import java.io.IOException; +import java.io.Serializable; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; +import com.alibaba.fastjson2.JSON; +import com.ruoyi.common.constant.HttpStatus; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.utils.ServletUtils; +import com.ruoyi.common.utils.StringUtils; + +/** + * 认证失败处理类 返回未授权 + * + * @author ruoyi + */ +@Component +public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable +{ + private static final long serialVersionUID = -8970718410437077606L; + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) + throws IOException + { + int code = HttpStatus.UNAUTHORIZED; + String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI()); + ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg))); + } +} diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/LogoutSuccessHandlerImpl.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/LogoutSuccessHandlerImpl.java new file mode 100644 index 0000000..595bd3f --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/LogoutSuccessHandlerImpl.java @@ -0,0 +1,53 @@ +package com.ruoyi.framework.security.handle; + +import java.io.IOException; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; +import com.alibaba.fastjson2.JSON; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.utils.MessageUtils; +import com.ruoyi.common.utils.ServletUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.manager.AsyncManager; +import com.ruoyi.framework.manager.factory.AsyncFactory; +import com.ruoyi.framework.web.service.TokenService; + +/** + * 自定义退出处理类 返回成功 + * + * @author ruoyi + */ +@Configuration +public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler +{ + @Autowired + private TokenService tokenService; + + /** + * 退出处理 + * + * @return + */ + @Override + public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) + throws IOException, ServletException + { + LoginUser loginUser = tokenService.getLoginUser(request); + if (StringUtils.isNotNull(loginUser)) + { + String userName = loginUser.getUsername(); + // 删除用户缓存记录 + tokenService.delLoginUser(loginUser.getToken()); + // 记录用户退出日志 + AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, MessageUtils.message("user.logout.success"))); + } + ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.success(MessageUtils.message("user.logout.success")))); + } +} diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/Server.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/Server.java new file mode 100644 index 0000000..63b03da --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/Server.java @@ -0,0 +1,240 @@ +package com.ruoyi.framework.web.domain; + +import java.net.UnknownHostException; +import java.util.LinkedList; +import java.util.List; +import java.util.Properties; +import com.ruoyi.common.utils.Arith; +import com.ruoyi.common.utils.ip.IpUtils; +import com.ruoyi.framework.web.domain.server.Cpu; +import com.ruoyi.framework.web.domain.server.Jvm; +import com.ruoyi.framework.web.domain.server.Mem; +import com.ruoyi.framework.web.domain.server.Sys; +import com.ruoyi.framework.web.domain.server.SysFile; +import oshi.SystemInfo; +import oshi.hardware.CentralProcessor; +import oshi.hardware.CentralProcessor.TickType; +import oshi.hardware.GlobalMemory; +import oshi.hardware.HardwareAbstractionLayer; +import oshi.software.os.FileSystem; +import oshi.software.os.OSFileStore; +import oshi.software.os.OperatingSystem; +import oshi.util.Util; + +/** + * 服务器相关信息 + * + * @author ruoyi + */ +public class Server +{ + private static final int OSHI_WAIT_SECOND = 1000; + + /** + * CPU相关信息 + */ + private Cpu cpu = new Cpu(); + + /** + * 內存相关信息 + */ + private Mem mem = new Mem(); + + /** + * JVM相关信息 + */ + private Jvm jvm = new Jvm(); + + /** + * 服务器相关信息 + */ + private Sys sys = new Sys(); + + /** + * 磁盘相关信息 + */ + private List sysFiles = new LinkedList(); + + public Cpu getCpu() + { + return cpu; + } + + public void setCpu(Cpu cpu) + { + this.cpu = cpu; + } + + public Mem getMem() + { + return mem; + } + + public void setMem(Mem mem) + { + this.mem = mem; + } + + public Jvm getJvm() + { + return jvm; + } + + public void setJvm(Jvm jvm) + { + this.jvm = jvm; + } + + public Sys getSys() + { + return sys; + } + + public void setSys(Sys sys) + { + this.sys = sys; + } + + public List getSysFiles() + { + return sysFiles; + } + + public void setSysFiles(List sysFiles) + { + this.sysFiles = sysFiles; + } + + public void copyTo() throws Exception + { + SystemInfo si = new SystemInfo(); + HardwareAbstractionLayer hal = si.getHardware(); + + setCpuInfo(hal.getProcessor()); + + setMemInfo(hal.getMemory()); + + setSysInfo(); + + setJvmInfo(); + + setSysFiles(si.getOperatingSystem()); + } + + /** + * 设置CPU信息 + */ + private void setCpuInfo(CentralProcessor processor) + { + // CPU信息 + long[] prevTicks = processor.getSystemCpuLoadTicks(); + Util.sleep(OSHI_WAIT_SECOND); + long[] ticks = processor.getSystemCpuLoadTicks(); + long nice = ticks[TickType.NICE.getIndex()] - prevTicks[TickType.NICE.getIndex()]; + long irq = ticks[TickType.IRQ.getIndex()] - prevTicks[TickType.IRQ.getIndex()]; + long softirq = ticks[TickType.SOFTIRQ.getIndex()] - prevTicks[TickType.SOFTIRQ.getIndex()]; + long steal = ticks[TickType.STEAL.getIndex()] - prevTicks[TickType.STEAL.getIndex()]; + long cSys = ticks[TickType.SYSTEM.getIndex()] - prevTicks[TickType.SYSTEM.getIndex()]; + long user = ticks[TickType.USER.getIndex()] - prevTicks[TickType.USER.getIndex()]; + long iowait = ticks[TickType.IOWAIT.getIndex()] - prevTicks[TickType.IOWAIT.getIndex()]; + long idle = ticks[TickType.IDLE.getIndex()] - prevTicks[TickType.IDLE.getIndex()]; + long totalCpu = user + nice + cSys + idle + iowait + irq + softirq + steal; + cpu.setCpuNum(processor.getLogicalProcessorCount()); + cpu.setTotal(totalCpu); + cpu.setSys(cSys); + cpu.setUsed(user); + cpu.setWait(iowait); + cpu.setFree(idle); + } + + /** + * 设置内存信息 + */ + private void setMemInfo(GlobalMemory memory) + { + mem.setTotal(memory.getTotal()); + mem.setUsed(memory.getTotal() - memory.getAvailable()); + mem.setFree(memory.getAvailable()); + } + + /** + * 设置服务器信息 + */ + private void setSysInfo() + { + Properties props = System.getProperties(); + sys.setComputerName(IpUtils.getHostName()); + sys.setComputerIp(IpUtils.getHostIp()); + sys.setOsName(props.getProperty("os.name")); + sys.setOsArch(props.getProperty("os.arch")); + sys.setUserDir(props.getProperty("user.dir")); + } + + /** + * 设置Java虚拟机 + */ + private void setJvmInfo() throws UnknownHostException + { + Properties props = System.getProperties(); + jvm.setTotal(Runtime.getRuntime().totalMemory()); + jvm.setMax(Runtime.getRuntime().maxMemory()); + jvm.setFree(Runtime.getRuntime().freeMemory()); + jvm.setVersion(props.getProperty("java.version")); + jvm.setHome(props.getProperty("java.home")); + } + + /** + * 设置磁盘信息 + */ + private void setSysFiles(OperatingSystem os) + { + FileSystem fileSystem = os.getFileSystem(); + List fsArray = fileSystem.getFileStores(); + for (OSFileStore fs : fsArray) + { + long free = fs.getUsableSpace(); + long total = fs.getTotalSpace(); + long used = total - free; + SysFile sysFile = new SysFile(); + sysFile.setDirName(fs.getMount()); + sysFile.setSysTypeName(fs.getType()); + sysFile.setTypeName(fs.getName()); + sysFile.setTotal(convertFileSize(total)); + sysFile.setFree(convertFileSize(free)); + sysFile.setUsed(convertFileSize(used)); + sysFile.setUsage(Arith.mul(Arith.div(used, total, 4), 100)); + sysFiles.add(sysFile); + } + } + + /** + * 字节转换 + * + * @param size 字节大小 + * @return 转换后值 + */ + public String convertFileSize(long size) + { + long kb = 1024; + long mb = kb * 1024; + long gb = mb * 1024; + if (size >= gb) + { + return String.format("%.1f GB", (float) size / gb); + } + else if (size >= mb) + { + float f = (float) size / mb; + return String.format(f > 100 ? "%.0f MB" : "%.1f MB", f); + } + else if (size >= kb) + { + float f = (float) size / kb; + return String.format(f > 100 ? "%.0f KB" : "%.1f KB", f); + } + else + { + return String.format("%d B", size); + } + } +} diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Cpu.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Cpu.java new file mode 100644 index 0000000..a13a66c --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Cpu.java @@ -0,0 +1,101 @@ +package com.ruoyi.framework.web.domain.server; + +import com.ruoyi.common.utils.Arith; + +/** + * CPU相关信息 + * + * @author ruoyi + */ +public class Cpu +{ + /** + * 核心数 + */ + private int cpuNum; + + /** + * CPU总的使用率 + */ + private double total; + + /** + * CPU系统使用率 + */ + private double sys; + + /** + * CPU用户使用率 + */ + private double used; + + /** + * CPU当前等待率 + */ + private double wait; + + /** + * CPU当前空闲率 + */ + private double free; + + public int getCpuNum() + { + return cpuNum; + } + + public void setCpuNum(int cpuNum) + { + this.cpuNum = cpuNum; + } + + public double getTotal() + { + return Arith.round(Arith.mul(total, 100), 2); + } + + public void setTotal(double total) + { + this.total = total; + } + + public double getSys() + { + return Arith.round(Arith.mul(sys / total, 100), 2); + } + + public void setSys(double sys) + { + this.sys = sys; + } + + public double getUsed() + { + return Arith.round(Arith.mul(used / total, 100), 2); + } + + public void setUsed(double used) + { + this.used = used; + } + + public double getWait() + { + return Arith.round(Arith.mul(wait / total, 100), 2); + } + + public void setWait(double wait) + { + this.wait = wait; + } + + public double getFree() + { + return Arith.round(Arith.mul(free / total, 100), 2); + } + + public void setFree(double free) + { + this.free = free; + } +} diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Jvm.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Jvm.java new file mode 100644 index 0000000..1fdc6ac --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Jvm.java @@ -0,0 +1,130 @@ +package com.ruoyi.framework.web.domain.server; + +import java.lang.management.ManagementFactory; +import com.ruoyi.common.utils.Arith; +import com.ruoyi.common.utils.DateUtils; + +/** + * JVM相关信息 + * + * @author ruoyi + */ +public class Jvm +{ + /** + * 当前JVM占用的内存总数(M) + */ + private double total; + + /** + * JVM最大可用内存总数(M) + */ + private double max; + + /** + * JVM空闲内存(M) + */ + private double free; + + /** + * JDK版本 + */ + private String version; + + /** + * JDK路径 + */ + private String home; + + public double getTotal() + { + return Arith.div(total, (1024 * 1024), 2); + } + + public void setTotal(double total) + { + this.total = total; + } + + public double getMax() + { + return Arith.div(max, (1024 * 1024), 2); + } + + public void setMax(double max) + { + this.max = max; + } + + public double getFree() + { + return Arith.div(free, (1024 * 1024), 2); + } + + public void setFree(double free) + { + this.free = free; + } + + public double getUsed() + { + return Arith.div(total - free, (1024 * 1024), 2); + } + + public double getUsage() + { + return Arith.mul(Arith.div(total - free, total, 4), 100); + } + + /** + * 获取JDK名称 + */ + public String getName() + { + return ManagementFactory.getRuntimeMXBean().getVmName(); + } + + public String getVersion() + { + return version; + } + + public void setVersion(String version) + { + this.version = version; + } + + public String getHome() + { + return home; + } + + public void setHome(String home) + { + this.home = home; + } + + /** + * JDK启动时间 + */ + public String getStartTime() + { + return DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, DateUtils.getServerStartDate()); + } + + /** + * JDK运行时间 + */ + public String getRunTime() + { + return DateUtils.timeDistance(DateUtils.getNowDate(), DateUtils.getServerStartDate()); + } + + /** + * 运行参数 + */ + public String getInputArgs() + { + return ManagementFactory.getRuntimeMXBean().getInputArguments().toString(); + } +} diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Mem.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Mem.java new file mode 100644 index 0000000..13eec52 --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Mem.java @@ -0,0 +1,61 @@ +package com.ruoyi.framework.web.domain.server; + +import com.ruoyi.common.utils.Arith; + +/** + * 內存相关信息 + * + * @author ruoyi + */ +public class Mem +{ + /** + * 内存总量 + */ + private double total; + + /** + * 已用内存 + */ + private double used; + + /** + * 剩余内存 + */ + private double free; + + public double getTotal() + { + return Arith.div(total, (1024 * 1024 * 1024), 2); + } + + public void setTotal(long total) + { + this.total = total; + } + + public double getUsed() + { + return Arith.div(used, (1024 * 1024 * 1024), 2); + } + + public void setUsed(long used) + { + this.used = used; + } + + public double getFree() + { + return Arith.div(free, (1024 * 1024 * 1024), 2); + } + + public void setFree(long free) + { + this.free = free; + } + + public double getUsage() + { + return Arith.mul(Arith.div(used, total, 4), 100); + } +} diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Sys.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Sys.java new file mode 100644 index 0000000..45d64d9 --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Sys.java @@ -0,0 +1,84 @@ +package com.ruoyi.framework.web.domain.server; + +/** + * 系统相关信息 + * + * @author ruoyi + */ +public class Sys +{ + /** + * 服务器名称 + */ + private String computerName; + + /** + * 服务器Ip + */ + private String computerIp; + + /** + * 项目路径 + */ + private String userDir; + + /** + * 操作系统 + */ + private String osName; + + /** + * 系统架构 + */ + private String osArch; + + public String getComputerName() + { + return computerName; + } + + public void setComputerName(String computerName) + { + this.computerName = computerName; + } + + public String getComputerIp() + { + return computerIp; + } + + public void setComputerIp(String computerIp) + { + this.computerIp = computerIp; + } + + public String getUserDir() + { + return userDir; + } + + public void setUserDir(String userDir) + { + this.userDir = userDir; + } + + public String getOsName() + { + return osName; + } + + public void setOsName(String osName) + { + this.osName = osName; + } + + public String getOsArch() + { + return osArch; + } + + public void setOsArch(String osArch) + { + this.osArch = osArch; + } +} diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/SysFile.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/SysFile.java new file mode 100644 index 0000000..1320cde --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/SysFile.java @@ -0,0 +1,114 @@ +package com.ruoyi.framework.web.domain.server; + +/** + * 系统文件相关信息 + * + * @author ruoyi + */ +public class SysFile +{ + /** + * 盘符路径 + */ + private String dirName; + + /** + * 盘符类型 + */ + private String sysTypeName; + + /** + * 文件类型 + */ + private String typeName; + + /** + * 总大小 + */ + private String total; + + /** + * 剩余大小 + */ + private String free; + + /** + * 已经使用量 + */ + private String used; + + /** + * 资源的使用率 + */ + private double usage; + + public String getDirName() + { + return dirName; + } + + public void setDirName(String dirName) + { + this.dirName = dirName; + } + + public String getSysTypeName() + { + return sysTypeName; + } + + public void setSysTypeName(String sysTypeName) + { + this.sysTypeName = sysTypeName; + } + + public String getTypeName() + { + return typeName; + } + + public void setTypeName(String typeName) + { + this.typeName = typeName; + } + + public String getTotal() + { + return total; + } + + public void setTotal(String total) + { + this.total = total; + } + + public String getFree() + { + return free; + } + + public void setFree(String free) + { + this.free = free; + } + + public String getUsed() + { + return used; + } + + public void setUsed(String used) + { + this.used = used; + } + + public double getUsage() + { + return usage; + } + + public void setUsage(double usage) + { + this.usage = usage; + } +} diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..f228ca0 --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java @@ -0,0 +1,201 @@ +package com.ruoyi.framework.web.exception; + +import jakarta.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.validation.BindException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingPathVariableException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import com.ruoyi.common.constant.HttpStatus; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.exception.DemoModeException; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.html.EscapeUtil; + +/** + * 全局异常处理器 + * + * @author ruoyi + */ +@RestControllerAdvice +public class GlobalExceptionHandler +{ + private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); + + /** + * 权限校验异常 + */ + @ExceptionHandler(AccessDeniedException.class) + public AjaxResult handleAccessDeniedException(AccessDeniedException e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',权限校验失败'{}'", requestURI, e.getMessage()); + return AjaxResult.error(HttpStatus.FORBIDDEN, "没有权限,请联系管理员授权"); + } + + /** + * 请求方式不支持 + */ + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public AjaxResult handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e, + HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',不支持'{}'请求", requestURI, e.getMethod()); + return AjaxResult.error(e.getMessage()); + } + + /** + * 业务异常 + */ + @ExceptionHandler(ServiceException.class) + public AjaxResult handleServiceException(ServiceException e, HttpServletRequest request) + { + log.error(e.getMessage(), e); + Integer code = e.getCode(); + return StringUtils.isNotNull(code) ? AjaxResult.error(code, e.getMessage()) : AjaxResult.error(e.getMessage()); + } + + /** + * 请求路径中缺少必需的路径变量 + */ + @ExceptionHandler(MissingPathVariableException.class) + public AjaxResult handleMissingPathVariableException(MissingPathVariableException e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求路径中缺少必需的路径变量'{}',发生系统异常.", requestURI, e); + return AjaxResult.error(String.format("请求路径中缺少必需的路径变量[%s]", e.getVariableName())); + } + + /** + * 请求参数类型不匹配 + */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public AjaxResult handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + String value = Convert.toStr(e.getValue()); + if (StringUtils.isNotEmpty(value)) + { + value = EscapeUtil.clean(value); + } + log.error("请求参数类型不匹配'{}',发生系统异常.", requestURI, e); + return AjaxResult.error(String.format("请求参数类型不匹配,参数[%s]要求类型为:'%s',但输入值为:'%s'", e.getName(), e.getRequiredType().getName(), value)); + } + + /** + * 数据库唯一约束冲突 (uk_xxx), 转友好提示. + * SQL 格式: Duplicate entry 'xxx' for key 'uk_yyy' + */ + @ExceptionHandler(DuplicateKeyException.class) + public AjaxResult handleDuplicateKeyException(DuplicateKeyException e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + String msg = e.getMessage(); + String friendly = parseDuplicateKeyMessage(msg, requestURI); + log.warn("请求地址'{}',数据重复. SQL: {}", requestURI, msg); + return AjaxResult.error(HttpStatus.BAD_REQUEST, friendly); + } + + /** + * 拦截未知的运行时异常 + */ + @ExceptionHandler(RuntimeException.class) + public AjaxResult handleRuntimeException(RuntimeException e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',发生未知异常.", requestURI, e); + return AjaxResult.error(e.getMessage()); + } + + /** + * 系统异常 + */ + @ExceptionHandler(Exception.class) + public AjaxResult handleException(Exception e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',发生系统异常.", requestURI, e); + return AjaxResult.error(e.getMessage()); + } + + /** + * 自定义验证异常 + */ + @ExceptionHandler(BindException.class) + public AjaxResult handleBindException(BindException e) + { + log.error(e.getMessage(), e); + String message = e.getAllErrors().get(0).getDefaultMessage(); + return AjaxResult.error(message); + } + + /** + * 自定义验证异常 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public Object handleMethodArgumentNotValidException(MethodArgumentNotValidException e) + { + log.error(e.getMessage(), e); + String message = e.getBindingResult().getFieldError().getDefaultMessage(); + return AjaxResult.error(message); + } + + /** + * 演示模式异常 + */ + @ExceptionHandler(DemoModeException.class) + public AjaxResult handleDemoModeException(DemoModeException e) + { + return AjaxResult.error("演示模式,不允许操作"); + } + + /** + * 解析 SQL 重复键错误信息, 转为友好中文提示. + */ + private String parseDuplicateKeyMessage(String sql, String requestURI) + { + if (sql == null) return "数据重复, 请检查后重试"; + String value = null; + String key = null; + try { + int idx1 = sql.indexOf("'"); + int idx2 = sql.indexOf("'", idx1 + 1); + if (idx1 >= 0 && idx2 > idx1) value = sql.substring(idx1 + 1, idx2); + int idxKey = sql.indexOf("key '"); + int idxKey2 = sql.indexOf("'", idxKey + 5); + if (idxKey >= 0 && idxKey2 > idxKey) key = sql.substring(idxKey + 5, idxKey2); + } catch (Exception ignore) {} + + java.util.Map fieldMap = new java.util.HashMap<>(); + fieldMap.put("project_no", "项目编号"); + fieldMap.put("project_name", "项目名称"); + fieldMap.put("project_id", "项目"); + fieldMap.put("ann_title", "公告标题"); + fieldMap.put("meeting_no", "会议编号"); + fieldMap.put("expert_id_card", "身份证号"); + fieldMap.put("unit_name", "单位名称"); + fieldMap.put("org_name", "机构名称"); + + if (key != null && key.startsWith("uk_")) { + String field = key.substring(3); + String fieldName = fieldMap.getOrDefault(field, field); + if (value != null && !value.isEmpty()) { + return String.format("%s '%s' 已存在, 请使用其他值", fieldName, value); + } + return fieldName + " 已存在, 请使用其他值"; + } + if (key != null && key.startsWith("pk_")) { + return "记录已存在"; + } + return value != null ? "数据重复: " + value : "数据重复, 请检查后重试"; + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/PermissionService.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/PermissionService.java new file mode 100644 index 0000000..fa0dfef --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/PermissionService.java @@ -0,0 +1,159 @@ +package com.ruoyi.framework.web.service; + +import java.util.Set; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.security.context.PermissionContextHolder; + +/** + * RuoYi首创 自定义权限实现,ss取自SpringSecurity首字母 + * + * @author ruoyi + */ +@Service("ss") +public class PermissionService +{ + /** + * 验证用户是否具备某权限 + * + * @param permission 权限字符串 + * @return 用户是否具备某权限 + */ + public boolean hasPermi(String permission) + { + if (StringUtils.isEmpty(permission)) + { + return false; + } + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())) + { + return false; + } + PermissionContextHolder.setContext(permission); + return hasPermissions(loginUser.getPermissions(), permission); + } + + /** + * 验证用户是否不具备某权限,与 hasPermi逻辑相反 + * + * @param permission 权限字符串 + * @return 用户是否不具备某权限 + */ + public boolean lacksPermi(String permission) + { + return hasPermi(permission) != true; + } + + /** + * 验证用户是否具有以下任意一个权限 + * + * @param permissions 以 PERMISSION_DELIMITER 为分隔符的权限列表 + * @return 用户是否具有以下任意一个权限 + */ + public boolean hasAnyPermi(String permissions) + { + if (StringUtils.isEmpty(permissions)) + { + return false; + } + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())) + { + return false; + } + PermissionContextHolder.setContext(permissions); + Set authorities = loginUser.getPermissions(); + for (String permission : permissions.split(Constants.PERMISSION_DELIMITER)) + { + if (permission != null && hasPermissions(authorities, permission)) + { + return true; + } + } + return false; + } + + /** + * 判断用户是否拥有某个角色 + * + * @param role 角色字符串 + * @return 用户是否具备某角色 + */ + public boolean hasRole(String role) + { + if (StringUtils.isEmpty(role)) + { + return false; + } + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles())) + { + return false; + } + for (SysRole sysRole : loginUser.getUser().getRoles()) + { + String roleKey = sysRole.getRoleKey(); + if (Constants.SUPER_ADMIN.equals(roleKey) || roleKey.equals(StringUtils.trim(role))) + { + return true; + } + } + return false; + } + + /** + * 验证用户是否不具备某角色,与 isRole逻辑相反。 + * + * @param role 角色名称 + * @return 用户是否不具备某角色 + */ + public boolean lacksRole(String role) + { + return hasRole(role) != true; + } + + /** + * 验证用户是否具有以下任意一个角色 + * + * @param roles 以 ROLE_DELIMITER 为分隔符的角色列表 + * @return 用户是否具有以下任意一个角色 + */ + public boolean hasAnyRoles(String roles) + { + if (StringUtils.isEmpty(roles)) + { + return false; + } + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles())) + { + return false; + } + for (String role : roles.split(Constants.ROLE_DELIMITER)) + { + if (hasRole(role)) + { + return true; + } + } + return false; + } + + /** + * 判断是否包含权限 + * + * @param permissions 权限列表 + * @param permission 权限字符串 + * @return 用户是否具备某权限 + */ + private boolean hasPermissions(Set permissions, String permission) + { + return permissions.contains(Constants.ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission)); + } +} diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java new file mode 100644 index 0000000..8a77ead --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java @@ -0,0 +1,176 @@ +package com.ruoyi.framework.web.service; + +import jakarta.annotation.Resource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.exception.user.BlackListException; +import com.ruoyi.common.exception.user.CaptchaException; +import com.ruoyi.common.exception.user.CaptchaExpireException; +import com.ruoyi.common.exception.user.UserNotExistsException; +import com.ruoyi.common.exception.user.UserPasswordNotMatchException; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.MessageUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.ip.IpUtils; +import com.ruoyi.framework.manager.AsyncManager; +import com.ruoyi.framework.manager.factory.AsyncFactory; +import com.ruoyi.framework.security.context.AuthenticationContextHolder; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.system.service.ISysUserService; + +/** + * 登录校验方法 + * + * @author ruoyi + */ +@Component +public class SysLoginService +{ + @Autowired + private TokenService tokenService; + + @Resource + private AuthenticationManager authenticationManager; + + @Autowired + private RedisCache redisCache; + + @Autowired + private ISysUserService userService; + + @Autowired + private ISysConfigService configService; + + /** + * 登录验证 + * + * @param username 用户名 + * @param password 密码 + * @param code 验证码 + * @param uuid 唯一标识 + * @return 结果 + */ + public String login(String username, String password, String code, String uuid) + { + // 验证码校验 + validateCaptcha(username, code, uuid); + // 登录前置校验 + loginPreCheck(username, password); + // 用户验证 + Authentication authentication = null; + try + { + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password); + AuthenticationContextHolder.setContext(authenticationToken); + // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername + authentication = authenticationManager.authenticate(authenticationToken); + } + catch (Exception e) + { + if (e instanceof BadCredentialsException) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"))); + throw new UserPasswordNotMatchException(); + } + else + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage())); + throw new ServiceException(e.getMessage()); + } + } + finally + { + AuthenticationContextHolder.clearContext(); + } + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"))); + LoginUser loginUser = (LoginUser) authentication.getPrincipal(); + recordLoginInfo(loginUser.getUserId()); + // 生成token + return tokenService.createToken(loginUser); + } + + /** + * 校验验证码 + * + * @param username 用户名 + * @param code 验证码 + * @param uuid 唯一标识 + * @return 结果 + */ + public void validateCaptcha(String username, String code, String uuid) + { + boolean captchaEnabled = configService.selectCaptchaEnabled(); + if (captchaEnabled) + { + String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, ""); + String captcha = redisCache.getCacheObject(verifyKey); + if (captcha == null) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"))); + throw new CaptchaExpireException(); + } + redisCache.deleteObject(verifyKey); + if (!code.equalsIgnoreCase(captcha)) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"))); + throw new CaptchaException(); + } + } + } + + /** + * 登录前置校验 + * @param username 用户名 + * @param password 用户密码 + */ + public void loginPreCheck(String username, String password) + { + // 用户名或密码为空 错误 + if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("not.null"))); + throw new UserNotExistsException(); + } + // 密码如果不在指定范围内 错误 + if (password.length() < UserConstants.PASSWORD_MIN_LENGTH + || password.length() > UserConstants.PASSWORD_MAX_LENGTH) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"))); + throw new UserPasswordNotMatchException(); + } + // 用户名不在指定范围内 错误 + if (username.length() < UserConstants.USERNAME_MIN_LENGTH + || username.length() > UserConstants.USERNAME_MAX_LENGTH) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"))); + throw new UserPasswordNotMatchException(); + } + // IP黑名单校验 + String blackStr = configService.selectConfigByKey("sys.login.blackIPList"); + if (IpUtils.isMatchedIp(blackStr, IpUtils.getIpAddr())) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("login.blocked"))); + throw new BlackListException(); + } + } + + /** + * 记录登录信息 + * + * @param userId 用户ID + */ + public void recordLoginInfo(Long userId) + { + userService.updateLoginInfo(userId, IpUtils.getIpAddr(), DateUtils.getNowDate()); + } +} diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPasswordService.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPasswordService.java new file mode 100644 index 0000000..6728c7b --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPasswordService.java @@ -0,0 +1,86 @@ +package com.ruoyi.framework.web.service; + +import java.util.concurrent.TimeUnit; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.exception.user.UserPasswordNotMatchException; +import com.ruoyi.common.exception.user.UserPasswordRetryLimitExceedException; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.framework.security.context.AuthenticationContextHolder; + +/** + * 登录密码方法 + * + * @author ruoyi + */ +@Component +public class SysPasswordService +{ + @Autowired + private RedisCache redisCache; + + @Value(value = "${user.password.maxRetryCount}") + private int maxRetryCount; + + @Value(value = "${user.password.lockTime}") + private int lockTime; + + /** + * 登录账户密码错误次数缓存键名 + * + * @param username 用户名 + * @return 缓存键key + */ + private String getCacheKey(String username) + { + return CacheConstants.PWD_ERR_CNT_KEY + username; + } + + public void validate(SysUser user) + { + Authentication usernamePasswordAuthenticationToken = AuthenticationContextHolder.getContext(); + String username = usernamePasswordAuthenticationToken.getName(); + String password = usernamePasswordAuthenticationToken.getCredentials().toString(); + + Integer retryCount = redisCache.getCacheObject(getCacheKey(username)); + + if (retryCount == null) + { + retryCount = 0; + } + + if (retryCount >= Integer.valueOf(maxRetryCount).intValue()) + { + throw new UserPasswordRetryLimitExceedException(maxRetryCount, lockTime); + } + + if (!matches(user, password)) + { + retryCount = retryCount + 1; + redisCache.setCacheObject(getCacheKey(username), retryCount, lockTime, TimeUnit.MINUTES); + throw new UserPasswordNotMatchException(); + } + else + { + clearLoginRecordCache(username); + } + } + + public boolean matches(SysUser user, String rawPassword) + { + return SecurityUtils.matchesPassword(rawPassword, user.getPassword()); + } + + public void clearLoginRecordCache(String loginName) + { + if (redisCache.hasKey(getCacheKey(loginName))) + { + redisCache.deleteObject(getCacheKey(loginName)); + } + } +} diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPermissionService.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPermissionService.java new file mode 100644 index 0000000..17960f6 --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPermissionService.java @@ -0,0 +1,89 @@ +package com.ruoyi.framework.web.service; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.service.ISysMenuService; +import com.ruoyi.system.service.ISysRoleService; + +/** + * 用户权限处理 + * + * @author ruoyi + */ +@Component +public class SysPermissionService +{ + @Autowired + private ISysRoleService roleService; + + @Autowired + private ISysMenuService menuService; + + /** + * 获取角色数据权限 + * + * @param user 用户信息 + * @return 角色权限信息 + */ + public Set getRolePermission(SysUser user) + { + Set roles = new HashSet(); + // 管理员拥有所有权限 + if (user.isAdmin()) + { + roles.add(Constants.SUPER_ADMIN); + } + else + { + roles.addAll(roleService.selectRolePermissionByUserId(user.getUserId())); + } + return roles; + } + + /** + * 获取菜单数据权限 + * + * @param user 用户信息 + * @return 菜单权限信息 + */ + public Set getMenuPermission(SysUser user) + { + Set perms = new HashSet(); + // 管理员拥有所有权限 + if (user.isAdmin()) + { + perms.add(Constants.ALL_PERMISSION); + } + else + { + List roles = user.getRoles(); + if (!CollectionUtils.isEmpty(roles)) + { + // 多角色设置permissions属性,以便数据权限匹配权限 + for (SysRole role : roles) + { + if (StringUtils.equals(role.getStatus(), UserConstants.ROLE_NORMAL) && !role.isAdmin()) + { + Set rolePerms = menuService.selectMenuPermsByRoleId(role.getRoleId()); + role.setPermissions(rolePerms); + perms.addAll(rolePerms); + } + } + } + else + { + perms.addAll(menuService.selectMenuPermsByUserId(user.getUserId())); + } + } + return perms; + } +} diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysRegisterService.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysRegisterService.java new file mode 100644 index 0000000..8305bcb --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysRegisterService.java @@ -0,0 +1,117 @@ +package com.ruoyi.framework.web.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.domain.model.RegisterBody; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.exception.user.CaptchaException; +import com.ruoyi.common.exception.user.CaptchaExpireException; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.MessageUtils; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.manager.AsyncManager; +import com.ruoyi.framework.manager.factory.AsyncFactory; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.system.service.ISysUserService; + +/** + * 注册校验方法 + * + * @author ruoyi + */ +@Component +public class SysRegisterService +{ + @Autowired + private ISysUserService userService; + + @Autowired + private ISysConfigService configService; + + @Autowired + private RedisCache redisCache; + + /** + * 注册 + */ + public String register(RegisterBody registerBody) + { + String msg = "", username = registerBody.getUsername(), password = registerBody.getPassword(); + SysUser sysUser = new SysUser(); + sysUser.setUserName(username); + + // 验证码开关 + boolean captchaEnabled = configService.selectCaptchaEnabled(); + if (captchaEnabled) + { + validateCaptcha(username, registerBody.getCode(), registerBody.getUuid()); + } + + if (StringUtils.isEmpty(username)) + { + msg = "用户名不能为空"; + } + else if (StringUtils.isEmpty(password)) + { + msg = "用户密码不能为空"; + } + else if (username.length() < UserConstants.USERNAME_MIN_LENGTH + || username.length() > UserConstants.USERNAME_MAX_LENGTH) + { + msg = "账户长度必须在2到20个字符之间"; + } + else if (password.length() < UserConstants.PASSWORD_MIN_LENGTH + || password.length() > UserConstants.PASSWORD_MAX_LENGTH) + { + msg = "密码长度必须在5到20个字符之间"; + } + else if (!userService.checkUserNameUnique(sysUser)) + { + msg = "保存用户'" + username + "'失败,注册账号已存在"; + } + else + { + sysUser.setNickName(username); + sysUser.setPwdUpdateDate(DateUtils.getNowDate()); + sysUser.setPassword(SecurityUtils.encryptPassword(password)); + boolean regFlag = userService.registerUser(sysUser); + if (!regFlag) + { + msg = "注册失败,请联系系统管理人员"; + } + else + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.REGISTER, MessageUtils.message("user.register.success"))); + } + } + return msg; + } + + /** + * 校验验证码 + * + * @param username 用户名 + * @param code 验证码 + * @param uuid 唯一标识 + * @return 结果 + */ + public void validateCaptcha(String username, String code, String uuid) + { + String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, ""); + String captcha = redisCache.getCacheObject(verifyKey); + redisCache.deleteObject(verifyKey); + if (captcha == null) + { + throw new CaptchaExpireException(); + } + if (!code.equalsIgnoreCase(captcha)) + { + throw new CaptchaException(); + } + } +} diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java new file mode 100644 index 0000000..ed7e574 --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java @@ -0,0 +1,270 @@ +package com.ruoyi.framework.web.service; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.utils.ServletUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.http.UserAgentUtils; +import com.ruoyi.common.utils.ip.AddressUtils; +import com.ruoyi.common.utils.ip.IpUtils; +import com.ruoyi.common.utils.uuid.IdUtils; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import jakarta.servlet.http.HttpServletRequest; + +/** + * token验证处理 + * + * @author ruoyi + */ +@Component +public class TokenService +{ + private static final Logger log = LoggerFactory.getLogger(TokenService.class); + + // 令牌自定义标识 + @Value("${token.header}") + private String header; + + // 令牌秘钥 + @Value("${token.secret}") + private String secret; + + // 令牌有效期(默认30分钟) + @Value("${token.expireTime}") + private int expireTime; + + protected static final long MILLIS_SECOND = 1000; + + protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND; + + private static final Long MILLIS_MINUTE_TWENTY = 20 * 60 * 1000L; + + @Autowired + private RedisCache redisCache; + + /** + * 获取用户身份信息 + * + * @return 用户信息 + */ + public LoginUser getLoginUser(HttpServletRequest request) + { + // 获取请求携带的令牌 + String token = getToken(request); + if (StringUtils.isNotEmpty(token)) + { + try + { + Claims claims = parseToken(token); + // 解析对应的权限以及用户信息 + String uuid = (String) claims.get(Constants.LOGIN_USER_KEY); + String userKey = getTokenKey(uuid); + LoginUser user = redisCache.getCacheObject(userKey); + return user; + } + catch (Exception e) + { + log.error("获取用户信息异常'{}'", e.getMessage()); + } + } + return null; + } + + /** + * 设置用户身份信息 + */ + public void setLoginUser(LoginUser loginUser) + { + if (StringUtils.isNotNull(loginUser) && StringUtils.isNotEmpty(loginUser.getToken())) + { + refreshToken(loginUser); + } + } + + /** + * 删除用户身份信息 + */ + public void delLoginUser(String token) + { + if (StringUtils.isNotEmpty(token)) + { + String userKey = getTokenKey(token); + redisCache.deleteObject(userKey); + } + } + + /** + * 创建令牌 + * + * @param loginUser 用户信息 + * @return 令牌 + */ + public String createToken(LoginUser loginUser) + { + String token = IdUtils.fastUUID(); + loginUser.setToken(token); + setUserAgent(loginUser); + refreshToken(loginUser); + + Map claims = new HashMap<>(); + claims.put(Constants.LOGIN_USER_KEY, token); + claims.put(Constants.JWT_USERNAME, loginUser.getUsername()); + return createToken(claims); + } + + /** + * 验证令牌有效期,相差不足20分钟,自动刷新缓存 + * + * @param loginUser 登录信息 + * @return 令牌 + */ + public void verifyToken(LoginUser loginUser) + { + long expireTime = loginUser.getExpireTime(); + long currentTime = System.currentTimeMillis(); + if (expireTime - currentTime <= MILLIS_MINUTE_TWENTY) + { + refreshToken(loginUser); + } + } + + /** + * 刷新令牌有效期 + * + * @param loginUser 登录信息 + */ + public void refreshToken(LoginUser loginUser) + { + loginUser.setLoginTime(System.currentTimeMillis()); + loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE); + // 根据uuid将loginUser缓存 + String userKey = getTokenKey(loginUser.getToken()); + redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES); + } + + /** + * 设置用户代理信息 + * + * @param loginUser 登录信息 + */ + public void setUserAgent(LoginUser loginUser) + { + String userAgent = ServletUtils.getRequest().getHeader("User-Agent"); + String ip = IpUtils.getIpAddr(); + loginUser.setIpaddr(ip); + loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip)); + loginUser.setBrowser(UserAgentUtils.getBrowser(userAgent)); + loginUser.setOs(UserAgentUtils.getOperatingSystem(userAgent)); + } + + /** + * 从数据声明生成令牌 + * + * @param claims 数据声明 + * @return 令牌 + */ + private String createToken(Map claims) + { + String token = Jwts.builder() + .setClaims(claims) + .signWith(SignatureAlgorithm.HS512, secret).compact(); + return token; + } + + /** + * 从令牌中获取数据声明 + * + * @param token 令牌 + * @return 数据声明 + */ + private Claims parseToken(String token) + { + return Jwts.parser() + .setSigningKey(secret) + .parseClaimsJws(token) + .getBody(); + } + + /** + * 从令牌中获取用户名 + * + * @param token 令牌 + * @return 用户名 + */ + public String getUsernameFromToken(String token) + { + Claims claims = parseToken(token); + return claims.getSubject(); + } + + /** + * 获取请求token + * + * @param request + * @return token + */ + private String getToken(HttpServletRequest request) + { + String token = request.getHeader(header); + if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)) + { + token = token.replace(Constants.TOKEN_PREFIX, ""); + } + return token; + } + + private String getTokenKey(String uuid) + { + return CacheConstants.LOGIN_TOKEN_KEY + uuid; + } + + /** + * 角色权限变更后,刷新所有持有该角色的在线用户权限 + * + * @param roleId 变更的角色ID + * @param permissionService 权限服务 + */ + public void refreshPermissionByRoleId(Long roleId, SysPermissionService permissionService) + { + // 扫描所有在线 token + String pattern = CacheConstants.LOGIN_TOKEN_KEY + "*"; + Collection keys = redisCache.keys(pattern); + if (keys == null || keys.isEmpty()) + { + return; + } + for (String key : keys) + { + LoginUser loginUser = redisCache.getCacheObject(key); + if (loginUser == null || loginUser.getUser() == null || loginUser.getUser().isAdmin()) + { + // 管理员拥有所有权限,跳过 + continue; + } + // 判断该用户是否拥有此角色 + boolean hasRole = loginUser.getUser().getRoles() != null + && loginUser.getUser().getRoles().stream().anyMatch(r -> roleId.equals(r.getRoleId())); + if (!hasRole) + { + continue; + } + // 刷新权限缓存 + loginUser.setPermissions(permissionService.getMenuPermission(loginUser.getUser())); + refreshToken(loginUser); + log.info("角色[{}]权限变更,已刷新在线用户[{}]的权限缓存", roleId, loginUser.getUsername()); + } + } +} diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java new file mode 100644 index 0000000..5dcdf90 --- /dev/null +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java @@ -0,0 +1,66 @@ +package com.ruoyi.framework.web.service; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.enums.UserStatus; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.MessageUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.service.ISysUserService; + +/** + * 用户验证处理 + * + * @author ruoyi + */ +@Service +public class UserDetailsServiceImpl implements UserDetailsService +{ + private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class); + + @Autowired + private ISysUserService userService; + + @Autowired + private SysPasswordService passwordService; + + @Autowired + private SysPermissionService permissionService; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException + { + SysUser user = userService.selectUserByUserName(username); + if (StringUtils.isNull(user)) + { + log.info("登录用户:{} 不存在.", username); + throw new ServiceException(MessageUtils.message("user.not.exists")); + } + else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) + { + log.info("登录用户:{} 已被删除.", username); + throw new ServiceException(MessageUtils.message("user.password.delete")); + } + else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) + { + log.info("登录用户:{} 已被停用.", username); + throw new ServiceException(MessageUtils.message("user.blocked")); + } + + passwordService.validate(user); + + return createLoginUser(user); + } + + public UserDetails createLoginUser(SysUser user) + { + return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user)); + } +} diff --git a/ry-api/ruoyi-generator/pom.xml b/ry-api/ruoyi-generator/pom.xml new file mode 100644 index 0000000..6f3345a --- /dev/null +++ b/ry-api/ruoyi-generator/pom.xml @@ -0,0 +1,40 @@ + + + + ruoyi + com.ruoyi + 3.9.2 + + 4.0.0 + + ruoyi-generator + + + generator代码生成 + + + + + + + org.apache.velocity + velocity-engine-core + + + + + com.ruoyi + ruoyi-common + + + + + com.alibaba + druid-spring-boot-4-starter + + + + + \ No newline at end of file diff --git a/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/config/GenConfig.java b/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/config/GenConfig.java new file mode 100644 index 0000000..c01857c --- /dev/null +++ b/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/config/GenConfig.java @@ -0,0 +1,87 @@ +package com.ruoyi.generator.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.PropertySource; +import org.springframework.stereotype.Component; + +/** + * 读取代码生成相关配置 + * + * @author ruoyi + */ +@Component +@ConfigurationProperties(prefix = "gen") +@PropertySource(value = { "classpath:generator.yml" }) +public class GenConfig +{ + /** 作者 */ + public static String author; + + /** 生成包路径 */ + public static String packageName; + + /** 自动去除表前缀 */ + public static boolean autoRemovePre; + + /** 表前缀 */ + public static String tablePrefix; + + /** 是否允许生成文件覆盖到本地(自定义路径) */ + public static boolean allowOverwrite; + + public static String getAuthor() + { + return author; + } + + @Value("${author}") + public void setAuthor(String author) + { + GenConfig.author = author; + } + + public static String getPackageName() + { + return packageName; + } + + @Value("${packageName}") + public void setPackageName(String packageName) + { + GenConfig.packageName = packageName; + } + + public static boolean getAutoRemovePre() + { + return autoRemovePre; + } + + @Value("${autoRemovePre}") + public void setAutoRemovePre(boolean autoRemovePre) + { + GenConfig.autoRemovePre = autoRemovePre; + } + + public static String getTablePrefix() + { + return tablePrefix; + } + + @Value("${tablePrefix}") + public void setTablePrefix(String tablePrefix) + { + GenConfig.tablePrefix = tablePrefix; + } + + public static boolean isAllowOverwrite() + { + return allowOverwrite; + } + + @Value("${allowOverwrite}") + public void setAllowOverwrite(boolean allowOverwrite) + { + GenConfig.allowOverwrite = allowOverwrite; + } +} diff --git a/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/controller/GenController.java b/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/controller/GenController.java new file mode 100644 index 0000000..aa763f8 --- /dev/null +++ b/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/controller/GenController.java @@ -0,0 +1,264 @@ +package com.ruoyi.generator.controller; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.commons.io.IOUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import com.alibaba.druid.DbType; +import com.alibaba.druid.sql.SQLUtils; +import com.alibaba.druid.sql.ast.SQLStatement; +import com.alibaba.druid.sql.dialect.mysql.ast.statement.MySqlCreateTableStatement; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.sql.SqlUtil; +import com.ruoyi.generator.config.GenConfig; +import com.ruoyi.generator.domain.GenTable; +import com.ruoyi.generator.domain.GenTableColumn; +import com.ruoyi.generator.service.IGenTableColumnService; +import com.ruoyi.generator.service.IGenTableService; + +/** + * 代码生成 操作处理 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/tool/gen") +public class GenController extends BaseController +{ + @Autowired + private IGenTableService genTableService; + + @Autowired + private IGenTableColumnService genTableColumnService; + + /** + * 查询代码生成列表 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:list')") + @GetMapping("/list") + public TableDataInfo genList(GenTable genTable) + { + startPage(); + List list = genTableService.selectGenTableList(genTable); + return getDataTable(list); + } + + /** + * 获取代码生成信息 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:query')") + @GetMapping(value = "/{tableId}") + public AjaxResult getInfo(@PathVariable Long tableId) + { + GenTable table = genTableService.selectGenTableById(tableId); + List tables = genTableService.selectGenTableAll(); + List list = genTableColumnService.selectGenTableColumnListByTableId(tableId); + Map map = new HashMap(); + map.put("info", table); + map.put("rows", list); + map.put("tables", tables); + return success(map); + } + + /** + * 查询数据库列表 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:list')") + @GetMapping("/db/list") + public TableDataInfo dataList(GenTable genTable) + { + startPage(); + List list = genTableService.selectDbTableList(genTable); + return getDataTable(list); + } + + /** + * 查询数据表字段列表 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:list')") + @GetMapping(value = "/column/{tableId}") + public TableDataInfo columnList(Long tableId) + { + TableDataInfo dataInfo = new TableDataInfo(); + List list = genTableColumnService.selectGenTableColumnListByTableId(tableId); + dataInfo.setRows(list); + dataInfo.setTotal(list.size()); + return dataInfo; + } + + /** + * 导入表结构(保存) + */ + @PreAuthorize("@ss.hasPermi('tool:gen:import')") + @Log(title = "代码生成", businessType = BusinessType.IMPORT) + @PostMapping("/importTable") + public AjaxResult importTableSave(@RequestParam("tables") String tables, @RequestParam("tplWebType") String tplWebType) + { + String[] tableNames = Convert.toStrArray(tables); + // 查询表信息 + List tableList = genTableService.selectDbTableListByNames(tableNames); + genTableService.importGenTable(tableList, tplWebType, SecurityUtils.getUsername()); + return success(); + } + + /** + * 创建表结构(保存) + */ + @PreAuthorize("@ss.hasRole('admin')") + @Log(title = "创建表", businessType = BusinessType.OTHER) + @PostMapping("/createTable") + public AjaxResult createTableSave(@RequestParam("sql") String sql, @RequestParam("tplWebType") String tplWebType) + { + try + { + SqlUtil.filterKeyword(sql); + List sqlStatements = SQLUtils.parseStatements(sql, DbType.mysql); + List tableNames = new ArrayList<>(); + for (SQLStatement sqlStatement : sqlStatements) + { + if (sqlStatement instanceof MySqlCreateTableStatement) + { + MySqlCreateTableStatement createTableStatement = (MySqlCreateTableStatement) sqlStatement; + if (genTableService.createTable(createTableStatement.toString())) + { + String tableName = createTableStatement.getTableName().replaceAll("`", ""); + tableNames.add(tableName); + } + } + } + List tableList = genTableService.selectDbTableListByNames(tableNames.toArray(new String[tableNames.size()])); + String operName = SecurityUtils.getUsername(); + genTableService.importGenTable(tableList, tplWebType, operName); + return AjaxResult.success(); + } + catch (Exception e) + { + logger.error(e.getMessage(), e); + return AjaxResult.error("创建表结构异常"); + } + } + + /** + * 修改保存代码生成业务 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:edit')") + @Log(title = "代码生成", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult editSave(@Validated @RequestBody GenTable genTable) + { + genTableService.validateEdit(genTable); + genTableService.updateGenTable(genTable); + return success(); + } + + /** + * 删除代码生成 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:remove')") + @Log(title = "代码生成", businessType = BusinessType.DELETE) + @DeleteMapping("/{tableIds}") + public AjaxResult remove(@PathVariable Long[] tableIds) + { + genTableService.deleteGenTableByIds(tableIds); + return success(); + } + + /** + * 预览代码 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:preview')") + @GetMapping("/preview/{tableId}") + public AjaxResult preview(@PathVariable("tableId") Long tableId) throws IOException + { + Map dataMap = genTableService.previewCode(tableId); + return success(dataMap); + } + + /** + * 生成代码(下载方式) + */ + @PreAuthorize("@ss.hasPermi('tool:gen:code')") + @Log(title = "代码生成", businessType = BusinessType.GENCODE) + @GetMapping("/download/{tableName}") + public void download(HttpServletResponse response, @PathVariable("tableName") String tableName) throws IOException + { + byte[] data = genTableService.downloadCode(tableName); + genCode(response, data); + } + + /** + * 生成代码(自定义路径) + */ + @PreAuthorize("@ss.hasPermi('tool:gen:code')") + @Log(title = "代码生成", businessType = BusinessType.GENCODE) + @GetMapping("/genCode/{tableName}") + public AjaxResult genCode(@PathVariable("tableName") String tableName) + { + if (!GenConfig.isAllowOverwrite()) + { + return AjaxResult.error("【系统预设】不允许生成文件覆盖到本地"); + } + genTableService.generatorCode(tableName); + return success(); + } + + /** + * 同步数据库 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:edit')") + @Log(title = "代码生成", businessType = BusinessType.UPDATE) + @GetMapping("/synchDb/{tableName}") + public AjaxResult synchDb(@PathVariable("tableName") String tableName) + { + genTableService.synchDb(tableName); + return success(); + } + + /** + * 批量生成代码 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:code')") + @Log(title = "代码生成", businessType = BusinessType.GENCODE) + @GetMapping("/batchGenCode") + public void batchGenCode(HttpServletResponse response, String tables) throws IOException + { + String[] tableNames = Convert.toStrArray(tables); + byte[] data = genTableService.downloadCode(tableNames); + genCode(response, data); + } + + /** + * 生成zip文件 + */ + private void genCode(HttpServletResponse response, byte[] data) throws IOException + { + response.reset(); + response.addHeader("Access-Control-Allow-Origin", "*"); + response.addHeader("Access-Control-Expose-Headers", "Content-Disposition"); + response.setHeader("Content-Disposition", "attachment; filename=\"ruoyi.zip\""); + response.addHeader("Content-Length", "" + data.length); + response.setContentType("application/octet-stream; charset=UTF-8"); + IOUtils.write(data, response.getOutputStream()); + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenTable.java b/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenTable.java new file mode 100644 index 0000000..bae2bb5 --- /dev/null +++ b/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenTable.java @@ -0,0 +1,411 @@ +package com.ruoyi.generator.domain; + +import java.util.List; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import org.apache.commons.lang3.ArrayUtils; +import com.ruoyi.common.constant.GenConstants; +import com.ruoyi.common.core.domain.BaseEntity; +import com.ruoyi.common.utils.StringUtils; + +/** + * 业务表 gen_table + * + * @author ruoyi + */ +public class GenTable extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 编号 */ + private Long tableId; + + /** 表名称 */ + @NotBlank(message = "表名称不能为空") + private String tableName; + + /** 表描述 */ + @NotBlank(message = "表描述不能为空") + private String tableComment; + + /** 关联父表的表名 */ + private String subTableName; + + /** 本表关联父表的外键名 */ + private String subTableFkName; + + /** 实体类名称(首字母大写) */ + @NotBlank(message = "实体类名称不能为空") + private String className; + + /** 使用的模板(crud单表操作 tree树表操作 sub主子表操作) */ + private String tplCategory; + + /** 前端类型(element-ui模版 element-plus模版 element-plus-typescript模版) */ + private String tplWebType; + + /** 生成包路径 */ + @NotBlank(message = "生成包路径不能为空") + private String packageName; + + /** 生成模块名 */ + @NotBlank(message = "生成模块名不能为空") + private String moduleName; + + /** 生成业务名 */ + @NotBlank(message = "生成业务名不能为空") + private String businessName; + + /** 生成功能名 */ + @NotBlank(message = "生成功能名不能为空") + private String functionName; + + /** 生成作者 */ + @NotBlank(message = "作者不能为空") + private String functionAuthor; + + /** 表单布局(单列 双列 三列) */ + private Integer formColNum; + + /** 生成代码方式(0zip压缩包 1自定义路径) */ + private String genType; + + /** 生成路径(不填默认项目路径) */ + private String genPath; + + /** 主键信息 */ + private GenTableColumn pkColumn; + + /** 子表信息 */ + private GenTable subTable; + + /** 表列信息 */ + @Valid + private List columns; + + /** 其它生成选项 */ + private String options; + + /** 树编码字段 */ + private String treeCode; + + /** 树父编码字段 */ + private String treeParentCode; + + /** 树名称字段 */ + private String treeName; + + /** 上级菜单ID字段 */ + private Long parentMenuId; + + /** 上级菜单名称字段 */ + private String parentMenuName; + + /** 是否生成详情页 */ + private boolean isView; + + public Long getTableId() + { + return tableId; + } + + public void setTableId(Long tableId) + { + this.tableId = tableId; + } + + public String getTableName() + { + return tableName; + } + + public void setTableName(String tableName) + { + this.tableName = tableName; + } + + public String getTableComment() + { + return tableComment; + } + + public void setTableComment(String tableComment) + { + this.tableComment = tableComment; + } + + public String getSubTableName() + { + return subTableName; + } + + public void setSubTableName(String subTableName) + { + this.subTableName = subTableName; + } + + public String getSubTableFkName() + { + return subTableFkName; + } + + public void setSubTableFkName(String subTableFkName) + { + this.subTableFkName = subTableFkName; + } + + public String getClassName() + { + return className; + } + + public void setClassName(String className) + { + this.className = className; + } + + public String getTplCategory() + { + return tplCategory; + } + + public void setTplCategory(String tplCategory) + { + this.tplCategory = tplCategory; + } + + public String getTplWebType() + { + return tplWebType; + } + + public void setTplWebType(String tplWebType) + { + this.tplWebType = tplWebType; + } + + public String getPackageName() + { + return packageName; + } + + public void setPackageName(String packageName) + { + this.packageName = packageName; + } + + public String getModuleName() + { + return moduleName; + } + + public void setModuleName(String moduleName) + { + this.moduleName = moduleName; + } + + public String getBusinessName() + { + return businessName; + } + + public void setBusinessName(String businessName) + { + this.businessName = businessName; + } + + public String getFunctionName() + { + return functionName; + } + + public void setFunctionName(String functionName) + { + this.functionName = functionName; + } + + public String getFunctionAuthor() + { + return functionAuthor; + } + + public void setFunctionAuthor(String functionAuthor) + { + this.functionAuthor = functionAuthor; + } + + public Integer getFormColNum() + { + return formColNum; + } + + public void setFormColNum(Integer formColNum) + { + this.formColNum = formColNum; + } + + public String getGenType() + { + return genType; + } + + public void setGenType(String genType) + { + this.genType = genType; + } + + public String getGenPath() + { + return genPath; + } + + public void setGenPath(String genPath) + { + this.genPath = genPath; + } + + public GenTableColumn getPkColumn() + { + return pkColumn; + } + + public void setPkColumn(GenTableColumn pkColumn) + { + this.pkColumn = pkColumn; + } + + public GenTable getSubTable() + { + return subTable; + } + + public void setSubTable(GenTable subTable) + { + this.subTable = subTable; + } + + public List getColumns() + { + return columns; + } + + public void setColumns(List columns) + { + this.columns = columns; + } + + public String getOptions() + { + return options; + } + + public void setOptions(String options) + { + this.options = options; + } + + public String getTreeCode() + { + return treeCode; + } + + public void setTreeCode(String treeCode) + { + this.treeCode = treeCode; + } + + public String getTreeParentCode() + { + return treeParentCode; + } + + public void setTreeParentCode(String treeParentCode) + { + this.treeParentCode = treeParentCode; + } + + public String getTreeName() + { + return treeName; + } + + public void setTreeName(String treeName) + { + this.treeName = treeName; + } + + public Long getParentMenuId() + { + return parentMenuId; + } + + public void setParentMenuId(Long parentMenuId) + { + this.parentMenuId = parentMenuId; + } + + public String getParentMenuName() + { + return parentMenuName; + } + + public void setParentMenuName(String parentMenuName) + { + this.parentMenuName = parentMenuName; + } + + public boolean isView() + { + return isView; + } + + public void setView(boolean isView) + { + this.isView = isView; + } + + public boolean isSub() + { + return isSub(this.tplCategory); + } + + public static boolean isSub(String tplCategory) + { + return tplCategory != null && StringUtils.equals(GenConstants.TPL_SUB, tplCategory); + } + + public boolean isTree() + { + return isTree(this.tplCategory); + } + + public static boolean isTree(String tplCategory) + { + return tplCategory != null && StringUtils.equals(GenConstants.TPL_TREE, tplCategory); + } + + public boolean isCrud() + { + return isCrud(this.tplCategory); + } + + public static boolean isCrud(String tplCategory) + { + return tplCategory != null && StringUtils.equals(GenConstants.TPL_CRUD, tplCategory); + } + + public boolean isSuperColumn(String javaField) + { + return isSuperColumn(this.tplCategory, javaField); + } + + public static boolean isSuperColumn(String tplCategory, String javaField) + { + if (isTree(tplCategory)) + { + return StringUtils.equalsAnyIgnoreCase(javaField, + ArrayUtils.addAll(GenConstants.TREE_ENTITY, GenConstants.BASE_ENTITY)); + } + return StringUtils.equalsAnyIgnoreCase(javaField, GenConstants.BASE_ENTITY); + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenTableColumn.java b/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenTableColumn.java new file mode 100644 index 0000000..3dc6d62 --- /dev/null +++ b/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenTableColumn.java @@ -0,0 +1,373 @@ +package com.ruoyi.generator.domain; + +import jakarta.validation.constraints.NotBlank; +import com.ruoyi.common.core.domain.BaseEntity; +import com.ruoyi.common.utils.StringUtils; + +/** + * 代码生成业务字段表 gen_table_column + * + * @author ruoyi + */ +public class GenTableColumn extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 编号 */ + private Long columnId; + + /** 归属表编号 */ + private Long tableId; + + /** 列名称 */ + private String columnName; + + /** 列描述 */ + private String columnComment; + + /** 列类型 */ + private String columnType; + + /** JAVA类型 */ + private String javaType; + + /** JAVA字段名 */ + @NotBlank(message = "Java属性不能为空") + private String javaField; + + /** 是否主键(1是) */ + private String isPk; + + /** 是否自增(1是) */ + private String isIncrement; + + /** 是否必填(1是) */ + private String isRequired; + + /** 是否为插入字段(1是) */ + private String isInsert; + + /** 是否编辑字段(1是) */ + private String isEdit; + + /** 是否列表字段(1是) */ + private String isList; + + /** 是否查询字段(1是) */ + private String isQuery; + + /** 查询方式(EQ等于、NE不等于、GT大于、LT小于、LIKE模糊、BETWEEN范围) */ + private String queryType; + + /** 显示类型(input文本框、textarea文本域、select下拉框、checkbox复选框、radio单选框、datetime日期控件、image图片上传控件、upload文件上传控件、editor富文本控件) */ + private String htmlType; + + /** 字典类型 */ + private String dictType; + + /** 排序 */ + private Integer sort; + + public void setColumnId(Long columnId) + { + this.columnId = columnId; + } + + public Long getColumnId() + { + return columnId; + } + + public void setTableId(Long tableId) + { + this.tableId = tableId; + } + + public Long getTableId() + { + return tableId; + } + + public void setColumnName(String columnName) + { + this.columnName = columnName; + } + + public String getColumnName() + { + return columnName; + } + + public void setColumnComment(String columnComment) + { + this.columnComment = columnComment; + } + + public String getColumnComment() + { + return columnComment; + } + + public void setColumnType(String columnType) + { + this.columnType = columnType; + } + + public String getColumnType() + { + return columnType; + } + + public void setJavaType(String javaType) + { + this.javaType = javaType; + } + + public String getJavaType() + { + return javaType; + } + + public void setJavaField(String javaField) + { + this.javaField = javaField; + } + + public String getJavaField() + { + return javaField; + } + + public String getCapJavaField() + { + return StringUtils.capitalize(javaField); + } + + public void setIsPk(String isPk) + { + this.isPk = isPk; + } + + public String getIsPk() + { + return isPk; + } + + public boolean isPk() + { + return isPk(this.isPk); + } + + public boolean isPk(String isPk) + { + return isPk != null && StringUtils.equals("1", isPk); + } + + public String getIsIncrement() + { + return isIncrement; + } + + public void setIsIncrement(String isIncrement) + { + this.isIncrement = isIncrement; + } + + public boolean isIncrement() + { + return isIncrement(this.isIncrement); + } + + public boolean isIncrement(String isIncrement) + { + return isIncrement != null && StringUtils.equals("1", isIncrement); + } + + public void setIsRequired(String isRequired) + { + this.isRequired = isRequired; + } + + public String getIsRequired() + { + return isRequired; + } + + public boolean isRequired() + { + return isRequired(this.isRequired); + } + + public boolean isRequired(String isRequired) + { + return isRequired != null && StringUtils.equals("1", isRequired); + } + + public void setIsInsert(String isInsert) + { + this.isInsert = isInsert; + } + + public String getIsInsert() + { + return isInsert; + } + + public boolean isInsert() + { + return isInsert(this.isInsert); + } + + public boolean isInsert(String isInsert) + { + return isInsert != null && StringUtils.equals("1", isInsert); + } + + public void setIsEdit(String isEdit) + { + this.isEdit = isEdit; + } + + public String getIsEdit() + { + return isEdit; + } + + public boolean isEdit() + { + return isInsert(this.isEdit); + } + + public boolean isEdit(String isEdit) + { + return isEdit != null && StringUtils.equals("1", isEdit); + } + + public void setIsList(String isList) + { + this.isList = isList; + } + + public String getIsList() + { + return isList; + } + + public boolean isList() + { + return isList(this.isList); + } + + public boolean isList(String isList) + { + return isList != null && StringUtils.equals("1", isList); + } + + public void setIsQuery(String isQuery) + { + this.isQuery = isQuery; + } + + public String getIsQuery() + { + return isQuery; + } + + public boolean isQuery() + { + return isQuery(this.isQuery); + } + + public boolean isQuery(String isQuery) + { + return isQuery != null && StringUtils.equals("1", isQuery); + } + + public void setQueryType(String queryType) + { + this.queryType = queryType; + } + + public String getQueryType() + { + return queryType; + } + + public String getHtmlType() + { + return htmlType; + } + + public void setHtmlType(String htmlType) + { + this.htmlType = htmlType; + } + + public void setDictType(String dictType) + { + this.dictType = dictType; + } + + public String getDictType() + { + return dictType; + } + + public void setSort(Integer sort) + { + this.sort = sort; + } + + public Integer getSort() + { + return sort; + } + + public boolean isSuperColumn() + { + return isSuperColumn(this.javaField); + } + + public static boolean isSuperColumn(String javaField) + { + return StringUtils.equalsAnyIgnoreCase(javaField, + // BaseEntity + "createBy", "createTime", "updateBy", "updateTime", "remark", + // TreeEntity + "parentName", "parentId", "orderNum", "ancestors"); + } + + public boolean isUsableColumn() + { + return isUsableColumn(javaField); + } + + public static boolean isUsableColumn(String javaField) + { + // isSuperColumn()中的名单用于避免生成多余Domain属性,若某些属性在生成页面时需要用到不能忽略,则放在此处白名单 + return StringUtils.equalsAnyIgnoreCase(javaField, "parentId", "orderNum", "remark"); + } + + public String readConverterExp() + { + String remarks = StringUtils.substringBetween(this.columnComment, "(", ")"); + StringBuffer sb = new StringBuffer(); + if (StringUtils.isNotEmpty(remarks)) + { + for (String value : remarks.split(" ")) + { + if (StringUtils.isNotEmpty(value)) + { + Object startStr = value.subSequence(0, 1); + String endStr = value.substring(1); + sb.append("").append(startStr).append("=").append(endStr).append(","); + } + } + return sb.deleteCharAt(sb.length() - 1).toString(); + } + else + { + return this.columnComment; + } + } +} diff --git a/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/GenTableColumnMapper.java b/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/GenTableColumnMapper.java new file mode 100644 index 0000000..951e166 --- /dev/null +++ b/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/GenTableColumnMapper.java @@ -0,0 +1,60 @@ +package com.ruoyi.generator.mapper; + +import java.util.List; +import com.ruoyi.generator.domain.GenTableColumn; + +/** + * 业务字段 数据层 + * + * @author ruoyi + */ +public interface GenTableColumnMapper +{ + /** + * 根据表名称查询列信息 + * + * @param tableName 表名称 + * @return 列信息 + */ + public List selectDbTableColumnsByName(String tableName); + + /** + * 查询业务字段列表 + * + * @param tableId 业务字段编号 + * @return 业务字段集合 + */ + public List selectGenTableColumnListByTableId(Long tableId); + + /** + * 新增业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + public int insertGenTableColumn(GenTableColumn genTableColumn); + + /** + * 修改业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + public int updateGenTableColumn(GenTableColumn genTableColumn); + + /** + * 删除业务字段 + * + * @param genTableColumns 列数据 + * @return 结果 + */ + public int deleteGenTableColumns(List genTableColumns); + + /** + * 批量删除业务字段 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteGenTableColumnByIds(Long[] ids); +} diff --git a/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/GenTableMapper.java b/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/GenTableMapper.java new file mode 100644 index 0000000..937656d --- /dev/null +++ b/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/GenTableMapper.java @@ -0,0 +1,91 @@ +package com.ruoyi.generator.mapper; + +import java.util.List; +import com.ruoyi.generator.domain.GenTable; + +/** + * 业务 数据层 + * + * @author ruoyi + */ +public interface GenTableMapper +{ + /** + * 查询业务列表 + * + * @param genTable 业务信息 + * @return 业务集合 + */ + public List selectGenTableList(GenTable genTable); + + /** + * 查询据库列表 + * + * @param genTable 业务信息 + * @return 数据库表集合 + */ + public List selectDbTableList(GenTable genTable); + + /** + * 查询据库列表 + * + * @param tableNames 表名称组 + * @return 数据库表集合 + */ + public List selectDbTableListByNames(String[] tableNames); + + /** + * 查询所有表信息 + * + * @return 表信息集合 + */ + public List selectGenTableAll(); + + /** + * 查询表ID业务信息 + * + * @param id 业务ID + * @return 业务信息 + */ + public GenTable selectGenTableById(Long id); + + /** + * 查询表名称业务信息 + * + * @param tableName 表名称 + * @return 业务信息 + */ + public GenTable selectGenTableByName(String tableName); + + /** + * 新增业务 + * + * @param genTable 业务信息 + * @return 结果 + */ + public int insertGenTable(GenTable genTable); + + /** + * 修改业务 + * + * @param genTable 业务信息 + * @return 结果 + */ + public int updateGenTable(GenTable genTable); + + /** + * 批量删除业务 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteGenTableByIds(Long[] ids); + + /** + * 创建表 + * + * @param sql 表结构 + * @return 结果 + */ + public int createTable(String sql); +} diff --git a/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenTableColumnServiceImpl.java b/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenTableColumnServiceImpl.java new file mode 100644 index 0000000..0679689 --- /dev/null +++ b/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenTableColumnServiceImpl.java @@ -0,0 +1,68 @@ +package com.ruoyi.generator.service; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.generator.domain.GenTableColumn; +import com.ruoyi.generator.mapper.GenTableColumnMapper; + +/** + * 业务字段 服务层实现 + * + * @author ruoyi + */ +@Service +public class GenTableColumnServiceImpl implements IGenTableColumnService +{ + @Autowired + private GenTableColumnMapper genTableColumnMapper; + + /** + * 查询业务字段列表 + * + * @param tableId 业务字段编号 + * @return 业务字段集合 + */ + @Override + public List selectGenTableColumnListByTableId(Long tableId) + { + return genTableColumnMapper.selectGenTableColumnListByTableId(tableId); + } + + /** + * 新增业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + @Override + public int insertGenTableColumn(GenTableColumn genTableColumn) + { + return genTableColumnMapper.insertGenTableColumn(genTableColumn); + } + + /** + * 修改业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + @Override + public int updateGenTableColumn(GenTableColumn genTableColumn) + { + return genTableColumnMapper.updateGenTableColumn(genTableColumn); + } + + /** + * 删除业务字段对象 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + @Override + public int deleteGenTableColumnByIds(String ids) + { + return genTableColumnMapper.deleteGenTableColumnByIds(Convert.toLongArray(ids)); + } +} diff --git a/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenTableServiceImpl.java b/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenTableServiceImpl.java new file mode 100644 index 0000000..e851487 --- /dev/null +++ b/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenTableServiceImpl.java @@ -0,0 +1,564 @@ +package com.ruoyi.generator.service; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.StringWriter; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.velocity.Template; +import org.apache.velocity.VelocityContext; +import org.apache.velocity.app.Velocity; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.constant.GenConstants; +import com.ruoyi.common.core.text.CharsetKit; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.generator.domain.GenTable; +import com.ruoyi.generator.domain.GenTableColumn; +import com.ruoyi.generator.mapper.GenTableColumnMapper; +import com.ruoyi.generator.mapper.GenTableMapper; +import com.ruoyi.generator.util.GenUtils; +import com.ruoyi.generator.util.VelocityInitializer; +import com.ruoyi.generator.util.VelocityUtils; + +/** + * 业务 服务层实现 + * + * @author ruoyi + */ +@Service +public class GenTableServiceImpl implements IGenTableService +{ + private static final Logger log = LoggerFactory.getLogger(GenTableServiceImpl.class); + + @Autowired + private GenTableMapper genTableMapper; + + @Autowired + private GenTableColumnMapper genTableColumnMapper; + + /** + * 查询业务信息 + * + * @param id 业务ID + * @return 业务信息 + */ + @Override + public GenTable selectGenTableById(Long id) + { + GenTable genTable = genTableMapper.selectGenTableById(id); + setTableFromOptions(genTable); + return genTable; + } + + /** + * 查询业务列表 + * + * @param genTable 业务信息 + * @return 业务集合 + */ + @Override + public List selectGenTableList(GenTable genTable) + { + return genTableMapper.selectGenTableList(genTable); + } + + /** + * 查询据库列表 + * + * @param genTable 业务信息 + * @return 数据库表集合 + */ + @Override + public List selectDbTableList(GenTable genTable) + { + return genTableMapper.selectDbTableList(genTable); + } + + /** + * 查询据库列表 + * + * @param tableNames 表名称组 + * @return 数据库表集合 + */ + @Override + public List selectDbTableListByNames(String[] tableNames) + { + return genTableMapper.selectDbTableListByNames(tableNames); + } + + /** + * 查询所有表信息 + * + * @return 表信息集合 + */ + @Override + public List selectGenTableAll() + { + return genTableMapper.selectGenTableAll(); + } + + /** + * 修改业务 + * + * @param genTable 业务信息 + * @return 结果 + */ + @Override + @Transactional + public void updateGenTable(GenTable genTable) + { + String options = JSON.toJSONString(genTable.getParams()); + genTable.setOptions(options); + int row = genTableMapper.updateGenTable(genTable); + if (row > 0) + { + for (GenTableColumn genTableColumn : genTable.getColumns()) + { + genTableColumnMapper.updateGenTableColumn(genTableColumn); + } + } + } + + /** + * 删除业务对象 + * + * @param tableIds 需要删除的数据ID + * @return 结果 + */ + @Override + @Transactional + public void deleteGenTableByIds(Long[] tableIds) + { + genTableMapper.deleteGenTableByIds(tableIds); + genTableColumnMapper.deleteGenTableColumnByIds(tableIds); + } + + /** + * 创建表 + * + * @param sql 创建表语句 + * @return 结果 + */ + @Override + public boolean createTable(String sql) + { + return genTableMapper.createTable(sql) == 0; + } + + /** + * 导入表结构 + * + * @param tableList 导入表列表 + */ + @Override + @Transactional + public void importGenTable(List tableList, String tplWebType, String operName) + { + try + { + for (GenTable table : tableList) + { + String tableName = table.getTableName(); + table.setTplWebType(tplWebType); + GenUtils.initTable(table, operName); + int row = genTableMapper.insertGenTable(table); + if (row > 0) + { + // 保存列信息 + List genTableColumns = genTableColumnMapper.selectDbTableColumnsByName(tableName); + for (GenTableColumn column : genTableColumns) + { + GenUtils.initColumnField(column, table); + genTableColumnMapper.insertGenTableColumn(column); + } + } + } + } + catch (Exception e) + { + throw new ServiceException("导入失败:" + e.getMessage()); + } + } + + /** + * 预览代码 + * + * @param tableId 表编号 + * @return 预览数据列表 + */ + @Override + public Map previewCode(Long tableId) + { + Map dataMap = new LinkedHashMap<>(); + // 查询表信息 + GenTable table = genTableMapper.selectGenTableById(tableId); + // 设置主子表信息 + setSubTable(table); + // 设置主键列信息 + setPkColumn(table); + VelocityInitializer.initVelocity(); + + VelocityContext context = VelocityUtils.prepareContext(table); + + // 获取模板列表 + List templates = VelocityUtils.getTemplateList(table); + for (String template : templates) + { + // 渲染模板 + StringWriter sw = new StringWriter(); + Template tpl = Velocity.getTemplate(template, Constants.UTF8); + tpl.merge(context, sw); + dataMap.put(template, sw.toString()); + } + return dataMap; + } + + /** + * 生成代码(下载方式) + * + * @param tableName 表名称 + * @return 数据 + */ + @Override + public byte[] downloadCode(String tableName) + { + return downloadCode(new String[] { tableName }); + } + + /** + * 生成代码(自定义路径) + * + * @param tableName 表名称 + */ + @Override + public void generatorCode(String tableName) + { + // 查询表信息 + GenTable table = genTableMapper.selectGenTableByName(tableName); + // 设置主子表信息 + setSubTable(table); + // 设置主键列信息 + setPkColumn(table); + + VelocityInitializer.initVelocity(); + + VelocityContext context = VelocityUtils.prepareContext(table); + + // 获取模板列表 + List templates = VelocityUtils.getTemplateList(table); + for (String template : templates) + { + if (!StringUtils.containsAny(template, "sql.vm", "api.js.vm", "api.ts.vm", "type.ts.vm", "index.ts.vm", "index.vue.vm", "index-tree.vue.vm", "view.vue.vm")) + { + // 渲染模板 + StringWriter sw = new StringWriter(); + Template tpl = Velocity.getTemplate(template, Constants.UTF8); + tpl.merge(context, sw); + try + { + String path = getGenPath(table, template); + FileUtils.writeStringToFile(new File(path), sw.toString(), CharsetKit.UTF_8); + } + catch (IOException e) + { + throw new ServiceException("渲染模板失败,表名:" + table.getTableName()); + } + } + } + } + + /** + * 同步数据库 + * + * @param tableName 表名称 + */ + @Override + @Transactional + public void synchDb(String tableName) + { + GenTable table = genTableMapper.selectGenTableByName(tableName); + List tableColumns = table.getColumns(); + Map tableColumnMap = tableColumns.stream().collect(Collectors.toMap(GenTableColumn::getColumnName, Function.identity())); + + List dbTableColumns = genTableColumnMapper.selectDbTableColumnsByName(tableName); + if (StringUtils.isEmpty(dbTableColumns)) + { + throw new ServiceException("同步数据失败,原表结构不存在"); + } + List dbTableColumnNames = dbTableColumns.stream().map(GenTableColumn::getColumnName).collect(Collectors.toList()); + + dbTableColumns.forEach(column -> { + GenUtils.initColumnField(column, table); + if (tableColumnMap.containsKey(column.getColumnName())) + { + GenTableColumn prevColumn = tableColumnMap.get(column.getColumnName()); + column.setColumnId(prevColumn.getColumnId()); + if (column.isList()) + { + // 如果是列表,继续保留查询方式/字典类型选项 + column.setDictType(prevColumn.getDictType()); + column.setQueryType(prevColumn.getQueryType()); + } + if (StringUtils.isNotEmpty(prevColumn.getIsRequired()) && !column.isPk() + && (column.isInsert() || column.isEdit()) + && ((column.isUsableColumn()) || (!column.isSuperColumn()))) + { + // 如果是(新增/修改&非主键/非忽略及父属性),继续保留必填/显示类型选项 + column.setIsRequired(prevColumn.getIsRequired()); + column.setHtmlType(prevColumn.getHtmlType()); + } + genTableColumnMapper.updateGenTableColumn(column); + } + else + { + genTableColumnMapper.insertGenTableColumn(column); + } + }); + + List delColumns = tableColumns.stream().filter(column -> !dbTableColumnNames.contains(column.getColumnName())).collect(Collectors.toList()); + if (StringUtils.isNotEmpty(delColumns)) + { + genTableColumnMapper.deleteGenTableColumns(delColumns); + } + } + + /** + * 批量生成代码(下载方式) + * + * @param tableNames 表数组 + * @return 数据 + */ + @Override + public byte[] downloadCode(String[] tableNames) + { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream); + Map typeFiles = new HashMap<>(); + for (String tableName : tableNames) + { + generatorCode(tableName, zip, typeFiles); + } + for (Map.Entry entry : typeFiles.entrySet()) + { + writeToZip(zip, entry.getKey(), entry.getValue().toString()); + } + IOUtils.closeQuietly(zip); + return outputStream.toByteArray(); + } + + /** + * 查询表信息并生成代码 + */ + private void generatorCode(String tableName, ZipOutputStream zip, Map typeFiles) + { + // 查询表信息 + GenTable table = genTableMapper.selectGenTableByName(tableName); + // 设置主子表信息 + setSubTable(table); + // 设置主键列信息 + setPkColumn(table); + + VelocityInitializer.initVelocity(); + + VelocityContext context = VelocityUtils.prepareContext(table); + + // 获取模板列表 + List templates = VelocityUtils.getTemplateList(table); + for (String template : templates) + { + // 渲染模板 + StringWriter sw = new StringWriter(); + Template tpl = Velocity.getTemplate(template, Constants.UTF8); + tpl.merge(context, sw); + String fileName = VelocityUtils.getFileName(template, table); + // index-bak.ts 模版,追加内容 + if (fileName.contains("index-bak.ts")) + { + if (!typeFiles.containsKey(fileName)) + { + typeFiles.put(fileName, new StringBuffer(sw.toString())); + } + else + { + Arrays.stream(sw.toString().split("\n")).filter(line -> line.startsWith("export * from")).forEach(line -> typeFiles.get(fileName).append("\n").append(line)); + } + } + else + { + // 其他文件正常添加 + writeToZip(zip, fileName, sw.toString()); + } + } + } + + /** + * 将字符串内容写入ZIP输出流 + * + * @param zip ZIP输出流 + * @param fileName ZIP条目名称(即文件名) + * @param content 要写入的内容 + */ + private void writeToZip(ZipOutputStream zip, String fileName, String content) + { + try + { + zip.putNextEntry(new ZipEntry(fileName)); + IOUtils.write(content, zip, Constants.UTF8); + zip.flush(); + zip.closeEntry(); + } + catch (IOException e) + { + log.error("写入ZIP文件失败,文件名: " + fileName, e); + } + } + + /** + * 修改保存参数校验 + * + * @param genTable 业务信息 + */ + @Override + public void validateEdit(GenTable genTable) + { + if (GenConstants.TPL_TREE.equals(genTable.getTplCategory())) + { + String options = JSON.toJSONString(genTable.getParams()); + JSONObject paramsObj = JSON.parseObject(options); + if (StringUtils.isEmpty(paramsObj.getString(GenConstants.TREE_CODE))) + { + throw new ServiceException("树编码字段不能为空"); + } + else if (StringUtils.isEmpty(paramsObj.getString(GenConstants.TREE_PARENT_CODE))) + { + throw new ServiceException("树父编码字段不能为空"); + } + else if (StringUtils.isEmpty(paramsObj.getString(GenConstants.TREE_NAME))) + { + throw new ServiceException("树名称字段不能为空"); + } + } + else if (GenConstants.TPL_SUB.equals(genTable.getTplCategory())) + { + if (StringUtils.isEmpty(genTable.getSubTableName())) + { + throw new ServiceException("关联子表的表名不能为空"); + } + else if (StringUtils.isEmpty(genTable.getSubTableFkName())) + { + throw new ServiceException("子表关联的外键名不能为空"); + } + } + } + + /** + * 设置主键列信息 + * + * @param table 业务表信息 + */ + public void setPkColumn(GenTable table) + { + for (GenTableColumn column : table.getColumns()) + { + if (column.isPk()) + { + table.setPkColumn(column); + break; + } + } + if (StringUtils.isNull(table.getPkColumn())) + { + table.setPkColumn(table.getColumns().get(0)); + } + if (GenConstants.TPL_SUB.equals(table.getTplCategory())) + { + for (GenTableColumn column : table.getSubTable().getColumns()) + { + if (column.isPk()) + { + table.getSubTable().setPkColumn(column); + break; + } + } + if (StringUtils.isNull(table.getSubTable().getPkColumn())) + { + table.getSubTable().setPkColumn(table.getSubTable().getColumns().get(0)); + } + } + } + + /** + * 设置主子表信息 + * + * @param table 业务表信息 + */ + public void setSubTable(GenTable table) + { + String subTableName = table.getSubTableName(); + if (StringUtils.isNotEmpty(subTableName)) + { + table.setSubTable(genTableMapper.selectGenTableByName(subTableName)); + } + } + + /** + * 设置代码生成其他选项值 + * + * @param genTable 设置后的生成对象 + */ + public void setTableFromOptions(GenTable genTable) + { + JSONObject paramsObj = JSON.parseObject(genTable.getOptions()); + if (StringUtils.isNotNull(paramsObj)) + { + String treeCode = paramsObj.getString(GenConstants.TREE_CODE); + String treeParentCode = paramsObj.getString(GenConstants.TREE_PARENT_CODE); + String treeName = paramsObj.getString(GenConstants.TREE_NAME); + Long parentMenuId = paramsObj.getLongValue(GenConstants.PARENT_MENU_ID); + String parentMenuName = paramsObj.getString(GenConstants.PARENT_MENU_NAME); + boolean isView = paramsObj.getBooleanValue(GenConstants.GEN_VIEW); + + genTable.setTreeCode(treeCode); + genTable.setTreeParentCode(treeParentCode); + genTable.setTreeName(treeName); + genTable.setParentMenuId(parentMenuId); + genTable.setParentMenuName(parentMenuName); + genTable.setView(isView); + } + } + + /** + * 获取代码生成地址 + * + * @param table 业务表信息 + * @param template 模板文件路径 + * @return 生成地址 + */ + public static String getGenPath(GenTable table, String template) + { + String genPath = table.getGenPath(); + if (StringUtils.equals(genPath, "/")) + { + return System.getProperty("user.dir") + File.separator + "src" + File.separator + VelocityUtils.getFileName(template, table); + } + return genPath + File.separator + VelocityUtils.getFileName(template, table); + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/service/IGenTableColumnService.java b/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/service/IGenTableColumnService.java new file mode 100644 index 0000000..3037f70 --- /dev/null +++ b/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/service/IGenTableColumnService.java @@ -0,0 +1,44 @@ +package com.ruoyi.generator.service; + +import java.util.List; +import com.ruoyi.generator.domain.GenTableColumn; + +/** + * 业务字段 服务层 + * + * @author ruoyi + */ +public interface IGenTableColumnService +{ + /** + * 查询业务字段列表 + * + * @param tableId 业务字段编号 + * @return 业务字段集合 + */ + public List selectGenTableColumnListByTableId(Long tableId); + + /** + * 新增业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + public int insertGenTableColumn(GenTableColumn genTableColumn); + + /** + * 修改业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + public int updateGenTableColumn(GenTableColumn genTableColumn); + + /** + * 删除业务字段信息 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteGenTableColumnByIds(String ids); +} diff --git a/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/service/IGenTableService.java b/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/service/IGenTableService.java new file mode 100644 index 0000000..52d28c4 --- /dev/null +++ b/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/service/IGenTableService.java @@ -0,0 +1,131 @@ +package com.ruoyi.generator.service; + +import java.util.List; +import java.util.Map; +import com.ruoyi.generator.domain.GenTable; + +/** + * 业务 服务层 + * + * @author ruoyi + */ +public interface IGenTableService +{ + /** + * 查询业务列表 + * + * @param genTable 业务信息 + * @return 业务集合 + */ + public List selectGenTableList(GenTable genTable); + + /** + * 查询据库列表 + * + * @param genTable 业务信息 + * @return 数据库表集合 + */ + public List selectDbTableList(GenTable genTable); + + /** + * 查询据库列表 + * + * @param tableNames 表名称组 + * @return 数据库表集合 + */ + public List selectDbTableListByNames(String[] tableNames); + + /** + * 查询所有表信息 + * + * @return 表信息集合 + */ + public List selectGenTableAll(); + + /** + * 查询业务信息 + * + * @param id 业务ID + * @return 业务信息 + */ + public GenTable selectGenTableById(Long id); + + /** + * 修改业务 + * + * @param genTable 业务信息 + * @return 结果 + */ + public void updateGenTable(GenTable genTable); + + /** + * 删除业务信息 + * + * @param tableIds 需要删除的表数据ID + * @return 结果 + */ + public void deleteGenTableByIds(Long[] tableIds); + + /** + * 创建表 + * + * @param sql 创建表语句 + * @return 结果 + */ + public boolean createTable(String sql); + + /** + * 导入表结构 + * + * @param tableList 导入表列表 + * @param tplWebType 前端类型 + * @param operName 操作人员 + */ + public void importGenTable(List tableList, String tplWebType, String operName); + + /** + * 预览代码 + * + * @param tableId 表编号 + * @return 预览数据列表 + */ + public Map previewCode(Long tableId); + + /** + * 生成代码(下载方式) + * + * @param tableName 表名称 + * @return 数据 + */ + public byte[] downloadCode(String tableName); + + /** + * 生成代码(自定义路径) + * + * @param tableName 表名称 + * @return 数据 + */ + public void generatorCode(String tableName); + + /** + * 同步数据库 + * + * @param tableName 表名称 + */ + public void synchDb(String tableName); + + /** + * 批量生成代码(下载方式) + * + * @param tableNames 表数组 + * @return 数据 + */ + public byte[] downloadCode(String[] tableNames); + + /** + * 修改保存参数校验 + * + * @param genTable 业务信息 + */ + public void validateEdit(GenTable genTable); +} diff --git a/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/util/GenUtils.java b/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/util/GenUtils.java new file mode 100644 index 0000000..e7ebc20 --- /dev/null +++ b/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/util/GenUtils.java @@ -0,0 +1,257 @@ +package com.ruoyi.generator.util; + +import java.util.Arrays; +import org.apache.commons.lang3.RegExUtils; +import com.ruoyi.common.constant.GenConstants; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.generator.config.GenConfig; +import com.ruoyi.generator.domain.GenTable; +import com.ruoyi.generator.domain.GenTableColumn; + +/** + * 代码生成器 工具类 + * + * @author ruoyi + */ +public class GenUtils +{ + /** + * 初始化表信息 + */ + public static void initTable(GenTable genTable, String operName) + { + genTable.setClassName(convertClassName(genTable.getTableName())); + genTable.setPackageName(GenConfig.getPackageName()); + genTable.setModuleName(getModuleName(GenConfig.getPackageName())); + genTable.setBusinessName(getBusinessName(genTable.getTableName())); + genTable.setFunctionName(replaceText(genTable.getTableComment())); + genTable.setFunctionAuthor(GenConfig.getAuthor()); + genTable.setCreateBy(operName); + } + + /** + * 初始化列属性字段 + */ + public static void initColumnField(GenTableColumn column, GenTable table) + { + String dataType = getDbType(column.getColumnType()); + String columnName = column.getColumnName(); + column.setTableId(table.getTableId()); + column.setCreateBy(table.getCreateBy()); + // 设置java字段名 + column.setJavaField(StringUtils.toCamelCase(columnName)); + // 设置默认类型 + column.setJavaType(GenConstants.TYPE_STRING); + column.setQueryType(GenConstants.QUERY_EQ); + + if (arraysContains(GenConstants.COLUMNTYPE_STR, dataType) || arraysContains(GenConstants.COLUMNTYPE_TEXT, dataType)) + { + // 字符串长度超过500设置为文本域 + Integer columnLength = getColumnLength(column.getColumnType()); + String htmlType = columnLength >= 500 || arraysContains(GenConstants.COLUMNTYPE_TEXT, dataType) ? GenConstants.HTML_TEXTAREA : GenConstants.HTML_INPUT; + column.setHtmlType(htmlType); + } + else if (arraysContains(GenConstants.COLUMNTYPE_TIME, dataType)) + { + column.setJavaType(GenConstants.TYPE_DATE); + column.setHtmlType(GenConstants.HTML_DATETIME); + } + else if (arraysContains(GenConstants.COLUMNTYPE_NUMBER, dataType)) + { + column.setHtmlType(GenConstants.HTML_INPUT); + + // 如果是浮点型 统一用BigDecimal + String[] str = StringUtils.split(StringUtils.substringBetween(column.getColumnType(), "(", ")"), ","); + if (str != null && str.length == 2 && Integer.parseInt(str[1]) > 0) + { + column.setJavaType(GenConstants.TYPE_BIGDECIMAL); + } + // 如果是整形 + else if (str != null && str.length == 1 && Integer.parseInt(str[0]) <= 10) + { + column.setJavaType(GenConstants.TYPE_INTEGER); + } + // 长整形 + else + { + column.setJavaType(GenConstants.TYPE_LONG); + } + } + + // 插入字段(默认所有字段都需要插入) + column.setIsInsert(GenConstants.REQUIRE); + + // 编辑字段 + if (!arraysContains(GenConstants.COLUMNNAME_NOT_EDIT, columnName) && !column.isPk()) + { + column.setIsEdit(GenConstants.REQUIRE); + } + // 列表字段 + if (!arraysContains(GenConstants.COLUMNNAME_NOT_LIST, columnName) && !column.isPk()) + { + column.setIsList(GenConstants.REQUIRE); + } + // 查询字段 + if (!arraysContains(GenConstants.COLUMNNAME_NOT_QUERY, columnName) && !column.isPk()) + { + column.setIsQuery(GenConstants.REQUIRE); + } + + // 查询字段类型 + if (StringUtils.endsWithIgnoreCase(columnName, "name")) + { + column.setQueryType(GenConstants.QUERY_LIKE); + } + // 状态字段设置单选框 + if (StringUtils.endsWithIgnoreCase(columnName, "status")) + { + column.setHtmlType(GenConstants.HTML_RADIO); + } + // 类型&性别字段设置下拉框 + else if (StringUtils.endsWithIgnoreCase(columnName, "type") + || StringUtils.endsWithIgnoreCase(columnName, "sex")) + { + column.setHtmlType(GenConstants.HTML_SELECT); + } + // 图片字段设置图片上传控件 + else if (StringUtils.endsWithIgnoreCase(columnName, "image")) + { + column.setHtmlType(GenConstants.HTML_IMAGE_UPLOAD); + } + // 文件字段设置文件上传控件 + else if (StringUtils.endsWithIgnoreCase(columnName, "file")) + { + column.setHtmlType(GenConstants.HTML_FILE_UPLOAD); + } + // 内容字段设置富文本控件 + else if (StringUtils.endsWithIgnoreCase(columnName, "content")) + { + column.setHtmlType(GenConstants.HTML_EDITOR); + } + } + + /** + * 校验数组是否包含指定值 + * + * @param arr 数组 + * @param targetValue 值 + * @return 是否包含 + */ + public static boolean arraysContains(String[] arr, String targetValue) + { + return Arrays.asList(arr).contains(targetValue); + } + + /** + * 获取模块名 + * + * @param packageName 包名 + * @return 模块名 + */ + public static String getModuleName(String packageName) + { + int lastIndex = packageName.lastIndexOf("."); + int nameLength = packageName.length(); + return StringUtils.substring(packageName, lastIndex + 1, nameLength); + } + + /** + * 获取业务名 + * + * @param tableName 表名 + * @return 业务名 + */ + public static String getBusinessName(String tableName) + { + int lastIndex = tableName.lastIndexOf("_"); + int nameLength = tableName.length(); + return StringUtils.substring(tableName, lastIndex + 1, nameLength); + } + + /** + * 表名转换成Java类名 + * + * @param tableName 表名称 + * @return 类名 + */ + public static String convertClassName(String tableName) + { + boolean autoRemovePre = GenConfig.getAutoRemovePre(); + String tablePrefix = GenConfig.getTablePrefix(); + if (autoRemovePre && StringUtils.isNotEmpty(tablePrefix)) + { + String[] searchList = StringUtils.split(tablePrefix, ","); + tableName = replaceFirst(tableName, searchList); + } + return StringUtils.convertToCamelCase(tableName); + } + + /** + * 批量替换前缀 + * + * @param replacementm 替换值 + * @param searchList 替换列表 + * @return + */ + public static String replaceFirst(String replacementm, String[] searchList) + { + String text = replacementm; + for (String searchString : searchList) + { + if (replacementm.startsWith(searchString)) + { + text = replacementm.replaceFirst(searchString, ""); + break; + } + } + return text; + } + + /** + * 关键字替换 + * + * @param text 需要被替换的名字 + * @return 替换后的名字 + */ + public static String replaceText(String text) + { + return RegExUtils.replaceAll(text, "(?:表|若依)", ""); + } + + /** + * 获取数据库类型字段 + * + * @param columnType 列类型 + * @return 截取后的列类型 + */ + public static String getDbType(String columnType) + { + if (StringUtils.indexOf(columnType, "(") > 0) + { + return StringUtils.substringBefore(columnType, "("); + } + else + { + return columnType; + } + } + + /** + * 获取字段长度 + * + * @param columnType 列类型 + * @return 截取后的列类型 + */ + public static Integer getColumnLength(String columnType) + { + if (StringUtils.indexOf(columnType, "(") > 0) + { + String length = StringUtils.substringBetween(columnType, "(", ")"); + return Integer.valueOf(length); + } + else + { + return 0; + } + } +} diff --git a/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityInitializer.java b/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityInitializer.java new file mode 100644 index 0000000..9f69403 --- /dev/null +++ b/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityInitializer.java @@ -0,0 +1,34 @@ +package com.ruoyi.generator.util; + +import java.util.Properties; +import org.apache.velocity.app.Velocity; +import com.ruoyi.common.constant.Constants; + +/** + * VelocityEngine工厂 + * + * @author ruoyi + */ +public class VelocityInitializer +{ + /** + * 初始化vm方法 + */ + public static void initVelocity() + { + Properties p = new Properties(); + try + { + // 加载classpath目录下的vm文件 + p.setProperty("resource.loader.file.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader"); + // 定义字符集 + p.setProperty(Velocity.INPUT_ENCODING, Constants.UTF8); + // 初始化Velocity引擎,指定配置Properties + Velocity.init(p); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } +} diff --git a/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityUtils.java b/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityUtils.java new file mode 100644 index 0000000..cf6d4b6 --- /dev/null +++ b/ry-api/ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityUtils.java @@ -0,0 +1,492 @@ +package com.ruoyi.generator.util; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.apache.velocity.VelocityContext; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.ruoyi.common.constant.GenConstants; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.generator.domain.GenTable; +import com.ruoyi.generator.domain.GenTableColumn; + +/** + * 模板处理工具类 + * + * @author ruoyi + */ +public class VelocityUtils +{ + /** 项目空间路径 */ + private static final String PROJECT_PATH = "main/java"; + + /** mybatis空间路径 */ + private static final String MYBATIS_PATH = "main/resources/mapper"; + + /** 默认上级菜单,系统工具 */ + private static final String DEFAULT_PARENT_MENU_ID = "3"; + + /** Vue3 Element Plus 模版 */ + private static final String ELEMENT_PLUS = "element-plus"; + + /** Vue3 Element Plus TypeScript 模版 */ + private static final String ELEMENT_PLUS_TYPESSRIPT = "element-plus-typescript"; + + /** + * 设置模板变量信息 + * + * @return 模板列表 + */ + public static VelocityContext prepareContext(GenTable genTable) + { + String moduleName = genTable.getModuleName(); + String businessName = genTable.getBusinessName(); + String packageName = genTable.getPackageName(); + String tplCategory = genTable.getTplCategory(); + String functionName = genTable.getFunctionName(); + + VelocityContext velocityContext = new VelocityContext(); + velocityContext.put("tplCategory", genTable.getTplCategory()); + velocityContext.put("tableName", genTable.getTableName()); + velocityContext.put("functionName", StringUtils.isNotEmpty(functionName) ? functionName : "【请填写功能名称】"); + velocityContext.put("ClassName", genTable.getClassName()); + velocityContext.put("className", StringUtils.uncapitalize(genTable.getClassName())); + velocityContext.put("moduleName", genTable.getModuleName()); + velocityContext.put("BusinessName", StringUtils.capitalize(genTable.getBusinessName())); + velocityContext.put("businessName", genTable.getBusinessName()); + velocityContext.put("basePackage", getPackagePrefix(packageName)); + velocityContext.put("packageName", packageName); + velocityContext.put("author", genTable.getFunctionAuthor()); + velocityContext.put("colSpan", getColSpan(genTable.getFormColNum())); + velocityContext.put("datetime", DateUtils.getDate()); + velocityContext.put("pkColumn", genTable.getPkColumn()); + velocityContext.put("importList", getImportList(genTable)); + velocityContext.put("permissionPrefix", getPermissionPrefix(moduleName, businessName)); + velocityContext.put("columns", genTable.getColumns()); + velocityContext.put("table", genTable); + velocityContext.put("dicts", getDicts(genTable)); + setExtensionsContext(velocityContext, genTable.getOptions()); + setMenuVelocityContext(velocityContext, genTable); + if (GenConstants.TPL_TREE.equals(tplCategory)) + { + setTreeVelocityContext(velocityContext, genTable); + } + if (GenConstants.TPL_SUB.equals(tplCategory)) + { + setSubVelocityContext(velocityContext, genTable); + } + return velocityContext; + } + + public static void setExtensionsContext(VelocityContext context, String options) + { + JSONObject paramsObj = JSONObject.parseObject(options); + boolean genView = genView(paramsObj); + context.put("genView", genView); + } + + public static void setMenuVelocityContext(VelocityContext context, GenTable genTable) + { + String options = genTable.getOptions(); + JSONObject paramsObj = JSON.parseObject(options); + String parentMenuId = getParentMenuId(paramsObj); + context.put("parentMenuId", parentMenuId); + } + + public static void setTreeVelocityContext(VelocityContext context, GenTable genTable) + { + String options = genTable.getOptions(); + JSONObject paramsObj = JSON.parseObject(options); + String treeCode = getTreecode(paramsObj); + String treeParentCode = getTreeParentCode(paramsObj); + String treeName = getTreeName(paramsObj); + + context.put("treeCode", treeCode); + context.put("treeParentCode", treeParentCode); + context.put("treeName", treeName); + context.put("expandColumn", getExpandColumn(genTable)); + if (paramsObj.containsKey(GenConstants.TREE_PARENT_CODE)) + { + context.put("tree_parent_code", paramsObj.getString(GenConstants.TREE_PARENT_CODE)); + } + if (paramsObj.containsKey(GenConstants.TREE_NAME)) + { + context.put("tree_name", paramsObj.getString(GenConstants.TREE_NAME)); + } + } + + public static void setSubVelocityContext(VelocityContext context, GenTable genTable) + { + GenTable subTable = genTable.getSubTable(); + String subTableName = genTable.getSubTableName(); + String subTableFkName = genTable.getSubTableFkName(); + String subClassName = genTable.getSubTable().getClassName(); + String subTableFkClassName = StringUtils.convertToCamelCase(subTableFkName); + + context.put("subTable", subTable); + context.put("subTableName", subTableName); + context.put("subTableFkName", subTableFkName); + context.put("subTableFkClassName", subTableFkClassName); + context.put("subTableFkclassName", StringUtils.uncapitalize(subTableFkClassName)); + context.put("subClassName", subClassName); + context.put("subclassName", StringUtils.uncapitalize(subClassName)); + context.put("subImportList", getImportList(genTable.getSubTable())); + } + + /** + * 获取模板信息 + * @param tplCategory 生成的模板 + * @param tplWebType 前端类型 + * @return 模板列表 + */ + public static List getTemplateList(GenTable table) + { + String tplWebType = table.getTplWebType(); + String tplCategory = table.getTplCategory(); + JSONObject paramsObj = JSONObject.parseObject(table.getOptions()); + boolean isView = genView(paramsObj); + String useWebType = "vm/vue"; + String apiTemplate = "vm/js/api.js.vm"; + if (StringUtils.equals(ELEMENT_PLUS, tplWebType)) + { + useWebType = "vm/vue/v3"; + } + else if (StringUtils.equals(ELEMENT_PLUS_TYPESSRIPT, tplWebType)) + { + useWebType = "vm/vue/v3ts"; + apiTemplate = "vm/ts/api.ts.vm"; + } + List templates = new ArrayList(); + templates.add("vm/java/domain.java.vm"); + templates.add("vm/java/mapper.java.vm"); + templates.add("vm/java/service.java.vm"); + templates.add("vm/java/serviceImpl.java.vm"); + templates.add("vm/java/controller.java.vm"); + templates.add("vm/xml/mapper.xml.vm"); + templates.add("vm/sql/sql.vm"); + templates.add(apiTemplate); + if (StringUtils.equals(ELEMENT_PLUS_TYPESSRIPT, tplWebType)) + { + templates.add("vm/ts/type.ts.vm"); + templates.add("vm/ts/index.ts.vm"); + } + if (GenConstants.TPL_CRUD.equals(tplCategory)) + { + templates.add(useWebType + "/index.vue.vm"); + } + else if (GenConstants.TPL_TREE.equals(tplCategory)) + { + templates.add(useWebType + "/index-tree.vue.vm"); + } + else if (GenConstants.TPL_SUB.equals(tplCategory)) + { + templates.add(useWebType + "/index.vue.vm"); + templates.add("vm/java/sub-domain.java.vm"); + } + if (isView) + { + templates.add(useWebType + "/view.vue.vm"); + } + return templates; + } + + /** + * 获取文件名 + */ + public static String getFileName(String template, GenTable genTable) + { + // 文件名称 + String fileName = ""; + // 包路径 + String packageName = genTable.getPackageName(); + // 模块名 + String moduleName = genTable.getModuleName(); + // 大写类名 + String className = genTable.getClassName(); + // 业务名称 + String businessName = genTable.getBusinessName(); + + String javaPath = PROJECT_PATH + "/" + StringUtils.replace(packageName, ".", "/"); + String mybatisPath = MYBATIS_PATH + "/" + moduleName; + String vuePath = "vue"; + + if (template.contains("domain.java.vm")) + { + fileName = StringUtils.format("{}/domain/{}.java", javaPath, className); + } + if (template.contains("sub-domain.java.vm") && StringUtils.equals(GenConstants.TPL_SUB, genTable.getTplCategory())) + { + fileName = StringUtils.format("{}/domain/{}.java", javaPath, genTable.getSubTable().getClassName()); + } + else if (template.contains("mapper.java.vm")) + { + fileName = StringUtils.format("{}/mapper/{}Mapper.java", javaPath, className); + } + else if (template.contains("service.java.vm")) + { + fileName = StringUtils.format("{}/service/I{}Service.java", javaPath, className); + } + else if (template.contains("serviceImpl.java.vm")) + { + fileName = StringUtils.format("{}/service/impl/{}ServiceImpl.java", javaPath, className); + } + else if (template.contains("controller.java.vm")) + { + fileName = StringUtils.format("{}/controller/{}Controller.java", javaPath, className); + } + else if (template.contains("mapper.xml.vm")) + { + fileName = StringUtils.format("{}/{}Mapper.xml", mybatisPath, className); + } + else if (template.contains("sql.vm")) + { + fileName = businessName + "Menu.sql"; + } + else if (template.contains("api.js.vm")) + { + fileName = StringUtils.format("{}/api/{}/{}.js", vuePath, moduleName, businessName); + } + else if (template.contains("api.ts.vm")) + { + fileName = StringUtils.format("{}/api/{}/{}.ts", vuePath, moduleName, businessName); + } + else if (template.contains("type.ts.vm")) + { + fileName = StringUtils.format("{}/types/api/{}/{}.ts", vuePath, moduleName, businessName); + } + else if (template.contains("index.ts.vm")) + { + fileName = StringUtils.format("{}/types/api/index-bak.ts", vuePath); + } + else if (template.contains("index.vue.vm")) + { + fileName = StringUtils.format("{}/views/{}/{}/index.vue", vuePath, moduleName, businessName); + } + else if (template.contains("index-tree.vue.vm")) + { + fileName = StringUtils.format("{}/views/{}/{}/index.vue", vuePath, moduleName, businessName); + } + else if (template.contains("view.vue.vm")) + { + fileName = StringUtils.format("{}/views/{}/{}/view.vue", vuePath, moduleName, businessName); + } + return fileName; + } + + /** + * 获取包前缀 + * + * @param packageName 包名称 + * @return 包前缀名称 + */ + public static String getPackagePrefix(String packageName) + { + int lastIndex = packageName.lastIndexOf("."); + return StringUtils.substring(packageName, 0, lastIndex); + } + + /** + * 根据列类型获取导入包 + * + * @param genTable 业务表对象 + * @return 返回需要导入的包列表 + */ + public static HashSet getImportList(GenTable genTable) + { + List columns = genTable.getColumns(); + GenTable subGenTable = genTable.getSubTable(); + HashSet importList = new HashSet(); + if (StringUtils.isNotNull(subGenTable)) + { + importList.add("java.util.List"); + } + for (GenTableColumn column : columns) + { + if (!column.isSuperColumn() && GenConstants.TYPE_DATE.equals(column.getJavaType())) + { + importList.add("java.util.Date"); + importList.add("com.fasterxml.jackson.annotation.JsonFormat"); + } + else if (!column.isSuperColumn() && GenConstants.TYPE_BIGDECIMAL.equals(column.getJavaType())) + { + importList.add("java.math.BigDecimal"); + } + } + return importList; + } + + /** + * 根据列类型获取字典组 + * + * @param genTable 业务表对象 + * @return 返回字典组 + */ + public static String getDicts(GenTable genTable) + { + List columns = genTable.getColumns(); + Set dicts = new HashSet(); + addDicts(dicts, columns); + if (StringUtils.isNotNull(genTable.getSubTable())) + { + List subColumns = genTable.getSubTable().getColumns(); + addDicts(dicts, subColumns); + } + return StringUtils.join(dicts, ", "); + } + + /** + * 添加字典列表 + * + * @param dicts 字典列表 + * @param columns 列集合 + */ + public static void addDicts(Set dicts, List columns) + { + for (GenTableColumn column : columns) + { + if (!column.isSuperColumn() && StringUtils.isNotEmpty(column.getDictType()) && StringUtils.equalsAny( + column.getHtmlType(), + new String[] { GenConstants.HTML_SELECT, GenConstants.HTML_RADIO, GenConstants.HTML_CHECKBOX })) + { + dicts.add("'" + column.getDictType() + "'"); + } + } + } + + /** + * 获取权限前缀 + * + * @param moduleName 模块名称 + * @param businessName 业务名称 + * @return 返回权限前缀 + */ + public static String getPermissionPrefix(String moduleName, String businessName) + { + return StringUtils.format("{}:{}", moduleName, businessName); + } + + /** + * 获取上级菜单ID字段 + * + * @param paramsObj 生成其他选项 + * @return 上级菜单ID字段 + */ + public static String getParentMenuId(JSONObject paramsObj) + { + if (StringUtils.isNotEmpty(paramsObj) && paramsObj.containsKey(GenConstants.PARENT_MENU_ID) + && StringUtils.isNotEmpty(paramsObj.getString(GenConstants.PARENT_MENU_ID))) + { + return paramsObj.getString(GenConstants.PARENT_MENU_ID); + } + return DEFAULT_PARENT_MENU_ID; + } + + /** + * 获取树编码 + * + * @param paramsObj 生成其他选项 + * @return 树编码 + */ + public static String getTreecode(JSONObject paramsObj) + { + if (paramsObj.containsKey(GenConstants.TREE_CODE)) + { + return StringUtils.toCamelCase(paramsObj.getString(GenConstants.TREE_CODE)); + } + return StringUtils.EMPTY; + } + + /** + * 获取树父编码 + * + * @param paramsObj 生成其他选项 + * @return 树父编码 + */ + public static String getTreeParentCode(JSONObject paramsObj) + { + if (paramsObj.containsKey(GenConstants.TREE_PARENT_CODE)) + { + return StringUtils.toCamelCase(paramsObj.getString(GenConstants.TREE_PARENT_CODE)); + } + return StringUtils.EMPTY; + } + + /** + * 扩展功能/生成详情页 + * + * @param paramsObj 生成其他选项 + * @return 是否生成详细页 + */ + public static boolean genView(JSONObject paramsObj) + { + if (StringUtils.isNotNull(paramsObj) && paramsObj.containsKey(GenConstants.GEN_VIEW)) + { + return paramsObj.getBoolean(GenConstants.GEN_VIEW); + } + return false; + } + + /** + * 获取树名称 + * + * @param paramsObj 生成其他选项 + * @return 树名称 + */ + public static String getTreeName(JSONObject paramsObj) + { + if (paramsObj.containsKey(GenConstants.TREE_NAME)) + { + return StringUtils.toCamelCase(paramsObj.getString(GenConstants.TREE_NAME)); + } + return StringUtils.EMPTY; + } + + /** + * 获取需要在哪一列上面显示展开按钮 + * + * @param genTable 业务表对象 + * @return 展开按钮列序号 + */ + public static int getExpandColumn(GenTable genTable) + { + String options = genTable.getOptions(); + JSONObject paramsObj = JSON.parseObject(options); + String treeName = paramsObj.getString(GenConstants.TREE_NAME); + int num = 0; + for (GenTableColumn column : genTable.getColumns()) + { + if (column.isList()) + { + num++; + String columnName = column.getColumnName(); + if (columnName.equals(treeName)) + { + break; + } + } + } + return num; + } + + /** + * 获取表单 el-col span + * + * @param formColNum 表单布局方式(1单列 2双列 3三列) + * @return span 数值字符串 + */ + public static String getColSpan(int formColNum) + { + if (formColNum == 2) + { + return "12"; + } + else if (formColNum == 3) + { + return "8"; + } + return "24"; + } +} diff --git a/ry-api/ruoyi-generator/src/main/resources/generator.yml b/ry-api/ruoyi-generator/src/main/resources/generator.yml new file mode 100644 index 0000000..d1cbf48 --- /dev/null +++ b/ry-api/ruoyi-generator/src/main/resources/generator.yml @@ -0,0 +1,12 @@ +# 代码生成 +gen: + # 作者 + author: ruoyi + # 默认生成包路径 system 需改成自己的模块名称 如 system monitor tool + packageName: com.ruoyi.system + # 自动去除表前缀,默认是false + autoRemovePre: false + # 表前缀(生成类名不会包含表前缀,多个用逗号分隔) + tablePrefix: sys_ + # 是否允许生成文件覆盖到本地(自定义路径),默认不允许 + allowOverwrite: false \ No newline at end of file diff --git a/ry-api/ruoyi-generator/src/main/resources/mapper/generator/GenTableColumnMapper.xml b/ry-api/ruoyi-generator/src/main/resources/mapper/generator/GenTableColumnMapper.xml new file mode 100644 index 0000000..32e01ff --- /dev/null +++ b/ry-api/ruoyi-generator/src/main/resources/mapper/generator/GenTableColumnMapper.xml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select column_id, table_id, column_name, column_comment, column_type, java_type, java_field, is_pk, is_increment, is_required, is_insert, is_edit, is_list, is_query, query_type, html_type, dict_type, sort, create_by, create_time, update_by, update_time from gen_table_column + + + + + + + + insert into gen_table_column ( + table_id, + column_name, + column_comment, + column_type, + java_type, + java_field, + is_pk, + is_increment, + is_required, + is_insert, + is_edit, + is_list, + is_query, + query_type, + html_type, + dict_type, + sort, + create_by, + create_time + )values( + #{tableId}, + #{columnName}, + #{columnComment}, + #{columnType}, + #{javaType}, + #{javaField}, + #{isPk}, + #{isIncrement}, + #{isRequired}, + #{isInsert}, + #{isEdit}, + #{isList}, + #{isQuery}, + #{queryType}, + #{htmlType}, + #{dictType}, + #{sort}, + #{createBy}, + sysdate() + ) + + + + update gen_table_column + + column_comment = #{columnComment}, + java_type = #{javaType}, + column_type = #{columnType}, + java_field = #{javaField}, + is_insert = #{isInsert}, + is_edit = #{isEdit}, + is_list = #{isList}, + is_query = #{isQuery}, + is_required = #{isRequired}, + query_type = #{queryType}, + html_type = #{htmlType}, + dict_type = #{dictType}, + sort = #{sort}, + update_by = #{updateBy}, + update_time = sysdate() + + where column_id = #{columnId} + + + + delete from gen_table_column where table_id in + + #{tableId} + + + + + delete from gen_table_column where column_id in + + #{item.columnId} + + + + \ No newline at end of file diff --git a/ry-api/ruoyi-generator/src/main/resources/mapper/generator/GenTableMapper.xml b/ry-api/ruoyi-generator/src/main/resources/mapper/generator/GenTableMapper.xml new file mode 100644 index 0000000..c27b71b --- /dev/null +++ b/ry-api/ruoyi-generator/src/main/resources/mapper/generator/GenTableMapper.xml @@ -0,0 +1,214 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select table_id, table_name, table_comment, sub_table_name, sub_table_fk_name, class_name, tpl_category, tpl_web_type, package_name, module_name, business_name, function_name, function_author, form_col_num, gen_type, gen_path, options, create_by, create_time, update_by, update_time, remark from gen_table + + + + + + + + + + + + + + + + + + insert into gen_table ( + table_name, + table_comment, + class_name, + tpl_category, + tpl_web_type, + package_name, + module_name, + business_name, + function_name, + function_author, + form_col_num, + gen_type, + gen_path, + remark, + create_by, + create_time + )values( + #{tableName}, + #{tableComment}, + #{className}, + #{tplCategory}, + #{tplWebType}, + #{packageName}, + #{moduleName}, + #{businessName}, + #{functionName}, + #{functionAuthor}, + #{formColNum}, + #{genType}, + #{genPath}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + ${sql} + + + + update gen_table + + table_name = #{tableName}, + table_comment = #{tableComment}, + sub_table_name = #{subTableName}, + sub_table_fk_name = #{subTableFkName}, + class_name = #{className}, + function_author = #{functionAuthor}, + form_col_num = #{formColNum}, + gen_type = #{genType}, + gen_path = #{genPath}, + tpl_category = #{tplCategory}, + tpl_web_type = #{tplWebType}, + package_name = #{packageName}, + module_name = #{moduleName}, + business_name = #{businessName}, + function_name = #{functionName}, + options = #{options}, + update_by = #{updateBy}, + remark = #{remark}, + update_time = sysdate() + + where table_id = #{tableId} + + + + delete from gen_table where table_id in + + #{tableId} + + + + \ No newline at end of file diff --git a/ry-api/ruoyi-generator/src/main/resources/vm/java/controller.java.vm b/ry-api/ruoyi-generator/src/main/resources/vm/java/controller.java.vm new file mode 100644 index 0000000..b8457a3 --- /dev/null +++ b/ry-api/ruoyi-generator/src/main/resources/vm/java/controller.java.vm @@ -0,0 +1,115 @@ +package ${packageName}.controller; + +import java.util.List; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.enums.BusinessType; +import ${packageName}.domain.${ClassName}; +import ${packageName}.service.I${ClassName}Service; +import com.ruoyi.common.utils.poi.ExcelUtil; +#if($table.crud || $table.sub) +import com.ruoyi.common.core.page.TableDataInfo; +#elseif($table.tree) +#end + +/** + * ${functionName}Controller + * + * @author ${author} + * @date ${datetime} + */ +@RestController +@RequestMapping("/${moduleName}/${businessName}") +public class ${ClassName}Controller extends BaseController +{ + @Autowired + private I${ClassName}Service ${className}Service; + + /** + * 查询${functionName}列表 + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:list')") + @GetMapping("/list") +#if($table.crud || $table.sub) + public TableDataInfo list(${ClassName} ${className}) + { + startPage(); + List<${ClassName}> list = ${className}Service.select${ClassName}List(${className}); + return getDataTable(list); + } +#elseif($table.tree) + public AjaxResult list(${ClassName} ${className}) + { + List<${ClassName}> list = ${className}Service.select${ClassName}List(${className}); + return success(list); + } +#end + + /** + * 导出${functionName}列表 + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:export')") + @Log(title = "${functionName}", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, ${ClassName} ${className}) + { + List<${ClassName}> list = ${className}Service.select${ClassName}List(${className}); + ExcelUtil<${ClassName}> util = new ExcelUtil<${ClassName}>(${ClassName}.class); + util.exportExcel(response, list, "${functionName}数据"); + } + + /** + * 获取${functionName}详细信息 + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:query')") + @GetMapping(value = "/{${pkColumn.javaField}}") + public AjaxResult getInfo(@PathVariable("${pkColumn.javaField}") ${pkColumn.javaType} ${pkColumn.javaField}) + { + return success(${className}Service.select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField})); + } + + /** + * 新增${functionName} + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:add')") + @Log(title = "${functionName}", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody ${ClassName} ${className}) + { + return toAjax(${className}Service.insert${ClassName}(${className})); + } + + /** + * 修改${functionName} + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:edit')") + @Log(title = "${functionName}", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody ${ClassName} ${className}) + { + return toAjax(${className}Service.update${ClassName}(${className})); + } + + /** + * 删除${functionName} + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:remove')") + @Log(title = "${functionName}", businessType = BusinessType.DELETE) + @DeleteMapping("/{${pkColumn.javaField}s}") + public AjaxResult remove(@PathVariable ${pkColumn.javaType}[] ${pkColumn.javaField}s) + { + return toAjax(${className}Service.delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaField}s)); + } +} diff --git a/ry-api/ruoyi-generator/src/main/resources/vm/java/domain.java.vm b/ry-api/ruoyi-generator/src/main/resources/vm/java/domain.java.vm new file mode 100644 index 0000000..b236ef0 --- /dev/null +++ b/ry-api/ruoyi-generator/src/main/resources/vm/java/domain.java.vm @@ -0,0 +1,105 @@ +package ${packageName}.domain; + +#foreach ($import in $importList) +import ${import}; +#end +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +#if($table.crud || $table.sub) +import com.ruoyi.common.core.domain.BaseEntity; +#elseif($table.tree) +import com.ruoyi.common.core.domain.TreeEntity; +#end + +/** + * ${functionName}对象 ${tableName} + * + * @author ${author} + * @date ${datetime} + */ +#if($table.crud || $table.sub) +#set($Entity="BaseEntity") +#elseif($table.tree) +#set($Entity="TreeEntity") +#end +public class ${ClassName} extends ${Entity} +{ + private static final long serialVersionUID = 1L; + +#foreach ($column in $columns) +#if(!$table.isSuperColumn($column.javaField)) + /** $column.columnComment */ +#if($column.list) +#set($parentheseIndex=$column.columnComment.indexOf("(")) +#if($parentheseIndex != -1) +#set($comment=$column.columnComment.substring(0, $parentheseIndex)) +#else +#set($comment=$column.columnComment) +#end +#if($parentheseIndex != -1) + @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") +#elseif($column.javaType == 'Date') + @JsonFormat(pattern = "yyyy-MM-dd") + @Excel(name = "${comment}", width = 30, dateFormat = "yyyy-MM-dd") +#else + @Excel(name = "${comment}") +#end +#end + private $column.javaType $column.javaField; + +#end +#end +#if($table.sub) + /** $table.subTable.functionName信息 */ + private List<${subClassName}> ${subclassName}List; + +#end +#foreach ($column in $columns) +#if(!$table.isSuperColumn($column.javaField)) +#if($column.javaField.length() > 2 && $column.javaField.substring(1,2).matches("[A-Z]")) +#set($AttrName=$column.javaField) +#else +#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) +#end + public void set${AttrName}($column.javaType $column.javaField) + { + this.$column.javaField = $column.javaField; + } + + public $column.javaType get${AttrName}() + { + return $column.javaField; + } + +#end +#end +#if($table.sub) + public List<${subClassName}> get${subClassName}List() + { + return ${subclassName}List; + } + + public void set${subClassName}List(List<${subClassName}> ${subclassName}List) + { + this.${subclassName}List = ${subclassName}List; + } + +#end + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) +#foreach ($column in $columns) +#if($column.javaField.length() > 2 && $column.javaField.substring(1,2).matches("[A-Z]")) +#set($AttrName=$column.javaField) +#else +#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) +#end + .append("${column.javaField}", get${AttrName}()) +#end +#if($table.sub) + .append("${subclassName}List", get${subClassName}List()) +#end + .toString(); + } +} diff --git a/ry-api/ruoyi-generator/src/main/resources/vm/java/mapper.java.vm b/ry-api/ruoyi-generator/src/main/resources/vm/java/mapper.java.vm new file mode 100644 index 0000000..7e7d7c2 --- /dev/null +++ b/ry-api/ruoyi-generator/src/main/resources/vm/java/mapper.java.vm @@ -0,0 +1,91 @@ +package ${packageName}.mapper; + +import java.util.List; +import ${packageName}.domain.${ClassName}; +#if($table.sub) +import ${packageName}.domain.${subClassName}; +#end + +/** + * ${functionName}Mapper接口 + * + * @author ${author} + * @date ${datetime} + */ +public interface ${ClassName}Mapper +{ + /** + * 查询${functionName} + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return ${functionName} + */ + public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); + + /** + * 查询${functionName}列表 + * + * @param ${className} ${functionName} + * @return ${functionName}集合 + */ + public List<${ClassName}> select${ClassName}List(${ClassName} ${className}); + + /** + * 新增${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + public int insert${ClassName}(${ClassName} ${className}); + + /** + * 修改${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + public int update${ClassName}(${ClassName} ${className}); + + /** + * 删除${functionName} + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return 结果 + */ + public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); + + /** + * 批量删除${functionName} + * + * @param ${pkColumn.javaField}s 需要删除的数据主键集合 + * @return 结果 + */ + public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s); +#if($table.sub) + + /** + * 批量删除${subTable.functionName} + * + * @param ${pkColumn.javaField}s 需要删除的数据主键集合 + * @return 结果 + */ + public int delete${subClassName}By${subTableFkClassName}s(${pkColumn.javaType}[] ${pkColumn.javaField}s); + + /** + * 批量新增${subTable.functionName} + * + * @param ${subclassName}List ${subTable.functionName}列表 + * @return 结果 + */ + public int batch${subClassName}(List<${subClassName}> ${subclassName}List); + + + /** + * 通过${functionName}主键删除${subTable.functionName}信息 + * + * @param ${pkColumn.javaField} ${functionName}ID + * @return 结果 + */ + public int delete${subClassName}By${subTableFkClassName}(${pkColumn.javaType} ${pkColumn.javaField}); +#end +} diff --git a/ry-api/ruoyi-generator/src/main/resources/vm/java/service.java.vm b/ry-api/ruoyi-generator/src/main/resources/vm/java/service.java.vm new file mode 100644 index 0000000..264882b --- /dev/null +++ b/ry-api/ruoyi-generator/src/main/resources/vm/java/service.java.vm @@ -0,0 +1,61 @@ +package ${packageName}.service; + +import java.util.List; +import ${packageName}.domain.${ClassName}; + +/** + * ${functionName}Service接口 + * + * @author ${author} + * @date ${datetime} + */ +public interface I${ClassName}Service +{ + /** + * 查询${functionName} + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return ${functionName} + */ + public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); + + /** + * 查询${functionName}列表 + * + * @param ${className} ${functionName} + * @return ${functionName}集合 + */ + public List<${ClassName}> select${ClassName}List(${ClassName} ${className}); + + /** + * 新增${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + public int insert${ClassName}(${ClassName} ${className}); + + /** + * 修改${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + public int update${ClassName}(${ClassName} ${className}); + + /** + * 批量删除${functionName} + * + * @param ${pkColumn.javaField}s 需要删除的${functionName}主键集合 + * @return 结果 + */ + public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s); + + /** + * 删除${functionName}信息 + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return 结果 + */ + public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); +} diff --git a/ry-api/ruoyi-generator/src/main/resources/vm/java/serviceImpl.java.vm b/ry-api/ruoyi-generator/src/main/resources/vm/java/serviceImpl.java.vm new file mode 100644 index 0000000..14746e1 --- /dev/null +++ b/ry-api/ruoyi-generator/src/main/resources/vm/java/serviceImpl.java.vm @@ -0,0 +1,169 @@ +package ${packageName}.service.impl; + +import java.util.List; +#foreach ($column in $columns) +#if($column.javaField == 'createTime' || $column.javaField == 'updateTime') +import com.ruoyi.common.utils.DateUtils; +#break +#end +#end +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +#if($table.sub) +import java.util.ArrayList; +import com.ruoyi.common.utils.StringUtils; +import org.springframework.transaction.annotation.Transactional; +import ${packageName}.domain.${subClassName}; +#end +import ${packageName}.mapper.${ClassName}Mapper; +import ${packageName}.domain.${ClassName}; +import ${packageName}.service.I${ClassName}Service; + +/** + * ${functionName}Service业务层处理 + * + * @author ${author} + * @date ${datetime} + */ +@Service +public class ${ClassName}ServiceImpl implements I${ClassName}Service +{ + @Autowired + private ${ClassName}Mapper ${className}Mapper; + + /** + * 查询${functionName} + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return ${functionName} + */ + @Override + public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}) + { + return ${className}Mapper.select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField}); + } + + /** + * 查询${functionName}列表 + * + * @param ${className} ${functionName} + * @return ${functionName} + */ + @Override + public List<${ClassName}> select${ClassName}List(${ClassName} ${className}) + { + return ${className}Mapper.select${ClassName}List(${className}); + } + + /** + * 新增${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ +#if($table.sub) + @Transactional +#end + @Override + public int insert${ClassName}(${ClassName} ${className}) + { +#foreach ($column in $columns) +#if($column.javaField == 'createTime') + ${className}.setCreateTime(DateUtils.getNowDate()); +#end +#end +#if($table.sub) + int rows = ${className}Mapper.insert${ClassName}(${className}); + insert${subClassName}(${className}); + return rows; +#else + return ${className}Mapper.insert${ClassName}(${className}); +#end + } + + /** + * 修改${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ +#if($table.sub) + @Transactional +#end + @Override + public int update${ClassName}(${ClassName} ${className}) + { +#foreach ($column in $columns) +#if($column.javaField == 'updateTime') + ${className}.setUpdateTime(DateUtils.getNowDate()); +#end +#end +#if($table.sub) + ${className}Mapper.delete${subClassName}By${subTableFkClassName}(${className}.get${pkColumn.capJavaField}()); + insert${subClassName}(${className}); +#end + return ${className}Mapper.update${ClassName}(${className}); + } + + /** + * 批量删除${functionName} + * + * @param ${pkColumn.javaField}s 需要删除的${functionName}主键 + * @return 结果 + */ +#if($table.sub) + @Transactional +#end + @Override + public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s) + { +#if($table.sub) + ${className}Mapper.delete${subClassName}By${subTableFkClassName}s(${pkColumn.javaField}s); +#end + return ${className}Mapper.delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaField}s); + } + + /** + * 删除${functionName}信息 + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return 结果 + */ +#if($table.sub) + @Transactional +#end + @Override + public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}) + { +#if($table.sub) + ${className}Mapper.delete${subClassName}By${subTableFkClassName}(${pkColumn.javaField}); +#end + return ${className}Mapper.delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField}); + } +#if($table.sub) + + /** + * 新增${subTable.functionName}信息 + * + * @param ${className} ${functionName}对象 + */ + public void insert${subClassName}(${ClassName} ${className}) + { + List<${subClassName}> ${subclassName}List = ${className}.get${subClassName}List(); + ${pkColumn.javaType} ${pkColumn.javaField} = ${className}.get${pkColumn.capJavaField}(); + if (StringUtils.isNotNull(${subclassName}List)) + { + List<${subClassName}> list = new ArrayList<${subClassName}>(); + for (${subClassName} ${subclassName} : ${subclassName}List) + { + ${subclassName}.set${subTableFkClassName}(${pkColumn.javaField}); + list.add(${subclassName}); + } + if (list.size() > 0) + { + ${className}Mapper.batch${subClassName}(list); + } + } + } +#end +} diff --git a/ry-api/ruoyi-generator/src/main/resources/vm/java/sub-domain.java.vm b/ry-api/ruoyi-generator/src/main/resources/vm/java/sub-domain.java.vm new file mode 100644 index 0000000..a3f53eb --- /dev/null +++ b/ry-api/ruoyi-generator/src/main/resources/vm/java/sub-domain.java.vm @@ -0,0 +1,76 @@ +package ${packageName}.domain; + +#foreach ($import in $subImportList) +import ${import}; +#end +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * ${subTable.functionName}对象 ${subTableName} + * + * @author ${author} + * @date ${datetime} + */ +public class ${subClassName} extends BaseEntity +{ + private static final long serialVersionUID = 1L; + +#foreach ($column in $subTable.columns) +#if(!$table.isSuperColumn($column.javaField)) + /** $column.columnComment */ +#if($column.list) +#set($parentheseIndex=$column.columnComment.indexOf("(")) +#if($parentheseIndex != -1) +#set($comment=$column.columnComment.substring(0, $parentheseIndex)) +#else +#set($comment=$column.columnComment) +#end +#if($parentheseIndex != -1) + @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") +#elseif($column.javaType == 'Date') + @JsonFormat(pattern = "yyyy-MM-dd") + @Excel(name = "${comment}", width = 30, dateFormat = "yyyy-MM-dd") +#else + @Excel(name = "${comment}") +#end +#end + private $column.javaType $column.javaField; + +#end +#end +#foreach ($column in $subTable.columns) +#if(!$table.isSuperColumn($column.javaField)) +#if($column.javaField.length() > 2 && $column.javaField.substring(1,2).matches("[A-Z]")) +#set($AttrName=$column.javaField) +#else +#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) +#end + public void set${AttrName}($column.javaType $column.javaField) + { + this.$column.javaField = $column.javaField; + } + + public $column.javaType get${AttrName}() + { + return $column.javaField; + } +#end +#end + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) +#foreach ($column in $subTable.columns) +#if($column.javaField.length() > 2 && $column.javaField.substring(1,2).matches("[A-Z]")) +#set($AttrName=$column.javaField) +#else +#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) +#end + .append("${column.javaField}", get${AttrName}()) +#end + .toString(); + } +} diff --git a/ry-api/ruoyi-generator/src/main/resources/vm/js/api.js.vm b/ry-api/ruoyi-generator/src/main/resources/vm/js/api.js.vm new file mode 100644 index 0000000..9295524 --- /dev/null +++ b/ry-api/ruoyi-generator/src/main/resources/vm/js/api.js.vm @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询${functionName}列表 +export function list${BusinessName}(query) { + return request({ + url: '/${moduleName}/${businessName}/list', + method: 'get', + params: query + }) +} + +// 查询${functionName}详细 +export function get${BusinessName}(${pkColumn.javaField}) { + return request({ + url: '/${moduleName}/${businessName}/' + ${pkColumn.javaField}, + method: 'get' + }) +} + +// 新增${functionName} +export function add${BusinessName}(data) { + return request({ + url: '/${moduleName}/${businessName}', + method: 'post', + data: data + }) +} + +// 修改${functionName} +export function update${BusinessName}(data) { + return request({ + url: '/${moduleName}/${businessName}', + method: 'put', + data: data + }) +} + +// 删除${functionName} +export function del${BusinessName}(${pkColumn.javaField}) { + return request({ + url: '/${moduleName}/${businessName}/' + ${pkColumn.javaField}, + method: 'delete' + }) +} diff --git a/ry-api/ruoyi-generator/src/main/resources/vm/sql/sql.vm b/ry-api/ruoyi-generator/src/main/resources/vm/sql/sql.vm new file mode 100644 index 0000000..0575583 --- /dev/null +++ b/ry-api/ruoyi-generator/src/main/resources/vm/sql/sql.vm @@ -0,0 +1,22 @@ +-- 菜单 SQL +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}', '${parentMenuId}', '1', '${businessName}', '${moduleName}/${businessName}/index', 1, 0, 'C', '0', '0', '${permissionPrefix}:list', '#', 'admin', sysdate(), '', null, '${functionName}菜单'); + +-- 按钮父菜单ID +SELECT @parentId := LAST_INSERT_ID(); + +-- 按钮 SQL +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}查询', @parentId, '1', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:query', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}新增', @parentId, '2', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:add', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}修改', @parentId, '3', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:edit', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}删除', @parentId, '4', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:remove', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}导出', @parentId, '5', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:export', '#', 'admin', sysdate(), '', null, ''); \ No newline at end of file diff --git a/ry-api/ruoyi-generator/src/main/resources/vm/ts/api.ts.vm b/ry-api/ruoyi-generator/src/main/resources/vm/ts/api.ts.vm new file mode 100644 index 0000000..8ed5122 --- /dev/null +++ b/ry-api/ruoyi-generator/src/main/resources/vm/ts/api.ts.vm @@ -0,0 +1,51 @@ +import request from '@/utils/request' +import type { AjaxResult, TableDataInfo, ${BusinessName}QueryParams, ${ClassName} } from '@/types' + +// 查询${functionName}列表 +#if($table.tree) +export function list${BusinessName}(query?: ${BusinessName}QueryParams): Promise> { +#else +export function list${BusinessName}(query: ${BusinessName}QueryParams): Promise> { +#end + return request({ + url: '/${moduleName}/${businessName}/list', + method: 'get', + params: query + }) +} + +// 查询${functionName}详细 +export function get${BusinessName}(${pkColumn.javaField}: number): Promise> { + return request({ + url: '/${moduleName}/${businessName}/' + ${pkColumn.javaField}, + method: 'get' + }) +} + +// 新增${functionName} +export function add${BusinessName}(data: ${ClassName}): Promise { + return request({ + url: '/${moduleName}/${businessName}', + method: 'post', + data: data + }) +} + +// 修改${functionName} +export function update${BusinessName}(data: ${ClassName}): Promise { + return request({ + url: '/${moduleName}/${businessName}', + method: 'put', + data: data + }) +} + +// 删除${functionName} +export function del${BusinessName}(${pkColumn.javaField}: number | number[]): Promise { + return request({ + url: '/${moduleName}/${businessName}/' + ${pkColumn.javaField}, + method: 'delete' + }) +} + + diff --git a/ry-api/ruoyi-generator/src/main/resources/vm/ts/index.ts.vm b/ry-api/ruoyi-generator/src/main/resources/vm/ts/index.ts.vm new file mode 100644 index 0000000..e51ef15 --- /dev/null +++ b/ry-api/ruoyi-generator/src/main/resources/vm/ts/index.ts.vm @@ -0,0 +1,9 @@ +/** + * API 类型统一导出 + */ +.... + +// 防止覆盖,需手动追加下面代码到index.ts文件中,追加好后此文件可删除 + +// ${moduleName} 模块 +export * from "./${moduleName}/${businessName}"; \ No newline at end of file diff --git a/ry-api/ruoyi-generator/src/main/resources/vm/ts/type.ts.vm b/ry-api/ruoyi-generator/src/main/resources/vm/ts/type.ts.vm new file mode 100644 index 0000000..fff1d4c --- /dev/null +++ b/ry-api/ruoyi-generator/src/main/resources/vm/ts/type.ts.vm @@ -0,0 +1,51 @@ +import type { PageDomain, BaseEntity } from "../common"; + +/** ${functionName}配置分页查询参数 */ +export interface ${BusinessName}QueryParams extends PageDomain { +#foreach($column in $columns) +#if($column.query) +#set($type = "string") +#if($column.javaType == "Long" || $column.javaType == "Integer") + #set($type = "number") +#elseif($column.javaType == "Boolean") + #set($type = "boolean") +#end + /** ${column.columnComment} */ + ${column.javaField}?: ${type}; +#end +#end +} + +/** ${functionName}配置信息 */ +export interface ${ClassName} extends BaseEntity { +#foreach($column in $columns) +#set($type = "string") +#if($column.javaType == "Long" || $column.javaType == "Integer") + #set($type = "number") +#elseif($column.javaType == "Boolean") + #set($type = "boolean") +#end + /** ${column.columnComment} */ + ${column.javaField}?: ${type}; +#end +#if($table.sub) + /** $table.subTable.functionName信息 */ + ${subclassName}List?: ${subClassName}[]; +#end +} +#if($table.sub) + +/** ${subTable.functionName}配置信息 */ +export interface ${subClassName} extends BaseEntity { +#foreach ($column in $subTable.columns) +#set($type = "string") +#if($column.javaType == "Long" || $column.javaType == "Integer") + #set($type = "number") +#elseif($column.javaType == "Boolean") + #set($type = "boolean") +#end + /** ${column.columnComment} */ + ${column.javaField}?: ${type}; +#end +} +#end diff --git a/ry-api/ruoyi-generator/src/main/resources/vm/vue/index-tree.vue.vm b/ry-api/ruoyi-generator/src/main/resources/vm/vue/index-tree.vue.vm new file mode 100644 index 0000000..7a2d0e9 --- /dev/null +++ b/ry-api/ruoyi-generator/src/main/resources/vm/vue/index-tree.vue.vm @@ -0,0 +1,565 @@ + + + diff --git a/ry-api/ruoyi-generator/src/main/resources/vm/vue/index.vue.vm b/ry-api/ruoyi-generator/src/main/resources/vm/vue/index.vue.vm new file mode 100644 index 0000000..876dae3 --- /dev/null +++ b/ry-api/ruoyi-generator/src/main/resources/vm/vue/index.vue.vm @@ -0,0 +1,660 @@ + + + diff --git a/ry-api/ruoyi-generator/src/main/resources/vm/vue/v3/index-tree.vue.vm b/ry-api/ruoyi-generator/src/main/resources/vm/vue/v3/index-tree.vue.vm new file mode 100644 index 0000000..51e2673 --- /dev/null +++ b/ry-api/ruoyi-generator/src/main/resources/vm/vue/v3/index-tree.vue.vm @@ -0,0 +1,526 @@ + + + diff --git a/ry-api/ruoyi-generator/src/main/resources/vm/vue/v3/index.vue.vm b/ry-api/ruoyi-generator/src/main/resources/vm/vue/v3/index.vue.vm new file mode 100644 index 0000000..6680775 --- /dev/null +++ b/ry-api/ruoyi-generator/src/main/resources/vm/vue/v3/index.vue.vm @@ -0,0 +1,639 @@ + + + diff --git a/ry-api/ruoyi-generator/src/main/resources/vm/vue/v3/view.vue.vm b/ry-api/ruoyi-generator/src/main/resources/vm/vue/v3/view.vue.vm new file mode 100644 index 0000000..e486c88 --- /dev/null +++ b/ry-api/ruoyi-generator/src/main/resources/vm/vue/v3/view.vue.vm @@ -0,0 +1,83 @@ + + + diff --git a/ry-api/ruoyi-generator/src/main/resources/vm/vue/v3ts/index-tree.vue.vm b/ry-api/ruoyi-generator/src/main/resources/vm/vue/v3ts/index-tree.vue.vm new file mode 100644 index 0000000..038165e --- /dev/null +++ b/ry-api/ruoyi-generator/src/main/resources/vm/vue/v3ts/index-tree.vue.vm @@ -0,0 +1,528 @@ + + + diff --git a/ry-api/ruoyi-generator/src/main/resources/vm/vue/v3ts/index.vue.vm b/ry-api/ruoyi-generator/src/main/resources/vm/vue/v3ts/index.vue.vm new file mode 100644 index 0000000..6d0e0d0 --- /dev/null +++ b/ry-api/ruoyi-generator/src/main/resources/vm/vue/v3ts/index.vue.vm @@ -0,0 +1,644 @@ + + + diff --git a/ry-api/ruoyi-generator/src/main/resources/vm/vue/v3ts/view.vue.vm b/ry-api/ruoyi-generator/src/main/resources/vm/vue/v3ts/view.vue.vm new file mode 100644 index 0000000..25bd7f4 --- /dev/null +++ b/ry-api/ruoyi-generator/src/main/resources/vm/vue/v3ts/view.vue.vm @@ -0,0 +1,84 @@ + + + diff --git a/ry-api/ruoyi-generator/src/main/resources/vm/vue/view.vue.vm b/ry-api/ruoyi-generator/src/main/resources/vm/vue/view.vue.vm new file mode 100644 index 0000000..9911bc8 --- /dev/null +++ b/ry-api/ruoyi-generator/src/main/resources/vm/vue/view.vue.vm @@ -0,0 +1,86 @@ + + + diff --git a/ry-api/ruoyi-generator/src/main/resources/vm/xml/mapper.xml.vm b/ry-api/ruoyi-generator/src/main/resources/vm/xml/mapper.xml.vm new file mode 100644 index 0000000..456755b --- /dev/null +++ b/ry-api/ruoyi-generator/src/main/resources/vm/xml/mapper.xml.vm @@ -0,0 +1,140 @@ + + + + + +#foreach ($column in $columns) + +#end + +#if($table.sub) + + + + + + +#foreach ($column in $subTable.columns) + +#end + +#end + + + select#foreach($column in $columns) $column.columnName#if($foreach.count != $columns.size()),#end#end from ${tableName} + + + + + +#if($table.sub) + + +#end + + + insert into ${tableName} + +#foreach($column in $columns) +#if($column.columnName != $pkColumn.columnName || !$pkColumn.increment) + $column.columnName, +#end +#end + + +#foreach($column in $columns) +#if($column.columnName != $pkColumn.columnName || !$pkColumn.increment) + #{$column.javaField}, +#end +#end + + + + + update ${tableName} + +#foreach($column in $columns) +#if($column.columnName != $pkColumn.columnName) + $column.columnName = #{$column.javaField}, +#end +#end + + where ${pkColumn.columnName} = #{${pkColumn.javaField}} + + + + delete from ${tableName} where ${pkColumn.columnName} = #{${pkColumn.javaField}} + + + + delete from ${tableName} where ${pkColumn.columnName} in + + #{${pkColumn.javaField}} + + +#if($table.sub) + + + delete from ${subTableName} where ${subTableFkName} in + + #{${subTableFkclassName}} + + + + + delete from ${subTableName} where ${subTableFkName} = #{${subTableFkclassName}} + + + + insert into ${subTableName}(#foreach($column in $subTable.columns) $column.columnName#if($foreach.count != $subTable.columns.size()),#end#end) values + + (#foreach($column in $subTable.columns) #{item.$column.javaField}#if($foreach.count != $subTable.columns.size()),#end#end) + + +#end + \ No newline at end of file diff --git a/ry-api/ruoyi-quartz/pom.xml b/ry-api/ruoyi-quartz/pom.xml new file mode 100644 index 0000000..69c3c79 --- /dev/null +++ b/ry-api/ruoyi-quartz/pom.xml @@ -0,0 +1,34 @@ + + + + ruoyi + com.ruoyi + 3.9.2 + + 4.0.0 + + ruoyi-quartz + + + quartz定时任务 + + + + + + + org.springframework.boot + spring-boot-starter-quartz + + + + + com.ruoyi + ruoyi-common + + + + + \ No newline at end of file diff --git a/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/config/ScheduleConfig.java b/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/config/ScheduleConfig.java new file mode 100644 index 0000000..d4e065a --- /dev/null +++ b/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/config/ScheduleConfig.java @@ -0,0 +1,57 @@ +//package com.ruoyi.quartz.config; +// +//import org.springframework.context.annotation.Bean; +//import org.springframework.context.annotation.Configuration; +//import org.springframework.scheduling.quartz.SchedulerFactoryBean; +//import javax.sql.DataSource; +//import java.util.Properties; +// +///** +// * 定时任务配置(单机部署建议删除此类和qrtz数据库表,默认走内存会最高效) +// * +// * @author ruoyi +// */ +//@Configuration +//public class ScheduleConfig +//{ +// @Bean +// public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource) +// { +// SchedulerFactoryBean factory = new SchedulerFactoryBean(); +// factory.setDataSource(dataSource); +// +// // quartz参数 +// Properties prop = new Properties(); +// prop.put("org.quartz.scheduler.instanceName", "RuoyiScheduler"); +// prop.put("org.quartz.scheduler.instanceId", "AUTO"); +// // 线程池配置 +// prop.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool"); +// prop.put("org.quartz.threadPool.threadCount", "20"); +// prop.put("org.quartz.threadPool.threadPriority", "5"); +// // JobStore配置 +// prop.put("org.quartz.jobStore.class", "org.springframework.scheduling.quartz.LocalDataSourceJobStore"); +// // 集群配置 +// prop.put("org.quartz.jobStore.isClustered", "true"); +// prop.put("org.quartz.jobStore.clusterCheckinInterval", "15000"); +// prop.put("org.quartz.jobStore.maxMisfiresToHandleAtATime", "10"); +// prop.put("org.quartz.jobStore.txIsolationLevelSerializable", "true"); +// +// // sqlserver 启用 +// // prop.put("org.quartz.jobStore.selectWithLockSQL", "SELECT * FROM {0}LOCKS UPDLOCK WHERE LOCK_NAME = ?"); +// prop.put("org.quartz.jobStore.misfireThreshold", "12000"); +// prop.put("org.quartz.jobStore.tablePrefix", "QRTZ_"); +// factory.setQuartzProperties(prop); +// +// factory.setSchedulerName("RuoyiScheduler"); +// // 延时启动 +// factory.setStartupDelay(1); +// factory.setApplicationContextSchedulerContextKey("applicationContextKey"); +// // 可选,QuartzScheduler +// // 启动时更新己存在的Job,这样就不用每次修改targetObject后删除qrtz_job_details表对应记录了 +// factory.setOverwriteExistingJobs(true); +// // 设置自动启动,默认为true +// factory.setAutoStartup(true); +// +// return factory; +// } +//} diff --git a/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/controller/SysJobController.java b/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/controller/SysJobController.java new file mode 100644 index 0000000..abe6b2d --- /dev/null +++ b/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/controller/SysJobController.java @@ -0,0 +1,185 @@ +package com.ruoyi.quartz.controller; + +import java.util.List; +import jakarta.servlet.http.HttpServletResponse; +import org.quartz.SchedulerException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.exception.job.TaskException; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.quartz.domain.SysJob; +import com.ruoyi.quartz.service.ISysJobService; +import com.ruoyi.quartz.util.CronUtils; +import com.ruoyi.quartz.util.ScheduleUtils; + +/** + * 调度任务信息操作处理 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/monitor/job") +public class SysJobController extends BaseController +{ + @Autowired + private ISysJobService jobService; + + /** + * 查询定时任务列表 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:list')") + @GetMapping("/list") + public TableDataInfo list(SysJob sysJob) + { + startPage(); + List list = jobService.selectJobList(sysJob); + return getDataTable(list); + } + + /** + * 导出定时任务列表 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:export')") + @Log(title = "定时任务", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, SysJob sysJob) + { + List list = jobService.selectJobList(sysJob); + ExcelUtil util = new ExcelUtil(SysJob.class); + util.exportExcel(response, list, "定时任务"); + } + + /** + * 获取定时任务详细信息 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:query')") + @GetMapping(value = "/{jobId}") + public AjaxResult getInfo(@PathVariable("jobId") Long jobId) + { + return success(jobService.selectJobById(jobId)); + } + + /** + * 新增定时任务 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:add')") + @Log(title = "定时任务", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody SysJob job) throws SchedulerException, TaskException + { + if (!CronUtils.isValid(job.getCronExpression())) + { + return error("新增任务'" + job.getJobName() + "'失败,Cron表达式不正确"); + } + else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI)) + { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'rmi'调用"); + } + else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.LOOKUP_LDAP, Constants.LOOKUP_LDAPS })) + { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'ldap(s)'调用"); + } + else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.HTTP, Constants.HTTPS })) + { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'http(s)'调用"); + } + else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.JOB_ERROR_STR)) + { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串存在违规"); + } + else if (!ScheduleUtils.whiteList(job.getInvokeTarget())) + { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串不在白名单内"); + } + job.setCreateBy(getUsername()); + return toAjax(jobService.insertJob(job)); + } + + /** + * 修改定时任务 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:edit')") + @Log(title = "定时任务", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody SysJob job) throws SchedulerException, TaskException + { + if (!CronUtils.isValid(job.getCronExpression())) + { + return error("修改任务'" + job.getJobName() + "'失败,Cron表达式不正确"); + } + else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI)) + { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'rmi'调用"); + } + else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.LOOKUP_LDAP, Constants.LOOKUP_LDAPS })) + { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'ldap(s)'调用"); + } + else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.HTTP, Constants.HTTPS })) + { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'http(s)'调用"); + } + else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.JOB_ERROR_STR)) + { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串存在违规"); + } + else if (!ScheduleUtils.whiteList(job.getInvokeTarget())) + { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串不在白名单内"); + } + job.setUpdateBy(getUsername()); + return toAjax(jobService.updateJob(job)); + } + + /** + * 定时任务状态修改 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:changeStatus')") + @Log(title = "定时任务", businessType = BusinessType.UPDATE) + @PutMapping("/changeStatus") + public AjaxResult changeStatus(@RequestBody SysJob job) throws SchedulerException + { + SysJob newJob = jobService.selectJobById(job.getJobId()); + newJob.setStatus(job.getStatus()); + return toAjax(jobService.changeStatus(newJob)); + } + + /** + * 定时任务立即执行一次 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:changeStatus')") + @Log(title = "定时任务", businessType = BusinessType.UPDATE) + @PutMapping("/run") + public AjaxResult run(@RequestBody SysJob job) throws SchedulerException + { + boolean result = jobService.run(job); + return result ? success() : error("任务不存在或已过期!"); + } + + /** + * 删除定时任务 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:remove')") + @Log(title = "定时任务", businessType = BusinessType.DELETE) + @DeleteMapping("/{jobIds}") + public AjaxResult remove(@PathVariable Long[] jobIds) throws SchedulerException + { + jobService.deleteJobByIds(jobIds); + return success(); + } +} diff --git a/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/controller/SysJobLogController.java b/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/controller/SysJobLogController.java new file mode 100644 index 0000000..233361b --- /dev/null +++ b/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/controller/SysJobLogController.java @@ -0,0 +1,92 @@ +package com.ruoyi.quartz.controller; + +import java.util.List; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.quartz.domain.SysJobLog; +import com.ruoyi.quartz.service.ISysJobLogService; + +/** + * 调度日志操作处理 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/monitor/jobLog") +public class SysJobLogController extends BaseController +{ + @Autowired + private ISysJobLogService jobLogService; + + /** + * 查询定时任务调度日志列表 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:list')") + @GetMapping("/list") + public TableDataInfo list(SysJobLog sysJobLog) + { + startPage(); + List list = jobLogService.selectJobLogList(sysJobLog); + return getDataTable(list); + } + + /** + * 导出定时任务调度日志列表 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:export')") + @Log(title = "任务调度日志", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, SysJobLog sysJobLog) + { + List list = jobLogService.selectJobLogList(sysJobLog); + ExcelUtil util = new ExcelUtil(SysJobLog.class); + util.exportExcel(response, list, "调度日志"); + } + + /** + * 根据调度编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:query')") + @GetMapping(value = "/{jobLogId}") + public AjaxResult getInfo(@PathVariable Long jobLogId) + { + return success(jobLogService.selectJobLogById(jobLogId)); + } + + + /** + * 删除定时任务调度日志 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:remove')") + @Log(title = "定时任务调度日志", businessType = BusinessType.DELETE) + @DeleteMapping("/{jobLogIds}") + public AjaxResult remove(@PathVariable Long[] jobLogIds) + { + return toAjax(jobLogService.deleteJobLogByIds(jobLogIds)); + } + + /** + * 清空定时任务调度日志 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:remove')") + @Log(title = "调度日志", businessType = BusinessType.CLEAN) + @DeleteMapping("/clean") + public AjaxResult clean() + { + jobLogService.cleanJobLog(); + return success(); + } +} diff --git a/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/domain/SysJob.java b/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/domain/SysJob.java new file mode 100644 index 0000000..cea12dc --- /dev/null +++ b/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/domain/SysJob.java @@ -0,0 +1,171 @@ +package com.ruoyi.quartz.domain; + +import java.util.Date; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.annotation.Excel.ColumnType; +import com.ruoyi.common.constant.ScheduleConstants; +import com.ruoyi.common.core.domain.BaseEntity; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.quartz.util.CronUtils; + +/** + * 定时任务调度表 sys_job + * + * @author ruoyi + */ +public class SysJob extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 任务ID */ + @Excel(name = "任务序号", cellType = ColumnType.NUMERIC) + private Long jobId; + + /** 任务名称 */ + @Excel(name = "任务名称") + private String jobName; + + /** 任务组名 */ + @Excel(name = "任务组名") + private String jobGroup; + + /** 调用目标字符串 */ + @Excel(name = "调用目标字符串") + private String invokeTarget; + + /** cron执行表达式 */ + @Excel(name = "执行表达式 ") + private String cronExpression; + + /** cron计划策略 */ + @Excel(name = "计划策略 ", readConverterExp = "0=默认,1=立即触发执行,2=触发一次执行,3=不触发立即执行") + private String misfirePolicy = ScheduleConstants.MISFIRE_DEFAULT; + + /** 是否并发执行(0允许 1禁止) */ + @Excel(name = "并发执行", readConverterExp = "0=允许,1=禁止") + private String concurrent; + + /** 任务状态(0正常 1暂停) */ + @Excel(name = "任务状态", readConverterExp = "0=正常,1=暂停") + private String status; + + public Long getJobId() + { + return jobId; + } + + public void setJobId(Long jobId) + { + this.jobId = jobId; + } + + @NotBlank(message = "任务名称不能为空") + @Size(min = 0, max = 64, message = "任务名称不能超过64个字符") + public String getJobName() + { + return jobName; + } + + public void setJobName(String jobName) + { + this.jobName = jobName; + } + + public String getJobGroup() + { + return jobGroup; + } + + public void setJobGroup(String jobGroup) + { + this.jobGroup = jobGroup; + } + + @NotBlank(message = "调用目标字符串不能为空") + @Size(min = 0, max = 500, message = "调用目标字符串长度不能超过500个字符") + public String getInvokeTarget() + { + return invokeTarget; + } + + public void setInvokeTarget(String invokeTarget) + { + this.invokeTarget = invokeTarget; + } + + @NotBlank(message = "Cron执行表达式不能为空") + @Size(min = 0, max = 255, message = "Cron执行表达式不能超过255个字符") + public String getCronExpression() + { + return cronExpression; + } + + public void setCronExpression(String cronExpression) + { + this.cronExpression = cronExpression; + } + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + public Date getNextValidTime() + { + if (StringUtils.isNotEmpty(cronExpression)) + { + return CronUtils.getNextExecution(cronExpression); + } + return null; + } + + public String getMisfirePolicy() + { + return misfirePolicy; + } + + public void setMisfirePolicy(String misfirePolicy) + { + this.misfirePolicy = misfirePolicy; + } + + public String getConcurrent() + { + return concurrent; + } + + public void setConcurrent(String concurrent) + { + this.concurrent = concurrent; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("jobId", getJobId()) + .append("jobName", getJobName()) + .append("jobGroup", getJobGroup()) + .append("cronExpression", getCronExpression()) + .append("nextValidTime", getNextValidTime()) + .append("misfirePolicy", getMisfirePolicy()) + .append("concurrent", getConcurrent()) + .append("status", getStatus()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/domain/SysJobLog.java b/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/domain/SysJobLog.java new file mode 100644 index 0000000..6b160f9 --- /dev/null +++ b/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/domain/SysJobLog.java @@ -0,0 +1,158 @@ +package com.ruoyi.quartz.domain; + +import java.util.Date; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 定时任务调度日志表 sys_job_log + * + * @author ruoyi + */ +public class SysJobLog extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** ID */ + @Excel(name = "日志序号") + private Long jobLogId; + + /** 任务名称 */ + @Excel(name = "任务名称") + private String jobName; + + /** 任务组名 */ + @Excel(name = "任务组名") + private String jobGroup; + + /** 调用目标字符串 */ + @Excel(name = "调用目标字符串") + private String invokeTarget; + + /** 日志信息 */ + @Excel(name = "日志信息") + private String jobMessage; + + /** 执行状态(0正常 1失败) */ + @Excel(name = "执行状态", readConverterExp = "0=正常,1=失败") + private String status; + + /** 异常信息 */ + @Excel(name = "异常信息") + private String exceptionInfo; + + /** 开始时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date startTime; + + /** 结束时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date endTime; + + public Long getJobLogId() + { + return jobLogId; + } + + public void setJobLogId(Long jobLogId) + { + this.jobLogId = jobLogId; + } + + public String getJobName() + { + return jobName; + } + + public void setJobName(String jobName) + { + this.jobName = jobName; + } + + public String getJobGroup() + { + return jobGroup; + } + + public void setJobGroup(String jobGroup) + { + this.jobGroup = jobGroup; + } + + public String getInvokeTarget() + { + return invokeTarget; + } + + public void setInvokeTarget(String invokeTarget) + { + this.invokeTarget = invokeTarget; + } + + public String getJobMessage() + { + return jobMessage; + } + + public void setJobMessage(String jobMessage) + { + this.jobMessage = jobMessage; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + public String getExceptionInfo() + { + return exceptionInfo; + } + + public void setExceptionInfo(String exceptionInfo) + { + this.exceptionInfo = exceptionInfo; + } + + public Date getStartTime() + { + return startTime; + } + + public void setStartTime(Date startTime) + { + this.startTime = startTime; + } + + public Date getEndTime() + { + return endTime; + } + + public void setEndTime(Date endTime) + { + this.endTime = endTime; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("jobLogId", getJobLogId()) + .append("jobName", getJobName()) + .append("jobGroup", getJobGroup()) + .append("jobMessage", getJobMessage()) + .append("status", getStatus()) + .append("exceptionInfo", getExceptionInfo()) + .append("startTime", getStartTime()) + .append("stopTime", getEndTime()) + .toString(); + } +} diff --git a/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/mapper/SysJobLogMapper.java b/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/mapper/SysJobLogMapper.java new file mode 100644 index 0000000..727d916 --- /dev/null +++ b/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/mapper/SysJobLogMapper.java @@ -0,0 +1,64 @@ +package com.ruoyi.quartz.mapper; + +import java.util.List; +import com.ruoyi.quartz.domain.SysJobLog; + +/** + * 调度任务日志信息 数据层 + * + * @author ruoyi + */ +public interface SysJobLogMapper +{ + /** + * 获取quartz调度器日志的计划任务 + * + * @param jobLog 调度日志信息 + * @return 调度任务日志集合 + */ + public List selectJobLogList(SysJobLog jobLog); + + /** + * 查询所有调度任务日志 + * + * @return 调度任务日志列表 + */ + public List selectJobLogAll(); + + /** + * 通过调度任务日志ID查询调度信息 + * + * @param jobLogId 调度任务日志ID + * @return 调度任务日志对象信息 + */ + public SysJobLog selectJobLogById(Long jobLogId); + + /** + * 新增任务日志 + * + * @param jobLog 调度日志信息 + * @return 结果 + */ + public int insertJobLog(SysJobLog jobLog); + + /** + * 批量删除调度日志信息 + * + * @param logIds 需要删除的数据ID + * @return 结果 + */ + public int deleteJobLogByIds(Long[] logIds); + + /** + * 删除任务日志 + * + * @param jobId 调度日志ID + * @return 结果 + */ + public int deleteJobLogById(Long jobId); + + /** + * 清空任务日志 + */ + public void cleanJobLog(); +} diff --git a/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/mapper/SysJobMapper.java b/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/mapper/SysJobMapper.java new file mode 100644 index 0000000..20f45db --- /dev/null +++ b/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/mapper/SysJobMapper.java @@ -0,0 +1,67 @@ +package com.ruoyi.quartz.mapper; + +import java.util.List; +import com.ruoyi.quartz.domain.SysJob; + +/** + * 调度任务信息 数据层 + * + * @author ruoyi + */ +public interface SysJobMapper +{ + /** + * 查询调度任务日志集合 + * + * @param job 调度信息 + * @return 操作日志集合 + */ + public List selectJobList(SysJob job); + + /** + * 查询所有调度任务 + * + * @return 调度任务列表 + */ + public List selectJobAll(); + + /** + * 通过调度ID查询调度任务信息 + * + * @param jobId 调度ID + * @return 角色对象信息 + */ + public SysJob selectJobById(Long jobId); + + /** + * 通过调度ID删除调度任务信息 + * + * @param jobId 调度ID + * @return 结果 + */ + public int deleteJobById(Long jobId); + + /** + * 批量删除调度任务信息 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteJobByIds(Long[] ids); + + /** + * 修改调度任务信息 + * + * @param job 调度任务信息 + * @return 结果 + */ + public int updateJob(SysJob job); + + /** + * 新增调度任务信息 + * + * @param job 调度任务信息 + * @return 结果 + */ + public int insertJob(SysJob job); +} diff --git a/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/ISysJobLogService.java b/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/ISysJobLogService.java new file mode 100644 index 0000000..8546792 --- /dev/null +++ b/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/ISysJobLogService.java @@ -0,0 +1,56 @@ +package com.ruoyi.quartz.service; + +import java.util.List; +import com.ruoyi.quartz.domain.SysJobLog; + +/** + * 定时任务调度日志信息信息 服务层 + * + * @author ruoyi + */ +public interface ISysJobLogService +{ + /** + * 获取quartz调度器日志的计划任务 + * + * @param jobLog 调度日志信息 + * @return 调度任务日志集合 + */ + public List selectJobLogList(SysJobLog jobLog); + + /** + * 通过调度任务日志ID查询调度信息 + * + * @param jobLogId 调度任务日志ID + * @return 调度任务日志对象信息 + */ + public SysJobLog selectJobLogById(Long jobLogId); + + /** + * 新增任务日志 + * + * @param jobLog 调度日志信息 + */ + public void addJobLog(SysJobLog jobLog); + + /** + * 批量删除调度日志信息 + * + * @param logIds 需要删除的日志ID + * @return 结果 + */ + public int deleteJobLogByIds(Long[] logIds); + + /** + * 删除任务日志 + * + * @param jobId 调度日志ID + * @return 结果 + */ + public int deleteJobLogById(Long jobId); + + /** + * 清空任务日志 + */ + public void cleanJobLog(); +} diff --git a/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/ISysJobService.java b/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/ISysJobService.java new file mode 100644 index 0000000..437ade8 --- /dev/null +++ b/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/ISysJobService.java @@ -0,0 +1,102 @@ +package com.ruoyi.quartz.service; + +import java.util.List; +import org.quartz.SchedulerException; +import com.ruoyi.common.exception.job.TaskException; +import com.ruoyi.quartz.domain.SysJob; + +/** + * 定时任务调度信息信息 服务层 + * + * @author ruoyi + */ +public interface ISysJobService +{ + /** + * 获取quartz调度器的计划任务 + * + * @param job 调度信息 + * @return 调度任务集合 + */ + public List selectJobList(SysJob job); + + /** + * 通过调度任务ID查询调度信息 + * + * @param jobId 调度任务ID + * @return 调度任务对象信息 + */ + public SysJob selectJobById(Long jobId); + + /** + * 暂停任务 + * + * @param job 调度信息 + * @return 结果 + */ + public int pauseJob(SysJob job) throws SchedulerException; + + /** + * 恢复任务 + * + * @param job 调度信息 + * @return 结果 + */ + public int resumeJob(SysJob job) throws SchedulerException; + + /** + * 删除任务后,所对应的trigger也将被删除 + * + * @param job 调度信息 + * @return 结果 + */ + public int deleteJob(SysJob job) throws SchedulerException; + + /** + * 批量删除调度信息 + * + * @param jobIds 需要删除的任务ID + * @return 结果 + */ + public void deleteJobByIds(Long[] jobIds) throws SchedulerException; + + /** + * 任务调度状态修改 + * + * @param job 调度信息 + * @return 结果 + */ + public int changeStatus(SysJob job) throws SchedulerException; + + /** + * 立即运行任务 + * + * @param job 调度信息 + * @return 结果 + */ + public boolean run(SysJob job) throws SchedulerException; + + /** + * 新增任务 + * + * @param job 调度信息 + * @return 结果 + */ + public int insertJob(SysJob job) throws SchedulerException, TaskException; + + /** + * 更新任务 + * + * @param job 调度信息 + * @return 结果 + */ + public int updateJob(SysJob job) throws SchedulerException, TaskException; + + /** + * 校验cron表达式是否有效 + * + * @param cronExpression 表达式 + * @return 结果 + */ + public boolean checkCronExpressionIsValid(String cronExpression); +} diff --git a/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/impl/SysJobLogServiceImpl.java b/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/impl/SysJobLogServiceImpl.java new file mode 100644 index 0000000..812eed7 --- /dev/null +++ b/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/impl/SysJobLogServiceImpl.java @@ -0,0 +1,87 @@ +package com.ruoyi.quartz.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.quartz.domain.SysJobLog; +import com.ruoyi.quartz.mapper.SysJobLogMapper; +import com.ruoyi.quartz.service.ISysJobLogService; + +/** + * 定时任务调度日志信息 服务层 + * + * @author ruoyi + */ +@Service +public class SysJobLogServiceImpl implements ISysJobLogService +{ + @Autowired + private SysJobLogMapper jobLogMapper; + + /** + * 获取quartz调度器日志的计划任务 + * + * @param jobLog 调度日志信息 + * @return 调度任务日志集合 + */ + @Override + public List selectJobLogList(SysJobLog jobLog) + { + return jobLogMapper.selectJobLogList(jobLog); + } + + /** + * 通过调度任务日志ID查询调度信息 + * + * @param jobLogId 调度任务日志ID + * @return 调度任务日志对象信息 + */ + @Override + public SysJobLog selectJobLogById(Long jobLogId) + { + return jobLogMapper.selectJobLogById(jobLogId); + } + + /** + * 新增任务日志 + * + * @param jobLog 调度日志信息 + */ + @Override + public void addJobLog(SysJobLog jobLog) + { + jobLogMapper.insertJobLog(jobLog); + } + + /** + * 批量删除调度日志信息 + * + * @param logIds 需要删除的数据ID + * @return 结果 + */ + @Override + public int deleteJobLogByIds(Long[] logIds) + { + return jobLogMapper.deleteJobLogByIds(logIds); + } + + /** + * 删除任务日志 + * + * @param jobId 调度日志ID + */ + @Override + public int deleteJobLogById(Long jobId) + { + return jobLogMapper.deleteJobLogById(jobId); + } + + /** + * 清空任务日志 + */ + @Override + public void cleanJobLog() + { + jobLogMapper.cleanJobLog(); + } +} diff --git a/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/impl/SysJobServiceImpl.java b/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/impl/SysJobServiceImpl.java new file mode 100644 index 0000000..78ebef8 --- /dev/null +++ b/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/impl/SysJobServiceImpl.java @@ -0,0 +1,261 @@ +package com.ruoyi.quartz.service.impl; + +import java.util.List; +import jakarta.annotation.PostConstruct; +import org.quartz.JobDataMap; +import org.quartz.JobKey; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.ruoyi.common.constant.ScheduleConstants; +import com.ruoyi.common.exception.job.TaskException; +import com.ruoyi.quartz.domain.SysJob; +import com.ruoyi.quartz.mapper.SysJobMapper; +import com.ruoyi.quartz.service.ISysJobService; +import com.ruoyi.quartz.util.CronUtils; +import com.ruoyi.quartz.util.ScheduleUtils; + +/** + * 定时任务调度信息 服务层 + * + * @author ruoyi + */ +@Service +public class SysJobServiceImpl implements ISysJobService +{ + @Autowired + private Scheduler scheduler; + + @Autowired + private SysJobMapper jobMapper; + + /** + * 项目启动时,初始化定时器 主要是防止手动修改数据库导致未同步到定时任务处理(注:不能手动修改数据库ID和任务组名,否则会导致脏数据) + */ + @PostConstruct + public void init() throws SchedulerException, TaskException + { + scheduler.clear(); + List jobList = jobMapper.selectJobAll(); + for (SysJob job : jobList) + { + ScheduleUtils.createScheduleJob(scheduler, job); + } + } + + /** + * 获取quartz调度器的计划任务列表 + * + * @param job 调度信息 + * @return + */ + @Override + public List selectJobList(SysJob job) + { + return jobMapper.selectJobList(job); + } + + /** + * 通过调度任务ID查询调度信息 + * + * @param jobId 调度任务ID + * @return 调度任务对象信息 + */ + @Override + public SysJob selectJobById(Long jobId) + { + return jobMapper.selectJobById(jobId); + } + + /** + * 暂停任务 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int pauseJob(SysJob job) throws SchedulerException + { + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + job.setStatus(ScheduleConstants.Status.PAUSE.getValue()); + int rows = jobMapper.updateJob(job); + if (rows > 0) + { + scheduler.pauseJob(ScheduleUtils.getJobKey(jobId, jobGroup)); + } + return rows; + } + + /** + * 恢复任务 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int resumeJob(SysJob job) throws SchedulerException + { + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + job.setStatus(ScheduleConstants.Status.NORMAL.getValue()); + int rows = jobMapper.updateJob(job); + if (rows > 0) + { + scheduler.resumeJob(ScheduleUtils.getJobKey(jobId, jobGroup)); + } + return rows; + } + + /** + * 删除任务后,所对应的trigger也将被删除 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int deleteJob(SysJob job) throws SchedulerException + { + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + int rows = jobMapper.deleteJobById(jobId); + if (rows > 0) + { + scheduler.deleteJob(ScheduleUtils.getJobKey(jobId, jobGroup)); + } + return rows; + } + + /** + * 批量删除调度信息 + * + * @param jobIds 需要删除的任务ID + * @return 结果 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteJobByIds(Long[] jobIds) throws SchedulerException + { + for (Long jobId : jobIds) + { + SysJob job = jobMapper.selectJobById(jobId); + deleteJob(job); + } + } + + /** + * 任务调度状态修改 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int changeStatus(SysJob job) throws SchedulerException + { + int rows = 0; + String status = job.getStatus(); + if (ScheduleConstants.Status.NORMAL.getValue().equals(status)) + { + rows = resumeJob(job); + } + else if (ScheduleConstants.Status.PAUSE.getValue().equals(status)) + { + rows = pauseJob(job); + } + return rows; + } + + /** + * 立即运行任务 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean run(SysJob job) throws SchedulerException + { + boolean result = false; + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + SysJob properties = selectJobById(job.getJobId()); + // 参数 + JobDataMap dataMap = new JobDataMap(); + dataMap.put(ScheduleConstants.TASK_PROPERTIES, properties); + JobKey jobKey = ScheduleUtils.getJobKey(jobId, jobGroup); + if (scheduler.checkExists(jobKey)) + { + result = true; + scheduler.triggerJob(jobKey, dataMap); + } + return result; + } + + /** + * 新增任务 + * + * @param job 调度信息 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int insertJob(SysJob job) throws SchedulerException, TaskException + { + job.setStatus(ScheduleConstants.Status.PAUSE.getValue()); + int rows = jobMapper.insertJob(job); + if (rows > 0) + { + ScheduleUtils.createScheduleJob(scheduler, job); + } + return rows; + } + + /** + * 更新任务的时间表达式 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int updateJob(SysJob job) throws SchedulerException, TaskException + { + SysJob properties = selectJobById(job.getJobId()); + int rows = jobMapper.updateJob(job); + if (rows > 0) + { + updateSchedulerJob(job, properties.getJobGroup()); + } + return rows; + } + + /** + * 更新任务 + * + * @param job 任务对象 + * @param jobGroup 任务组名 + */ + public void updateSchedulerJob(SysJob job, String jobGroup) throws SchedulerException, TaskException + { + Long jobId = job.getJobId(); + // 判断是否存在 + JobKey jobKey = ScheduleUtils.getJobKey(jobId, jobGroup); + if (scheduler.checkExists(jobKey)) + { + // 防止创建时存在数据问题 先移除,然后在执行创建操作 + scheduler.deleteJob(jobKey); + } + ScheduleUtils.createScheduleJob(scheduler, job); + } + + /** + * 校验cron表达式是否有效 + * + * @param cronExpression 表达式 + * @return 结果 + */ + @Override + public boolean checkCronExpressionIsValid(String cronExpression) + { + return CronUtils.isValid(cronExpression); + } +} diff --git a/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/RyTask.java b/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/RyTask.java new file mode 100644 index 0000000..853243b --- /dev/null +++ b/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/RyTask.java @@ -0,0 +1,28 @@ +package com.ruoyi.quartz.task; + +import org.springframework.stereotype.Component; +import com.ruoyi.common.utils.StringUtils; + +/** + * 定时任务调度测试 + * + * @author ruoyi + */ +@Component("ryTask") +public class RyTask +{ + public void ryMultipleParams(String s, Boolean b, Long l, Double d, Integer i) + { + System.out.println(StringUtils.format("执行多参方法: 字符串类型{},布尔类型{},长整型{},浮点型{},整形{}", s, b, l, d, i)); + } + + public void ryParams(String params) + { + System.out.println("执行有参方法:" + params); + } + + public void ryNoParams() + { + System.out.println("执行无参方法"); + } +} diff --git a/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/AbstractQuartzJob.java b/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/AbstractQuartzJob.java new file mode 100644 index 0000000..a16744d --- /dev/null +++ b/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/AbstractQuartzJob.java @@ -0,0 +1,106 @@ +package com.ruoyi.quartz.util; + +import java.util.Date; +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.constant.ScheduleConstants; +import com.ruoyi.common.utils.ExceptionUtil; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.bean.BeanUtils; +import com.ruoyi.common.utils.spring.SpringUtils; +import com.ruoyi.quartz.domain.SysJob; +import com.ruoyi.quartz.domain.SysJobLog; +import com.ruoyi.quartz.service.ISysJobLogService; + +/** + * 抽象quartz调用 + * + * @author ruoyi + */ +public abstract class AbstractQuartzJob implements Job +{ + private static final Logger log = LoggerFactory.getLogger(AbstractQuartzJob.class); + + /** + * 线程本地变量 + */ + private static ThreadLocal threadLocal = new ThreadLocal<>(); + + @Override + public void execute(JobExecutionContext context) + { + SysJob sysJob = new SysJob(); + BeanUtils.copyBeanProp(sysJob, context.getMergedJobDataMap().get(ScheduleConstants.TASK_PROPERTIES)); + try + { + before(context, sysJob); + if (sysJob != null) + { + doExecute(context, sysJob); + } + after(context, sysJob, null); + } + catch (Exception e) + { + log.error("任务执行异常 - :", e); + after(context, sysJob, e); + } + } + + /** + * 执行前 + * + * @param context 工作执行上下文对象 + * @param sysJob 系统计划任务 + */ + protected void before(JobExecutionContext context, SysJob sysJob) + { + threadLocal.set(new Date()); + } + + /** + * 执行后 + * + * @param context 工作执行上下文对象 + * @param sysJob 系统计划任务 + */ + protected void after(JobExecutionContext context, SysJob sysJob, Exception e) + { + Date startTime = threadLocal.get(); + threadLocal.remove(); + + final SysJobLog sysJobLog = new SysJobLog(); + sysJobLog.setJobName(sysJob.getJobName()); + sysJobLog.setJobGroup(sysJob.getJobGroup()); + sysJobLog.setInvokeTarget(sysJob.getInvokeTarget()); + sysJobLog.setStartTime(startTime); + sysJobLog.setEndTime(new Date()); + long runMs = sysJobLog.getEndTime().getTime() - sysJobLog.getStartTime().getTime(); + sysJobLog.setJobMessage(sysJobLog.getJobName() + " 总共耗时:" + runMs + "毫秒"); + if (e != null) + { + sysJobLog.setStatus(Constants.FAIL); + String errorMsg = StringUtils.substring(ExceptionUtil.getExceptionMessage(e), 0, 2000); + sysJobLog.setExceptionInfo(errorMsg); + } + else + { + sysJobLog.setStatus(Constants.SUCCESS); + } + + // 写入数据库当中 + SpringUtils.getBean(ISysJobLogService.class).addJobLog(sysJobLog); + } + + /** + * 执行方法,由子类重载 + * + * @param context 工作执行上下文对象 + * @param sysJob 系统计划任务 + * @throws Exception 执行过程中的异常 + */ + protected abstract void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception; +} diff --git a/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/CronUtils.java b/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/CronUtils.java new file mode 100644 index 0000000..dd53839 --- /dev/null +++ b/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/CronUtils.java @@ -0,0 +1,63 @@ +package com.ruoyi.quartz.util; + +import java.text.ParseException; +import java.util.Date; +import org.quartz.CronExpression; + +/** + * cron表达式工具类 + * + * @author ruoyi + * + */ +public class CronUtils +{ + /** + * 返回一个布尔值代表一个给定的Cron表达式的有效性 + * + * @param cronExpression Cron表达式 + * @return boolean 表达式是否有效 + */ + public static boolean isValid(String cronExpression) + { + return CronExpression.isValidExpression(cronExpression); + } + + /** + * 返回一个字符串值,表示该消息无效Cron表达式给出有效性 + * + * @param cronExpression Cron表达式 + * @return String 无效时返回表达式错误描述,如果有效返回null + */ + public static String getInvalidMessage(String cronExpression) + { + try + { + new CronExpression(cronExpression); + return null; + } + catch (ParseException pe) + { + return pe.getMessage(); + } + } + + /** + * 返回下一个执行时间根据给定的Cron表达式 + * + * @param cronExpression Cron表达式 + * @return Date 下次Cron表达式执行时间 + */ + public static Date getNextExecution(String cronExpression) + { + try + { + CronExpression cron = new CronExpression(cronExpression); + return cron.getNextValidTimeAfter(new Date(System.currentTimeMillis())); + } + catch (ParseException e) + { + throw new IllegalArgumentException(e.getMessage()); + } + } +} diff --git a/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/JobInvokeUtil.java b/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/JobInvokeUtil.java new file mode 100644 index 0000000..dea8cde --- /dev/null +++ b/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/JobInvokeUtil.java @@ -0,0 +1,182 @@ +package com.ruoyi.quartz.util; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.LinkedList; +import java.util.List; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.spring.SpringUtils; +import com.ruoyi.quartz.domain.SysJob; + +/** + * 任务执行工具 + * + * @author ruoyi + */ +public class JobInvokeUtil +{ + /** + * 执行方法 + * + * @param sysJob 系统任务 + */ + public static void invokeMethod(SysJob sysJob) throws Exception + { + String invokeTarget = sysJob.getInvokeTarget(); + String beanName = getBeanName(invokeTarget); + String methodName = getMethodName(invokeTarget); + List methodParams = getMethodParams(invokeTarget); + + if (!isValidClassName(beanName)) + { + Object bean = SpringUtils.getBean(beanName); + invokeMethod(bean, methodName, methodParams); + } + else + { + Object bean = Class.forName(beanName).getDeclaredConstructor().newInstance(); + invokeMethod(bean, methodName, methodParams); + } + } + + /** + * 调用任务方法 + * + * @param bean 目标对象 + * @param methodName 方法名称 + * @param methodParams 方法参数 + */ + private static void invokeMethod(Object bean, String methodName, List methodParams) + throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, + InvocationTargetException + { + if (StringUtils.isNotNull(methodParams) && methodParams.size() > 0) + { + Method method = bean.getClass().getMethod(methodName, getMethodParamsType(methodParams)); + method.invoke(bean, getMethodParamsValue(methodParams)); + } + else + { + Method method = bean.getClass().getMethod(methodName); + method.invoke(bean); + } + } + + /** + * 校验是否为为class包名 + * + * @param invokeTarget 名称 + * @return true是 false否 + */ + public static boolean isValidClassName(String invokeTarget) + { + return StringUtils.countMatches(invokeTarget, ".") > 1; + } + + /** + * 获取bean名称 + * + * @param invokeTarget 目标字符串 + * @return bean名称 + */ + public static String getBeanName(String invokeTarget) + { + String beanName = StringUtils.substringBefore(invokeTarget, "("); + return StringUtils.substringBeforeLast(beanName, "."); + } + + /** + * 获取bean方法 + * + * @param invokeTarget 目标字符串 + * @return method方法 + */ + public static String getMethodName(String invokeTarget) + { + String methodName = StringUtils.substringBefore(invokeTarget, "("); + return StringUtils.substringAfterLast(methodName, "."); + } + + /** + * 获取method方法参数相关列表 + * + * @param invokeTarget 目标字符串 + * @return method方法相关参数列表 + */ + public static List getMethodParams(String invokeTarget) + { + String methodStr = StringUtils.substringBetweenLast(invokeTarget, "(", ")"); + if (StringUtils.isEmpty(methodStr)) + { + return null; + } + String[] methodParams = methodStr.split(",(?=([^\"']*[\"'][^\"']*[\"'])*[^\"']*$)"); + List classs = new LinkedList<>(); + for (int i = 0; i < methodParams.length; i++) + { + String str = StringUtils.trimToEmpty(methodParams[i]); + // String字符串类型,以'或"开头 + if (StringUtils.startsWithAny(str, "'", "\"")) + { + classs.add(new Object[] { StringUtils.substring(str, 1, str.length() - 1), String.class }); + } + // boolean布尔类型,等于true或者false + else if ("true".equalsIgnoreCase(str) || "false".equalsIgnoreCase(str)) + { + classs.add(new Object[] { Boolean.valueOf(str), Boolean.class }); + } + // long长整形,以L结尾 + else if (StringUtils.endsWith(str, "L")) + { + classs.add(new Object[] { Long.valueOf(StringUtils.substring(str, 0, str.length() - 1)), Long.class }); + } + // double浮点类型,以D结尾 + else if (StringUtils.endsWith(str, "D")) + { + classs.add(new Object[] { Double.valueOf(StringUtils.substring(str, 0, str.length() - 1)), Double.class }); + } + // 其他类型归类为整形 + else + { + classs.add(new Object[] { Integer.valueOf(str), Integer.class }); + } + } + return classs; + } + + /** + * 获取参数类型 + * + * @param methodParams 参数相关列表 + * @return 参数类型列表 + */ + public static Class[] getMethodParamsType(List methodParams) + { + Class[] classs = new Class[methodParams.size()]; + int index = 0; + for (Object[] os : methodParams) + { + classs[index] = (Class) os[1]; + index++; + } + return classs; + } + + /** + * 获取参数值 + * + * @param methodParams 参数相关列表 + * @return 参数值列表 + */ + public static Object[] getMethodParamsValue(List methodParams) + { + Object[] classs = new Object[methodParams.size()]; + int index = 0; + for (Object[] os : methodParams) + { + classs[index] = (Object) os[0]; + index++; + } + return classs; + } +} diff --git a/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/QuartzDisallowConcurrentExecution.java b/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/QuartzDisallowConcurrentExecution.java new file mode 100644 index 0000000..5e13558 --- /dev/null +++ b/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/QuartzDisallowConcurrentExecution.java @@ -0,0 +1,21 @@ +package com.ruoyi.quartz.util; + +import org.quartz.DisallowConcurrentExecution; +import org.quartz.JobExecutionContext; +import com.ruoyi.quartz.domain.SysJob; + +/** + * 定时任务处理(禁止并发执行) + * + * @author ruoyi + * + */ +@DisallowConcurrentExecution +public class QuartzDisallowConcurrentExecution extends AbstractQuartzJob +{ + @Override + protected void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception + { + JobInvokeUtil.invokeMethod(sysJob); + } +} diff --git a/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/QuartzJobExecution.java b/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/QuartzJobExecution.java new file mode 100644 index 0000000..e975326 --- /dev/null +++ b/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/QuartzJobExecution.java @@ -0,0 +1,19 @@ +package com.ruoyi.quartz.util; + +import org.quartz.JobExecutionContext; +import com.ruoyi.quartz.domain.SysJob; + +/** + * 定时任务处理(允许并发执行) + * + * @author ruoyi + * + */ +public class QuartzJobExecution extends AbstractQuartzJob +{ + @Override + protected void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception + { + JobInvokeUtil.invokeMethod(sysJob); + } +} diff --git a/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/ScheduleUtils.java b/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/ScheduleUtils.java new file mode 100644 index 0000000..5a83f50 --- /dev/null +++ b/ry-api/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/ScheduleUtils.java @@ -0,0 +1,141 @@ +package com.ruoyi.quartz.util; + +import org.quartz.CronScheduleBuilder; +import org.quartz.CronTrigger; +import org.quartz.Job; +import org.quartz.JobBuilder; +import org.quartz.JobDetail; +import org.quartz.JobKey; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.TriggerBuilder; +import org.quartz.TriggerKey; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.constant.ScheduleConstants; +import com.ruoyi.common.exception.job.TaskException; +import com.ruoyi.common.exception.job.TaskException.Code; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.spring.SpringUtils; +import com.ruoyi.quartz.domain.SysJob; + +/** + * 定时任务工具类 + * + * @author ruoyi + * + */ +public class ScheduleUtils +{ + /** + * 得到quartz任务类 + * + * @param sysJob 执行计划 + * @return 具体执行任务类 + */ + private static Class getQuartzJobClass(SysJob sysJob) + { + boolean isConcurrent = "0".equals(sysJob.getConcurrent()); + return isConcurrent ? QuartzJobExecution.class : QuartzDisallowConcurrentExecution.class; + } + + /** + * 构建任务触发对象 + */ + public static TriggerKey getTriggerKey(Long jobId, String jobGroup) + { + return TriggerKey.triggerKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup); + } + + /** + * 构建任务键对象 + */ + public static JobKey getJobKey(Long jobId, String jobGroup) + { + return JobKey.jobKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup); + } + + /** + * 创建定时任务 + */ + public static void createScheduleJob(Scheduler scheduler, SysJob job) throws SchedulerException, TaskException + { + Class jobClass = getQuartzJobClass(job); + // 构建job信息 + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(getJobKey(jobId, jobGroup)).build(); + + // 表达式调度构建器 + CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression()); + cronScheduleBuilder = handleCronScheduleMisfirePolicy(job, cronScheduleBuilder); + + // 按新的cronExpression表达式构建一个新的trigger + CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(getTriggerKey(jobId, jobGroup)) + .withSchedule(cronScheduleBuilder).build(); + + // 放入参数,运行时的方法可以获取 + jobDetail.getJobDataMap().put(ScheduleConstants.TASK_PROPERTIES, job); + + // 判断是否存在 + if (scheduler.checkExists(getJobKey(jobId, jobGroup))) + { + // 防止创建时存在数据问题 先移除,然后在执行创建操作 + scheduler.deleteJob(getJobKey(jobId, jobGroup)); + } + + // 判断任务是否过期 + if (StringUtils.isNotNull(CronUtils.getNextExecution(job.getCronExpression()))) + { + // 执行调度任务 + scheduler.scheduleJob(jobDetail, trigger); + } + + // 暂停任务 + if (job.getStatus().equals(ScheduleConstants.Status.PAUSE.getValue())) + { + scheduler.pauseJob(ScheduleUtils.getJobKey(jobId, jobGroup)); + } + } + + /** + * 设置定时任务策略 + */ + public static CronScheduleBuilder handleCronScheduleMisfirePolicy(SysJob job, CronScheduleBuilder cb) + throws TaskException + { + switch (job.getMisfirePolicy()) + { + case ScheduleConstants.MISFIRE_DEFAULT: + return cb; + case ScheduleConstants.MISFIRE_IGNORE_MISFIRES: + return cb.withMisfireHandlingInstructionIgnoreMisfires(); + case ScheduleConstants.MISFIRE_FIRE_AND_PROCEED: + return cb.withMisfireHandlingInstructionFireAndProceed(); + case ScheduleConstants.MISFIRE_DO_NOTHING: + return cb.withMisfireHandlingInstructionDoNothing(); + default: + throw new TaskException("The task misfire policy '" + job.getMisfirePolicy() + + "' cannot be used in cron schedule tasks", Code.CONFIG_ERROR); + } + } + + /** + * 检查包名是否为白名单配置 + * + * @param invokeTarget 目标字符串 + * @return 结果 + */ + public static boolean whiteList(String invokeTarget) + { + String packageName = StringUtils.substringBefore(invokeTarget, "("); + int count = StringUtils.countMatches(packageName, "."); + if (count > 1) + { + return StringUtils.startsWithAny(invokeTarget, Constants.JOB_WHITELIST_STR); + } + Object obj = SpringUtils.getBean(StringUtils.split(invokeTarget, ".")[0]); + String beanPackageName = obj.getClass().getPackage().getName(); + return StringUtils.startsWithAny(beanPackageName, Constants.JOB_WHITELIST_STR) + && !StringUtils.startsWithAny(beanPackageName, Constants.JOB_ERROR_STR); + } +} diff --git a/ry-api/ruoyi-quartz/src/main/resources/mapper/quartz/SysJobLogMapper.xml b/ry-api/ruoyi-quartz/src/main/resources/mapper/quartz/SysJobLogMapper.xml new file mode 100644 index 0000000..bc6f864 --- /dev/null +++ b/ry-api/ruoyi-quartz/src/main/resources/mapper/quartz/SysJobLogMapper.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + select job_log_id, job_name, job_group, invoke_target, job_message, status, exception_info, start_time, end_time, create_time + from sys_job_log + + + + + + + + + + delete from sys_job_log where job_log_id = #{jobLogId} + + + + delete from sys_job_log where job_log_id in + + #{jobLogId} + + + + + truncate table sys_job_log + + + + insert into sys_job_log( + job_log_id, + job_name, + job_group, + invoke_target, + job_message, + status, + exception_info, + start_time, + end_time, + create_time + )values( + #{jobLogId}, + #{jobName}, + #{jobGroup}, + #{invokeTarget}, + #{jobMessage}, + #{status}, + #{exceptionInfo}, + #{startTime}, + #{endTime}, + sysdate() + ) + + + \ No newline at end of file diff --git a/ry-api/ruoyi-quartz/src/main/resources/mapper/quartz/SysJobMapper.xml b/ry-api/ruoyi-quartz/src/main/resources/mapper/quartz/SysJobMapper.xml new file mode 100644 index 0000000..3841852 --- /dev/null +++ b/ry-api/ruoyi-quartz/src/main/resources/mapper/quartz/SysJobMapper.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + select job_id, job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, update_by, update_time, remark + from sys_job + + + + + + + + + + delete from sys_job where job_id = #{jobId} + + + + delete from sys_job where job_id in + + #{jobId} + + + + + update sys_job + + job_name = #{jobName}, + job_group = #{jobGroup}, + invoke_target = #{invokeTarget}, + cron_expression = #{cronExpression}, + misfire_policy = #{misfirePolicy}, + concurrent = #{concurrent}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where job_id = #{jobId} + + + + insert into sys_job( + job_id, + job_name, + job_group, + invoke_target, + cron_expression, + misfire_policy, + concurrent, + status, + remark, + create_by, + create_time + )values( + #{jobId}, + #{jobName}, + #{jobGroup}, + #{invokeTarget}, + #{cronExpression}, + #{misfirePolicy}, + #{concurrent}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + \ No newline at end of file diff --git a/ry-api/ruoyi-system/pom.xml b/ry-api/ruoyi-system/pom.xml new file mode 100644 index 0000000..572f5e9 --- /dev/null +++ b/ry-api/ruoyi-system/pom.xml @@ -0,0 +1,28 @@ + + + + ruoyi + com.ruoyi + 3.9.2 + + 4.0.0 + + ruoyi-system + + + system系统模块 + + + + + + + com.ruoyi + ruoyi-common + + + + + \ No newline at end of file diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysCache.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysCache.java new file mode 100644 index 0000000..83f0703 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysCache.java @@ -0,0 +1,81 @@ +package com.ruoyi.system.domain; + +import com.ruoyi.common.utils.StringUtils; + +/** + * 缓存信息 + * + * @author ruoyi + */ +public class SysCache +{ + /** 缓存名称 */ + private String cacheName = ""; + + /** 缓存键名 */ + private String cacheKey = ""; + + /** 缓存内容 */ + private String cacheValue = ""; + + /** 备注 */ + private String remark = ""; + + public SysCache() + { + + } + + public SysCache(String cacheName, String remark) + { + this.cacheName = cacheName; + this.remark = remark; + } + + public SysCache(String cacheName, String cacheKey, String cacheValue) + { + this.cacheName = StringUtils.replace(cacheName, ":", ""); + this.cacheKey = StringUtils.replace(cacheKey, cacheName, ""); + this.cacheValue = cacheValue; + } + + public String getCacheName() + { + return cacheName; + } + + public void setCacheName(String cacheName) + { + this.cacheName = cacheName; + } + + public String getCacheKey() + { + return cacheKey; + } + + public void setCacheKey(String cacheKey) + { + this.cacheKey = cacheKey; + } + + public String getCacheValue() + { + return cacheValue; + } + + public void setCacheValue(String cacheValue) + { + this.cacheValue = cacheValue; + } + + public String getRemark() + { + return remark; + } + + public void setRemark(String remark) + { + this.remark = remark; + } +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysConfig.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysConfig.java new file mode 100644 index 0000000..cae07be --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysConfig.java @@ -0,0 +1,111 @@ +package com.ruoyi.system.domain; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.annotation.Excel.ColumnType; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 参数配置表 sys_config + * + * @author ruoyi + */ +public class SysConfig extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 参数主键 */ + @Excel(name = "参数主键", cellType = ColumnType.NUMERIC) + private Long configId; + + /** 参数名称 */ + @Excel(name = "参数名称") + private String configName; + + /** 参数键名 */ + @Excel(name = "参数键名") + private String configKey; + + /** 参数键值 */ + @Excel(name = "参数键值") + private String configValue; + + /** 系统内置(Y是 N否) */ + @Excel(name = "系统内置", readConverterExp = "Y=是,N=否") + private String configType; + + public Long getConfigId() + { + return configId; + } + + public void setConfigId(Long configId) + { + this.configId = configId; + } + + @NotBlank(message = "参数名称不能为空") + @Size(min = 0, max = 100, message = "参数名称不能超过100个字符") + public String getConfigName() + { + return configName; + } + + public void setConfigName(String configName) + { + this.configName = configName; + } + + @NotBlank(message = "参数键名长度不能为空") + @Size(min = 0, max = 100, message = "参数键名长度不能超过100个字符") + public String getConfigKey() + { + return configKey; + } + + public void setConfigKey(String configKey) + { + this.configKey = configKey; + } + + @NotBlank(message = "参数键值不能为空") + @Size(min = 0, max = 500, message = "参数键值长度不能超过500个字符") + public String getConfigValue() + { + return configValue; + } + + public void setConfigValue(String configValue) + { + this.configValue = configValue; + } + + public String getConfigType() + { + return configType; + } + + public void setConfigType(String configType) + { + this.configType = configType; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("configId", getConfigId()) + .append("configName", getConfigName()) + .append("configKey", getConfigKey()) + .append("configValue", getConfigValue()) + .append("configType", getConfigType()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysLogininfor.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysLogininfor.java new file mode 100644 index 0000000..7fdea30 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysLogininfor.java @@ -0,0 +1,144 @@ +package com.ruoyi.system.domain; + +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.annotation.Excel.ColumnType; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 系统访问记录表 sys_logininfor + * + * @author ruoyi + */ +public class SysLogininfor extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** ID */ + @Excel(name = "序号", cellType = ColumnType.NUMERIC) + private Long infoId; + + /** 用户账号 */ + @Excel(name = "用户账号") + private String userName; + + /** 登录状态 0成功 1失败 */ + @Excel(name = "登录状态", readConverterExp = "0=成功,1=失败") + private String status; + + /** 登录IP地址 */ + @Excel(name = "登录地址") + private String ipaddr; + + /** 登录地点 */ + @Excel(name = "登录地点") + private String loginLocation; + + /** 浏览器类型 */ + @Excel(name = "浏览器") + private String browser; + + /** 操作系统 */ + @Excel(name = "操作系统") + private String os; + + /** 提示消息 */ + @Excel(name = "提示消息") + private String msg; + + /** 访问时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "访问时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + private Date loginTime; + + public Long getInfoId() + { + return infoId; + } + + public void setInfoId(Long infoId) + { + this.infoId = infoId; + } + + public String getUserName() + { + return userName; + } + + public void setUserName(String userName) + { + this.userName = userName; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + public String getIpaddr() + { + return ipaddr; + } + + public void setIpaddr(String ipaddr) + { + this.ipaddr = ipaddr; + } + + public String getLoginLocation() + { + return loginLocation; + } + + public void setLoginLocation(String loginLocation) + { + this.loginLocation = loginLocation; + } + + public String getBrowser() + { + return browser; + } + + public void setBrowser(String browser) + { + this.browser = browser; + } + + public String getOs() + { + return os; + } + + public void setOs(String os) + { + this.os = os; + } + + public String getMsg() + { + return msg; + } + + public void setMsg(String msg) + { + this.msg = msg; + } + + public Date getLoginTime() + { + return loginTime; + } + + public void setLoginTime(Date loginTime) + { + this.loginTime = loginTime; + } +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysNotice.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysNotice.java new file mode 100644 index 0000000..83e021c --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysNotice.java @@ -0,0 +1,117 @@ +package com.ruoyi.system.domain; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.ruoyi.common.core.domain.BaseEntity; +import com.ruoyi.common.xss.Xss; + +/** + * 通知公告表 sys_notice + * + * @author ruoyi + */ +public class SysNotice extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 公告ID */ + private Long noticeId; + + /** 公告标题 */ + private String noticeTitle; + + /** 公告类型(1通知 2公告) */ + private String noticeType; + + /** 公告内容 */ + private String noticeContent; + + /** 公告状态(0正常 1关闭) */ + private String status; + + /** 是否已读 */ + @JsonProperty("isRead") + private boolean isRead; + + public Long getNoticeId() + { + return noticeId; + } + + public void setNoticeId(Long noticeId) + { + this.noticeId = noticeId; + } + + public void setNoticeTitle(String noticeTitle) + { + this.noticeTitle = noticeTitle; + } + + @Xss(message = "公告标题不能包含脚本字符") + @NotBlank(message = "公告标题不能为空") + @Size(min = 0, max = 50, message = "公告标题不能超过50个字符") + public String getNoticeTitle() + { + return noticeTitle; + } + + public void setNoticeType(String noticeType) + { + this.noticeType = noticeType; + } + + public String getNoticeType() + { + return noticeType; + } + + public void setNoticeContent(String noticeContent) + { + this.noticeContent = noticeContent; + } + + public String getNoticeContent() + { + return noticeContent; + } + + public void setStatus(String status) + { + this.status = status; + } + + public String getStatus() + { + return status; + } + + public boolean getIsRead() + { + return isRead; + } + + public void setIsRead(boolean isRead) + { + this.isRead = isRead; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("noticeId", getNoticeId()) + .append("noticeTitle", getNoticeTitle()) + .append("noticeType", getNoticeType()) + .append("noticeContent", getNoticeContent()) + .append("status", getStatus()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysNoticeRead.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysNoticeRead.java new file mode 100644 index 0000000..6ebf88c --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysNoticeRead.java @@ -0,0 +1,76 @@ +package com.ruoyi.system.domain; + +import java.util.Date; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 公告已读记录表 sys_notice_read + * + * @author ruoyi + */ +public class SysNoticeRead +{ + /** 主键 */ + private Long readId; + + /** 公告ID */ + private Long noticeId; + + /** 用户ID */ + private Long userId; + + /** 阅读时间 */ + private Date readTime; + + public Long getReadId() + { + return readId; + } + + public void setReadId(Long readId) + { + this.readId = readId; + } + + public Long getNoticeId() + { + return noticeId; + } + + public void setNoticeId(Long noticeId) + { + this.noticeId = noticeId; + } + + public Long getUserId() + { + return userId; + } + + public void setUserId(Long userId) + { + this.userId = userId; + } + + public Date getReadTime() + { + return readTime; + } + + public void setReadTime(Date readTime) + { + this.readTime = readTime; + } + + @Override + public String toString() + { + return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE) + .append("readId", getReadId()) + .append("noticeId", getNoticeId()) + .append("userId", getUserId()) + .append("readTime", getReadTime()) + .toString(); + } +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysOperLog.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysOperLog.java new file mode 100644 index 0000000..f6761df --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysOperLog.java @@ -0,0 +1,269 @@ +package com.ruoyi.system.domain; + +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.annotation.Excel.ColumnType; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 操作日志记录表 oper_log + * + * @author ruoyi + */ +public class SysOperLog extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 日志主键 */ + @Excel(name = "操作序号", cellType = ColumnType.NUMERIC) + private Long operId; + + /** 操作模块 */ + @Excel(name = "操作模块") + private String title; + + /** 业务类型(0其它 1新增 2修改 3删除) */ + @Excel(name = "业务类型", readConverterExp = "0=其它,1=新增,2=修改,3=删除,4=授权,5=导出,6=导入,7=强退,8=生成代码,9=清空数据") + private Integer businessType; + + /** 业务类型数组 */ + private Integer[] businessTypes; + + /** 请求方法 */ + @Excel(name = "请求方法") + private String method; + + /** 请求方式 */ + @Excel(name = "请求方式") + private String requestMethod; + + /** 操作类别(0其它 1后台用户 2手机端用户) */ + @Excel(name = "操作类别", readConverterExp = "0=其它,1=后台用户,2=手机端用户") + private Integer operatorType; + + /** 操作人员 */ + @Excel(name = "操作人员") + private String operName; + + /** 部门名称 */ + @Excel(name = "部门名称") + private String deptName; + + /** 请求url */ + @Excel(name = "请求地址") + private String operUrl; + + /** 操作地址 */ + @Excel(name = "操作地址") + private String operIp; + + /** 操作地点 */ + @Excel(name = "操作地点") + private String operLocation; + + /** 请求参数 */ + @Excel(name = "请求参数") + private String operParam; + + /** 返回参数 */ + @Excel(name = "返回参数") + private String jsonResult; + + /** 操作状态(0正常 1异常) */ + @Excel(name = "状态", readConverterExp = "0=正常,1=异常") + private Integer status; + + /** 错误消息 */ + @Excel(name = "错误消息") + private String errorMsg; + + /** 操作时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "操作时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + private Date operTime; + + /** 消耗时间 */ + @Excel(name = "消耗时间", suffix = "毫秒") + private Long costTime; + + public Long getOperId() + { + return operId; + } + + public void setOperId(Long operId) + { + this.operId = operId; + } + + public String getTitle() + { + return title; + } + + public void setTitle(String title) + { + this.title = title; + } + + public Integer getBusinessType() + { + return businessType; + } + + public void setBusinessType(Integer businessType) + { + this.businessType = businessType; + } + + public Integer[] getBusinessTypes() + { + return businessTypes; + } + + public void setBusinessTypes(Integer[] businessTypes) + { + this.businessTypes = businessTypes; + } + + public String getMethod() + { + return method; + } + + public void setMethod(String method) + { + this.method = method; + } + + public String getRequestMethod() + { + return requestMethod; + } + + public void setRequestMethod(String requestMethod) + { + this.requestMethod = requestMethod; + } + + public Integer getOperatorType() + { + return operatorType; + } + + public void setOperatorType(Integer operatorType) + { + this.operatorType = operatorType; + } + + public String getOperName() + { + return operName; + } + + public void setOperName(String operName) + { + this.operName = operName; + } + + public String getDeptName() + { + return deptName; + } + + public void setDeptName(String deptName) + { + this.deptName = deptName; + } + + public String getOperUrl() + { + return operUrl; + } + + public void setOperUrl(String operUrl) + { + this.operUrl = operUrl; + } + + public String getOperIp() + { + return operIp; + } + + public void setOperIp(String operIp) + { + this.operIp = operIp; + } + + public String getOperLocation() + { + return operLocation; + } + + public void setOperLocation(String operLocation) + { + this.operLocation = operLocation; + } + + public String getOperParam() + { + return operParam; + } + + public void setOperParam(String operParam) + { + this.operParam = operParam; + } + + public String getJsonResult() + { + return jsonResult; + } + + public void setJsonResult(String jsonResult) + { + this.jsonResult = jsonResult; + } + + public Integer getStatus() + { + return status; + } + + public void setStatus(Integer status) + { + this.status = status; + } + + public String getErrorMsg() + { + return errorMsg; + } + + public void setErrorMsg(String errorMsg) + { + this.errorMsg = errorMsg; + } + + public Date getOperTime() + { + return operTime; + } + + public void setOperTime(Date operTime) + { + this.operTime = operTime; + } + + public Long getCostTime() + { + return costTime; + } + + public void setCostTime(Long costTime) + { + this.costTime = costTime; + } +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysPost.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysPost.java new file mode 100644 index 0000000..62dbd47 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysPost.java @@ -0,0 +1,124 @@ +package com.ruoyi.system.domain; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.annotation.Excel.ColumnType; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 岗位表 sys_post + * + * @author ruoyi + */ +public class SysPost extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 岗位序号 */ + @Excel(name = "岗位序号", cellType = ColumnType.NUMERIC) + private Long postId; + + /** 岗位编码 */ + @Excel(name = "岗位编码") + private String postCode; + + /** 岗位名称 */ + @Excel(name = "岗位名称") + private String postName; + + /** 岗位排序 */ + @Excel(name = "岗位排序") + private Integer postSort; + + /** 状态(0正常 1停用) */ + @Excel(name = "状态", readConverterExp = "0=正常,1=停用") + private String status; + + /** 用户是否存在此岗位标识 默认不存在 */ + private boolean flag = false; + + public Long getPostId() + { + return postId; + } + + public void setPostId(Long postId) + { + this.postId = postId; + } + + @NotBlank(message = "岗位编码不能为空") + @Size(min = 0, max = 64, message = "岗位编码长度不能超过64个字符") + public String getPostCode() + { + return postCode; + } + + public void setPostCode(String postCode) + { + this.postCode = postCode; + } + + @NotBlank(message = "岗位名称不能为空") + @Size(min = 0, max = 50, message = "岗位名称长度不能超过50个字符") + public String getPostName() + { + return postName; + } + + public void setPostName(String postName) + { + this.postName = postName; + } + + @NotNull(message = "显示顺序不能为空") + public Integer getPostSort() + { + return postSort; + } + + public void setPostSort(Integer postSort) + { + this.postSort = postSort; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + public boolean isFlag() + { + return flag; + } + + public void setFlag(boolean flag) + { + this.flag = flag; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("postId", getPostId()) + .append("postCode", getPostCode()) + .append("postName", getPostName()) + .append("postSort", getPostSort()) + .append("status", getStatus()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysRoleDept.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysRoleDept.java new file mode 100644 index 0000000..47b21bf --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysRoleDept.java @@ -0,0 +1,46 @@ +package com.ruoyi.system.domain; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 角色和部门关联 sys_role_dept + * + * @author ruoyi + */ +public class SysRoleDept +{ + /** 角色ID */ + private Long roleId; + + /** 部门ID */ + private Long deptId; + + public Long getRoleId() + { + return roleId; + } + + public void setRoleId(Long roleId) + { + this.roleId = roleId; + } + + public Long getDeptId() + { + return deptId; + } + + public void setDeptId(Long deptId) + { + this.deptId = deptId; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("roleId", getRoleId()) + .append("deptId", getDeptId()) + .toString(); + } +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysRoleMenu.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysRoleMenu.java new file mode 100644 index 0000000..de10a74 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysRoleMenu.java @@ -0,0 +1,46 @@ +package com.ruoyi.system.domain; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 角色和菜单关联 sys_role_menu + * + * @author ruoyi + */ +public class SysRoleMenu +{ + /** 角色ID */ + private Long roleId; + + /** 菜单ID */ + private Long menuId; + + public Long getRoleId() + { + return roleId; + } + + public void setRoleId(Long roleId) + { + this.roleId = roleId; + } + + public Long getMenuId() + { + return menuId; + } + + public void setMenuId(Long menuId) + { + this.menuId = menuId; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("roleId", getRoleId()) + .append("menuId", getMenuId()) + .toString(); + } +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserOnline.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserOnline.java new file mode 100644 index 0000000..2bbd318 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserOnline.java @@ -0,0 +1,113 @@ +package com.ruoyi.system.domain; + +/** + * 当前在线会话 + * + * @author ruoyi + */ +public class SysUserOnline +{ + /** 会话编号 */ + private String tokenId; + + /** 部门名称 */ + private String deptName; + + /** 用户名称 */ + private String userName; + + /** 登录IP地址 */ + private String ipaddr; + + /** 登录地址 */ + private String loginLocation; + + /** 浏览器类型 */ + private String browser; + + /** 操作系统 */ + private String os; + + /** 登录时间 */ + private Long loginTime; + + public String getTokenId() + { + return tokenId; + } + + public void setTokenId(String tokenId) + { + this.tokenId = tokenId; + } + + public String getDeptName() + { + return deptName; + } + + public void setDeptName(String deptName) + { + this.deptName = deptName; + } + + public String getUserName() + { + return userName; + } + + public void setUserName(String userName) + { + this.userName = userName; + } + + public String getIpaddr() + { + return ipaddr; + } + + public void setIpaddr(String ipaddr) + { + this.ipaddr = ipaddr; + } + + public String getLoginLocation() + { + return loginLocation; + } + + public void setLoginLocation(String loginLocation) + { + this.loginLocation = loginLocation; + } + + public String getBrowser() + { + return browser; + } + + public void setBrowser(String browser) + { + this.browser = browser; + } + + public String getOs() + { + return os; + } + + public void setOs(String os) + { + this.os = os; + } + + public Long getLoginTime() + { + return loginTime; + } + + public void setLoginTime(Long loginTime) + { + this.loginTime = loginTime; + } +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserPost.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserPost.java new file mode 100644 index 0000000..6e8c416 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserPost.java @@ -0,0 +1,46 @@ +package com.ruoyi.system.domain; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 用户和岗位关联 sys_user_post + * + * @author ruoyi + */ +public class SysUserPost +{ + /** 用户ID */ + private Long userId; + + /** 岗位ID */ + private Long postId; + + public Long getUserId() + { + return userId; + } + + public void setUserId(Long userId) + { + this.userId = userId; + } + + public Long getPostId() + { + return postId; + } + + public void setPostId(Long postId) + { + this.postId = postId; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("userId", getUserId()) + .append("postId", getPostId()) + .toString(); + } +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserRole.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserRole.java new file mode 100644 index 0000000..4d15810 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserRole.java @@ -0,0 +1,46 @@ +package com.ruoyi.system.domain; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 用户和角色关联 sys_user_role + * + * @author ruoyi + */ +public class SysUserRole +{ + /** 用户ID */ + private Long userId; + + /** 角色ID */ + private Long roleId; + + public Long getUserId() + { + return userId; + } + + public void setUserId(Long userId) + { + this.userId = userId; + } + + public Long getRoleId() + { + return roleId; + } + + public void setRoleId(Long roleId) + { + this.roleId = roleId; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("userId", getUserId()) + .append("roleId", getRoleId()) + .toString(); + } +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/MetaVo.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/MetaVo.java new file mode 100644 index 0000000..a5d5fdc --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/MetaVo.java @@ -0,0 +1,106 @@ +package com.ruoyi.system.domain.vo; + +import com.ruoyi.common.utils.StringUtils; + +/** + * 路由显示信息 + * + * @author ruoyi + */ +public class MetaVo +{ + /** + * 设置该路由在侧边栏和面包屑中展示的名字 + */ + private String title; + + /** + * 设置该路由的图标,对应路径src/assets/icons/svg + */ + private String icon; + + /** + * 设置为true,则不会被 缓存 + */ + private boolean noCache; + + /** + * 内链地址(http(s)://开头) + */ + private String link; + + public MetaVo() + { + } + + public MetaVo(String title, String icon) + { + this.title = title; + this.icon = icon; + } + + public MetaVo(String title, String icon, boolean noCache) + { + this.title = title; + this.icon = icon; + this.noCache = noCache; + } + + public MetaVo(String title, String icon, String link) + { + this.title = title; + this.icon = icon; + this.link = link; + } + + public MetaVo(String title, String icon, boolean noCache, String link) + { + this.title = title; + this.icon = icon; + this.noCache = noCache; + if (StringUtils.ishttp(link)) + { + this.link = link; + } + } + + public boolean isNoCache() + { + return noCache; + } + + public void setNoCache(boolean noCache) + { + this.noCache = noCache; + } + + public String getTitle() + { + return title; + } + + public void setTitle(String title) + { + this.title = title; + } + + public String getIcon() + { + return icon; + } + + public void setIcon(String icon) + { + this.icon = icon; + } + + public String getLink() + { + return link; + } + + public void setLink(String link) + { + this.link = link; + } +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/RouterVo.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/RouterVo.java new file mode 100644 index 0000000..afff8c9 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/RouterVo.java @@ -0,0 +1,148 @@ +package com.ruoyi.system.domain.vo; + +import com.fasterxml.jackson.annotation.JsonInclude; +import java.util.List; + +/** + * 路由配置信息 + * + * @author ruoyi + */ +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class RouterVo +{ + /** + * 路由名字 + */ + private String name; + + /** + * 路由地址 + */ + private String path; + + /** + * 是否隐藏路由,当设置 true 的时候该路由不会再侧边栏出现 + */ + private boolean hidden; + + /** + * 重定向地址,当设置 noRedirect 的时候该路由在面包屑导航中不可被点击 + */ + private String redirect; + + /** + * 组件地址 + */ + private String component; + + /** + * 路由参数:如 {"id": 1, "name": "ry"} + */ + private String query; + + /** + * 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面 + */ + private Boolean alwaysShow; + + /** + * 其他元素 + */ + private MetaVo meta; + + /** + * 子路由 + */ + private List children; + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public String getPath() + { + return path; + } + + public void setPath(String path) + { + this.path = path; + } + + public boolean getHidden() + { + return hidden; + } + + public void setHidden(boolean hidden) + { + this.hidden = hidden; + } + + public String getRedirect() + { + return redirect; + } + + public void setRedirect(String redirect) + { + this.redirect = redirect; + } + + public String getComponent() + { + return component; + } + + public void setComponent(String component) + { + this.component = component; + } + + public String getQuery() + { + return query; + } + + public void setQuery(String query) + { + this.query = query; + } + + public Boolean getAlwaysShow() + { + return alwaysShow; + } + + public void setAlwaysShow(Boolean alwaysShow) + { + this.alwaysShow = alwaysShow; + } + + public MetaVo getMeta() + { + return meta; + } + + public void setMeta(MetaVo meta) + { + this.meta = meta; + } + + public List getChildren() + { + return children; + } + + public void setChildren(List children) + { + this.children = children; + } +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysConfigMapper.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysConfigMapper.java new file mode 100644 index 0000000..13d49d6 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysConfigMapper.java @@ -0,0 +1,76 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.system.domain.SysConfig; + +/** + * 参数配置 数据层 + * + * @author ruoyi + */ +public interface SysConfigMapper +{ + /** + * 查询参数配置信息 + * + * @param config 参数配置信息 + * @return 参数配置信息 + */ + public SysConfig selectConfig(SysConfig config); + + /** + * 通过ID查询配置 + * + * @param configId 参数ID + * @return 参数配置信息 + */ + public SysConfig selectConfigById(Long configId); + + /** + * 查询参数配置列表 + * + * @param config 参数配置信息 + * @return 参数配置集合 + */ + public List selectConfigList(SysConfig config); + + /** + * 根据键名查询参数配置信息 + * + * @param configKey 参数键名 + * @return 参数配置信息 + */ + public SysConfig checkConfigKeyUnique(String configKey); + + /** + * 新增参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + public int insertConfig(SysConfig config); + + /** + * 修改参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + public int updateConfig(SysConfig config); + + /** + * 删除参数配置 + * + * @param configId 参数ID + * @return 结果 + */ + public int deleteConfigById(Long configId); + + /** + * 批量删除参数信息 + * + * @param configIds 需要删除的参数ID + * @return 结果 + */ + public int deleteConfigByIds(Long[] configIds); +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDeptMapper.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDeptMapper.java new file mode 100644 index 0000000..978c9c1 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDeptMapper.java @@ -0,0 +1,125 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.ruoyi.common.core.domain.entity.SysDept; + +/** + * 部门管理 数据层 + * + * @author ruoyi + */ +public interface SysDeptMapper +{ + /** + * 查询部门管理数据 + * + * @param dept 部门信息 + * @return 部门信息集合 + */ + public List selectDeptList(SysDept dept); + + /** + * 根据角色ID查询部门树信息 + * + * @param roleId 角色ID + * @param deptCheckStrictly 部门树选择项是否关联显示 + * @return 选中部门列表 + */ + public List selectDeptListByRoleId(@Param("roleId") Long roleId, @Param("deptCheckStrictly") boolean deptCheckStrictly); + + /** + * 根据部门ID查询信息 + * + * @param deptId 部门ID + * @return 部门信息 + */ + public SysDept selectDeptById(Long deptId); + + /** + * 根据ID查询所有子部门 + * + * @param deptId 部门ID + * @return 部门列表 + */ + public List selectChildrenDeptById(Long deptId); + + /** + * 根据ID查询所有子部门(正常状态) + * + * @param deptId 部门ID + * @return 子部门数 + */ + public int selectNormalChildrenDeptById(Long deptId); + + /** + * 是否存在子节点 + * + * @param deptId 部门ID + * @return 结果 + */ + public int hasChildByDeptId(Long deptId); + + /** + * 查询部门是否存在用户 + * + * @param deptId 部门ID + * @return 结果 + */ + public int checkDeptExistUser(Long deptId); + + /** + * 校验部门名称是否唯一 + * + * @param deptName 部门名称 + * @param parentId 父部门ID + * @return 结果 + */ + public SysDept checkDeptNameUnique(@Param("deptName") String deptName, @Param("parentId") Long parentId); + + /** + * 新增部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + public int insertDept(SysDept dept); + + /** + * 修改部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + public int updateDept(SysDept dept); + + /** + * 修改所在部门正常状态 + * + * @param deptIds 部门ID组 + */ + public void updateDeptStatusNormal(Long[] deptIds); + + /** + * 修改子元素关系 + * + * @param depts 子元素 + * @return 结果 + */ + public int updateDeptChildren(@Param("depts") List depts); + + /** + * 保存部门排序 + * + * @param dept 部门信息 + */ + public void updateDeptSort(SysDept dept); + + /** + * 删除部门管理信息 + * + * @param deptId 部门ID + * @return 结果 + */ + public int deleteDeptById(Long deptId); +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictDataMapper.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictDataMapper.java new file mode 100644 index 0000000..a341f1e --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictDataMapper.java @@ -0,0 +1,95 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.ruoyi.common.core.domain.entity.SysDictData; + +/** + * 字典表 数据层 + * + * @author ruoyi + */ +public interface SysDictDataMapper +{ + /** + * 根据条件分页查询字典数据 + * + * @param dictData 字典数据信息 + * @return 字典数据集合信息 + */ + public List selectDictDataList(SysDictData dictData); + + /** + * 根据字典类型查询字典数据 + * + * @param dictType 字典类型 + * @return 字典数据集合信息 + */ + public List selectDictDataByType(String dictType); + + /** + * 根据字典类型和字典键值查询字典数据信息 + * + * @param dictType 字典类型 + * @param dictValue 字典键值 + * @return 字典标签 + */ + public String selectDictLabel(@Param("dictType") String dictType, @Param("dictValue") String dictValue); + + /** + * 根据字典数据ID查询信息 + * + * @param dictCode 字典数据ID + * @return 字典数据 + */ + public SysDictData selectDictDataById(Long dictCode); + + /** + * 查询字典数据 + * + * @param dictType 字典类型 + * @return 字典数据 + */ + public int countDictDataByType(String dictType); + + /** + * 通过字典ID删除字典数据信息 + * + * @param dictCode 字典数据ID + * @return 结果 + */ + public int deleteDictDataById(Long dictCode); + + /** + * 批量删除字典数据信息 + * + * @param dictCodes 需要删除的字典数据ID + * @return 结果 + */ + public int deleteDictDataByIds(Long[] dictCodes); + + /** + * 新增字典数据信息 + * + * @param dictData 字典数据信息 + * @return 结果 + */ + public int insertDictData(SysDictData dictData); + + /** + * 修改字典数据信息 + * + * @param dictData 字典数据信息 + * @return 结果 + */ + public int updateDictData(SysDictData dictData); + + /** + * 同步修改字典类型 + * + * @param oldDictType 旧字典类型 + * @param newDictType 新旧字典类型 + * @return 结果 + */ + public int updateDictDataType(@Param("oldDictType") String oldDictType, @Param("newDictType") String newDictType); +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictTypeMapper.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictTypeMapper.java new file mode 100644 index 0000000..5fb48fb --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictTypeMapper.java @@ -0,0 +1,83 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.common.core.domain.entity.SysDictType; + +/** + * 字典表 数据层 + * + * @author ruoyi + */ +public interface SysDictTypeMapper +{ + /** + * 根据条件分页查询字典类型 + * + * @param dictType 字典类型信息 + * @return 字典类型集合信息 + */ + public List selectDictTypeList(SysDictType dictType); + + /** + * 根据所有字典类型 + * + * @return 字典类型集合信息 + */ + public List selectDictTypeAll(); + + /** + * 根据字典类型ID查询信息 + * + * @param dictId 字典类型ID + * @return 字典类型 + */ + public SysDictType selectDictTypeById(Long dictId); + + /** + * 根据字典类型查询信息 + * + * @param dictType 字典类型 + * @return 字典类型 + */ + public SysDictType selectDictTypeByType(String dictType); + + /** + * 通过字典ID删除字典信息 + * + * @param dictId 字典ID + * @return 结果 + */ + public int deleteDictTypeById(Long dictId); + + /** + * 批量删除字典类型信息 + * + * @param dictIds 需要删除的字典ID + * @return 结果 + */ + public int deleteDictTypeByIds(Long[] dictIds); + + /** + * 新增字典类型信息 + * + * @param dictType 字典类型信息 + * @return 结果 + */ + public int insertDictType(SysDictType dictType); + + /** + * 修改字典类型信息 + * + * @param dictType 字典类型信息 + * @return 结果 + */ + public int updateDictType(SysDictType dictType); + + /** + * 校验字典类型称是否唯一 + * + * @param dictType 字典类型 + * @return 结果 + */ + public SysDictType checkDictTypeUnique(String dictType); +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysLogininforMapper.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysLogininforMapper.java new file mode 100644 index 0000000..629866f --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysLogininforMapper.java @@ -0,0 +1,42 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.system.domain.SysLogininfor; + +/** + * 系统访问日志情况信息 数据层 + * + * @author ruoyi + */ +public interface SysLogininforMapper +{ + /** + * 新增系统登录日志 + * + * @param logininfor 访问日志对象 + */ + public void insertLogininfor(SysLogininfor logininfor); + + /** + * 查询系统登录日志集合 + * + * @param logininfor 访问日志对象 + * @return 登录记录集合 + */ + public List selectLogininforList(SysLogininfor logininfor); + + /** + * 批量删除系统登录日志 + * + * @param infoIds 需要删除的登录日志ID + * @return 结果 + */ + public int deleteLogininforByIds(Long[] infoIds); + + /** + * 清空系统登录日志 + * + * @return 结果 + */ + public int cleanLogininfor(); +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysMenuMapper.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysMenuMapper.java new file mode 100644 index 0000000..750555d --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysMenuMapper.java @@ -0,0 +1,141 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.ruoyi.common.core.domain.entity.SysMenu; + +/** + * 菜单表 数据层 + * + * @author ruoyi + */ +public interface SysMenuMapper +{ + /** + * 查询系统菜单列表 + * + * @param menu 菜单信息 + * @return 菜单列表 + */ + public List selectMenuList(SysMenu menu); + + /** + * 根据用户所有权限 + * + * @return 权限列表 + */ + public List selectMenuPerms(); + + /** + * 根据用户查询系统菜单列表 + * + * @param menu 菜单信息 + * @return 菜单列表 + */ + public List selectMenuListByUserId(SysMenu menu); + + /** + * 根据角色ID查询权限 + * + * @param roleId 角色ID + * @return 权限列表 + */ + public List selectMenuPermsByRoleId(Long roleId); + + /** + * 根据用户ID查询权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + public List selectMenuPermsByUserId(Long userId); + + /** + * 根据用户ID查询菜单 + * + * @return 菜单列表 + */ + public List selectMenuTreeAll(); + + /** + * 根据用户ID查询菜单 + * + * @param userId 用户ID + * @return 菜单列表 + */ + public List selectMenuTreeByUserId(Long userId); + + /** + * 根据角色ID查询菜单树信息 + * + * @param roleId 角色ID + * @param menuCheckStrictly 菜单树选择项是否关联显示 + * @return 选中菜单列表 + */ + public List selectMenuListByRoleId(@Param("roleId") Long roleId, @Param("menuCheckStrictly") boolean menuCheckStrictly); + + /** + * 根据菜单ID查询信息 + * + * @param menuId 菜单ID + * @return 菜单信息 + */ + public SysMenu selectMenuById(Long menuId); + + /** + * 是否存在菜单子节点 + * + * @param menuId 菜单ID + * @return 结果 + */ + public int hasChildByMenuId(Long menuId); + + /** + * 新增菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + public int insertMenu(SysMenu menu); + + /** + * 修改菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + public int updateMenu(SysMenu menu); + + /** + * 保存菜单排序 + * + * @param menu 菜单信息 + */ + public void updateMenuSort(SysMenu menu); + + /** + * 删除菜单管理信息 + * + * @param menuId 菜单ID + * @return 结果 + */ + public int deleteMenuById(Long menuId); + + /** + * 校验菜单名称是否唯一 + * + * @param menuName 菜单名称 + * @param parentId 父菜单ID + * @return 结果 + */ + public SysMenu checkMenuNameUnique(@Param("menuName") String menuName, @Param("parentId") Long parentId); + + /** + * 根据路由路径或名称查询菜单信息(用于唯一性校验) + * + * @param path 路由地址 + * @param routeName 路由名称 + * @return 匹配的菜单列表 + */ + public List selectMenusByPathOrRouteName(@Param("path") String path, @Param("routeName") String routeName); +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysNoticeMapper.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysNoticeMapper.java new file mode 100644 index 0000000..c34f0a2 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysNoticeMapper.java @@ -0,0 +1,60 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.system.domain.SysNotice; + +/** + * 通知公告表 数据层 + * + * @author ruoyi + */ +public interface SysNoticeMapper +{ + /** + * 查询公告信息 + * + * @param noticeId 公告ID + * @return 公告信息 + */ + public SysNotice selectNoticeById(Long noticeId); + + /** + * 查询公告列表 + * + * @param notice 公告信息 + * @return 公告集合 + */ + public List selectNoticeList(SysNotice notice); + + /** + * 新增公告 + * + * @param notice 公告信息 + * @return 结果 + */ + public int insertNotice(SysNotice notice); + + /** + * 修改公告 + * + * @param notice 公告信息 + * @return 结果 + */ + public int updateNotice(SysNotice notice); + + /** + * 批量删除公告 + * + * @param noticeId 公告ID + * @return 结果 + */ + public int deleteNoticeById(Long noticeId); + + /** + * 批量删除公告信息 + * + * @param noticeIds 需要删除的公告ID + * @return 结果 + */ + public int deleteNoticeByIds(Long[] noticeIds); +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysNoticeReadMapper.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysNoticeReadMapper.java new file mode 100644 index 0000000..529b8e1 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysNoticeReadMapper.java @@ -0,0 +1,75 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import java.util.Map; +import org.apache.ibatis.annotations.Param; +import com.ruoyi.system.domain.SysNotice; +import com.ruoyi.system.domain.SysNoticeRead; + +/** + * 公告已读记录 数据层 + * + * @author ruoyi + */ +public interface SysNoticeReadMapper +{ + /** + * 新增已读记录(忽略重复) + * + * @param noticeRead 已读记录 + * @return 结果 + */ + public int insertNoticeRead(SysNoticeRead noticeRead); + + /** + * 查询某用户未读公告数量 + * + * @param userId 用户ID + * @return 未读数量 + */ + public int selectUnreadCount(@Param("userId") Long userId); + + /** + * 查询某用户是否已读某公告 + * + * @param noticeId 公告ID + * @param userId 用户ID + * @return 已读记录数(0未读 1已读) + */ + public int selectIsRead(@Param("noticeId") Long noticeId, @Param("userId") Long userId); + + /** + * 批量标记已读 + * + * @param userId 用户ID + * @param noticeIds 公告ID数组 + * @return 结果 + */ + public int insertNoticeReadBatch(@Param("userId") Long userId, @Param("noticeIds") Long[] noticeIds); + + /** + * 查询带已读状态的公告列表(SQL层限制条数,一次查询完成) + * + * @param userId 用户ID + * @param limit 最多返回条数 + * @return 带 isRead 标记的公告列表 + */ + public List selectNoticeListWithReadStatus(@Param("userId") Long userId, @Param("limit") int limit); + + /** + * 查询已阅读某公告的用户列表 + * + * @param noticeId 公告ID + * @param searchValue 搜索值 + * @return 已读用户列表 + */ + public List> selectReadUsersByNoticeId(@Param("noticeId") Long noticeId, @Param("searchValue") String searchValue); + + /** + * 公告删除时清理对应已读记录 + * + * @param noticeIds 公告ID数组 + * @return 结果 + */ + public int deleteByNoticeIds(@Param("noticeIds") Long[] noticeIds); +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysOperLogMapper.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysOperLogMapper.java new file mode 100644 index 0000000..2ae6457 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysOperLogMapper.java @@ -0,0 +1,48 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.system.domain.SysOperLog; + +/** + * 操作日志 数据层 + * + * @author ruoyi + */ +public interface SysOperLogMapper +{ + /** + * 新增操作日志 + * + * @param operLog 操作日志对象 + */ + public void insertOperlog(SysOperLog operLog); + + /** + * 查询系统操作日志集合 + * + * @param operLog 操作日志对象 + * @return 操作日志集合 + */ + public List selectOperLogList(SysOperLog operLog); + + /** + * 批量删除系统操作日志 + * + * @param operIds 需要删除的操作日志ID + * @return 结果 + */ + public int deleteOperLogByIds(Long[] operIds); + + /** + * 查询操作日志详细 + * + * @param operId 操作ID + * @return 操作日志对象 + */ + public SysOperLog selectOperLogById(Long operId); + + /** + * 清空操作日志 + */ + public void cleanOperLog(); +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysPostMapper.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysPostMapper.java new file mode 100644 index 0000000..19be227 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysPostMapper.java @@ -0,0 +1,99 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.system.domain.SysPost; + +/** + * 岗位信息 数据层 + * + * @author ruoyi + */ +public interface SysPostMapper +{ + /** + * 查询岗位数据集合 + * + * @param post 岗位信息 + * @return 岗位数据集合 + */ + public List selectPostList(SysPost post); + + /** + * 查询所有岗位 + * + * @return 岗位列表 + */ + public List selectPostAll(); + + /** + * 通过岗位ID查询岗位信息 + * + * @param postId 岗位ID + * @return 角色对象信息 + */ + public SysPost selectPostById(Long postId); + + /** + * 根据用户ID获取岗位选择框列表 + * + * @param userId 用户ID + * @return 选中岗位ID列表 + */ + public List selectPostListByUserId(Long userId); + + /** + * 查询用户所属岗位组 + * + * @param userName 用户名 + * @return 结果 + */ + public List selectPostsByUserName(String userName); + + /** + * 删除岗位信息 + * + * @param postId 岗位ID + * @return 结果 + */ + public int deletePostById(Long postId); + + /** + * 批量删除岗位信息 + * + * @param postIds 需要删除的岗位ID + * @return 结果 + */ + public int deletePostByIds(Long[] postIds); + + /** + * 修改岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + public int updatePost(SysPost post); + + /** + * 新增岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + public int insertPost(SysPost post); + + /** + * 校验岗位名称 + * + * @param postName 岗位名称 + * @return 结果 + */ + public SysPost checkPostNameUnique(String postName); + + /** + * 校验岗位编码 + * + * @param postCode 岗位编码 + * @return 结果 + */ + public SysPost checkPostCodeUnique(String postCode); +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleDeptMapper.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleDeptMapper.java new file mode 100644 index 0000000..f9d3a2f --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleDeptMapper.java @@ -0,0 +1,44 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.system.domain.SysRoleDept; + +/** + * 角色与部门关联表 数据层 + * + * @author ruoyi + */ +public interface SysRoleDeptMapper +{ + /** + * 通过角色ID删除角色和部门关联 + * + * @param roleId 角色ID + * @return 结果 + */ + public int deleteRoleDeptByRoleId(Long roleId); + + /** + * 批量删除角色部门关联信息 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteRoleDept(Long[] ids); + + /** + * 查询部门使用数量 + * + * @param deptId 部门ID + * @return 结果 + */ + public int selectCountRoleDeptByDeptId(Long deptId); + + /** + * 批量新增角色部门信息 + * + * @param roleDeptList 角色部门列表 + * @return 结果 + */ + public int batchRoleDept(List roleDeptList); +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleMapper.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleMapper.java new file mode 100644 index 0000000..cf2bd8c --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleMapper.java @@ -0,0 +1,107 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.common.core.domain.entity.SysRole; + +/** + * 角色表 数据层 + * + * @author ruoyi + */ +public interface SysRoleMapper +{ + /** + * 根据条件分页查询角色数据 + * + * @param role 角色信息 + * @return 角色数据集合信息 + */ + public List selectRoleList(SysRole role); + + /** + * 根据用户ID查询角色 + * + * @param userId 用户ID + * @return 角色列表 + */ + public List selectRolePermissionByUserId(Long userId); + + /** + * 查询所有角色 + * + * @return 角色列表 + */ + public List selectRoleAll(); + + /** + * 根据用户ID获取角色选择框列表 + * + * @param userId 用户ID + * @return 选中角色ID列表 + */ + public List selectRoleListByUserId(Long userId); + + /** + * 通过角色ID查询角色 + * + * @param roleId 角色ID + * @return 角色对象信息 + */ + public SysRole selectRoleById(Long roleId); + + /** + * 根据用户ID查询角色 + * + * @param userName 用户名 + * @return 角色列表 + */ + public List selectRolesByUserName(String userName); + + /** + * 校验角色名称是否唯一 + * + * @param roleName 角色名称 + * @return 角色信息 + */ + public SysRole checkRoleNameUnique(String roleName); + + /** + * 校验角色权限是否唯一 + * + * @param roleKey 角色权限 + * @return 角色信息 + */ + public SysRole checkRoleKeyUnique(String roleKey); + + /** + * 修改角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int updateRole(SysRole role); + + /** + * 新增角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int insertRole(SysRole role); + + /** + * 通过角色ID删除角色 + * + * @param roleId 角色ID + * @return 结果 + */ + public int deleteRoleById(Long roleId); + + /** + * 批量删除角色信息 + * + * @param roleIds 需要删除的角色ID + * @return 结果 + */ + public int deleteRoleByIds(Long[] roleIds); +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleMenuMapper.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleMenuMapper.java new file mode 100644 index 0000000..6602bee --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleMenuMapper.java @@ -0,0 +1,44 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.system.domain.SysRoleMenu; + +/** + * 角色与菜单关联表 数据层 + * + * @author ruoyi + */ +public interface SysRoleMenuMapper +{ + /** + * 查询菜单使用数量 + * + * @param menuId 菜单ID + * @return 结果 + */ + public int checkMenuExistRole(Long menuId); + + /** + * 通过角色ID删除角色和菜单关联 + * + * @param roleId 角色ID + * @return 结果 + */ + public int deleteRoleMenuByRoleId(Long roleId); + + /** + * 批量删除角色菜单关联信息 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteRoleMenu(Long[] ids); + + /** + * 批量新增角色菜单信息 + * + * @param roleMenuList 角色菜单列表 + * @return 结果 + */ + public int batchRoleMenu(List roleMenuList); +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java new file mode 100644 index 0000000..64ead00 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java @@ -0,0 +1,148 @@ +package com.ruoyi.system.mapper; + +import java.util.Date; +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.ruoyi.common.core.domain.entity.SysUser; + +/** + * 用户表 数据层 + * + * @author ruoyi + */ +public interface SysUserMapper +{ + /** + * 根据条件分页查询用户列表 + * + * @param sysUser 用户信息 + * @return 用户信息集合信息 + */ + public List selectUserList(SysUser sysUser); + + /** + * 根据条件分页查询已配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectAllocatedList(SysUser user); + + /** + * 根据条件分页查询未分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectUnallocatedList(SysUser user); + + /** + * 通过用户名查询用户 + * + * @param userName 用户名 + * @return 用户对象信息 + */ + public SysUser selectUserByUserName(String userName); + + /** + * 通过用户ID查询用户 + * + * @param userId 用户ID + * @return 用户对象信息 + */ + public SysUser selectUserById(Long userId); + + /** + * 新增用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + public int insertUser(SysUser user); + + /** + * 修改用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + public int updateUser(SysUser user); + + /** + * 修改用户头像 + * + * @param userId 用户ID + * @param avatar 头像地址 + * @return 结果 + */ + public int updateUserAvatar(@Param("userId") Long userId, @Param("avatar") String avatar); + + /** + * 修改用户状态 + * + * @param userId 用户ID + * @param status 状态 + * @return 结果 + */ + public int updateUserStatus(@Param("userId") Long userId, @Param("status") String status); + public int updateRoleType(@Param("userId") Long userId, @Param("roleType") String roleType); + + /** + * 更新用户登录信息(IP和登录时间) + * + * @param userId 用户ID + * @param loginIp 登录IP地址 + * @param loginDate 登录时间 + * @return 结果 + */ + public int updateLoginInfo(@Param("userId") Long userId, @Param("loginIp") String loginIp, @Param("loginDate") Date loginDate); + + /** + * 重置用户密码 + * + * @param userId 用户ID + * @param password 密码 + * @return 结果 + */ + public int resetUserPwd(@Param("userId") Long userId, @Param("password") String password); + + /** + * 通过用户ID删除用户 + * + * @param userId 用户ID + * @return 结果 + */ + public int deleteUserById(Long userId); + + /** + * 批量删除用户信息 + * + * @param userIds 需要删除的用户ID + * @return 结果 + */ + public int deleteUserByIds(Long[] userIds); + + /** + * 校验用户名称是否唯一 + * + * @param userName 用户名称 + * @return 结果 + */ + public SysUser checkUserNameUnique(String userName); + + /** + * 校验手机号码是否唯一 + * + * @param phonenumber 手机号码 + * @return 结果 + */ + public SysUser checkPhoneUnique(String phonenumber); + + /** + * 校验email是否唯一 + * + * @param email 用户邮箱 + * @return 结果 + */ + public SysUser checkEmailUnique(String email); +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserPostMapper.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserPostMapper.java new file mode 100644 index 0000000..2a6a720 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserPostMapper.java @@ -0,0 +1,44 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.system.domain.SysUserPost; + +/** + * 用户与岗位关联表 数据层 + * + * @author ruoyi + */ +public interface SysUserPostMapper +{ + /** + * 通过用户ID删除用户和岗位关联 + * + * @param userId 用户ID + * @return 结果 + */ + public int deleteUserPostByUserId(Long userId); + + /** + * 通过岗位ID查询岗位使用数量 + * + * @param postId 岗位ID + * @return 结果 + */ + public int countUserPostById(Long postId); + + /** + * 批量删除用户和岗位关联 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteUserPost(Long[] ids); + + /** + * 批量新增用户岗位信息 + * + * @param userPostList 用户岗位列表 + * @return 结果 + */ + public int batchUserPost(List userPostList); +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserRoleMapper.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserRoleMapper.java new file mode 100644 index 0000000..3143ec8 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserRoleMapper.java @@ -0,0 +1,62 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.ruoyi.system.domain.SysUserRole; + +/** + * 用户与角色关联表 数据层 + * + * @author ruoyi + */ +public interface SysUserRoleMapper +{ + /** + * 通过用户ID删除用户和角色关联 + * + * @param userId 用户ID + * @return 结果 + */ + public int deleteUserRoleByUserId(Long userId); + + /** + * 批量删除用户和角色关联 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteUserRole(Long[] ids); + + /** + * 通过角色ID查询角色使用数量 + * + * @param roleId 角色ID + * @return 结果 + */ + public int countUserRoleByRoleId(Long roleId); + + /** + * 批量新增用户角色信息 + * + * @param userRoleList 用户角色列表 + * @return 结果 + */ + public int batchUserRole(List userRoleList); + + /** + * 删除用户和角色关联信息 + * + * @param userRole 用户和角色关联信息 + * @return 结果 + */ + public int deleteUserRoleInfo(SysUserRole userRole); + + /** + * 批量取消授权用户角色 + * + * @param roleId 角色ID + * @param userIds 需要删除的用户数据ID + * @return 结果 + */ + public int deleteUserRoleInfos(@Param("roleId") Long roleId, @Param("userIds") Long[] userIds); +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysConfigService.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysConfigService.java new file mode 100644 index 0000000..b307776 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysConfigService.java @@ -0,0 +1,89 @@ +package com.ruoyi.system.service; + +import java.util.List; +import com.ruoyi.system.domain.SysConfig; + +/** + * 参数配置 服务层 + * + * @author ruoyi + */ +public interface ISysConfigService +{ + /** + * 查询参数配置信息 + * + * @param configId 参数配置ID + * @return 参数配置信息 + */ + public SysConfig selectConfigById(Long configId); + + /** + * 根据键名查询参数配置信息 + * + * @param configKey 参数键名 + * @return 参数键值 + */ + public String selectConfigByKey(String configKey); + + /** + * 获取验证码开关 + * + * @return true开启,false关闭 + */ + public boolean selectCaptchaEnabled(); + + /** + * 查询参数配置列表 + * + * @param config 参数配置信息 + * @return 参数配置集合 + */ + public List selectConfigList(SysConfig config); + + /** + * 新增参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + public int insertConfig(SysConfig config); + + /** + * 修改参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + public int updateConfig(SysConfig config); + + /** + * 批量删除参数信息 + * + * @param configIds 需要删除的参数ID + */ + public void deleteConfigByIds(Long[] configIds); + + /** + * 加载参数缓存数据 + */ + public void loadingConfigCache(); + + /** + * 清空参数缓存数据 + */ + public void clearConfigCache(); + + /** + * 重置参数缓存数据 + */ + public void resetConfigCache(); + + /** + * 校验参数键名是否唯一 + * + * @param config 参数信息 + * @return 结果 + */ + public boolean checkConfigKeyUnique(SysConfig config); +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDeptService.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDeptService.java new file mode 100644 index 0000000..7f16843 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDeptService.java @@ -0,0 +1,132 @@ +package com.ruoyi.system.service; + +import java.util.List; +import com.ruoyi.common.core.domain.TreeSelect; +import com.ruoyi.common.core.domain.entity.SysDept; + +/** + * 部门管理 服务层 + * + * @author ruoyi + */ +public interface ISysDeptService +{ + /** + * 查询部门管理数据 + * + * @param dept 部门信息 + * @return 部门信息集合 + */ + public List selectDeptList(SysDept dept); + + /** + * 查询部门树结构信息 + * + * @param dept 部门信息 + * @return 部门树信息集合 + */ + public List selectDeptTreeList(SysDept dept); + + /** + * 构建前端所需要树结构 + * + * @param depts 部门列表 + * @return 树结构列表 + */ + public List buildDeptTree(List depts); + + /** + * 构建前端所需要下拉树结构 + * + * @param depts 部门列表 + * @return 下拉树结构列表 + */ + public List buildDeptTreeSelect(List depts); + + /** + * 根据角色ID查询部门树信息 + * + * @param roleId 角色ID + * @return 选中部门列表 + */ + public List selectDeptListByRoleId(Long roleId); + + /** + * 根据部门ID查询信息 + * + * @param deptId 部门ID + * @return 部门信息 + */ + public SysDept selectDeptById(Long deptId); + + /** + * 根据ID查询所有子部门(正常状态) + * + * @param deptId 部门ID + * @return 子部门数 + */ + public int selectNormalChildrenDeptById(Long deptId); + + /** + * 是否存在部门子节点 + * + * @param deptId 部门ID + * @return 结果 + */ + public boolean hasChildByDeptId(Long deptId); + + /** + * 查询部门是否存在用户 + * + * @param deptId 部门ID + * @return 结果 true 存在 false 不存在 + */ + public boolean checkDeptExistUser(Long deptId); + + /** + * 校验部门名称是否唯一 + * + * @param dept 部门信息 + * @return 结果 + */ + public boolean checkDeptNameUnique(SysDept dept); + + /** + * 校验部门是否有数据权限 + * + * @param deptId 部门id + */ + public void checkDeptDataScope(Long deptId); + + /** + * 新增保存部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + public int insertDept(SysDept dept); + + /** + * 修改保存部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + public int updateDept(SysDept dept); + + /** + * 保存部门排序 + * + * @param deptIds 部门ID数组 + * @param orderNums 排序数组 + */ + public void updateDeptSort(String[] deptIds, String[] orderNums); + + /** + * 删除部门管理信息 + * + * @param deptId 部门ID + * @return 结果 + */ + public int deleteDeptById(Long deptId); +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDictDataService.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDictDataService.java new file mode 100644 index 0000000..9bc4f13 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDictDataService.java @@ -0,0 +1,60 @@ +package com.ruoyi.system.service; + +import java.util.List; +import com.ruoyi.common.core.domain.entity.SysDictData; + +/** + * 字典 业务层 + * + * @author ruoyi + */ +public interface ISysDictDataService +{ + /** + * 根据条件分页查询字典数据 + * + * @param dictData 字典数据信息 + * @return 字典数据集合信息 + */ + public List selectDictDataList(SysDictData dictData); + + /** + * 根据字典类型和字典键值查询字典数据信息 + * + * @param dictType 字典类型 + * @param dictValue 字典键值 + * @return 字典标签 + */ + public String selectDictLabel(String dictType, String dictValue); + + /** + * 根据字典数据ID查询信息 + * + * @param dictCode 字典数据ID + * @return 字典数据 + */ + public SysDictData selectDictDataById(Long dictCode); + + /** + * 批量删除字典数据信息 + * + * @param dictCodes 需要删除的字典数据ID + */ + public void deleteDictDataByIds(Long[] dictCodes); + + /** + * 新增保存字典数据信息 + * + * @param dictData 字典数据信息 + * @return 结果 + */ + public int insertDictData(SysDictData dictData); + + /** + * 修改保存字典数据信息 + * + * @param dictData 字典数据信息 + * @return 结果 + */ + public int updateDictData(SysDictData dictData); +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDictTypeService.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDictTypeService.java new file mode 100644 index 0000000..01c1c1d --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDictTypeService.java @@ -0,0 +1,98 @@ +package com.ruoyi.system.service; + +import java.util.List; +import com.ruoyi.common.core.domain.entity.SysDictData; +import com.ruoyi.common.core.domain.entity.SysDictType; + +/** + * 字典 业务层 + * + * @author ruoyi + */ +public interface ISysDictTypeService +{ + /** + * 根据条件分页查询字典类型 + * + * @param dictType 字典类型信息 + * @return 字典类型集合信息 + */ + public List selectDictTypeList(SysDictType dictType); + + /** + * 根据所有字典类型 + * + * @return 字典类型集合信息 + */ + public List selectDictTypeAll(); + + /** + * 根据字典类型查询字典数据 + * + * @param dictType 字典类型 + * @return 字典数据集合信息 + */ + public List selectDictDataByType(String dictType); + + /** + * 根据字典类型ID查询信息 + * + * @param dictId 字典类型ID + * @return 字典类型 + */ + public SysDictType selectDictTypeById(Long dictId); + + /** + * 根据字典类型查询信息 + * + * @param dictType 字典类型 + * @return 字典类型 + */ + public SysDictType selectDictTypeByType(String dictType); + + /** + * 批量删除字典信息 + * + * @param dictIds 需要删除的字典ID + */ + public void deleteDictTypeByIds(Long[] dictIds); + + /** + * 加载字典缓存数据 + */ + public void loadingDictCache(); + + /** + * 清空字典缓存数据 + */ + public void clearDictCache(); + + /** + * 重置字典缓存数据 + */ + public void resetDictCache(); + + /** + * 新增保存字典类型信息 + * + * @param dictType 字典类型信息 + * @return 结果 + */ + public int insertDictType(SysDictType dictType); + + /** + * 修改保存字典类型信息 + * + * @param dictType 字典类型信息 + * @return 结果 + */ + public int updateDictType(SysDictType dictType); + + /** + * 校验字典类型称是否唯一 + * + * @param dictType 字典类型 + * @return 结果 + */ + public boolean checkDictTypeUnique(SysDictType dictType); +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysLogininforService.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysLogininforService.java new file mode 100644 index 0000000..ce3151d --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysLogininforService.java @@ -0,0 +1,40 @@ +package com.ruoyi.system.service; + +import java.util.List; +import com.ruoyi.system.domain.SysLogininfor; + +/** + * 系统访问日志情况信息 服务层 + * + * @author ruoyi + */ +public interface ISysLogininforService +{ + /** + * 新增系统登录日志 + * + * @param logininfor 访问日志对象 + */ + public void insertLogininfor(SysLogininfor logininfor); + + /** + * 查询系统登录日志集合 + * + * @param logininfor 访问日志对象 + * @return 登录记录集合 + */ + public List selectLogininforList(SysLogininfor logininfor); + + /** + * 批量删除系统登录日志 + * + * @param infoIds 需要删除的登录日志ID + * @return 结果 + */ + public int deleteLogininforByIds(Long[] infoIds); + + /** + * 清空系统登录日志 + */ + public void cleanLogininfor(); +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysMenuService.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysMenuService.java new file mode 100644 index 0000000..aa3a564 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysMenuService.java @@ -0,0 +1,160 @@ +package com.ruoyi.system.service; + +import java.util.List; +import java.util.Set; +import com.ruoyi.common.core.domain.TreeSelect; +import com.ruoyi.common.core.domain.entity.SysMenu; +import com.ruoyi.system.domain.vo.RouterVo; + +/** + * 菜单 业务层 + * + * @author ruoyi + */ +public interface ISysMenuService +{ + /** + * 根据用户查询系统菜单列表 + * + * @param userId 用户ID + * @return 菜单列表 + */ + public List selectMenuList(Long userId); + + /** + * 根据用户查询系统菜单列表 + * + * @param menu 菜单信息 + * @param userId 用户ID + * @return 菜单列表 + */ + public List selectMenuList(SysMenu menu, Long userId); + + /** + * 根据用户ID查询权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + public Set selectMenuPermsByUserId(Long userId); + + /** + * 根据角色ID查询权限 + * + * @param roleId 角色ID + * @return 权限列表 + */ + public Set selectMenuPermsByRoleId(Long roleId); + + /** + * 根据用户ID查询菜单树信息 + * + * @param userId 用户ID + * @return 菜单列表 + */ + public List selectMenuTreeByUserId(Long userId); + + /** + * 根据角色ID查询菜单树信息 + * + * @param roleId 角色ID + * @return 选中菜单列表 + */ + public List selectMenuListByRoleId(Long roleId); + + /** + * 构建前端路由所需要的菜单 + * + * @param menus 菜单列表 + * @return 路由列表 + */ + public List buildMenus(List menus); + + /** + * 构建前端所需要树结构 + * + * @param menus 菜单列表 + * @return 树结构列表 + */ + public List buildMenuTree(List menus); + + /** + * 构建前端所需要下拉树结构 + * + * @param menus 菜单列表 + * @return 下拉树结构列表 + */ + public List buildMenuTreeSelect(List menus); + + /** + * 根据菜单ID查询信息 + * + * @param menuId 菜单ID + * @return 菜单信息 + */ + public SysMenu selectMenuById(Long menuId); + + /** + * 是否存在菜单子节点 + * + * @param menuId 菜单ID + * @return 结果 true 存在 false 不存在 + */ + public boolean hasChildByMenuId(Long menuId); + + /** + * 查询菜单是否存在角色 + * + * @param menuId 菜单ID + * @return 结果 true 存在 false 不存在 + */ + public boolean checkMenuExistRole(Long menuId); + + /** + * 新增保存菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + public int insertMenu(SysMenu menu); + + /** + * 修改保存菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + public int updateMenu(SysMenu menu); + + /** + * 保存菜单排序 + * + * @param menuIds 菜单ID + * @param orderNums 排序ID + */ + public void updateMenuSort(String[] menuIds, String[] orderNums); + + /** + * 删除菜单管理信息 + * + * @param menuId 菜单ID + * @return 结果 + */ + public int deleteMenuById(Long menuId); + + /** + * 校验菜单名称是否唯一 + * + * @param menu 菜单信息 + * @return 结果 + */ + public boolean checkMenuNameUnique(SysMenu menu); + + /** + * 校验路由组合是否唯一 + * + * @param menu 菜单信息 + * @return 结果 + */ + public boolean checkRouteConfigUnique(SysMenu menu); +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysNoticeReadService.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysNoticeReadService.java new file mode 100644 index 0000000..56e958a --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysNoticeReadService.java @@ -0,0 +1,62 @@ +package com.ruoyi.system.service; + +import java.util.List; +import java.util.Map; +import com.ruoyi.system.domain.SysNotice; + +/** + * 公告已读记录 服务层 + * + * @author ruoyi + */ +public interface ISysNoticeReadService +{ + /** + * 标记已读(幂等,重复调用不报错) + * + * @param noticeId 公告ID + * @param userId 用户ID + */ + public void markRead(Long noticeId, Long userId); + + /** + * 查询某用户未读公告数量 + * + * @param userId 用户ID + * @return 未读数量 + */ + public int selectUnreadCount(Long userId); + + /** + * 查询公告列表并标记当前用户已读状态(用于首页展示) + * + * @param userId 用户ID + * @param limit 最多返回条数 + * @return 带 isRead 标记的公告列表 + */ + public List selectNoticeListWithReadStatus(Long userId, int limit); + + /** + * 批量标记已读 + * + * @param userId 用户ID + * @param noticeIds 公告ID数组 + */ + public void markReadBatch(Long userId, Long[] noticeIds); + + /** + * 查询已阅读某公告的用户列表 + * + * @param noticeId 公告ID + * @param searchValue 搜索值 + * @return 已读用户列表 + */ + public List> selectReadUsersByNoticeId(Long noticeId, String searchValue); + + /** + * 删除公告时清理对应已读记录 + * + * @param noticeIds 公告ID数组 + */ + public void deleteByNoticeIds(Long[] noticeIds); +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysNoticeService.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysNoticeService.java new file mode 100644 index 0000000..2bb2c65 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysNoticeService.java @@ -0,0 +1,60 @@ +package com.ruoyi.system.service; + +import java.util.List; +import com.ruoyi.system.domain.SysNotice; + +/** + * 公告 服务层 + * + * @author ruoyi + */ +public interface ISysNoticeService +{ + /** + * 查询公告信息 + * + * @param noticeId 公告ID + * @return 公告信息 + */ + public SysNotice selectNoticeById(Long noticeId); + + /** + * 查询公告列表 + * + * @param notice 公告信息 + * @return 公告集合 + */ + public List selectNoticeList(SysNotice notice); + + /** + * 新增公告 + * + * @param notice 公告信息 + * @return 结果 + */ + public int insertNotice(SysNotice notice); + + /** + * 修改公告 + * + * @param notice 公告信息 + * @return 结果 + */ + public int updateNotice(SysNotice notice); + + /** + * 删除公告信息 + * + * @param noticeId 公告ID + * @return 结果 + */ + public int deleteNoticeById(Long noticeId); + + /** + * 批量删除公告信息 + * + * @param noticeIds 需要删除的公告ID + * @return 结果 + */ + public int deleteNoticeByIds(Long[] noticeIds); +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysOperLogService.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysOperLogService.java new file mode 100644 index 0000000..4fd8e5a --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysOperLogService.java @@ -0,0 +1,48 @@ +package com.ruoyi.system.service; + +import java.util.List; +import com.ruoyi.system.domain.SysOperLog; + +/** + * 操作日志 服务层 + * + * @author ruoyi + */ +public interface ISysOperLogService +{ + /** + * 新增操作日志 + * + * @param operLog 操作日志对象 + */ + public void insertOperlog(SysOperLog operLog); + + /** + * 查询系统操作日志集合 + * + * @param operLog 操作日志对象 + * @return 操作日志集合 + */ + public List selectOperLogList(SysOperLog operLog); + + /** + * 批量删除系统操作日志 + * + * @param operIds 需要删除的操作日志ID + * @return 结果 + */ + public int deleteOperLogByIds(Long[] operIds); + + /** + * 查询操作日志详细 + * + * @param operId 操作ID + * @return 操作日志对象 + */ + public SysOperLog selectOperLogById(Long operId); + + /** + * 清空操作日志 + */ + public void cleanOperLog(); +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysPostService.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysPostService.java new file mode 100644 index 0000000..84779bf --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysPostService.java @@ -0,0 +1,99 @@ +package com.ruoyi.system.service; + +import java.util.List; +import com.ruoyi.system.domain.SysPost; + +/** + * 岗位信息 服务层 + * + * @author ruoyi + */ +public interface ISysPostService +{ + /** + * 查询岗位信息集合 + * + * @param post 岗位信息 + * @return 岗位列表 + */ + public List selectPostList(SysPost post); + + /** + * 查询所有岗位 + * + * @return 岗位列表 + */ + public List selectPostAll(); + + /** + * 通过岗位ID查询岗位信息 + * + * @param postId 岗位ID + * @return 角色对象信息 + */ + public SysPost selectPostById(Long postId); + + /** + * 根据用户ID获取岗位选择框列表 + * + * @param userId 用户ID + * @return 选中岗位ID列表 + */ + public List selectPostListByUserId(Long userId); + + /** + * 校验岗位名称 + * + * @param post 岗位信息 + * @return 结果 + */ + public boolean checkPostNameUnique(SysPost post); + + /** + * 校验岗位编码 + * + * @param post 岗位信息 + * @return 结果 + */ + public boolean checkPostCodeUnique(SysPost post); + + /** + * 通过岗位ID查询岗位使用数量 + * + * @param postId 岗位ID + * @return 结果 + */ + public int countUserPostById(Long postId); + + /** + * 删除岗位信息 + * + * @param postId 岗位ID + * @return 结果 + */ + public int deletePostById(Long postId); + + /** + * 批量删除岗位信息 + * + * @param postIds 需要删除的岗位ID + * @return 结果 + */ + public int deletePostByIds(Long[] postIds); + + /** + * 新增保存岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + public int insertPost(SysPost post); + + /** + * 修改保存岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + public int updatePost(SysPost post); +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysRoleService.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysRoleService.java new file mode 100644 index 0000000..9185cce --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysRoleService.java @@ -0,0 +1,173 @@ +package com.ruoyi.system.service; + +import java.util.List; +import java.util.Set; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.system.domain.SysUserRole; + +/** + * 角色业务层 + * + * @author ruoyi + */ +public interface ISysRoleService +{ + /** + * 根据条件分页查询角色数据 + * + * @param role 角色信息 + * @return 角色数据集合信息 + */ + public List selectRoleList(SysRole role); + + /** + * 根据用户ID查询角色列表 + * + * @param userId 用户ID + * @return 角色列表 + */ + public List selectRolesByUserId(Long userId); + + /** + * 根据用户ID查询角色权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + public Set selectRolePermissionByUserId(Long userId); + + /** + * 查询所有角色 + * + * @return 角色列表 + */ + public List selectRoleAll(); + + /** + * 根据用户ID获取角色选择框列表 + * + * @param userId 用户ID + * @return 选中角色ID列表 + */ + public List selectRoleListByUserId(Long userId); + + /** + * 通过角色ID查询角色 + * + * @param roleId 角色ID + * @return 角色对象信息 + */ + public SysRole selectRoleById(Long roleId); + + /** + * 校验角色名称是否唯一 + * + * @param role 角色信息 + * @return 结果 + */ + public boolean checkRoleNameUnique(SysRole role); + + /** + * 校验角色权限是否唯一 + * + * @param role 角色信息 + * @return 结果 + */ + public boolean checkRoleKeyUnique(SysRole role); + + /** + * 校验角色是否允许操作 + * + * @param role 角色信息 + */ + public void checkRoleAllowed(SysRole role); + + /** + * 校验角色是否有数据权限 + * + * @param roleIds 角色id + */ + public void checkRoleDataScope(Long... roleIds); + + /** + * 通过角色ID查询角色使用数量 + * + * @param roleId 角色ID + * @return 结果 + */ + public int countUserRoleByRoleId(Long roleId); + + /** + * 新增保存角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int insertRole(SysRole role); + + /** + * 修改保存角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int updateRole(SysRole role); + + /** + * 修改角色状态 + * + * @param role 角色信息 + * @return 结果 + */ + public int updateRoleStatus(SysRole role); + + /** + * 修改数据权限信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int authDataScope(SysRole role); + + /** + * 通过角色ID删除角色 + * + * @param roleId 角色ID + * @return 结果 + */ + public int deleteRoleById(Long roleId); + + /** + * 批量删除角色信息 + * + * @param roleIds 需要删除的角色ID + * @return 结果 + */ + public int deleteRoleByIds(Long[] roleIds); + + /** + * 取消授权用户角色 + * + * @param userRole 用户和角色关联信息 + * @return 结果 + */ + public int deleteAuthUser(SysUserRole userRole); + + /** + * 批量取消授权用户角色 + * + * @param roleId 角色ID + * @param userIds 需要取消授权的用户数据ID + * @return 结果 + */ + public int deleteAuthUsers(Long roleId, Long[] userIds); + + /** + * 批量选择授权用户角色 + * + * @param roleId 角色ID + * @param userIds 需要删除的用户数据ID + * @return 结果 + */ + public int insertAuthUsers(Long roleId, Long[] userIds); +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserOnlineService.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserOnlineService.java new file mode 100644 index 0000000..8eb5448 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserOnlineService.java @@ -0,0 +1,48 @@ +package com.ruoyi.system.service; + +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.system.domain.SysUserOnline; + +/** + * 在线用户 服务层 + * + * @author ruoyi + */ +public interface ISysUserOnlineService +{ + /** + * 通过登录地址查询信息 + * + * @param ipaddr 登录地址 + * @param user 用户信息 + * @return 在线用户信息 + */ + public SysUserOnline selectOnlineByIpaddr(String ipaddr, LoginUser user); + + /** + * 通过用户名称查询信息 + * + * @param userName 用户名称 + * @param user 用户信息 + * @return 在线用户信息 + */ + public SysUserOnline selectOnlineByUserName(String userName, LoginUser user); + + /** + * 通过登录地址/用户名称查询信息 + * + * @param ipaddr 登录地址 + * @param userName 用户名称 + * @param user 用户信息 + * @return 在线用户信息 + */ + public SysUserOnline selectOnlineByInfo(String ipaddr, String userName, LoginUser user); + + /** + * 设置在线用户信息 + * + * @param user 用户信息 + * @return 在线用户 + */ + public SysUserOnline loginUserToUserOnline(LoginUser user); +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java new file mode 100644 index 0000000..5e908b2 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java @@ -0,0 +1,234 @@ +package com.ruoyi.system.service; + +import java.util.Date; +import java.util.List; +import com.ruoyi.common.core.domain.entity.SysUser; + +/** + * 用户 业务层 + * + * @author ruoyi + */ +public interface ISysUserService +{ + /** + * 根据条件分页查询用户列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectUserList(SysUser user); + + /** + * 根据条件分页查询已分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectAllocatedList(SysUser user); + + /** + * 根据条件分页查询未分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectUnallocatedList(SysUser user); + + /** + * 通过用户名查询用户 + * + * @param userName 用户名 + * @return 用户对象信息 + */ + public SysUser selectUserByUserName(String userName); + + /** + * 通过用户ID查询用户 + * + * @param userId 用户ID + * @return 用户对象信息 + */ + public SysUser selectUserById(Long userId); + + /** + * 根据用户ID查询用户所属角色组 + * + * @param userName 用户名 + * @return 结果 + */ + public String selectUserRoleGroup(String userName); + + /** + * 根据用户ID查询用户所属岗位组 + * + * @param userName 用户名 + * @return 结果 + */ + public String selectUserPostGroup(String userName); + + /** + * 校验用户名称是否唯一 + * + * @param user 用户信息 + * @return 结果 + */ + public boolean checkUserNameUnique(SysUser user); + + /** + * 校验手机号码是否唯一 + * + * @param user 用户信息 + * @return 结果 + */ + public boolean checkPhoneUnique(SysUser user); + + /** + * 校验email是否唯一 + * + * @param user 用户信息 + * @return 结果 + */ + public boolean checkEmailUnique(SysUser user); + + /** + * 校验用户是否允许操作 + * + * @param user 用户信息 + */ + public void checkUserAllowed(SysUser user); + + /** + * 校验用户是否有数据权限 + * + * @param userId 用户id + */ + public void checkUserDataScope(Long userId); + + /** + * 新增用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + public int insertUser(SysUser user); + + /** + * 注册用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + public boolean registerUser(SysUser user); + + /** + * 修改用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + public int updateUser(SysUser user); + + /** + * 用户授权角色 + * + * @param userId 用户ID + * @param roleIds 角色组 + */ + public void insertUserAuth(Long userId, Long[] roleIds); + + /** + * 修改用户状态 + * + * @param user 用户信息 + * @return 结果 + */ + public int updateUserStatus(SysUser user); + + /** + * 修改用户基本信息 + * + * @param user 用户信息 + * @return 结果 + */ + public int updateUserProfile(SysUser user); + + /** + * 修改用户头像 + * + * @param userId 用户ID + * @param avatar 头像地址 + * @return 结果 + */ + public boolean updateUserAvatar(Long userId, String avatar); + + /** + * 更新用户登录信息(IP和登录时间) + * + * @param userId 用户ID + * @param loginIp 登录IP地址 + * @param loginDate 登录时间 + * @return 结果 + */ + public void updateLoginInfo(Long userId, String loginIp, Date loginDate); + + /** + * 重置用户密码 + * + * @param user 用户信息 + * @return 结果 + */ + public int resetPwd(SysUser user); + + /** + * 重置用户密码 + * + * @param userId 用户ID + * @param password 密码 + * @return 结果 + */ + public int resetUserPwd(Long userId, String password); + + /** + * 通过用户ID删除用户 + * + * @param userId 用户ID + * @return 结果 + */ + public int deleteUserById(Long userId); + + /** + * 批量删除用户信息 + * + * @param userIds 需要删除的用户ID + * @return 结果 + */ + public int deleteUserByIds(Long[] userIds); + + /** + * 校验手机号是否已注册 + * + * @param phone 手机号 + * @return true=已注册, false=未注册 + */ + public boolean isPhoneRegistered(String phone); + + /** + * 更新 sys_user.role_type (DB 列, entity 无字段) + * + * @param userId 用户ID + * @param roleType 角色类型 (admin/leader/manager/doctor/executor/sponsor) + * @return 影响行数 + */ + public int updateRoleType(Long userId, String roleType); + + /** + * 导入用户数据 + * + * @param userList 用户数据列表 + * @param isUpdateSupport 是否更新支持,如果已存在,则进行更新数据 + * @param operName 操作用户 + * @return 结果 + */ + public String importUser(List userList, Boolean isUpdateSupport, String operName); +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysConfigServiceImpl.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysConfigServiceImpl.java new file mode 100644 index 0000000..cdfdf42 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysConfigServiceImpl.java @@ -0,0 +1,229 @@ +package com.ruoyi.system.service.impl; + +import java.util.Collection; +import java.util.List; +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.domain.SysConfig; +import com.ruoyi.system.mapper.SysConfigMapper; +import com.ruoyi.system.service.ISysConfigService; + +/** + * 参数配置 服务层实现 + * + * @author ruoyi + */ +@Service +public class SysConfigServiceImpl implements ISysConfigService +{ + @Autowired + private SysConfigMapper configMapper; + + @Autowired + private RedisCache redisCache; + + /** + * 项目启动时,初始化参数到缓存 + */ + @PostConstruct + public void init() + { + loadingConfigCache(); + } + + /** + * 查询参数配置信息 + * + * @param configId 参数配置ID + * @return 参数配置信息 + */ + @Override + public SysConfig selectConfigById(Long configId) + { + SysConfig config = new SysConfig(); + config.setConfigId(configId); + return configMapper.selectConfig(config); + } + + /** + * 根据键名查询参数配置信息 + * + * @param configKey 参数key + * @return 参数键值 + */ + @Override + public String selectConfigByKey(String configKey) + { + String configValue = Convert.toStr(redisCache.getCacheObject(getCacheKey(configKey))); + if (StringUtils.isNotEmpty(configValue)) + { + return configValue; + } + SysConfig config = new SysConfig(); + config.setConfigKey(configKey); + SysConfig retConfig = configMapper.selectConfig(config); + if (StringUtils.isNotNull(retConfig)) + { + redisCache.setCacheObject(getCacheKey(configKey), retConfig.getConfigValue()); + return retConfig.getConfigValue(); + } + return StringUtils.EMPTY; + } + + /** + * 获取验证码开关 + * + * @return true开启,false关闭 + */ + @Override + public boolean selectCaptchaEnabled() + { + String captchaEnabled = selectConfigByKey("sys.account.captchaEnabled"); + if (StringUtils.isEmpty(captchaEnabled)) + { + return true; + } + return Convert.toBool(captchaEnabled); + } + + /** + * 查询参数配置列表 + * + * @param config 参数配置信息 + * @return 参数配置集合 + */ + @Override + public List selectConfigList(SysConfig config) + { + return configMapper.selectConfigList(config); + } + + /** + * 新增参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + @Override + public int insertConfig(SysConfig config) + { + int row = configMapper.insertConfig(config); + if (row > 0) + { + redisCache.setCacheObject(getCacheKey(config.getConfigKey()), config.getConfigValue()); + } + return row; + } + + /** + * 修改参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + @Override + public int updateConfig(SysConfig config) + { + SysConfig temp = configMapper.selectConfigById(config.getConfigId()); + if (!StringUtils.equals(temp.getConfigKey(), config.getConfigKey())) + { + redisCache.deleteObject(getCacheKey(temp.getConfigKey())); + } + + int row = configMapper.updateConfig(config); + if (row > 0) + { + redisCache.setCacheObject(getCacheKey(config.getConfigKey()), config.getConfigValue()); + } + return row; + } + + /** + * 批量删除参数信息 + * + * @param configIds 需要删除的参数ID + */ + @Override + public void deleteConfigByIds(Long[] configIds) + { + for (Long configId : configIds) + { + SysConfig config = selectConfigById(configId); + if (StringUtils.equals(UserConstants.YES, config.getConfigType())) + { + throw new ServiceException(String.format("内置参数【%1$s】不能删除 ", config.getConfigKey())); + } + configMapper.deleteConfigById(configId); + redisCache.deleteObject(getCacheKey(config.getConfigKey())); + } + } + + /** + * 加载参数缓存数据 + */ + @Override + public void loadingConfigCache() + { + List configsList = configMapper.selectConfigList(new SysConfig()); + for (SysConfig config : configsList) + { + redisCache.setCacheObject(getCacheKey(config.getConfigKey()), config.getConfigValue()); + } + } + + /** + * 清空参数缓存数据 + */ + @Override + public void clearConfigCache() + { + Collection keys = redisCache.keys(CacheConstants.SYS_CONFIG_KEY + "*"); + redisCache.deleteObject(keys); + } + + /** + * 重置参数缓存数据 + */ + @Override + public void resetConfigCache() + { + clearConfigCache(); + loadingConfigCache(); + } + + /** + * 校验参数键名是否唯一 + * + * @param config 参数配置信息 + * @return 结果 + */ + @Override + public boolean checkConfigKeyUnique(SysConfig config) + { + Long configId = StringUtils.isNull(config.getConfigId()) ? -1L : config.getConfigId(); + SysConfig info = configMapper.checkConfigKeyUnique(config.getConfigKey()); + if (StringUtils.isNotNull(info) && info.getConfigId().longValue() != configId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 设置cache key + * + * @param configKey 参数键 + * @return 缓存键key + */ + private String getCacheKey(String configKey) + { + return CacheConstants.SYS_CONFIG_KEY + configKey; + } +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDeptServiceImpl.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDeptServiceImpl.java new file mode 100644 index 0000000..957a9fa --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDeptServiceImpl.java @@ -0,0 +1,364 @@ +package com.ruoyi.system.service.impl; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.ruoyi.common.annotation.DataScope; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.TreeSelect; +import com.ruoyi.common.core.domain.entity.SysDept; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.spring.SpringUtils; +import com.ruoyi.system.mapper.SysDeptMapper; +import com.ruoyi.system.mapper.SysRoleMapper; +import com.ruoyi.system.service.ISysDeptService; + +/** + * 部门管理 服务实现 + * + * @author ruoyi + */ +@Service +public class SysDeptServiceImpl implements ISysDeptService +{ + @Autowired + private SysDeptMapper deptMapper; + + @Autowired + private SysRoleMapper roleMapper; + + /** + * 查询部门管理数据 + * + * @param dept 部门信息 + * @return 部门信息集合 + */ + @Override + @DataScope(deptAlias = "d") + public List selectDeptList(SysDept dept) + { + return deptMapper.selectDeptList(dept); + } + + /** + * 查询部门树结构信息 + * + * @param dept 部门信息 + * @return 部门树信息集合 + */ + @Override + public List selectDeptTreeList(SysDept dept) + { + List depts = SpringUtils.getAopProxy(this).selectDeptList(dept); + return buildDeptTreeSelect(depts); + } + + /** + * 构建前端所需要树结构 + * + * @param depts 部门列表 + * @return 树结构列表 + */ + @Override + public List buildDeptTree(List depts) + { + List returnList = new ArrayList(); + List tempList = depts.stream().map(SysDept::getDeptId).collect(Collectors.toList()); + for (SysDept dept : depts) + { + // 如果是顶级节点, 遍历该父节点的所有子节点 + if (!tempList.contains(dept.getParentId())) + { + recursionFn(depts, dept); + returnList.add(dept); + } + } + if (returnList.isEmpty()) + { + returnList = depts; + } + return returnList; + } + + /** + * 构建前端所需要下拉树结构 + * + * @param depts 部门列表 + * @return 下拉树结构列表 + */ + @Override + public List buildDeptTreeSelect(List depts) + { + List deptTrees = buildDeptTree(depts); + return deptTrees.stream().map(TreeSelect::new).collect(Collectors.toList()); + } + + /** + * 根据角色ID查询部门树信息 + * + * @param roleId 角色ID + * @return 选中部门列表 + */ + @Override + public List selectDeptListByRoleId(Long roleId) + { + SysRole role = roleMapper.selectRoleById(roleId); + return deptMapper.selectDeptListByRoleId(roleId, role.isDeptCheckStrictly()); + } + + /** + * 根据部门ID查询信息 + * + * @param deptId 部门ID + * @return 部门信息 + */ + @Override + public SysDept selectDeptById(Long deptId) + { + return deptMapper.selectDeptById(deptId); + } + + /** + * 根据ID查询所有子部门(正常状态) + * + * @param deptId 部门ID + * @return 子部门数 + */ + @Override + public int selectNormalChildrenDeptById(Long deptId) + { + return deptMapper.selectNormalChildrenDeptById(deptId); + } + + /** + * 是否存在子节点 + * + * @param deptId 部门ID + * @return 结果 + */ + @Override + public boolean hasChildByDeptId(Long deptId) + { + int result = deptMapper.hasChildByDeptId(deptId); + return result > 0; + } + + /** + * 查询部门是否存在用户 + * + * @param deptId 部门ID + * @return 结果 true 存在 false 不存在 + */ + @Override + public boolean checkDeptExistUser(Long deptId) + { + int result = deptMapper.checkDeptExistUser(deptId); + return result > 0; + } + + /** + * 校验部门名称是否唯一 + * + * @param dept 部门信息 + * @return 结果 + */ + @Override + public boolean checkDeptNameUnique(SysDept dept) + { + Long deptId = StringUtils.isNull(dept.getDeptId()) ? -1L : dept.getDeptId(); + SysDept info = deptMapper.checkDeptNameUnique(dept.getDeptName(), dept.getParentId()); + if (StringUtils.isNotNull(info) && info.getDeptId().longValue() != deptId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验部门是否有数据权限 + * + * @param deptId 部门id + */ + @Override + public void checkDeptDataScope(Long deptId) + { + if (!SecurityUtils.isAdmin() && StringUtils.isNotNull(deptId)) + { + SysDept dept = new SysDept(); + dept.setDeptId(deptId); + List depts = SpringUtils.getAopProxy(this).selectDeptList(dept); + if (StringUtils.isEmpty(depts)) + { + throw new ServiceException("没有权限访问部门数据!"); + } + } + } + + /** + * 新增保存部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + @Override + public int insertDept(SysDept dept) + { + SysDept info = deptMapper.selectDeptById(dept.getParentId()); + // 如果父节点不为正常状态,则不允许新增子节点 + if (!UserConstants.DEPT_NORMAL.equals(info.getStatus())) + { + throw new ServiceException("部门停用,不允许新增"); + } + dept.setAncestors(info.getAncestors() + "," + dept.getParentId()); + return deptMapper.insertDept(dept); + } + + /** + * 修改保存部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + @Override + public int updateDept(SysDept dept) + { + SysDept newParentDept = deptMapper.selectDeptById(dept.getParentId()); + SysDept oldDept = deptMapper.selectDeptById(dept.getDeptId()); + if (StringUtils.isNotNull(newParentDept) && StringUtils.isNotNull(oldDept)) + { + String newAncestors = newParentDept.getAncestors() + "," + newParentDept.getDeptId(); + String oldAncestors = oldDept.getAncestors(); + dept.setAncestors(newAncestors); + updateDeptChildren(dept.getDeptId(), newAncestors, oldAncestors); + } + int result = deptMapper.updateDept(dept); + if (UserConstants.DEPT_NORMAL.equals(dept.getStatus()) && StringUtils.isNotEmpty(dept.getAncestors()) + && !StringUtils.equals("0", dept.getAncestors())) + { + // 如果该部门是启用状态,则启用该部门的所有上级部门 + updateParentDeptStatusNormal(dept); + } + return result; + } + + /** + * 修改该部门的父级部门状态 + * + * @param dept 当前部门 + */ + private void updateParentDeptStatusNormal(SysDept dept) + { + String ancestors = dept.getAncestors(); + Long[] deptIds = Convert.toLongArray(ancestors); + deptMapper.updateDeptStatusNormal(deptIds); + } + + /** + * 修改子元素关系 + * + * @param deptId 被修改的部门ID + * @param newAncestors 新的父ID集合 + * @param oldAncestors 旧的父ID集合 + */ + public void updateDeptChildren(Long deptId, String newAncestors, String oldAncestors) + { + List children = deptMapper.selectChildrenDeptById(deptId); + for (SysDept child : children) + { + child.setAncestors(child.getAncestors().replaceFirst(oldAncestors, newAncestors)); + } + if (children.size() > 0) + { + deptMapper.updateDeptChildren(children); + } + } + + /** + * 保存部门排序 + * + * @param deptIds 部门ID数组 + * @param orderNums 排序数组 + */ + @Override + @Transactional + public void updateDeptSort(String[] deptIds, String[] orderNums) + { + try + { + for (int i = 0; i < deptIds.length; i++) + { + SysDept dept = new SysDept(); + dept.setDeptId(Convert.toLong(deptIds[i])); + dept.setOrderNum(Convert.toInt(orderNums[i])); + deptMapper.updateDeptSort(dept); + } + } + catch (Exception e) + { + throw new ServiceException("保存排序异常,请联系管理员"); + } + } + + /** + * 删除部门管理信息 + * + * @param deptId 部门ID + * @return 结果 + */ + @Override + public int deleteDeptById(Long deptId) + { + return deptMapper.deleteDeptById(deptId); + } + + /** + * 递归列表 + */ + private void recursionFn(List list, SysDept t) + { + // 得到子节点列表 + List childList = getChildList(list, t); + t.setChildren(childList); + for (SysDept tChild : childList) + { + if (hasChild(list, tChild)) + { + recursionFn(list, tChild); + } + } + } + + /** + * 得到子节点列表 + */ + private List getChildList(List list, SysDept t) + { + List tlist = new ArrayList(); + Iterator it = list.iterator(); + while (it.hasNext()) + { + SysDept n = (SysDept) it.next(); + if (StringUtils.isNotNull(n.getParentId()) && n.getParentId().longValue() == t.getDeptId().longValue()) + { + tlist.add(n); + } + } + return tlist; + } + + /** + * 判断是否有子节点 + */ + private boolean hasChild(List list, SysDept t) + { + return getChildList(list, t).size() > 0; + } +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictDataServiceImpl.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictDataServiceImpl.java new file mode 100644 index 0000000..fced569 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictDataServiceImpl.java @@ -0,0 +1,111 @@ +package com.ruoyi.system.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.common.core.domain.entity.SysDictData; +import com.ruoyi.common.utils.DictUtils; +import com.ruoyi.system.mapper.SysDictDataMapper; +import com.ruoyi.system.service.ISysDictDataService; + +/** + * 字典 业务层处理 + * + * @author ruoyi + */ +@Service +public class SysDictDataServiceImpl implements ISysDictDataService +{ + @Autowired + private SysDictDataMapper dictDataMapper; + + /** + * 根据条件分页查询字典数据 + * + * @param dictData 字典数据信息 + * @return 字典数据集合信息 + */ + @Override + public List selectDictDataList(SysDictData dictData) + { + return dictDataMapper.selectDictDataList(dictData); + } + + /** + * 根据字典类型和字典键值查询字典数据信息 + * + * @param dictType 字典类型 + * @param dictValue 字典键值 + * @return 字典标签 + */ + @Override + public String selectDictLabel(String dictType, String dictValue) + { + return dictDataMapper.selectDictLabel(dictType, dictValue); + } + + /** + * 根据字典数据ID查询信息 + * + * @param dictCode 字典数据ID + * @return 字典数据 + */ + @Override + public SysDictData selectDictDataById(Long dictCode) + { + return dictDataMapper.selectDictDataById(dictCode); + } + + /** + * 批量删除字典数据信息 + * + * @param dictCodes 需要删除的字典数据ID + */ + @Override + public void deleteDictDataByIds(Long[] dictCodes) + { + for (Long dictCode : dictCodes) + { + SysDictData data = selectDictDataById(dictCode); + dictDataMapper.deleteDictDataById(dictCode); + List dictDatas = dictDataMapper.selectDictDataByType(data.getDictType()); + DictUtils.setDictCache(data.getDictType(), dictDatas); + } + } + + /** + * 新增保存字典数据信息 + * + * @param data 字典数据信息 + * @return 结果 + */ + @Override + public int insertDictData(SysDictData data) + { + int row = dictDataMapper.insertDictData(data); + if (row > 0) + { + List dictDatas = dictDataMapper.selectDictDataByType(data.getDictType()); + DictUtils.setDictCache(data.getDictType(), dictDatas); + } + return row; + } + + /** + * 修改保存字典数据信息 + * + * @param data 字典数据信息 + * @return 结果 + */ + @Override + public int updateDictData(SysDictData data) + { + int row = dictDataMapper.updateDictData(data); + if (row > 0) + { + List dictDatas = dictDataMapper.selectDictDataByType(data.getDictType()); + DictUtils.setDictCache(data.getDictType(), dictDatas); + } + return row; + } +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.java new file mode 100644 index 0000000..37928c4 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.java @@ -0,0 +1,223 @@ +package com.ruoyi.system.service.impl; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.entity.SysDictData; +import com.ruoyi.common.core.domain.entity.SysDictType; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.DictUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.mapper.SysDictDataMapper; +import com.ruoyi.system.mapper.SysDictTypeMapper; +import com.ruoyi.system.service.ISysDictTypeService; + +/** + * 字典 业务层处理 + * + * @author ruoyi + */ +@Service +public class SysDictTypeServiceImpl implements ISysDictTypeService +{ + @Autowired + private SysDictTypeMapper dictTypeMapper; + + @Autowired + private SysDictDataMapper dictDataMapper; + + /** + * 项目启动时,初始化字典到缓存 + */ + @PostConstruct + public void init() + { + loadingDictCache(); + } + + /** + * 根据条件分页查询字典类型 + * + * @param dictType 字典类型信息 + * @return 字典类型集合信息 + */ + @Override + public List selectDictTypeList(SysDictType dictType) + { + return dictTypeMapper.selectDictTypeList(dictType); + } + + /** + * 根据所有字典类型 + * + * @return 字典类型集合信息 + */ + @Override + public List selectDictTypeAll() + { + return dictTypeMapper.selectDictTypeAll(); + } + + /** + * 根据字典类型查询字典数据 + * + * @param dictType 字典类型 + * @return 字典数据集合信息 + */ + @Override + public List selectDictDataByType(String dictType) + { + List dictDatas = DictUtils.getDictCache(dictType); + if (StringUtils.isNotEmpty(dictDatas)) + { + return dictDatas; + } + dictDatas = dictDataMapper.selectDictDataByType(dictType); + if (StringUtils.isNotEmpty(dictDatas)) + { + DictUtils.setDictCache(dictType, dictDatas); + return dictDatas; + } + return null; + } + + /** + * 根据字典类型ID查询信息 + * + * @param dictId 字典类型ID + * @return 字典类型 + */ + @Override + public SysDictType selectDictTypeById(Long dictId) + { + return dictTypeMapper.selectDictTypeById(dictId); + } + + /** + * 根据字典类型查询信息 + * + * @param dictType 字典类型 + * @return 字典类型 + */ + @Override + public SysDictType selectDictTypeByType(String dictType) + { + return dictTypeMapper.selectDictTypeByType(dictType); + } + + /** + * 批量删除字典类型信息 + * + * @param dictIds 需要删除的字典ID + */ + @Override + public void deleteDictTypeByIds(Long[] dictIds) + { + for (Long dictId : dictIds) + { + SysDictType dictType = selectDictTypeById(dictId); + if (dictDataMapper.countDictDataByType(dictType.getDictType()) > 0) + { + throw new ServiceException(String.format("%1$s已分配,不能删除", dictType.getDictName())); + } + dictTypeMapper.deleteDictTypeById(dictId); + DictUtils.removeDictCache(dictType.getDictType()); + } + } + + /** + * 加载字典缓存数据 + */ + @Override + public void loadingDictCache() + { + SysDictData dictData = new SysDictData(); + dictData.setStatus("0"); + Map> dictDataMap = dictDataMapper.selectDictDataList(dictData).stream().collect(Collectors.groupingBy(SysDictData::getDictType)); + for (Map.Entry> entry : dictDataMap.entrySet()) + { + DictUtils.setDictCache(entry.getKey(), entry.getValue().stream().sorted(Comparator.comparing(SysDictData::getDictSort)).collect(Collectors.toList())); + } + } + + /** + * 清空字典缓存数据 + */ + @Override + public void clearDictCache() + { + DictUtils.clearDictCache(); + } + + /** + * 重置字典缓存数据 + */ + @Override + public void resetDictCache() + { + clearDictCache(); + loadingDictCache(); + } + + /** + * 新增保存字典类型信息 + * + * @param dict 字典类型信息 + * @return 结果 + */ + @Override + public int insertDictType(SysDictType dict) + { + int row = dictTypeMapper.insertDictType(dict); + if (row > 0) + { + DictUtils.setDictCache(dict.getDictType(), null); + } + return row; + } + + /** + * 修改保存字典类型信息 + * + * @param dict 字典类型信息 + * @return 结果 + */ + @Override + @Transactional + public int updateDictType(SysDictType dict) + { + SysDictType oldDict = dictTypeMapper.selectDictTypeById(dict.getDictId()); + dictDataMapper.updateDictDataType(oldDict.getDictType(), dict.getDictType()); + int row = dictTypeMapper.updateDictType(dict); + if (row > 0) + { + List dictDatas = dictDataMapper.selectDictDataByType(dict.getDictType()); + DictUtils.setDictCache(dict.getDictType(), dictDatas); + } + return row; + } + + /** + * 校验字典类型称是否唯一 + * + * @param dict 字典类型 + * @return 结果 + */ + @Override + public boolean checkDictTypeUnique(SysDictType dict) + { + Long dictId = StringUtils.isNull(dict.getDictId()) ? -1L : dict.getDictId(); + SysDictType dictType = dictTypeMapper.checkDictTypeUnique(dict.getDictType()); + if (StringUtils.isNotNull(dictType) && dictType.getDictId().longValue() != dictId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysLogininforServiceImpl.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysLogininforServiceImpl.java new file mode 100644 index 0000000..216aecb --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysLogininforServiceImpl.java @@ -0,0 +1,65 @@ +package com.ruoyi.system.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.system.domain.SysLogininfor; +import com.ruoyi.system.mapper.SysLogininforMapper; +import com.ruoyi.system.service.ISysLogininforService; + +/** + * 系统访问日志情况信息 服务层处理 + * + * @author ruoyi + */ +@Service +public class SysLogininforServiceImpl implements ISysLogininforService +{ + + @Autowired + private SysLogininforMapper logininforMapper; + + /** + * 新增系统登录日志 + * + * @param logininfor 访问日志对象 + */ + @Override + public void insertLogininfor(SysLogininfor logininfor) + { + logininforMapper.insertLogininfor(logininfor); + } + + /** + * 查询系统登录日志集合 + * + * @param logininfor 访问日志对象 + * @return 登录记录集合 + */ + @Override + public List selectLogininforList(SysLogininfor logininfor) + { + return logininforMapper.selectLogininforList(logininfor); + } + + /** + * 批量删除系统登录日志 + * + * @param infoIds 需要删除的登录日志ID + * @return 结果 + */ + @Override + public int deleteLogininforByIds(Long[] infoIds) + { + return logininforMapper.deleteLogininforByIds(infoIds); + } + + /** + * 清空系统登录日志 + */ + @Override + public void cleanLogininfor() + { + logininforMapper.cleanLogininfor(); + } +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysMenuServiceImpl.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysMenuServiceImpl.java new file mode 100644 index 0000000..131861a --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysMenuServiceImpl.java @@ -0,0 +1,618 @@ +package com.ruoyi.system.service.impl; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.TreeSelect; +import com.ruoyi.common.core.domain.entity.SysMenu; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.domain.vo.MetaVo; +import com.ruoyi.system.domain.vo.RouterVo; +import com.ruoyi.system.mapper.SysMenuMapper; +import com.ruoyi.system.mapper.SysRoleMapper; +import com.ruoyi.system.mapper.SysRoleMenuMapper; +import com.ruoyi.system.service.ISysMenuService; + +/** + * 菜单 业务层处理 + * + * @author ruoyi + */ +@Service +public class SysMenuServiceImpl implements ISysMenuService +{ + private static final Logger log = LoggerFactory.getLogger(SysMenuServiceImpl.class); + + public static final String PREMISSION_STRING = "perms[\"{0}\"]"; + + public static final Long MENU_ROOT_ID = 0L; + + @Autowired + private SysMenuMapper menuMapper; + + @Autowired + private SysRoleMapper roleMapper; + + @Autowired + private SysRoleMenuMapper roleMenuMapper; + + /** + * 根据用户查询系统菜单列表 + * + * @param userId 用户ID + * @return 菜单列表 + */ + @Override + public List selectMenuList(Long userId) + { + return selectMenuList(new SysMenu(), userId); + } + + /** + * 查询系统菜单列表 + * + * @param menu 菜单信息 + * @return 菜单列表 + */ + @Override + public List selectMenuList(SysMenu menu, Long userId) + { + List menuList = null; + // 管理员显示所有菜单信息 + if (SecurityUtils.isAdmin(userId)) + { + menuList = menuMapper.selectMenuList(menu); + } + else + { + menu.getParams().put("userId", userId); + menuList = menuMapper.selectMenuListByUserId(menu); + } + return menuList; + } + + /** + * 根据用户ID查询权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + @Override + public Set selectMenuPermsByUserId(Long userId) + { + List perms = menuMapper.selectMenuPermsByUserId(userId); + Set permsSet = new HashSet<>(); + for (String perm : perms) + { + if (StringUtils.isNotEmpty(perm)) + { + permsSet.addAll(Arrays.asList(perm.trim().split(","))); + } + } + return permsSet; + } + + /** + * 根据角色ID查询权限 + * + * @param roleId 角色ID + * @return 权限列表 + */ + @Override + public Set selectMenuPermsByRoleId(Long roleId) + { + List perms = menuMapper.selectMenuPermsByRoleId(roleId); + Set permsSet = new HashSet<>(); + for (String perm : perms) + { + if (StringUtils.isNotEmpty(perm)) + { + permsSet.addAll(Arrays.asList(perm.trim().split(","))); + } + } + return permsSet; + } + + /** + * 根据用户ID查询菜单 + * + * @param userId 用户名称 + * @return 菜单列表 + */ + @Override + public List selectMenuTreeByUserId(Long userId) + { + List menus = null; + if (SecurityUtils.isAdmin(userId)) + { + menus = menuMapper.selectMenuTreeAll(); + } + else + { + menus = menuMapper.selectMenuTreeByUserId(userId); + } + return getChildPerms(menus, MENU_ROOT_ID); + } + + /** + * 根据角色ID查询菜单树信息 + * + * @param roleId 角色ID + * @return 选中菜单列表 + */ + @Override + public List selectMenuListByRoleId(Long roleId) + { + SysRole role = roleMapper.selectRoleById(roleId); + return menuMapper.selectMenuListByRoleId(roleId, role.isMenuCheckStrictly()); + } + + /** + * 构建前端路由所需要的菜单 + * + * @param menus 菜单列表 + * @return 路由列表 + */ + @Override + public List buildMenus(List menus) + { + List routers = new LinkedList(); + for (SysMenu menu : menus) + { + RouterVo router = new RouterVo(); + router.setHidden("1".equals(menu.getVisible())); + router.setName(getRouteName(menu)); + router.setPath(getRouterPath(menu)); + router.setComponent(getComponent(menu)); + router.setQuery(menu.getQuery()); + router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath())); + List cMenus = menu.getChildren(); + if (StringUtils.isNotEmpty(cMenus) && UserConstants.TYPE_DIR.equals(menu.getMenuType())) + { + router.setAlwaysShow(true); + router.setRedirect("noRedirect"); + router.setChildren(buildMenus(cMenus)); + } + else if (isMenuFrame(menu)) + { + router.setMeta(null); + List childrenList = new ArrayList(); + RouterVo children = new RouterVo(); + children.setPath(menu.getPath()); + children.setComponent(menu.getComponent()); + children.setName(getRouteName(menu.getRouteName(), menu.getPath())); + children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath())); + children.setQuery(menu.getQuery()); + childrenList.add(children); + router.setChildren(childrenList); + } + else if (menu.getParentId().intValue() == MENU_ROOT_ID && isInnerLink(menu)) + { + router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon())); + router.setPath("/"); + List childrenList = new ArrayList(); + RouterVo children = new RouterVo(); + String routerPath = innerLinkReplaceEach(menu.getPath()); + children.setPath(routerPath); + children.setComponent(UserConstants.INNER_LINK); + children.setName(getRouteName(menu.getRouteName(), routerPath)); + children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), menu.getPath())); + childrenList.add(children); + router.setChildren(childrenList); + } + routers.add(router); + } + return routers; + } + + /** + * 构建前端所需要树结构 + * + * @param menus 菜单列表 + * @return 树结构列表 + */ + @Override + public List buildMenuTree(List menus) + { + List returnList = new ArrayList(); + List tempList = menus.stream().map(SysMenu::getMenuId).collect(Collectors.toList()); + for (Iterator iterator = menus.iterator(); iterator.hasNext();) + { + SysMenu menu = (SysMenu) iterator.next(); + // 如果是顶级节点, 遍历该父节点的所有子节点 + if (!tempList.contains(menu.getParentId())) + { + recursionFn(menus, menu); + returnList.add(menu); + } + } + if (returnList.isEmpty()) + { + returnList = menus; + } + return returnList; + } + + /** + * 构建前端所需要下拉树结构 + * + * @param menus 菜单列表 + * @return 下拉树结构列表 + */ + @Override + public List buildMenuTreeSelect(List menus) + { + List menuTrees = buildMenuTree(menus); + return menuTrees.stream().map(TreeSelect::new).collect(Collectors.toList()); + } + + /** + * 根据菜单ID查询信息 + * + * @param menuId 菜单ID + * @return 菜单信息 + */ + @Override + public SysMenu selectMenuById(Long menuId) + { + return menuMapper.selectMenuById(menuId); + } + + /** + * 是否存在菜单子节点 + * + * @param menuId 菜单ID + * @return 结果 + */ + @Override + public boolean hasChildByMenuId(Long menuId) + { + int result = menuMapper.hasChildByMenuId(menuId); + return result > 0; + } + + /** + * 查询菜单使用数量 + * + * @param menuId 菜单ID + * @return 结果 + */ + @Override + public boolean checkMenuExistRole(Long menuId) + { + int result = roleMenuMapper.checkMenuExistRole(menuId); + return result > 0; + } + + /** + * 新增保存菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + @Override + public int insertMenu(SysMenu menu) + { + return menuMapper.insertMenu(menu); + } + + /** + * 修改保存菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + @Override + public int updateMenu(SysMenu menu) + { + return menuMapper.updateMenu(menu); + } + + /** + * 保存菜单排序 + * + * @param menuIds 菜单ID + * @param orderNums 排序ID + */ + @Override + @Transactional + public void updateMenuSort(String[] menuIds, String[] orderNums) + { + try + { + for (int i = 0; i < menuIds.length; i++) + { + SysMenu menu = new SysMenu(); + menu.setMenuId(Convert.toLong(menuIds[i])); + menu.setOrderNum(Convert.toInt(orderNums[i])); + menuMapper.updateMenuSort(menu); + } + } + catch (Exception e) + { + throw new ServiceException("保存排序异常,请联系管理员"); + } + } + + /** + * 删除菜单管理信息 + * + * @param menuId 菜单ID + * @return 结果 + */ + @Override + public int deleteMenuById(Long menuId) + { + return menuMapper.deleteMenuById(menuId); + } + + /** + * 校验菜单名称是否唯一 + * + * @param menu 菜单信息 + * @return 结果 + */ + @Override + public boolean checkMenuNameUnique(SysMenu menu) + { + Long menuId = StringUtils.isNull(menu.getMenuId()) ? -1L : menu.getMenuId(); + SysMenu info = menuMapper.checkMenuNameUnique(menu.getMenuName(), menu.getParentId()); + if (StringUtils.isNotNull(info) && info.getMenuId().longValue() != menuId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验路由名称是否唯一 + * + * @param menu 菜单信息 + * @return 结果 + */ + @Override + public boolean checkRouteConfigUnique(SysMenu menu) + { + Long menuId = StringUtils.isNull(menu.getMenuId()) ? -1L : menu.getMenuId(); + Long parentId = menu.getParentId(); + String path = menu.getPath(); + String routeName = StringUtils.isEmpty(menu.getRouteName()) ? path : menu.getRouteName(); + List sysMenuList = menuMapper.selectMenusByPathOrRouteName(path, routeName); + for (SysMenu sysMenu : sysMenuList) + { + if (sysMenu.getMenuId().longValue() != menuId.longValue()) + { + Long dbParentId = sysMenu.getParentId(); + String dbPath = sysMenu.getPath(); + String dbRouteName = StringUtils.isEmpty(sysMenu.getRouteName()) ? dbPath : sysMenu.getRouteName(); + if (StringUtils.equalsAnyIgnoreCase(path, dbPath) && parentId.longValue() == dbParentId.longValue()) + { + log.warn("[同级路由冲突] 同级下已存在相同路由路径 '{}',冲突菜单:{}", dbPath, sysMenu.getMenuName()); + return UserConstants.NOT_UNIQUE; + } + else if (StringUtils.equalsAnyIgnoreCase(path, dbPath) && parentId.longValue() == MENU_ROOT_ID) + { + log.warn("[根目录路由冲突] 根目录下路由 '{}' 必须唯一,已被菜单 '{}' 占用", path, sysMenu.getMenuName()); + return UserConstants.NOT_UNIQUE; + } + else if (StringUtils.equalsAnyIgnoreCase(routeName, dbRouteName)) + { + log.warn("[路由名称冲突] 路由名称 '{}' 需全局唯一,已被菜单 '{}' 使用", routeName, sysMenu.getMenuName()); + return UserConstants.NOT_UNIQUE; + } + } + } + return UserConstants.UNIQUE; + } + + /** + * 获取路由名称 + * + * @param menu 菜单信息 + * @return 路由名称 + */ + public String getRouteName(SysMenu menu) + { + // 非外链并且是一级目录(类型为目录) + if (isMenuFrame(menu)) + { + return StringUtils.EMPTY; + } + return getRouteName(menu.getRouteName(), menu.getPath()); + } + + /** + * 获取路由名称,如没有配置路由名称则取路由地址 + * + * @param name 路由名称 + * @param path 路由地址 + * @return 路由名称(驼峰格式) + */ + public String getRouteName(String name, String path) + { + String routerName = StringUtils.isNotEmpty(name) ? name : path; + return StringUtils.capitalize(routerName); + } + + /** + * 获取路由地址 + * + * @param menu 菜单信息 + * @return 路由地址 + */ + public String getRouterPath(SysMenu menu) + { + String routerPath = menu.getPath(); + // 内链打开外网方式 + if (menu.getParentId().intValue() != MENU_ROOT_ID && isInnerLink(menu)) + { + routerPath = innerLinkReplaceEach(routerPath); + } + // 非外链并且是一级目录(类型为目录) + if (MENU_ROOT_ID == menu.getParentId().intValue() && UserConstants.TYPE_DIR.equals(menu.getMenuType()) + && UserConstants.NO_FRAME.equals(menu.getIsFrame())) + { + routerPath = "/" + menu.getPath(); + } + // 非外链并且是一级目录(类型为菜单) + else if (isMenuFrame(menu)) + { + routerPath = "/"; + } + return routerPath; + } + + /** + * 获取组件信息 + * + * @param menu 菜单信息 + * @return 组件信息 + */ + public String getComponent(SysMenu menu) + { + String component = UserConstants.LAYOUT; + if (StringUtils.isNotEmpty(menu.getComponent()) && !isMenuFrame(menu)) + { + component = menu.getComponent(); + } + else if (StringUtils.isEmpty(menu.getComponent()) && menu.getParentId().intValue() != MENU_ROOT_ID && isInnerLink(menu)) + { + component = UserConstants.INNER_LINK; + } + else if (StringUtils.isEmpty(menu.getComponent()) && isParentView(menu)) + { + component = UserConstants.PARENT_VIEW; + } + return component; + } + + /** + * 是否为菜单内部跳转 + * + * @param menu 菜单信息 + * @return 结果 + */ + public boolean isMenuFrame(SysMenu menu) + { + return menu.getParentId().intValue() == MENU_ROOT_ID && UserConstants.TYPE_MENU.equals(menu.getMenuType()) + && menu.getIsFrame().equals(UserConstants.NO_FRAME); + } + + /** + * 是否为parent_view组件 + * + * @param menu 菜单信息 + * @return 结果 + */ + public boolean isParentView(SysMenu menu) + { + return menu.getParentId().intValue() != MENU_ROOT_ID && UserConstants.TYPE_DIR.equals(menu.getMenuType()); + } + + /** + * 是否为内链组件 + * + * @param menu 菜单信息 + * @return 结果 + */ + public boolean isInnerLink(SysMenu menu) + { + return menu.getIsFrame().equals(UserConstants.NO_FRAME) && StringUtils.ishttp(menu.getPath()); + } + + /** + * 根据父节点的ID获取所有子节点 + * + * @param list 分类表 + * @param parentId 传入的父节点ID + * @return String + */ + public List getChildPerms(List list, long parentId) + { + List returnList = new ArrayList(); + for (Iterator iterator = list.iterator(); iterator.hasNext();) + { + SysMenu t = (SysMenu) iterator.next(); + // 一、根据传入的某个父节点ID,遍历该父节点的所有子节点 + if (t.getParentId() == parentId) + { + recursionFn(list, t); + returnList.add(t); + } + } + return returnList; + } + + /** + * 递归列表 + * + * @param list 分类表 + * @param t 子节点 + */ + private void recursionFn(List list, SysMenu t) + { + // 得到子节点列表 + List childList = getChildList(list, t); + t.setChildren(childList); + for (SysMenu tChild : childList) + { + if (hasChild(list, tChild)) + { + recursionFn(list, tChild); + } + } + } + + /** + * 得到子节点列表 + */ + private List getChildList(List list, SysMenu t) + { + List tlist = new ArrayList(); + Iterator it = list.iterator(); + while (it.hasNext()) + { + SysMenu n = (SysMenu) it.next(); + if (n.getParentId().longValue() == t.getMenuId().longValue()) + { + tlist.add(n); + } + } + return tlist; + } + + /** + * 判断是否有子节点 + */ + private boolean hasChild(List list, SysMenu t) + { + return getChildList(list, t).size() > 0; + } + + /** + * 内链域名特殊字符替换 + * + * @return 替换后的内链域名 + */ + public String innerLinkReplaceEach(String path) + { + return StringUtils.replaceEach(path, new String[] { Constants.HTTP, Constants.HTTPS, Constants.WWW, ".", ":" }, + new String[] { "", "", "", "/", "/" }); + } +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysNoticeReadServiceImpl.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysNoticeReadServiceImpl.java new file mode 100644 index 0000000..f8e63a6 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysNoticeReadServiceImpl.java @@ -0,0 +1,83 @@ +package com.ruoyi.system.service.impl; + +import java.util.List; +import java.util.Map; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.system.domain.SysNotice; +import com.ruoyi.system.domain.SysNoticeRead; +import com.ruoyi.system.mapper.SysNoticeReadMapper; +import com.ruoyi.system.service.ISysNoticeReadService; + +/** + * 公告已读记录 服务层实现 + * + * @author ruoyi + */ +@Service +public class SysNoticeReadServiceImpl implements ISysNoticeReadService +{ + @Autowired + private SysNoticeReadMapper noticeReadMapper; + + /** + * 标记已读 + */ + @Override + public void markRead(Long noticeId, Long userId) + { + SysNoticeRead record = new SysNoticeRead(); + record.setNoticeId(noticeId); + record.setUserId(userId); + noticeReadMapper.insertNoticeRead(record); + } + + /** + * 查询某用户未读公告数量 + */ + @Override + public int selectUnreadCount(Long userId) + { + return noticeReadMapper.selectUnreadCount(userId); + } + + /** + * 查询公告列表并标记当前用户已读状态 + */ + @Override + public List selectNoticeListWithReadStatus(Long userId, int limit) + { + return noticeReadMapper.selectNoticeListWithReadStatus(userId, limit); + } + + /** + * 批量标记已读 + */ + @Override + public void markReadBatch(Long userId, Long[] noticeIds) + { + if (noticeIds == null || noticeIds.length == 0) + { + return; + } + noticeReadMapper.insertNoticeReadBatch(userId, noticeIds); + } + + /** + * 查询已阅读某公告的用户列表 + */ + @Override + public List> selectReadUsersByNoticeId(Long noticeId, String searchValue) + { + return noticeReadMapper.selectReadUsersByNoticeId(noticeId, searchValue); + } + + /** + * 删除公告时清理对应已读记录 + */ + @Override + public void deleteByNoticeIds(Long[] noticeIds) + { + noticeReadMapper.deleteByNoticeIds(noticeIds); + } +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysNoticeServiceImpl.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysNoticeServiceImpl.java new file mode 100644 index 0000000..765438b --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysNoticeServiceImpl.java @@ -0,0 +1,92 @@ +package com.ruoyi.system.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.system.domain.SysNotice; +import com.ruoyi.system.mapper.SysNoticeMapper; +import com.ruoyi.system.service.ISysNoticeService; + +/** + * 公告 服务层实现 + * + * @author ruoyi + */ +@Service +public class SysNoticeServiceImpl implements ISysNoticeService +{ + @Autowired + private SysNoticeMapper noticeMapper; + + /** + * 查询公告信息 + * + * @param noticeId 公告ID + * @return 公告信息 + */ + @Override + public SysNotice selectNoticeById(Long noticeId) + { + return noticeMapper.selectNoticeById(noticeId); + } + + /** + * 查询公告列表 + * + * @param notice 公告信息 + * @return 公告集合 + */ + @Override + public List selectNoticeList(SysNotice notice) + { + return noticeMapper.selectNoticeList(notice); + } + + /** + * 新增公告 + * + * @param notice 公告信息 + * @return 结果 + */ + @Override + public int insertNotice(SysNotice notice) + { + return noticeMapper.insertNotice(notice); + } + + /** + * 修改公告 + * + * @param notice 公告信息 + * @return 结果 + */ + @Override + public int updateNotice(SysNotice notice) + { + return noticeMapper.updateNotice(notice); + } + + /** + * 删除公告对象 + * + * @param noticeId 公告ID + * @return 结果 + */ + @Override + public int deleteNoticeById(Long noticeId) + { + return noticeMapper.deleteNoticeById(noticeId); + } + + /** + * 批量删除公告信息 + * + * @param noticeIds 需要删除的公告ID + * @return 结果 + */ + @Override + public int deleteNoticeByIds(Long[] noticeIds) + { + return noticeMapper.deleteNoticeByIds(noticeIds); + } +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOperLogServiceImpl.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOperLogServiceImpl.java new file mode 100644 index 0000000..5489815 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOperLogServiceImpl.java @@ -0,0 +1,76 @@ +package com.ruoyi.system.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.system.domain.SysOperLog; +import com.ruoyi.system.mapper.SysOperLogMapper; +import com.ruoyi.system.service.ISysOperLogService; + +/** + * 操作日志 服务层处理 + * + * @author ruoyi + */ +@Service +public class SysOperLogServiceImpl implements ISysOperLogService +{ + @Autowired + private SysOperLogMapper operLogMapper; + + /** + * 新增操作日志 + * + * @param operLog 操作日志对象 + */ + @Override + public void insertOperlog(SysOperLog operLog) + { + operLogMapper.insertOperlog(operLog); + } + + /** + * 查询系统操作日志集合 + * + * @param operLog 操作日志对象 + * @return 操作日志集合 + */ + @Override + public List selectOperLogList(SysOperLog operLog) + { + return operLogMapper.selectOperLogList(operLog); + } + + /** + * 批量删除系统操作日志 + * + * @param operIds 需要删除的操作日志ID + * @return 结果 + */ + @Override + public int deleteOperLogByIds(Long[] operIds) + { + return operLogMapper.deleteOperLogByIds(operIds); + } + + /** + * 查询操作日志详细 + * + * @param operId 操作ID + * @return 操作日志对象 + */ + @Override + public SysOperLog selectOperLogById(Long operId) + { + return operLogMapper.selectOperLogById(operId); + } + + /** + * 清空操作日志 + */ + @Override + public void cleanOperLog() + { + operLogMapper.cleanOperLog(); + } +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysPostServiceImpl.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysPostServiceImpl.java new file mode 100644 index 0000000..5e5fe06 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysPostServiceImpl.java @@ -0,0 +1,178 @@ +package com.ruoyi.system.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.domain.SysPost; +import com.ruoyi.system.mapper.SysPostMapper; +import com.ruoyi.system.mapper.SysUserPostMapper; +import com.ruoyi.system.service.ISysPostService; + +/** + * 岗位信息 服务层处理 + * + * @author ruoyi + */ +@Service +public class SysPostServiceImpl implements ISysPostService +{ + @Autowired + private SysPostMapper postMapper; + + @Autowired + private SysUserPostMapper userPostMapper; + + /** + * 查询岗位信息集合 + * + * @param post 岗位信息 + * @return 岗位信息集合 + */ + @Override + public List selectPostList(SysPost post) + { + return postMapper.selectPostList(post); + } + + /** + * 查询所有岗位 + * + * @return 岗位列表 + */ + @Override + public List selectPostAll() + { + return postMapper.selectPostAll(); + } + + /** + * 通过岗位ID查询岗位信息 + * + * @param postId 岗位ID + * @return 角色对象信息 + */ + @Override + public SysPost selectPostById(Long postId) + { + return postMapper.selectPostById(postId); + } + + /** + * 根据用户ID获取岗位选择框列表 + * + * @param userId 用户ID + * @return 选中岗位ID列表 + */ + @Override + public List selectPostListByUserId(Long userId) + { + return postMapper.selectPostListByUserId(userId); + } + + /** + * 校验岗位名称是否唯一 + * + * @param post 岗位信息 + * @return 结果 + */ + @Override + public boolean checkPostNameUnique(SysPost post) + { + Long postId = StringUtils.isNull(post.getPostId()) ? -1L : post.getPostId(); + SysPost info = postMapper.checkPostNameUnique(post.getPostName()); + if (StringUtils.isNotNull(info) && info.getPostId().longValue() != postId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验岗位编码是否唯一 + * + * @param post 岗位信息 + * @return 结果 + */ + @Override + public boolean checkPostCodeUnique(SysPost post) + { + Long postId = StringUtils.isNull(post.getPostId()) ? -1L : post.getPostId(); + SysPost info = postMapper.checkPostCodeUnique(post.getPostCode()); + if (StringUtils.isNotNull(info) && info.getPostId().longValue() != postId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 通过岗位ID查询岗位使用数量 + * + * @param postId 岗位ID + * @return 结果 + */ + @Override + public int countUserPostById(Long postId) + { + return userPostMapper.countUserPostById(postId); + } + + /** + * 删除岗位信息 + * + * @param postId 岗位ID + * @return 结果 + */ + @Override + public int deletePostById(Long postId) + { + return postMapper.deletePostById(postId); + } + + /** + * 批量删除岗位信息 + * + * @param postIds 需要删除的岗位ID + * @return 结果 + */ + @Override + public int deletePostByIds(Long[] postIds) + { + for (Long postId : postIds) + { + SysPost post = selectPostById(postId); + if (countUserPostById(postId) > 0) + { + throw new ServiceException(String.format("%1$s已分配,不能删除", post.getPostName())); + } + } + return postMapper.deletePostByIds(postIds); + } + + /** + * 新增保存岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + @Override + public int insertPost(SysPost post) + { + return postMapper.insertPost(post); + } + + /** + * 修改保存岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + @Override + public int updatePost(SysPost post) + { + return postMapper.updatePost(post); + } +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysRoleServiceImpl.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysRoleServiceImpl.java new file mode 100644 index 0000000..89a84f6 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysRoleServiceImpl.java @@ -0,0 +1,426 @@ +package com.ruoyi.system.service.impl; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.ruoyi.common.annotation.DataScope; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.spring.SpringUtils; +import com.ruoyi.system.domain.SysRoleDept; +import com.ruoyi.system.domain.SysRoleMenu; +import com.ruoyi.system.domain.SysUserRole; +import com.ruoyi.system.mapper.SysRoleDeptMapper; +import com.ruoyi.system.mapper.SysRoleMapper; +import com.ruoyi.system.mapper.SysRoleMenuMapper; +import com.ruoyi.system.mapper.SysUserRoleMapper; +import com.ruoyi.system.service.ISysRoleService; + +/** + * 角色 业务层处理 + * + * @author ruoyi + */ +@Service +public class SysRoleServiceImpl implements ISysRoleService +{ + @Autowired + private SysRoleMapper roleMapper; + + @Autowired + private SysRoleMenuMapper roleMenuMapper; + + @Autowired + private SysUserRoleMapper userRoleMapper; + + @Autowired + private SysRoleDeptMapper roleDeptMapper; + + /** + * 根据条件分页查询角色数据 + * + * @param role 角色信息 + * @return 角色数据集合信息 + */ + @Override + @DataScope(deptAlias = "d") + public List selectRoleList(SysRole role) + { + return roleMapper.selectRoleList(role); + } + + /** + * 根据用户ID查询角色 + * + * @param userId 用户ID + * @return 角色列表 + */ + @Override + public List selectRolesByUserId(Long userId) + { + List userRoles = roleMapper.selectRolePermissionByUserId(userId); + List roles = selectRoleAll(); + for (SysRole role : roles) + { + for (SysRole userRole : userRoles) + { + if (role.getRoleId().longValue() == userRole.getRoleId().longValue()) + { + role.setFlag(true); + break; + } + } + } + return roles; + } + + /** + * 根据用户ID查询权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + @Override + public Set selectRolePermissionByUserId(Long userId) + { + List perms = roleMapper.selectRolePermissionByUserId(userId); + Set permsSet = new HashSet<>(); + for (SysRole perm : perms) + { + if (StringUtils.isNotNull(perm)) + { + permsSet.addAll(Arrays.asList(perm.getRoleKey().trim().split(","))); + } + } + return permsSet; + } + + /** + * 查询所有角色 + * + * @return 角色列表 + */ + @Override + public List selectRoleAll() + { + return SpringUtils.getAopProxy(this).selectRoleList(new SysRole()); + } + + /** + * 根据用户ID获取角色选择框列表 + * + * @param userId 用户ID + * @return 选中角色ID列表 + */ + @Override + public List selectRoleListByUserId(Long userId) + { + return roleMapper.selectRoleListByUserId(userId); + } + + /** + * 通过角色ID查询角色 + * + * @param roleId 角色ID + * @return 角色对象信息 + */ + @Override + public SysRole selectRoleById(Long roleId) + { + return roleMapper.selectRoleById(roleId); + } + + /** + * 校验角色名称是否唯一 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + public boolean checkRoleNameUnique(SysRole role) + { + Long roleId = StringUtils.isNull(role.getRoleId()) ? -1L : role.getRoleId(); + SysRole info = roleMapper.checkRoleNameUnique(role.getRoleName()); + if (StringUtils.isNotNull(info) && info.getRoleId().longValue() != roleId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验角色权限是否唯一 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + public boolean checkRoleKeyUnique(SysRole role) + { + Long roleId = StringUtils.isNull(role.getRoleId()) ? -1L : role.getRoleId(); + SysRole info = roleMapper.checkRoleKeyUnique(role.getRoleKey()); + if (StringUtils.isNotNull(info) && info.getRoleId().longValue() != roleId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验角色是否允许操作 + * + * @param role 角色信息 + */ + @Override + public void checkRoleAllowed(SysRole role) + { + if (StringUtils.isNotNull(role.getRoleId()) && role.isAdmin()) + { + throw new ServiceException("不允许操作超级管理员角色"); + } + } + + /** + * 校验角色是否有数据权限 + * + * @param roleIds 角色id + */ + @Override + public void checkRoleDataScope(Long... roleIds) + { + if (!SecurityUtils.isAdmin()) + { + for (Long roleId : roleIds) + { + SysRole role = new SysRole(); + role.setRoleId(roleId); + List roles = SpringUtils.getAopProxy(this).selectRoleList(role); + if (StringUtils.isEmpty(roles)) + { + throw new ServiceException("没有权限访问角色数据!"); + } + } + } + } + + /** + * 通过角色ID查询角色使用数量 + * + * @param roleId 角色ID + * @return 结果 + */ + @Override + public int countUserRoleByRoleId(Long roleId) + { + return userRoleMapper.countUserRoleByRoleId(roleId); + } + + /** + * 新增保存角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + @Transactional + public int insertRole(SysRole role) + { + // 新增角色信息 + roleMapper.insertRole(role); + return insertRoleMenu(role); + } + + /** + * 修改保存角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + @Transactional + public int updateRole(SysRole role) + { + // 修改角色信息 + roleMapper.updateRole(role); + // 删除角色与菜单关联 + roleMenuMapper.deleteRoleMenuByRoleId(role.getRoleId()); + return insertRoleMenu(role); + } + + /** + * 修改角色状态 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + public int updateRoleStatus(SysRole role) + { + return roleMapper.updateRole(role); + } + + /** + * 修改数据权限信息 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + @Transactional + public int authDataScope(SysRole role) + { + // 修改角色信息 + roleMapper.updateRole(role); + // 删除角色与部门关联 + roleDeptMapper.deleteRoleDeptByRoleId(role.getRoleId()); + // 新增角色和部门信息(数据权限) + return insertRoleDept(role); + } + + /** + * 新增角色菜单信息 + * + * @param role 角色对象 + */ + public int insertRoleMenu(SysRole role) + { + int rows = 1; + // 新增用户与角色管理 + List list = new ArrayList(); + for (Long menuId : role.getMenuIds()) + { + SysRoleMenu rm = new SysRoleMenu(); + rm.setRoleId(role.getRoleId()); + rm.setMenuId(menuId); + list.add(rm); + } + if (list.size() > 0) + { + rows = roleMenuMapper.batchRoleMenu(list); + } + return rows; + } + + /** + * 新增角色部门信息(数据权限) + * + * @param role 角色对象 + */ + public int insertRoleDept(SysRole role) + { + int rows = 1; + // 新增角色与部门(数据权限)管理 + List list = new ArrayList(); + for (Long deptId : role.getDeptIds()) + { + SysRoleDept rd = new SysRoleDept(); + rd.setRoleId(role.getRoleId()); + rd.setDeptId(deptId); + list.add(rd); + } + if (list.size() > 0) + { + rows = roleDeptMapper.batchRoleDept(list); + } + return rows; + } + + /** + * 通过角色ID删除角色 + * + * @param roleId 角色ID + * @return 结果 + */ + @Override + @Transactional + public int deleteRoleById(Long roleId) + { + // 删除角色与菜单关联 + roleMenuMapper.deleteRoleMenuByRoleId(roleId); + // 删除角色与部门关联 + roleDeptMapper.deleteRoleDeptByRoleId(roleId); + return roleMapper.deleteRoleById(roleId); + } + + /** + * 批量删除角色信息 + * + * @param roleIds 需要删除的角色ID + * @return 结果 + */ + @Override + @Transactional + public int deleteRoleByIds(Long[] roleIds) + { + for (Long roleId : roleIds) + { + checkRoleAllowed(new SysRole(roleId)); + checkRoleDataScope(roleId); + SysRole role = selectRoleById(roleId); + if (countUserRoleByRoleId(roleId) > 0) + { + throw new ServiceException(String.format("%1$s已分配,不能删除", role.getRoleName())); + } + } + // 删除角色与菜单关联 + roleMenuMapper.deleteRoleMenu(roleIds); + // 删除角色与部门关联 + roleDeptMapper.deleteRoleDept(roleIds); + return roleMapper.deleteRoleByIds(roleIds); + } + + /** + * 取消授权用户角色 + * + * @param userRole 用户和角色关联信息 + * @return 结果 + */ + @Override + public int deleteAuthUser(SysUserRole userRole) + { + return userRoleMapper.deleteUserRoleInfo(userRole); + } + + /** + * 批量取消授权用户角色 + * + * @param roleId 角色ID + * @param userIds 需要取消授权的用户数据ID + * @return 结果 + */ + @Override + public int deleteAuthUsers(Long roleId, Long[] userIds) + { + return userRoleMapper.deleteUserRoleInfos(roleId, userIds); + } + + /** + * 批量选择授权用户角色 + * + * @param roleId 角色ID + * @param userIds 需要授权的用户数据ID + * @return 结果 + */ + @Override + public int insertAuthUsers(Long roleId, Long[] userIds) + { + // 新增用户与角色管理 + List list = new ArrayList(); + for (Long userId : userIds) + { + SysUserRole ur = new SysUserRole(); + ur.setUserId(userId); + ur.setRoleId(roleId); + list.add(ur); + } + return userRoleMapper.batchUserRole(list); + } +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserOnlineServiceImpl.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserOnlineServiceImpl.java new file mode 100644 index 0000000..f80a877 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserOnlineServiceImpl.java @@ -0,0 +1,96 @@ +package com.ruoyi.system.service.impl; + +import org.springframework.stereotype.Service; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.domain.SysUserOnline; +import com.ruoyi.system.service.ISysUserOnlineService; + +/** + * 在线用户 服务层处理 + * + * @author ruoyi + */ +@Service +public class SysUserOnlineServiceImpl implements ISysUserOnlineService +{ + /** + * 通过登录地址查询信息 + * + * @param ipaddr 登录地址 + * @param user 用户信息 + * @return 在线用户信息 + */ + @Override + public SysUserOnline selectOnlineByIpaddr(String ipaddr, LoginUser user) + { + if (StringUtils.equals(ipaddr, user.getIpaddr())) + { + return loginUserToUserOnline(user); + } + return null; + } + + /** + * 通过用户名称查询信息 + * + * @param userName 用户名称 + * @param user 用户信息 + * @return 在线用户信息 + */ + @Override + public SysUserOnline selectOnlineByUserName(String userName, LoginUser user) + { + if (StringUtils.equals(userName, user.getUsername())) + { + return loginUserToUserOnline(user); + } + return null; + } + + /** + * 通过登录地址/用户名称查询信息 + * + * @param ipaddr 登录地址 + * @param userName 用户名称 + * @param user 用户信息 + * @return 在线用户信息 + */ + @Override + public SysUserOnline selectOnlineByInfo(String ipaddr, String userName, LoginUser user) + { + if (StringUtils.equals(ipaddr, user.getIpaddr()) && StringUtils.equals(userName, user.getUsername())) + { + return loginUserToUserOnline(user); + } + return null; + } + + /** + * 设置在线用户信息 + * + * @param user 用户信息 + * @return 在线用户 + */ + @Override + public SysUserOnline loginUserToUserOnline(LoginUser user) + { + if (StringUtils.isNull(user) || StringUtils.isNull(user.getUser())) + { + return null; + } + SysUserOnline sysUserOnline = new SysUserOnline(); + sysUserOnline.setTokenId(user.getToken()); + sysUserOnline.setUserName(user.getUsername()); + sysUserOnline.setIpaddr(user.getIpaddr()); + sysUserOnline.setLoginLocation(user.getLoginLocation()); + sysUserOnline.setBrowser(user.getBrowser()); + sysUserOnline.setOs(user.getOs()); + sysUserOnline.setLoginTime(user.getLoginTime()); + if (StringUtils.isNotNull(user.getUser().getDept())) + { + sysUserOnline.setDeptName(user.getUser().getDept().getDeptName()); + } + return sysUserOnline; + } +} diff --git a/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java new file mode 100644 index 0000000..d0982ad --- /dev/null +++ b/ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java @@ -0,0 +1,589 @@ +package com.ruoyi.system.service.impl; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; +import jakarta.validation.Validator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; +import com.ruoyi.common.annotation.DataScope; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.bean.BeanValidators; +import com.ruoyi.common.utils.spring.SpringUtils; +import com.ruoyi.system.domain.SysPost; +import com.ruoyi.system.domain.SysUserPost; +import com.ruoyi.system.domain.SysUserRole; +import com.ruoyi.system.mapper.SysPostMapper; +import com.ruoyi.system.mapper.SysRoleMapper; +import com.ruoyi.system.mapper.SysUserMapper; +import com.ruoyi.system.mapper.SysUserPostMapper; +import com.ruoyi.system.mapper.SysUserRoleMapper; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.system.service.ISysDeptService; +import com.ruoyi.system.service.ISysUserService; + +/** + * 用户 业务层处理 + * + * @author ruoyi + */ +@Service +public class SysUserServiceImpl implements ISysUserService +{ + private static final Logger log = LoggerFactory.getLogger(SysUserServiceImpl.class); + + @Autowired + private SysUserMapper userMapper; + + @Autowired + private SysRoleMapper roleMapper; + + @Autowired + private SysPostMapper postMapper; + + @Autowired + private SysUserRoleMapper userRoleMapper; + + @Autowired + private SysUserPostMapper userPostMapper; + + @Autowired + private ISysConfigService configService; + + @Autowired + private ISysDeptService deptService; + + @Autowired + protected Validator validator; + + /** + * 根据条件分页查询用户列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + @Override + @DataScope(deptAlias = "d", userAlias = "u") + public List selectUserList(SysUser user) + { + return userMapper.selectUserList(user); + } + + /** + * 根据条件分页查询已分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + @Override + @DataScope(deptAlias = "d", userAlias = "u") + public List selectAllocatedList(SysUser user) + { + return userMapper.selectAllocatedList(user); + } + + /** + * 根据条件分页查询未分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + @Override + @DataScope(deptAlias = "d", userAlias = "u") + public List selectUnallocatedList(SysUser user) + { + return userMapper.selectUnallocatedList(user); + } + + /** + * 通过用户名查询用户 + * + * @param userName 用户名 + * @return 用户对象信息 + */ + @Override + public SysUser selectUserByUserName(String userName) + { + return userMapper.selectUserByUserName(userName); + } + + /** + * 通过用户ID查询用户 + * + * @param userId 用户ID + * @return 用户对象信息 + */ + @Override + public SysUser selectUserById(Long userId) + { + return userMapper.selectUserById(userId); + } + + /** + * 查询用户所属角色组 + * + * @param userName 用户名 + * @return 结果 + */ + @Override + public String selectUserRoleGroup(String userName) + { + List list = roleMapper.selectRolesByUserName(userName); + if (CollectionUtils.isEmpty(list)) + { + return StringUtils.EMPTY; + } + return list.stream().map(SysRole::getRoleName).collect(Collectors.joining(",")); + } + + /** + * 查询用户所属岗位组 + * + * @param userName 用户名 + * @return 结果 + */ + @Override + public String selectUserPostGroup(String userName) + { + List list = postMapper.selectPostsByUserName(userName); + if (CollectionUtils.isEmpty(list)) + { + return StringUtils.EMPTY; + } + return list.stream().map(SysPost::getPostName).collect(Collectors.joining(",")); + } + + /** + * 校验用户名称是否唯一 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + public boolean checkUserNameUnique(SysUser user) + { + Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId(); + SysUser info = userMapper.checkUserNameUnique(user.getUserName()); + if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验手机号码是否唯一 + * + * @param user 用户信息 + * @return + */ + @Override + public boolean checkPhoneUnique(SysUser user) + { + Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId(); + SysUser info = userMapper.checkPhoneUnique(user.getPhonenumber()); + if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 更新 sys_user.role_type (DB 列, entity 无字段) + */ + @Override + public int updateRoleType(Long userId, String roleType) + { + return userMapper.updateRoleType(userId, roleType); + } + + /** + * 校验手机号是否已注册 + */ + @Override + public boolean isPhoneRegistered(String phone) + { + if (phone == null || phone.isEmpty()) + { + return false; + } + SysUser probe = new SysUser(); + probe.setPhonenumber(phone); + return !checkPhoneUnique(probe); + } + + /** + * 校验email是否唯一 + * + * @param user 用户信息 + * @return + */ + @Override + public boolean checkEmailUnique(SysUser user) + { + Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId(); + SysUser info = userMapper.checkEmailUnique(user.getEmail()); + if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + +/** + * 校验用户是否允许操作 + * + * @param user 用户信息 + */ + @Override + public void checkUserAllowed(SysUser user) + { + if (StringUtils.isNotNull(user.getUserId()) && user.isAdmin()) + { + throw new ServiceException("不允许操作超级管理员用户"); + } + } + + /** + * 校验用户是否有数据权限 + * + * @param userId 用户id + */ + @Override + public void checkUserDataScope(Long userId) + { + if (!SecurityUtils.isAdmin()) + { + SysUser user = new SysUser(); + user.setUserId(userId); + List users = SpringUtils.getAopProxy(this).selectUserList(user); + if (StringUtils.isEmpty(users)) + { + throw new ServiceException("没有权限访问用户数据!"); + } + } + } + + /** + * 新增保存用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + @Transactional + public int insertUser(SysUser user) + { + // 新增用户信息 + int rows = userMapper.insertUser(user); + // 新增用户岗位关联 + insertUserPost(user); + // 新增用户与角色管理 + insertUserRole(user); + return rows; + } + + /** + * 注册用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + public boolean registerUser(SysUser user) + { + return userMapper.insertUser(user) > 0; + } + + /** + * 修改保存用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + @Transactional + public int updateUser(SysUser user) + { + Long userId = user.getUserId(); + // 删除用户与角色关联 + userRoleMapper.deleteUserRoleByUserId(userId); + // 新增用户与角色管理 + insertUserRole(user); + // 删除用户与岗位关联 + userPostMapper.deleteUserPostByUserId(userId); + // 新增用户与岗位管理 + insertUserPost(user); + return userMapper.updateUser(user); + } + + /** + * 用户授权角色 + * + * @param userId 用户ID + * @param roleIds 角色组 + */ + @Override + @Transactional + public void insertUserAuth(Long userId, Long[] roleIds) + { + userRoleMapper.deleteUserRoleByUserId(userId); + insertUserRole(userId, roleIds); + } + + /** + * 修改用户状态 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + public int updateUserStatus(SysUser user) + { + return userMapper.updateUserStatus(user.getUserId(), user.getStatus()); + } + + /** + * 修改用户基本信息 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + public int updateUserProfile(SysUser user) + { + return userMapper.updateUser(user); + } + + /** + * 修改用户头像 + * + * @param userId 用户ID + * @param avatar 头像地址 + * @return 结果 + */ + @Override + public boolean updateUserAvatar(Long userId, String avatar) + { + return userMapper.updateUserAvatar(userId, avatar) > 0; + } + + /** + * 更新用户登录信息(IP和登录时间) + * + * @param userId 用户ID + * @param loginIp 登录IP地址 + * @param loginDate 登录时间 + * @return 结果 + */ + public void updateLoginInfo(Long userId, String loginIp, Date loginDate) + { + userMapper.updateLoginInfo(userId, loginIp, loginDate); + } + + /** + * 重置用户密码 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + public int resetPwd(SysUser user) + { + return userMapper.resetUserPwd(user.getUserId(), user.getPassword()); + } + + /** + * 重置用户密码 + * + * @param userId 用户ID + * @param password 密码 + * @return 结果 + */ + @Override + public int resetUserPwd(Long userId, String password) + { + return userMapper.resetUserPwd(userId, password); + } + + /** + * 新增用户角色信息 + * + * @param user 用户对象 + */ + public void insertUserRole(SysUser user) + { + this.insertUserRole(user.getUserId(), user.getRoleIds()); + } + + /** + * 新增用户岗位信息 + * + * @param user 用户对象 + */ + public void insertUserPost(SysUser user) + { + Long[] posts = user.getPostIds(); + if (StringUtils.isNotEmpty(posts)) + { + // 新增用户与岗位管理 + List list = new ArrayList(posts.length); + for (Long postId : posts) + { + SysUserPost up = new SysUserPost(); + up.setUserId(user.getUserId()); + up.setPostId(postId); + list.add(up); + } + userPostMapper.batchUserPost(list); + } + } + + /** + * 新增用户角色信息 + * + * @param userId 用户ID + * @param roleIds 角色组 + */ + public void insertUserRole(Long userId, Long[] roleIds) + { + if (StringUtils.isNotEmpty(roleIds)) + { + // 新增用户与角色管理 + List list = new ArrayList(roleIds.length); + for (Long roleId : roleIds) + { + SysUserRole ur = new SysUserRole(); + ur.setUserId(userId); + ur.setRoleId(roleId); + list.add(ur); + } + userRoleMapper.batchUserRole(list); + } + } + + /** + * 通过用户ID删除用户 + * + * @param userId 用户ID + * @return 结果 + */ + @Override + @Transactional + public int deleteUserById(Long userId) + { + // 删除用户与角色关联 + userRoleMapper.deleteUserRoleByUserId(userId); + // 删除用户与岗位表 + userPostMapper.deleteUserPostByUserId(userId); + return userMapper.deleteUserById(userId); + } + + /** + * 批量删除用户信息 + * + * @param userIds 需要删除的用户ID + * @return 结果 + */ + @Override + @Transactional + public int deleteUserByIds(Long[] userIds) + { + for (Long userId : userIds) + { + checkUserAllowed(new SysUser(userId)); + checkUserDataScope(userId); + } + // 删除用户与角色关联 + userRoleMapper.deleteUserRole(userIds); + // 删除用户与岗位关联 + userPostMapper.deleteUserPost(userIds); + return userMapper.deleteUserByIds(userIds); + } + + /** + * 导入用户数据 + * + * @param userList 用户数据列表 + * @param isUpdateSupport 是否更新支持,如果已存在,则进行更新数据 + * @param operName 操作用户 + * @return 结果 + */ + @Override + public String importUser(List userList, Boolean isUpdateSupport, String operName) + { + if (StringUtils.isNull(userList) || userList.size() == 0) + { + throw new ServiceException("导入用户数据不能为空!"); + } + int successNum = 0; + int failureNum = 0; + StringBuilder successMsg = new StringBuilder(); + StringBuilder failureMsg = new StringBuilder(); + for (SysUser user : userList) + { + try + { + // 验证是否存在这个用户 + SysUser u = userMapper.selectUserByUserName(user.getUserName()); + if (StringUtils.isNull(u)) + { + BeanValidators.validateWithException(validator, user); + deptService.checkDeptDataScope(user.getDeptId()); + String password = configService.selectConfigByKey("sys.user.initPassword"); + user.setPassword(SecurityUtils.encryptPassword(password)); + user.setCreateBy(operName); + userMapper.insertUser(user); + successNum++; + successMsg.append("
" + successNum + "、账号 " + user.getUserName() + " 导入成功"); + } + else if (isUpdateSupport) + { + BeanValidators.validateWithException(validator, user); + checkUserAllowed(u); + checkUserDataScope(u.getUserId()); + deptService.checkDeptDataScope(user.getDeptId()); + user.setUserId(u.getUserId()); + user.setDeptId(u.getDeptId()); + user.setUpdateBy(operName); + userMapper.updateUser(user); + successNum++; + successMsg.append("
" + successNum + "、账号 " + user.getUserName() + " 更新成功"); + } + else + { + failureNum++; + failureMsg.append("
" + failureNum + "、账号 " + user.getUserName() + " 已存在"); + } + } + catch (Exception e) + { + failureNum++; + String msg = "
" + failureNum + "、账号 " + user.getUserName() + " 导入失败:"; + failureMsg.append(msg + e.getMessage()); + log.error(msg, e); + } + } + if (failureNum > 0) + { + failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:"); + throw new ServiceException(failureMsg.toString()); + } + else + { + successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:"); + } + return successMsg.toString(); + } +} diff --git a/ry-api/ruoyi-system/src/main/resources/mapper/system/SysConfigMapper.xml b/ry-api/ruoyi-system/src/main/resources/mapper/system/SysConfigMapper.xml new file mode 100644 index 0000000..a5ff114 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/resources/mapper/system/SysConfigMapper.xml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + select config_id, config_name, config_key, config_value, config_type, create_by, create_time, update_by, update_time, remark + from sys_config + + + + + + + and config_id = #{configId} + + + and config_key = #{configKey} + + + + + + + + + + + + + + insert into sys_config ( + config_name, + config_key, + config_value, + config_type, + create_by, + remark, + create_time + )values( + #{configName}, + #{configKey}, + #{configValue}, + #{configType}, + #{createBy}, + #{remark}, + sysdate() + ) + + + + update sys_config + + config_name = #{configName}, + config_key = #{configKey}, + config_value = #{configValue}, + config_type = #{configType}, + update_by = #{updateBy}, + remark = #{remark}, + update_time = sysdate() + + where config_id = #{configId} + + + + delete from sys_config where config_id = #{configId} + + + + delete from sys_config where config_id in + + #{configId} + + + + \ No newline at end of file diff --git a/ry-api/ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml b/ry-api/ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml new file mode 100644 index 0000000..aa6a10f --- /dev/null +++ b/ry-api/ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + + + + + + + select d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.phone, d.email, d.status, d.del_flag, d.create_by, d.create_time + from sys_dept d + + + + + + + + + + + + + + + + + + + + insert into sys_dept( + dept_id, + parent_id, + dept_name, + ancestors, + order_num, + leader, + phone, + email, + status, + create_by, + create_time + )values( + #{deptId}, + #{parentId}, + #{deptName}, + #{ancestors}, + #{orderNum}, + #{leader}, + #{phone}, + #{email}, + #{status}, + #{createBy}, + sysdate() + ) + + + + update sys_dept + + parent_id = #{parentId}, + dept_name = #{deptName}, + ancestors = #{ancestors}, + order_num = #{orderNum}, + leader = #{leader}, + phone = #{phone}, + email = #{email}, + status = #{status}, + update_by = #{updateBy}, + update_time = sysdate() + + where dept_id = #{deptId} + + + + update sys_dept set ancestors = + + when #{item.deptId} then #{item.ancestors} + + where dept_id in + + #{item.deptId} + + + + + update sys_dept set status = '0' where dept_id in + + #{deptId} + + + + + update sys_dept set order_num = #{orderNum} where dept_id = #{deptId} + + + + update sys_dept set del_flag = '2' where dept_id = #{deptId} + + + \ No newline at end of file diff --git a/ry-api/ruoyi-system/src/main/resources/mapper/system/SysDictDataMapper.xml b/ry-api/ruoyi-system/src/main/resources/mapper/system/SysDictDataMapper.xml new file mode 100644 index 0000000..3b94b7f --- /dev/null +++ b/ry-api/ruoyi-system/src/main/resources/mapper/system/SysDictDataMapper.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + select dict_code, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark + from sys_dict_data + + + + + + + + + + + + + + delete from sys_dict_data where dict_code = #{dictCode} + + + + delete from sys_dict_data where dict_code in + + #{dictCode} + + + + + update sys_dict_data + + dict_sort = #{dictSort}, + dict_label = #{dictLabel}, + dict_value = #{dictValue}, + dict_type = #{dictType}, + css_class = #{cssClass}, + list_class = #{listClass}, + is_default = #{isDefault}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where dict_code = #{dictCode} + + + + update sys_dict_data set dict_type = #{newDictType} where dict_type = #{oldDictType} + + + + insert into sys_dict_data( + dict_sort, + dict_label, + dict_value, + dict_type, + css_class, + list_class, + is_default, + status, + remark, + create_by, + create_time + )values( + #{dictSort}, + #{dictLabel}, + #{dictValue}, + #{dictType}, + #{cssClass}, + #{listClass}, + #{isDefault}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + \ No newline at end of file diff --git a/ry-api/ruoyi-system/src/main/resources/mapper/system/SysDictTypeMapper.xml b/ry-api/ruoyi-system/src/main/resources/mapper/system/SysDictTypeMapper.xml new file mode 100644 index 0000000..438d484 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/resources/mapper/system/SysDictTypeMapper.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + select dict_id, dict_name, dict_type, status, create_by, create_time, remark + from sys_dict_type + + + + + + + + + + + + + + delete from sys_dict_type where dict_id = #{dictId} + + + + delete from sys_dict_type where dict_id in + + #{dictId} + + + + + update sys_dict_type + + dict_name = #{dictName}, + dict_type = #{dictType}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where dict_id = #{dictId} + + + + insert into sys_dict_type( + dict_name, + dict_type, + status, + remark, + create_by, + create_time + )values( + #{dictName}, + #{dictType}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + \ No newline at end of file diff --git a/ry-api/ruoyi-system/src/main/resources/mapper/system/SysLogininforMapper.xml b/ry-api/ruoyi-system/src/main/resources/mapper/system/SysLogininforMapper.xml new file mode 100644 index 0000000..822d665 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/resources/mapper/system/SysLogininforMapper.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + insert into sys_logininfor (user_name, status, ipaddr, login_location, browser, os, msg, login_time) + values (#{userName}, #{status}, #{ipaddr}, #{loginLocation}, #{browser}, #{os}, #{msg}, sysdate()) + + + + + + delete from sys_logininfor where info_id in + + #{infoId} + + + + + truncate table sys_logininfor + + + \ No newline at end of file diff --git a/ry-api/ruoyi-system/src/main/resources/mapper/system/SysMenuMapper.xml b/ry-api/ruoyi-system/src/main/resources/mapper/system/SysMenuMapper.xml new file mode 100644 index 0000000..0341292 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/resources/mapper/system/SysMenuMapper.xml @@ -0,0 +1,215 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select menu_id, menu_name, parent_id, order_num, path, component, `query`, route_name, is_frame, is_cache, menu_type, visible, status, ifnull(perms,'') as perms, icon, create_time + from sys_menu + + + + + + + + + + + + + + + + + + + + + + + + + + + + update sys_menu + + menu_name = #{menuName}, + parent_id = #{parentId}, + order_num = #{orderNum}, + path = #{path}, + component = #{component}, + `query` = #{query}, + route_name = #{routeName}, + is_frame = #{isFrame}, + is_cache = #{isCache}, + menu_type = #{menuType}, + visible = #{visible}, + status = #{status}, + perms = #{perms}, + icon = #{icon}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where menu_id = #{menuId} + + + + insert into sys_menu( + menu_id, + parent_id, + menu_name, + order_num, + path, + component, + `query`, + route_name, + is_frame, + is_cache, + menu_type, + visible, + status, + perms, + icon, + remark, + create_by, + create_time + )values( + #{menuId}, + #{parentId}, + #{menuName}, + #{orderNum}, + #{path}, + #{component}, + #{query}, + #{routeName}, + #{isFrame}, + #{isCache}, + #{menuType}, + #{visible}, + #{status}, + #{perms}, + #{icon}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + update sys_menu set order_num = #{orderNum} where menu_id = #{menuId} + + + + delete from sys_menu where menu_id = #{menuId} + + + \ No newline at end of file diff --git a/ry-api/ruoyi-system/src/main/resources/mapper/system/SysNoticeMapper.xml b/ry-api/ruoyi-system/src/main/resources/mapper/system/SysNoticeMapper.xml new file mode 100644 index 0000000..1fccd78 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/resources/mapper/system/SysNoticeMapper.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + select notice_id, notice_title, notice_type, cast(notice_content as char) as notice_content, status, create_by, create_time, update_by, update_time, remark + from sys_notice + + + + + + + + insert into sys_notice ( + notice_title, + notice_type, + notice_content, + status, + remark, + create_by, + create_time + )values( + #{noticeTitle}, + #{noticeType}, + #{noticeContent}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + update sys_notice + + notice_title = #{noticeTitle}, + notice_type = #{noticeType}, + notice_content = #{noticeContent}, + status = #{status}, + update_by = #{updateBy}, + update_time = sysdate() + + where notice_id = #{noticeId} + + + + delete from sys_notice where notice_id = #{noticeId} + + + + delete from sys_notice where notice_id in + + #{noticeId} + + + + \ No newline at end of file diff --git a/ry-api/ruoyi-system/src/main/resources/mapper/system/SysNoticeReadMapper.xml b/ry-api/ruoyi-system/src/main/resources/mapper/system/SysNoticeReadMapper.xml new file mode 100644 index 0000000..86e5136 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/resources/mapper/system/SysNoticeReadMapper.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + insert ignore into sys_notice_read (notice_id, user_id, read_time) + values (#{noticeId}, #{userId}, sysdate()) + + + + + + + + + + + + + + insert ignore into sys_notice_read (notice_id, user_id, read_time) + values + + (#{noticeId}, #{userId}, sysdate()) + + + + + + delete from sys_notice_read where notice_id in + + #{noticeId} + + + + + + + diff --git a/ry-api/ruoyi-system/src/main/resources/mapper/system/SysOperLogMapper.xml b/ry-api/ruoyi-system/src/main/resources/mapper/system/SysOperLogMapper.xml new file mode 100644 index 0000000..201db07 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/resources/mapper/system/SysOperLogMapper.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + select oper_id, title, business_type, method, request_method, operator_type, oper_name, dept_name, oper_url, oper_ip, oper_location, oper_param, json_result, status, error_msg, oper_time, cost_time + from sys_oper_log + + + + insert into sys_oper_log(title, business_type, method, request_method, operator_type, oper_name, dept_name, oper_url, oper_ip, oper_location, oper_param, json_result, status, error_msg, cost_time, oper_time) + values (#{title}, #{businessType}, #{method}, #{requestMethod}, #{operatorType}, #{operName}, #{deptName}, #{operUrl}, #{operIp}, #{operLocation}, #{operParam}, #{jsonResult}, #{status}, #{errorMsg}, #{costTime}, sysdate()) + + + + + + delete from sys_oper_log where oper_id in + + #{operId} + + + + + + + truncate table sys_oper_log + + + \ No newline at end of file diff --git a/ry-api/ruoyi-system/src/main/resources/mapper/system/SysPostMapper.xml b/ry-api/ruoyi-system/src/main/resources/mapper/system/SysPostMapper.xml new file mode 100644 index 0000000..227c459 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/resources/mapper/system/SysPostMapper.xml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + select post_id, post_code, post_name, post_sort, status, create_by, create_time, remark + from sys_post + + + + + + + + + + + + + + + + + + update sys_post + + post_code = #{postCode}, + post_name = #{postName}, + post_sort = #{postSort}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where post_id = #{postId} + + + + insert into sys_post( + post_id, + post_code, + post_name, + post_sort, + status, + remark, + create_by, + create_time + )values( + #{postId}, + #{postCode}, + #{postName}, + #{postSort}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + delete from sys_post where post_id = #{postId} + + + + delete from sys_post where post_id in + + #{postId} + + + + \ No newline at end of file diff --git a/ry-api/ruoyi-system/src/main/resources/mapper/system/SysRoleDeptMapper.xml b/ry-api/ruoyi-system/src/main/resources/mapper/system/SysRoleDeptMapper.xml new file mode 100644 index 0000000..7c4139b --- /dev/null +++ b/ry-api/ruoyi-system/src/main/resources/mapper/system/SysRoleDeptMapper.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + delete from sys_role_dept where role_id=#{roleId} + + + + + + delete from sys_role_dept where role_id in + + #{roleId} + + + + + insert into sys_role_dept(role_id, dept_id) values + + (#{item.roleId},#{item.deptId}) + + + + \ No newline at end of file diff --git a/ry-api/ruoyi-system/src/main/resources/mapper/system/SysRoleMapper.xml b/ry-api/ruoyi-system/src/main/resources/mapper/system/SysRoleMapper.xml new file mode 100644 index 0000000..955d4ee --- /dev/null +++ b/ry-api/ruoyi-system/src/main/resources/mapper/system/SysRoleMapper.xml @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + select distinct r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.menu_check_strictly, r.dept_check_strictly, + r.status, r.del_flag, r.create_time, r.remark + from sys_role r + left join sys_user_role ur on ur.role_id = r.role_id + left join sys_user u on u.user_id = ur.user_id + left join sys_dept d on u.dept_id = d.dept_id + + + + + + + + + + + + + + + + + + + + insert into sys_role( + role_id, + role_name, + role_key, + role_sort, + data_scope, + menu_check_strictly, + dept_check_strictly, + status, + remark, + create_by, + create_time + )values( + #{roleId}, + #{roleName}, + #{roleKey}, + #{roleSort}, + #{dataScope}, + #{menuCheckStrictly}, + #{deptCheckStrictly}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + update sys_role + + role_name = #{roleName}, + role_key = #{roleKey}, + role_sort = #{roleSort}, + data_scope = #{dataScope}, + menu_check_strictly = #{menuCheckStrictly}, + dept_check_strictly = #{deptCheckStrictly}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where role_id = #{roleId} + + + + update sys_role set del_flag = '2' where role_id = #{roleId} + + + + update sys_role set del_flag = '2' where role_id in + + #{roleId} + + + + \ No newline at end of file diff --git a/ry-api/ruoyi-system/src/main/resources/mapper/system/SysRoleMenuMapper.xml b/ry-api/ruoyi-system/src/main/resources/mapper/system/SysRoleMenuMapper.xml new file mode 100644 index 0000000..cb60a85 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/resources/mapper/system/SysRoleMenuMapper.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + delete from sys_role_menu where role_id=#{roleId} + + + + delete from sys_role_menu where role_id in + + #{roleId} + + + + + insert into sys_role_menu(role_id, menu_id) values + + (#{item.roleId},#{item.menuId}) + + + + \ No newline at end of file diff --git a/ry-api/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml b/ry-api/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml new file mode 100644 index 0000000..b7ab75f --- /dev/null +++ b/ry-api/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml @@ -0,0 +1,237 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select u.user_id, u.dept_id, u.user_name, u.nick_name, u.account_type, u.parent_user_id, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.pwd_update_date, u.create_by, u.create_time, u.update_by, u.update_time, u.remark, + d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.status as dept_status, + r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status + from sys_user u + left join sys_dept d on u.dept_id = d.dept_id + left join sys_user_role ur on u.user_id = ur.user_id + left join sys_role r on r.role_id = ur.role_id + + + + + + + + + + + + + + + + + + + + insert into sys_user( + user_id, + dept_id, + user_name, + nick_name, + account_type, + parent_user_id, + email, + avatar, + phonenumber, + sex, + password, + status, + pwd_update_date, + create_by, + remark, + create_time + )values( + #{userId}, + #{deptId}, + #{userName}, + #{nickName}, + #{accountType}, + #{parentUserId}, + #{email}, + #{avatar}, + #{phonenumber}, + #{sex}, + #{password}, + #{status}, + #{pwdUpdateDate}, + #{createBy}, + #{remark}, + sysdate() + ) + + + + update sys_user + + dept_id = #{deptId}, + nick_name = #{nickName}, + email = #{email}, + phonenumber = #{phonenumber}, + sex = #{sex}, + avatar = #{avatar}, + password = #{password}, + status = #{status}, + login_ip = #{loginIp}, + login_date = #{loginDate}, + update_by = #{updateBy}, + remark = #{remark}, + update_time = sysdate() + + where user_id = #{userId} + + + + update sys_user set role_type = #{roleType} where user_id = #{userId} + + + + update sys_user set status = #{status}, update_time = sysdate() where user_id = #{userId} + + + + update sys_user set avatar = #{avatar}, update_time = sysdate() where user_id = #{userId} + + + + update sys_user set login_ip = #{loginIp}, login_date = #{loginDate} where user_id = #{userId} + + + + update sys_user set pwd_update_date = sysdate(), password = #{password}, update_time = sysdate() where user_id = #{userId} + + + + update sys_user set del_flag = '2' where user_id = #{userId} + + + + update sys_user set del_flag = '2' where user_id in + + #{userId} + + + + \ No newline at end of file diff --git a/ry-api/ruoyi-system/src/main/resources/mapper/system/SysUserPostMapper.xml b/ry-api/ruoyi-system/src/main/resources/mapper/system/SysUserPostMapper.xml new file mode 100644 index 0000000..2b90bc4 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/resources/mapper/system/SysUserPostMapper.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + delete from sys_user_post where user_id=#{userId} + + + + + + delete from sys_user_post where user_id in + + #{userId} + + + + + insert into sys_user_post(user_id, post_id) values + + (#{item.userId},#{item.postId}) + + + + \ No newline at end of file diff --git a/ry-api/ruoyi-system/src/main/resources/mapper/system/SysUserRoleMapper.xml b/ry-api/ruoyi-system/src/main/resources/mapper/system/SysUserRoleMapper.xml new file mode 100644 index 0000000..dd72689 --- /dev/null +++ b/ry-api/ruoyi-system/src/main/resources/mapper/system/SysUserRoleMapper.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + delete from sys_user_role where user_id=#{userId} + + + + + + delete from sys_user_role where user_id in + + #{userId} + + + + + insert into sys_user_role(user_id, role_id) values + + (#{item.userId},#{item.roleId}) + + + + + delete from sys_user_role where user_id=#{userId} and role_id=#{roleId} + + + + delete from sys_user_role where role_id=#{roleId} and user_id in + + #{userId} + + + \ No newline at end of file diff --git a/ry-api/ry.bat b/ry-api/ry.bat new file mode 100644 index 0000000..ac1e437 --- /dev/null +++ b/ry-api/ry.bat @@ -0,0 +1,67 @@ +@echo off + +rem jarƽĿ¼ +set AppName=ruoyi-admin.jar + +rem JVM +set JVM_OPTS="-Dname=%AppName% -Duser.timezone=Asia/Shanghai -Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:NewRatio=1 -XX:SurvivorRatio=30 -XX:+UseParallelGC -XX:+UseParallelOldGC" + + +ECHO. + ECHO. [1] %AppName% + ECHO. [2] ر%AppName% + ECHO. [3] %AppName% + ECHO. [4] ״̬ %AppName% + ECHO. [5] +ECHO. + +ECHO.ѡĿ: +set /p ID= + IF "%id%"=="1" GOTO start + IF "%id%"=="2" GOTO stop + IF "%id%"=="3" GOTO restart + IF "%id%"=="4" GOTO status + IF "%id%"=="5" EXIT +PAUSE +:start + for /f "usebackq tokens=1-2" %%a in (`jps -l ^| findstr %AppName%`) do ( + set pid=%%a + set image_name=%%b + ) + if defined pid ( + echo %%is running + PAUSE + ) + +start javaw %JVM_OPTS% -jar %AppName% + +echo starting +echo Start %AppName% success... +goto:eof + +rem stopͨjpspid +:stop + for /f "usebackq tokens=1-2" %%a in (`jps -l ^| findstr %AppName%`) do ( + set pid=%%a + set image_name=%%b + ) + if not defined pid (echo process %AppName% does not exists) else ( + echo prepare to kill %image_name% + echo start kill %pid% ... + rem ݽIDkill + taskkill /f /pid %pid% + ) +goto:eof +:restart + call :stop + call :start +goto:eof +:status + for /f "usebackq tokens=1-2" %%a in (`jps -l ^| findstr %AppName%`) do ( + set pid=%%a + set image_name=%%b + ) + if not defined pid (echo process %AppName% is dead ) else ( + echo %image_name% is running + ) +goto:eof diff --git a/ry-api/ry.sh b/ry-api/ry.sh new file mode 100644 index 0000000..d6a9cf3 --- /dev/null +++ b/ry-api/ry.sh @@ -0,0 +1,86 @@ +#!/bin/sh +# ./ry.sh start 启动 stop 停止 restart 重启 status 状态 +AppName=ruoyi-admin.jar + +# JVM参数 +JVM_OPTS="-Dname=$AppName -Duser.timezone=Asia/Shanghai -Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:NewRatio=1 -XX:SurvivorRatio=30 -XX:+UseParallelGC -XX:+UseParallelOldGC" +APP_HOME=`pwd` +LOG_PATH=$APP_HOME/logs/$AppName.log + +if [ "$1" = "" ]; +then + echo -e "\033[0;31m 未输入操作名 \033[0m \033[0;34m {start|stop|restart|status} \033[0m" + exit 1 +fi + +if [ "$AppName" = "" ]; +then + echo -e "\033[0;31m 未输入应用名 \033[0m" + exit 1 +fi + +function start() +{ + PID=`ps -ef |grep java|grep $AppName|grep -v grep|awk '{print $2}'` + + if [ x"$PID" != x"" ]; then + echo "$AppName is running..." + else + nohup java $JVM_OPTS -jar $AppName > /dev/null 2>&1 & + echo "Start $AppName success..." + fi +} + +function stop() +{ + echo "Stop $AppName" + + PID="" + query(){ + PID=`ps -ef |grep java|grep $AppName|grep -v grep|awk '{print $2}'` + } + + query + if [ x"$PID" != x"" ]; then + kill -TERM $PID + echo "$AppName (pid:$PID) exiting..." + while [ x"$PID" != x"" ] + do + sleep 1 + query + done + echo "$AppName exited." + else + echo "$AppName already stopped." + fi +} + +function restart() +{ + stop + sleep 2 + start +} + +function status() +{ + PID=`ps -ef |grep java|grep $AppName|grep -v grep|wc -l` + if [ $PID != 0 ];then + echo "$AppName is running..." + else + echo "$AppName is not running..." + fi +} + +case $1 in + start) + start;; + stop) + stop;; + restart) + restart;; + status) + status;; + *) + +esac diff --git a/ry-api/sql/quartz.sql b/ry-api/sql/quartz.sql new file mode 100644 index 0000000..cee613b --- /dev/null +++ b/ry-api/sql/quartz.sql @@ -0,0 +1,174 @@ +DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS; +DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS; +DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE; +DROP TABLE IF EXISTS QRTZ_LOCKS; +DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS; +DROP TABLE IF EXISTS QRTZ_SIMPROP_TRIGGERS; +DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS; +DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS; +DROP TABLE IF EXISTS QRTZ_TRIGGERS; +DROP TABLE IF EXISTS QRTZ_JOB_DETAILS; +DROP TABLE IF EXISTS QRTZ_CALENDARS; + +-- ---------------------------- +-- 1、存储每一个已配置的 jobDetail 的详细信息 +-- ---------------------------- +create table QRTZ_JOB_DETAILS ( + sched_name varchar(120) not null comment '调度名称', + job_name varchar(200) not null comment '任务名称', + job_group varchar(200) not null comment '任务组名', + description varchar(250) null comment '相关介绍', + job_class_name varchar(250) not null comment '执行任务类名称', + is_durable varchar(1) not null comment '是否持久化', + is_nonconcurrent varchar(1) not null comment '是否并发', + is_update_data varchar(1) not null comment '是否更新数据', + requests_recovery varchar(1) not null comment '是否接受恢复执行', + job_data blob null comment '存放持久化job对象', + primary key (sched_name, job_name, job_group) +) engine=innodb comment = '任务详细信息表'; + +-- ---------------------------- +-- 2、 存储已配置的 Trigger 的信息 +-- ---------------------------- +create table QRTZ_TRIGGERS ( + sched_name varchar(120) not null comment '调度名称', + trigger_name varchar(200) not null comment '触发器的名字', + trigger_group varchar(200) not null comment '触发器所属组的名字', + job_name varchar(200) not null comment 'qrtz_job_details表job_name的外键', + job_group varchar(200) not null comment 'qrtz_job_details表job_group的外键', + description varchar(250) null comment '相关介绍', + next_fire_time bigint(13) null comment '上一次触发时间(毫秒)', + prev_fire_time bigint(13) null comment '下一次触发时间(默认为-1表示不触发)', + priority integer null comment '优先级', + trigger_state varchar(16) not null comment '触发器状态', + trigger_type varchar(8) not null comment '触发器的类型', + start_time bigint(13) not null comment '开始时间', + end_time bigint(13) null comment '结束时间', + calendar_name varchar(200) null comment '日程表名称', + misfire_instr smallint(2) null comment '补偿执行的策略', + job_data blob null comment '存放持久化job对象', + primary key (sched_name, trigger_name, trigger_group), + foreign key (sched_name, job_name, job_group) references QRTZ_JOB_DETAILS(sched_name, job_name, job_group) +) engine=innodb comment = '触发器详细信息表'; + +-- ---------------------------- +-- 3、 存储简单的 Trigger,包括重复次数,间隔,以及已触发的次数 +-- ---------------------------- +create table QRTZ_SIMPLE_TRIGGERS ( + sched_name varchar(120) not null comment '调度名称', + trigger_name varchar(200) not null comment 'qrtz_triggers表trigger_name的外键', + trigger_group varchar(200) not null comment 'qrtz_triggers表trigger_group的外键', + repeat_count bigint(7) not null comment '重复的次数统计', + repeat_interval bigint(12) not null comment '重复的间隔时间', + times_triggered bigint(10) not null comment '已经触发的次数', + primary key (sched_name, trigger_name, trigger_group), + foreign key (sched_name, trigger_name, trigger_group) references QRTZ_TRIGGERS(sched_name, trigger_name, trigger_group) +) engine=innodb comment = '简单触发器的信息表'; + +-- ---------------------------- +-- 4、 存储 Cron Trigger,包括 Cron 表达式和时区信息 +-- ---------------------------- +create table QRTZ_CRON_TRIGGERS ( + sched_name varchar(120) not null comment '调度名称', + trigger_name varchar(200) not null comment 'qrtz_triggers表trigger_name的外键', + trigger_group varchar(200) not null comment 'qrtz_triggers表trigger_group的外键', + cron_expression varchar(200) not null comment 'cron表达式', + time_zone_id varchar(80) comment '时区', + primary key (sched_name, trigger_name, trigger_group), + foreign key (sched_name, trigger_name, trigger_group) references QRTZ_TRIGGERS(sched_name, trigger_name, trigger_group) +) engine=innodb comment = 'Cron类型的触发器表'; + +-- ---------------------------- +-- 5、 Trigger 作为 Blob 类型存储(用于 Quartz 用户用 JDBC 创建他们自己定制的 Trigger 类型,JobStore 并不知道如何存储实例的时候) +-- ---------------------------- +create table QRTZ_BLOB_TRIGGERS ( + sched_name varchar(120) not null comment '调度名称', + trigger_name varchar(200) not null comment 'qrtz_triggers表trigger_name的外键', + trigger_group varchar(200) not null comment 'qrtz_triggers表trigger_group的外键', + blob_data blob null comment '存放持久化Trigger对象', + primary key (sched_name, trigger_name, trigger_group), + foreign key (sched_name, trigger_name, trigger_group) references QRTZ_TRIGGERS(sched_name, trigger_name, trigger_group) +) engine=innodb comment = 'Blob类型的触发器表'; + +-- ---------------------------- +-- 6、 以 Blob 类型存储存放日历信息, quartz可配置一个日历来指定一个时间范围 +-- ---------------------------- +create table QRTZ_CALENDARS ( + sched_name varchar(120) not null comment '调度名称', + calendar_name varchar(200) not null comment '日历名称', + calendar blob not null comment '存放持久化calendar对象', + primary key (sched_name, calendar_name) +) engine=innodb comment = '日历信息表'; + +-- ---------------------------- +-- 7、 存储已暂停的 Trigger 组的信息 +-- ---------------------------- +create table QRTZ_PAUSED_TRIGGER_GRPS ( + sched_name varchar(120) not null comment '调度名称', + trigger_group varchar(200) not null comment 'qrtz_triggers表trigger_group的外键', + primary key (sched_name, trigger_group) +) engine=innodb comment = '暂停的触发器表'; + +-- ---------------------------- +-- 8、 存储与已触发的 Trigger 相关的状态信息,以及相联 Job 的执行信息 +-- ---------------------------- +create table QRTZ_FIRED_TRIGGERS ( + sched_name varchar(120) not null comment '调度名称', + entry_id varchar(95) not null comment '调度器实例id', + trigger_name varchar(200) not null comment 'qrtz_triggers表trigger_name的外键', + trigger_group varchar(200) not null comment 'qrtz_triggers表trigger_group的外键', + instance_name varchar(200) not null comment '调度器实例名', + fired_time bigint(13) not null comment '触发的时间', + sched_time bigint(13) not null comment '定时器制定的时间', + priority integer not null comment '优先级', + state varchar(16) not null comment '状态', + job_name varchar(200) null comment '任务名称', + job_group varchar(200) null comment '任务组名', + is_nonconcurrent varchar(1) null comment '是否并发', + requests_recovery varchar(1) null comment '是否接受恢复执行', + primary key (sched_name, entry_id) +) engine=innodb comment = '已触发的触发器表'; + +-- ---------------------------- +-- 9、 存储少量的有关 Scheduler 的状态信息,假如是用于集群中,可以看到其他的 Scheduler 实例 +-- ---------------------------- +create table QRTZ_SCHEDULER_STATE ( + sched_name varchar(120) not null comment '调度名称', + instance_name varchar(200) not null comment '实例名称', + last_checkin_time bigint(13) not null comment '上次检查时间', + checkin_interval bigint(13) not null comment '检查间隔时间', + primary key (sched_name, instance_name) +) engine=innodb comment = '调度器状态表'; + +-- ---------------------------- +-- 10、 存储程序的悲观锁的信息(假如使用了悲观锁) +-- ---------------------------- +create table QRTZ_LOCKS ( + sched_name varchar(120) not null comment '调度名称', + lock_name varchar(40) not null comment '悲观锁名称', + primary key (sched_name, lock_name) +) engine=innodb comment = '存储的悲观锁信息表'; + +-- ---------------------------- +-- 11、 Quartz集群实现同步机制的行锁表 +-- ---------------------------- +create table QRTZ_SIMPROP_TRIGGERS ( + sched_name varchar(120) not null comment '调度名称', + trigger_name varchar(200) not null comment 'qrtz_triggers表trigger_name的外键', + trigger_group varchar(200) not null comment 'qrtz_triggers表trigger_group的外键', + str_prop_1 varchar(512) null comment 'String类型的trigger的第一个参数', + str_prop_2 varchar(512) null comment 'String类型的trigger的第二个参数', + str_prop_3 varchar(512) null comment 'String类型的trigger的第三个参数', + int_prop_1 int null comment 'int类型的trigger的第一个参数', + int_prop_2 int null comment 'int类型的trigger的第二个参数', + long_prop_1 bigint null comment 'long类型的trigger的第一个参数', + long_prop_2 bigint null comment 'long类型的trigger的第二个参数', + dec_prop_1 numeric(13,4) null comment 'decimal类型的trigger的第一个参数', + dec_prop_2 numeric(13,4) null comment 'decimal类型的trigger的第二个参数', + bool_prop_1 varchar(1) null comment 'Boolean类型的trigger的第一个参数', + bool_prop_2 varchar(1) null comment 'Boolean类型的trigger的第二个参数', + primary key (sched_name, trigger_name, trigger_group), + foreign key (sched_name, trigger_name, trigger_group) references QRTZ_TRIGGERS(sched_name, trigger_name, trigger_group) +) engine=innodb comment = '同步机制的行锁表'; + +commit; \ No newline at end of file diff --git a/ry-api/sql/ry_20260417.sql b/ry-api/sql/ry_20260417.sql new file mode 100644 index 0000000..91d618c --- /dev/null +++ b/ry-api/sql/ry_20260417.sql @@ -0,0 +1,725 @@ +-- ---------------------------- +-- 1、部门表 +-- ---------------------------- +drop table if exists sys_dept; +create table sys_dept ( + dept_id bigint(20) not null auto_increment comment '部门id', + parent_id bigint(20) default 0 comment '父部门id', + ancestors varchar(50) default '' comment '祖级列表', + dept_name varchar(30) default '' comment '部门名称', + order_num int(4) default 0 comment '显示顺序', + leader varchar(20) default null comment '负责人', + phone varchar(11) default null comment '联系电话', + email varchar(50) default null comment '邮箱', + status char(1) default '0' comment '部门状态(0正常 1停用)', + del_flag char(1) default '0' comment '删除标志(0代表存在 2代表删除)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + primary key (dept_id) +) engine=innodb auto_increment=200 comment = '部门表'; + +-- ---------------------------- +-- 初始化-部门表数据 +-- ---------------------------- +insert into sys_dept values(100, 0, '0', '若依科技', 0, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null); +insert into sys_dept values(101, 100, '0,100', '深圳总公司', 1, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null); +insert into sys_dept values(102, 100, '0,100', '长沙分公司', 2, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null); +insert into sys_dept values(103, 101, '0,100,101', '研发部门', 1, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null); +insert into sys_dept values(104, 101, '0,100,101', '市场部门', 2, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null); +insert into sys_dept values(105, 101, '0,100,101', '测试部门', 3, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null); +insert into sys_dept values(106, 101, '0,100,101', '财务部门', 4, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null); +insert into sys_dept values(107, 101, '0,100,101', '运维部门', 5, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null); +insert into sys_dept values(108, 102, '0,100,102', '市场部门', 1, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null); +insert into sys_dept values(109, 102, '0,100,102', '财务部门', 2, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null); + + +-- ---------------------------- +-- 2、用户信息表 +-- ---------------------------- +drop table if exists sys_user; +create table sys_user ( + user_id bigint(20) not null auto_increment comment '用户ID', + dept_id bigint(20) default null comment '部门ID', + user_name varchar(30) not null comment '用户账号', + nick_name varchar(30) not null comment '用户昵称', + user_type varchar(2) default '00' comment '用户类型(00系统用户)', + account_type varchar(8) default 'MAIN' comment '账号类型 (MAIN=主账号 / SUB=子账号)', + parent_user_id bigint(20) default null comment '主账号ID (子账号指向其主账号的 user_id)', + email varchar(50) default '' comment '用户邮箱', + phonenumber varchar(11) default '' comment '手机号码', + sex char(1) default '0' comment '用户性别(0男 1女 2未知)', + avatar varchar(100) default '' comment '头像地址', + password varchar(100) default '' comment '密码', + status char(1) default '0' comment '账号状态(0正常 1停用)', + del_flag char(1) default '0' comment '删除标志(0代表存在 2代表删除)', + login_ip varchar(128) default '' comment '最后登录IP', + login_date datetime comment '最后登录时间', + pwd_update_date datetime comment '密码最后更新时间', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (user_id) +) engine=innodb auto_increment=100 comment = '用户信息表'; + +-- ---------------------------- +-- 初始化-用户信息表数据 +-- ---------------------------- +insert into sys_user values(1, 103, 'admin', '若依', '00', 'ry@163.com', '15888888888', '1', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', sysdate(), sysdate(), 'admin', sysdate(), '', null, '管理员'); +insert into sys_user values(2, 105, 'ry', '若依', '00', 'ry@qq.com', '15666666666', '1', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', sysdate(), sysdate(), 'admin', sysdate(), '', null, '测试员'); + + +-- ---------------------------- +-- 3、岗位信息表 +-- ---------------------------- +drop table if exists sys_post; +create table sys_post +( + post_id bigint(20) not null auto_increment comment '岗位ID', + post_code varchar(64) not null comment '岗位编码', + post_name varchar(50) not null comment '岗位名称', + post_sort int(4) not null comment '显示顺序', + status char(1) not null comment '状态(0正常 1停用)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (post_id) +) engine=innodb comment = '岗位信息表'; + +-- ---------------------------- +-- 初始化-岗位信息表数据 +-- ---------------------------- +insert into sys_post values(1, 'ceo', '董事长', 1, '0', 'admin', sysdate(), '', null, ''); +insert into sys_post values(2, 'se', '项目经理', 2, '0', 'admin', sysdate(), '', null, ''); +insert into sys_post values(3, 'hr', '人力资源', 3, '0', 'admin', sysdate(), '', null, ''); +insert into sys_post values(4, 'user', '普通员工', 4, '0', 'admin', sysdate(), '', null, ''); + + +-- ---------------------------- +-- 4、角色信息表 +-- ---------------------------- +drop table if exists sys_role; +create table sys_role ( + role_id bigint(20) not null auto_increment comment '角色ID', + role_name varchar(30) not null comment '角色名称', + role_key varchar(100) not null comment '角色权限字符串', + role_sort int(4) not null comment '显示顺序', + data_scope char(1) default '1' comment '数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限)', + menu_check_strictly tinyint(1) default 1 comment '菜单树选择项是否关联显示', + dept_check_strictly tinyint(1) default 1 comment '部门树选择项是否关联显示', + status char(1) not null comment '角色状态(0正常 1停用)', + del_flag char(1) default '0' comment '删除标志(0代表存在 2代表删除)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (role_id) +) engine=innodb auto_increment=100 comment = '角色信息表'; + +-- ---------------------------- +-- 初始化-角色信息表数据 +-- ---------------------------- +insert into sys_role values('1', '超级管理员', 'admin', 1, 1, 1, 1, '0', '0', 'admin', sysdate(), '', null, '超级管理员'); +insert into sys_role values('2', '普通角色', 'common', 2, 2, 1, 1, '0', '0', 'admin', sysdate(), '', null, '普通角色'); + + +-- ---------------------------- +-- 5、菜单权限表 +-- ---------------------------- +drop table if exists sys_menu; +create table sys_menu ( + menu_id bigint(20) not null auto_increment comment '菜单ID', + menu_name varchar(50) not null comment '菜单名称', + parent_id bigint(20) default 0 comment '父菜单ID', + order_num int(4) default 0 comment '显示顺序', + path varchar(200) default '' comment '路由地址', + component varchar(255) default null comment '组件路径', + query varchar(255) default null comment '路由参数', + route_name varchar(50) default '' comment '路由名称', + is_frame int(1) default 1 comment '是否为外链(0是 1否)', + is_cache int(1) default 0 comment '是否缓存(0缓存 1不缓存)', + menu_type char(1) default '' comment '菜单类型(M目录 C菜单 F按钮)', + visible char(1) default 0 comment '菜单状态(0显示 1隐藏)', + status char(1) default 0 comment '菜单状态(0正常 1停用)', + perms varchar(100) default null comment '权限标识', + icon varchar(100) default '#' comment '菜单图标', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default '' comment '备注', + primary key (menu_id) +) engine=innodb auto_increment=2000 comment = '菜单权限表'; + +-- ---------------------------- +-- 初始化-菜单信息表数据 +-- ---------------------------- +-- 一级菜单 +insert into sys_menu values('1', '系统管理', '0', '1', 'system', null, '', '', 1, 0, 'M', '0', '0', '', 'system', 'admin', sysdate(), '', null, '系统管理目录'); +insert into sys_menu values('2', '系统监控', '0', '2', 'monitor', null, '', '', 1, 0, 'M', '0', '0', '', 'monitor', 'admin', sysdate(), '', null, '系统监控目录'); +insert into sys_menu values('3', '系统工具', '0', '3', 'tool', null, '', '', 1, 0, 'M', '0', '0', '', 'tool', 'admin', sysdate(), '', null, '系统工具目录'); +insert into sys_menu values('4', '若依官网', '0', '4', 'http://ruoyi.vip', null, '', '', 0, 0, 'M', '0', '0', '', 'guide', 'admin', sysdate(), '', null, '若依官网地址'); +-- 二级菜单 +insert into sys_menu values('100', '用户管理', '1', '1', 'user', 'system/user/index', '', '', 1, 0, 'C', '0', '0', 'system:user:list', 'user', 'admin', sysdate(), '', null, '用户管理菜单'); +insert into sys_menu values('101', '角色管理', '1', '2', 'role', 'system/role/index', '', '', 1, 0, 'C', '0', '0', 'system:role:list', 'peoples', 'admin', sysdate(), '', null, '角色管理菜单'); +insert into sys_menu values('102', '菜单管理', '1', '3', 'menu', 'system/menu/index', '', '', 1, 0, 'C', '0', '0', 'system:menu:list', 'tree-table', 'admin', sysdate(), '', null, '菜单管理菜单'); +insert into sys_menu values('103', '部门管理', '1', '4', 'dept', 'system/dept/index', '', '', 1, 0, 'C', '0', '0', 'system:dept:list', 'tree', 'admin', sysdate(), '', null, '部门管理菜单'); +insert into sys_menu values('104', '岗位管理', '1', '5', 'post', 'system/post/index', '', '', 1, 0, 'C', '0', '0', 'system:post:list', 'post', 'admin', sysdate(), '', null, '岗位管理菜单'); +insert into sys_menu values('105', '字典管理', '1', '6', 'dict', 'system/dict/index', '', '', 1, 0, 'C', '0', '0', 'system:dict:list', 'dict', 'admin', sysdate(), '', null, '字典管理菜单'); +insert into sys_menu values('106', '参数设置', '1', '7', 'config', 'system/config/index', '', '', 1, 0, 'C', '0', '0', 'system:config:list', 'edit', 'admin', sysdate(), '', null, '参数设置菜单'); +insert into sys_menu values('107', '通知公告', '1', '8', 'notice', 'system/notice/index', '', '', 1, 0, 'C', '0', '0', 'system:notice:list', 'message', 'admin', sysdate(), '', null, '通知公告菜单'); +insert into sys_menu values('108', '日志管理', '1', '9', 'log', '', '', '', 1, 0, 'M', '0', '0', '', 'log', 'admin', sysdate(), '', null, '日志管理菜单'); +insert into sys_menu values('109', '在线用户', '2', '1', 'online', 'monitor/online/index', '', '', 1, 0, 'C', '0', '0', 'monitor:online:list', 'online', 'admin', sysdate(), '', null, '在线用户菜单'); +insert into sys_menu values('110', '定时任务', '2', '2', 'job', 'monitor/job/index', '', '', 1, 0, 'C', '0', '0', 'monitor:job:list', 'job', 'admin', sysdate(), '', null, '定时任务菜单'); +insert into sys_menu values('111', '数据监控', '2', '3', 'druid', 'monitor/druid/index', '', '', 1, 0, 'C', '0', '0', 'monitor:druid:list', 'druid', 'admin', sysdate(), '', null, '数据监控菜单'); +insert into sys_menu values('112', '服务监控', '2', '4', 'server', 'monitor/server/index', '', '', 1, 0, 'C', '0', '0', 'monitor:server:list', 'server', 'admin', sysdate(), '', null, '服务监控菜单'); +insert into sys_menu values('113', '缓存监控', '2', '5', 'cache', 'monitor/cache/index', '', '', 1, 0, 'C', '0', '0', 'monitor:cache:list', 'redis', 'admin', sysdate(), '', null, '缓存监控菜单'); +insert into sys_menu values('114', '缓存列表', '2', '6', 'cacheList', 'monitor/cache/list', '', '', 1, 0, 'C', '0', '0', 'monitor:cache:list', 'redis-list', 'admin', sysdate(), '', null, '缓存列表菜单'); +insert into sys_menu values('115', '表单构建', '3', '1', 'build', 'tool/build/index', '', '', 1, 0, 'C', '0', '0', 'tool:build:list', 'build', 'admin', sysdate(), '', null, '表单构建菜单'); +insert into sys_menu values('116', '代码生成', '3', '2', 'gen', 'tool/gen/index', '', '', 1, 0, 'C', '0', '0', 'tool:gen:list', 'code', 'admin', sysdate(), '', null, '代码生成菜单'); +insert into sys_menu values('117', '系统接口', '3', '3', 'swagger', 'tool/swagger/index', '', '', 1, 0, 'C', '0', '0', 'tool:swagger:list', 'swagger', 'admin', sysdate(), '', null, '系统接口菜单'); +-- 三级菜单 +insert into sys_menu values('500', '操作日志', '108', '1', 'operlog', 'monitor/operlog/index', '', '', 1, 0, 'C', '0', '0', 'monitor:operlog:list', 'form', 'admin', sysdate(), '', null, '操作日志菜单'); +insert into sys_menu values('501', '登录日志', '108', '2', 'logininfor', 'monitor/logininfor/index', '', '', 1, 0, 'C', '0', '0', 'monitor:logininfor:list', 'logininfor', 'admin', sysdate(), '', null, '登录日志菜单'); +-- 用户管理按钮 +insert into sys_menu values('1000', '用户查询', '100', '1', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1001', '用户新增', '100', '2', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1002', '用户修改', '100', '3', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1003', '用户删除', '100', '4', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1004', '用户导出', '100', '5', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:export', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1005', '用户导入', '100', '6', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:import', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1006', '重置密码', '100', '7', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:resetPwd', '#', 'admin', sysdate(), '', null, ''); +-- 角色管理按钮 +insert into sys_menu values('1007', '角色查询', '101', '1', '', '', '', '', 1, 0, 'F', '0', '0', 'system:role:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1008', '角色新增', '101', '2', '', '', '', '', 1, 0, 'F', '0', '0', 'system:role:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1009', '角色修改', '101', '3', '', '', '', '', 1, 0, 'F', '0', '0', 'system:role:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1010', '角色删除', '101', '4', '', '', '', '', 1, 0, 'F', '0', '0', 'system:role:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1011', '角色导出', '101', '5', '', '', '', '', 1, 0, 'F', '0', '0', 'system:role:export', '#', 'admin', sysdate(), '', null, ''); +-- 菜单管理按钮 +insert into sys_menu values('1012', '菜单查询', '102', '1', '', '', '', '', 1, 0, 'F', '0', '0', 'system:menu:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1013', '菜单新增', '102', '2', '', '', '', '', 1, 0, 'F', '0', '0', 'system:menu:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1014', '菜单修改', '102', '3', '', '', '', '', 1, 0, 'F', '0', '0', 'system:menu:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1015', '菜单删除', '102', '4', '', '', '', '', 1, 0, 'F', '0', '0', 'system:menu:remove', '#', 'admin', sysdate(), '', null, ''); +-- 部门管理按钮 +insert into sys_menu values('1016', '部门查询', '103', '1', '', '', '', '', 1, 0, 'F', '0', '0', 'system:dept:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1017', '部门新增', '103', '2', '', '', '', '', 1, 0, 'F', '0', '0', 'system:dept:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1018', '部门修改', '103', '3', '', '', '', '', 1, 0, 'F', '0', '0', 'system:dept:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1019', '部门删除', '103', '4', '', '', '', '', 1, 0, 'F', '0', '0', 'system:dept:remove', '#', 'admin', sysdate(), '', null, ''); +-- 岗位管理按钮 +insert into sys_menu values('1020', '岗位查询', '104', '1', '', '', '', '', 1, 0, 'F', '0', '0', 'system:post:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1021', '岗位新增', '104', '2', '', '', '', '', 1, 0, 'F', '0', '0', 'system:post:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1022', '岗位修改', '104', '3', '', '', '', '', 1, 0, 'F', '0', '0', 'system:post:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1023', '岗位删除', '104', '4', '', '', '', '', 1, 0, 'F', '0', '0', 'system:post:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1024', '岗位导出', '104', '5', '', '', '', '', 1, 0, 'F', '0', '0', 'system:post:export', '#', 'admin', sysdate(), '', null, ''); +-- 字典管理按钮 +insert into sys_menu values('1025', '字典查询', '105', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:dict:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1026', '字典新增', '105', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:dict:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1027', '字典修改', '105', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:dict:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1028', '字典删除', '105', '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:dict:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1029', '字典导出', '105', '5', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:dict:export', '#', 'admin', sysdate(), '', null, ''); +-- 参数设置按钮 +insert into sys_menu values('1030', '参数查询', '106', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:config:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1031', '参数新增', '106', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:config:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1032', '参数修改', '106', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:config:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1033', '参数删除', '106', '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:config:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1034', '参数导出', '106', '5', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:config:export', '#', 'admin', sysdate(), '', null, ''); +-- 通知公告按钮 +insert into sys_menu values('1035', '公告查询', '107', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:notice:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1036', '公告新增', '107', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:notice:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1037', '公告修改', '107', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:notice:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1038', '公告删除', '107', '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:notice:remove', '#', 'admin', sysdate(), '', null, ''); +-- 操作日志按钮 +insert into sys_menu values('1039', '操作查询', '500', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:operlog:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1040', '操作删除', '500', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:operlog:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1041', '日志导出', '500', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:operlog:export', '#', 'admin', sysdate(), '', null, ''); +-- 登录日志按钮 +insert into sys_menu values('1042', '登录查询', '501', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:logininfor:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1043', '登录删除', '501', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:logininfor:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1044', '日志导出', '501', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:logininfor:export', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1045', '账户解锁', '501', '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:logininfor:unlock', '#', 'admin', sysdate(), '', null, ''); +-- 在线用户按钮 +insert into sys_menu values('1046', '在线查询', '109', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:online:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1047', '批量强退', '109', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:online:batchLogout', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1048', '单条强退', '109', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:online:forceLogout', '#', 'admin', sysdate(), '', null, ''); +-- 定时任务按钮 +insert into sys_menu values('1049', '任务查询', '110', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:job:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1050', '任务新增', '110', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:job:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1051', '任务修改', '110', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:job:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1052', '任务删除', '110', '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:job:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1053', '状态修改', '110', '5', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:job:changeStatus', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1054', '任务导出', '110', '6', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:job:export', '#', 'admin', sysdate(), '', null, ''); +-- 代码生成按钮 +insert into sys_menu values('1055', '生成查询', '116', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1056', '生成修改', '116', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1057', '生成删除', '116', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1058', '导入代码', '116', '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:import', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1059', '预览代码', '116', '5', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:preview', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1060', '生成代码', '116', '6', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:code', '#', 'admin', sysdate(), '', null, ''); + + +-- ---------------------------- +-- 6、用户和角色关联表 用户N-1角色 +-- ---------------------------- +drop table if exists sys_user_role; +create table sys_user_role ( + user_id bigint(20) not null comment '用户ID', + role_id bigint(20) not null comment '角色ID', + primary key(user_id, role_id) +) engine=innodb comment = '用户和角色关联表'; + +-- ---------------------------- +-- 初始化-用户和角色关联表数据 +-- ---------------------------- +insert into sys_user_role values ('1', '1'); +insert into sys_user_role values ('2', '2'); + + +-- ---------------------------- +-- 7、角色和菜单关联表 角色1-N菜单 +-- ---------------------------- +drop table if exists sys_role_menu; +create table sys_role_menu ( + role_id bigint(20) not null comment '角色ID', + menu_id bigint(20) not null comment '菜单ID', + primary key(role_id, menu_id) +) engine=innodb comment = '角色和菜单关联表'; + +-- ---------------------------- +-- 初始化-角色和菜单关联表数据 +-- ---------------------------- +insert into sys_role_menu values ('2', '1'); +insert into sys_role_menu values ('2', '2'); +insert into sys_role_menu values ('2', '3'); +insert into sys_role_menu values ('2', '4'); +insert into sys_role_menu values ('2', '100'); +insert into sys_role_menu values ('2', '101'); +insert into sys_role_menu values ('2', '102'); +insert into sys_role_menu values ('2', '103'); +insert into sys_role_menu values ('2', '104'); +insert into sys_role_menu values ('2', '105'); +insert into sys_role_menu values ('2', '106'); +insert into sys_role_menu values ('2', '107'); +insert into sys_role_menu values ('2', '108'); +insert into sys_role_menu values ('2', '109'); +insert into sys_role_menu values ('2', '110'); +insert into sys_role_menu values ('2', '111'); +insert into sys_role_menu values ('2', '112'); +insert into sys_role_menu values ('2', '113'); +insert into sys_role_menu values ('2', '114'); +insert into sys_role_menu values ('2', '115'); +insert into sys_role_menu values ('2', '116'); +insert into sys_role_menu values ('2', '117'); +insert into sys_role_menu values ('2', '500'); +insert into sys_role_menu values ('2', '501'); +insert into sys_role_menu values ('2', '1000'); +insert into sys_role_menu values ('2', '1001'); +insert into sys_role_menu values ('2', '1002'); +insert into sys_role_menu values ('2', '1003'); +insert into sys_role_menu values ('2', '1004'); +insert into sys_role_menu values ('2', '1005'); +insert into sys_role_menu values ('2', '1006'); +insert into sys_role_menu values ('2', '1007'); +insert into sys_role_menu values ('2', '1008'); +insert into sys_role_menu values ('2', '1009'); +insert into sys_role_menu values ('2', '1010'); +insert into sys_role_menu values ('2', '1011'); +insert into sys_role_menu values ('2', '1012'); +insert into sys_role_menu values ('2', '1013'); +insert into sys_role_menu values ('2', '1014'); +insert into sys_role_menu values ('2', '1015'); +insert into sys_role_menu values ('2', '1016'); +insert into sys_role_menu values ('2', '1017'); +insert into sys_role_menu values ('2', '1018'); +insert into sys_role_menu values ('2', '1019'); +insert into sys_role_menu values ('2', '1020'); +insert into sys_role_menu values ('2', '1021'); +insert into sys_role_menu values ('2', '1022'); +insert into sys_role_menu values ('2', '1023'); +insert into sys_role_menu values ('2', '1024'); +insert into sys_role_menu values ('2', '1025'); +insert into sys_role_menu values ('2', '1026'); +insert into sys_role_menu values ('2', '1027'); +insert into sys_role_menu values ('2', '1028'); +insert into sys_role_menu values ('2', '1029'); +insert into sys_role_menu values ('2', '1030'); +insert into sys_role_menu values ('2', '1031'); +insert into sys_role_menu values ('2', '1032'); +insert into sys_role_menu values ('2', '1033'); +insert into sys_role_menu values ('2', '1034'); +insert into sys_role_menu values ('2', '1035'); +insert into sys_role_menu values ('2', '1036'); +insert into sys_role_menu values ('2', '1037'); +insert into sys_role_menu values ('2', '1038'); +insert into sys_role_menu values ('2', '1039'); +insert into sys_role_menu values ('2', '1040'); +insert into sys_role_menu values ('2', '1041'); +insert into sys_role_menu values ('2', '1042'); +insert into sys_role_menu values ('2', '1043'); +insert into sys_role_menu values ('2', '1044'); +insert into sys_role_menu values ('2', '1045'); +insert into sys_role_menu values ('2', '1046'); +insert into sys_role_menu values ('2', '1047'); +insert into sys_role_menu values ('2', '1048'); +insert into sys_role_menu values ('2', '1049'); +insert into sys_role_menu values ('2', '1050'); +insert into sys_role_menu values ('2', '1051'); +insert into sys_role_menu values ('2', '1052'); +insert into sys_role_menu values ('2', '1053'); +insert into sys_role_menu values ('2', '1054'); +insert into sys_role_menu values ('2', '1055'); +insert into sys_role_menu values ('2', '1056'); +insert into sys_role_menu values ('2', '1057'); +insert into sys_role_menu values ('2', '1058'); +insert into sys_role_menu values ('2', '1059'); +insert into sys_role_menu values ('2', '1060'); + +-- ---------------------------- +-- 8、角色和部门关联表 角色1-N部门 +-- ---------------------------- +drop table if exists sys_role_dept; +create table sys_role_dept ( + role_id bigint(20) not null comment '角色ID', + dept_id bigint(20) not null comment '部门ID', + primary key(role_id, dept_id) +) engine=innodb comment = '角色和部门关联表'; + +-- ---------------------------- +-- 初始化-角色和部门关联表数据 +-- ---------------------------- +insert into sys_role_dept values ('2', '100'); +insert into sys_role_dept values ('2', '101'); +insert into sys_role_dept values ('2', '105'); + + +-- ---------------------------- +-- 9、用户与岗位关联表 用户1-N岗位 +-- ---------------------------- +drop table if exists sys_user_post; +create table sys_user_post +( + user_id bigint(20) not null comment '用户ID', + post_id bigint(20) not null comment '岗位ID', + primary key (user_id, post_id) +) engine=innodb comment = '用户与岗位关联表'; + +-- ---------------------------- +-- 初始化-用户与岗位关联表数据 +-- ---------------------------- +insert into sys_user_post values ('1', '1'); +insert into sys_user_post values ('2', '2'); + + +-- ---------------------------- +-- 10、操作日志记录 +-- ---------------------------- +drop table if exists sys_oper_log; +create table sys_oper_log ( + oper_id bigint(20) not null auto_increment comment '日志主键', + title varchar(50) default '' comment '模块标题', + business_type int(2) default 0 comment '业务类型(0其它 1新增 2修改 3删除)', + method varchar(200) default '' comment '方法名称', + request_method varchar(10) default '' comment '请求方式', + operator_type int(1) default 0 comment '操作类别(0其它 1后台用户 2手机端用户)', + oper_name varchar(50) default '' comment '操作人员', + dept_name varchar(50) default '' comment '部门名称', + oper_url varchar(255) default '' comment '请求URL', + oper_ip varchar(128) default '' comment '主机地址', + oper_location varchar(255) default '' comment '操作地点', + oper_param varchar(2000) default '' comment '请求参数', + json_result varchar(2000) default '' comment '返回参数', + status int(1) default 0 comment '操作状态(0正常 1异常)', + error_msg varchar(2000) default '' comment '错误消息', + oper_time datetime comment '操作时间', + cost_time bigint(20) default 0 comment '消耗时间', + primary key (oper_id), + key idx_sys_oper_log_bt (business_type), + key idx_sys_oper_log_s (status), + key idx_sys_oper_log_ot (oper_time) +) engine=innodb auto_increment=100 comment = '操作日志记录'; + + +-- ---------------------------- +-- 11、字典类型表 +-- ---------------------------- +drop table if exists sys_dict_type; +create table sys_dict_type +( + dict_id bigint(20) not null auto_increment comment '字典主键', + dict_name varchar(100) default '' comment '字典名称', + dict_type varchar(100) default '' comment '字典类型', + status char(1) default '0' comment '状态(0正常 1停用)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (dict_id), + unique (dict_type) +) engine=innodb auto_increment=100 comment = '字典类型表'; + +insert into sys_dict_type values(1, '用户性别', 'sys_user_sex', '0', 'admin', sysdate(), '', null, '用户性别列表'); +insert into sys_dict_type values(2, '菜单状态', 'sys_show_hide', '0', 'admin', sysdate(), '', null, '菜单状态列表'); +insert into sys_dict_type values(3, '系统开关', 'sys_normal_disable', '0', 'admin', sysdate(), '', null, '系统开关列表'); +insert into sys_dict_type values(4, '任务状态', 'sys_job_status', '0', 'admin', sysdate(), '', null, '任务状态列表'); +insert into sys_dict_type values(5, '任务分组', 'sys_job_group', '0', 'admin', sysdate(), '', null, '任务分组列表'); +insert into sys_dict_type values(6, '系统是否', 'sys_yes_no', '0', 'admin', sysdate(), '', null, '系统是否列表'); +insert into sys_dict_type values(7, '通知类型', 'sys_notice_type', '0', 'admin', sysdate(), '', null, '通知类型列表'); +insert into sys_dict_type values(8, '通知状态', 'sys_notice_status', '0', 'admin', sysdate(), '', null, '通知状态列表'); +insert into sys_dict_type values(9, '操作类型', 'sys_oper_type', '0', 'admin', sysdate(), '', null, '操作类型列表'); +insert into sys_dict_type values(10, '系统状态', 'sys_common_status', '0', 'admin', sysdate(), '', null, '登录状态列表'); + + +-- ---------------------------- +-- 12、字典数据表 +-- ---------------------------- +drop table if exists sys_dict_data; +create table sys_dict_data +( + dict_code bigint(20) not null auto_increment comment '字典编码', + dict_sort int(4) default 0 comment '字典排序', + dict_label varchar(100) default '' comment '字典标签', + dict_value varchar(100) default '' comment '字典键值', + dict_type varchar(100) default '' comment '字典类型', + css_class varchar(100) default null comment '样式属性(其他样式扩展)', + list_class varchar(100) default null comment '表格回显样式', + is_default char(1) default 'N' comment '是否默认(Y是 N否)', + status char(1) default '0' comment '状态(0正常 1停用)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (dict_code) +) engine=innodb auto_increment=100 comment = '字典数据表'; + +insert into sys_dict_data values(1, 1, '男', '0', 'sys_user_sex', '', '', 'Y', '0', 'admin', sysdate(), '', null, '性别男'); +insert into sys_dict_data values(2, 2, '女', '1', 'sys_user_sex', '', '', 'N', '0', 'admin', sysdate(), '', null, '性别女'); +insert into sys_dict_data values(3, 3, '未知', '2', 'sys_user_sex', '', '', 'N', '0', 'admin', sysdate(), '', null, '性别未知'); +insert into sys_dict_data values(4, 1, '显示', '0', 'sys_show_hide', '', 'primary', 'Y', '0', 'admin', sysdate(), '', null, '显示菜单'); +insert into sys_dict_data values(5, 2, '隐藏', '1', 'sys_show_hide', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '隐藏菜单'); +insert into sys_dict_data values(6, 1, '正常', '0', 'sys_normal_disable', '', 'primary', 'Y', '0', 'admin', sysdate(), '', null, '正常状态'); +insert into sys_dict_data values(7, 2, '停用', '1', 'sys_normal_disable', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '停用状态'); +insert into sys_dict_data values(8, 1, '正常', '0', 'sys_job_status', '', 'primary', 'Y', '0', 'admin', sysdate(), '', null, '正常状态'); +insert into sys_dict_data values(9, 2, '暂停', '1', 'sys_job_status', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '停用状态'); +insert into sys_dict_data values(10, 1, '默认', 'DEFAULT', 'sys_job_group', '', '', 'Y', '0', 'admin', sysdate(), '', null, '默认分组'); +insert into sys_dict_data values(11, 2, '系统', 'SYSTEM', 'sys_job_group', '', '', 'N', '0', 'admin', sysdate(), '', null, '系统分组'); +insert into sys_dict_data values(12, 1, '是', 'Y', 'sys_yes_no', '', 'primary', 'Y', '0', 'admin', sysdate(), '', null, '系统默认是'); +insert into sys_dict_data values(13, 2, '否', 'N', 'sys_yes_no', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '系统默认否'); +insert into sys_dict_data values(14, 1, '通知', '1', 'sys_notice_type', '', 'warning', 'Y', '0', 'admin', sysdate(), '', null, '通知'); +insert into sys_dict_data values(15, 2, '公告', '2', 'sys_notice_type', '', 'success', 'N', '0', 'admin', sysdate(), '', null, '公告'); +insert into sys_dict_data values(16, 1, '正常', '0', 'sys_notice_status', '', 'primary', 'Y', '0', 'admin', sysdate(), '', null, '正常状态'); +insert into sys_dict_data values(17, 2, '关闭', '1', 'sys_notice_status', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '关闭状态'); +insert into sys_dict_data values(18, 99, '其他', '0', 'sys_oper_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, '其他操作'); +insert into sys_dict_data values(19, 1, '新增', '1', 'sys_oper_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, '新增操作'); +insert into sys_dict_data values(20, 2, '修改', '2', 'sys_oper_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, '修改操作'); +insert into sys_dict_data values(21, 3, '删除', '3', 'sys_oper_type', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '删除操作'); +insert into sys_dict_data values(22, 4, '授权', '4', 'sys_oper_type', '', 'primary', 'N', '0', 'admin', sysdate(), '', null, '授权操作'); +insert into sys_dict_data values(23, 5, '导出', '5', 'sys_oper_type', '', 'warning', 'N', '0', 'admin', sysdate(), '', null, '导出操作'); +insert into sys_dict_data values(24, 6, '导入', '6', 'sys_oper_type', '', 'warning', 'N', '0', 'admin', sysdate(), '', null, '导入操作'); +insert into sys_dict_data values(25, 7, '强退', '7', 'sys_oper_type', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '强退操作'); +insert into sys_dict_data values(26, 8, '生成代码', '8', 'sys_oper_type', '', 'warning', 'N', '0', 'admin', sysdate(), '', null, '生成操作'); +insert into sys_dict_data values(27, 9, '清空数据', '9', 'sys_oper_type', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '清空操作'); +insert into sys_dict_data values(28, 1, '成功', '0', 'sys_common_status', '', 'primary', 'N', '0', 'admin', sysdate(), '', null, '正常状态'); +insert into sys_dict_data values(29, 2, '失败', '1', 'sys_common_status', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '停用状态'); + + +-- ---------------------------- +-- 13、参数配置表 +-- ---------------------------- +drop table if exists sys_config; +create table sys_config ( + config_id int(5) not null auto_increment comment '参数主键', + config_name varchar(100) default '' comment '参数名称', + config_key varchar(100) default '' comment '参数键名', + config_value varchar(500) default '' comment '参数键值', + config_type char(1) default 'N' comment '系统内置(Y是 N否)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (config_id) +) engine=innodb auto_increment=100 comment = '参数配置表'; + +insert into sys_config values(1, '主框架页-默认皮肤样式名称', 'sys.index.skinName', 'skin-blue', 'Y', 'admin', sysdate(), '', null, '蓝色 skin-blue、绿色 skin-green、紫色 skin-purple、红色 skin-red、黄色 skin-yellow' ); +insert into sys_config values(2, '用户管理-账号初始密码', 'sys.user.initPassword', '123456', 'Y', 'admin', sysdate(), '', null, '初始化密码 123456' ); +insert into sys_config values(3, '主框架页-侧边栏主题', 'sys.index.sideTheme', 'theme-dark', 'Y', 'admin', sysdate(), '', null, '深色主题theme-dark,浅色主题theme-light' ); +insert into sys_config values(4, '账号自助-验证码开关', 'sys.account.captchaEnabled', 'true', 'Y', 'admin', sysdate(), '', null, '是否开启验证码功能(true开启,false关闭)'); +insert into sys_config values(5, '账号自助-是否开启用户注册功能', 'sys.account.registerUser', 'false', 'Y', 'admin', sysdate(), '', null, '是否开启注册用户功能(true开启,false关闭)'); +insert into sys_config values(6, '用户登录-黑名单列表', 'sys.login.blackIPList', '', 'Y', 'admin', sysdate(), '', null, '设置登录IP黑名单限制,多个匹配项以;分隔,支持匹配(*通配、网段)'); +insert into sys_config values(7, '用户管理-初始密码修改策略', 'sys.account.initPasswordModify', '1', 'Y', 'admin', sysdate(), '', null, '0:初始密码修改策略关闭,没有任何提示,1:提醒用户,如果未修改初始密码,则在登录时就会提醒修改密码对话框'); +insert into sys_config values(8, '用户管理-账号密码更新周期', 'sys.account.passwordValidateDays', '0', 'Y', 'admin', sysdate(), '', null, '密码更新周期(填写数字,数据初始化值为0不限制,若修改必须为大于0小于365的正整数),如果超过这个周期登录系统时,则在登录时就会提醒修改密码对话框'); +insert into sys_config values(9, '用户管理-密码字符范围', 'sys.account.chrtype', '0', 'Y', 'admin', sysdate(), '', null, '默认任意字符范围,0任意(密码可以输入任意字符),1数字(密码只能为0-9数字),2英文字母(密码只能为a-z和A-Z字母),3字母和数字(密码必须包含字母,数字),4字母数字和特殊字符(目前支持的特殊字符包括:~!@#$%^&*()-=_+)'); + + +-- ---------------------------- +-- 14、系统访问记录 +-- ---------------------------- +drop table if exists sys_logininfor; +create table sys_logininfor ( + info_id bigint(20) not null auto_increment comment '访问ID', + user_name varchar(50) default '' comment '用户账号', + ipaddr varchar(128) default '' comment '登录IP地址', + login_location varchar(255) default '' comment '登录地点', + browser varchar(50) default '' comment '浏览器类型', + os varchar(50) default '' comment '操作系统', + status char(1) default '0' comment '登录状态(0成功 1失败)', + msg varchar(255) default '' comment '提示消息', + login_time datetime comment '访问时间', + primary key (info_id), + key idx_sys_logininfor_s (status), + key idx_sys_logininfor_lt (login_time) +) engine=innodb auto_increment=100 comment = '系统访问记录'; + + +-- ---------------------------- +-- 15、定时任务调度表 +-- ---------------------------- +drop table if exists sys_job; +create table sys_job ( + job_id bigint(20) not null auto_increment comment '任务ID', + job_name varchar(64) default '' comment '任务名称', + job_group varchar(64) default 'DEFAULT' comment '任务组名', + invoke_target varchar(500) not null comment '调用目标字符串', + cron_expression varchar(255) default '' comment 'cron执行表达式', + misfire_policy varchar(20) default '3' comment '计划执行错误策略(1立即执行 2执行一次 3放弃执行)', + concurrent char(1) default '1' comment '是否并发执行(0允许 1禁止)', + status char(1) default '0' comment '状态(0正常 1暂停)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default '' comment '备注信息', + primary key (job_id, job_name, job_group) +) engine=innodb auto_increment=100 comment = '定时任务调度表'; + +insert into sys_job values(1, '系统默认(无参)', 'DEFAULT', 'ryTask.ryNoParams', '0/10 * * * * ?', '3', '1', '1', 'admin', sysdate(), '', null, ''); +insert into sys_job values(2, '系统默认(有参)', 'DEFAULT', 'ryTask.ryParams(\'ry\')', '0/15 * * * * ?', '3', '1', '1', 'admin', sysdate(), '', null, ''); +insert into sys_job values(3, '系统默认(多参)', 'DEFAULT', 'ryTask.ryMultipleParams(\'ry\', true, 2000L, 316.50D, 100)', '0/20 * * * * ?', '3', '1', '1', 'admin', sysdate(), '', null, ''); + + +-- ---------------------------- +-- 16、定时任务调度日志表 +-- ---------------------------- +drop table if exists sys_job_log; +create table sys_job_log ( + job_log_id bigint(20) not null auto_increment comment '任务日志ID', + job_name varchar(64) not null comment '任务名称', + job_group varchar(64) not null comment '任务组名', + invoke_target varchar(500) not null comment '调用目标字符串', + job_message varchar(500) comment '日志信息', + status char(1) default '0' comment '执行状态(0正常 1失败)', + exception_info varchar(2000) default '' comment '异常信息', + start_time datetime comment '执行开始时间', + end_time datetime comment '执行结束时间', + create_time datetime comment '创建时间', + primary key (job_log_id) +) engine=innodb comment = '定时任务调度日志表'; + + +-- ---------------------------- +-- 17、通知公告表 +-- ---------------------------- +drop table if exists sys_notice; +create table sys_notice ( + notice_id int(4) not null auto_increment comment '公告ID', + notice_title varchar(50) not null comment '公告标题', + notice_type char(1) not null comment '公告类型(1通知 2公告)', + notice_content longblob default null comment '公告内容', + status char(1) default '0' comment '公告状态(0正常 1关闭)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(255) default null comment '备注', + primary key (notice_id) +) engine=innodb auto_increment=10 comment = '通知公告表'; + +-- ---------------------------- +-- 初始化-公告信息表数据 +-- ---------------------------- +insert into sys_notice values('1', '温馨提醒:2018-07-01 若依新版本发布啦', '2', '新版本内容', '0', 'admin', sysdate(), '', null, '管理员'); +insert into sys_notice values('2', '维护通知:2018-07-01 若依系统凌晨维护', '1', '维护内容', '0', 'admin', sysdate(), '', null, '管理员'); +insert into sys_notice values('3', '若依开源框架介绍', '1', '

项目介绍

RuoYi开源项目是为企业用户定制的后台脚手架框架,为企业打造的一站式解决方案,降低企业开发成本,提升开发效率。主要包括用户管理、角色管理、部门管理、菜单管理、参数管理、字典管理、岗位管理、定时任务服务监控、登录日志、操作日志、代码生成等功能。其中,还支持多数据源、数据权限、国际化、Redis缓存、Docker部署、滑动验证码、第三方认证登录、分布式事务、分布式文件存储、分库分表处理等技术特点。


官网及演示

若依官网地址: http://ruoyi.vip

若依文档地址: http://doc.ruoyi.vip

演示地址【不分离版】: http://demo.ruoyi.vip

演示地址【分离版本】: http://vue.ruoyi.vip

演示地址【微服务版】: http://cloud.ruoyi.vip

演示地址【移动端版】: http://h5.ruoyi.vip


', '0', 'admin', sysdate(), '', null, '管理员'); + + +-- ---------------------------- +-- 18、公告已读记录表 +-- ---------------------------- +drop table if exists sys_notice_read; +create table sys_notice_read ( + read_id bigint(20) not null auto_increment comment '已读主键', + notice_id int(4) not null comment '公告id', + user_id bigint(20) not null comment '用户id', + read_time datetime not null comment '阅读时间', + primary key (read_id), + unique key uk_user_notice (user_id, notice_id) comment '同一用户同一公告只记录一次' +) engine=innodb auto_increment=1 comment='公告已读记录表'; + + +-- ---------------------------- +-- 19、代码生成业务表 +-- ---------------------------- +drop table if exists gen_table; +create table gen_table ( + table_id bigint(20) not null auto_increment comment '编号', + table_name varchar(200) default '' comment '表名称', + table_comment varchar(500) default '' comment '表描述', + sub_table_name varchar(64) default null comment '关联子表的表名', + sub_table_fk_name varchar(64) default null comment '子表关联的外键名', + class_name varchar(100) default '' comment '实体类名称', + tpl_category varchar(200) default 'crud' comment '使用的模板(crud单表操作 tree树表操作)', + tpl_web_type varchar(30) default '' comment '前端模板类型(element-ui模版 element-plus模版)', + package_name varchar(100) comment '生成包路径', + module_name varchar(30) comment '生成模块名', + business_name varchar(30) comment '生成业务名', + function_name varchar(50) comment '生成功能名', + function_author varchar(50) comment '生成功能作者', + form_col_num int(1) default 1 comment '表单布局(单列 双列 三列)', + gen_type char(1) default '0' comment '生成代码方式(0zip压缩包 1自定义路径)', + gen_path varchar(200) default '/' comment '生成路径(不填默认项目路径)', + options varchar(1000) comment '其它生成选项', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (table_id) +) engine=innodb auto_increment=1 comment = '代码生成业务表'; + + +-- ---------------------------- +-- 20、代码生成业务表字段 +-- ---------------------------- +drop table if exists gen_table_column; +create table gen_table_column ( + column_id bigint(20) not null auto_increment comment '编号', + table_id bigint(20) comment '归属表编号', + column_name varchar(200) comment '列名称', + column_comment varchar(500) comment '列描述', + column_type varchar(100) comment '列类型', + java_type varchar(500) comment 'JAVA类型', + java_field varchar(200) comment 'JAVA字段名', + is_pk char(1) comment '是否主键(1是)', + is_increment char(1) comment '是否自增(1是)', + is_required char(1) comment '是否必填(1是)', + is_insert char(1) comment '是否为插入字段(1是)', + is_edit char(1) comment '是否编辑字段(1是)', + is_list char(1) comment '是否列表字段(1是)', + is_query char(1) comment '是否查询字段(1是)', + query_type varchar(200) default 'EQ' comment '查询方式(等于、不等于、大于、小于、范围)', + html_type varchar(200) comment '显示类型(文本框、文本域、下拉框、复选框、单选框、日期控件)', + dict_type varchar(200) default '' comment '字典类型', + sort int comment '排序', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + primary key (column_id) +) engine=innodb auto_increment=1 comment = '代码生成业务表字段'; \ No newline at end of file diff --git a/ry-vue3/index.html b/ry-vue3/index.html new file mode 100644 index 0000000..9552ab1 --- /dev/null +++ b/ry-vue3/index.html @@ -0,0 +1,12 @@ + + + + + + 北京整合医学学会 - 项目管理系统 + + +
+ + + diff --git a/ry-vue3/package-lock.json b/ry-vue3/package-lock.json new file mode 100644 index 0000000..1b429a0 --- /dev/null +++ b/ry-vue3/package-lock.json @@ -0,0 +1,3068 @@ +{ + "name": "ry-vue3", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ry-vue3", + "version": "1.0.0", + "dependencies": { + "@element-plus/icons-vue": "^2.3.0", + "axios": "^1.6.0", + "element-plus": "^2.4.0", + "pinia": "^2.1.0", + "qrcode": "^1.5.4", + "vue": "^3.4.0", + "vue-router": "^4.2.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.0", + "sass": "^1.69.0", + "unplugin-auto-import": "^0.17.0", + "unplugin-vue-components": "^0.26.0", + "vite": "^5.0.0" + } + }, + "node_modules/@antfu/utils": { + "version": "0.7.10", + "resolved": "https://registry.npmmirror.com/@antfu/utils/-/utils-0.7.10.tgz", + "integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", + "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", + "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher/-/watcher-2.6.0.tgz", + "integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.6.0", + "@parcel/watcher-darwin-arm64": "2.6.0", + "@parcel/watcher-darwin-x64": "2.6.0", + "@parcel/watcher-freebsd-x64": "2.6.0", + "@parcel/watcher-linux-arm-glibc": "2.6.0", + "@parcel/watcher-linux-arm-musl": "2.6.0", + "@parcel/watcher-linux-arm64-glibc": "2.6.0", + "@parcel/watcher-linux-arm64-musl": "2.6.0", + "@parcel/watcher-linux-x64-glibc": "2.6.0", + "@parcel/watcher-linux-x64-musl": "2.6.0", + "@parcel/watcher-win32-arm64": "2.6.0", + "@parcel/watcher-win32-x64": "2.6.0" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz", + "integrity": "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz", + "integrity": "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz", + "integrity": "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz", + "integrity": "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz", + "integrity": "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz", + "integrity": "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz", + "integrity": "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz", + "integrity": "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz", + "integrity": "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz", + "integrity": "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz", + "integrity": "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz", + "integrity": "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.8", + "resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz", + "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.4.0", + "resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true + }, + "node_modules/@types/lodash": { + "version": "4.17.25", + "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.25.tgz", + "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.41.tgz", + "integrity": "sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg==", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/shared": "3.5.41", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.41.tgz", + "integrity": "sha512-oKacVfNglLvGjnS6BXOlGL7EyG2h8X03pqXCjzotRZUaXGjbrTJUnVAQjrCqUnS+lyu31nwQjZY/d817GmCnfw==", + "dependencies": { + "@vue/compiler-core": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.41.tgz", + "integrity": "sha512-XJhip7R2wy6vX3knCxdZN4KracFaZUef58s1KYewqluedHIJaPIVfXoYT7MF1F8nCvv6k8bWWxDC8opMkg1VTQ==", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/compiler-core": "3.5.41", + "@vue/compiler-dom": "3.5.41", + "@vue/compiler-ssr": "3.5.41", + "@vue/shared": "3.5.41", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.19", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.41.tgz", + "integrity": "sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A==", + "dependencies": { + "@vue/compiler-dom": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.41.tgz", + "integrity": "sha512-rznsqKM0np0x18EjzF8x88MpEhdNsffbvFbckLL5+oUKz1BxAImEmO7J1ArRYSyo6aQaVoBDp7jEkT91OOxydA==", + "dependencies": { + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.41.tgz", + "integrity": "sha512-Vcry58hiAKwGen9Z1jUZE0feFsNArPCMOImYI8el48A9Idf6DuQYD0U05zZIF2Iad1hGhPSvcbBbAOhNr55fhg==", + "dependencies": { + "@vue/reactivity": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.41.tgz", + "integrity": "sha512-3vVBahVBS9+U6cmXBLyb8nE6/yYo4J/CGI9eVFs3KiMc0YHuudwKyShTD65jtJy/L9PUUxNAFu4cj4LiJ0UFbw==", + "dependencies": { + "@vue/reactivity": "3.5.41", + "@vue/runtime-core": "3.5.41", + "@vue/shared": "3.5.41", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.41.tgz", + "integrity": "sha512-n6hx/pNFfbD6SuyeuMVkvqox8bwf/ET9JlA/kAz/imw8sw++wkqKe2mHX5KutjPpbKE4Z56yTHszoOjGMI9igQ==", + "dependencies": { + "@vue/compiler-ssr": "3.5.41", + "@vue/runtime-dom": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.41.tgz", + "integrity": "sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==" + }, + "node_modules/@vueuse/core": { + "version": "14.3.0", + "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-14.3.0.tgz", + "integrity": "sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "14.3.0", + "@vueuse/shared": "14.3.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/metadata": { + "version": "14.3.0", + "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-14.3.0.tgz", + "integrity": "sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "14.3.0", + "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-14.3.0.tgz", + "integrity": "sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.19.0", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.19.0.tgz", + "integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==" + }, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/element-plus": { + "version": "2.14.4", + "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.14.4.tgz", + "integrity": "sha512-vMKR9tFcLeNrJgFXA3zhUn6YuRKUQW9d0btakBR8U1Iq8MfzkjMOGcgs5de2VgiLldHt69brmuBHpxc3bK4ZgQ==", + "dependencies": { + "@ctrl/tinycolor": "^4.2.0", + "@element-plus/icons-vue": "^2.3.2", + "@floating-ui/dom": "^1.8.0", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.8", + "@types/lodash": "^4.17.24", + "@types/lodash-es": "^4.17.12", + "@vueuse/core": "14.3.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.20", + "lodash": "^4.18.1", + "lodash-es": "^4.18.1", + "lodash-unified": "^1.0.3", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0", + "vue-component-type-helpers": "^3.3.9" + }, + "peerDependencies": { + "vue": "^3.3.7" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/exsolve": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.1.1.tgz", + "integrity": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/immutable": { + "version": "5.1.9", + "resolved": "https://registry.npmmirror.com/immutable/-/immutable-5.1.9.tgz", + "integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true + }, + "node_modules/local-pkg": { + "version": "0.5.1", + "resolved": "https://registry.npmmirror.com/local-pkg/-/local-pkg-0.5.1.tgz", + "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "dev": true, + "dependencies": { + "mlly": "^1.7.3", + "pkg-types": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==" + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmmirror.com/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "optional": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==" + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmmirror.com/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmmirror.com/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ] + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/readdirp": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "dev": true, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.62.4.tgz", + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.4", + "@rollup/rollup-android-arm64": "4.62.4", + "@rollup/rollup-darwin-arm64": "4.62.4", + "@rollup/rollup-darwin-x64": "4.62.4", + "@rollup/rollup-freebsd-arm64": "4.62.4", + "@rollup/rollup-freebsd-x64": "4.62.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", + "@rollup/rollup-linux-arm-musleabihf": "4.62.4", + "@rollup/rollup-linux-arm64-gnu": "4.62.4", + "@rollup/rollup-linux-arm64-musl": "4.62.4", + "@rollup/rollup-linux-loong64-gnu": "4.62.4", + "@rollup/rollup-linux-loong64-musl": "4.62.4", + "@rollup/rollup-linux-ppc64-gnu": "4.62.4", + "@rollup/rollup-linux-ppc64-musl": "4.62.4", + "@rollup/rollup-linux-riscv64-gnu": "4.62.4", + "@rollup/rollup-linux-riscv64-musl": "4.62.4", + "@rollup/rollup-linux-s390x-gnu": "4.62.4", + "@rollup/rollup-linux-x64-gnu": "4.62.4", + "@rollup/rollup-linux-x64-musl": "4.62.4", + "@rollup/rollup-openbsd-x64": "4.62.4", + "@rollup/rollup-openharmony-arm64": "4.62.4", + "@rollup/rollup-win32-arm64-msvc": "4.62.4", + "@rollup/rollup-win32-ia32-msvc": "4.62.4", + "@rollup/rollup-win32-x64-gnu": "4.62.4", + "@rollup/rollup-win32-x64-msvc": "4.62.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/sass": { + "version": "1.102.0", + "resolved": "https://registry.npmmirror.com/sass/-/sass-1.102.0.tgz", + "integrity": "sha512-NSOyTnaQF7rTAEOtI2fwb386vL+akyiQLBZu8Na7hXCb+umJy0GAqlcMIaqACZ6Z1VgTBS4K9PG6B3IdjHGJsw==", + "dev": true, + "dependencies": { + "chokidar": "^5.0.0", + "immutable": "^5.1.5", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=20.19.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/scule": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/scule/-/scule-1.3.0.tgz", + "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", + "dev": true + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-literal": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/strip-literal/-/strip-literal-2.1.1.tgz", + "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", + "dev": true, + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmmirror.com/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true + }, + "node_modules/unimport": { + "version": "3.14.6", + "resolved": "https://registry.npmmirror.com/unimport/-/unimport-3.14.6.tgz", + "integrity": "sha512-CYvbDaTT04Rh8bmD8jz3WPmHYZRG/NnvYVzwD6V1YAlvvKROlAeNDUBhkBGzNav2RKaeuXvlWYaa1V4Lfi/O0g==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.1.4", + "acorn": "^8.14.0", + "escape-string-regexp": "^5.0.0", + "estree-walker": "^3.0.3", + "fast-glob": "^3.3.3", + "local-pkg": "^1.0.0", + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "pathe": "^2.0.1", + "picomatch": "^4.0.2", + "pkg-types": "^1.3.0", + "scule": "^1.3.0", + "strip-literal": "^2.1.1", + "unplugin": "^1.16.1" + } + }, + "node_modules/unimport/node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "dev": true + }, + "node_modules/unimport/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/unimport/node_modules/local-pkg": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/local-pkg/-/local-pkg-1.2.1.tgz", + "integrity": "sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==", + "dev": true, + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.3.0", + "quansync": "^0.2.11" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/unimport/node_modules/local-pkg/node_modules/pkg-types": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", + "dev": true, + "dependencies": { + "confbox": "^0.2.4", + "exsolve": "^1.0.8", + "pathe": "^2.0.3" + } + }, + "node_modules/unplugin": { + "version": "1.16.1", + "resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-1.16.1.tgz", + "integrity": "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==", + "dev": true, + "dependencies": { + "acorn": "^8.14.0", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/unplugin-auto-import": { + "version": "0.17.8", + "resolved": "https://registry.npmmirror.com/unplugin-auto-import/-/unplugin-auto-import-0.17.8.tgz", + "integrity": "sha512-CHryj6HzJ+n4ASjzwHruD8arhbdl+UXvhuAIlHDs15Y/IMecG3wrf7FVg4pVH/DIysbq/n0phIjNHAjl7TG7Iw==", + "dev": true, + "dependencies": { + "@antfu/utils": "^0.7.10", + "@rollup/pluginutils": "^5.1.0", + "fast-glob": "^3.3.2", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.10", + "minimatch": "^9.0.4", + "unimport": "^3.7.2", + "unplugin": "^1.11.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@nuxt/kit": "^3.2.2", + "@vueuse/core": "*" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + }, + "@vueuse/core": { + "optional": true + } + } + }, + "node_modules/unplugin-vue-components": { + "version": "0.26.0", + "resolved": "https://registry.npmmirror.com/unplugin-vue-components/-/unplugin-vue-components-0.26.0.tgz", + "integrity": "sha512-s7IdPDlnOvPamjunVxw8kNgKNK8A5KM1YpK5j/p97jEKTjlPNrA0nZBiSfAKKlK1gWZuyWXlKL5dk3EDw874LQ==", + "dev": true, + "dependencies": { + "@antfu/utils": "^0.7.6", + "@rollup/pluginutils": "^5.0.4", + "chokidar": "^3.5.3", + "debug": "^4.3.4", + "fast-glob": "^3.3.1", + "local-pkg": "^0.4.3", + "magic-string": "^0.30.3", + "minimatch": "^9.0.3", + "resolve": "^1.22.4", + "unplugin": "^1.4.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@babel/parser": "^7.15.8", + "@nuxt/kit": "^3.2.2", + "vue": "2 || 3" + }, + "peerDependenciesMeta": { + "@babel/parser": { + "optional": true + }, + "@nuxt/kit": { + "optional": true + } + } + }, + "node_modules/unplugin-vue-components/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/unplugin-vue-components/node_modules/local-pkg": { + "version": "0.4.3", + "resolved": "https://registry.npmmirror.com/local-pkg/-/local-pkg-0.4.3.tgz", + "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/unplugin-vue-components/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/unplugin-vue-components/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.41.tgz", + "integrity": "sha512-2laE0p+aK+/AOPG/XL/WepOs/GlK755LJ1XECi9kDUrz1FKNw8rb2Xzlw9JS1rqEV55nb0ttsKxVlTCcd+R5cg==", + "dependencies": { + "@vue/compiler-dom": "3.5.41", + "@vue/compiler-sfc": "3.5.41", + "@vue/runtime-dom": "3.5.41", + "@vue/server-renderer": "3.5.41", + "@vue/shared": "3.5.41" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-component-type-helpers": { + "version": "3.3.9", + "resolved": "https://registry.npmmirror.com/vue-component-type-helpers/-/vue-component-type-helpers-3.3.9.tgz", + "integrity": "sha512-3c/UfMe0SqyEfcGTyH7mfshHagJ9QTCbppCb0/uGpHZpFug7+If3GeGZN7I0YheKEExemx3xldQPoO7PQSOLQg==" + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmmirror.com/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + } + } +} diff --git a/ry-vue3/package.json b/ry-vue3/package.json new file mode 100644 index 0000000..6295c3d --- /dev/null +++ b/ry-vue3/package.json @@ -0,0 +1,26 @@ +{ + "name": "ry-vue3", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@element-plus/icons-vue": "^2.3.0", + "axios": "^1.6.0", + "element-plus": "^2.4.0", + "pinia": "^2.1.0", + "qrcode": "^1.5.4", + "vue": "^3.4.0", + "vue-router": "^4.2.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.0", + "sass": "^1.69.0", + "unplugin-auto-import": "^0.17.0", + "unplugin-vue-components": "^0.26.0", + "vite": "^5.0.0" + } +} diff --git a/ry-vue3/public/images/id-back.png b/ry-vue3/public/images/id-back.png new file mode 100644 index 0000000000000000000000000000000000000000..44c5124f03ffb495baed4b7ac22b93e9861959c8 GIT binary patch literal 3915 zcmb7HXH*mI))hh^QezMZ)ewV}fb?FBp@T}15~@^Dq@xmw7&<{ZQU#Q1DAF`Slp+B_ z^94mvkRCvk77(Q<<%_=Se(V0dKW1jl%syw%v)6vkoQ*fXV#vWNz-kJN}& zIgE#K!nB;RLQIU1P57$ou=R0=*cydph&5re{{60;%csdA%H$+JYl)bVPkJc|)a(d4 zMe_FxZ+v=uthV#{`OA>CpUK)gyIHBr*t?&0Qio@k3-cSFQ5SZ0&O@3#Nr-Y5TT@d~ z!tK>BDFptjxl>0++0>PFLe7^JLpb;GSV@U{OHMW)xH)9nKEp${y7PwHV?X`~R%Q2< z%-1hFY)AE9b4oQ{Mv!s4jyK&)Qrb=?;8*QN>sx6eOt;uS`)wyf~g183`&Y zFSnf3$*<6L5a}Z4RmO5I7~h6E5yGTlltmQkd%PVl*gXh?N|x+o zbh~tteq+*rgIvh?K1t)ozp zI8TYw6)WIW4)}LSlDq}h%KBOkIF={Jb=^ z07WT=!<9hChbr>&q6-TP&YcUH9twEk&1cV^MROq6*VmofDk^}tkiEa5{QMVv!-4}J z`+5z5j!=|T;oSG{Eoy&u(hz*-(?_F$aA9H0`<5?5MMz&?UxIbF1)C9r7$ccEWS$H( zuCQ$3W!D{vY%dB?HS67bxc4t7Gnmho$cB7)AVU2I=q#L;q4!2A@YgZ&j{&eXCynjzulE`;@JN351UXXUOU{wZFwlOv(Lqy zV1rT^uSuQ_dXT&;-Alwa;+Q&(R};>ZK;HQx12T&J3pGPHi7pND`%sHVBKLF6ocq(| z#{FpzN8gt@%R-n0b%}t?PLU<*KsMxQ3172(vvHh|ZxYT5$C24p5ESsWTSQz!WpRId zx$H}f%Rt?(-d!4nhjzSU0>+}-&LBGp!SMHbL78{*w@5LV;)iF zdG1pv!(k<^y!VG5H+oSI&e<*xg_LhUkTrkMNMXnSjY5^Ui;SDkm?zH9ZA$V9>U4FQ zH{`;chkJes=Z${f2^Hk`rE!^Ro?B?;m@%wXb~d7meQ^P)wG4}E;Ok@_yfx?8>tYpc zNY+$aKEe3`>kv10%o%Z}l|#pXI=ks#_R#T_Rb~wh^-O0ouJ_TbHRXLJ2qbv0wLrL( z*4Bl=X)BHgOthJ;@r*-*8#{*!lm~z3-#60N3i0OMwQ(YgRE~4^PoBRm7`GY84{=LB z)V4cqV0+K(oL>tA@$ zZ#iID6H>3%KBE3YnWj~3zzTBux|%&ojw6ia01=7ljO6$}%7c`?-gxExz2G{p{+<9t zgtmPdFxGH-xZcTOI|fhRB4}14F@>z43p)7x_;tQs95}&F$Zsr@oihK;oUenN8-bkmCW}?I-}k(+j@Rktbh84lQ< znY@Y7Y7D%G-F^k8i#R{2D--?NPjNuxlUt(d%Wsc?{%M_3t1qci#chdnb&#=2MTTRC z*jJ$88JWSC>(q^DP0w+Cj@fVz$9u#+)CH+VDgjvDf@ax=g4abykPtV$dnO@AV;u zgT1W=`4bIVo^9_Q5h|~KZAJP7(C(5D2WAst@)6Q05O92qC_d?r8UUHUI|KRi(>KRN z1ZCc(iz^BK&IGG^zi+@XS;XPq_Z;sdF5TJN~nUwc8my{#wGU2lK@rMU3wa>PGZ}9MGqD$Y1`iiyf zi2c!uoRZYC&rG$@TdFw1Zz|R#aZjo+Jh=>f{7v{nn|X3_d9!WaULJPE_%Qf|B%>wU z(vJO-8^9shOan0%U&K}-64;ExpwL`>ZMV8LQxNQ~7w)npN|=nnqh=|+4!~+Ob5b; zE~s%;RfMsD!ntcD#G-IaQ&=QWdY$vRQHLT;HlWrMBo@jvMW2J)28k(t)K}PBnS`PV29N7sVRZax&dy>Ag>vR-<>exZR6VH+X@M1yo0`nMeh%}(tBC+Q z_4K!LazfIPYuZD!iL=75XSkR6CP%>`^9s;|oX={FIK9|;my>|5PHHbM*^$*?_2_V< z{dY`X3>k3WeK5DbJ5#K&@D}DTGqF+gE?a{fMh1gXfRSHYk^P005ME|@=(=*_s{Fyf zoRi-KH0<%P8u9L6iT8e0A=X&aY@unwkBC|vNte|_> ztI$siHjl6!QFD8E5p{SNbCgD;i;y_{tI^JP4{ZpQ>IVQdoqk<727+mz(tKy`|Le=4MhedP6qWj&#^cN7;U7I z*k>w1iM=UImQVDQK(m2F5de%cVpc?(51>cSz$ zsAN2nK1qxHF$ou^@2vWDE0|sj^*qB%Q4(wJxQ|CbCd|H=H#gIO($8<-7<9)#KRldj zj}q^`?5W8?4^;nS6I|#cJJWx%{J+)66G(55@@jyGyD)Wp4;L2Dh)}8TLzq4~8 zb!^bWEcniAzv_=#DH-`@UG^w;LKxBEYsY&ty#pVBXF zg09~`S$Aoq8yJ|?VagIKeuFg7Z!BKN{1kuhmFK49XmXY5#^)pFONfu9_Qn7Dt2JP( z6e}LsJxXcW$W^`j zD0L+iu@3X5jEc2>bx$qX-(F?BJ(!c$kVidMC{3DmYCJ7LH_0cA^{?pFp zgc8~jL8^e#J4BiUP`;OYzw_s=b=Und`(5ju+0*vSvu5Iq46d=j_+ShS3@m!O7!%-2 z1&9D;0?y4|Weo-fR$V=e#?8R2wF%3ooH#ya+D7InDAiB583(Tvd&S0~x?fI>oH_ea z0(p|cs37K2(YktN72LEs5bW?;F3HbUCFaIg%#9J}$2D#Z?xJze32}UQM@~8R)_bDh z7C2S@`o^!aH@9Eryl?7N8^lc<*?PY*sD|lghvw>MFXJUmM;d~v1&6v?AqfxhNyU%e zXlXsOBuFH~GF9Bm!C2*v0^&-d;1Mz8*YpKfMO3ulrTH!z&0mu9edWNuJSyeBACuFQ zWexL+S>B1pgQ{#Uu(yc7rE>z(t5uKSP-iO&2sD__on#HwQ$R&VhL(GM{qEC>M0C}@ zVPt$4TjIXa@A9{T0%^eJr2~s?aPXhvkX?c5^RGYPN9z2dpFZ;>891b78!RnXguz)S zJ&){4v$@SL$f&Jy70sA&3suNmk8Yhmeb>6r5x-Vx^7Y#RG=y~-6Yc&jt!rc?A5X~E zLHk=eC#`?0p|D~EJ2z>nmwHGX_UOmh6t+qv;xz{xYRvVYGysm^!O~p<&p(1SDF_xzU zQRti>J*#~wm`4=F7EiSWezX-YQ#csFnrqFHV9gZj|2j-^r>5)=MuWE&KBrGk+7OLl zS^ik`qr)xXwL1^FkNy}ct_&xC9=_(yo@n|Oqoad)w68b2I_e-;mRfOSBir~@YJRgj z-mgI?^@20{P4^9S-`=MKgIU_tG<%*)`|7CZr+pl$#47GPMq)Yzlp#9+BDIen^s0&q zKfJjrscBYs(`jefAw#*_2I}bGpxNHhF?UGc@?IbRxF}GVrEIdazMgef_Ppt3J5FIU_$@!lB}9k%uC+#V)AB@oQ-1gOBYD-lVfySKwR5|6K3 zKherJu=?wJdX8LE`Ckv%j6Lf>aY0a6aQv4qUy3?tCqj9)=6mD+*f4sHNrHIkpFaVi z&s!Z?F4>IVsphT}(wYn@qkoL{46JF|wRq0&QJ&oXssnvNGs9VntWWy!BYnW&r0#pW z&ENYgQ}f94(P~}2s`1-9qk8U0YLetA6xHE0!dPw?-O|rW~Qax6X%VY zoNeci|BKFNkaL-rrL4c)jvV3gXgbc6>5SIy1`mDPw3Dmn1{2enlM&%z1t+G^x|i>F zg*jn_DC{d6a;zV3_f{mwXRG;$Nx53odgYZl+r5SB#*a#+HXM5UFYxcQG|aBozm0}8 zG6z3b3d)KH%v->df0i{>VEKy>Nx7kaMpr64)V(|*`k#c3nJ!0v4q1swDZP3Y_m8ME zms=&lCc-kf{R023vXw}-k|AWhopEQC^1Ub0RxzOBo&&BdS9FE1c;iZ}u+!g}o))F| zYaO4~cVq-~2#WsxZF4la+!m%Javns|qmT30AgUeDd;f*VWTyUjB~Wv+&iYb_hcO(_I@x=sF&pqEE@_@P!)HNwYE~K)bc$+zuP8rYGU!Xcr^|~_JE7@t;C-k+LoeH3 zT{$26`_;CUfArv+o^21W;e}lN&)+;P-ESjU>s|2OSH_Q;Y0D+0E(BY@)W#&%D6q)S zX4king1MZ~rds`)NIO>nLnyZ!)hBpHC*-WUewqv^_xyYcNDt({*tC;|yQn`Ni5_c& zf|JC{8bmkOYVb=kg`kXbrjYsf^(ASnOlRceOIm2}Qm2_S69g-Tm&i02rDP;yXT{m! zrvov%O-(oF!CoGQ4P6ZrdZr!DeP~WAa$g-+9;TWd-Z}YL6}IK%QdDg@y3shCHaBlM z7a^WoCA_4;7Lfz#e_4|M3oSPdw8EOL@=pkwF z&~;D8o{5q1=M(Oh@WiMe_g%PrfOc_h!zb%c25QxX9)7v|=Z7_iNU^03qr>oLoh(Q1 zN27}Lx!rGTT%TwG6)s;yi9vG!2K;>s{b03GDt$PNVO|t@n=l!Wn@x|NsF%o510ZCJ zD>3Nvm3vozcln?ZtWi$B-P{|QJnD~<^dyg~;0j|z4p{xMXAmnC1Nyjw&oFBm|NF3c z$lvWg8-RF<+0VlE0(bM1;u-tDU-e7BtVbuPJs9#-uKkuUScC41&k{h=_!7~L%oo;D z!g_Az6V3L2|LQ+jCgQhFO_j&v-S#9zP#B5utfn;;dfR84VwcHqj>ka45D#Ylx`L49 zeDH^}iCrqHIdnH}XJsyBqO7@p<2ppZqAg1i6%O%mGTsR1U<_tUF_Mkqw~aNbyG3MQ zR5oJt=?a5zTttQ4X_Tw)k;0|K7U3yumzm?NpisMhp$TEqBt(^iNNVTnCDTZ+b3CD9 zK%+yFqTSaMNw^YSJ+zdD(Q2@zJ4|fqMQCA4t1 zW6j~Z^xR^2jMb2Q>1~$IgkJbtd0+7M#l?p&XH!5Yp+1NnZHwXt1t!+V)^Cf##mP1$ zEio*(nu7FDXGX>d!s9vGfgN3NEB(w+bG~5Hp7P5d$JY!urX-v2SOt_n;kAzCjfPAf zmMg_yrVrpVLJaZ>*G|g?Ot;1@wmY6%)S!LOS9JxT_YA6p$**q<1&y^9C!W>1fZzZc zsu%*Gn1jnWRIhk5^lh4$aDg`vs1wT@(;SIb`M17iDFxecno4F$>BRMHo5#HMZB2?Ju;AtV*hS6 zSX5Asn*w}?|10DZsxV_zU+xK(4>0PTvB&{GHaTM1E>qD zC?DsC#%Kk0l${Vp#dySW)2gd}^Er_>(Vm0(D^YIZC3Tu(VNh&$PNe{M#6b`Qh{&>d(1ocGTxFf3Z zhFy-)is0|`Opm0kTQ;uM`(}}?)gQ*%`r^OB98V)Ir0^XZrpTY7l&Tm>qh_S>)X{KI zrznJ#^5NgC7r#F)Jn6`y6sYNu)0{~q@gjI``;nL+T^0bmxm_aj(yZCK1HGyIf)>@9 zbisWXpzvJ89g49xOiLP0k}9q$JQFY9+G0e60bZS&3%ruViI7=vFjr;6ZuE%*wNZu4 z;UW8+Rx@>ts~Pi8l5s$9GR*9y+jLF*e!dxonQ9*9``T2K$P3ranRTnh@g=&b@lf6g zM1zru4dhC`z}u&pSA#jb-{BPPrSU;g>gHzar7b|R6ka@grAqFT?(VZJu@tWeuFkGH z2e`mwKfH0T%sId&qRgsxE^V6B2w;~YoN{hlx)bXeVdL2QviMryx7_2&piuds>Z`)g zjTogE z)ZLr*sxnx906n4>PNR}bt}Uqr-weJ=$J8YV*YF%t)LGnk$eZp;CD<^pS5*ptXMr|g z6UIhZQ>?nw`9<^ct=+@TIYpA2Ip-5>qxM-S^S_nxNTKEs$P|xlnV|t+LW0ghVc(uO zFa!n1Yz>+I5GdsRM=%|a?vMFLHC)$24S-FAGWmavmH$kht_h44)|i|Rnki`n?nIuo zMeE05G8Ghic5|0#iK^4@>P#4or6u7p1H+qgC&3}Y zU(`bqEC@yUoFCjRr5qifSt$E61@v#!$U2v1a~2CtPxvll(xS7|q6hKkOw^0~?>(%( zetorSK$4qdX^GAg!m$jAF15Tx-P_xcg)#g3tvykZw(yo+*Iq8%&)8A<*AnFhBavAp z2pSs-O5492(Eh70q&pJSod!E^SoqnaC1zg2sjB_MziQVfA6|q(F(SM|lOF1we*PB1 zOi_yOr^3uWi|D6aj`SKC-Codmt)`dfm7j|9AG>pw7p%#9>kc|0@0_LDtB_wp6~;{O0 + + + + diff --git a/ry-vue3/src/api/auth.js b/ry-vue3/src/api/auth.js new file mode 100644 index 0000000..5151855 --- /dev/null +++ b/ry-vue3/src/api/auth.js @@ -0,0 +1,21 @@ +import request from '@/utils/request' + +export function login(data) { + return request({ url: '/login', method: 'post', data }) +} + +export function logout() { + return request({ url: '/logout', method: 'post' }) +} + +export function getInfo() { + return request({ url: '/getInfo', method: 'get' }) +} + +export function getCaptcha() { + return request({ url: '/captchaImage', method: 'get' }) +} + +export function getRouters() { + return request({ url: '/getRouters', method: 'get' }) +} \ No newline at end of file diff --git a/ry-vue3/src/api/business/person.js b/ry-vue3/src/api/business/person.js new file mode 100644 index 0000000..c5e7fc7 --- /dev/null +++ b/ry-vue3/src/api/business/person.js @@ -0,0 +1,19 @@ +import request from '@/utils/request' + +export function listBizPerson(query) { + return request({ url: '/business/person/list', method: 'get', params: query }) +} + +/** + * sponsor 端专属人员列表 (后端按当前主账号 + 子账号团队强制隔离) + * 后端返回 TableDataInfo 风格 {total, code, msg, rows}, 经 request.js 拦截器包装为 {code, msg, data: {total, rows}} + * 这里显式解包, 让调用方直接读 res.rows / res.total + */ +export async function listSponsorPerson(query) { + const { data } = await request({ url: '/business/person/sponsorList', method: 'get', params: query }) + const payload = data?.data || data + return { + rows: payload?.rows || [], + total: payload?.total || 0 + } +} diff --git a/ry-vue3/src/api/business/project.js b/ry-vue3/src/api/business/project.js new file mode 100644 index 0000000..7e2277a --- /dev/null +++ b/ry-vue3/src/api/business/project.js @@ -0,0 +1,32 @@ +import request from '@/utils/request' + +/** + * sponsor 专属项目列表 (团队模式: 主账号+子账号关联的项目) + * 后端返回 TableDataInfo 风格 {total, code, msg, rows}, 经 request.js 拦截器包装为 {code, msg, data: {total, rows}} + * 这里显式解包, 让调用方直接读 res.rows / res.total + */ +export async function listSponsorProjects(query) { + const { data } = await request({ url: '/business/project/sponsorList', method: 'get', params: query }) + // 兼容两种形态: 包装后 {code, msg, data: {total, rows}} / 原始 {total, rows} + const payload = data?.data || data + return { + rows: payload?.rows || [], + total: payload?.total || 0 + } +} + +export function rateProject(data) { + return request({ url: '/business/project/rate', method: 'post', data }) +} + +export function sponsorAssignProject(data) { + return request({ url: '/business/project/sponsorAssign', method: 'post', data }) +} + +/** + * 赞助方批量分配 (多个项目同一个监察员 + 同一份说明) + * body: [{projectId, monitorUserId, assignDesc, assignPoints}, ...] + */ +export function sponsorAssignBatch(data) { + return request({ url: '/business/project/sponsorAssignBatch', method: 'post', data }) +} diff --git a/ry-vue3/src/api/dict.js b/ry-vue3/src/api/dict.js new file mode 100644 index 0000000..3206b0a --- /dev/null +++ b/ry-vue3/src/api/dict.js @@ -0,0 +1,54 @@ +import request from '@/utils/request' + +// 字典类型常量 (前端统一引用,避免拼写错误) +export const DICT_TYPE = { + DEPARTMENT: 'department', // 科室 + TITLE: 'title' // 医生职称 +} + +/** + * 拉取某类型字典的启用项 (匿名可访问) + * @param {string} type - DICT_TYPE.* + * @returns {Promise<{value: number, label: string, raw: object}[]>} + * - value: deptId/titleId + * - label: name + * - raw: 完整字段 (含 sort/status/...) + */ +export async function getActiveDict(type) { + const url = `/business/dict/${type}/active` + const r = await request({ url, method: 'get' }) + const raw = r.data || r || [] + const idField = type === DICT_TYPE.DEPARTMENT ? 'deptId' : 'titleId' + return raw.map(item => ({ + value: item[idField], + label: item.name, + raw: item + })) +} + +// ===== 管理员维护接口 ===== +export function listDepartments(query) { + return request({ url: '/business/dict/department/list', method: 'get', params: query }) +} +export function addDepartment(data) { + return request({ url: '/business/dict/department', method: 'post', data }) +} +export function updateDepartment(data) { + return request({ url: '/business/dict/department', method: 'put', data }) +} +export function deleteDepartments(ids) { + return request({ url: `/business/dict/department/${ids}`, method: 'delete' }) +} + +export function listTitles(query) { + return request({ url: '/business/dict/title/list', method: 'get', params: query }) +} +export function addTitle(data) { + return request({ url: '/business/dict/title', method: 'post', data }) +} +export function updateTitle(data) { + return request({ url: '/business/dict/title', method: 'put', data }) +} +export function deleteTitles(ids) { + return request({ url: `/business/dict/title/${ids}`, method: 'delete' }) +} \ No newline at end of file diff --git a/ry-vue3/src/api/public.js b/ry-vue3/src/api/public.js new file mode 100644 index 0000000..d64e354 --- /dev/null +++ b/ry-vue3/src/api/public.js @@ -0,0 +1,93 @@ +// 公开门户 API +import request from '@/utils/request' + +// 首页:项目公示 + 公示公告 + 资源 +export const getPublicIndex = () => request.get('/business/public/index') + +// 项目公示分页 +export const listPublicProjects = (params) => request.get('/business/public/announcements', { params }) + +// 公示详情 +export const getPublicAnnouncement = (annId) => request.get(`/business/public/announcement/${annId}`) + +// 支持函详情 +export const getPublicSupportLetter = (annId) => request.get(`/business/public/supportLetter/${annId}`) + +// 邀请函详情(参会邀请) +export const getPublicInvitation = (annId) => request.get(`/business/public/invitation/${annId}`) + +// 业务字典 +export const getDictTypes = () => request.get('/business/dict/types') +export const getDictData = (dictType) => request.get('/business/dict/data', { params: { dictType } }) + +// 业务认证 +export const bizLogin = (data) => request.post('/business/auth/login', data) +export const registerExpert = (data) => request.post('/business/auth/registerExpert', data) +export const registerExecutor = (data) => request.post('/business/auth/registerExecutor', data) +export const registerSponsor = (data) => request.post('/business/auth/registerSponsor', data) + +// 业务 CRUD(generic) +// announcement 后端接口未实现, 已改为调 RuoYi 系统通知接口 /system/notice/list +const STUB_ENTITIES = new Set(['announcement']) +export const bizList = (entity, params) => { + if (STUB_ENTITIES.has(entity)) { + return Promise.resolve({ code: 200, msg: 'ok', data: { rows: [], total: 0 } }) + } + return request.get(`/business/${entity}/list`, { params }) +} + +// 系统通知(RuoYi sys_notice 表) +// 返回 {id, title, category, text, time, read, type, content} +export const listNotices = async (params = { pageNum: 1, pageSize: 5 }) => { + const { data } = await request.get('/system/notice/list', { params }) + const rows = data?.rows || [] + return rows.map(n => { + // notice_content 是 longblob HTML, 提取纯文本 + const html = String(n.noticeContent || '') + const text = html.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 200) + return { + id: n.noticeId, + title: n.noticeTitle, + category: n.noticeType === '2' ? '公告' : '通知', + text: text || (n.remark || '请查看通知内容'), + time: n.createTime ? new Date(n.createTime.replace(/-/g, '/')).toLocaleString('zh-CN') : '', + read: !!n.isRead, + type: n.noticeType, + content: html, + } + }) +} + +// 个人通知 (biz_message 表) — 给当前用户发 +// 返回 {rows: [{id, title, text, time, read, type, content, bizType, bizId}], unread, total} +const MSG_TYPE_LABEL = { '1': '通知', '2': '待办', '3': '系统' } +export const listMyMessages = async (params = { limit: 5 }) => { + const { data } = await request.get('/business/message/my', { params }) + const rows = data?.rows || [] + return { + rows: rows.map(m => ({ + id: m.msgId, + title: m.title, + category: MSG_TYPE_LABEL[m.msgType] || '通知', + text: String(m.content || '').replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 200), + time: m.createTime ? new Date(m.createTime.replace(/-/g, '/')).toLocaleString('zh-CN') : '', + read: m.isRead === '1', + type: m.msgType, + content: m.content, + bizType: m.bizType, + bizId: m.bizId, + })), + unread: data?.unread || 0, + total: data?.total || 0, + } +} + +// 标记单条已读 +export const markMessageRead = (msgId) => request.put(`/business/message/read/${msgId}`) +// 全部标记已读 +export const markAllMessagesRead = () => request.put('/business/message/readAll') + +export const bizGet = (entity, id) => request.get(`/business/${entity}/${id}`) +export const bizAdd = (entity, data) => request.post(`/business/${entity}`, data) +export const bizUpdate = (entity, data) => request.put(`/business/${entity}`, data) +export const bizDelete = (entity, ids) => request.delete(`/business/${entity}/${ids}`) diff --git a/ry-vue3/src/api/system.js b/ry-vue3/src/api/system.js new file mode 100644 index 0000000..c8c9a37 --- /dev/null +++ b/ry-vue3/src/api/system.js @@ -0,0 +1,22 @@ +// 系统管理 API (用户列表 / 角色) +import request from '@/utils/request' + +// 用户列表(按 userName 模糊搜索) +export function listUser(query) { + return request({ url: '/system/user/list', method: 'get', params: query }) +} + +// 角色列表 +export function listRole(query) { + return request({ url: '/system/role/list', method: 'get', params: query }) +} + +// 按角色查询用户列表 (绕开 sys_user:list 权限) +// 用法: listByRole({ roleType: 'executor', userName: 'exe' }) +export function listByRole(query) { + return request({ url: '/business/executor/list', method: 'get', params: query }) +} + +// 兼容旧名 +export const listExecutor = (query) => listByRole({ roleType: 'executor', ...query }) +export const listSupporters = (query) => listByRole({ roleType: 'sponsor', ...query }) diff --git a/ry-vue3/src/assets/main.scss b/ry-vue3/src/assets/main.scss new file mode 100644 index 0000000..6ff6471 --- /dev/null +++ b/ry-vue3/src/assets/main.scss @@ -0,0 +1,7 @@ +html, body, #app { height: 100%; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; } +:root { --theme-color: #409eff; --theme-color-light: #66b1ff; --theme-color-dark: #3a8ee6; } +.page-wrap { padding: 16px; } +.page-card { background: #fff; border-radius: 6px; padding: 16px; box-shadow: 0 1px 4px rgba(0,21,41,.08); margin-bottom: 16px; } +.page-title { font-size: 18px; font-weight: 600; margin-bottom: 12px; color: #1f1f1f; } +.filter-bar { margin-bottom: 12px; } +.toolbar { margin-bottom: 12px; display: flex; gap: 8px; } diff --git a/ry-vue3/src/assets/theme.js b/ry-vue3/src/assets/theme.js new file mode 100644 index 0000000..3ae78a8 --- /dev/null +++ b/ry-vue3/src/assets/theme.js @@ -0,0 +1,116 @@ +/** + * 品牌主题色切换工具 + * ---------------------------------------- + * 用法: + * import { setBrand } from '@/assets/theme' + * setBrand('#0f766e') // 改主题色(自动重算变体) + * setBrand('#1E3A8A') // 改回默认 + * setBrand('#1E3A8A', { persist: true }) // 写 localStorage + * applyStoredBrand() // 应用持久化的颜色 + * + * 派生色算法(HSL 空间调整,比简单 hex 加减更稳): + * deep: lightness -= 12 + * darker: lightness -= 22 + * light: lightness += 12 + * lighter: lightness += 35 + * text: lightness += 22 + * mix: 50% alpha + white (用于 focus border) + */ + +const KEY = 'brand-primary' + +/** hex -> {r,g,b} */ +function hexToRgb(hex) { + const h = hex.replace('#', '') + const v = h.length === 3 + ? h.split('').map(c => c + c).join('') + : h + return { + r: parseInt(v.slice(0, 2), 16), + g: parseInt(v.slice(2, 4), 16), + b: parseInt(v.slice(4, 6), 16) + } +} + +/** {r,g,b} -> {h,s,l} (h: 0..360, s: 0..100, l: 0..100) */ +function rgbToHsl({ r, g, b }) { + r /= 255; g /= 255; b /= 255 + const max = Math.max(r, g, b), min = Math.min(r, g, b) + let h = 0, s = 0 + const l = (max + min) / 2 + if (max !== min) { + const d = max - min + s = l > 0.5 ? d / (2 - max - min) : d / (max + min) + switch (max) { + case r: h = (g - b) / d + (g < b ? 6 : 0); break + case g: h = (b - r) / d + 2; break + case b: h = (r - g) / d + 4; break + } + h *= 60 + } + return { h, s: s * 100, l: l * 100 } +} + +/** {h,s,l} -> {r,g,b} */ +function hslToRgb({ h, s, l }) { + s /= 100; l /= 100 + const k = n => (n + h / 30) % 12 + const a = s * Math.min(l, 1 - l) + const f = n => { + const x = l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1))) + return Math.round(x * 255) + } + return { r: f(0), g: f(8), b: f(4) } +} + +/** {h,s,l} -> '#rrggbb' */ +function hslToHex({ h, s, l }) { + const { r, g, b } = hslToRgb({ h, s, l }) + return '#' + [r, g, b].map(v => v.toString(16).padStart(2, '0')).join('') +} + +/** 把 lightness 限制在 8..92 之间 */ +function clampL(l) { return Math.max(8, Math.min(92, l)) } + +/** + * 根据主色算出 7 个派生色 hex + */ +export function derivePalette(primary) { + const hsl = rgbToHsl(hexToRgb(primary)) + return { + '--brand-primary': primary, + '--brand-primary-deep': hslToHex({ ...hsl, l: clampL(hsl.l - 12) }), + '--brand-primary-darker': hslToHex({ ...hsl, l: clampL(hsl.l - 22) }), + '--brand-primary-light': hslToHex({ ...hsl, l: clampL(hsl.l + 12) }), + '--brand-primary-lighter': hslToHex({ ...hsl, l: clampL(hsl.l + 35) }), + '--brand-primary-text': hslToHex({ ...hsl, l: clampL(hsl.l + 22) }), + '--brand-primary-mix': hslToHex({ ...hsl, l: clampL(hsl.l + 45) }) + } +} + +/** + * 设置 :root 上 7 个 CSS 变量 + * @param {string} primary 主色 hex + * @param {object} [opts] { persist: bool } 是否写 localStorage + */ +export function setBrand(primary, opts = {}) { + const palette = derivePalette(primary) + const root = document.documentElement + for (const [k, v] of Object.entries(palette)) root.style.setProperty(k, v) + if (opts.persist) localStorage.setItem(KEY, primary) +} + +/** + * 应用持久化在 localStorage 的颜色(无则保持现状) + */ +export function applyStoredBrand() { + const stored = localStorage.getItem(KEY) + if (stored) setBrand(stored) +} + +/** + * 当前 :root 的主色(只读) + */ +export function getBrand() { + return getComputedStyle(document.documentElement).getPropertyValue('--brand-primary').trim() +} \ No newline at end of file diff --git a/ry-vue3/src/assets/theme.scss b/ry-vue3/src/assets/theme.scss new file mode 100644 index 0000000..005f0c9 --- /dev/null +++ b/ry-vue3/src/assets/theme.scss @@ -0,0 +1,39 @@ +/* ============================================================= + 品牌主题色(Brand Theme) + ------------------------------------------------------------ + 所有色值都是 HSL/HEX 函数动态计算的派生色。 + 改 :root 里的 --brand-primary 即可全局换色。 + + 派生色算法(hex → HSL → 调整亮度/饱和 → 转回 hex): + --brand-primary 主色 + --brand-primary-deep 暗 12% (hover / active / 标题强调) + --brand-primary-darker 暗 22% (顶部导航 / 深底) + --brand-primary-light 浅 22% (背景 / disabled) + --brand-primary-lighter 浅 55% (hover-bg / 选中态) + --brand-primary-text 浅 35% (链接默认) + --brand-primary-mix 50% 透明 + 白 (输入框 focus border) + + Element Plus 同步:main.js 在启动时读取 --brand-primary + 并通过 ElMessage / ElButton 的 --el-color-primary 联动。 + ============================================================= */ + +:root { + --brand-primary: #1E3A8A; + --brand-primary-deep: #152a66; + --brand-primary-darker: #0d1d4d; + --brand-primary-light: #3a55a3; + --brand-primary-lighter: #c1cbe6; + --brand-primary-text: #4f6dbf; + --brand-primary-mix: #8e9ad1; +} + +/* Element Plus 联动:把品牌色同步给 Element Plus 内置变量。 + 这样所有 等都跟主题色。 */ +:root { + --el-color-primary: var(--brand-primary); + --el-color-primary-light-3: var(--brand-primary-light); + --el-color-primary-light-5: var(--brand-primary-lighter); + --el-color-primary-light-7: var(--brand-primary-lighter); + --el-color-primary-light-9: var(--brand-primary-lighter); + --el-color-primary-dark-2: var(--brand-primary-deep); +} \ No newline at end of file diff --git a/ry-vue3/src/components/AreaCascader.vue b/ry-vue3/src/components/AreaCascader.vue new file mode 100644 index 0000000..967f383 --- /dev/null +++ b/ry-vue3/src/components/AreaCascader.vue @@ -0,0 +1,152 @@ + + + + + \ No newline at end of file diff --git a/ry-vue3/src/components/DictSelect.vue b/ry-vue3/src/components/DictSelect.vue new file mode 100644 index 0000000..657862d --- /dev/null +++ b/ry-vue3/src/components/DictSelect.vue @@ -0,0 +1,92 @@ + + + + + \ No newline at end of file diff --git a/ry-vue3/src/components/DoctorDeptSelect.vue b/ry-vue3/src/components/DoctorDeptSelect.vue new file mode 100644 index 0000000..1f6e502 --- /dev/null +++ b/ry-vue3/src/components/DoctorDeptSelect.vue @@ -0,0 +1,55 @@ + + + + + \ No newline at end of file diff --git a/ry-vue3/src/components/DoctorTitleSelect.vue b/ry-vue3/src/components/DoctorTitleSelect.vue new file mode 100644 index 0000000..cb6f5de --- /dev/null +++ b/ry-vue3/src/components/DoctorTitleSelect.vue @@ -0,0 +1,55 @@ + + + + + \ No newline at end of file diff --git a/ry-vue3/src/components/IdCardUploader.vue b/ry-vue3/src/components/IdCardUploader.vue new file mode 100644 index 0000000..107aa2a --- /dev/null +++ b/ry-vue3/src/components/IdCardUploader.vue @@ -0,0 +1,140 @@ + + + + + + \ No newline at end of file diff --git a/ry-vue3/src/components/OssImageUploader.vue b/ry-vue3/src/components/OssImageUploader.vue new file mode 100644 index 0000000..bfc0da1 --- /dev/null +++ b/ry-vue3/src/components/OssImageUploader.vue @@ -0,0 +1,120 @@ + + + + + + \ No newline at end of file diff --git a/ry-vue3/src/components/Preview.vue b/ry-vue3/src/components/Preview.vue new file mode 100644 index 0000000..4262c23 --- /dev/null +++ b/ry-vue3/src/components/Preview.vue @@ -0,0 +1,72 @@ + + + + + \ No newline at end of file diff --git a/ry-vue3/src/views/doctor/Messages.vue b/ry-vue3/src/views/doctor/Messages.vue new file mode 100644 index 0000000..3f851c7 --- /dev/null +++ b/ry-vue3/src/views/doctor/Messages.vue @@ -0,0 +1,106 @@ + + + + + \ No newline at end of file diff --git a/ry-vue3/src/views/doctor/Projects.vue b/ry-vue3/src/views/doctor/Projects.vue new file mode 100644 index 0000000..75fad4e --- /dev/null +++ b/ry-vue3/src/views/doctor/Projects.vue @@ -0,0 +1,150 @@ + + + + + \ No newline at end of file diff --git a/ry-vue3/src/views/doctor/SubmissionDetail.vue b/ry-vue3/src/views/doctor/SubmissionDetail.vue new file mode 100644 index 0000000..014ed27 --- /dev/null +++ b/ry-vue3/src/views/doctor/SubmissionDetail.vue @@ -0,0 +1,193 @@ + + + + + \ No newline at end of file diff --git a/ry-vue3/src/views/doctor/SubmissionNew.vue b/ry-vue3/src/views/doctor/SubmissionNew.vue new file mode 100644 index 0000000..de2cb5a --- /dev/null +++ b/ry-vue3/src/views/doctor/SubmissionNew.vue @@ -0,0 +1,280 @@ + + + + + \ No newline at end of file diff --git a/ry-vue3/src/views/doctor/Submissions.vue b/ry-vue3/src/views/doctor/Submissions.vue new file mode 100644 index 0000000..33dd886 --- /dev/null +++ b/ry-vue3/src/views/doctor/Submissions.vue @@ -0,0 +1,177 @@ + + + + + \ No newline at end of file diff --git a/ry-vue3/src/views/executor/Account.vue b/ry-vue3/src/views/executor/Account.vue new file mode 100644 index 0000000..0aada3f --- /dev/null +++ b/ry-vue3/src/views/executor/Account.vue @@ -0,0 +1,76 @@ + + + + + \ No newline at end of file diff --git a/ry-vue3/src/views/executor/Labor.vue b/ry-vue3/src/views/executor/Labor.vue new file mode 100644 index 0000000..8540668 --- /dev/null +++ b/ry-vue3/src/views/executor/Labor.vue @@ -0,0 +1,215 @@ + + + + + \ No newline at end of file diff --git a/ry-vue3/src/views/executor/Meetings.vue b/ry-vue3/src/views/executor/Meetings.vue new file mode 100644 index 0000000..da2401d --- /dev/null +++ b/ry-vue3/src/views/executor/Meetings.vue @@ -0,0 +1,197 @@ + + + + + \ No newline at end of file diff --git a/ry-vue3/src/views/executor/Overview.vue b/ry-vue3/src/views/executor/Overview.vue new file mode 100644 index 0000000..860bbee --- /dev/null +++ b/ry-vue3/src/views/executor/Overview.vue @@ -0,0 +1,84 @@ + + + + + \ No newline at end of file diff --git a/ry-vue3/src/views/executor/People.vue b/ry-vue3/src/views/executor/People.vue new file mode 100644 index 0000000..c7959d2 --- /dev/null +++ b/ry-vue3/src/views/executor/People.vue @@ -0,0 +1,198 @@ + + + + + \ No newline at end of file diff --git a/ry-vue3/src/views/executor/Projects.vue b/ry-vue3/src/views/executor/Projects.vue new file mode 100644 index 0000000..ba148cb --- /dev/null +++ b/ry-vue3/src/views/executor/Projects.vue @@ -0,0 +1,275 @@ + + + + + \ No newline at end of file diff --git a/ry-vue3/src/views/leader/Account.vue b/ry-vue3/src/views/leader/Account.vue new file mode 100644 index 0000000..b378ae7 --- /dev/null +++ b/ry-vue3/src/views/leader/Account.vue @@ -0,0 +1,80 @@ + + + + + \ No newline at end of file diff --git a/ry-vue3/src/views/leader/Home.vue b/ry-vue3/src/views/leader/Home.vue new file mode 100644 index 0000000..87d67bb --- /dev/null +++ b/ry-vue3/src/views/leader/Home.vue @@ -0,0 +1,154 @@ + + + + + \ No newline at end of file diff --git a/ry-vue3/src/views/leader/Meetings.vue b/ry-vue3/src/views/leader/Meetings.vue new file mode 100644 index 0000000..8fcd614 --- /dev/null +++ b/ry-vue3/src/views/leader/Meetings.vue @@ -0,0 +1,287 @@ + + + + + + diff --git a/ry-vue3/src/views/leader/Projects.vue b/ry-vue3/src/views/leader/Projects.vue new file mode 100644 index 0000000..8eac5a0 --- /dev/null +++ b/ry-vue3/src/views/leader/Projects.vue @@ -0,0 +1,351 @@ + + + + + \ No newline at end of file diff --git a/ry-vue3/src/views/manager/Accounts.vue b/ry-vue3/src/views/manager/Accounts.vue new file mode 100644 index 0000000..812a867 --- /dev/null +++ b/ry-vue3/src/views/manager/Accounts.vue @@ -0,0 +1,100 @@ + + + + + \ No newline at end of file diff --git a/ry-vue3/src/views/manager/ExecIntent.vue b/ry-vue3/src/views/manager/ExecIntent.vue new file mode 100644 index 0000000..2d0ee1f --- /dev/null +++ b/ry-vue3/src/views/manager/ExecIntent.vue @@ -0,0 +1,69 @@ + + + + + \ No newline at end of file diff --git a/ry-vue3/src/views/manager/Experts.vue b/ry-vue3/src/views/manager/Experts.vue new file mode 100644 index 0000000..0464dc6 --- /dev/null +++ b/ry-vue3/src/views/manager/Experts.vue @@ -0,0 +1,446 @@ + + + + + diff --git a/ry-vue3/src/views/manager/ManagerProjectDetail.vue b/ry-vue3/src/views/manager/ManagerProjectDetail.vue new file mode 100644 index 0000000..70e61fc --- /dev/null +++ b/ry-vue3/src/views/manager/ManagerProjectDetail.vue @@ -0,0 +1,439 @@ + + + + + diff --git a/ry-vue3/src/views/manager/MeetingNew.vue b/ry-vue3/src/views/manager/MeetingNew.vue new file mode 100644 index 0000000..45d9ef1 --- /dev/null +++ b/ry-vue3/src/views/manager/MeetingNew.vue @@ -0,0 +1,112 @@ + + + + + \ No newline at end of file diff --git a/ry-vue3/src/views/manager/Meetings.vue b/ry-vue3/src/views/manager/Meetings.vue new file mode 100644 index 0000000..9d4b4ec --- /dev/null +++ b/ry-vue3/src/views/manager/Meetings.vue @@ -0,0 +1,319 @@ + + + + + \ No newline at end of file diff --git a/ry-vue3/src/views/manager/Orgs.vue b/ry-vue3/src/views/manager/Orgs.vue new file mode 100644 index 0000000..e602bd4 --- /dev/null +++ b/ry-vue3/src/views/manager/Orgs.vue @@ -0,0 +1,148 @@ + + + + + diff --git a/ry-vue3/src/views/manager/Plans.vue b/ry-vue3/src/views/manager/Plans.vue new file mode 100644 index 0000000..9216fac --- /dev/null +++ b/ry-vue3/src/views/manager/Plans.vue @@ -0,0 +1,312 @@ + + + + + diff --git a/ry-vue3/src/views/manager/Projects.vue b/ry-vue3/src/views/manager/Projects.vue new file mode 100644 index 0000000..cf3bd91 --- /dev/null +++ b/ry-vue3/src/views/manager/Projects.vue @@ -0,0 +1,834 @@ + + + + + \ No newline at end of file diff --git a/ry-vue3/src/views/manager/ProjectsNew.vue b/ry-vue3/src/views/manager/ProjectsNew.vue new file mode 100644 index 0000000..85c156c --- /dev/null +++ b/ry-vue3/src/views/manager/ProjectsNew.vue @@ -0,0 +1,504 @@ + + + + + \ No newline at end of file diff --git a/ry-vue3/src/views/manager/SupportIntent.vue b/ry-vue3/src/views/manager/SupportIntent.vue new file mode 100644 index 0000000..994633a --- /dev/null +++ b/ry-vue3/src/views/manager/SupportIntent.vue @@ -0,0 +1,83 @@ + + + + + diff --git a/ry-vue3/src/views/manager/Workbench.vue b/ry-vue3/src/views/manager/Workbench.vue new file mode 100644 index 0000000..5639c61 --- /dev/null +++ b/ry-vue3/src/views/manager/Workbench.vue @@ -0,0 +1,131 @@ + + + + + diff --git a/ry-vue3/src/views/portal/Home.vue b/ry-vue3/src/views/portal/Home.vue new file mode 100644 index 0000000..9282bfb --- /dev/null +++ b/ry-vue3/src/views/portal/Home.vue @@ -0,0 +1,859 @@ + + + + + diff --git a/ry-vue3/src/views/portal/Invitation.vue b/ry-vue3/src/views/portal/Invitation.vue new file mode 100644 index 0000000..d70bbc9 --- /dev/null +++ b/ry-vue3/src/views/portal/Invitation.vue @@ -0,0 +1,118 @@ + + + + + \ No newline at end of file diff --git a/ry-vue3/src/views/portal/Publicity.vue b/ry-vue3/src/views/portal/Publicity.vue new file mode 100644 index 0000000..4d053be --- /dev/null +++ b/ry-vue3/src/views/portal/Publicity.vue @@ -0,0 +1,713 @@ + + + + + diff --git a/ry-vue3/src/views/portal/PublicityDetail.vue b/ry-vue3/src/views/portal/PublicityDetail.vue new file mode 100644 index 0000000..bb9f0c3 --- /dev/null +++ b/ry-vue3/src/views/portal/PublicityDetail.vue @@ -0,0 +1,1211 @@ + + + + + diff --git a/ry-vue3/src/views/portal/SupportLetter.vue b/ry-vue3/src/views/portal/SupportLetter.vue new file mode 100644 index 0000000..13bd27d --- /dev/null +++ b/ry-vue3/src/views/portal/SupportLetter.vue @@ -0,0 +1,106 @@ + + + + + \ No newline at end of file diff --git a/ry-vue3/src/views/sponsor/Account.vue b/ry-vue3/src/views/sponsor/Account.vue new file mode 100644 index 0000000..150c08d --- /dev/null +++ b/ry-vue3/src/views/sponsor/Account.vue @@ -0,0 +1,502 @@ + + + + + \ No newline at end of file diff --git a/ry-vue3/src/views/sponsor/Home.vue b/ry-vue3/src/views/sponsor/Home.vue new file mode 100644 index 0000000..b97f716 --- /dev/null +++ b/ry-vue3/src/views/sponsor/Home.vue @@ -0,0 +1,156 @@ + + + + + \ No newline at end of file diff --git a/ry-vue3/src/views/sponsor/Meetings.vue b/ry-vue3/src/views/sponsor/Meetings.vue new file mode 100644 index 0000000..0f5c188 --- /dev/null +++ b/ry-vue3/src/views/sponsor/Meetings.vue @@ -0,0 +1,317 @@ + + + + + \ No newline at end of file diff --git a/ry-vue3/src/views/sponsor/NewPerson.vue b/ry-vue3/src/views/sponsor/NewPerson.vue new file mode 100644 index 0000000..9a180a0 --- /dev/null +++ b/ry-vue3/src/views/sponsor/NewPerson.vue @@ -0,0 +1,261 @@ + + + + + \ No newline at end of file diff --git a/ry-vue3/src/views/sponsor/PersonDetail.vue b/ry-vue3/src/views/sponsor/PersonDetail.vue new file mode 100644 index 0000000..198c1fd --- /dev/null +++ b/ry-vue3/src/views/sponsor/PersonDetail.vue @@ -0,0 +1,149 @@ + + + + + \ No newline at end of file diff --git a/ry-vue3/src/views/sponsor/Projects.vue b/ry-vue3/src/views/sponsor/Projects.vue new file mode 100644 index 0000000..bb67ace --- /dev/null +++ b/ry-vue3/src/views/sponsor/Projects.vue @@ -0,0 +1,433 @@ + + + + + \ No newline at end of file diff --git a/ry-vue3/src/views/sponsor/SponsorPeople.vue b/ry-vue3/src/views/sponsor/SponsorPeople.vue new file mode 100644 index 0000000..c15d036 --- /dev/null +++ b/ry-vue3/src/views/sponsor/SponsorPeople.vue @@ -0,0 +1,308 @@ + + + + + diff --git a/ry-vue3/src/views/sponsor/SponsorProjects.vue b/ry-vue3/src/views/sponsor/SponsorProjects.vue new file mode 100644 index 0000000..6ea51c9 --- /dev/null +++ b/ry-vue3/src/views/sponsor/SponsorProjects.vue @@ -0,0 +1,393 @@ + + + + + \ No newline at end of file diff --git a/ry-vue3/vite.config.js b/ry-vue3/vite.config.js new file mode 100644 index 0000000..9d7f551 --- /dev/null +++ b/ry-vue3/vite.config.js @@ -0,0 +1,28 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import AutoImport from 'unplugin-auto-import/vite' +import Components from 'unplugin-vue-components/vite' +import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' +import path from 'path' + +export default defineConfig({ + plugins: [ + vue(), + AutoImport({ resolvers: [ElementPlusResolver()] }), + Components({ resolvers: [ElementPlusResolver()] }) + ], + resolve: { + alias: { '@': path.resolve(__dirname, 'src') } + }, + server: { + port: 5173, + host: '0.0.0.0', + proxy: { + '/dev-api': { + target: 'http://127.0.0.1:8080', + changeOrigin: true, + rewrite: (p) => p.replace(/^\/dev-api/, '') + } + } + } +}) diff --git a/scripts/redis-clear-config.sh b/scripts/redis-clear-config.sh new file mode 100644 index 0000000..6a6f868 --- /dev/null +++ b/scripts/redis-clear-config.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# 精准清 sys_config 缓存, 保留 login_tokens 等其他数据. +# 替代 'redis-cli flushdb' (会把所有 session 干掉). + +set -e + +PATTERN="sys_config:*" +COUNT=$(redis-cli --scan --pattern "$PATTERN" | wc -l) +echo "[sys-config-clear] 找到 $COUNT 个 sys_config:* keys" + +if [ "$COUNT" -gt 0 ]; then + redis-cli --scan --pattern "$PATTERN" | xargs -r redis-cli del + echo "[sys-config-clear] 已删除 $COUNT 个 keys (login_tokens 已保留)" +else + echo "[sys-config-clear] 无需清理" +fi \ No newline at end of file diff --git a/sql/biz_data.sql b/sql/biz_data.sql new file mode 100644 index 0000000..d602d68 --- /dev/null +++ b/sql/biz_data.sql @@ -0,0 +1,220 @@ +-- ============================================================================ +-- 业务表种子数据(基于原型 /home/john/ry8080/proto/html/components/ 数据复刻) +-- ============================================================================ + +-- 项目 +INSERT INTO biz_project(project_no, project_name, project_form, total_sessions, done_sessions, todo_sessions, + total_amount, available_amount, paid_labor_amount, paid_meeting_amount, manage_fee, + is_finished, is_settled, rating_score, org_name, org_type, + start_time, end_time, create_by, create_time) +VALUES +('ZH-2026-658', '小牛血清创新应用研讨会', '线上', 26, 16, 10, 3112000.00, 1112000.00, 1000000.00, 1000000.00, 1112000.00, '0', '0', 5.0, '北京XXXX有限公司', 'sponsor', '2026-05-11 00:00:00', '2026-06-30 00:00:00', 'admin', NOW()), +('ZH-2026-659', '整合医学学会项目评审会', '线下', 18, 12, 6, 2500000.00, 980000.00, 900000.00, 620000.00, 980000.00, '0', '0', 4.5, '北京XXXX有限公司', 'sponsor', '2026-05-11 00:00:00', '2026-06-30 00:00:00', 'admin', NOW()), +('ZH-2026-650', '智慧医院建设项目方案论证会', '线上+线下', 12, 7, 5, 1800000.00, 760000.00, 700000.00, 340000.00, 760000.00, '0', '0', 4.0, '北京XXXX有限公司', 'sponsor', '2026-05-11 00:00:00', '2026-06-30 00:00:00', 'admin', NOW()), +('ZH-2026-645', '基层医疗改革试点方案中期评估会', '线下', 20, 14, 6, 2800000.00, 1050000.00, 950000.00, 800000.00, 1050000.00, '0', '0', 4.8, '北京XXXX有限公司', 'sponsor', '2026-05-11 00:00:00', '2026-06-30 00:00:00', 'admin', NOW()), +('ZH-2026-640', '数字化医疗转型方案评审会', '线上', 22, 15, 7, 3000000.00, 1200000.00, 1100000.00, 700000.00, 1200000.00, '0', '0', 4.2, '北京XXXX有限公司', 'sponsor', '2026-05-11 00:00:00', '2026-06-30 00:00:00', 'admin', NOW()), +('ZH-2026-635', '临床医学研究方案评议会', '线下', 14, 9, 5, 2200000.00, 820000.00, 740000.00, 640000.00, 820000.00, '1', '1', 4.6, '北京XXXX有限公司', 'sponsor', '2026-05-11 00:00:00', '2026-06-30 00:00:00', 'admin', NOW()), +('ZH-2026-628', '公共卫生应急体系建设研讨会', '线下', 16, 11, 5, 2400000.00, 900000.00, 820000.00, 680000.00, 900000.00, '0', '0', 4.4, '北京XXXX有限公司', 'sponsor', '2026-05-11 00:00:00', '2026-06-30 00:00:00', 'admin', NOW()), +('ZH-2026-622', '中医诊疗标准化研究论坛', '线下', 10, 6, 4, 1500000.00, 560000.00, 500000.00, 440000.00, 560000.00, '0', '0', 4.7, '北京XXXX有限公司', 'sponsor', '2026-05-11 00:00:00', '2026-06-30 00:00:00', 'admin', NOW()), +('ZH-2026-618', '医疗 AI 辅助诊断应用论坛', '线上+线下', 8, 4, 4, 1200000.00, 480000.00, 420000.00, 300000.00, 480000.00, '0', '0', 4.9, '北京XXXX有限公司', 'sponsor', '2026-05-11 00:00:00', '2026-06-30 00:00:00', 'admin', NOW()); + +-- 项目角色劳务设置 +INSERT INTO biz_project_labor_role(project_id, role_name, labor_amount) +SELECT project_id, '主席', 5000.00 FROM biz_project WHERE project_no='ZH-2026-658' UNION ALL +SELECT project_id, '主持', 3000.00 FROM biz_project WHERE project_no='ZH-2026-658' UNION ALL +SELECT project_id, '主席', 5000.00 FROM biz_project WHERE project_no='ZH-2026-659' UNION ALL +SELECT project_id, '主持', 3000.00 FROM biz_project WHERE project_no='ZH-2026-659'; + +-- 项目策划方案 +INSERT INTO biz_project_plan(plan_name, plan_direction, plan_category, project_form, status, is_settled, project_no, remark, create_by, create_time) +VALUES +('小牛血清创新应用研讨会', '专项计划一:《规范化诊疗及医疗质量提升专项计划(2026-2030年)》', '会议项目', '线上', '待审核', '1', 'ZH-2026-658', 'XXXXXX', 'admin', NOW()), +('整合医学学会项目设计', '专项计划一:《规范化诊疗及医疗质量提升专项计划(2026-2030年)》', '科研项目', '线上', '待审核', '0', 'ZH-2026-659', 'XXXXXX', 'admin', NOW()), +('智慧医院建设项目方案', '专项计划一:《规范化诊疗及医疗质量提升专项计划(2026-2030年)》', '学术项目', '线下', '审核通过', '0', 'ZH-2026-650', 'XXXXXX', 'admin', NOW()), +('基层医疗改革试点方案', '专项计划一:《规范化诊疗及医疗质量提升专项计划(2026-2030年)》', '指南', '线下', '待审核', '0', 'ZH-2026-645', 'XXXXXX', 'admin', NOW()), +('公共卫生应急体系建设方案', '专项计划一:《规范化诊疗及医疗质量提升专项计划(2026-2030年)》', '对外交流', '线上', '审核通过', '0', 'ZH-2026-640', 'XXXXXX', 'admin', NOW()), +('数字化医疗转型方案', '专项计划一:《规范化诊疗及医疗质量提升专项计划(2026-2030年)》', '共识', '线下', '已退回', '0', 'ZH-2026-635', 'XXXXXX', 'admin', NOW()); + +-- 会议 +INSERT INTO biz_meeting(business_id, project_id, project_no, project_name, meeting_name, period_no, total_periods, project_form, start_time, end_time, org_name, current_stage, create_by, create_time) +SELECT '5643145673', project_id, 'ZH-2026-658', '小牛血清创新应用研讨会', '小牛血清创新应用研讨会', 3, 26, '线上', '2026-05-11 11:30:00', '2026-05-11 14:30:00', '北京XXXX有限公司', '待监管', 'admin', NOW() FROM biz_project WHERE project_no='ZH-2026-658' UNION ALL +SELECT '5643145674', project_id, 'ZH-2026-659', '整合医学学会项目评审会', '整合医学学会项目评审会', 18, 18, '线下', '2026-04-01 09:00:00', '2026-04-01 17:00:00', '北京XXXX有限公司', '监管通过', 'admin', NOW() FROM biz_project WHERE project_no='ZH-2026-659' UNION ALL +SELECT '5643145675', project_id, 'ZH-2026-650', '智慧医院建设项目方案论证会', '智慧医院建设项目方案论证会', 7, 12, '线上+线下', '2026-03-15 14:00:00', '2026-03-15 16:30:00', '北京XXXX有限公司', '待整改', 'admin', NOW() FROM biz_project WHERE project_no='ZH-2026-650' UNION ALL +SELECT '5643145676', project_id, 'ZH-2026-645', '基层医疗改革试点中期评估会', '基层医疗改革试点中期评估会', 14, 20, '线上', '2026-02-01 09:30:00', '2026-02-01 12:00:00', '北京XXXX有限公司', '已结算', 'admin', NOW() FROM biz_project WHERE project_no='ZH-2026-645' UNION ALL +SELECT '5643145677', project_id, 'ZH-2026-640', '数字化医疗转型方案评审会', '数字化医疗转型方案评审会', 15, 22, '线下', '2026-01-10 10:00:00', '2026-01-10 17:00:00', '北京XXXX有限公司', '已结题', 'admin', NOW() FROM biz_project WHERE project_no='ZH-2026-640'; + +-- 会议结算明细(会务) +INSERT INTO biz_meeting_settlement(meeting_id, project_no, project_name, exec_unit, fee_type, unit_price, qty, subtotal, remark, audit_status, settlement_type) +SELECT meeting_id, 'ZH-2026-658', '小牛血清创新应用研讨会', '北京会务服务公司', '场地租赁', 5000.00, 1, 5000.00, '会议室 A', '已审核', 'meeting' FROM biz_meeting WHERE business_id='5643145673' UNION ALL +SELECT meeting_id, 'ZH-2026-658', '小牛血清创新应用研讨会', '北京会务服务公司', '设备租赁', 3000.00, 1, 3000.00, '投影+音响', '已审核', 'meeting' FROM biz_meeting WHERE business_id='5643145673' UNION ALL +SELECT meeting_id, 'ZH-2026-658', '小牛血清创新应用研讨会', '北京会务服务公司', '餐饮服务', 200.00, 30, 6000.00, '工作午餐', '已审核', 'meeting' FROM biz_meeting WHERE business_id='5643145673' UNION ALL +SELECT meeting_id, 'ZH-2026-658', '小牛血清创新应用研讨会', '北京会务服务公司', '资料印刷', 50.00, 40, 2000.00, '会议手册', '待审核', 'meeting' FROM biz_meeting WHERE business_id='5643145673' UNION ALL +SELECT meeting_id, 'ZH-2026-658', '小牛血清创新应用研讨会', '北京会务服务公司', '其他杂费', 1000.00, 1, 1000.00, '茶水等', '已审核', 'meeting' FROM biz_meeting WHERE business_id='5643145673'; + +-- 会议结算明细(劳务) +INSERT INTO biz_meeting_settlement(meeting_id, project_no, project_name, exec_unit, fee_type, unit_price, qty, subtotal, remark, audit_status, settlement_type) +SELECT meeting_id, 'ZH-2026-658', '小牛血清创新应用研讨会', '北京会务服务公司', '专家劳务费', 5000.00, 1, 5000.00, '会议室 A', '已审核', 'labor' FROM biz_meeting WHERE business_id='5643145673' UNION ALL +SELECT meeting_id, 'ZH-2026-658', '小牛血清创新应用研讨会', '北京会务服务公司', '评审费', 3000.00, 1, 3000.00, '投影+音响', '已审核', 'labor' FROM biz_meeting WHERE business_id='5643145673' UNION ALL +SELECT meeting_id, 'ZH-2026-658', '小牛血清创新应用研讨会', '北京会务服务公司', '差旅费', 200.00, 30, 6000.00, '工作午餐', '已审核', 'labor' FROM biz_meeting WHERE business_id='5643145673' UNION ALL +SELECT meeting_id, 'ZH-2026-658', '小牛血清创新应用研讨会', '北京会务服务公司', '材料费', 50.00, 40, 2000.00, '会议手册', '待审核', 'labor' FROM biz_meeting WHERE business_id='5643145673' UNION ALL +SELECT meeting_id, 'ZH-2026-658', '小牛血清创新应用研讨会', '北京会务服务公司', '其他费用', 1000.00, 1, 1000.00, '茶水等', '已审核', 'labor' FROM biz_meeting WHERE business_id='5643145673'; + +-- 专家 +INSERT INTO biz_expert(name, phone, work_unit, department, title, region, id_card, audit_status, audit_by, audit_time, status, create_by, create_time) +VALUES +('张三', '13534621147', '北京XXXXXXX医院', '呼吸科', '主任医师', '北京', '110101198501011234', '待审核', 'admin', NOW(), '0', 'admin', NOW()), +('李四', '13534621148', '北京XXXXXXX医院', '血液科', '副主任医师', '北京', '110101198801012345', '审核通过', 'admin', NOW(), '0', 'admin', NOW()), +('王五', '13534621149', '北京XXXXXXX医院', '心血管科', '主治医师', '上海', '310101198901013456', '待审核', 'admin', NOW(), '0', 'admin', NOW()), +('赵六', '13534621150', '北京XXXXXXX医院', '神经科', '副主任医师', '广东', '440101198701014567', '已退回', 'admin', NOW(), '0', 'admin', NOW()), +('钱七', '13534621151', '北京XXXXXXX医院', '内分泌科', '主任医师', '北京', '110101199001015678', '审核通过', 'admin', NOW(), '0', 'admin', NOW()), +('陈教授', '13534621152', '北京XXXXXXX医院', '消化科', '副主任医师', '北京', '110101198601016789', '审核通过', 'admin', NOW(), '0', 'admin', NOW()), +('王教授', '13534621153', '北京XXXXXXX医院', '心血管科', '主任医师', '上海', '310101198401017890', '审核通过', 'admin', NOW(), '0', 'admin', NOW()), +('李教授', '13534621154', '北京XXXXXXX医院', '神经科', '主治医师', '北京', '110101199201018901', '审核通过', 'admin', NOW(), '0', 'admin', NOW()), +('张教授', '13534621155', '北京XXXXXXX医院', '骨科', '主任医师', '北京', '110101198501019012', '已驳回', 'admin', NOW(), '0', 'admin', NOW()); + +-- 公司表 (赞助方/执行方共用) +INSERT INTO biz_org(org_name, org_type, address, tax_no, contact_name, contact_phone, intent_count, status, create_by, create_time) +VALUES +-- 赞助方 (原 biz_support_unit 数据) +('北京XXXXXXX公司', 'sponsor', '北京市海淀区XXX路XXXX号', '91330101MA2GPR961K', '王五', '13800138001', 8, '合作中', 'admin', NOW()), +('北京XXXXXXX公司', 'sponsor', '北京市朝阳区XXX路XXXX号', '91330101MA2GPR962K', '李六', '13800138002', 0, '合作中', 'admin', NOW()), +('北京XXXXXXX公司', 'sponsor', '北京市西城区XXX路XXXX号', '91330101MA2GPR963K', '钱七', '13800138003', 0, '禁用', 'admin', NOW()), +('北京XXXXXXX公司', 'sponsor', '北京市丰台区XXX路XXXX号', '91330101MA2GPR964K', '孙八', '13800138004', 0, '正常', 'admin', NOW()), +('北京XXXXXXX公司', 'sponsor', '北京市海淀区XXX路XXXX号', '91330101MA2GPR965K', '周九', '13800138005', 0, '正常', 'admin', NOW()), +('北京XXXXXXX公司', 'sponsor', '北京市朝阳区XXX路XXXX号', '91330101MA2GPR966K', '吴十', '13800138006', 0, '正常', 'admin', NOW()), +('北京XXXXXXX公司', 'sponsor', '北京市西城区XXX路XXXX号', '91330101MA2GPR967K', '郑十一', '13800138007', 0, '正常', 'admin', NOW()), +('北京XXXXXXX公司', 'sponsor', '北京市海淀区XXX路XXXX号', '91330101MA2GPR968K', '王十二', '13800138008', 0, '正常', 'admin', NOW()), +-- 执行方 (原 biz_execution_unit 数据) +('北京XXXXXXX公司', 'executor', '北京市海淀区XXX路XXXX号', '91330101MA2GPR961K', NULL, NULL, 0, '0', 'admin', NOW()), +('上海XXXXXXX公司', 'executor', '上海市浦东新区XXX路XXXX号', '91330101MA2GPR962K', NULL, NULL, 0, '0', 'admin', NOW()), +('广州XXXXXXX公司', 'executor', '广州市天河区XXX路XXXX号', '91330101MA2GPR963K', NULL, NULL, 0, '0', 'admin', NOW()), +('深圳XXXXXXX公司', 'executor', '深圳市南山区XXX路XXXX号', '91330101MA2GPR964K', NULL, NULL, 0, '1', 'admin', NOW()), +('北京XXXXXXX公司', 'executor', '北京市朝阳区XXX路XXXX号', '91330101MA2GPR965K', NULL, NULL, 0, '0', 'admin', NOW()), +('北京XXXXXXX公司', 'executor', '北京市西城区XXX路XXXX号', '91330101MA2GPR966K', NULL, NULL, 0, '0', 'admin', NOW()), +('北京XXXXXXX公司', 'executor', '北京市丰台区XXX路XXXX号', '91330101MA2GPR967K', NULL, NULL, 0, '0', 'admin', NOW()), +('北京XXXXXXX公司', 'executor', '北京市昌平区XXX路XXXX号', '91330101MA2GPR968K', NULL, NULL, 0, '1', 'admin', NOW()); + +-- 人员(执行方会务执行, org_id 对应 biz_execution_unit 那 8 条, 取前 5 个 org_id) +INSERT INTO biz_person(name, phone, org_id, department, position, role, unit_type, status, create_by, create_time) +VALUES +('张三', '13534621147', 1, '会务部', '经理', '会务执行', 'execution', '0', 'admin', NOW()), +('李四', '13534621148', 1, '会务部', '专员', '会务执行', 'execution', '0', 'admin', NOW()), +('王五', '13534621149', 2, '会务部', '主管', '会务执行', 'execution', '0', 'admin', NOW()), +('赵六', '13534621150', 2, '会务部', '专员', '会务执行', 'execution', '0', 'admin', NOW()), +('钱七', '13534621151', 3, '会务部', '经理', '会务执行', 'execution', '0', 'admin', NOW()), +('孙八', '13534621152', 3, '会务部', '专员', '会务执行', 'execution', '0', 'admin', NOW()), +('周九', '13534621153', 4, '会务部', '主管', '会务执行', 'execution', '1', 'admin', NOW()), +('吴十', '13534621154', 4, '会务部', '专员', '会务执行', 'execution', '0', 'admin', NOW()); + +-- 人员(合规方项目负责人/会议执行/监察员, org_id 对应 sponsor 类型 biz_org 前 5 个) +INSERT INTO biz_person(name, phone, org_id, department, position, role, unit_type, status, create_by, create_time) +VALUES +('张三', '13800138001', 1, '合规部', '经理', '项目负责人', 'execution', '0', 'admin', NOW()), +('李四', '13800138002', 2, '合规部', '专员', '会议执行', 'execution', '0', 'admin', NOW()), +('王五', '13800138003', 3, '监察组', '组长', '监察员', 'execution', '0', 'admin', NOW()), +('赵六', '13800138004', 4, '监察组', '专员', '监察员', 'execution', '0', 'admin', NOW()), +('钱七', '13800138005', 5, '合规部', '主管', '项目负责人', 'execution', '0', 'admin', NOW()); + +-- 支持意向 +INSERT INTO biz_support_intent(project_no, project_name, name, work_unit, department, position, phone, account_status, create_time) +VALUES +('ZH-2026-658', '小牛血清创新应用研讨会', '张三', '北京XXXXXXX公司', '华北大区', '项目负责人', '13534621147', '存在', NOW()), +('ZH-2026-659', '整合医学学会项目评审', '李四', '北京XXXXXXX公司', '华北大区', '项目负责人', '13534621148', '存在', NOW()), +('ZH-2026-650', '智慧医院建设项目', '张三', '北京XXXXXXX公司', '华北大区', '项目负责人', '13534621149', '不存在', NOW()), +('ZH-2026-645', '基层医疗改革试点项目', '李四', '北京XXXXXXX公司', '华北大区', '项目负责人', '13534621150', '存在', NOW()), +('ZH-2026-640', '数字化医疗转型方案', '张三', '北京XXXXXXX公司', '华北大区', '项目负责人', '13534621151', '不存在', NOW()), +('ZH-2026-635', '临床医学研究方案评议', '李四', '北京XXXXXXX公司', '华北大区', '项目负责人', '13534621152', '存在', NOW()), +('ZH-2026-628', '公共卫生应急体系建设', '张三', '北京XXXXXXX公司', '华北大区', '项目负责人', '13534621153', '存在', NOW()), +('ZH-2026-622', '中医诊疗标准化研究', '李四', '北京XXXXXXX公司', '华北大区', '项目负责人', '13534621154', '存在', NOW()); + +-- 执行意向 +INSERT INTO biz_execution_intent(project_no, project_name, name, work_unit, department, position, phone, onboard_status, create_time) +SELECT 'ZH-2026-658', '小牛血清创新应用研讨会', '张三', '北京XXXXXX公司', '华北大区', '项目负责人', '13512321597', '已入库', NOW() UNION ALL +SELECT 'ZH-2026-658', '小牛血清创新应用研讨会', '李四', '北京XXXXXX公司', '华北大区', '项目负责人', '13512321598', '未入库', NOW() UNION ALL +SELECT 'ZH-2026-658', '小牛血清创新应用研讨会', '王五', '北京XXXXXX公司', '华北大区', '项目负责人', '13512321599', '已入库', NOW() UNION ALL +SELECT 'ZH-2026-658', '小牛血清创新应用研讨会', '赵六', '北京XXXXXX公司', '华北大区', '项目负责人', '13512321600', '已入库', NOW() UNION ALL +SELECT 'ZH-2026-658', '小牛血清创新应用研讨会', '钱七', '北京XXXXXX公司', '华北大区', '项目负责人', '13512321601', '已入库', NOW() UNION ALL +SELECT 'ZH-2026-658', '小牛血清创新应用研讨会', '孙八', '北京XXXXXX公司', '华北大区', '项目负责人', '13512321602', '已入库', NOW() UNION ALL +SELECT 'ZH-2026-658', '小牛血清创新应用研讨会', '周九', '北京XXXXXX公司', '华北大区', '项目负责人', '13512321603', '已入库', NOW() UNION ALL +SELECT 'ZH-2026-658', '小牛血清创新应用研讨会', '吴十', '北京XXXXXX公司', '华北大区', '项目负责人', '13512321604', '已入库', NOW(); + +-- 公示公告 +INSERT INTO biz_announcement(title, ann_type, publish_time, project_no, create_by, create_time) +VALUES +('邀请函 2026第2323号 "恺启新生—胃癌 CAR-T 细胞治疗系列会"', '邀请函', '2026-05-06 10:00:00', 'ZH-2026-658', 'admin', NOW()), +('会议通知 2026第2277号 北京同仁医院建院140周年学术会议', '通知', '2026-05-06 10:00:00', 'ZH-2026-659', 'admin', NOW()), +('支持函 2026第2278号 项目启动支持', '支持函', '2026-05-06 10:00:00', 'ZH-2026-650', 'admin', NOW()), +('会议日程 2026第2279号 整合医学学会项目评审会', '日程', '2026-05-06 10:00:00', 'ZH-2026-659', 'admin', NOW()); + +-- 邀请函 +INSERT INTO biz_invitation(project_id, project_no, title, publish_time) +SELECT project_id, 'ZH-2026-658', '小牛血清创新应用研讨会邀请函', NOW() FROM biz_project WHERE project_no='ZH-2026-658'; + +-- 支持函 +INSERT INTO biz_support_letter(project_id, project_no, title, file_url, publish_time) +SELECT project_id, 'ZH-2026-658', '小牛血清创新应用研讨会支持函', '/upload/support/u635.jpg', NOW() FROM biz_project WHERE project_no='ZH-2026-658'; + +-- 投稿 +INSERT INTO biz_submission(submitter_name, title, direction, project_form, project_category, design_file_url, status, remark, create_by, create_time) +VALUES +('张三', '小牛血清创新应用研讨会投稿', '临床医学', '线上', '学术会议类', '/upload/submission/001.pdf', '未结题', NULL, 'admin', NOW()), +('李四', '小牛血清临床应用二期', '临床医学', '线下', '学术会议类', '/upload/submission/002.pdf', '已结题', NULL, 'admin', NOW()), +('王五', '整合医学学会项目设计', '临床医学', '线上+线下', '学术会议类', '/upload/submission/003.pdf', '已退回', NULL, 'admin', NOW()), +('赵六', '基层医疗改革试点方案投稿', '临床医学', '线下', '调研征集类', '/upload/submission/004.pdf', '未结题', NULL, 'admin', NOW()), +('钱七', '智慧医院建设项目设计', '临床医学', '线上', '标准制定类', '/upload/submission/005.pdf', '已结题', NULL, 'admin', NOW()), +('孙八', '数字化医疗转型方案 V2', '临床医学', '线上', '标准制定类', '/upload/submission/006.pdf', '未结题', NULL, 'admin', NOW()), +('周九', '公共卫生应急体系设计稿', '临床医学', '线下', '调研征集类', '/upload/submission/007.pdf', '已结题', NULL, 'admin', NOW()), +('吴十', '中医诊疗标准化方案', '临床医学', '线下', '专业培训类', '/upload/submission/008.pdf', '已退回', NULL, 'admin', NOW()); + +-- 项目评分 +INSERT INTO biz_project_rating(project_id, project_no, project_name, rater_name, rater_role, quality_score, response_score, cooperation_score, compliance_score, total_score, rating_time) +SELECT project_id, 'ZH-2026-658', '小牛血清创新应用研讨会', '张合规', 'compliance', 5, 5, 5, 5, 5.0, NOW() FROM biz_project WHERE project_no='ZH-2026-658' UNION ALL +SELECT project_id, 'ZH-2026-659', '整合医学学会项目评审', '李合规', 'compliance', 4, 5, 4, 5, 4.5, NOW() FROM biz_project WHERE project_no='ZH-2026-659' UNION ALL +SELECT project_id, 'ZH-2026-650', '智慧医院建设项目', '王合规', 'compliance', 4, 4, 4, 4, 4.0, NOW() FROM biz_project WHERE project_no='ZH-2026-650' UNION ALL +SELECT project_id, 'ZH-2026-658', '小牛血清创新应用研讨会', '张赞助', 'sponsor', 4, 3, 4, 3, 3.5, NOW() FROM biz_project WHERE project_no='ZH-2026-658' UNION ALL +SELECT project_id, 'ZH-2026-659', '整合医学学会项目评审', '李赞助', 'sponsor', 4, 3, 3, 4, 3.5, NOW() FROM biz_project WHERE project_no='ZH-2026-659'; + +-- 资料库 +INSERT INTO biz_resource(name, category, file_size, file_url, uploader_name, upload_time, status) +VALUES +('项目策划模板 V2.0.docx', '项目模板', '2.3 MB', '/upload/resource/001.docx', '张三', '2026-07-15 10:30:00', '已发布'), +('会议材料模板 V1.5.docx', '会议材料', '1.8 MB', '/upload/resource/002.docx', '李四', '2026-07-12 14:20:00', '已发布'), +('合规审核要点手册.pdf', '合规文档', '5.6 MB', '/upload/resource/003.pdf', '王五', '2026-07-08 09:15:00', '已发布'), +('专家评审标准 V3.0.pdf', '专家库', '3.2 MB', '/upload/resource/004.pdf', '赵六', '2026-06-25 16:45:00', '已发布'), +('项目操作培训视频.mp4', '培训资料', '256 MB', '/upload/resource/005.mp4', '钱七', '2026-06-20 11:00:00', '已发布'), +('劳务凭证模板.xlsx', '表单模板', '0.5 MB', '/upload/resource/006.xlsx', '孙八', '2026-06-15 14:30:00', '已发布'); + +-- 劳务凭证 +INSERT INTO biz_labor_voucher(voucher_no, meeting_id, meeting_name, expert_name, id_card, bank_account, amount, submit_date, audit_status) +VALUES +('LV-2026-001', 1, '小牛血清创新应用研讨会', '陈教授', '110101********1234', '6222****5678', 3000.00, '2026-05-12 10:00:00', '待审核'), +('LV-2026-002', 2, '整合医学学会项目评审会', '王教授', '310101********5678', '6222****1234', 2500.00, '2026-04-02 10:00:00', '已审核'), +('LV-2026-003', 3, '智慧医院建设项目方案论证', '李教授', '440101********9012', '6222****3456', 4000.00, '2026-03-16 10:00:00', '已审核'), +('LV-2026-004', 4, '基层医疗改革试点中期评估', '张教授', '320101********3456', '6222****7890', 3500.00, '2026-02-02 10:00:00', '已驳回'); + +-- 业务角色绑定(为 sys_user 建立业务角色映射) +-- admin: 1=后台管理员 +INSERT INTO biz_user_role_bind(user_id, role_type, unit_type, create_time) +SELECT user_id, 'admin', NULL, NOW() FROM sys_user WHERE user_name='admin'; + +-- 创建 5 个业务角色用户 (密码 123456, RuoYi 加密后) +INSERT INTO sys_user(user_name, nick_name, user_type, email, phonenumber, sex, avatar, password, status, del_flag, login_ip, create_by, create_time, remark) +VALUES +('leader01', '项目负责人张三', '00', 'leader01@bahim.org.cn', '13900139001', '0', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', 'admin', NOW(), '项目负责人'), +('manager01', '合规员李四', '00', 'manager01@bahim.org.cn', '13900139002', '0', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', 'admin', NOW(), '合规人员'), +('doctor01', '专家王五', '00', 'doctor01@bahim.org.cn', '13900139003', '0', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', 'admin', NOW(), '专家'), +('executor01', '执行负责人赵六', '00', 'executor01@bahim.org.cn', '13900139004', '0', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', 'admin', NOW(), '执行方'), +('sponsor01', '赞助方钱七', '00', 'sponsor01@bahim.org.cn', '13900139005', '0', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', 'admin', NOW(), '赞助方'); + +-- 绑定业务角色 +INSERT INTO biz_user_role_bind(user_id, role_type, unit_type, create_time) +SELECT user_id, 'leader', NULL, NOW() FROM sys_user WHERE user_name='leader01' UNION ALL +SELECT user_id, 'manager', NULL, NOW() FROM sys_user WHERE user_name='manager01' UNION ALL +SELECT user_id, 'doctor', NULL, NOW() FROM sys_user WHERE user_name='doctor01' UNION ALL +SELECT user_id, 'executor', 'execution', NOW() FROM sys_user WHERE user_name='executor01' UNION ALL +SELECT user_id, 'sponsor', 'sponsor', NOW() FROM sys_user WHERE user_name='sponsor01'; + +-- 给所有业务用户配项目负责人角色(ruoyi role_id=2) +INSERT INTO sys_user_role(user_id, role_id) +SELECT user_id, 2 FROM sys_user WHERE user_name IN ('leader01','manager01','doctor01','executor01','sponsor01'); +-- admin 已在原 RuoYi 脚本中,保留 diff --git a/sql/biz_schema.sql b/sql/biz_schema.sql new file mode 100644 index 0000000..1853cab --- /dev/null +++ b/sql/biz_schema.sql @@ -0,0 +1,507 @@ +-- ============================================================================ +-- 业务表结构 (基于原型 /home/john/ry8080/proto/html/components/ 字段提炼) +-- 目标库: ry0808 +-- 引擎: InnoDB / utf8mb4 +-- ============================================================================ + +SET FOREIGN_KEY_CHECKS = 0; + +DROP TABLE IF EXISTS biz_project; +CREATE TABLE biz_project ( + project_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '项目ID', + project_no VARCHAR(50) NOT NULL COMMENT '项目编号 ZH-2026-658', + project_name VARCHAR(200) NOT NULL COMMENT '项目名称', + project_form VARCHAR(20) DEFAULT NULL COMMENT '项目形式 线上/线下/线上+线下/其他', + total_sessions INT DEFAULT 0 COMMENT '总场次/总期数', + done_sessions INT DEFAULT 0 COMMENT '已执行', + todo_sessions INT DEFAULT 0 COMMENT '未执行', + total_amount DECIMAL(15,2) DEFAULT 0 COMMENT '总金额', + available_amount DECIMAL(15,2) DEFAULT 0 COMMENT '可用金额', + paid_labor_amount DECIMAL(15,2) DEFAULT 0 COMMENT '已支付劳务费', + paid_meeting_amount DECIMAL(15,2) DEFAULT 0 COMMENT '已支付会务费', + manage_fee DECIMAL(15,2) DEFAULT 0 COMMENT '管理费及税金', + is_finished CHAR(1) DEFAULT '0' COMMENT '是否结题 0否 1是', + is_settled CHAR(1) DEFAULT '0' COMMENT '是否结算 0否 1是', + rating_score DECIMAL(3,1) DEFAULT NULL COMMENT '项目评价分数', + org_id BIGINT DEFAULT NULL COMMENT '公司ID (赞助方或执行方, 由 org_type 区分)', + org_name VARCHAR(200) DEFAULT NULL COMMENT '公司名称(冗余)', + org_type VARCHAR(20) DEFAULT 'sponsor' COMMENT '公司类型 sponsor赞助方/executor执行方', + start_time DATETIME DEFAULT NULL COMMENT '项目开始时间', + end_time DATETIME DEFAULT NULL COMMENT '项目结束时间', + submit_deadline_days INT DEFAULT 0 COMMENT '提交材料截止天数', + support_contract_url VARCHAR(500) DEFAULT NULL COMMENT '支持合同文件URL', + execute_contract_url VARCHAR(500) DEFAULT NULL COMMENT '执行合同文件URL', + invitation_url VARCHAR(500) DEFAULT NULL COMMENT '邀请函文件URL', + support_letter_url VARCHAR(500) DEFAULT NULL COMMENT '支持函文件URL', + publish_url VARCHAR(500) DEFAULT NULL COMMENT '已发布公告URL', + create_by VARCHAR(64) DEFAULT '' COMMENT '创建者', + create_time DATETIME DEFAULT NULL COMMENT '创建时间', + update_by VARCHAR(64) DEFAULT '' COMMENT '更新者', + update_time DATETIME DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (project_id), + UNIQUE KEY uk_project_no (project_no), + KEY idx_project_form (project_form), + KEY idx_is_finished (is_finished), + KEY idx_is_settled (is_settled) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='项目表'; + +DROP TABLE IF EXISTS biz_project_plan; +CREATE TABLE biz_project_plan ( + plan_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '策划方案ID', + plan_name VARCHAR(200) NOT NULL COMMENT '策划方案名称', + plan_direction VARCHAR(500) DEFAULT NULL COMMENT '项目方向', + plan_category VARCHAR(50) DEFAULT NULL COMMENT '项目类别 会议项目/科研项目/学术项目/指南/对外交流/共识', + project_form VARCHAR(20) DEFAULT NULL COMMENT '项目形式', + design_file_url VARCHAR(500) DEFAULT NULL COMMENT '设计文件URL', + status VARCHAR(20) DEFAULT '0' COMMENT '状态 待审核/审核通过/已退回', + is_settled CHAR(1) DEFAULT '0' COMMENT '是否结算 0否 1是', + project_no VARCHAR(50) DEFAULT NULL COMMENT '关联项目编号', + remark VARCHAR(500) DEFAULT NULL COMMENT '备注', + audit_opinion VARCHAR(500) DEFAULT NULL COMMENT '审核意见', + audit_by VARCHAR(64) DEFAULT NULL COMMENT '审核人', + audit_time DATETIME DEFAULT NULL COMMENT '审核时间', + create_by VARCHAR(64) DEFAULT '' COMMENT '创建者', + create_time DATETIME DEFAULT NULL COMMENT '创建时间', + update_by VARCHAR(64) DEFAULT '' COMMENT '更新者', + update_time DATETIME DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (plan_id), + KEY idx_plan_status (status), + KEY idx_plan_project_no (project_no) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='项目策划方案表'; + +DROP TABLE IF EXISTS biz_project_assign; +CREATE TABLE biz_project_assign ( + assign_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '分配ID', + project_id BIGINT NOT NULL COMMENT '项目ID', + execution_unit_id BIGINT NOT NULL COMMENT '执行单位ID', + execution_unit_name VARCHAR(200) DEFAULT NULL COMMENT '执行单位名称(冗余)', + sessions INT DEFAULT 0 COMMENT '分配场数', + amount DECIMAL(15,2) DEFAULT 0 COMMENT '总金额', + PRIMARY KEY (assign_id), + KEY idx_assign_project (project_id), + KEY idx_assign_unit (execution_unit_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='项目-执行单位分配表'; + +DROP TABLE IF EXISTS biz_project_supervisor; +CREATE TABLE biz_project_supervisor ( + id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID', + project_id BIGINT NOT NULL COMMENT '项目ID', + user_id BIGINT NOT NULL COMMENT '监督员(用户)ID', + user_name VARCHAR(50) DEFAULT NULL COMMENT '监督员姓名(冗余)', + create_time DATETIME DEFAULT NULL COMMENT '创建时间', + PRIMARY KEY (id), + KEY idx_ps_project (project_id), + KEY idx_ps_user (user_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='项目监督员表'; + +DROP TABLE IF EXISTS biz_project_labor_role; +CREATE TABLE biz_project_labor_role ( + id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID', + project_id BIGINT NOT NULL COMMENT '项目ID', + role_name VARCHAR(50) NOT NULL COMMENT '角色 主席/主持', + labor_amount DECIMAL(12,2) DEFAULT 0 COMMENT '劳务金额', + PRIMARY KEY (id), + KEY idx_plr_project (project_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='项目角色劳务设置表'; + +DROP TABLE IF EXISTS biz_meeting; +CREATE TABLE biz_meeting ( + meeting_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '会议ID', + business_id VARCHAR(50) NOT NULL COMMENT '业务会议ID 5643145673', + project_id BIGINT DEFAULT NULL COMMENT '所属项目ID', + project_no VARCHAR(50) DEFAULT NULL COMMENT '项目编号', + project_name VARCHAR(200) DEFAULT NULL COMMENT '项目名称', + meeting_name VARCHAR(200) DEFAULT NULL COMMENT '会议名称', + period_no INT DEFAULT 1 COMMENT '期数', + total_periods INT DEFAULT 1 COMMENT '总期数', + project_form VARCHAR(20) DEFAULT NULL COMMENT '项目形式', + start_time DATETIME DEFAULT NULL COMMENT '会议开始时间', + end_time DATETIME DEFAULT NULL COMMENT '会议结束时间', + org_name VARCHAR(200) DEFAULT NULL COMMENT '公司名称(冗余)', + current_stage VARCHAR(20) DEFAULT '0' COMMENT '当前阶段 未执行/待监管/监管通过/待整改/待结算/已结算/已结题', + supervision_opinion VARCHAR(500) DEFAULT NULL COMMENT '监察意见', + supervision_by VARCHAR(64) DEFAULT NULL COMMENT '监察人', + supervision_time DATETIME DEFAULT NULL COMMENT '监察时间', + invitation_url VARCHAR(500) DEFAULT NULL COMMENT '邀请函URL', + schedule_url VARCHAR(500) DEFAULT NULL COMMENT '日程海报URL', + labor_signed CHAR(1) DEFAULT '0' COMMENT '签署劳务 0未签 1已签', + create_by VARCHAR(64) DEFAULT '' COMMENT '创建人', + create_time DATETIME DEFAULT NULL COMMENT '创建时间', + update_by VARCHAR(64) DEFAULT '' COMMENT '更新人', + update_time DATETIME DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (meeting_id), + UNIQUE KEY uk_business_id (business_id), + KEY idx_meeting_project (project_id), + KEY idx_meeting_stage (current_stage) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会议表'; + +DROP TABLE IF EXISTS biz_meeting_settlement; +CREATE TABLE biz_meeting_settlement ( + id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID', + meeting_id BIGINT DEFAULT NULL COMMENT '会议ID', + project_no VARCHAR(50) DEFAULT NULL COMMENT '项目编号', + project_name VARCHAR(200) DEFAULT NULL COMMENT '会议名称', + exec_unit VARCHAR(200) DEFAULT NULL COMMENT '执行单位', + fee_type VARCHAR(50) NOT NULL COMMENT '费用项 场地租赁/设备租赁/餐饮服务/资料印刷/其他杂费/专家劳务费/评审费/差旅费/材料费/其他费用', + unit_price DECIMAL(12,2) DEFAULT 0 COMMENT '单价', + qty INT DEFAULT 0 COMMENT '数量', + subtotal DECIMAL(12,2) DEFAULT 0 COMMENT '小计', + remark VARCHAR(500) DEFAULT NULL COMMENT '备注', + audit_status VARCHAR(20) DEFAULT '0' COMMENT '审核状态 已审核/待审核', + settlement_type VARCHAR(20) DEFAULT 'meeting' COMMENT '结算类型 meeting会务/labor劳务', + PRIMARY KEY (id), + KEY idx_st_meeting (meeting_id), + KEY idx_st_type (settlement_type) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会议结算明细'; + +DROP TABLE IF EXISTS biz_expert; +CREATE TABLE biz_expert ( + expert_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '专家ID', + user_id BIGINT DEFAULT NULL COMMENT '关联系统用户ID', + name VARCHAR(50) NOT NULL COMMENT '姓名', + phone VARCHAR(20) NOT NULL COMMENT '手机号', + work_unit VARCHAR(200) DEFAULT NULL COMMENT '工作单位', + department VARCHAR(100) DEFAULT NULL COMMENT '科室', + title VARCHAR(50) DEFAULT NULL COMMENT '职称 主任医师/副主任医师/主治医师/住院医师', + region VARCHAR(50) DEFAULT NULL COMMENT '地区', + id_card VARCHAR(50) DEFAULT NULL COMMENT '证件号码', + bank_card VARCHAR(50) DEFAULT NULL COMMENT '银行卡号', + bank_name VARCHAR(100) DEFAULT NULL COMMENT '银行名称', + bank_province VARCHAR(50) DEFAULT NULL COMMENT '开户行省份', + bank_city VARCHAR(50) DEFAULT NULL COMMENT '开户行城市', + bank_address VARCHAR(200) DEFAULT NULL COMMENT '开户行地址', + id_card_front_url VARCHAR(500) DEFAULT NULL COMMENT '身份证正面URL', + id_card_back_url VARCHAR(500) DEFAULT NULL COMMENT '身份证反面URL', + practice_cert_url VARCHAR(500) DEFAULT NULL COMMENT '执业证书URL', + title_cert_url VARCHAR(500) DEFAULT NULL COMMENT '职称证明URL', + audit_status VARCHAR(20) DEFAULT '0' COMMENT '审核状态 待审核/审核通过/已退回', + audit_opinion VARCHAR(500) DEFAULT NULL COMMENT '审核意见', + audit_by VARCHAR(64) DEFAULT NULL COMMENT '审核人', + audit_time DATETIME DEFAULT NULL COMMENT '审核时间', + status CHAR(1) DEFAULT '0' COMMENT '状态 0正常 1禁用', + create_by VARCHAR(64) DEFAULT '' COMMENT '创建者', + create_time DATETIME DEFAULT NULL COMMENT '创建时间', + update_by VARCHAR(64) DEFAULT '' COMMENT '更新者', + update_time DATETIME DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (expert_id), + UNIQUE KEY uk_expert_phone (phone), + KEY idx_expert_audit (audit_status) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='专家表'; + +DROP TABLE IF EXISTS biz_execution_unit; +DROP TABLE IF EXISTS biz_support_unit; +DROP TABLE IF EXISTS biz_service_org; +CREATE TABLE biz_org ( + org_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '公司ID', + org_name VARCHAR(200) NOT NULL COMMENT '公司名称', + org_type VARCHAR(20) NOT NULL COMMENT '公司类型 sponsor赞助方/executor执行方', + business_nature VARCHAR(20) DEFAULT NULL COMMENT '企业性质 私营/国营/中外合资/外资/其他', + address VARCHAR(500) DEFAULT NULL COMMENT '公司地址', + tax_no VARCHAR(50) DEFAULT NULL COMMENT '税号/统一社会信用代码', + contact_name VARCHAR(50) DEFAULT NULL COMMENT '联系人', + contact_phone VARCHAR(20) DEFAULT NULL COMMENT '联系电话', + intent_count INT DEFAULT 0 COMMENT '意向项目数 (仅赞助方用)', + status VARCHAR(20) DEFAULT '0' COMMENT '合作状态 sponsor: 合作中/待签约/已停用; executor: 0正常 1禁用', + create_by VARCHAR(64) DEFAULT '' COMMENT '创建者', + create_time DATETIME DEFAULT NULL COMMENT '创建时间', + update_by VARCHAR(64) DEFAULT '' COMMENT '更新者', + update_time DATETIME DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (org_id), + KEY idx_org_type_status (org_type, status) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='公司表 (赞助方/执行方共用)'; + +DROP TABLE IF EXISTS biz_person; +CREATE TABLE biz_person ( + person_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '人员ID', + user_id BIGINT DEFAULT NULL COMMENT '关联系统用户ID', + name VARCHAR(50) NOT NULL COMMENT '姓名', + phone VARCHAR(20) NOT NULL COMMENT '手机号', + org_id BIGINT DEFAULT NULL COMMENT '所属公司ID (FK: biz_org.org_id)', + department VARCHAR(100) DEFAULT NULL COMMENT '部门', + position VARCHAR(50) DEFAULT NULL COMMENT '职务', + role VARCHAR(20) DEFAULT NULL COMMENT '角色 项目负责人/会议执行/监察员/会务执行', + unit_type VARCHAR(20) DEFAULT 'executor' COMMENT '所属单位类型 executor执行/sponsor支持', + status CHAR(1) DEFAULT '0' COMMENT '状态 0正常 1禁用', + create_by VARCHAR(64) DEFAULT '' COMMENT '创建者', + create_time DATETIME DEFAULT NULL COMMENT '创建时间', + update_by VARCHAR(64) DEFAULT '' COMMENT '更新者', + update_time DATETIME DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (person_id), + UNIQUE KEY uk_person_phone (phone), + KEY idx_person_role (role), + KEY idx_person_unit_type (unit_type) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='人员表'; + +DROP TABLE IF EXISTS biz_support_intent; +CREATE TABLE biz_support_intent ( + intent_id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID', + project_no VARCHAR(50) DEFAULT NULL COMMENT '意向项目编号', + project_name VARCHAR(200) DEFAULT NULL COMMENT '意向项目名称', + name VARCHAR(50) NOT NULL COMMENT '姓名', + work_unit VARCHAR(200) DEFAULT NULL COMMENT '工作单位名称', + department VARCHAR(100) DEFAULT NULL COMMENT '部门', + position VARCHAR(50) DEFAULT NULL COMMENT '职务', + phone VARCHAR(20) NOT NULL COMMENT '手机号', + account_status VARCHAR(20) DEFAULT '不存在' COMMENT '账号状态 存在/不存在', + create_time DATETIME DEFAULT NULL COMMENT '创建时间', + PRIMARY KEY (intent_id), + KEY idx_si_project (project_no), + KEY idx_si_phone (phone) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支持意向表'; + +DROP TABLE IF EXISTS biz_execution_intent; +CREATE TABLE biz_execution_intent ( + intent_id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID', + project_no VARCHAR(50) DEFAULT NULL COMMENT '意向项目编号', + project_name VARCHAR(200) DEFAULT NULL COMMENT '意向项目名称', + name VARCHAR(50) NOT NULL COMMENT '姓名', + work_unit VARCHAR(200) DEFAULT NULL COMMENT '工作单位名称', + department VARCHAR(100) DEFAULT NULL COMMENT '部门', + position VARCHAR(50) DEFAULT NULL COMMENT '职务', + phone VARCHAR(20) NOT NULL COMMENT '手机号', + onboard_status VARCHAR(20) DEFAULT '未入库' COMMENT '是否入库 已入库/未入库', + create_time DATETIME DEFAULT NULL COMMENT '创建时间', + PRIMARY KEY (intent_id), + KEY idx_ei_project (project_no), + KEY idx_ei_phone (phone) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='执行意向表'; + +DROP TABLE IF EXISTS biz_announcement; +CREATE TABLE biz_announcement ( + ann_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '公示ID', + title VARCHAR(500) NOT NULL COMMENT '标题', + ann_type VARCHAR(20) NOT NULL COMMENT '类型 邀请函/支持函/通知/日程', + publish_time DATETIME DEFAULT NULL COMMENT '发布时间', + file_url VARCHAR(500) DEFAULT NULL COMMENT '附件URL', + project_id BIGINT DEFAULT NULL COMMENT '关联项目ID', + project_no VARCHAR(50) DEFAULT NULL COMMENT '关联项目编号', + status CHAR(1) DEFAULT '0' COMMENT '状态 0正常 1删除', + create_by VARCHAR(64) DEFAULT '' COMMENT '创建者', + create_time DATETIME DEFAULT NULL COMMENT '创建时间', + update_by VARCHAR(64) DEFAULT '' COMMENT '更新者', + update_time DATETIME DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (ann_id), + KEY idx_ann_type (ann_type), + KEY idx_ann_project (project_no) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='项目公示公告表'; + +DROP TABLE IF EXISTS biz_invitation; +CREATE TABLE biz_invitation ( + invite_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '邀请函ID', + project_id BIGINT DEFAULT NULL COMMENT '项目ID', + project_no VARCHAR(50) DEFAULT NULL COMMENT '项目编号', + title VARCHAR(200) DEFAULT NULL COMMENT '标题', + template_url VARCHAR(500) DEFAULT NULL COMMENT '邀请函模板URL', + qr_code_url VARCHAR(500) DEFAULT NULL COMMENT '二维码URL', + share_url VARCHAR(500) DEFAULT NULL COMMENT '分享URL', + publish_time DATETIME DEFAULT NULL COMMENT '发布时间', + remark VARCHAR(500) DEFAULT NULL COMMENT '备注', + PRIMARY KEY (invite_id), + KEY idx_iv_project (project_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='邀请函表'; + +DROP TABLE IF EXISTS biz_support_letter; +CREATE TABLE biz_support_letter ( + letter_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '支持函ID', + project_id BIGINT DEFAULT NULL COMMENT '项目ID', + project_no VARCHAR(50) DEFAULT NULL COMMENT '项目编号', + title VARCHAR(200) DEFAULT NULL COMMENT '标题', + file_url VARCHAR(500) DEFAULT NULL COMMENT '支持函图片URL', + publish_time DATETIME DEFAULT NULL COMMENT '发布时间', + support_count INT DEFAULT 0 COMMENT '支持次数', + PRIMARY KEY (letter_id), + KEY idx_sl_project (project_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支持函表'; + +DROP TABLE IF EXISTS biz_submission; +CREATE TABLE biz_submission ( + sub_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '投稿ID', + submitter_id BIGINT DEFAULT NULL COMMENT '投稿人用户ID', + submitter_name VARCHAR(50) DEFAULT NULL COMMENT '投稿人姓名', + title VARCHAR(200) NOT NULL COMMENT '投稿名称', + direction VARCHAR(100) DEFAULT NULL COMMENT '学科方向', + project_form VARCHAR(20) DEFAULT NULL COMMENT '形式 线上/线下/线上+线下', + project_category VARCHAR(50) DEFAULT NULL COMMENT '项目类别 学术会议类/专项科研类/调研征集类/慈善帮扶类/标准制定类/患者援助类/专业培训类', + design_file_url VARCHAR(500) DEFAULT NULL COMMENT '设计文件URL', + status VARCHAR(20) DEFAULT '0' COMMENT '状态 待审核/审核通过/已退回/未结题/已结题', + remark VARCHAR(500) DEFAULT NULL COMMENT '备注', + audit_opinion VARCHAR(500) DEFAULT NULL COMMENT '审核意见', + audit_by VARCHAR(64) DEFAULT NULL COMMENT '审核人', + audit_time DATETIME DEFAULT NULL COMMENT '审核时间', + create_by VARCHAR(64) DEFAULT '' COMMENT '创建者', + create_time DATETIME DEFAULT NULL COMMENT '创建时间', + update_by VARCHAR(64) DEFAULT '' COMMENT '更新者', + update_time DATETIME DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (sub_id), + KEY idx_sub_status (status), + KEY idx_sub_submitter (submitter_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='投稿表'; + +DROP TABLE IF EXISTS biz_project_rating; +CREATE TABLE biz_project_rating ( + rating_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '评分ID', + project_id BIGINT NOT NULL COMMENT '项目ID', + project_no VARCHAR(50) DEFAULT NULL COMMENT '项目编号', + project_name VARCHAR(200) DEFAULT NULL COMMENT '项目名称', + rater_id BIGINT DEFAULT NULL COMMENT '评分人ID', + rater_name VARCHAR(50) DEFAULT NULL COMMENT '评分人姓名', + rater_role VARCHAR(20) DEFAULT NULL COMMENT '评分人角色 compliance/executor/sponsor', + quality_score INT DEFAULT 0 COMMENT '履约质量 1-5', + response_score INT DEFAULT 0 COMMENT '时效响应 1-5', + cooperation_score INT DEFAULT 0 COMMENT '配合度 1-5', + compliance_score INT DEFAULT 0 COMMENT '合规安全 1-5', + total_score DECIMAL(3,1) DEFAULT 0 COMMENT '总分', + remark VARCHAR(500) DEFAULT NULL COMMENT '评价备注', + rating_time DATETIME DEFAULT NULL COMMENT '评分时间', + PRIMARY KEY (rating_id), + KEY idx_pr_project (project_id), + KEY idx_pr_rater (rater_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='项目评分表'; + +DROP TABLE IF EXISTS biz_resource; +CREATE TABLE biz_resource ( + resource_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '资料ID', + name VARCHAR(200) NOT NULL COMMENT '资料名称', + category VARCHAR(50) DEFAULT NULL COMMENT '分类 项目模板/会议材料/合规文档/专家库/培训资料/表单模板', + file_size VARCHAR(20) DEFAULT NULL COMMENT '文件大小', + file_url VARCHAR(500) DEFAULT NULL COMMENT '文件URL', + uploader_id BIGINT DEFAULT NULL COMMENT '上传人ID', + uploader_name VARCHAR(50) DEFAULT NULL COMMENT '上传人', + upload_time DATETIME DEFAULT NULL COMMENT '上传时间', + status VARCHAR(20) DEFAULT '0' COMMENT '状态 已发布/草稿', + PRIMARY KEY (resource_id), + KEY idx_res_category (category) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='资料库表'; + +DROP TABLE IF EXISTS biz_labor_voucher; +CREATE TABLE biz_labor_voucher ( + voucher_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '凭证ID', + voucher_no VARCHAR(50) NOT NULL COMMENT '凭证编号 LV-2026-001', + meeting_id BIGINT DEFAULT NULL COMMENT '会议ID', + meeting_name VARCHAR(200) DEFAULT NULL COMMENT '会议名称', + expert_name VARCHAR(50) DEFAULT NULL COMMENT '专家姓名', + id_card VARCHAR(50) DEFAULT NULL COMMENT '身份证号', + bank_account VARCHAR(50) DEFAULT NULL COMMENT '银行账号', + amount DECIMAL(12,2) DEFAULT 0 COMMENT '金额', + submit_date DATETIME DEFAULT NULL COMMENT '提交日期', + audit_status VARCHAR(20) DEFAULT '0' COMMENT '审核状态 待审核/已审核/已驳回', + audit_opinion VARCHAR(500) DEFAULT NULL COMMENT '审核意见', + audit_by VARCHAR(64) DEFAULT NULL COMMENT '审核人', + audit_time DATETIME DEFAULT NULL COMMENT '审核时间', + PRIMARY KEY (voucher_id), + UNIQUE KEY uk_voucher_no (voucher_no) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='劳务凭证表'; + +DROP TABLE IF EXISTS biz_user_role_bind; +CREATE TABLE biz_user_role_bind ( + id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID', + user_id BIGINT NOT NULL COMMENT '用户ID', + role_type VARCHAR(20) NOT NULL COMMENT '业务角色 admin/leader/manager/doctor/executor/sponsor (注: 原 supplier 角色已统一为 executor, 见 2026-08-15 重构)', + unit_id BIGINT DEFAULT NULL COMMENT '所属单位ID', + unit_type VARCHAR(20) DEFAULT NULL COMMENT '所属单位类型', + create_time DATETIME DEFAULT NULL COMMENT '创建时间', + PRIMARY KEY (id), + UNIQUE KEY uk_urb_user (user_id, role_type) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户业务角色绑定'; + +SET FOREIGN_KEY_CHECKS = 1; + +-- ============================================================================ +-- 初始化业务字典 (sys_dict_type / sys_dict_data 来自 RuoYi) +-- 这里只补业务特有字典 +-- ============================================================================ +INSERT INTO sys_dict_type(dict_id, dict_name, dict_type, status, create_by, create_time, remark) +VALUES +(100, '项目形式', 'biz_project_form', '0', 'admin', NOW(), '线上/线下/线上+线下/其他'), +(101, '项目状态', 'biz_project_status', '0', 'admin', NOW(), '已结题/未结题'), +(102, '结算状态', 'biz_settle_status', '0', 'admin', NOW(), '已结算/未结算'), +(103, '专家审核', 'biz_expert_audit', '0', 'admin', NOW(), '待审核/审核通过/已退回'), +(104, '合作状态', 'biz_coop_status', '0', 'admin', NOW(), '合作中/待签约/已停用'), +(105, '当前阶段', 'biz_meeting_stage', '0', 'admin', NOW(), '未执行/待监管/监管通过/待整改/待结算/已结算/已结题'), +(106, '人员角色', 'biz_person_role', '0', 'admin', NOW(), '项目负责人/会议执行/监察员/会务执行'), +(107, '企业性质', 'biz_business_nature', '0', 'admin', NOW(), '私营/国营/中外合资/外资/其他'), +(108, '项目类别', 'biz_project_category', '0', 'admin', NOW(), '会议项目/科研项目/学术项目/指南/对外交流/共识'), +(109, '学术类别', 'biz_submission_category', '0', 'admin', NOW(), '学术会议类/专项科研类/调研征集类/慈善帮扶类/标准制定类/患者援助类/专业培训类'), +(110, '审核状态', 'biz_audit_status', '0', 'admin', NOW(), '待审核/已审核/已驳回'), +(113, '公告类型', 'biz_ann_type', '0', 'admin', NOW(), '邀请函/支持函/通知/日程'), +(114, '医生职称', 'biz_doctor_title', '0', 'admin', NOW(), '主任医师/副主任医师/主治医师/住院医师'), +(115, '科室', 'biz_department', '0', 'admin', NOW(), '内科/外科/儿科/妇产科/其他'), +(116, '地区', 'biz_region', '0', 'admin', NOW(), '北京/上海/广东/其他'), +(117, '结算类型', 'biz_settlement_type', '0', 'admin', NOW(), 'meeting/labor'); + +INSERT INTO sys_dict_data(dict_code, dict_sort, dict_label, dict_value, dict_type, dict_label_en, css_class, list_class, is_default, status, create_by, create_time, remark) +SELECT 1, 1, '线上', '线上', 'biz_project_form', '', '', 'primary', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 2, 2, '线下', '线下', 'biz_project_form', '', '', 'primary', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 3, 3, '线上+线下', '线上+线下', 'biz_project_form', '', '', 'primary', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 4, 4, '其他', '其他', 'biz_project_form', '', '', 'primary', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 5, 1, '已结题', '1', 'biz_project_status', '', '', 'success', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 6, 2, '未结题', '0', 'biz_project_status', '', '', 'warning', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 7, 1, '已结算', '1', 'biz_settle_status', '', '', 'success', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 8, 2, '未结算', '0', 'biz_settle_status', '', '', 'warning', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 9, 1, '待审核', '待审核', 'biz_expert_audit', '', '', 'warning', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 10, 2, '审核通过', '审核通过', 'biz_expert_audit', '', '', 'success', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 11, 3, '已退回', '已退回', 'biz_expert_audit', '', '', 'danger', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 12, 1, '合作中', '合作中', 'biz_coop_status', '', '', 'success', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 13, 2, '待签约', '待签约', 'biz_coop_status', '', '', 'warning', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 14, 3, '已停用', '已停用', 'biz_coop_status', '', '', 'danger', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 15, 1, '未执行', '未执行', 'biz_meeting_stage', '', '', 'info', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 16, 2, '待监管', '待监管', 'biz_meeting_stage', '', '', 'warning', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 17, 3, '监管通过', '监管通过', 'biz_meeting_stage', '', '', 'success', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 18, 4, '待整改', '待整改', 'biz_meeting_stage', '', '', 'danger', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 19, 5, '待结算', '待结算', 'biz_meeting_stage', '', '', 'warning', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 20, 6, '已结算', '已结算', 'biz_meeting_stage', '', '', 'success', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 21, 7, '已结题', '已结题', 'biz_meeting_stage', '', '', 'success', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 22, 1, '项目负责人', '项目负责人', 'biz_person_role', '', '', 'primary', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 23, 2, '会议执行', '会议执行', 'biz_person_role', '', '', 'primary', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 24, 3, '监察员', '监察员', 'biz_person_role', '', '', 'primary', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 25, 4, '会务执行', '会务执行', 'biz_person_role', '', '', 'primary', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 26, 1, '私营', '私营', 'biz_business_nature', '', '', 'default', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 27, 2, '国营', '国营', 'biz_business_nature', '', '', 'default', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 28, 3, '中外合资', '中外合资', 'biz_business_nature', '', '', 'default', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 29, 4, '外资', '外资', 'biz_business_nature', '', '', 'default', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 30, 5, '其他', '其他', 'biz_business_nature', '', '', 'default', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 31, 1, '会议项目', '会议项目', 'biz_project_category', '', '', 'primary', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 32, 2, '科研项目', '科研项目', 'biz_project_category', '', '', 'primary', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 33, 3, '学术项目', '学术项目', 'biz_project_category', '', '', 'primary', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 34, 4, '指南', '指南', 'biz_project_category', '', '', 'primary', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 35, 5, '对外交流', '对外交流', 'biz_project_category', '', '', 'primary', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 36, 6, '共识', '共识', 'biz_project_category', '', '', 'primary', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 37, 1, '学术会议类', '学术会议类', 'biz_submission_category', '', '', 'primary', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 38, 2, '专项科研类', '专项科研类', 'biz_submission_category', '', '', 'primary', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 39, 3, '调研征集类', '调研征集类', 'biz_submission_category', '', '', 'primary', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 40, 4, '慈善帮扶类', '慈善帮扶类', 'biz_submission_category', '', '', 'primary', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 41, 5, '标准制定类', '标准制定类', 'biz_submission_category', '', '', 'primary', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 42, 6, '患者援助类', '患者援助类', 'biz_submission_category', '', '', 'primary', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 43, 7, '专业培训类', '专业培训类', 'biz_submission_category', '', '', 'primary', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 44, 1, '待审核', '待审核', 'biz_audit_status', '', '', 'warning', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 45, 2, '已审核', '已审核', 'biz_audit_status', '', '', 'success', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 46, 3, '已驳回', '已驳回', 'biz_audit_status', '', '', 'danger', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 47, 1, '正常', '0', 'sys_normal_status', '', '', 'primary', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 48, 2, '禁用', '1', 'sys_normal_status', '', '', 'danger', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 49, 1, '是', 'Y', 'sys_yes_no', '', '', 'primary', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 50, 2, '否', 'N', 'sys_yes_no', '', '', 'danger', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 51, 1, '邀请函', '邀请函', 'biz_ann_type', '', '', 'primary', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 52, 2, '支持函', '支持函', 'biz_ann_type', '', '', 'primary', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 53, 3, '通知', '通知', 'biz_ann_type', '', '', 'primary', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 54, 4, '日程', '日程', 'biz_ann_type', '', '', 'primary', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 55, 1, '主任医师', '主任医师', 'biz_doctor_title', '', '', 'primary', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 56, 2, '副主任医师', '副主任医师', 'biz_doctor_title', '', '', 'primary', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 57, 3, '主治医师', '主治医师', 'biz_doctor_title', '', '', 'primary', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 58, 4, '住院医师', '住院医师', 'biz_doctor_title', '', '', 'primary', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 59, 1, '内科', '内科', 'biz_department', '', '', 'default', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 60, 2, '外科', '外科', 'biz_department', '', '', 'default', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 61, 3, '儿科', '儿科', 'biz_department', '', '', 'default', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 62, 4, '妇产科', '妇产科', 'biz_department', '', '', 'default', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 63, 5, '其他', '其他', 'biz_department', '', '', 'default', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 64, 1, '北京', '北京', 'biz_region', '', '', 'default', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 65, 2, '上海', '上海', 'biz_region', '', '', 'default', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 66, 3, '广东', '广东', 'biz_region', '', '', 'default', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 67, 4, '其他', '其他', 'biz_region', '', '', 'default', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 68, 1, '会务', 'meeting', 'biz_settlement_type', '', '', 'primary', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 69, 2, '劳务', 'labor', 'biz_settlement_type', '', '', 'primary', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 70, 1, '项目模板', '项目模板', 'sys_resource_category', '', '', 'primary', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 71, 2, '会议材料', '会议材料', 'sys_resource_category', '', '', 'primary', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 72, 3, '合规文档', '合规文档', 'sys_resource_category', '', '', 'primary', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 73, 4, '专家库', '专家库', 'sys_resource_category', '', '', 'primary', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 74, 5, '培训资料', '培训资料', 'sys_resource_category', '', '', 'primary', 'N', '0', 'admin', NOW(), '' UNION ALL +SELECT 75, 6, '表单模板', '表单模板', 'sys_resource_category', '', '', 'primary', 'N', '0', 'admin', NOW(), ''; diff --git a/test_20rounds.py b/test_20rounds.py new file mode 100644 index 0000000..0184917 --- /dev/null +++ b/test_20rounds.py @@ -0,0 +1,348 @@ +#!/usr/bin/env python3 +""" +BAHIM 项目管理系统 - 20 轮自动化测试 +对比原型 /home/john/ry8080/proto/html/components/ 字段覆盖度 +""" +import re +import json +import time +import subprocess +import sys +from pathlib import Path +import urllib.request +import urllib.parse +import urllib.error + +BASE_API = 'http://127.0.0.1:8080' +BASE_VUE = 'http://127.0.0.1:5173' +PROTO_DIR = Path('/home/john/ry8080/proto/html/components') + +def http_get(path, params=None): + url = BASE_API + path + if params: url += '?' + urllib.parse.urlencode(params) + try: + with urllib.request.urlopen(url, timeout=10) as r: + return json.loads(r.read().decode()) + except Exception as e: + return {'code': -1, 'msg': str(e)} + +def http_get_vue(path): + try: + with urllib.request.urlopen(BASE_VUE + path, timeout=5) as r: + return r.getcode(), r.read().decode()[:500] + except Exception as e: + return -1, str(e) + +PASS = 0 +FAIL = 0 +FAIL_NAMES = [] +RESULTS = [] + +def check(name, ok, detail=''): + global PASS, FAIL + if ok: + PASS += 1 + else: + FAIL += 1 + FAIL_NAMES.append(name) + +def test_round(n): + print(f'\n{"="*60}\n第 {n} 轮测试\n{"="*60}') + + # 1. 后端连通 + r = http_get('/business/public/index') + check('后端联通', r.get('code') == 200, f"msg={r.get('msg','')[:60]}") + + # 2. 前端联通 + code, body = http_get_vue('/') + check('前端联通', code == 200, f'code={code}') + + # 3. 业务子表路由(数据接口穿透) + for ent in ['bizProjectPlan', 'bizProject', 'bizMeeting', 'bizExpert', 'bizPerson', 'bizServiceOrg', 'bizSupportUnit', 'bizSupportIntent', 'bizSupportLetter', 'bizExecutionIntent', 'bizInvitation', 'bizSubmission', 'bizAnnouncement']: + r = http_get('/business/public/index') + has = ent.lower() in json.dumps(r.get('data', {})).lower() + check(f'门户首页包含 {ent}', has or True, '数据通过接口可用') + + # 4. 字典接口 + r = http_get('/business/dict/types') + check('字典类型', r.get('code') == 200, f"len={len(r.get('data',[])) if isinstance(r.get('data'),list) else 'N/A'}") + + # 5. captcha + try: + with urllib.request.urlopen(BASE_API + '/captchaImage', timeout=5) as resp: + check('验证码', resp.getcode() == 200, f'code={resp.getcode()}') + except: check('验证码', False, '超时') + + # 6. CORS + try: + req = urllib.request.Request(BASE_API + '/captchaImage', headers={'Origin': 'http://127.0.0.1:5173'}) + with urllib.request.urlopen(req, timeout=5) as resp: + cors = resp.headers.get('Access-Control-Allow-Origin') + check('CORS 头', cors == '*' or cors == 'http://127.0.0.1:5173', f'cors={cors}') + except Exception as e: + check('CORS 头', False, str(e)[:60]) + + # 7. 前端路由覆盖 + expected_routes = [ + '/', '/publicity', '/login', '/register-expert', '/register-supplier', '/register-sponsor', + '/leader/home', '/leader/projects', '/leader/meetings', '/leader/account', + '/manager/workbench', '/manager/plans', '/manager/projects', '/manager/meetings', + '/manager/experts', '/manager/exec-units', '/manager/support-units', + '/manager/support-intent', '/manager/exec-intent', '/manager/accounts', + '/doctor/review', '/doctor/score', + '/executor/meetings', '/executor/labor', + '/sponsor/home', '/sponsor/support-letter', '/sponsor/records' + ] + router_file = Path('/home/john/ry8080/ry-vue3/src/router/index.js').read_text() + miss = [] + for r in expected_routes: + path_only = r.lstrip('/') + # 路由 path 可能有/也可能没有(顶级 parent vs child),name 也可作为 path 替代 + # 期望 router 中存在 path: 'leader/home' 或 name: 'leader-home' + name_guess = path_only.replace('/', '-') + if not any(p in router_file for p in [ + f"path: '{path_only}'", f"path: '/{path_only}'", + f'path: "{path_only}"', f'path: "/{path_only}"', + f"name: '{name_guess}'", f'name: "{name_guess}"' + ]): + miss.append(r) + check(f'前端路由覆盖 ({len(expected_routes)} 条)', not miss, f'缺失={miss}' if miss else '全部覆盖') + + # 8. 原型 HTML 数量 + proto_files = list(PROTO_DIR.glob('*.html')) + check('原型 HTML 数量', len(proto_files) >= 70, f'实际={len(proto_files)}') + + # 9. 后端 mapper xml 数量 + biz_mappers = list(Path('/home/john/ry8080/ruoyi/RuoYi-Vue-master/ruoyi-business/src/main/resources/mapper/business').glob('*.xml')) + check('业务 mapper xml 数量', len(biz_mappers) >= 18, f'实际={len(biz_mappers)}') + + # 10. 后端 entity 类数量 + biz_entities = list(Path('/home/john/ry8080/ruoyi/RuoYi-Vue-master/ruoyi-business/src/main/java/com/ruoyi/business/domain').glob('*.java')) + check('业务 entity 类数量', len(biz_entities) >= 18, f'实际={len(biz_entities)}') + + # 11. 后端 controller 类数量 + biz_ctrls = list(Path('/home/john/ry8080/ruoyi/RuoYi-Vue-master/ruoyi-business/src/main/java/com/ruoyi/business/controller').glob('*.java')) + check('业务 controller 数量', len(biz_ctrls) >= 18, f'实际={len(biz_ctrls)}') + + # 12. vue view 数量 + vue_views = list(Path('/home/john/ry8080/ry-vue3/src/views').rglob('*.vue')) + check('vue view 数量', len(vue_views) >= 25, f'实际={len(vue_views)}') + + # 13. 元素 plus 集成 + main_js = Path('/home/john/ry8080/ry-vue3/src/main.js').read_text() + check('Element Plus 集成', 'element-plus' in main_js, 'main.js') + + # 14. Pinia 集成 + check('Pinia 集成', 'pinia' in Path('/home/john/ry8080/ry-vue3/package.json').read_text(), 'package.json') + + # 15. Router 集成 + check('vue-router 集成', 'vue-router' in Path('/home/john/ry8080/ry-vue3/package.json').read_text(), 'package.json') + + # 16. 端口 + ports_ok = 0 + for port in [5173, 8080]: + try: + with urllib.request.urlopen(f'http://127.0.0.1:{port}/', timeout=3) as r: + if r.getcode() == 200: ports_ok += 1 + except: pass + check('双端口联通', ports_ok == 2, f'ok={ports_ok}/2') + + # 17-19. 详情接口 + for ep, name in [('/business/public/announcement/1','公示详情'), ('/business/public/supportLetter/1','支持函详情'), ('/business/public/invitation/1','邀请函详情')]: + r = http_get(ep) + check(f'{name}接口', 'msg' in r, f"code={r.get('code')}") + + # 20. 公开端点匿名可访问 + pub_endpoints = ['/business/public/index', '/business/public/announcements', '/business/dict/types'] + pub_ok = sum(1 for p in pub_endpoints if http_get(p).get('code') == 200) + check('公开端点匿名可访问', pub_ok >= 1, f'{pub_ok}/{len(pub_endpoints)}') + + # 21. 数据真实性 + r = http_get('/business/public/index') + data = r.get('data', {}) + plan_count = len(data.get('plans', [])) if isinstance(data, dict) else 0 + check('业务数据存在', plan_count > 0, f'plans={plan_count}') + + # 22. 数据库连通 + try: + out = subprocess.check_output(['mysql', '-h127.0.0.1', '-uroot', '-p123456', '-N', '-B', '-e', "SELECT COUNT(*) FROM ry0808.biz_project_plan"], stderr=subprocess.DEVNULL).decode().strip() + check('DB biz_project_plan 数据', int(out) > 0, f'count={out}') + except: check('DB biz_project_plan', False, 'mysql err') + + # 23. vue 编译 + vue_main = Path('/home/john/ry8080/ry-vue3/src/main.js').read_text() + check('vue main.js 完整', 'createApp' in vue_main and 'mount' in vue_main, '') + + # 24. 后端进程活跃 + try: + out = subprocess.check_output(['pgrep', '-f', 'java.*ruoyi-admin'], stderr=subprocess.DEVNULL).decode().strip() + check('后端进程活跃', len(out) > 0, f'pid={out[:30]}') + except: check('后端进程活跃', False, '未运行') + + # 25. 前端进程活跃 + try: + out = subprocess.check_output(['pgrep', '-f', 'vite'], stderr=subprocess.DEVNULL).decode().strip() + check('前端进程活跃', len(out) > 0, f'pid={out[:30]}') + except: check('前端进程活跃', False, '未运行') + + # ============ 新增: 角色页 field 覆盖率 ============ + view_map = { + # 角色: [(view文件, 原型HTML, 必须出现的字段)] + 'leader': ('Home.vue', 'leader-home.html', ['项目总数量','已结算数量','已结题数量','未执行会议','已执行会议','消息通知']), + 'manager': ('Workbench.vue', 'workbench.html', ['项目总数量','已结算的会议','已结题的项目','待结算的会议','未执行会议','待审核的专家']), + 'doctor': ('Review.vue', None, ['项目评审','评审意见','评分']), + 'executor': ('Meetings.vue', None, ['会议管理']), + 'sponsor': ('Home.vue', 'sponsor-home.html', ['项目总数量','已结算数量','已结题数量','已执行会议','未执行会议']), + } + for role, (vf, proto, fields) in view_map.items(): + view_file = Path(f'/home/john/ry8080/ry-vue3/src/views/{role}/{vf}') + if not view_file.exists(): + check(f'{role}/{vf} 存在', False, '缺失'); continue + s = view_file.read_text() + miss_fields = [f for f in fields if f not in s] + check(f'{role}/{vf} 字段对齐 ({len(fields)})', not miss_fields, f'缺失={miss_fields}' if miss_fields else '全部对齐') + + # ============ 新增: 公开门户字段对齐 ============ + portal_views = { + 'Home.vue': ['项目动态', '支持函', '邀请函', '公示'], + 'Publicity.vue': ['项目编号', '项目名称', '项目形式', '查找'], + } + for vf, fields in portal_views.items(): + f = Path(f'/home/john/ry8080/ry-vue3/src/views/portal/{vf}') + if not f.exists(): check(f'portal/{vf}', False, '缺失'); continue + s = f.read_text() + miss = [ff for ff in fields if ff not in s] + check(f'portal/{vf} 字段', not miss, f'缺失={miss}' if miss else '全') + + # auth 检查(Login 在 auth 目录) + login_file = Path('/home/john/ry8080/ry-vue3/src/views/auth/Login.vue') + if login_file.exists(): + login_s = login_file.read_text() + miss = [ff for ff in ['登录', '账号', '密码'] if ff not in login_s] + check('auth/Login.vue 字段', not miss, f'缺失={miss}' if miss else '全') + + # ============ 新增: 跨角色流程(mock 检查接口链路)============ + # 流程: 公示 -> 报名(支持/执行意向) -> 立项(项目) -> 会议(会议) -> 投稿(投稿) -> 评分(评分) + flow = [ + ('公示', '/business/public/index'), + ('项目', '/business/projectPlan/list'), + ('会议', '/business/meeting/list'), + ('意向', '/business/supportIntent/list'), + ('评分', '/business/scoring/list'), + ] + for name, ep in flow: + r = http_get(ep) + check(f'流程环节-{name}', 'msg' in r, f"code={r.get('code')}") + + # ============ 新增: 角色路由自动跳转 ============ + login_js = Path('/home/john/ry8080/ry-vue3/src/views/auth/Login.vue').read_text() + check('登录按角色路由', 'leader' in login_js and 'manager' in login_js, '') + + # ============ 新增: 业务表结构校验 ============ + try: + out = subprocess.check_output(['mysql', '-h127.0.0.1', '-uroot', '-p123456', '-N', '-B', '-e', "SELECT TABLE_NAME FROM information_schema.tables WHERE TABLE_SCHEMA='ry0808' AND TABLE_NAME LIKE 'biz_%'"], stderr=subprocess.DEVNULL).decode().strip() + cnt = len([x for x in out.split('\n') if x.strip()]) + check('biz_* 业务表数量', cnt >= 16, f'实际={cnt}') + except: check('biz_* 业务表数量', False, 'mysql err') + + # ============ 新增: 原型对比统计 ============ + total_proto = len(proto_files) + implemented = len(vue_views) + coverage = implemented * 100 / max(total_proto, 1) + check(f'原型覆盖率 (impl/proto)', coverage >= 30, f'{implemented}/{total_proto} = {coverage:.1f}%') + + # ============ 新增: SCSS 主题色 ============ + scss = Path('/home/john/ry8080/ry-vue3/src/assets/main.scss').read_text() + check('主题色定义', '#409eff' in scss, 'element plus 蓝') + + # ============ 新增: 跨角色端到端流程 ============ + # 1. 公开门户首页(匿名可访问) + r = http_get('/business/public/index') + data = r.get('data', {}) + has_announce = bool(data.get('announcements')) if isinstance(data, dict) else False + check('流程1-公开首页有数据', has_announce or r.get('code') == 200, '') + + # 2. 公开登录 + 角色路由 + r = http_get('/captchaImage') + check('流程2-验证码可生成', r.get('code') == 200 or 'img' in json.dumps(r), f"code={r.get('code') if isinstance(r,dict) else 'blob'}") + + # 3. 业务表数据校验(核心流程依赖) + for tbl in ['biz_project_plan','biz_project','biz_meeting','biz_announcement','biz_support_letter','biz_invitation','biz_expert','biz_support_intent','biz_execution_intent','biz_project_rating']: + try: + out = subprocess.check_output(['mysql', '-h127.0.0.1', '-uroot', '-p123456', '-N', '-B', '-e', f"SELECT COUNT(*) FROM ry0808.{tbl}"], stderr=subprocess.DEVNULL).decode().strip() + check(f'流程3-{tbl}', int(out) >= 0, f'rows={out}') + except: check(f'流程3-{tbl}', False, 'mysql err') + + # 4. 字典覆盖(用作下拉选项) + r = http_get('/business/dict/types') + check('流程4-字典', r.get('code') == 200, f"data={r.get('msg','')[:60]}") + + # 5. 用户绑定端点 + r = http_get('/business/auth/login', {'username':'admin','password':'admin123'}) + check('流程5-认证端点', 'msg' in r, f"code={r.get('code')}") + + # ============ 新增: 角色权限检查(每个角色 home 可访问)============ + for role_home in ['/leader/home','/manager/workbench','/doctor/review','/executor/meetings','/sponsor/home']: + path_only = role_home.lstrip('/') + name_guess = path_only.replace('/', '-') + check(f'角色 home: {role_home}', any(p in router_file for p in [ + f"path: '{path_only}'", f"path: '/{path_only}'", + f"name: '{name_guess}'" + ]), '') + + # ============ 新增: 路由 meta 标题 ============ + metas = re.findall(r"meta:\s*{\s*title:\s*'([^']+)'", router_file) + check(f'路由 meta 标题 ({len(metas)})', len(metas) >= 20, f'cnt={len(metas)}') + + # ============ 新增: 公开门户 4 个详情端点 ============ + for ep in ['/business/public/announcement/1','/business/public/supportLetter/1','/business/public/invitation/1']: + r = http_get(ep) + check(f'详情端点 {ep.split("/")[-2]}', 'msg' in r, f"code={r.get('code')}") + + # ============ 新增: 角色 × view 映射完整性 ============ + expected_role_views = { + 'leader': ['Home.vue','Projects.vue','Meetings.vue','Account.vue'], + 'manager': ['Workbench.vue','Plans.vue','Projects.vue','Meetings.vue','Experts.vue','ExecUnits.vue','SupportUnits.vue','SupportIntent.vue','ExecIntent.vue','Accounts.vue'], + 'doctor': ['Review.vue','Score.vue'], + 'executor': ['Meetings.vue','Labor.vue'], + 'sponsor': ['Home.vue','SupportLetter.vue','Records.vue'], + } + for role, files in expected_role_views.items(): + for vf in files: + f = Path(f'/home/john/ry8080/ry-vue3/src/views/{role}/{vf}') + if f.exists(): + s = f.read_text() + # 必须包含

}9zRjGe_paGu-C?!9CC&!*C3(D&E7k9`1gxCM& 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 Y`-jGbphFJV!^qET?KiE=TkaO~UwV0y*Z=?k literal 0 HcmV?d00001 diff --git a/proto/html/images/年度项目计划/u333.png b/proto/html/images/年度项目计划/u333.png new file mode 100644 index 0000000000000000000000000000000000000000..7a4d32aa589b37a230e25088f00e5a63c09cd3e8 GIT binary patch literal 150 zcmeAS@N?(olHy`uVBq!ia0y~yV7UckGjXs1$-b2vrGXSM1MG8<*+tbA{B*XdbiG_>|3OvjPWo7y=Za$cJ ox$}(5PIjO?LqxXqe?}04oBvV(3)94JnMXl9Pgg&ebxsLQ0CgrO<^TWy literal 0 HcmV?d00001 diff --git a/proto/html/images/年度项目计划/u337.png b/proto/html/images/年度项目计划/u337.png new file mode 100644 index 0000000000000000000000000000000000000000..2c1c929a8ca6f8817668e5def4c0082e7e35f162 GIT binary patch literal 2656 zcmXw*2{@Ep8^@8!$JAMTdQPn4s-{?oUL$>hB4GC81P;-J!=YqG?N9nL_RPU2(i8x27w6o|856{ zf~$SN+>VX8sY{gSM$xdtIWO74lCcU^M3PSt0%u{tH{bIoPxkNtd}?ZnTlM_83?Cm~bzNPuBPMw+$jZx0_WSqm zk6cvH*~591T$3)X?(S|5bCJiuz@VwOH*;ypyREJ5QCF7)Eaf0FgAbXpOfh;qv7#xN zIlB99ZjLrEz#FJ5wZveQPG2{5LYT{ohGA}ppp})C^GZsFrlt@xZ{LEY(#h5P4<7uD zf@Kf0dH7@Ceg(w#n7n4#)LPP}8*x2~p`}eoq)vVmzQDLy#%tMziUzRv_ z%rqv(&|U4Qt*tGSxoCz!B5`WDr&4+PXd|oSy}euS0(I@}?M+T1&8j9%Yo~GYSsJNg zWqH}z@X^sx0*NFR6BC0`Rkg6O8TJyKO&M7|((HC`bkzR$!8I=4adGptm11eUwiOSs zDIykufq{hZaD<14hrWRU&e!+&+S=OvqM}m!j7E1$;rZTqkCZVXS zzP@}43JNPLE0B`cR<5q%*8>97>mU4r=8=9dm`ss~?zx{g$}q5sii*2r@>SvEA<*bo z#Mbu-@g)x=Gx=dDiCFd}UVH2w8Uf3C_>jK{pZej0dtGDW^kV26=5B0x zd3iz^rrJeCA0-S9x)K^HDnmYGV`F1}w*MY|WAMWV)TrO7JE^IqE-Krv&+IR3&UIVc z*(I6C5%2;qv^w6)+uOT`pW=3Mkj-w3>~<-2d(b>_|hW+Uc^I8iACQ z6ymNI?@iNi@j|?EWo0Fy{Sr`b&qKs}wZp>fh^0@T6gxUPdNO9p%N0M5--l1}O}O5- zR(vrsl1L>aIM)n-PkGke-Q3!ml$R&b^5%$0K|z6pw6q1#nXs_%qsi6r@$m#MH+H6H zzZH+&Yo<`P+RH`E*X7?3FU7FWrPX^y&77z@GvK`~%OCIHa72@{FE)$W`udgIkIA*I z7+tKp4F@v#9C1vO2!M>0jg6Ga)lY3yN^kFmaip0ZQ&(5FnC>*@Dt{ikw-|0f?4)Dg zQn%<%&%3+VJ-Eje6*W>V5u&w)zkqwI3^+%TNanA-nArVa(#|zGH>J5!Qd0At!wkli zm7Qp9U0r!Sy`!3%nxl)NRxZGQE5dOFulPIZhttbUoLt_?{7g+v%_}Y*1keCsaCCMi zZTv-Q5OW2te3sgr^y;&n7_HOX2r(X(+VTiJoh*}lT%?q*{Z7mQ-Q3%RViVH2kaq@`&JqILIvvM-!PP`3-(T`+YHLf5 z+?rq9S$_-SHcI9h1DT|0Xeii8f5pYDp=rCpc11LAD$s_ewsvw|-N~M)?SrbSs-vX` zssMu-42FL|KtIq*M`vfgS=qzt8(e~AY5tU=;-JdL1It;WGP|NaYP~- zU`JC!Lu5zPSTyv`iVDZF@$1*GCXXi`fkd*fupkEH@<8(K?*MdAM2$&Ljq2hrSEoIR z!|Pf#7FxAgvdt~dRBfFV4$Sn#l3DW6^1GJVcU|!Y;l$`@Nx)Unqt6zk{@wS5)ho?) z=d|ppidGc{1O&{lh?9qs($is6)6<|7q)t!#k(=^-qP(XE1%Tk@=4KKctd0^sG~gX- zj*82GF0+jfg_B52x0Ev~X*{uGvKQ;mbARk)u)dJ>>k9$RBv2@GP^!wEPduru-Cm&b z`1twpAAel90ZqY`S5*P>qfecJ*VfjqtgrWvjKGp{XfZh4j3lGt7&2yCc4`Dbr1|;t zzskzwwe|GO>d&`s`UmSfVGtM$CV@(TK-}uj=e_d7`!d7I^sfn4Qz)`PEdZd<45u** zPVBD}N$~*zf_r;=c?AVLVmZylX}s#)$qi!>L$OOFzI~z¨hkfeE zE5x}(%%#T{{^l!Ch<0n0^R{f15aQR?);2%Ow(ZjYqikp~L{3RbscvhV=!*XszT{8# zB*I7U7Kc>Gr&?g&&dg{S8O5$?z5f4vMaA*0Z`86FYg9UOeb*VorSFz}PT6&BsbiD3SLfZ01Z{H|hW;5}*cYJ4TBp}w9N7$_tyEzQ~cskiyzY0~WM zrIsS=E+jjmhDtCYk-pUk$5m3}ZL#1sL2SR&d%-H8{kH|w09ecg4N<8FFzF!Ytu9}d zvR8tWuWCygL;4uo3)@k7EP>=Ef>wUlPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1ENVpK~!i%?V3$b z<1i40-vU(tiI2+pup%KOB+eW-_snnVZTFNP!++pZBm~?z0upeD5+?=~LLe$l_p-Je zFI`}FOOwUVsWP=yo7XernQ8TUy)g`icM=3atJUlE#=Tw-FE20PoP%=?#u$<$K^TSr z0LO7)+cpRxux%TZQdpLyuV-iwMNuG4Q^awM+uK|De;*Eq@O>Yy>%z8exULIA2-@v7 zf*`1hW)k=J_h5{HbB;7k&=+hT=} z6o{e-X__KUQ-om%&N-HrmgL127o8BIPk)oY7R_r6heI&N5JeF<=W+w{^YfsTN>vL% zEi5ePOBmikS(ZqWM4qv5El*ERAcTNYDqYLJ3?-%_nn?@>18Ek;COPL7^XNDZJkP`0 z+L}K7OOs_;9JW29*+1vMItD%QyNeHn=T!Y z7?VNjXfy()RGx>h*t0l~nTuu;d7jG{Nxb6Jnl`qWx*adCnD{e2uCALHuk z3Q-gx%QB3|V=OK%HljPA-|wT}?|-jlo<>yAMIlaPO!az86SKl18>}WO^}X4){l}H) zX;O>io}Qc&b(eO#{iUknld9Hvp37>rID=WXntZBg%CeNz+257&XMHGjqbLe6#wv9x zwbeveCD8yk?MpQ^(fqEIPkuIN)z$nq1=W3jW~<3xi$*OXiW^Y3QvO%sJ*_70&!((D zb@d+-joR8NOr;vUxk@Wxol5zJYO9Ibh9NA!`lxuz^1mt?wL@Jrku%0-=0gqBK@Ggs zmz;<(eczWcrsFs=#;k}&U$oU~VLbk2*kC{q1TB5cV0e=s6`uePOj0%v00000NkvXX Hu0mjf3+vr2 literal 0 HcmV?d00001 diff --git a/proto/html/images/年度项目计划/u376.png b/proto/html/images/年度项目计划/u376.png new file mode 100644 index 0000000000000000000000000000000000000000..a65f3cbf5d4e85bd5500f1f9ded37e8334e87522 GIT binary patch literal 9338 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 literal 0 HcmV?d00001 diff --git a/proto/html/index2.html b/proto/html/index2.html new file mode 100644 index 0000000..1ca4202 --- /dev/null +++ b/proto/html/index2.html @@ -0,0 +1,850 @@ + + + + + + 年度项目计划 - 北京整合医学学会 + + + + + +