loemkie 1 month ago
commit
12ebc389ba
100 changed files with 44230 additions and 0 deletions
  1. 16 0
      .gitignore
  2. 25 0
      CHANGELOG.md
  3. 22 0
      Dockerfile
  4. 201 0
      LICENSE
  5. 111 0
      README.md
  6. 65 0
      k8s/deploy.yaml
  7. 20 0
      k8s/ingress.yaml
  8. 69 0
      plugins/addCustomizeCombox.js
  9. 202 0
      plugins/cupload/cupload-cap.css
  10. 616 0
      plugins/cupload/cupload-cap.js
  11. 136 0
      plugins/cupload/cupload.css
  12. 513 0
      plugins/cupload/cupload.js
  13. 1627 0
      plugins/diff/diff.js
  14. BIN
      plugins/tooth/image/11.png
  15. BIN
      plugins/tooth/image/12.png
  16. BIN
      plugins/tooth/image/13.png
  17. BIN
      plugins/tooth/image/14.png
  18. BIN
      plugins/tooth/image/15.png
  19. BIN
      plugins/tooth/image/16.png
  20. BIN
      plugins/tooth/image/17.png
  21. BIN
      plugins/tooth/image/18.png
  22. BIN
      plugins/tooth/image/21.png
  23. BIN
      plugins/tooth/image/22.png
  24. BIN
      plugins/tooth/image/23.png
  25. BIN
      plugins/tooth/image/24.png
  26. BIN
      plugins/tooth/image/25.png
  27. BIN
      plugins/tooth/image/26.png
  28. BIN
      plugins/tooth/image/27.png
  29. BIN
      plugins/tooth/image/28.png
  30. BIN
      plugins/tooth/image/31.png
  31. BIN
      plugins/tooth/image/32.png
  32. BIN
      plugins/tooth/image/33.png
  33. BIN
      plugins/tooth/image/34.png
  34. BIN
      plugins/tooth/image/35.png
  35. BIN
      plugins/tooth/image/36.png
  36. BIN
      plugins/tooth/image/37.png
  37. BIN
      plugins/tooth/image/38.png
  38. BIN
      plugins/tooth/image/41.png
  39. BIN
      plugins/tooth/image/42.png
  40. BIN
      plugins/tooth/image/43.png
  41. BIN
      plugins/tooth/image/44.png
  42. BIN
      plugins/tooth/image/45.png
  43. BIN
      plugins/tooth/image/46.png
  44. BIN
      plugins/tooth/image/47.png
  45. BIN
      plugins/tooth/image/48.png
  46. 334 0
      plugins/tooth/tooth.css
  47. 645 0
      plugins/tooth/tooth.js
  48. 283 0
      plugins/ue.js
  49. 423 0
      pom.xml
  50. 33933 0
      sql/parking-server.sql
  51. 21 0
      src/main/java/com/qmrb/system/SystemApplication.java
  52. 456 0
      src/main/java/com/qmrb/system/adatper/AbstractPdfAdatper.java
  53. 103 0
      src/main/java/com/qmrb/system/adatper/CaSignAdatper.java
  54. 111 0
      src/main/java/com/qmrb/system/adatper/EmrAdatper.java
  55. 27 0
      src/main/java/com/qmrb/system/adatper/PdfAdatper.java
  56. 39 0
      src/main/java/com/qmrb/system/common/base/BaseEntity.java
  57. 22 0
      src/main/java/com/qmrb/system/common/base/BasePageQuery.java
  58. 19 0
      src/main/java/com/qmrb/system/common/base/BaseVO.java
  59. 91 0
      src/main/java/com/qmrb/system/common/base/IBaseEnum.java
  60. 681 0
      src/main/java/com/qmrb/system/common/constant/DeptNameMapping.java
  61. 16 0
      src/main/java/com/qmrb/system/common/constant/ExcelConstants.java
  62. 40 0
      src/main/java/com/qmrb/system/common/constant/SecurityConstants.java
  63. 31 0
      src/main/java/com/qmrb/system/common/constant/SystemConstants.java
  64. 33 0
      src/main/java/com/qmrb/system/common/enums/DataScopeEnum.java
  65. 30 0
      src/main/java/com/qmrb/system/common/enums/GenderEnum.java
  66. 36 0
      src/main/java/com/qmrb/system/common/enums/MenuTypeEnum.java
  67. 28 0
      src/main/java/com/qmrb/system/common/enums/StatusEnum.java
  68. 39 0
      src/main/java/com/qmrb/system/common/exception/BusinessException.java
  69. 211 0
      src/main/java/com/qmrb/system/common/exception/GlobalExceptionHandler.java
  70. 41 0
      src/main/java/com/qmrb/system/common/proc/CommonJDBCTemplate.java
  71. 18 0
      src/main/java/com/qmrb/system/common/proc/CommonTemplate.java
  72. 21 0
      src/main/java/com/qmrb/system/common/proc/MainApp.java
  73. 12 0
      src/main/java/com/qmrb/system/common/result/IResultCode.java
  74. 51 0
      src/main/java/com/qmrb/system/common/result/PageResult.java
  75. 79 0
      src/main/java/com/qmrb/system/common/result/Result.java
  76. 114 0
      src/main/java/com/qmrb/system/common/result/ResultCode.java
  77. 186 0
      src/main/java/com/qmrb/system/common/util/AesUtil.java
  78. 163 0
      src/main/java/com/qmrb/system/common/util/AesUtils.java
  79. 246 0
      src/main/java/com/qmrb/system/common/util/AuthUtils.java
  80. 23 0
      src/main/java/com/qmrb/system/common/util/Base64Utils.java
  81. 21 0
      src/main/java/com/qmrb/system/common/util/ExcelUtils.java
  82. 261 0
      src/main/java/com/qmrb/system/common/util/RSAUtil.java
  83. 25 0
      src/main/java/com/qmrb/system/common/util/RequestUtils.java
  84. 46 0
      src/main/java/com/qmrb/system/common/util/ResponseUtils.java
  85. 45 0
      src/main/java/com/qmrb/system/config/CorsConfig.java
  86. 20 0
      src/main/java/com/qmrb/system/config/InsertBatchSqlInjector.java
  87. 60 0
      src/main/java/com/qmrb/system/config/MybatisPlusConfig.java
  88. 42 0
      src/main/java/com/qmrb/system/config/RedisConfig.java
  89. 59 0
      src/main/java/com/qmrb/system/config/SwaggerConfig.java
  90. 60 0
      src/main/java/com/qmrb/system/config/WebMvcConfig.java
  91. 16 0
      src/main/java/com/qmrb/system/config/WebSocketConfig.java
  92. 329 0
      src/main/java/com/qmrb/system/controller/AuthController.java
  93. 109 0
      src/main/java/com/qmrb/system/controller/CaPatientInfoController.java
  94. 167 0
      src/main/java/com/qmrb/system/controller/CaSignController.java
  95. 128 0
      src/main/java/com/qmrb/system/controller/ElmtBaseCategoryController.java
  96. 111 0
      src/main/java/com/qmrb/system/controller/ElmtBaseElementController.java
  97. 133 0
      src/main/java/com/qmrb/system/controller/ElmtBaseRangeCodeController.java
  98. 111 0
      src/main/java/com/qmrb/system/controller/ElmtBaseRangeController.java
  99. 128 0
      src/main/java/com/qmrb/system/controller/ElmtCpstCategoryController.java
  100. 130 0
      src/main/java/com/qmrb/system/controller/ElmtCpstElementController.java

+ 16 - 0
.gitignore

@@ -0,0 +1,16 @@
+# Created by .ignore support plugin (hsz.mobi)
+### Example sysUserDetails template template
+### Example sysUserDetails template
+
+# IntelliJ project files
+.idea
+*.iml
+out
+gen
+target
+*.log
+logs
+.settings
+.classpath
+.project
+.factorypath

+ 25 - 0
CHANGELOG.md

@@ -0,0 +1,25 @@
+# 2.2.1 (2023/5/25)
+
+### 🐛 fix
+
+- 修复多级路由的组件路径错误导致页面404问题
+
+# 2.2.0 (2023/5/21)
+
+### ✨ feat
+- 菜单、角色、字典、部门添加接口权限控制
+
+### 🐛 fix
+
+- 用户登录权限缓存键值不一致导致获取用户数据权限错误问题修复
+
+### ✂️ refactor
+
+- 递归获取菜单、部门属性列表代码重构优化
+
+### ⬆️ chore
+- 升级 SpringBoot 版本 `3.0.6` → `3.1.0`
+
+### 📝 docs
+- SQL 脚本更新,sys_menu 新增 `tree_path` 字段  (升级需更新SQL脚本)
+

+ 22 - 0
Dockerfile

@@ -0,0 +1,22 @@
+# SpringBoot单体应用部署Dockerfile
+FROM openjdk:17-jdk-alpine
+
+RUN apk --update --no-cache add tini
+ENTRYPOINT ["tini"]
+
+# /tmp 目录就会在运行时自动挂载为匿名卷,任何向 /tmp 中写入的信息都不会记录进容器存储层
+VOLUME /tmp
+
+ADD target/youlai-boot.jar app.jar
+
+CMD java \
+    -Djava.security.egd=file:/dev/./urandom \
+    -jar /app.jar
+
+EXPOSE 8989
+# 时区修改
+RUN /bin/cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \&& echo 'Asia/Shanghai' >/etc/timezone
+
+RUN echo -e https://mirrors.ustc.edu.cn/alpine/v3.7/main/ > /etc/apk/repositories
+
+RUN apk --no-cache add ttf-dejavu fontconfig

+ 201 - 0
LICENSE

@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright 2023 有来开源组织
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 111 - 0
README.md

@@ -0,0 +1,111 @@
+# CA 签名模板中心
+
+需要java17、Maven 3.6.3+
+
+项目基于 SpringBoot3、SpringSecurity6 、 JWT 、 Redis 、 Mybatis-Plus 、 Knife4j 等技术栈搭建的前后端分离开源权限管理系统。
+
+
+
+## 项目特色
+- Spring Boot 3.0 + Vue3 前后端分离单体应用,适合快速开发;
+- Spring Security + JWT 认证鉴权方案;
+- 基于 RBAC 模型的权限设计,细粒度接口方法、按钮级别权限控制。
+
+## 运行环境
+- JDK 17
+- IDEA Lombok 插件
+- IDEA MapStruct Support 插件
+- MySQL 8.x
+
+
+
+## 接口文档
+
+- `knife4j` 接口文档:[http://localhost:8989/doc.html](http://localhost:8989/doc.html)
+
+- `swagger` 接口文档:[http://localhost:8989/swagger-ui/index.html](http://localhost:8989/swagger-ui/index.html)
+
+## 项目运行
+
+### 1. 数据库创建
+
+执行 [youlai_boot.sql](sql/youlai_boot.sql) 脚本完成数据库创建、表结构和基础数据的初始化。
+
+### 2. 配置修改
+
+[application-dev.yml](src/main/resources/application-dev.yml) 修改MySQL、Redis连接配置;
+
+### 3. 后端启动
+执行 [SystemApplication.java](src/main/java/com/youlai/system/SystemApplication.java) 的 main 方法完成后端项目启动;
+
+访问接口文档地址 [http://localhost:8989/doc.html](http://localhost:8989/doc.html) 验证项目启动。
+
+### 4. 前端启动
+
+文档:[README.md](https://gitee.com/youlaiorg/vue3-element-admin#%E9%A1%B9%E7%9B%AE%E5%90%AF%E5%8A%A8)
+
+## 开发规范
+
+### 方法命名
+
+以下命名涵盖了Controller、Service和Mapper层
+
+|作用|示例|
+|---|---|
+|分页查询|getUserPage|
+|列表查询|listUsers|
+|单个查询|getUser/getUserDetail/getUserInfo ...|
+|新增|saveUser|
+|修改|updateUser|
+|删除|deleteUser/removeUser|
+
+
+### 实体命名
+
+| 名称     | 定义               | 示例        |
+|--------|------------------|-----------|
+| entity | 映射数据库实体,字段属性完全对应 | SysUser   |
+| bo     | 多表关联查询的业务实体      | UserBO    |
+| query  | 查询传参,建议参数≥3使用    | UserQuery |
+| form   | 表单对象             | UserForm  |
+| dto    | RPC调用,可替代VO      | UserDTO   |
+| vo     | 视图层对象            | UserVO    |
+
+### API规范
+在RESTFul架构中,每个URL代表一种资源,所以不能有动词,只能有名词,而且所用的名词往往与数据库的表格名对应。一般来说,数据库中的表都是同种记录的"集合",所以API中的名词也应该使用复数。
+
+**请求示例:**
+
+|请求描述|请求方法|请求路径|
+|---|---|---|
+|获取所有用户信息|GET|/api/v1/users|
+|获取标识为1用户信息|GET|/api/v1/users/1|
+|删除标识为1用户信息|DELETE|/api/v1/users/1|
+|新增用户|POST|/api/v1/users|
+|修改标识为1用户信息|PUT|/api/v1/users/1|
+|修改标识为1用户状态|PATCH|/api/v1/users/1/status|
+|获取当前登录用户信息|GET|/api/v1/users/{me,current}|
+
+
+## 请求状态码规范
+
+参考 [阿里Java开发手册](https://developer.aliyun.com/topic/java2020?utm_content=g_1000113416)
+
+## Git 提交规范
+
+
+参考 ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular)) 社区规范,建议 IDEA 安装 Git Commit Template 插件
+
+- `feat` 增加新功能
+- `fix` 修复问题/BUG
+- `style` 代码风格相关无影响运行结果的
+- `perf` 优化/性能提升
+- `refactor` 重构
+- `revert` 撤销修改
+- `test` 测试相关
+- `docs` 文档/注释
+- `chore` 依赖更新/脚手架配置修改等
+- `workflow` 工作流改进
+- `ci` 持续集成
+
+## 联系我们

+ 65 - 0
k8s/deploy.yaml

@@ -0,0 +1,65 @@
+---
+apiVersion: apps/v1
+kind: Deployment # 无状态部署
+metadata: # 资源元数据
+  name: youlai-boot
+  namespace: youlai-bootnfckx
+  labels:
+    app: youlai-boot
+spec: # 资源规约
+  replicas: 1 # 告知 Deployment 运行 1 个与该模板匹配的 Pod (默认1)
+  strategy:
+    type: RollingUpdate # Recreate:停止所有原来启动新的,适用开发环境;RollingUpdate: 滚动升级,启动新的完成后才停止旧的,保证业务连贯性,如果新的版本发布错误则会保持老的版本
+    rollingUpdate:
+      maxSurge: 25% # 100个pod,可启动25个新的pod
+      maxUnavailable: 25% # 100个pod,可关闭25旧的个pod
+  selector: # 圈定Deployment管理的Pod范围
+    matchLabels:
+      app: youlai-boot # 必须匹配 spec.template.metadata.labels
+  template:
+    metadata:
+      labels:
+        app: youlai-boot #必须匹配 spec.selector.matchLabels
+    spec:
+      containers:
+        - name: youlai-boot # 容器名称
+          image: registry.cn-hangzhou.aliyuncs.com/youlaitech/youlai-boot:latest # 容器镜像地址 (常用镜像仓库:aliyun容器镜像服务/Docker Hub/Harbor企业级私有镜像)
+          imagePullPolicy: Always # 镜像拉取策略(Always-总是拉取镜像(默认);IfNotPresent:本地有则不拉取镜像;Never:只使用本地镜像从不拉取)
+          ports:
+            - containerPort: 8989
+          env:
+            - name: spring.profiles.active
+              value: prod
+            - name: TZ
+              value: Asia/Shanghai
+          resources: # 资源管理
+            limits:
+              cpu: 256m # CPU 1核心 = 1000m
+              memory: 512Mi # 内存 1G = 1000Mi
+          volumeMounts: # 容器目录挂载配置
+            - mountPath: /logs/youlai-boot # 容器要挂载的目录
+              name: log-volume # 日志数据卷名称,和下文 volumes 配置的名称需一致
+
+      volumes: # 数据卷
+        - name: log-volume # 日志数据卷名称
+          hostPath:
+            path: /logs/youlai-boot # 宿主机目录
+            type: DirectoryOrCreate # 目录不存在则创建
+
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: youlai-boot
+  labels:
+    app: youlai-boot
+  namespace: youlai-bootnfckx
+spec:
+  selector:
+    app: youlai-boot
+  ports:
+    - name: http # 端口名称
+      protocol: TCP # 协议类型
+      port: 8989
+      targetPort: 8989
+  type: ClusterIP # Service类型:ClusterIP(默认)/NodePort/LoaderBalancer

+ 20 - 0
k8s/ingress.yaml

@@ -0,0 +1,20 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: test
+  namespace: youlai-bootnfckx
+  annotations:
+    nginx.ingress.kubernetes.io/rewrite-target: /
+spec:
+  ingressClassName: nginx # 关联的ingress-nginx控制器
+  rules:
+    - host: boot.qmrb.tech
+      http:
+        paths:
+          - path: /
+            pathType: Prefix
+            backend:
+              service:
+                name: youlai-boot
+                port:
+                  number: 8989

+ 69 - 0
plugins/addCustomizeCombox.js

@@ -0,0 +1,69 @@
+UE.registerUI('combox',function(editor,uiName){
+    //注册按钮执行时的command命令,用uiName作为command名字,使用命令默认就会带有回退操作
+    editor.registerCommand(uiName,{
+        execCommand:function(cmdName,value){
+            //这里借用fontsize的命令
+            this.execCommand('fontsize',value + 'px')
+        },
+        queryCommandValue:function(){
+            //这里借用fontsize的查询命令
+            return this.queryCommandValue('fontsize')
+        }
+    });
+
+
+    //创建下拉菜单中的键值对,这里我用字体大小作为例子
+    var items = [];
+    for(var i= 0,ci;ci=[10, 11, 12, 14, 16, 18, 20, 24, 36][i++];){
+        items.push({
+            //显示的条目
+            label:'字体:' + ci + 'px',
+            //选中条目后的返回值
+            value:ci,
+            //针对每个条目进行特殊的渲染
+            renderLabelHtml:function () {
+                //这个是希望每个条目的字体是不同的
+                return '<div class="edui-label %%-label" style="line-height:2;font-size:' +
+                    this.value + 'px;">' + (this.label || '') + '</div>';
+            }
+        });
+    }
+    //创建下来框
+    var combox = new UE.ui.Combox({
+        //需要指定当前的编辑器实例
+        editor:editor,
+        //添加条目
+        items:items,
+        //当选中时要做的事情
+        onselect:function (t, index) {
+            //拿到选中条目的值
+            editor.execCommand(uiName, this.items[index].value);
+        },
+        //提示
+        title:uiName,
+        //当编辑器没有焦点时,combox默认显示的内容
+        initValue:uiName
+    });
+
+    editor.addListener('selectionchange', function (type, causeByUi, uiReady) {
+        if (!uiReady) {
+            var state = editor.queryCommandState(uiName);
+            if (state == -1) {
+                combox.setDisabled(true);
+            } else {
+                combox.setDisabled(false);
+                var value = editor.queryCommandValue(uiName);
+                if(!value){
+                    combox.setValue(uiName);
+                    return;
+                }
+                //ie下从源码模式切换回来时,字体会带单引号,而且会有逗号
+                value && (value = value.replace(/['"]/g, '').split(',')[0]);
+                combox.setValue(value);
+
+            }
+        }
+
+    });
+    return combox;
+},2/*index 指定添加到工具栏上的那个位置,默认时追加到最后,editorId 指定这个UI是那个编辑器实例上的,默认是页面上所有的编辑器都会添加这个按钮*/);

File diff suppressed because it is too large
+ 202 - 0
plugins/cupload/cupload-cap.css


File diff suppressed because it is too large
+ 616 - 0
plugins/cupload/cupload-cap.js


+ 136 - 0
plugins/cupload/cupload.css

@@ -0,0 +1,136 @@
+
+.Cupload {
+    display: block;
+}
+
+.cupload-upload-box {
+    position: relative;
+    display: inline-block;
+    text-align: center;
+    background-color: rgb(251, 253, 255);
+    border: 1px dashed rgb(192, 204, 218);
+    border-radius: 6px;
+    box-sizing: border-box;
+    /* width: 148px; */
+    /* height: 148px; */
+    /* line-height: 148px; */
+}
+
+.cupload-upload-btn {
+    position: absolute;
+    /* left: 60px; */
+    font-size: 28px;
+    color: rgb(140, 147, 157);
+}
+
+.cupload-upload-input {
+    position: absolute;
+    top: 0px;
+    right: 0px;
+    width: 100% !important;
+    height: 100% !important;
+    opacity: 0;
+    cursor: pointer;
+}
+
+.cupload-image-list {
+    margin: 0px;
+    padding: 0px;
+    display: inline-block;
+}
+
+.cupload-image-box {
+    position: relative;
+    display: inline-block;
+    margin-right: 8px;
+    background-color: rgb(251, 253, 255);
+    border: 1px solid rgb(192, 204, 218);
+    border-radius: 6px;
+    box-sizing: border-box;
+}
+
+.cupload-image-preview {
+    position: absolute;
+    inset: 0px;
+    margin: auto;
+}
+
+.cupload-image-delete {
+    position: absolute;
+    width: 100%;
+    height: 100%;
+    left: 0px;
+    top: 0px;
+    text-align: center;
+    color: rgb(255, 255, 255);
+    opacity: 0;
+    cursor: zoom-in;
+    background-color: rgba(0, 0, 0, 0.5);
+    transition: all 0.3s ease 0s;
+}
+
+.cupload-delete-btn {
+    position: absolute;
+    top: 0px;
+    right: 0px;
+    margin: 0px;
+    padding: 0px;
+    font-size: 18px;
+    width: 24px;
+    height: 24px;
+    cursor: pointer;
+    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAAAwUlEQVRYhe2WwQ3CMAxFPxXqOjAAqGwHo3QNCIxSxuBzcaRStcRxcin4XSJHjv2aNkoBRwHJhmSgnhvJpqbAPqN5ZKeprbU8ydhvEgDoJ2uqCHQyXhW5Maf7mrUEyYdhu7WEab+5HXiZzJXPp88Uijsm6tQ7KkZcYF0CckSDNi5i7uudzqXipbkx63oFLuACPymwzcy/4/NKTcV2/Dp2AQBPACB5sBYneRzXyl18qfAXHDlbBFqRGAoaD1KjzRb4G96BvtfyCUSIygAAAABJRU5ErkJggg==);
+    background-size: 18px 18px;
+    background-repeat: no-repeat;
+    background-position: right top;
+}
+
+.cupload-sort-left {
+    position: absolute;
+    bottom: 0px;
+    left: 0px;
+    margin: 0px;
+    padding: 0px;
+    font-size: 18px;
+    width: 24px;
+    height: 24px;
+    cursor: pointer;
+    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAABP0lEQVRYhe2UIUsEURRGv7suGza4sCBoENYgGMwWs/4A/4AWwWRyMfgHRCyKGhWsYtegRYOCCGoUk1kwKLiws8dywRFGcXZ23yDMiW8e75z3YK5UUPBfAQbzlK8BEbCa9axySrFJ2pC0klWcGsCALb7Y8aAg8gHgICZfDyJ2eQU4dnEHCPf8QBU4dXkELIaU14BLl7eB+ZDyOnDt8g9gLqR8BHhw+Rsw00/ft98IaEg6lzTmS2eSbnroiyTtm9lT4ldgm/6z+9sLTPoLDPnSraTk2u5oSdo0s7sfdwATwLPXvgBTPQz4G0ADePSIV2A6j4hh4N4j3oHZPCLqwJVHtAg5D2IRNeDCI8JOxFhEFTjxiA6wnEdEBTiKRTTziCgDh7GhspT1zFKazWbWlrQgac+XRrMGdA0wDqS6QEFBEp/yS6NBq8E1tgAAAABJRU5ErkJggg==);
+    background-size: 18px 18px;
+    background-repeat: no-repeat;
+    background-position: left bottom;
+}
+
+.cupload-sort-right {
+    position: absolute;
+    bottom: 0px;
+    right: 0px;
+    margin: 0px;
+    padding: 0px;
+    font-size: 18px;
+    width: 24px;
+    height: 24px;
+    cursor: pointer;
+    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAAA4UlEQVRYhe3WMUpDQRCA4XkKWlhIqnQWwc4iKbyBTVohJ/AC2lh4gxxBA7lAqhwgvbWtpYWVnVgI4mfhghKeEPCFSeD93cIu8w+7OzMRLS3bDga4Qy9LYOqbZ5xkCBzjqUi84DRD4giPReIVZxkSXTwUiTcMMyQ6uC8S7xhlSBxgUSQ+cJEhsY95kfjEVYbEHmZ+GNftq2oOHkbETkMeuxExiYjzsh5XVXXz525cWz+Xv2MuZ6qhzFdms66gSVZ9hOsKnvcNZRYimaVYZjOS2Y5lDyQ2YCTr41bWUNrS8l++ABQQn/PCTE8cAAAAAElFTkSuQmCC);
+    background-size: 18px 18px;
+    background-repeat: no-repeat;
+    background-position: right bottom;
+}
+
+.cupload-overlay {
+    display: none;
+    position: fixed;
+    text-align: center;
+    inset: 0px;
+    z-index: 9115;
+    background-color: rgba(0, 0, 0, 0.3);
+}
+
+.cupload-overlay-img {
+    display: inline-block;
+    vertical-align: middle;
+    position: relative;
+    top: -35px;
+    max-width: calc(100% - 10px);
+    max-height: calc(100% - 10px);
+}

File diff suppressed because it is too large
+ 513 - 0
plugins/cupload/cupload.js


+ 1627 - 0
plugins/diff/diff.js

@@ -0,0 +1,1627 @@
+/*!
+
+ diff v5.1.0
+
+Software License Agreement (BSD License)
+
+Copyright (c) 2009-2015, Kevin Decker <kpdecker@gmail.com>
+
+All rights reserved.
+
+Redistribution and use of this software in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above
+  copyright notice, this list of conditions and the
+  following disclaimer.
+
+* Redistributions in binary form must reproduce the above
+  copyright notice, this list of conditions and the
+  following disclaimer in the documentation and/or other
+  materials provided with the distribution.
+
+* Neither the name of Kevin Decker nor the names of its
+  contributors may be used to endorse or promote products
+  derived from this software without specific prior
+  written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+@license
+*/
+(function (global, factory) {
+  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
+  typeof define === 'function' && define.amd ? define(['exports'], factory) :
+  (global = global || self, factory(global.Diff = {}));
+}(this, (function (exports) { 'use strict';
+
+  function Diff() {}
+  Diff.prototype = {
+    diff: function diff(oldString, newString) {
+      var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
+      var callback = options.callback;
+
+      if (typeof options === 'function') {
+        callback = options;
+        options = {};
+      }
+
+      this.options = options;
+      var self = this;
+
+      function done(value) {
+        if (callback) {
+          setTimeout(function () {
+            callback(undefined, value);
+          }, 0);
+          return true;
+        } else {
+          return value;
+        }
+      } // Allow subclasses to massage the input prior to running
+
+
+      oldString = this.castInput(oldString);
+      newString = this.castInput(newString);
+      oldString = this.removeEmpty(this.tokenize(oldString));
+      newString = this.removeEmpty(this.tokenize(newString));
+      var newLen = newString.length,
+          oldLen = oldString.length;
+      var editLength = 1;
+      var maxEditLength = newLen + oldLen;
+
+      if (options.maxEditLength) {
+        maxEditLength = Math.min(maxEditLength, options.maxEditLength);
+      }
+
+      var bestPath = [{
+        newPos: -1,
+        components: []
+      }]; // Seed editLength = 0, i.e. the content starts with the same values
+
+      var oldPos = this.extractCommon(bestPath[0], newString, oldString, 0);
+
+      if (bestPath[0].newPos + 1 >= newLen && oldPos + 1 >= oldLen) {
+        // Identity per the equality and tokenizer
+        return done([{
+          value: this.join(newString),
+          count: newString.length
+        }]);
+      } // Main worker method. checks all permutations of a given edit length for acceptance.
+
+
+      function execEditLength() {
+        for (var diagonalPath = -1 * editLength; diagonalPath <= editLength; diagonalPath += 2) {
+          var basePath = void 0;
+
+          var addPath = bestPath[diagonalPath - 1],
+              removePath = bestPath[diagonalPath + 1],
+              _oldPos = (removePath ? removePath.newPos : 0) - diagonalPath;
+
+          if (addPath) {
+            // No one else is going to attempt to use this value, clear it
+            bestPath[diagonalPath - 1] = undefined;
+          }
+
+          var canAdd = addPath && addPath.newPos + 1 < newLen,
+              canRemove = removePath && 0 <= _oldPos && _oldPos < oldLen;
+
+          if (!canAdd && !canRemove) {
+            // If this path is a terminal then prune
+            bestPath[diagonalPath] = undefined;
+            continue;
+          } // Select the diagonal that we want to branch from. We select the prior
+          // path whose position in the new string is the farthest from the origin
+          // and does not pass the bounds of the diff graph
+
+
+          if (!canAdd || canRemove && addPath.newPos < removePath.newPos) {
+            basePath = clonePath(removePath);
+            self.pushComponent(basePath.components, undefined, true);
+          } else {
+            basePath = addPath; // No need to clone, we've pulled it from the list
+
+            basePath.newPos++;
+            self.pushComponent(basePath.components, true, undefined);
+          }
+
+          _oldPos = self.extractCommon(basePath, newString, oldString, diagonalPath); // If we have hit the end of both strings, then we are done
+
+          if (basePath.newPos + 1 >= newLen && _oldPos + 1 >= oldLen) {
+            return done(buildValues(self, basePath.components, newString, oldString, self.useLongestToken));
+          } else {
+            // Otherwise track this path as a potential candidate and continue.
+            bestPath[diagonalPath] = basePath;
+          }
+        }
+
+        editLength++;
+      } // Performs the length of edit iteration. Is a bit fugly as this has to support the
+      // sync and async mode which is never fun. Loops over execEditLength until a value
+      // is produced, or until the edit length exceeds options.maxEditLength (if given),
+      // in which case it will return undefined.
+
+
+      if (callback) {
+        (function exec() {
+          setTimeout(function () {
+            if (editLength > maxEditLength) {
+              return callback();
+            }
+
+            if (!execEditLength()) {
+              exec();
+            }
+          }, 0);
+        })();
+      } else {
+        while (editLength <= maxEditLength) {
+          var ret = execEditLength();
+
+          if (ret) {
+            return ret;
+          }
+        }
+      }
+    },
+    pushComponent: function pushComponent(components, added, removed) {
+      var last = components[components.length - 1];
+
+      if (last && last.added === added && last.removed === removed) {
+        // We need to clone here as the component clone operation is just
+        // as shallow array clone
+        components[components.length - 1] = {
+          count: last.count + 1,
+          added: added,
+          removed: removed
+        };
+      } else {
+        components.push({
+          count: 1,
+          added: added,
+          removed: removed
+        });
+      }
+    },
+    extractCommon: function extractCommon(basePath, newString, oldString, diagonalPath) {
+      var newLen = newString.length,
+          oldLen = oldString.length,
+          newPos = basePath.newPos,
+          oldPos = newPos - diagonalPath,
+          commonCount = 0;
+
+      while (newPos + 1 < newLen && oldPos + 1 < oldLen && this.equals(newString[newPos + 1], oldString[oldPos + 1])) {
+        newPos++;
+        oldPos++;
+        commonCount++;
+      }
+
+      if (commonCount) {
+        basePath.components.push({
+          count: commonCount
+        });
+      }
+
+      basePath.newPos = newPos;
+      return oldPos;
+    },
+    equals: function equals(left, right) {
+      if (this.options.comparator) {
+        return this.options.comparator(left, right);
+      } else {
+        return left === right || this.options.ignoreCase && left.toLowerCase() === right.toLowerCase();
+      }
+    },
+    removeEmpty: function removeEmpty(array) {
+      var ret = [];
+
+      for (var i = 0; i < array.length; i++) {
+        if (array[i]) {
+          ret.push(array[i]);
+        }
+      }
+
+      return ret;
+    },
+    castInput: function castInput(value) {
+      return value;
+    },
+    tokenize: function tokenize(value) {
+      return value.split('');
+    },
+    join: function join(chars) {
+      return chars.join('');
+    }
+  };
+
+  function buildValues(diff, components, newString, oldString, useLongestToken) {
+    var componentPos = 0,
+        componentLen = components.length,
+        newPos = 0,
+        oldPos = 0;
+
+    for (; componentPos < componentLen; componentPos++) {
+      var component = components[componentPos];
+
+      if (!component.removed) {
+        if (!component.added && useLongestToken) {
+          var value = newString.slice(newPos, newPos + component.count);
+          value = value.map(function (value, i) {
+            var oldValue = oldString[oldPos + i];
+            return oldValue.length > value.length ? oldValue : value;
+          });
+          component.value = diff.join(value);
+        } else {
+          component.value = diff.join(newString.slice(newPos, newPos + component.count));
+        }
+
+        newPos += component.count; // Common case
+
+        if (!component.added) {
+          oldPos += component.count;
+        }
+      } else {
+        component.value = diff.join(oldString.slice(oldPos, oldPos + component.count));
+        oldPos += component.count; // Reverse add and remove so removes are output first to match common convention
+        // The diffing algorithm is tied to add then remove output and this is the simplest
+        // route to get the desired output with minimal overhead.
+
+        if (componentPos && components[componentPos - 1].added) {
+          var tmp = components[componentPos - 1];
+          components[componentPos - 1] = components[componentPos];
+          components[componentPos] = tmp;
+        }
+      }
+    } // Special case handle for when one terminal is ignored (i.e. whitespace).
+    // For this case we merge the terminal into the prior string and drop the change.
+    // This is only available for string mode.
+
+
+    var lastComponent = components[componentLen - 1];
+
+    if (componentLen > 1 && typeof lastComponent.value === 'string' && (lastComponent.added || lastComponent.removed) && diff.equals('', lastComponent.value)) {
+      components[componentLen - 2].value += lastComponent.value;
+      components.pop();
+    }
+
+    return components;
+  }
+
+  function clonePath(path) {
+    return {
+      newPos: path.newPos,
+      components: path.components.slice(0)
+    };
+  }
+
+  var characterDiff = new Diff();
+  function diffChars(oldStr, newStr, options) {
+    return characterDiff.diff(oldStr, newStr, options);
+  }
+
+  function generateOptions(options, defaults) {
+    if (typeof options === 'function') {
+      defaults.callback = options;
+    } else if (options) {
+      for (var name in options) {
+        /* istanbul ignore else */
+        if (options.hasOwnProperty(name)) {
+          defaults[name] = options[name];
+        }
+      }
+    }
+
+    return defaults;
+  }
+
+  //
+  // Ranges and exceptions:
+  // Latin-1 Supplement, 0080–00FF
+  //  - U+00D7  × Multiplication sign
+  //  - U+00F7  ÷ Division sign
+  // Latin Extended-A, 0100–017F
+  // Latin Extended-B, 0180–024F
+  // IPA Extensions, 0250–02AF
+  // Spacing Modifier Letters, 02B0–02FF
+  //  - U+02C7  ˇ &#711;  Caron
+  //  - U+02D8  ˘ &#728;  Breve
+  //  - U+02D9  ˙ &#729;  Dot Above
+  //  - U+02DA  ˚ &#730;  Ring Above
+  //  - U+02DB  ˛ &#731;  Ogonek
+  //  - U+02DC  ˜ &#732;  Small Tilde
+  //  - U+02DD  ˝ &#733;  Double Acute Accent
+  // Latin Extended Additional, 1E00–1EFF
+
+  var extendedWordChars = /^[A-Za-z\xC0-\u02C6\u02C8-\u02D7\u02DE-\u02FF\u1E00-\u1EFF]+$/;
+  var reWhitespace = /\S/;
+  var wordDiff = new Diff();
+
+  wordDiff.equals = function (left, right) {
+    if (this.options.ignoreCase) {
+      left = left.toLowerCase();
+      right = right.toLowerCase();
+    }
+
+    return left === right || this.options.ignoreWhitespace && !reWhitespace.test(left) && !reWhitespace.test(right);
+  };
+
+  wordDiff.tokenize = function (value) {
+    // All whitespace symbols except newline group into one token, each newline - in separate token
+    var tokens = value.split(/([^\S\r\n]+|[()[\]{}'"\r\n]|\b)/); // Join the boundary splits that we do not consider to be boundaries. This is primarily the extended Latin character set.
+
+    for (var i = 0; i < tokens.length - 1; i++) {
+      // If we have an empty string in the next field and we have only word chars before and after, merge
+      if (!tokens[i + 1] && tokens[i + 2] && extendedWordChars.test(tokens[i]) && extendedWordChars.test(tokens[i + 2])) {
+        tokens[i] += tokens[i + 2];
+        tokens.splice(i + 1, 2);
+        i--;
+      }
+    }
+
+    return tokens;
+  };
+
+  function diffWords(oldStr, newStr, options) {
+    options = generateOptions(options, {
+      ignoreWhitespace: true
+    });
+    return wordDiff.diff(oldStr, newStr, options);
+  }
+  function diffWordsWithSpace(oldStr, newStr, options) {
+    return wordDiff.diff(oldStr, newStr, options);
+  }
+
+  var lineDiff = new Diff();
+
+  lineDiff.tokenize = function (value) {
+    var retLines = [],
+        linesAndNewlines = value.split(/(\n|\r\n)/); // Ignore the final empty token that occurs if the string ends with a new line
+
+    if (!linesAndNewlines[linesAndNewlines.length - 1]) {
+      linesAndNewlines.pop();
+    } // Merge the content and line separators into single tokens
+
+
+    for (var i = 0; i < linesAndNewlines.length; i++) {
+      var line = linesAndNewlines[i];
+
+      if (i % 2 && !this.options.newlineIsToken) {
+        retLines[retLines.length - 1] += line;
+      } else {
+        if (this.options.ignoreWhitespace) {
+          line = line.trim();
+        }
+
+        retLines.push(line);
+      }
+    }
+
+    return retLines;
+  };
+
+  function diffLines(oldStr, newStr, callback) {
+    return lineDiff.diff(oldStr, newStr, callback);
+  }
+  function diffTrimmedLines(oldStr, newStr, callback) {
+    var options = generateOptions(callback, {
+      ignoreWhitespace: true
+    });
+    return lineDiff.diff(oldStr, newStr, options);
+  }
+
+  var sentenceDiff = new Diff();
+
+  sentenceDiff.tokenize = function (value) {
+    return value.split(/(\S.+?[.!?])(?=\s+|$)/);
+  };
+
+  function diffSentences(oldStr, newStr, callback) {
+    return sentenceDiff.diff(oldStr, newStr, callback);
+  }
+
+  var cssDiff = new Diff();
+
+  cssDiff.tokenize = function (value) {
+    return value.split(/([{}:;,]|\s+)/);
+  };
+
+  function diffCss(oldStr, newStr, callback) {
+    return cssDiff.diff(oldStr, newStr, callback);
+  }
+
+  function _typeof(obj) {
+    "@babel/helpers - typeof";
+
+    if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
+      _typeof = function (obj) {
+        return typeof obj;
+      };
+    } else {
+      _typeof = function (obj) {
+        return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
+      };
+    }
+
+    return _typeof(obj);
+  }
+
+  function _toConsumableArray(arr) {
+    return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread();
+  }
+
+  function _arrayWithoutHoles(arr) {
+    if (Array.isArray(arr)) return _arrayLikeToArray(arr);
+  }
+
+  function _iterableToArray(iter) {
+    if (typeof Symbol !== "undefined" && Symbol.iterator in Object(iter)) return Array.from(iter);
+  }
+
+  function _unsupportedIterableToArray(o, minLen) {
+    if (!o) return;
+    if (typeof o === "string") return _arrayLikeToArray(o, minLen);
+    var n = Object.prototype.toString.call(o).slice(8, -1);
+    if (n === "Object" && o.constructor) n = o.constructor.name;
+    if (n === "Map" || n === "Set") return Array.from(o);
+    if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
+  }
+
+  function _arrayLikeToArray(arr, len) {
+    if (len == null || len > arr.length) len = arr.length;
+
+    for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i];
+
+    return arr2;
+  }
+
+  function _nonIterableSpread() {
+    throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
+  }
+
+  var objectPrototypeToString = Object.prototype.toString;
+  var jsonDiff = new Diff(); // Discriminate between two lines of pretty-printed, serialized JSON where one of them has a
+  // dangling comma and the other doesn't. Turns out including the dangling comma yields the nicest output:
+
+  jsonDiff.useLongestToken = true;
+  jsonDiff.tokenize = lineDiff.tokenize;
+
+  jsonDiff.castInput = function (value) {
+    var _this$options = this.options,
+        undefinedReplacement = _this$options.undefinedReplacement,
+        _this$options$stringi = _this$options.stringifyReplacer,
+        stringifyReplacer = _this$options$stringi === void 0 ? function (k, v) {
+      return typeof v === 'undefined' ? undefinedReplacement : v;
+    } : _this$options$stringi;
+    return typeof value === 'string' ? value : JSON.stringify(canonicalize(value, null, null, stringifyReplacer), stringifyReplacer, '  ');
+  };
+
+  jsonDiff.equals = function (left, right) {
+    return Diff.prototype.equals.call(jsonDiff, left.replace(/,([\r\n])/g, '$1'), right.replace(/,([\r\n])/g, '$1'));
+  };
+
+  function diffJson(oldObj, newObj, options) {
+    return jsonDiff.diff(oldObj, newObj, options);
+  } // This function handles the presence of circular references by bailing out when encountering an
+  // object that is already on the "stack" of items being processed. Accepts an optional replacer
+
+  function canonicalize(obj, stack, replacementStack, replacer, key) {
+    stack = stack || [];
+    replacementStack = replacementStack || [];
+
+    if (replacer) {
+      obj = replacer(key, obj);
+    }
+
+    var i;
+
+    for (i = 0; i < stack.length; i += 1) {
+      if (stack[i] === obj) {
+        return replacementStack[i];
+      }
+    }
+
+    var canonicalizedObj;
+
+    if ('[object Array]' === objectPrototypeToString.call(obj)) {
+      stack.push(obj);
+      canonicalizedObj = new Array(obj.length);
+      replacementStack.push(canonicalizedObj);
+
+      for (i = 0; i < obj.length; i += 1) {
+        canonicalizedObj[i] = canonicalize(obj[i], stack, replacementStack, replacer, key);
+      }
+
+      stack.pop();
+      replacementStack.pop();
+      return canonicalizedObj;
+    }
+
+    if (obj && obj.toJSON) {
+      obj = obj.toJSON();
+    }
+
+    if (_typeof(obj) === 'object' && obj !== null) {
+      stack.push(obj);
+      canonicalizedObj = {};
+      replacementStack.push(canonicalizedObj);
+
+      var sortedKeys = [],
+          _key;
+
+      for (_key in obj) {
+        /* istanbul ignore else */
+        if (obj.hasOwnProperty(_key)) {
+          sortedKeys.push(_key);
+        }
+      }
+
+      sortedKeys.sort();
+
+      for (i = 0; i < sortedKeys.length; i += 1) {
+        _key = sortedKeys[i];
+        canonicalizedObj[_key] = canonicalize(obj[_key], stack, replacementStack, replacer, _key);
+      }
+
+      stack.pop();
+      replacementStack.pop();
+    } else {
+      canonicalizedObj = obj;
+    }
+
+    return canonicalizedObj;
+  }
+
+  var arrayDiff = new Diff();
+
+  arrayDiff.tokenize = function (value) {
+    return value.slice();
+  };
+
+  arrayDiff.join = arrayDiff.removeEmpty = function (value) {
+    return value;
+  };
+
+  function diffArrays(oldArr, newArr, callback) {
+    return arrayDiff.diff(oldArr, newArr, callback);
+  }
+
+  function parsePatch(uniDiff) {
+    var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
+    var diffstr = uniDiff.split(/\r\n|[\n\v\f\r\x85]/),
+        delimiters = uniDiff.match(/\r\n|[\n\v\f\r\x85]/g) || [],
+        list = [],
+        i = 0;
+
+    function parseIndex() {
+      var index = {};
+      list.push(index); // Parse diff metadata
+
+      while (i < diffstr.length) {
+        var line = diffstr[i]; // File header found, end parsing diff metadata
+
+        if (/^(\-\-\-|\+\+\+|@@)\s/.test(line)) {
+          break;
+        } // Diff index
+
+
+        var header = /^(?:Index:|diff(?: -r \w+)+)\s+(.+?)\s*$/.exec(line);
+
+        if (header) {
+          index.index = header[1];
+        }
+
+        i++;
+      } // Parse file headers if they are defined. Unified diff requires them, but
+      // there's no technical issues to have an isolated hunk without file header
+
+
+      parseFileHeader(index);
+      parseFileHeader(index); // Parse hunks
+
+      index.hunks = [];
+
+      while (i < diffstr.length) {
+        var _line = diffstr[i];
+
+        if (/^(Index:|diff|\-\-\-|\+\+\+)\s/.test(_line)) {
+          break;
+        } else if (/^@@/.test(_line)) {
+          index.hunks.push(parseHunk());
+        } else if (_line && options.strict) {
+          // Ignore unexpected content unless in strict mode
+          throw new Error('Unknown line ' + (i + 1) + ' ' + JSON.stringify(_line));
+        } else {
+          i++;
+        }
+      }
+    } // Parses the --- and +++ headers, if none are found, no lines
+    // are consumed.
+
+
+    function parseFileHeader(index) {
+      var fileHeader = /^(---|\+\+\+)\s+(.*)$/.exec(diffstr[i]);
+
+      if (fileHeader) {
+        var keyPrefix = fileHeader[1] === '---' ? 'old' : 'new';
+        var data = fileHeader[2].split('\t', 2);
+        var fileName = data[0].replace(/\\\\/g, '\\');
+
+        if (/^".*"$/.test(fileName)) {
+          fileName = fileName.substr(1, fileName.length - 2);
+        }
+
+        index[keyPrefix + 'FileName'] = fileName;
+        index[keyPrefix + 'Header'] = (data[1] || '').trim();
+        i++;
+      }
+    } // Parses a hunk
+    // This assumes that we are at the start of a hunk.
+
+
+    function parseHunk() {
+      var chunkHeaderIndex = i,
+          chunkHeaderLine = diffstr[i++],
+          chunkHeader = chunkHeaderLine.split(/@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
+      var hunk = {
+        oldStart: +chunkHeader[1],
+        oldLines: typeof chunkHeader[2] === 'undefined' ? 1 : +chunkHeader[2],
+        newStart: +chunkHeader[3],
+        newLines: typeof chunkHeader[4] === 'undefined' ? 1 : +chunkHeader[4],
+        lines: [],
+        linedelimiters: []
+      }; // Unified Diff Format quirk: If the chunk size is 0,
+      // the first number is one lower than one would expect.
+      // https://www.artima.com/weblogs/viewpost.jsp?thread=164293
+
+      if (hunk.oldLines === 0) {
+        hunk.oldStart += 1;
+      }
+
+      if (hunk.newLines === 0) {
+        hunk.newStart += 1;
+      }
+
+      var addCount = 0,
+          removeCount = 0;
+
+      for (; i < diffstr.length; i++) {
+        // Lines starting with '---' could be mistaken for the "remove line" operation
+        // But they could be the header for the next file. Therefore prune such cases out.
+        if (diffstr[i].indexOf('--- ') === 0 && i + 2 < diffstr.length && diffstr[i + 1].indexOf('+++ ') === 0 && diffstr[i + 2].indexOf('@@') === 0) {
+          break;
+        }
+
+        var operation = diffstr[i].length == 0 && i != diffstr.length - 1 ? ' ' : diffstr[i][0];
+
+        if (operation === '+' || operation === '-' || operation === ' ' || operation === '\\') {
+          hunk.lines.push(diffstr[i]);
+          hunk.linedelimiters.push(delimiters[i] || '\n');
+
+          if (operation === '+') {
+            addCount++;
+          } else if (operation === '-') {
+            removeCount++;
+          } else if (operation === ' ') {
+            addCount++;
+            removeCount++;
+          }
+        } else {
+          break;
+        }
+      } // Handle the empty block count case
+
+
+      if (!addCount && hunk.newLines === 1) {
+        hunk.newLines = 0;
+      }
+
+      if (!removeCount && hunk.oldLines === 1) {
+        hunk.oldLines = 0;
+      } // Perform optional sanity checking
+
+
+      if (options.strict) {
+        if (addCount !== hunk.newLines) {
+          throw new Error('Added line count did not match for hunk at line ' + (chunkHeaderIndex + 1));
+        }
+
+        if (removeCount !== hunk.oldLines) {
+          throw new Error('Removed line count did not match for hunk at line ' + (chunkHeaderIndex + 1));
+        }
+      }
+
+      return hunk;
+    }
+
+    while (i < diffstr.length) {
+      parseIndex();
+    }
+
+    return list;
+  }
+
+  // Iterator that traverses in the range of [min, max], stepping
+  // by distance from a given start position. I.e. for [0, 4], with
+  // start of 2, this will iterate 2, 3, 1, 4, 0.
+  function distanceIterator (start, minLine, maxLine) {
+    var wantForward = true,
+        backwardExhausted = false,
+        forwardExhausted = false,
+        localOffset = 1;
+    return function iterator() {
+      if (wantForward && !forwardExhausted) {
+        if (backwardExhausted) {
+          localOffset++;
+        } else {
+          wantForward = false;
+        } // Check if trying to fit beyond text length, and if not, check it fits
+        // after offset location (or desired location on first iteration)
+
+
+        if (start + localOffset <= maxLine) {
+          return localOffset;
+        }
+
+        forwardExhausted = true;
+      }
+
+      if (!backwardExhausted) {
+        if (!forwardExhausted) {
+          wantForward = true;
+        } // Check if trying to fit before text beginning, and if not, check it fits
+        // before offset location
+
+
+        if (minLine <= start - localOffset) {
+          return -localOffset++;
+        }
+
+        backwardExhausted = true;
+        return iterator();
+      } // We tried to fit hunk before text beginning and beyond text length, then
+      // hunk can't fit on the text. Return undefined
+
+    };
+  }
+
+  function applyPatch(source, uniDiff) {
+    var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
+
+    if (typeof uniDiff === 'string') {
+      uniDiff = parsePatch(uniDiff);
+    }
+
+    if (Array.isArray(uniDiff)) {
+      if (uniDiff.length > 1) {
+        throw new Error('applyPatch only works with a single input.');
+      }
+
+      uniDiff = uniDiff[0];
+    } // Apply the diff to the input
+
+
+    var lines = source.split(/\r\n|[\n\v\f\r\x85]/),
+        delimiters = source.match(/\r\n|[\n\v\f\r\x85]/g) || [],
+        hunks = uniDiff.hunks,
+        compareLine = options.compareLine || function (lineNumber, line, operation, patchContent) {
+      return line === patchContent;
+    },
+        errorCount = 0,
+        fuzzFactor = options.fuzzFactor || 0,
+        minLine = 0,
+        offset = 0,
+        removeEOFNL,
+        addEOFNL;
+    /**
+     * Checks if the hunk exactly fits on the provided location
+     */
+
+
+    function hunkFits(hunk, toPos) {
+      for (var j = 0; j < hunk.lines.length; j++) {
+        var line = hunk.lines[j],
+            operation = line.length > 0 ? line[0] : ' ',
+            content = line.length > 0 ? line.substr(1) : line;
+
+        if (operation === ' ' || operation === '-') {
+          // Context sanity check
+          if (!compareLine(toPos + 1, lines[toPos], operation, content)) {
+            errorCount++;
+
+            if (errorCount > fuzzFactor) {
+              return false;
+            }
+          }
+
+          toPos++;
+        }
+      }
+
+      return true;
+    } // Search best fit offsets for each hunk based on the previous ones
+
+
+    for (var i = 0; i < hunks.length; i++) {
+      var hunk = hunks[i],
+          maxLine = lines.length - hunk.oldLines,
+          localOffset = 0,
+          toPos = offset + hunk.oldStart - 1;
+      var iterator = distanceIterator(toPos, minLine, maxLine);
+
+      for (; localOffset !== undefined; localOffset = iterator()) {
+        if (hunkFits(hunk, toPos + localOffset)) {
+          hunk.offset = offset += localOffset;
+          break;
+        }
+      }
+
+      if (localOffset === undefined) {
+        return false;
+      } // Set lower text limit to end of the current hunk, so next ones don't try
+      // to fit over already patched text
+
+
+      minLine = hunk.offset + hunk.oldStart + hunk.oldLines;
+    } // Apply patch hunks
+
+
+    var diffOffset = 0;
+
+    for (var _i = 0; _i < hunks.length; _i++) {
+      var _hunk = hunks[_i],
+          _toPos = _hunk.oldStart + _hunk.offset + diffOffset - 1;
+
+      diffOffset += _hunk.newLines - _hunk.oldLines;
+
+      for (var j = 0; j < _hunk.lines.length; j++) {
+        var line = _hunk.lines[j],
+            operation = line.length > 0 ? line[0] : ' ',
+            content = line.length > 0 ? line.substr(1) : line,
+            delimiter = _hunk.linedelimiters[j];
+
+        if (operation === ' ') {
+          _toPos++;
+        } else if (operation === '-') {
+          lines.splice(_toPos, 1);
+          delimiters.splice(_toPos, 1);
+          /* istanbul ignore else */
+        } else if (operation === '+') {
+          lines.splice(_toPos, 0, content);
+          delimiters.splice(_toPos, 0, delimiter);
+          _toPos++;
+        } else if (operation === '\\') {
+          var previousOperation = _hunk.lines[j - 1] ? _hunk.lines[j - 1][0] : null;
+
+          if (previousOperation === '+') {
+            removeEOFNL = true;
+          } else if (previousOperation === '-') {
+            addEOFNL = true;
+          }
+        }
+      }
+    } // Handle EOFNL insertion/removal
+
+
+    if (removeEOFNL) {
+      while (!lines[lines.length - 1]) {
+        lines.pop();
+        delimiters.pop();
+      }
+    } else if (addEOFNL) {
+      lines.push('');
+      delimiters.push('\n');
+    }
+
+    for (var _k = 0; _k < lines.length - 1; _k++) {
+      lines[_k] = lines[_k] + delimiters[_k];
+    }
+
+    return lines.join('');
+  } // Wrapper that supports multiple file patches via callbacks.
+
+  function applyPatches(uniDiff, options) {
+    if (typeof uniDiff === 'string') {
+      uniDiff = parsePatch(uniDiff);
+    }
+
+    var currentIndex = 0;
+
+    function processIndex() {
+      var index = uniDiff[currentIndex++];
+
+      if (!index) {
+        return options.complete();
+      }
+
+      options.loadFile(index, function (err, data) {
+        if (err) {
+          return options.complete(err);
+        }
+
+        var updatedContent = applyPatch(data, index, options);
+        options.patched(index, updatedContent, function (err) {
+          if (err) {
+            return options.complete(err);
+          }
+
+          processIndex();
+        });
+      });
+    }
+
+    processIndex();
+  }
+
+  function structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options) {
+    if (!options) {
+      options = {};
+    }
+
+    if (typeof options.context === 'undefined') {
+      options.context = 4;
+    }
+
+    var diff = diffLines(oldStr, newStr, options);
+
+    if (!diff) {
+      return;
+    }
+
+    diff.push({
+      value: '',
+      lines: []
+    }); // Append an empty value to make cleanup easier
+
+    function contextLines(lines) {
+      return lines.map(function (entry) {
+        return ' ' + entry;
+      });
+    }
+
+    var hunks = [];
+    var oldRangeStart = 0,
+        newRangeStart = 0,
+        curRange = [],
+        oldLine = 1,
+        newLine = 1;
+
+    var _loop = function _loop(i) {
+      var current = diff[i],
+          lines = current.lines || current.value.replace(/\n$/, '').split('\n');
+      current.lines = lines;
+
+      if (current.added || current.removed) {
+        var _curRange;
+
+        // If we have previous context, start with that
+        if (!oldRangeStart) {
+          var prev = diff[i - 1];
+          oldRangeStart = oldLine;
+          newRangeStart = newLine;
+
+          if (prev) {
+            curRange = options.context > 0 ? contextLines(prev.lines.slice(-options.context)) : [];
+            oldRangeStart -= curRange.length;
+            newRangeStart -= curRange.length;
+          }
+        } // Output our changes
+
+
+        (_curRange = curRange).push.apply(_curRange, _toConsumableArray(lines.map(function (entry) {
+          return (current.added ? '+' : '-') + entry;
+        }))); // Track the updated file position
+
+
+        if (current.added) {
+          newLine += lines.length;
+        } else {
+          oldLine += lines.length;
+        }
+      } else {
+        // Identical context lines. Track line changes
+        if (oldRangeStart) {
+          // Close out any changes that have been output (or join overlapping)
+          if (lines.length <= options.context * 2 && i < diff.length - 2) {
+            var _curRange2;
+
+            // Overlapping
+            (_curRange2 = curRange).push.apply(_curRange2, _toConsumableArray(contextLines(lines)));
+          } else {
+            var _curRange3;
+
+            // end the range and output
+            var contextSize = Math.min(lines.length, options.context);
+
+            (_curRange3 = curRange).push.apply(_curRange3, _toConsumableArray(contextLines(lines.slice(0, contextSize))));
+
+            var hunk = {
+              oldStart: oldRangeStart,
+              oldLines: oldLine - oldRangeStart + contextSize,
+              newStart: newRangeStart,
+              newLines: newLine - newRangeStart + contextSize,
+              lines: curRange
+            };
+
+            if (i >= diff.length - 2 && lines.length <= options.context) {
+              // EOF is inside this hunk
+              var oldEOFNewline = /\n$/.test(oldStr);
+              var newEOFNewline = /\n$/.test(newStr);
+              var noNlBeforeAdds = lines.length == 0 && curRange.length > hunk.oldLines;
+
+              if (!oldEOFNewline && noNlBeforeAdds && oldStr.length > 0) {
+                // special case: old has no eol and no trailing context; no-nl can end up before adds
+                // however, if the old file is empty, do not output the no-nl line
+                curRange.splice(hunk.oldLines, 0, '\\ No newline at end of file');
+              }
+
+              if (!oldEOFNewline && !noNlBeforeAdds || !newEOFNewline) {
+                curRange.push('\\ No newline at end of file');
+              }
+            }
+
+            hunks.push(hunk);
+            oldRangeStart = 0;
+            newRangeStart = 0;
+            curRange = [];
+          }
+        }
+
+        oldLine += lines.length;
+        newLine += lines.length;
+      }
+    };
+
+    for (var i = 0; i < diff.length; i++) {
+      _loop(i);
+    }
+
+    return {
+      oldFileName: oldFileName,
+      newFileName: newFileName,
+      oldHeader: oldHeader,
+      newHeader: newHeader,
+      hunks: hunks
+    };
+  }
+  function formatPatch(diff) {
+    var ret = [];
+
+    if (diff.oldFileName == diff.newFileName) {
+      ret.push('Index: ' + diff.oldFileName);
+    }
+
+    ret.push('===================================================================');
+    ret.push('--- ' + diff.oldFileName + (typeof diff.oldHeader === 'undefined' ? '' : '\t' + diff.oldHeader));
+    ret.push('+++ ' + diff.newFileName + (typeof diff.newHeader === 'undefined' ? '' : '\t' + diff.newHeader));
+
+    for (var i = 0; i < diff.hunks.length; i++) {
+      var hunk = diff.hunks[i]; // Unified Diff Format quirk: If the chunk size is 0,
+      // the first number is one lower than one would expect.
+      // https://www.artima.com/weblogs/viewpost.jsp?thread=164293
+
+      if (hunk.oldLines === 0) {
+        hunk.oldStart -= 1;
+      }
+
+      if (hunk.newLines === 0) {
+        hunk.newStart -= 1;
+      }
+
+      ret.push('@@ -' + hunk.oldStart + ',' + hunk.oldLines + ' +' + hunk.newStart + ',' + hunk.newLines + ' @@');
+      ret.push.apply(ret, hunk.lines);
+    }
+
+    return ret.join('\n') + '\n';
+  }
+  function createTwoFilesPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options) {
+    return formatPatch(structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options));
+  }
+  function createPatch(fileName, oldStr, newStr, oldHeader, newHeader, options) {
+    return createTwoFilesPatch(fileName, fileName, oldStr, newStr, oldHeader, newHeader, options);
+  }
+
+  function arrayEqual(a, b) {
+    if (a.length !== b.length) {
+      return false;
+    }
+
+    return arrayStartsWith(a, b);
+  }
+  function arrayStartsWith(array, start) {
+    if (start.length > array.length) {
+      return false;
+    }
+
+    for (var i = 0; i < start.length; i++) {
+      if (start[i] !== array[i]) {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+  function calcLineCount(hunk) {
+    var _calcOldNewLineCount = calcOldNewLineCount(hunk.lines),
+        oldLines = _calcOldNewLineCount.oldLines,
+        newLines = _calcOldNewLineCount.newLines;
+
+    if (oldLines !== undefined) {
+      hunk.oldLines = oldLines;
+    } else {
+      delete hunk.oldLines;
+    }
+
+    if (newLines !== undefined) {
+      hunk.newLines = newLines;
+    } else {
+      delete hunk.newLines;
+    }
+  }
+  function merge(mine, theirs, base) {
+    mine = loadPatch(mine, base);
+    theirs = loadPatch(theirs, base);
+    var ret = {}; // For index we just let it pass through as it doesn't have any necessary meaning.
+    // Leaving sanity checks on this to the API consumer that may know more about the
+    // meaning in their own context.
+
+    if (mine.index || theirs.index) {
+      ret.index = mine.index || theirs.index;
+    }
+
+    if (mine.newFileName || theirs.newFileName) {
+      if (!fileNameChanged(mine)) {
+        // No header or no change in ours, use theirs (and ours if theirs does not exist)
+        ret.oldFileName = theirs.oldFileName || mine.oldFileName;
+        ret.newFileName = theirs.newFileName || mine.newFileName;
+        ret.oldHeader = theirs.oldHeader || mine.oldHeader;
+        ret.newHeader = theirs.newHeader || mine.newHeader;
+      } else if (!fileNameChanged(theirs)) {
+        // No header or no change in theirs, use ours
+        ret.oldFileName = mine.oldFileName;
+        ret.newFileName = mine.newFileName;
+        ret.oldHeader = mine.oldHeader;
+        ret.newHeader = mine.newHeader;
+      } else {
+        // Both changed... figure it out
+        ret.oldFileName = selectField(ret, mine.oldFileName, theirs.oldFileName);
+        ret.newFileName = selectField(ret, mine.newFileName, theirs.newFileName);
+        ret.oldHeader = selectField(ret, mine.oldHeader, theirs.oldHeader);
+        ret.newHeader = selectField(ret, mine.newHeader, theirs.newHeader);
+      }
+    }
+
+    ret.hunks = [];
+    var mineIndex = 0,
+        theirsIndex = 0,
+        mineOffset = 0,
+        theirsOffset = 0;
+
+    while (mineIndex < mine.hunks.length || theirsIndex < theirs.hunks.length) {
+      var mineCurrent = mine.hunks[mineIndex] || {
+        oldStart: Infinity
+      },
+          theirsCurrent = theirs.hunks[theirsIndex] || {
+        oldStart: Infinity
+      };
+
+      if (hunkBefore(mineCurrent, theirsCurrent)) {
+        // This patch does not overlap with any of the others, yay.
+        ret.hunks.push(cloneHunk(mineCurrent, mineOffset));
+        mineIndex++;
+        theirsOffset += mineCurrent.newLines - mineCurrent.oldLines;
+      } else if (hunkBefore(theirsCurrent, mineCurrent)) {
+        // This patch does not overlap with any of the others, yay.
+        ret.hunks.push(cloneHunk(theirsCurrent, theirsOffset));
+        theirsIndex++;
+        mineOffset += theirsCurrent.newLines - theirsCurrent.oldLines;
+      } else {
+        // Overlap, merge as best we can
+        var mergedHunk = {
+          oldStart: Math.min(mineCurrent.oldStart, theirsCurrent.oldStart),
+          oldLines: 0,
+          newStart: Math.min(mineCurrent.newStart + mineOffset, theirsCurrent.oldStart + theirsOffset),
+          newLines: 0,
+          lines: []
+        };
+        mergeLines(mergedHunk, mineCurrent.oldStart, mineCurrent.lines, theirsCurrent.oldStart, theirsCurrent.lines);
+        theirsIndex++;
+        mineIndex++;
+        ret.hunks.push(mergedHunk);
+      }
+    }
+
+    return ret;
+  }
+
+  function loadPatch(param, base) {
+    if (typeof param === 'string') {
+      if (/^@@/m.test(param) || /^Index:/m.test(param)) {
+        return parsePatch(param)[0];
+      }
+
+      if (!base) {
+        throw new Error('Must provide a base reference or pass in a patch');
+      }
+
+      return structuredPatch(undefined, undefined, base, param);
+    }
+
+    return param;
+  }
+
+  function fileNameChanged(patch) {
+    return patch.newFileName && patch.newFileName !== patch.oldFileName;
+  }
+
+  function selectField(index, mine, theirs) {
+    if (mine === theirs) {
+      return mine;
+    } else {
+      index.conflict = true;
+      return {
+        mine: mine,
+        theirs: theirs
+      };
+    }
+  }
+
+  function hunkBefore(test, check) {
+    return test.oldStart < check.oldStart && test.oldStart + test.oldLines < check.oldStart;
+  }
+
+  function cloneHunk(hunk, offset) {
+    return {
+      oldStart: hunk.oldStart,
+      oldLines: hunk.oldLines,
+      newStart: hunk.newStart + offset,
+      newLines: hunk.newLines,
+      lines: hunk.lines
+    };
+  }
+
+  function mergeLines(hunk, mineOffset, mineLines, theirOffset, theirLines) {
+    // This will generally result in a conflicted hunk, but there are cases where the context
+    // is the only overlap where we can successfully merge the content here.
+    var mine = {
+      offset: mineOffset,
+      lines: mineLines,
+      index: 0
+    },
+        their = {
+      offset: theirOffset,
+      lines: theirLines,
+      index: 0
+    }; // Handle any leading content
+
+    insertLeading(hunk, mine, their);
+    insertLeading(hunk, their, mine); // Now in the overlap content. Scan through and select the best changes from each.
+
+    while (mine.index < mine.lines.length && their.index < their.lines.length) {
+      var mineCurrent = mine.lines[mine.index],
+          theirCurrent = their.lines[their.index];
+
+      if ((mineCurrent[0] === '-' || mineCurrent[0] === '+') && (theirCurrent[0] === '-' || theirCurrent[0] === '+')) {
+        // Both modified ...
+        mutualChange(hunk, mine, their);
+      } else if (mineCurrent[0] === '+' && theirCurrent[0] === ' ') {
+        var _hunk$lines;
+
+        // Mine inserted
+        (_hunk$lines = hunk.lines).push.apply(_hunk$lines, _toConsumableArray(collectChange(mine)));
+      } else if (theirCurrent[0] === '+' && mineCurrent[0] === ' ') {
+        var _hunk$lines2;
+
+        // Theirs inserted
+        (_hunk$lines2 = hunk.lines).push.apply(_hunk$lines2, _toConsumableArray(collectChange(their)));
+      } else if (mineCurrent[0] === '-' && theirCurrent[0] === ' ') {
+        // Mine removed or edited
+        removal(hunk, mine, their);
+      } else if (theirCurrent[0] === '-' && mineCurrent[0] === ' ') {
+        // Their removed or edited
+        removal(hunk, their, mine, true);
+      } else if (mineCurrent === theirCurrent) {
+        // Context identity
+        hunk.lines.push(mineCurrent);
+        mine.index++;
+        their.index++;
+      } else {
+        // Context mismatch
+        conflict(hunk, collectChange(mine), collectChange(their));
+      }
+    } // Now push anything that may be remaining
+
+
+    insertTrailing(hunk, mine);
+    insertTrailing(hunk, their);
+    calcLineCount(hunk);
+  }
+
+  function mutualChange(hunk, mine, their) {
+    var myChanges = collectChange(mine),
+        theirChanges = collectChange(their);
+
+    if (allRemoves(myChanges) && allRemoves(theirChanges)) {
+      // Special case for remove changes that are supersets of one another
+      if (arrayStartsWith(myChanges, theirChanges) && skipRemoveSuperset(their, myChanges, myChanges.length - theirChanges.length)) {
+        var _hunk$lines3;
+
+        (_hunk$lines3 = hunk.lines).push.apply(_hunk$lines3, _toConsumableArray(myChanges));
+
+        return;
+      } else if (arrayStartsWith(theirChanges, myChanges) && skipRemoveSuperset(mine, theirChanges, theirChanges.length - myChanges.length)) {
+        var _hunk$lines4;
+
+        (_hunk$lines4 = hunk.lines).push.apply(_hunk$lines4, _toConsumableArray(theirChanges));
+
+        return;
+      }
+    } else if (arrayEqual(myChanges, theirChanges)) {
+      var _hunk$lines5;
+
+      (_hunk$lines5 = hunk.lines).push.apply(_hunk$lines5, _toConsumableArray(myChanges));
+
+      return;
+    }
+
+    conflict(hunk, myChanges, theirChanges);
+  }
+
+  function removal(hunk, mine, their, swap) {
+    var myChanges = collectChange(mine),
+        theirChanges = collectContext(their, myChanges);
+
+    if (theirChanges.merged) {
+      var _hunk$lines6;
+
+      (_hunk$lines6 = hunk.lines).push.apply(_hunk$lines6, _toConsumableArray(theirChanges.merged));
+    } else {
+      conflict(hunk, swap ? theirChanges : myChanges, swap ? myChanges : theirChanges);
+    }
+  }
+
+  function conflict(hunk, mine, their) {
+    hunk.conflict = true;
+    hunk.lines.push({
+      conflict: true,
+      mine: mine,
+      theirs: their
+    });
+  }
+
+  function insertLeading(hunk, insert, their) {
+    while (insert.offset < their.offset && insert.index < insert.lines.length) {
+      var line = insert.lines[insert.index++];
+      hunk.lines.push(line);
+      insert.offset++;
+    }
+  }
+
+  function insertTrailing(hunk, insert) {
+    while (insert.index < insert.lines.length) {
+      var line = insert.lines[insert.index++];
+      hunk.lines.push(line);
+    }
+  }
+
+  function collectChange(state) {
+    var ret = [],
+        operation = state.lines[state.index][0];
+
+    while (state.index < state.lines.length) {
+      var line = state.lines[state.index]; // Group additions that are immediately after subtractions and treat them as one "atomic" modify change.
+
+      if (operation === '-' && line[0] === '+') {
+        operation = '+';
+      }
+
+      if (operation === line[0]) {
+        ret.push(line);
+        state.index++;
+      } else {
+        break;
+      }
+    }
+
+    return ret;
+  }
+
+  function collectContext(state, matchChanges) {
+    var changes = [],
+        merged = [],
+        matchIndex = 0,
+        contextChanges = false,
+        conflicted = false;
+
+    while (matchIndex < matchChanges.length && state.index < state.lines.length) {
+      var change = state.lines[state.index],
+          match = matchChanges[matchIndex]; // Once we've hit our add, then we are done
+
+      if (match[0] === '+') {
+        break;
+      }
+
+      contextChanges = contextChanges || change[0] !== ' ';
+      merged.push(match);
+      matchIndex++; // Consume any additions in the other block as a conflict to attempt
+      // to pull in the remaining context after this
+
+      if (change[0] === '+') {
+        conflicted = true;
+
+        while (change[0] === '+') {
+          changes.push(change);
+          change = state.lines[++state.index];
+        }
+      }
+
+      if (match.substr(1) === change.substr(1)) {
+        changes.push(change);
+        state.index++;
+      } else {
+        conflicted = true;
+      }
+    }
+
+    if ((matchChanges[matchIndex] || '')[0] === '+' && contextChanges) {
+      conflicted = true;
+    }
+
+    if (conflicted) {
+      return changes;
+    }
+
+    while (matchIndex < matchChanges.length) {
+      merged.push(matchChanges[matchIndex++]);
+    }
+
+    return {
+      merged: merged,
+      changes: changes
+    };
+  }
+
+  function allRemoves(changes) {
+    return changes.reduce(function (prev, change) {
+      return prev && change[0] === '-';
+    }, true);
+  }
+
+  function skipRemoveSuperset(state, removeChanges, delta) {
+    for (var i = 0; i < delta; i++) {
+      var changeContent = removeChanges[removeChanges.length - delta + i].substr(1);
+
+      if (state.lines[state.index + i] !== ' ' + changeContent) {
+        return false;
+      }
+    }
+
+    state.index += delta;
+    return true;
+  }
+
+  function calcOldNewLineCount(lines) {
+    var oldLines = 0;
+    var newLines = 0;
+    lines.forEach(function (line) {
+      if (typeof line !== 'string') {
+        var myCount = calcOldNewLineCount(line.mine);
+        var theirCount = calcOldNewLineCount(line.theirs);
+
+        if (oldLines !== undefined) {
+          if (myCount.oldLines === theirCount.oldLines) {
+            oldLines += myCount.oldLines;
+          } else {
+            oldLines = undefined;
+          }
+        }
+
+        if (newLines !== undefined) {
+          if (myCount.newLines === theirCount.newLines) {
+            newLines += myCount.newLines;
+          } else {
+            newLines = undefined;
+          }
+        }
+      } else {
+        if (newLines !== undefined && (line[0] === '+' || line[0] === ' ')) {
+          newLines++;
+        }
+
+        if (oldLines !== undefined && (line[0] === '-' || line[0] === ' ')) {
+          oldLines++;
+        }
+      }
+    });
+    return {
+      oldLines: oldLines,
+      newLines: newLines
+    };
+  }
+
+  // See: http://code.google.com/p/google-diff-match-patch/wiki/API
+  function convertChangesToDMP(changes) {
+    var ret = [],
+        change,
+        operation;
+
+    for (var i = 0; i < changes.length; i++) {
+      change = changes[i];
+
+      if (change.added) {
+        operation = 1;
+      } else if (change.removed) {
+        operation = -1;
+      } else {
+        operation = 0;
+      }
+
+      ret.push([operation, change.value]);
+    }
+
+    return ret;
+  }
+
+  function convertChangesToXML(changes) {
+    var ret = [];
+
+    for (var i = 0; i < changes.length; i++) {
+      var change = changes[i];
+
+      if (change.added) {
+        ret.push('<ins>');
+      } else if (change.removed) {
+        ret.push('<del>');
+      }
+
+      ret.push(escapeHTML(change.value));
+
+      if (change.added) {
+        ret.push('</ins>');
+      } else if (change.removed) {
+        ret.push('</del>');
+      }
+    }
+
+    return ret.join('');
+  }
+
+  function escapeHTML(s) {
+    var n = s;
+    n = n.replace(/&/g, '&amp;');
+    n = n.replace(/</g, '&lt;');
+    n = n.replace(/>/g, '&gt;');
+    n = n.replace(/"/g, '&quot;');
+    return n;
+  }
+
+  exports.Diff = Diff;
+  exports.applyPatch = applyPatch;
+  exports.applyPatches = applyPatches;
+  exports.canonicalize = canonicalize;
+  exports.convertChangesToDMP = convertChangesToDMP;
+  exports.convertChangesToXML = convertChangesToXML;
+  exports.createPatch = createPatch;
+  exports.createTwoFilesPatch = createTwoFilesPatch;
+  exports.diffArrays = diffArrays;
+  exports.diffChars = diffChars;
+  exports.diffCss = diffCss;
+  exports.diffJson = diffJson;
+  exports.diffLines = diffLines;
+  exports.diffSentences = diffSentences;
+  exports.diffTrimmedLines = diffTrimmedLines;
+  exports.diffWords = diffWords;
+  exports.diffWordsWithSpace = diffWordsWithSpace;
+  exports.merge = merge;
+  exports.parsePatch = parsePatch;
+  exports.structuredPatch = structuredPatch;
+
+  Object.defineProperty(exports, '__esModule', { value: true });
+
+})));

BIN
plugins/tooth/image/11.png


BIN
plugins/tooth/image/12.png


BIN
plugins/tooth/image/13.png


BIN
plugins/tooth/image/14.png


BIN
plugins/tooth/image/15.png


BIN
plugins/tooth/image/16.png


BIN
plugins/tooth/image/17.png


BIN
plugins/tooth/image/18.png


BIN
plugins/tooth/image/21.png


BIN
plugins/tooth/image/22.png


BIN
plugins/tooth/image/23.png


BIN
plugins/tooth/image/24.png


BIN
plugins/tooth/image/25.png


BIN
plugins/tooth/image/26.png


BIN
plugins/tooth/image/27.png


BIN
plugins/tooth/image/28.png


BIN
plugins/tooth/image/31.png


BIN
plugins/tooth/image/32.png


BIN
plugins/tooth/image/33.png


BIN
plugins/tooth/image/34.png


BIN
plugins/tooth/image/35.png


BIN
plugins/tooth/image/36.png


BIN
plugins/tooth/image/37.png


BIN
plugins/tooth/image/38.png


BIN
plugins/tooth/image/41.png


BIN
plugins/tooth/image/42.png


BIN
plugins/tooth/image/43.png


BIN
plugins/tooth/image/44.png


BIN
plugins/tooth/image/45.png


BIN
plugins/tooth/image/46.png


BIN
plugins/tooth/image/47.png


BIN
plugins/tooth/image/48.png


+ 334 - 0
plugins/tooth/tooth.css

@@ -0,0 +1,334 @@
+.tooth-all{
+    display: block;
+}
+
+.tooth-all span{
+    display: block;
+}
+
+.div_td { 
+    float: left;
+    color: #615c5c;
+    text-align: center;
+}
+
+.left_up {
+    border-bottom:1px #000000 solid;
+    text-align: left;
+    padding-left: 2px;
+}
+
+.right_up {
+    border-bottom:1px #000000 solid; 
+    border-right:1px #000000 solid;
+    text-align: right;
+    padding-right: 2px;
+}
+
+.left_down {
+    text-align: left;
+    padding-left: 2px;
+}
+
+.right_down {
+    border-right:1px #000000 solid;
+    text-align: right;
+    padding-right: 2px;
+}
+
+#tooth_popup_div {
+    box-shadow: 0 0 10px rgb(0 0 0 / 20%);
+    transition: .25s;
+    color: #333;
+    background-color: #FFF;
+    padding: 10px;
+    border-radius: 5px;
+    z-index: 99999;
+    width: 840px;
+    position: fixed;
+    left: calc(50% - 420px);
+    top: calc(50% - 280px);
+}
+
+#tooth_popup_div .title {
+    text-align: center;
+    border-bottom: 1px solid #807b7b;
+    height: 45px;
+    line-height: 45px;
+    font-weight: bold;
+}
+
+#tooth_popup_div .close_label {
+    display: inline-block;
+    cursor: pointer;
+    width: 25px;
+    position: absolute;
+    right: 13px;
+    top: 5px;
+}
+
+#tooth_popup_div .close {
+    position: relative;
+    width: 2px;
+    height: 15px;
+    background: #807b7b;
+    -webkit-transform: rotate(45deg);
+    -moz-transform: rotate(45deg);
+    -o-transform: rotate(45deg);
+    -ms-transform: rotate(45deg);
+    transform: rotate(45deg);
+    display: inline-block;
+}
+
+#tooth_popup_div .close:after {
+    content: "";
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 2px;
+    height: 15px;
+    background: #807b7b;
+    -webkit-transform: rotate(270deg);
+    -moz-transform: rotate(270deg);
+    -o-transform: rotate(270deg);
+    -ms-transform: rotate(270deg);
+    transform: rotate(270deg);
+}
+
+#tooth_popup_div .content {
+    width: 805px;
+    height: 370px;
+}
+
+#tooth_popup_div .div_td { 
+    width: 400px;
+    height: 110px;
+    line-height: 110px;
+    float: left;
+    text-align: center;
+}
+
+.tooth-image span {
+    display: inline-block;
+    background-repeat: no-repeat;
+    background-size: 45px 110px;
+    width:45px;
+    height: 110px;
+}
+
+/* 右上 */
+.tooth_rght_up1 {
+    background-image: url("./image/11.png");
+}
+.tooth_rght_up2 {
+    background-image: url("./image/12.png");
+}
+.tooth_rght_up3 {
+    background-image: url("./image/13.png");
+}
+.tooth_rght_up4 {
+    background-image: url("./image/14.png");
+}
+.tooth_rght_up5 {
+    background-image: url("./image/15.png");
+}
+.tooth_rght_up6 {
+    background-image: url("./image/16.png");
+}
+.tooth_rght_up7 {
+    background-image: url("./image/17.png");
+}
+.tooth_rght_up8 {
+    background-image: url("./image/18.png");
+}
+
+
+/* 左上 */
+.tooth_left_up1 {
+    background-image: url("./image/21.png");
+}
+
+.tooth_left_up2 {
+    background-image: url("./image/22.png");
+}
+.tooth_left_up3 {
+    background-image: url("./image/23.png");
+}
+.tooth_left_up4 {
+    background-image: url("./image/24.png");
+}
+.tooth_left_up5 {
+    background-image: url("./image/25.png");
+}
+.tooth_left_up6 {
+    background-image: url("./image/26.png");
+}
+.tooth_left_up7 {
+    background-image: url("./image/27.png");
+}
+.tooth_left_up8 {
+    background-image: url("./image/28.png");
+}
+
+/* 右下 */
+.tooth_rght_down1 {
+    background-image: url("./image/41.png");
+}
+.tooth_rght_down2 {
+    background-image: url("./image/42.png");
+}
+.tooth_rght_down3 {
+    background-image: url("./image/43.png");
+}
+.tooth_rght_down4 {
+    background-image: url("./image/44.png");
+}
+.tooth_rght_down5 {
+    background-image: url("./image/45.png");
+}
+.tooth_rght_down6 {
+    background-image: url("./image/46.png");
+}
+.tooth_rght_down7 {
+    background-image: url("./image/47.png");
+}
+.tooth_rght_down8 {
+    background-image: url("./image/48.png");
+}
+
+/* 左下 */
+.tooth_left_down1 {
+    background-image: url("./image/31.png");
+}
+.tooth_left_down2 {
+    background-image: url("./image/32.png");
+}
+.tooth_left_down3 {
+    background-image: url("./image/33.png");
+}
+.tooth_left_down4 {
+    background-image: url("./image/34.png");
+}
+.tooth_left_down5 {
+    background-image: url("./image/35.png");
+}
+.tooth_left_down6 {
+    background-image: url("./image/36.png");
+}
+.tooth_left_down7 {
+    background-image: url("./image/37.png");
+}
+.tooth_left_down8 {
+    background-image: url("./image/38.png");
+}
+
+
+.num-select {
+    width: 100%;
+    height: 30px;
+    line-height: 30px;
+    text-align: center;
+}
+
+.num-select span {
+    display: inline-block;
+    width: 45px;
+    /* height: 25px; */
+    /* line-height: 25px; */
+}
+
+.wisdom-select {
+    width: 100%;
+    /* height: 100px; */
+    text-align: center;
+    position: absolute;
+    top: 233px;
+    left: 805px;
+}
+
+.wisdom-select span {
+    display: block;
+    width: 45px;
+    height: 27px
+}
+
+.wisdom-select span>label,.num-select span>label {
+    display: inline-block;
+    width: 25px !important;
+    height: 25px !important;
+    line-height: 25px !important;
+    border-radius: 50%;
+    cursor: pointer;
+    color: #FFF;
+    background-color: #d2d0d0;
+    /* color: #807b7b; */
+    /* background-color: rgb(52, 168, 166); */
+}
+
+.num-select-hover {
+    color: #FFF !important;
+    background-color: #9fe9e7 !important;
+    border: none !important;
+}
+
+.num-select-confirm {
+    color: #FFF !important;
+    background-color: #34a8a6 !important;
+    border: none !important;
+}
+
+.drop-shadow-confirm,.drop-shadow {
+    -webkit-filter: drop-shadow(0px 0px 4px #34a8a6);
+    filter: drop-shadow(0px 0px 4px #34a8a6);
+}
+
+.header{
+    padding-top: 5px;
+    text-align: center;
+}
+
+.header button {
+    background-color: #fff;
+    border: 2px solid #34a8a6;
+    color: #34a8a6;
+    width: 70px;
+    height: 35px;
+    font-family: 微软雅黑;
+    font-size: 16px;
+    margin: 5px;
+    cursor: pointer;
+}
+
+.header button:hover {
+    background-color: #34a8a6;
+    border: 1px solid #34a8a6;
+    color: #fff;
+}
+
+.footer {
+    text-align: right;
+}
+
+.footer button {
+    width: 110px;
+    height: 35px;
+    margin: 5px;
+    font-size: 16px;
+    cursor: pointer;
+    border: none;
+    border-radius: 3px;
+}
+
+#btnSure {
+    background-color: #34a8a6;
+    color: #fff;
+}
+
+#btnSure:hover {
+    background-color: #9fe9e7;
+    color: #fff;
+}
+
+#btnCancel:hover {
+    background-color: #f5f58e;
+}

+ 645 - 0
plugins/tooth/tooth.js

@@ -0,0 +1,645 @@
+(function($){
+    $.fn.tooth = function(options){
+        let toothId = this.initToothHtml(options);
+        this.initPopup(toothId,options ? options.success : function(){});
+    }
+
+    $.fn.initToothHtml  = function(options){
+        let toothId = this.attr("id");
+        let settings = $.extend({
+            'width': 192,
+            'height': 40,
+            success: function(){}
+        }, options);
+
+        let tdWidth = Math.round(parseInt(settings.width-1)/2-1)-2;
+        let tdHeight = Math.round(parseInt(settings.height)/2-1);
+
+        let html ='<span class="tooth-all" id="tooth_all_'+toothId+'" contenteditable="false" style="width: '+settings.width+'px;">';
+        html += '<span id="tooth_value_'+toothId+'" class="content" style="width: '+settings.width+'px;height: '+settings.height+'px;">';
+        html += '<span class="div_td right_up" style="width: '+tdWidth+'px;height: '+tdHeight+'px;line-height: '+tdHeight+'px;"></span>';
+        html += '<span class="div_td left_up" style="width: '+tdWidth+'px;height: '+tdHeight+'px;line-height: '+tdHeight+'px;"></span>';
+        html += '<span class="div_td right_down" style="width: '+tdWidth+'px;height: '+tdHeight+'px;line-height: '+tdHeight+'px;"></span>';
+        html += '<span class="div_td left_down" style="width: '+tdWidth+'px;height: '+tdHeight+'px;line-height: '+tdHeight+'px;"></span>';
+        html += '</span>';
+        html += '</span>';
+        this.html(html);
+        return toothId;
+    }
+
+    /**
+     * 电子病历选择牙位使用
+    */
+    $.fn.initPopupByEmr = function(toothId,ele,callback,cancelCallback){
+        //点击时弹出牙位选择
+        if ($("#tooth_popup_div").length > 0) { 
+            // 存在删除
+            $("#tooth_popup_div").remove();
+        }
+
+        $("#"+toothId).append(getPopupHtml());
+
+        initPopupEvent(toothId,callback,cancelCallback);
+
+        // 初始化
+        // 右上
+        setValueStyle($(ele).find(".right_up").html(),'rght_up');
+
+        // 左上
+        setValueStyle($(ele).find(".left_up").html(),'left_up');
+
+        // 右下
+        setValueStyle($(ele).find(".right_down").html(),'rght_down');
+
+        // 左下
+        setValueStyle($(ele).find(".left_down").html(),'left_down');
+    }
+
+    initPopupEvent = function(toothId,callback,cancelCallback){
+        //关闭弹出窗口
+        $("#tooth_popup_div .close_label").on("click",function(){
+            $("#tooth_popup_div").remove();
+        });
+
+        //鼠标悬浮事件
+        $(".wisdom-select span>label, .num-select span>label").hover(function(){
+            $(this).toggleClass("num-select-hover"); 
+            let classNm = "tooth_"+$(this).attr("position")+$(this).attr("value");
+            $("."+classNm).toggleClass("drop-shadow");
+        },function(){
+            $(this).toggleClass("num-select-hover"); 
+            let classNm = "tooth_"+$(this).attr("position")+$(this).attr("value");
+            $("."+classNm).toggleClass("drop-shadow"); 
+        });
+
+        //选择效果
+        $(".wisdom-select span>label, .num-select span>label").on("click",function(e){
+            if(e){
+                let target = e.currentTarget;
+                //选择数字字母,有样式删除,无样式添加
+                $(target).toggleClass("num-select-confirm"); 
+                setToothClass($(target));
+            }
+        });
+
+        //添加按钮事件
+        setPopupButton();
+        
+        //确定
+        $("#btnSure").on("click",function(){
+            let allSelects = $(".num-select-confirm");
+            if(!allSelects || allSelects.length == 0){
+                return false;
+            }
+            // console.log(allSelects)
+            let toothResult = {};
+            // toothResult = [];
+            allSelects.each(function(index,ele){
+                let jele = $(ele)
+                // console.log(ele,jele.attr("position"));
+                let position = jele.attr("position");
+                let type = jele.attr("type");
+                let value = jele.attr("value");
+                
+                let obj = {
+                    position: position,
+                    type: type, 
+                    value: value
+                }
+                // toothResult.push(obj);
+                if(type == "letter"){
+                    //乳牙
+                    toothResult[position+"-"+type+"-"+value] = obj;
+                } else {
+                    //恒牙
+                    toothResult[position+"-pmnt-"+value] = obj;
+                }
+            });
+
+            //设置显示
+            // id="tooth_value_'+toothId+'" 
+            //98765E4D3C2B1A
+            let result = setResultHtml(toothResult,toothId)
+
+            // console.log(toothResult);
+            callback(result);
+            $("#tooth_popup_div").remove();
+        });
+
+        //取消
+        $("#btnCancel").on("click",function(){
+            $("#tooth_popup_div").remove();
+            if(cancelCallback){
+                cancelCallback();
+            }
+        });
+
+        //初始化
+        initPopupValue(toothId);
+    }
+
+    $.fn.initPopup = function(toothId,callback){
+        //点击时弹出牙位选择
+        $("#tooth_value_"+toothId).on("click",function(){
+            if ($("#tooth_popup_div").length > 0) { 
+                // 存在删除
+                $("#tooth_popup_div").remove();
+            }
+
+            $("#tooth_all_"+toothId).append(getPopupHtml());
+
+            initPopupEvent(toothId,callback);
+        });
+
+    }
+
+    // 初始化
+    function initPopupValue(toothId){
+        // 右上
+        setValueStyle($("#tooth_all_"+toothId+" .right_up").html(),'rght_up');
+
+        // 左上
+        setValueStyle($("#tooth_all_"+toothId+" .left_up").html(),'left_up');
+
+        // 右下
+        setValueStyle($("#tooth_all_"+toothId+" .right_down").html(),'rght_down');
+
+        // 左下
+        setValueStyle($("#tooth_all_"+toothId+" .left_down").html(),'left_down');
+    }
+
+    function setValueStyle(html,positionVal){
+        if(html){
+            let list = html.split("");
+            list.forEach(value => {
+                let type = "pmnt";
+                if("ABCDE".indexOf(value) >= 0){
+                    type = "letter";
+                }
+
+                let position = positionVal;
+                if(value == "9"){
+                    position = 'wisdom_'+positionVal;
+                    type = "wisdom";
+                }
+
+                let target = $("label[position='"+position+"'][type='"+type+"'][value='"+value+"']");
+                if(target && target.length != 0){
+                    target.addClass("num-select-confirm");
+                    setToothClass(target);
+                }
+            });
+        }
+    }
+
+    function setResultHtml(toothResult,toothId){
+        let result = {};
+
+        // 右上
+        let rghtUpHtml = fmtVal(toothResult["wisdom_rght_up-pmnt-9"]);
+        rghtUpHtml += fmtVal(toothResult["rght_up-pmnt-8"]);
+        rghtUpHtml += fmtVal(toothResult["rght_up-pmnt-7"]);
+        rghtUpHtml += fmtVal(toothResult["rght_up-pmnt-6"]);
+        rghtUpHtml += fmtVal(toothResult["rght_up-pmnt-5"]);
+        rghtUpHtml += fmtVal(toothResult["rght_up-letter-E"]);
+        rghtUpHtml += fmtVal(toothResult["rght_up-pmnt-4"]);
+        rghtUpHtml += fmtVal(toothResult["rght_up-letter-D"]);
+        rghtUpHtml += fmtVal(toothResult["rght_up-pmnt-3"]);
+        rghtUpHtml += fmtVal(toothResult["rght_up-letter-C"]);
+        rghtUpHtml += fmtVal(toothResult["rght_up-pmnt-2"]);
+        rghtUpHtml += fmtVal(toothResult["rght_up-letter-B"]);
+        rghtUpHtml += fmtVal(toothResult["rght_up-pmnt-1"]);
+        rghtUpHtml += fmtVal(toothResult["rght_up-letter-A"]);
+        $("#tooth_all_"+toothId+" .right_up").html(rghtUpHtml);
+        result["rght_up"] = rghtUpHtml;
+
+        //左上
+        let leftUpHtml = fmtVal(toothResult["left_up-pmnt-1"]);
+        leftUpHtml += fmtVal(toothResult["left_up-letter-A"]);
+        leftUpHtml += fmtVal(toothResult["left_up-pmnt-2"]);
+        leftUpHtml += fmtVal(toothResult["left_up-letter-B"]);
+        leftUpHtml += fmtVal(toothResult["left_up-pmnt-3"]);
+        leftUpHtml += fmtVal(toothResult["left_up-letter-C"]);
+        leftUpHtml += fmtVal(toothResult["left_up-pmnt-4"]);
+        leftUpHtml += fmtVal(toothResult["left_up-letter-D"]);
+        leftUpHtml += fmtVal(toothResult["left_up-pmnt-5"]);
+        leftUpHtml += fmtVal(toothResult["left_up-letter-E"]);
+        leftUpHtml += fmtVal(toothResult["left_up-pmnt-6"]);
+        leftUpHtml += fmtVal(toothResult["left_up-pmnt-7"]);
+        leftUpHtml += fmtVal(toothResult["left_up-pmnt-8"]);
+        leftUpHtml += fmtVal(toothResult["wisdom_left_up-pmnt-9"]);
+        $("#tooth_all_"+toothId+" .left_up").html(leftUpHtml);
+        result["left_up"] = leftUpHtml;
+
+        // 右下
+        let rghtDownHtml = fmtVal(toothResult["wisdom_rght_down-pmnt-9"]);
+        rghtDownHtml += fmtVal(toothResult["rght_down-pmnt-8"]);
+        rghtDownHtml += fmtVal(toothResult["rght_down-pmnt-7"]);
+        rghtDownHtml += fmtVal(toothResult["rght_down-pmnt-6"]);
+        rghtDownHtml += fmtVal(toothResult["rght_down-pmnt-5"]);
+        rghtDownHtml += fmtVal(toothResult["rght_down-letter-E"]);
+        rghtDownHtml += fmtVal(toothResult["rght_down-pmnt-4"]);
+        rghtDownHtml += fmtVal(toothResult["rght_down-letter-D"]);
+        rghtDownHtml += fmtVal(toothResult["rght_down-pmnt-3"]);
+        rghtDownHtml += fmtVal(toothResult["rght_down-letter-C"]);
+        rghtDownHtml += fmtVal(toothResult["rght_down-pmnt-2"]);
+        rghtDownHtml += fmtVal(toothResult["rght_down-letter-B"]);
+        rghtDownHtml += fmtVal(toothResult["rght_down-pmnt-1"]);
+        rghtDownHtml += fmtVal(toothResult["rght_down-letter-A"]);
+        $("#tooth_all_"+toothId+" .right_down").html(rghtDownHtml);
+        result["rght_down"] = rghtDownHtml;
+
+        //左下
+        let leftDownHtml = fmtVal(toothResult["left_down-pmnt-1"]);
+        leftDownHtml += fmtVal(toothResult["left_down-letter-A"]);
+        leftDownHtml += fmtVal(toothResult["left_down-pmnt-2"]);
+        leftDownHtml += fmtVal(toothResult["left_down-letter-B"]);
+        leftDownHtml += fmtVal(toothResult["left_down-pmnt-3"]);
+        leftDownHtml += fmtVal(toothResult["left_down-letter-C"]);
+        leftDownHtml += fmtVal(toothResult["left_down-pmnt-4"]);
+        leftDownHtml += fmtVal(toothResult["left_down-letter-D"]);
+        leftDownHtml += fmtVal(toothResult["left_down-pmnt-5"]);
+        leftDownHtml += fmtVal(toothResult["left_down-letter-E"]);
+        leftDownHtml += fmtVal(toothResult["left_down-pmnt-6"]);
+        leftDownHtml += fmtVal(toothResult["left_down-pmnt-7"]);
+        leftDownHtml += fmtVal(toothResult["left_down-pmnt-8"]);
+        leftDownHtml += fmtVal(toothResult["wisdom_left_down-pmnt-9"]);
+        $("#tooth_all_"+toothId+" .left_down").html(leftDownHtml);
+        result["left_down"] = leftDownHtml;
+
+        return result;
+    }
+
+    function fmtVal(value){
+        if(value){
+            return value.value;
+        }
+        return "";
+    }
+
+    // 选中数字 或者字母时,设置牙齿的样式
+    function setToothClass(ele){
+        let key = ele.attr("value");
+        let isSelect1 = ele.hasClass("num-select-confirm");
+        let isSelect2 = null;
+        if(ele.attr("type") == "letter"){
+            switch (key) {
+                case "A":
+                    key = "1";
+                    break;
+                case "B":
+                    key = "2";
+                    break;
+                case "C":
+                    key = "3";
+                    break;
+                case "D":
+                    key = "4";
+                    break;
+                case "E":
+                    key = "5";
+                    break;
+                default:
+                    break;
+            }
+            isSelect2 = $("label[position='"+ele.attr("position")+"'][type='pmnt'][value='"+key+"']").hasClass("num-select-confirm");
+        } else {
+            switch (key) {
+                case "1":
+                    key = "A";
+                    break;
+                case "2":
+                    key = "B";
+                    break;
+                case "3":
+                    key = "C";
+                    break;
+                case "4":
+                    key = "D";
+                    break;
+                case "5":
+                    key = "E";
+                    break;
+                default:
+                    break;
+            }
+            isSelect2 = $("label[position='"+ele.attr("position")+"'][type='letter'][value='"+key+"']").hasClass("num-select-confirm");
+        }
+        // console.log("isSelect1="+isSelect1,"isSelect2="+isSelect2)
+        //数字或者字母有一天选择,则牙齿选中
+        let classNm = "tooth_"+ele.attr("position")+key;
+        // console.log("classNm=",classNm)
+        if(isSelect1 || isSelect2){
+            //选择牙齿
+            $("."+classNm).addClass("drop-shadow-confirm"); 
+        } else {
+            $("."+classNm).removeClass("drop-shadow-confirm"); 
+        }
+    }
+
+    function getPopupHtml(){
+        let html = '<div id="tooth_popup_div">';
+        html += '<div class="title">选择牙位<label class="close_label" title="关闭"><span class="close"></span></label></div>';
+        html += '<div class="header">';
+        html += '<button type="button" id="btnMilkTeeth">乳牙</button>';
+        html += '<button type="button" id="btnAllTeeth">全口</button>';
+        html += '<button type="button" id="btnLeftUpTeeth">左上</button>';
+        html += '<button type="button" id="btnLeftDownTeeth">左下</button>';
+        html += '<button type="button" id="btnRghtUpTeeth">右上</button>';
+        html += '<button type="button" id="btnRghtDownTeeth">右下</button>';
+        html += '<button type="button" id="btnUpHalfTeeth">上半口</button>';
+        html += '<button type="button" id="btnDownHalfTeeth">下半口</button>';
+        html += '<button type="button" id="btnClearTeeth">清除</button>';
+        html += '</div>';
+        html += '<div class="content">';
+        
+        //右上牙图
+        html += '<div class="div_td tooth-image" id="tooth_rght_up">';
+        html += '<span class="tooth_rght_up8"></span>';
+        html += '<span class="tooth_rght_up7"></span>';
+        html += '<span class="tooth_rght_up6"></span>';
+        html += '<span class="tooth_rght_up5 tooth_rght_upE"></span>';
+        html += '<span class="tooth_rght_up4 tooth_rght_upD"></span>';
+        html += '<span class="tooth_rght_up3 tooth_rght_upC"></span>';
+        html += '<span class="tooth_rght_up2 tooth_rght_upB"></span>';
+        html += '<span class="tooth_rght_up1 tooth_rght_upA"></span>';
+        html += '</div>';
+
+        //左上牙图
+        html += '<div class="div_td tooth-image" id="tooth_left_up">';
+        html += '<span class="tooth_left_up1 tooth_left_upA"></span>';
+        html += '<span class="tooth_left_up2 tooth_left_upB"></span>';
+        html += '<span class="tooth_left_up3 tooth_left_upC"></span>';
+        html += '<span class="tooth_left_up4 tooth_left_upD"></span>';
+        html += '<span class="tooth_left_up5 tooth_left_upE"></span>';
+        html += '<span class="tooth_left_up6"></span>';
+        html += '<span class="tooth_left_up7"></span>';
+        html += '<span class="tooth_left_up8"></span>';
+        html += '</div>';
+
+        //右上选择
+        html += '<div class="div_td right_up" style="height:60px;line-height: 60px;padding-bottom: 10px;">';
+        //数字选择
+        html += '<div class="num-select">'
+        html += '<span><label position="rght_up" type="pmnt" value="8">8</label></span>';
+        html += '<span><label position="rght_up" type="pmnt" value="7">7</label></span>';
+        html += '<span><label position="rght_up" type="pmnt" value="6">6</label></span>';
+        html += '<span><label position="rght_up" type="pmnt" value="5">5</label></span>';
+        html += '<span><label position="rght_up" type="pmnt" value="4">4</label></span>';
+        html += '<span><label position="rght_up" type="pmnt" value="3">3</label></span>';
+        html += '<span><label position="rght_up" type="pmnt" value="2">2</label></span>';
+        html += '<span><label position="rght_up" type="pmnt" value="1">1</label></span>';
+        html += '</div>';
+        //字母选择
+        html += '<div class="num-select">'
+        html += '<span></span>';
+        html += '<span></span>';
+        html += '<span></span>';
+        html += '<span><label position="rght_up" type="letter" value="E">E</label></span>';
+        html += '<span><label position="rght_up" type="letter" value="D">D</label></span>';
+        html += '<span><label position="rght_up" type="letter" value="C">C</label></span>';
+        html += '<span><label position="rght_up" type="letter" value="B">B</label></span>';
+        html += '<span><label position="rght_up" type="letter" value="A">A</label></span>';
+        html += '</div>';
+        html += '</div>';
+
+        //左上选择
+        html += '<div class="div_td left_up" style="height:60px;line-height: 60px;padding-bottom: 10px;">';
+        //数字选择
+        html += '<div class="num-select">'
+        html += '<span><label position="left_up" type="pmnt" value="1">1</label></span>';
+        html += '<span><label position="left_up" type="pmnt" value="2">2</label></span>';
+        html += '<span><label position="left_up" type="pmnt" value="3">3</label></span>';
+        html += '<span><label position="left_up" type="pmnt" value="4">4</label></span>';
+        html += '<span><label position="left_up" type="pmnt" value="5">5</label></span>';
+        html += '<span><label position="left_up" type="pmnt" value="6">6</label></span>';
+        html += '<span><label position="left_up" type="pmnt" value="7">7</label></span>';
+        html += '<span><label position="left_up" type="pmnt" value="8">8</label></span>';
+        html += '</div>';
+        //字母选择
+        html += '<div class="num-select">'
+        html += '<span><label position="left_up" type="letter" value="A">A</label></span>';
+        html += '<span><label position="left_up" type="letter" value="B">B</label></span>';
+        html += '<span><label position="left_up" type="letter" value="C">C</label></span>';
+        html += '<span><label position="left_up" type="letter" value="D">D</label></span>';
+        html += '<span><label position="left_up" type="letter" value="E">E</label></span>';
+        html += '<span></span>';
+        html += '<span></span>';
+        html += '<span></span>';
+        html += '</div>';
+        html += '</div>';
+
+        // 右下选择
+        html += '<div class="div_td right_down" style="height:60px;line-height: 60px;padding-top: 10px;">';
+        //字母选择
+        html += '<div class="num-select">'
+        html += '<span></span>';
+        html += '<span></span>';
+        html += '<span></span>';
+        html += '<span><label position="rght_down" type="letter" value="E">E</label></span>';
+        html += '<span><label position="rght_down" type="letter" value="D">D</label></span>';
+        html += '<span><label position="rght_down" type="letter" value="C">C</label></span>';
+        html += '<span><label position="rght_down" type="letter" value="B">B</label></span>';
+        html += '<span><label position="rght_down" type="letter" value="A">A</label></span>';
+        html += '</div>';
+        //数字选择
+        html += '<div class="num-select">'
+        html += '<span><label position="rght_down" type="pmnt" value="8">8</label></span>';
+        html += '<span><label position="rght_down" type="pmnt" value="7">7</label></span>';
+        html += '<span><label position="rght_down" type="pmnt" value="6">6</label></span>';
+        html += '<span><label position="rght_down" type="pmnt" value="5">5</label></span>';
+        html += '<span><label position="rght_down" type="pmnt" value="4">4</label></span>';
+        html += '<span><label position="rght_down" type="pmnt" value="3">3</label></span>';
+        html += '<span><label position="rght_down" type="pmnt" value="2">2</label></span>';
+        html += '<span><label position="rght_down" type="pmnt" value="1">1</label></span>';
+        html += '</div>';
+        html += '</div>';
+
+        // 左下选择
+        html += '<div class="div_td left_down" style="height:60px;line-height: 60px;padding-top: 10px;">';
+        //字母选择
+        html += '<div class="num-select">'
+        html += '<span><label position="left_down" type="letter" value="A">A</label></span>';
+        html += '<span><label position="left_down" type="letter" value="B">B</label></span>';
+        html += '<span><label position="left_down" type="letter" value="C">C</label></span>';
+        html += '<span><label position="left_down" type="letter" value="D">D</label></span>';
+        html += '<span><label position="left_down" type="letter" value="E">E</label></span>';
+        html += '<span></span>';
+        html += '<span></span>';
+        html += '<span></span>';
+        html += '</div>';
+        //数字选择
+        html += '<div class="num-select">'
+        html += '<span><label position="left_down" type="pmnt" value="1">1</label></span>';
+        html += '<span><label position="left_down" type="pmnt" value="2">2</label></span>';
+        html += '<span><label position="left_down" type="pmnt" value="3">3</label></span>';
+        html += '<span><label position="left_down" type="pmnt" value="4">4</label></span>';
+        html += '<span><label position="left_down" type="pmnt" value="5">5</label></span>';
+        html += '<span><label position="left_down" type="pmnt" value="6">6</label></span>';
+        html += '<span><label position="left_down" type="pmnt" value="7">7</label></span>';
+        html += '<span><label position="left_down" type="pmnt" value="8">8</label></span>';
+        html += '</div>';
+        html += '</div>';
+
+        //右下牙图
+        html += '<div class="div_td tooth-image" id="tooth_rght_down">';
+        html += '<span class="tooth_rght_down8"></span>';
+        html += '<span class="tooth_rght_down7"></span>';
+        html += '<span class="tooth_rght_down6"></span>';
+        html += '<span class="tooth_rght_down5 tooth_rght_downE"></span>';
+        html += '<span class="tooth_rght_down4 tooth_rght_downD"></span>';
+        html += '<span class="tooth_rght_down3 tooth_rght_downC"></span>';
+        html += '<span class="tooth_rght_down2 tooth_rght_downB"></span>';
+        html += '<span class="tooth_rght_down1 tooth_rght_downA"></span>';
+        html += '</div>';
+
+        //左下牙图
+        html += '<div class="div_td tooth-image" id="tooth_left_down">';
+        html += '<span class="tooth_left_down1 tooth_left_downA"></span>';
+        html += '<span class="tooth_left_down2 tooth_left_downB"></span>';
+        html += '<span class="tooth_left_down3 tooth_left_downC"></span>';
+        html += '<span class="tooth_left_down4 tooth_left_downD"></span>';
+        html += '<span class="tooth_left_down5 tooth_left_downE"></span>';
+        html += '<span class="tooth_left_down6"></span>';
+        html += '<span class="tooth_left_down7"></span>';
+        html += '<span class="tooth_left_down8"></span>';
+        html += '</div>';
+
+        //智齿
+        html += '<div class="wisdom-select" id="wisdom_tooth">';
+        html += '<span><label position="wisdom_rght_up" type="wisdom" value="9">19</label></span>';
+        html += '<span><label position="wisdom_left_up" type="wisdom" value="9">29</label></span>';
+        html += '<span><label position="wisdom_left_down" type="wisdom" value="9">39</label></span>';
+        html += '<span><label position="wisdom_rght_down" type="wisdom" value="9">49</label></span>';
+        html += '</div>';
+
+        html += '</div>';
+
+        //按钮
+        html += '<div class="footer">';
+        html += '<button type="button" id="btnSure">确定</button>';
+        html += '<button type="button" id="btnCancel">取消</button>';
+        html += '</div>';
+
+        html += '</div>';
+        return html;
+    }
+
+    function setPopupButton(){
+
+        // 左上
+        function setLeftUpTeeth(){
+            //数字
+            $("label[position='left_up'][type='pmnt']").addClass("num-select-confirm");
+            //牙齿
+            $("#tooth_left_up span").addClass("drop-shadow-confirm");
+        }
+
+        // 左下
+        function setLeftDownTeeth(){
+            //数字
+            $("label[position='left_down'][type='pmnt']").addClass("num-select-confirm");
+            //牙齿
+            $("#tooth_left_down span").addClass("drop-shadow-confirm");
+        }
+
+        // 右上
+        function setRghtUpTeeth(){
+            //数字
+            $("label[position='rght_up'][type='pmnt']").addClass("num-select-confirm");
+            //牙齿
+            $("#tooth_rght_up span").addClass("drop-shadow-confirm");
+        }
+
+        // 右下
+        function setRghtDownTeeth(){
+            //数字
+            $("label[position='rght_down'][type='pmnt']").addClass("num-select-confirm");
+            //牙齿
+            $("#tooth_rght_down span").addClass("drop-shadow-confirm");
+        }
+
+        function clearSelect(){
+            $(".drop-shadow-confirm").removeClass("drop-shadow-confirm");
+            $(".num-select-confirm").removeClass("num-select-confirm");
+        }
+
+        //乳牙
+        $("#btnMilkTeeth").on("click",function(){
+            clearSelect();
+
+            //字母
+            $("label[position='left_up'][type='letter']").addClass("num-select-confirm");
+            $("label[position='left_down'][type='letter']").addClass("num-select-confirm");
+            $("label[position='rght_up'][type='letter']").addClass("num-select-confirm");
+            $("label[position='rght_down'][type='letter']").addClass("num-select-confirm");
+
+            for(let i=1;i<=5;i++){
+                //数字
+                // $("label[position='left_up'][value='"+i+"']").addClass("num-select-confirm");
+                // $("label[position='left_down'][value='"+i+"']").addClass("num-select-confirm");
+                // $("label[position='rght_up'][value='"+i+"']").addClass("num-select-confirm");
+                // $("label[position='rght_down'][value='"+i+"']").addClass("num-select-confirm");
+
+                //牙齿
+                $(".tooth_left_up"+i).addClass("drop-shadow-confirm");
+                $(".tooth_left_down"+i).addClass("drop-shadow-confirm");
+                $(".tooth_rght_up"+i).addClass("drop-shadow-confirm");
+                $(".tooth_rght_down"+i).addClass("drop-shadow-confirm");
+            }
+        });
+
+        //全口
+        $("#btnAllTeeth").on("click",function(){
+            clearSelect();
+            setLeftUpTeeth();
+            setLeftDownTeeth();
+            setRghtUpTeeth();
+            setRghtDownTeeth();
+        });
+
+        //左上
+        $("#btnLeftUpTeeth").on("click",function(){
+            clearSelect()
+            setLeftUpTeeth();
+        });
+
+        //左下
+        $("#btnLeftDownTeeth").on("click",function(){
+            clearSelect()
+            setLeftDownTeeth();
+        });
+
+        //右上
+        $("#btnRghtUpTeeth").on("click",function(){
+            clearSelect()
+            setRghtUpTeeth();
+        });
+
+        //右下
+        $("#btnRghtDownTeeth").on("click",function(){
+            clearSelect()
+            setRghtDownTeeth();
+        });
+
+        //上半口
+        $("#btnUpHalfTeeth").on("click",function(){
+            clearSelect()
+            setLeftUpTeeth();
+            setRghtUpTeeth();
+        });
+
+        //下半口
+        $("#btnDownHalfTeeth").on("click",function(){
+            clearSelect()
+            setLeftDownTeeth();
+            setRghtDownTeeth();
+        });
+
+        
+        //清除
+        $("#btnClearTeeth").on("click",function(){
+            clearSelect();
+        });
+    }
+})(jQuery);

+ 283 - 0
plugins/ue.js

@@ -0,0 +1,283 @@
+/** appendbase 追加内容到基本元素中
+ * **/
+UE.plugins["appendbase"] = function() {
+  let me = this;
+  me.commands["appendbase"] = {
+    execCommand: function(cmd,text) {
+      let range = me.selection.getRange();
+      //判断是否在基本元素中
+      let startNode = range.startContainer;
+      let endNode = range.endContainer;
+
+      let startA = UE.dom.domUtils.findParentByTagName(startNode, "a", true);
+      let endA  = UE.dom.domUtils.findParentByTagName(endNode, "a", true);
+
+      if(!startA || startA.getAttribute("class").indexOf("input") == -1){
+          return false;
+      }
+
+      if(!endA || endA.getAttribute("class").indexOf("input") == -1){
+          return false;
+      }
+
+      //在同一个元素内才插入
+      if(startA == endA){
+          let dataType = startA.getAttribute("dataType");
+          //文本02  多行文本03  数字04
+          if(dataType == "02"){
+              // startA.innerHTML = startA.innerHTML+text;
+              // insertAfterText(startA, text)
+              let value = startA.innerHTML;
+              let newValue = value.substring(0, range.startOffset) + text + value.substring(range.endOffset, value.length);
+              startA.innerHTML = newValue;
+          } else if(dataType == "03"){
+              let textarea = $(startA).find("textarea");
+              textarea.insertAtCaret(text);
+          }
+      }
+    }
+  }
+};
+//end UE.plugins["appendbase"]
+
+/** amendment 修订
+ * **/
+UE.plugins["amendment"] = function() {
+  let me = this;
+
+  // emrRemark.init(me);
+
+  function getValue(ele){
+    let value = "";
+    if(ele){
+      if(ele.nodeName == "TEXTAREA"){
+        value = ele.textContent;
+      } else if(ele.nodeName == "INPUT"){
+        value = ele.value;
+      } else {
+        value = ele.textContent;
+      }
+    }
+    return value;
+  }
+
+  function beforeInputEvent(e){
+    // console.log("beforeInputEvent")
+    // console.log(e)
+    let ele = e.target;
+    // console.log(ele)
+    // console.log(util)
+    if(ele){
+      // 获取到a标签
+      let inputA = emrRemark.getParentA(ele);
+      if(inputA){
+        let value = getValue(ele);
+        emrRemark.setData(inputA,value);
+      }
+    }
+  }
+  
+  function inputEvent(e){
+      // console.log("amendment.inputEvent")
+      let ele = e.target;
+      // console.log(ele)
+      // showEleRemark(ele);
+      if(ele){
+        let inputA = emrRemark.getParentA(ele);
+        if(inputA){
+          emrRemark.showAllRemark();
+        }
+      }
+  }
+
+  me.commands["amendment"] = { 
+    execCommand: function(cmd,ui) {
+      me.options.amendment = !me.options.amendment;
+      // console.log(me.options.amendment)
+      if(ui.target){
+        if(me.options.amendment){
+          me.document.addEventListener("beforeinput", beforeInputEvent),
+          me.document.addEventListener("input", inputEvent);
+          ui.target.classList.add("edui-active");
+        } else {
+          me.document.removeEventListener("beforeinput", beforeInputEvent),
+          me.document.removeEventListener("input", inputEvent);
+          ui.target.classList.remove("edui-active");
+        }
+        emrRemark.showAllRemark();
+      }
+    }
+  }
+};
+//end UE.plugins["amendment"]
+
+/** annotations 批注
+ * **/
+UE.plugins["annotations"] = function() {
+  let me = this;
+
+  me.commands["annotations"] = {
+    execCommand: function(cmd,ui) {
+      me.options.annotations = !me.options.annotations;
+      // console.log(me.options.annotations)
+      if(ui.target){
+        if(me.options.annotations){
+          ui.target.classList.add("edui-active");
+          me.document.addEventListener("input", emrRemark.annotationsInput);
+        } else {
+          ui.target.classList.remove("edui-active");
+          me.document.removeEventListener("input", emrRemark.annotationsInput);
+        }
+        emrRemark.showAllRemark();
+      }
+    }
+  }
+};
+//end UE.plugins["annotations"]
+
+/** addannotations 添加批注
+ * **/
+UE.plugins["addannotations"] = function() {
+  let me = this;
+
+  function createSpan(){
+    let span = me.document.createElement("label");
+    span.classList.add("note");
+    span.setAttribute("note","true");
+    span.setAttribute("data-original","");
+    span.setAttribute("data-create-time", util.formatDate(new Date()));
+    span.setAttribute("data-creator", me.options.userName);
+    // span.setAttribute("data-id", dataId);
+    return span;
+  }
+
+  function setAndShowRemark(){
+    if(!me.options.annotations){
+      me.options.annotations = !me.options.annotations;
+      me.document.addEventListener("input", emrRemark.annotationsInput);
+      // 设置批注为选中状态
+      let toolbar = document.querySelector(".edui-for-annotations");
+      if(toolbar && toolbar.firstChild){
+        toolbar.firstChild.classList.add("edui-active");
+      }
+    }
+    emrRemark.showAllRemark();
+  }
+
+  me.commands["addannotations"] = {
+    execCommand: function(cmd,ui) {
+      // console.log("addannotations")
+      let range = me.selection.getRange().adjustmentBoundary();
+      // console.log(range.startContainer)
+
+      // 处理牙位
+      let startA = emrRemark.getParentA(range.startContainer);
+      let endA = emrRemark.getParentA(range.endContainer);
+      // console.log(startA);
+      // console.log(endA);
+
+      // 同一个牙位
+      if(startA && endA && startA == endA && startA.getAttribute("datatype") == "tooth"){
+        let tooth = startA.querySelector(".tooth-all");
+        tooth.innerHTML = '<label class="note" note="true" data-create-time="'+util.formatDate(new Date())
+              +'" data-creator="'+me.options.userName+'" data-original="">'+tooth.innerHTML+'</label>';
+        setAndShowRemark();
+        return false;
+      }
+      // 处理牙位 end
+
+      // 处理图片
+      if(startA && endA && startA == endA && startA.getAttribute("datatype") == "imgUpload"){
+        let note = me.document.createElement("label");
+        note.className = "note";
+        note.style.position="absolute";
+        // 是否可以单独remove而不需要其他处理
+        note.setAttribute("remove","true");
+        note.setAttribute("note","true");
+        note.setAttribute("data-create-time", util.formatDate(new Date()));
+        note.setAttribute("data-creator", me.options.userName);
+        note.setAttribute("data-original", "");
+
+        let cupload = startA.querySelector(".Cupload")
+        cupload.prepend(note);
+        
+        setAndShowRemark();
+        // startA.innerHTML = '<label class="note" note="true" data-create-time="'+util.formatDate(new Date())
+        //       +'" data-creator="'+me.options.userName+'" data-original="">'+startA.innerHTML+'</label>';
+        return false;
+      }
+      // 处理图片 end
+
+      //若是未选择,就在当前位置插入节点
+      if(range.endOffset == range.startOffset){
+        range.insertNode(createSpan());
+        setAndShowRemark();
+        return;
+      }
+      //若是已经选择,走以下流程
+      // let endOffset = range.endOffset;
+      // console.log(range);
+
+      // 起始节点若是空的,跳过
+      let rangeNode = null;
+      range.traversal(function(node){
+        let content = node.textContent;
+        if(content && content.trim() && !rangeNode){
+          rangeNode = node;
+          return false;
+        }
+      });
+
+      range.setStart(rangeNode,0);
+      // console.log(rangeNode);
+      // console.log(range);
+
+      range.applyInlineStyle("label",{
+        "class":"note",
+        "note":"true",
+        "data-original":"",
+        "data-create-time": util.formatDate(new Date()),
+        "data-creator": me.options.userName
+      }, null, true);
+
+      // let text = startNode.textContent;
+      // let selectText = "";
+      // let preText = text.substring(0,range.startOffset);
+      // let suffText = "";
+      // // 若是选择同一个元素,则按位移量获取文本,若不是同一个元素,则以起始元素为准,结束位移量就是起始元素的结束位置
+      // if(range.startContainer == range.endContainer){
+      //   selectText = text.substring(range.startOffset,endOffset);
+      //   suffText = text.substring(endOffset,text.length);
+      // } else {
+      //   selectText = text.substring(range.startOffset,text.length);
+      // }
+     
+      // let resultText = '<span class="note" note="true" data-create-time="'+datetime
+      //             +'" data-creator="'+me.options.userName+'" data-id="'+remarkId+'">'+selectText+'</span>';
+
+      // console.log("preText=="+preText);
+      // console.log("selectText=="+selectText);
+      // console.log("suffText=="+suffText);
+      // console.log("resultText=="+resultText);
+      // startNode.parentNode.innerHTML = startNode.parentNode.innerHTML.replace(selectText,resultText);
+
+      // console.log(me.selection)
+      me.selection.clearRange();
+      // debugger
+      setAndShowRemark();
+
+    }
+  }
+};
+//end UE.plugins["addannotations"]
+
+  /** 签名
+   * **/
+  UE.plugins["sign"] = function() {
+    let me = this;
+    me.commands["sign"] = {
+      execCommand: function(cmd) {
+        sign(me);
+      }
+    }
+  };

+ 423 - 0
pom.xml

@@ -0,0 +1,423 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>com.qmrb</groupId>
+    <artifactId>parking-server</artifactId>
+    <version>2.2.1</version>
+    <description>基于 SpringBoot3 快速构建的权限管理系统</description>
+
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>3.1.0</version> <!-- lookup parent from repository -->
+        <relativePath/>
+    </parent>
+
+    <properties>
+    	<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
+        <maven.compiler.source>17</maven.compiler.source>
+        <maven.compiler.target>17</maven.compiler.target>
+
+        <hutool.version>5.8.15</hutool.version>
+
+        <mysql.version>8.0.28</mysql.version>
+        <druid.version>1.2.16</druid.version>
+        <mybatis-plus.version>3.5.3.1</mybatis-plus.version>
+        <dynamic-datasource.version>4.1.3</dynamic-datasource.version>
+
+        <knife4j.version>4.0.0</knife4j.version>
+
+        <mapstruct.version>1.5.3.Final</mapstruct.version>
+        <lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
+
+        <xxl-job.version>2.4.0</xxl-job.version>
+
+        <jjwt.version>0.11.5</jjwt.version>
+
+        <easyexcel.version>3.1.5</easyexcel.version>
+
+        <!-- 分布式文件存储 -->
+        <minio.version>8.5.2</minio.version>
+        <okhttp3.version>4.8.1</okhttp3.version>
+
+        <!-- 验证码 -->
+        <easy-captcha.version>1.6.2</easy-captcha.version>
+        <nashorn.version>15.4</nashorn.version>
+
+        <!-- redisson 分布式锁 -->
+        <redisson.version>3.21.0</redisson.version>
+    </properties>
+
+    <dependencies>
+		
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <!--编译测试环境,不打包在lib-->
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+            <version>${hutool.version}</version>
+        </dependency>
+
+        <!-- 允许使用Lombok的Java Bean类中使用MapStruct注解 (Lombok 1.18.20+) -->
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok-mapstruct-binding</artifactId>
+            <version>${lombok-mapstruct-binding.version}</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-security</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-aop</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-pool2</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+            <version>${mysql.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.microsoft.sqlserver</groupId>
+            <artifactId>mssql-jdbc</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>druid-spring-boot-starter</artifactId>
+            <version>${druid.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-boot-starter</artifactId>
+            <version>${mybatis-plus.version}</version>
+        </dependency>
+        
+        <dependency>
+		    <groupId>org.mybatis.spring.boot</groupId>
+		    <artifactId>mybatis-spring-boot-starter-test</artifactId>
+		    <version>1.3.2</version>
+		    <scope>test</scope>
+		</dependency>
+
+		<dependency>
+			<groupId>com.baomidou</groupId>
+			<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
+			<version>${dynamic-datasource.version}</version>
+		</dependency>
+
+        <dependency>
+            <groupId>com.github.xiaoymin</groupId>
+            <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
+            <version>${knife4j.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.mapstruct</groupId>
+            <artifactId>mapstruct</artifactId>
+            <version>${mapstruct.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.mapstruct</groupId>
+            <artifactId>mapstruct-processor</artifactId>
+            <version>${mapstruct.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.xuxueli</groupId>
+            <artifactId>xxl-job-core</artifactId>
+            <version>${xxl-job.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt-api</artifactId>
+            <version>${jjwt.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt-impl</artifactId>
+            <version>${jjwt.version}</version>
+            <scope>runtime</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt-jackson</artifactId>
+            <version>${jjwt.version}</version>
+            <scope>runtime</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>easyexcel</artifactId>
+            <version>${easyexcel.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+
+        <!-- 分布式文件存储 -->
+        <dependency>
+            <groupId>io.minio</groupId>
+            <artifactId>minio</artifactId>
+            <version>${minio.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.github.whvcse</groupId>
+            <artifactId>easy-captcha</artifactId>
+            <version>${easy-captcha.version}</version>
+        </dependency>
+
+        <!-- Java8 之后JavaScript引擎nashorn被移除导致验证码解析报错-->
+        <dependency>
+            <groupId>org.openjdk.nashorn</groupId>
+            <artifactId>nashorn-core</artifactId>
+            <version>${nashorn.version}</version>
+        </dependency>
+
+        <!-- redisson 分布式锁 -->
+        <dependency>
+            <groupId>org.redisson</groupId>
+            <artifactId>redisson-spring-boot-starter</artifactId>
+            <version>${redisson.version}</version>
+        </dependency>
+
+        <dependency>
+        	<groupId>org.springframework.boot</groupId>
+        	<artifactId>spring-boot-configuration-processor</artifactId>
+        	<optional>true</optional>
+        </dependency>
+
+
+
+        <dependency>
+            <groupId>org.apache.axis</groupId>
+            <artifactId>axis</artifactId>
+            <version>1.4</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.axis</groupId>
+            <artifactId>axis-jaxrpc</artifactId>
+            <version>1.4</version>
+        </dependency>
+        <dependency>
+            <groupId>commons-logging</groupId>
+            <artifactId>commons-logging</artifactId>
+            <version>1.2</version>
+        </dependency>
+        <dependency>
+            <groupId>commons-discovery</groupId>
+            <artifactId>commons-discovery</artifactId>
+            <version>0.5</version>
+        </dependency>
+        <dependency>
+            <groupId>javax.xml.soap</groupId>
+            <artifactId>javax.xml.soap-api</artifactId>
+            <version>1.4.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>wsdl4j</groupId>
+            <artifactId>wsdl4j</artifactId>
+            <version>1.6.3</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-websocket</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.freemarker</groupId>
+            <artifactId>freemarker</artifactId>
+            <version>2.3.28</version>
+        </dependency>
+        <dependency>
+            <groupId>com.itextpdf</groupId>
+            <artifactId>html2pdf</artifactId>
+            <version>2.1.3</version>
+        </dependency>
+        <dependency>
+            <groupId>com.itextpdf</groupId>
+            <artifactId>font-asian</artifactId>
+            <version>7.1.6</version>
+        </dependency>
+        <dependency>
+            <groupId>com.itextpdf</groupId>
+            <artifactId>barcodes</artifactId>
+            <version>7.1.6</version>
+        </dependency>
+        <dependency>
+            <groupId>com.itextpdf</groupId>
+            <artifactId>itextpdf</artifactId>
+            <version>5.5.12</version>
+        </dependency>
+        <dependency>
+            <groupId>com.itextpdf.tool</groupId>
+            <artifactId>xmlworker</artifactId>
+            <version>5.5.8</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.lowagie</groupId>
+            <artifactId>itext</artifactId>
+            <version>2.0.8</version>
+        </dependency>
+        <dependency>
+            <groupId>org.xhtmlrenderer</groupId>
+            <artifactId>core-renderer</artifactId>
+            <version>R8</version>
+        </dependency>
+        <dependency>
+            <groupId>fr.opensagres.xdocreport</groupId>
+            <artifactId>org.apache.poi.xwpf.converter.pdf</artifactId>
+            <version>1.0.6</version>
+        </dependency>
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>fastjson</artifactId>
+            <version>1.2.61</version>
+        </dependency>
+
+<!--        <dependency>-->
+<!--            <groupId>com.microsoft.sqlserver</groupId>-->
+<!--            <artifactId>mssql-jdbc</artifactId>-->
+<!--            <version>6.2.0.jre8</version>-->
+<!--            <scope>runtime</scope>-->
+<!--        </dependency>-->
+
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-jdbc</artifactId>
+            <version>5.3.4</version>
+        </dependency>
+
+        <dependency>
+            <groupId>javax.xml.bind</groupId>
+            <artifactId>jaxb-api</artifactId>
+            <version>2.3.1</version>
+        </dependency>
+
+        <!-- zxing生成二维码 -->
+        <dependency>
+            <groupId>com.google.zxing</groupId>
+            <artifactId>core</artifactId>
+            <version>3.3.3</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.google.zxing</groupId>
+            <artifactId>javase</artifactId>
+            <version>3.3.3</version>
+        </dependency>
+
+<!--        <dependency>-->
+<!--            <groupId>com.oracle</groupId>-->
+<!--            <artifactId>ojdbc14</artifactId>-->
+<!--            <version>10.2.0.4.0</version>-->
+<!--        </dependency>-->
+        <dependency>
+            <groupId>com.oracle</groupId>
+            <artifactId>ojdbc11</artifactId>
+            <version>11.2.0.1.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.oracle.database.nls</groupId>
+            <artifactId>orai18n</artifactId>
+            <version>21.5.0.0</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>2.4</version>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.dataformat</groupId>
+            <artifactId>jackson-dataformat-xml</artifactId>
+            <version>2.10.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.pdfbox</groupId>
+            <artifactId>pdfbox</artifactId>
+            <version>2.0.25</version> <!-- 使用适合你项目的版本 -->
+        </dependency>
+
+    </dependencies>
+    <build>
+        <finalName>${project.artifactId}</finalName>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <configuration>
+                    <mainClass>com.qmrb.system.SystemApplication</mainClass>
+                    <layout>ZIP</layout>
+                    <includes>
+                        <include>
+                            <groupId>nothing</groupId>
+                            <artifactId>nothing</artifactId>
+                        </include>
+                    </includes>
+                    <includeSystemScope>true</includeSystemScope>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <configuration>
+                	<includeSystemScope>true</includeSystemScope>
+            	</configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <configuration>
+                    <skip>true</skip>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

File diff suppressed because it is too large
+ 33933 - 0
sql/parking-server.sql


+ 21 - 0
src/main/java/com/qmrb/system/SystemApplication.java

@@ -0,0 +1,21 @@
+package com.qmrb.system;
+
+import com.fasterxml.jackson.core.StreamReadConstraints;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ImportResource;
+
+@SpringBootApplication
+//@ImportResource(locations = { "application-beans.xml" }) // 导入spring配置文件
+public class SystemApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(SystemApplication.class, args);
+    }
+    @Bean
+    Jackson2ObjectMapperBuilderCustomizer customStreamReadConstraints() {
+        return (builder) -> builder.postConfigurer((objectMapper) -> objectMapper.getFactory()
+                .setStreamReadConstraints(StreamReadConstraints.builder().maxNestingDepth(2000).maxStringLength(100_000_000).build()));
+    }
+}

+ 456 - 0
src/main/java/com/qmrb/system/adatper/AbstractPdfAdatper.java

@@ -0,0 +1,456 @@
+package com.qmrb.system.adatper;
+
+import cn.hutool.core.date.DateUtil;
+import com.alibaba.fastjson.JSONObject;
+import com.qmrb.system.common.result.Result;
+import com.qmrb.system.common.util.AesUtils;
+import com.qmrb.system.framework.security.CaConstants;
+import com.qmrb.system.pojo.entity.CaSignFile;
+import com.qmrb.system.pojo.entity.TplTemplate;
+import com.qmrb.system.pojo.form.TplCategoryForm;
+import com.qmrb.system.pojo.query.PatientInfo;
+import com.qmrb.system.service.*;
+import com.qmrb.system.utils.Base64Util;
+import com.qmrb.system.utils.FreeMarkerUtil;
+import com.qmrb.system.utils.HtmlToPdfConverter;
+import com.qmrb.system.utils.OkHttpUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.axis.client.Call;
+import org.apache.axis.client.Service;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import javax.xml.namespace.QName;
+import java.io.File;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.text.MessageFormat;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+
+/**
+ * pdf生成器抽象类
+ */
+@Slf4j
+public abstract class AbstractPdfAdatper implements PdfAdatper{
+
+
+    @Autowired
+    private ITplTemplateService templateService;
+
+    @Autowired
+    private IEmrEditorService emrEditorService;
+
+    @Autowired
+    private ITplCategoryService tplCategoryService;
+
+    /**
+     * 医护签溯源接口
+     * @param idCard
+     * @param srcPdfPath
+     * @param toPdfPath
+     * @param signX
+     * @param signY
+     * @return
+     */
+    private String addSignImageNew(String idCard,String srcPdfPath,String toPdfPath,String signX,String signY,PatientInfo patientInfo){
+        String image = "";
+        String extInfo = "{\"busSys\":\""+CaConstants.sys+"\"}";
+        String fileInfo = Base64Util.fileToBase64(srcPdfPath);
+        String sealInfo = "1,"+signX+","+signY+",123502004266006872,1";
+        String token = OkHttpUtils.getTokenNew();
+        String url = CaConstants.domain + "/patientsign/pdfSign/sealBatch?access_token="+token;
+//        ?isAddWatermark=0&isAddSeal=1&sealInfo={0}&fileInfo={1}&extInfo={2}&access_token={3}",
+//        sealInfo, fileInfo,  Base64.getEncoder().encodeToString(extInfo.getBytes()),token
+        JSONObject param  = new JSONObject();
+        param.put("isAddWatermark","1");
+        param.put("isAddSeal","1");
+        param.put("sealInfo",sealInfo);
+        param.put("fileInfo",fileInfo);
+        param.put("extInfo", Base64.getEncoder().encodeToString(extInfo.getBytes()));
+        param.put("access_token",token);
+        String result = OkHttpUtils.sendPost(url,param.toJSONString());
+        log.info(idCard+"#sealInfo:"+sealInfo);
+//        log.info(param.getInnerMap().toString());
+//        String result = cn.hutool.http.HttpUtil.post(url,param.getInnerMap());
+//        log.info("upload result>>>>>>>>>>>>>>>>>>>>>>:{}", result);
+        JSONObject obj = JSONObject.parseObject(result);
+        if (!"1000".equals(obj.getString("code"))) {
+            log.error("生成签章失败,"+obj.get("msg"));
+        }else{
+            log.info(idCard+"#添加盖章文件成功");
+            JSONObject data = (JSONObject) obj.get("data");
+            image = (String)data.get("signFile");
+            OkHttpUtils.generateBase64StringToFile(image,toPdfPath);
+            log.info(idCard+"#"+patientInfo.getFullName()+"#"+patientInfo.getTplId()+"#"+patientInfo.getDoctorDeptCode()+"#"+patientInfo.getPatientType()+"#"+toPdfPath+"#保存盖章文件成功");
+        }
+
+        return image;
+    }
+    /**
+     * 子类实现
+     * @return
+     */
+    @Override
+    public String getBasePath(){
+        return null;
+    }
+
+    /**
+     * 保存签名文件
+     * @param fileId
+     * @param patientInfo
+     * @param seiralNo
+     * @param status
+     * @param pdfPath
+     */
+    @Override
+    public CaSignFile saveFile(String fileId, PatientInfo patientInfo, String seiralNo, String status, String pdfPath){
+        return null;
+    }
+
+    public void deleteFileTmp(PatientInfo patientInfo){
+        return;
+    }
+
+    /**
+     * 同步数据至电子病历系统
+     * @param caSignFile
+     */
+    private void uploadToEmr(CaSignFile caSignFile,String base64FileStr){
+        String url =CaConstants.htEmrServer+"/Service.asmx?wsdl";
+        /*String xml = "<soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:web=\"http://tempuri.org//\">\n" +
+                "   <soapenv:Header/>\n" +
+                "   <soapenv:Body>\n" +
+                "      <web:INSERTEMRHUANZHEQIANMING>\n" +
+                "         <patient_id>"+caSignFile.getInpatientNo()+"</patient_id>\n" +
+                "         <visit_id>"+caSignFile.getInpatientTimes()+"</visit_id>\n" +
+                "         <QMSJ>"+base64FileStr+"</QMSJ>\n" +
+                "         <start_date>"+DateUtil.format(caSignFile.getCreateTime(),"yyyy-MM-dd HH:mm:ss")+"</start_date>\n" +
+                "         <sign_date>"+DateUtil.format(caSignFile.getSignDate(),"yyyy-MM-dd HH:mm:ss")+"</sign_date>\n" +
+                "         <id_no>"+caSignFile.getIdentityCard()+"</id_no>\n" +
+                "         <file_no>"+caSignFile.getFileId()+"</file_no>\n" +
+                "      </web:INSERTEMRHUANZHEQIANMING>\n" +
+                "   </soapenv:Body>\n" +
+                "</soapenv:Envelope>";
+        try {
+            String result = WebServiceUtil.httpClick(url, "INSERTEMRHUANZHEQIANMING", xml);
+            log.info("同步emr結果"+result);
+        } catch (Exception e) {
+            log.error(e.getMessage(),e);
+        }*/
+        try{
+            // webService链接地址
+//            String url = "http://192.1.33.126:81/Service.asmx?wsdl";
+            // server域名地址,为了统一规范,一般都是这个域名
+            String soapaction = "http://tempuri.org/";
+            // 方法名
+            String methodName = "INSERTEMRHUANZHEQIANMING";
+            // 用户提供测试的两个参数
+            Service service = new Service();
+            Call call = (Call) service.createCall();
+            call.setTargetEndpointAddress(url);
+            // 设置要调用哪个方法
+            call.setOperationName(new QName(soapaction,methodName));
+            // 设置要传递的参数名
+            call.addParameter(new QName(soapaction,"patient_id"),org.apache.axis.encoding.XMLType.XSD_STRING,
+                    javax.xml.rpc.ParameterMode.IN);
+            call.addParameter(new QName(soapaction,"visit_id"),org.apache.axis.encoding.XMLType.XSD_STRING,
+                    javax.xml.rpc.ParameterMode.IN);
+            call.addParameter(new QName(soapaction,"QMSJ"),org.apache.axis.encoding.XMLType.XSD_STRING,
+                    javax.xml.rpc.ParameterMode.IN);
+            call.addParameter(new QName(soapaction,"start_date"),org.apache.axis.encoding.XMLType.XSD_STRING,
+                    javax.xml.rpc.ParameterMode.IN);
+            call.addParameter(new QName(soapaction,"sign_date"),org.apache.axis.encoding.XMLType.XSD_STRING,
+                    javax.xml.rpc.ParameterMode.IN);
+            call.addParameter(new QName(soapaction,"id_no"),org.apache.axis.encoding.XMLType.XSD_STRING,
+                    javax.xml.rpc.ParameterMode.IN);
+            call.addParameter(new QName(soapaction,"file_no"),org.apache.axis.encoding.XMLType.XSD_STRING,
+                    javax.xml.rpc.ParameterMode.IN);
+            call.addParameter(new QName(soapaction,"system"),org.apache.axis.encoding.XMLType.XSD_STRING,
+                    javax.xml.rpc.ParameterMode.IN);
+            call.addParameter(new QName(soapaction,"name"),org.apache.axis.encoding.XMLType.XSD_STRING,
+                    javax.xml.rpc.ParameterMode.IN);
+            call.addParameter(new QName(soapaction,"file_name"),org.apache.axis.encoding.XMLType.XSD_STRING,
+                    javax.xml.rpc.ParameterMode.IN);
+            call.addParameter(new QName(soapaction,"operationflow"),org.apache.axis.encoding.XMLType.XSD_STRING,
+                    javax.xml.rpc.ParameterMode.IN);
+            call.addParameter(new QName(soapaction,"file_flag"),org.apache.axis.encoding.XMLType.XSD_STRING,
+                    javax.xml.rpc.ParameterMode.IN);
+            // 提供标准类型
+            call.setReturnType(org.apache.axis.encoding.XMLType.XSD_STRING);
+            call.setUseSOAPAction(true);
+            call.setSOAPActionURI(soapaction + methodName);
+            String file_flag = "2";//已签
+            if("1".equals(caSignFile.getStatus())){
+                file_flag = "";//空和1都是已签
+            }else{
+                file_flag = "2";//作废
+            }
+            // 调用方法并传递参数
+            String ref = (String)call.invoke(new Object[]{caSignFile.getInpatientNo(),caSignFile.getInpatientTimes(),base64FileStr, DateUtil.format(caSignFile.getCreateTime(),"yyyy-MM-dd HH:mm:ss"),
+                    DateUtil.format(caSignFile.getSignDate(),"yyyy-MM-dd HH:mm:ss"),
+                    caSignFile.getIdentityCard(),caSignFile.getFileId(),CaConstants.sys,caSignFile.getFullName(),caSignFile.getFileName(),caSignFile.getOperationflow(),file_flag});
+            log.info(caSignFile.getIdentityCard()+"#"+ref);
+        }catch (Exception e){
+            log.error(e.getMessage(),e);
+        }
+    }
+    @Override
+    public Result makePdf(PatientInfo patientInfo) {
+        if("zy".equals(patientInfo.getPatientType()) && patientInfo.getTplId()!=null
+                && (patientInfo.getTplId().longValue()==836||patientInfo.getTplId().longValue()==299)){
+            log.info("住院号:"+patientInfo.getInpatientNo()+",住院次数:"+patientInfo.getInpatientTimes());
+        }
+        log.info(patientInfo.getInpatientTimes());
+        //拼接回调地址
+        String signType = "0";//待签名
+        String deviceType = "1";//手写板
+        String callBackUrl = CaConstants.notifyUrl + "/api/v1/patientSign/uploadCallBack";
+        String patientInfoJson = "{'tplName':'知情同意书'}";
+        PatientInfo patientInfox = JSONObject.parseObject(patientInfoJson,PatientInfo.class);
+        String tplName = "";
+        TplTemplate tplTemplate = new TplTemplate();
+        if(patientInfo.getTplId() !=0){
+            tplTemplate = templateService.getById(patientInfo.getTplId());
+            tplName = tplTemplate.getTemplateName();
+        }
+        //1107 嘉禾电子病历系统手术名称逻辑修改 start
+        if(StringUtils.isNotBlank(patientInfo.getOperationflow())){
+            try{
+                //根据流水号获取手术名称,作为手术名称+tplName当文件名
+                List<Map> operationInfoList = emrEditorService.getOperationInfo(patientInfo.getInpatientNo(), patientInfo.getInpatientTimes(),"");
+                if(operationInfoList!=null && operationInfoList.size()>0){
+                    Map result = operationInfoList.get(0);
+                    tplName = (String)result.get("OPERATION_NAME")+tplName;
+                }
+            }catch (Exception e){
+                log.error(e.getMessage(),e);
+            }
+
+        }
+        //1107 嘉禾电子病历系统手术名称逻辑修改 end
+        patientInfo.setTplName(tplName);
+        String seiralNo = patientInfo.getSeiralNo();
+        if(StringUtils.isEmpty(seiralNo)){
+            seiralNo = UUID.randomUUID().toString().replaceAll("-","");
+        }
+//        Long tplId = patientInfo.getTplId();
+        if(StringUtils.isBlank(patientInfo.getIdentityCard())){
+            return Result.failed("参数非法");
+        }else {
+            //需要对加密的身份证解密
+            try {
+                //241127 如果身份证号传的是门诊的固定值,并且门诊号不为空,身份证号设置为门诊号
+//                actOrderNo
+                patientInfo.setIdentityCard(AesUtils.decrypt(patientInfo.getIdentityCard(),CaConstants.serverSecret));
+                /*//判断解密后的值
+                if(StringUtils.isNotBlank(patientInfo.getActOrderNo()) && "IDCARD".equals(patientInfo.getIdentityCard())){//his 系统传
+                    patientInfo.setIdentityCard(patientInfo.getActOrderNo());
+                }*/
+
+            } catch (Exception e) {
+                log.error("解密失败"+patientInfo.getIdentityCard(),e);
+                return Result.failed("解密失败");
+            }
+        }
+        if(StringUtils.isBlank(patientInfo.getDoctorIdentityCard())){
+            return Result.failed("参数非法");
+        }else {
+            //需要对加密的身份证解密
+            try {
+                patientInfo.setDoctorIdentityCard(AesUtils.decrypt(patientInfo.getDoctorIdentityCard(),CaConstants.serverSecret));
+            } catch (Exception e) {
+                log.error("解密失败"+patientInfo.getDoctorIdentityCard(),e);
+                return Result.failed("解密失败");
+            }
+        }
+        //根据模板id获取模板名称
+
+        String bizNo = seiralNo;
+        if(StringUtils.isNotBlank(patientInfo.getInpatientNo())){
+            bizNo = seiralNo+"|"+patientInfo.getInpatientNo();
+            try {
+                bizNo = URLEncoder.encode(bizNo,"UTF-8");
+            } catch (UnsupportedEncodingException e) {
+                log.error(e.getMessage());
+            }
+        }
+
+
+        String fileName = seiralNo + ".pdf";
+        //模板路径
+        String tplPath = CaConstants.basePath+"/tpl";
+        String outPdfPath = CaConstants.basePath+"/temppdf";
+        File pdfFile = new File(outPdfPath);
+        if(!pdfFile.exists()){
+            pdfFile.mkdirs();
+        }
+        String outPdfFilePath = outPdfPath+"/"+fileName;//输出路径
+
+        String tplFileName = "commonPdfTpl.html";
+        //代表耳鼻喉科报告
+        if(StringUtils.equals("0",patientInfo.getCaFlag())){
+            tplFileName = "capPdfTpl.html";
+        }
+        String tplFileNameX = seiralNo+tplFileName+".ftl";//替换的后的PDF模板,解决多个模板同时上传覆盖的为你
+        //替换模板内容
+        String outFileHtmlPath = CaConstants.basePath+"/temptpl";//内容较多配置到文件比较的文件夹下
+        FreeMarkerUtil.process(tplPath,tplFileName, patientInfo.getPrintHtml(), outFileHtmlPath,seiralNo);
+        //生成pdf
+        HtmlToPdfConverter.convertHtml2Pdf(tplFileNameX,outPdfFilePath,seiralNo);
+        //TODO 后续改为配置,如何判断医疗文书
+        TplCategoryForm categoryForm = tplCategoryService.getFormData(tplTemplate.getCategoryId());
+        //非医疗文书\非评估量表\非病历
+        if(tplTemplate.getCategoryId().intValue() !=CaConstants.WENSHU_ID
+                && (!categoryForm.getCategoryName().contains("医疗文书"))
+                && (!categoryForm.getCategoryName().contains("评估量表")) && (!categoryForm.getCategoryName().contains("评分量表"))
+                && (tplTemplate.getCategoryId().intValue() !=CaConstants.BL_ID)) {//知情同意书,医疗文书的类型为39
+            //构建url
+            String token = OkHttpUtils.getTokenNew();
+            String signUrl = CaConstants.domain+"/patientsign/signpage/unsignlist/"+patientInfo.getIdentityCard()+"?access_token="+token;
+            log.info(signUrl);
+            String url = MessageFormat.format(
+                    CaConstants.domain + "/patientsign/v1/receiveFile?identityCard={0}&fullName={1}&deviceType={2}&signType={3}&phoneNum={4}&seiralNo={5}&access_token={6}&doctorIdentityCard={7}&doctorWorkno={8}&notifyUrl={9}&fileType={10}&signPositionType=0",
+                    patientInfo.getIdentityCard(), patientInfo.getFullName(), deviceType, signType,
+                    patientInfo.getPhoneNum(), bizNo, token, patientInfo.getDoctorIdentityCard(),
+                    patientInfo.getDoctorWorkno(), callBackUrl, patientInfo.getTplName());
+            //add by chenqiwang 04-233
+            String extInfo = "{\"signType\": \"\"}";
+            if(tplName.contains("双签")){
+//            url += "&extInfo="+URLEncoder.encode(extInfo);
+                extInfo = "{\"signType\": \"2\"}";
+            }
+            String result = OkHttpUtils.uploadPdf(url, fileName, seiralNo, outPdfFilePath,extInfo);
+            log.info("upload result>>>>>>>>>>>>>>>>>>>>>>:{}", result);
+            JSONObject obj = JSONObject.parseObject(result);
+//        String qr_url = MessageFormat.format(
+//                CaConstants.domain + "/patientsign/signpage/fileHandSign" +
+//                        "/{0}?access_token={1}&tab=&name=&sex=&age=&idcard=&tel=&address=&gx=",
+//                obj.get("id"), token);
+            if (obj.getIntValue("result") == 1) {
+//            obj.put("qr_url",qr_url);
+                //默认待签,也保存pdf
+                String curDateStr = DateUtil.format(new Date(),"yyyMMdd");
+                String pdfPath  = CaConstants.basePath+"/pdf/"+curDateStr+"/"+fileName;
+                File file = new File(pdfPath);
+                if (!file.getParentFile().exists()) {
+                    file.getParentFile().mkdirs();
+                }
+                String fileInfo = Base64Util.fileToBase64(outPdfFilePath);//delete tmppdf file @TODO
+                OkHttpUtils.generateBase64StringToFile(fileInfo,pdfPath);
+                String savePdfPath = "/"+curDateStr+"/"+fileName;
+                CaSignFile caSignFile = saveFile(obj.getString("id"), patientInfo, seiralNo,"0",savePdfPath);
+                //add by chenqiwang 0426
+                deleteFileTmp(patientInfo);
+                File tempPdf = new File(outPdfFilePath);
+                try{
+                    tempPdf.delete();
+                    log.error("删除临时文件成功"+outPdfFilePath);
+                }catch (Exception e){
+                    log.error("删除临时文件失败",e);
+                }
+                File tempPdfTpl = new File(outFileHtmlPath+"/"+tplFileNameX);
+                try{
+                    tempPdfTpl.delete();
+                    log.info("删除临时文件模版成功"+outFileHtmlPath+"/"+tplFileNameX);
+                }catch (Exception e){
+                    log.error("删除临时文件模版失败",e);
+                }
+
+                //待签同步
+                /*new Thread(new Runnable() {
+                    @Override
+                    public void run() {
+                        try {
+                            if ("zy".equals(caSignFile.getPatientType())) {
+                                log.info(caSignFile.getIdentityCard() + "同步emr start....." + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
+                                uploadToEmr(caSignFile, Base64Util.fileToBase64(outPdfFilePath));
+                                log.info(caSignFile.getIdentityCard() + "同步emr end....." + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
+                            }
+                        } catch (Exception e) {
+                            log.error(caSignFile.getIdentityCard() + "同步EMR系统失败", e.getMessage());
+                        }
+                    }
+                }).start();*/
+                //返回待签的文件url
+                signUrl = CaConstants.qrCodeDomain + "/goto.html?cardId=" + patientInfo.getIdentityCard() + "&access_token=" + token;
+                obj.put("signUrl",signUrl);
+                obj.put("localFileId",caSignFile.getId());//自动上传接口返回
+                return Result.success(obj);
+            } else {
+                return Result.failed(obj.getString("msg"));
+            }
+        }else{
+            //生成
+            JSONObject obj = new JSONObject();
+            String result = "0";
+            //outPdfFilePath 里的pdf加签章
+            String curDateStr = DateUtil.format(new Date(),"yyyMMdd");
+            String pdfPath  = CaConstants.basePath+"/pdf/"+curDateStr+"/"+fileName;
+            File wenshuFile = new File(CaConstants.basePath+"/pdf/");
+            if(!wenshuFile.exists()){
+                wenshuFile.mkdirs();
+            }
+            File file = new File(pdfPath);
+            if (!file.getParentFile().exists()) {
+                file.getParentFile().mkdirs();
+            }
+            if (file.isFile()) {
+                file.delete();
+            }
+            String savePdfPath = "/"+curDateStr+"/"+fileName;
+            String fileId = "";
+            if(StringUtils.equals("0",patientInfo.getCaFlag())){
+                //不用ca盖章
+                CaSignFile caSignFile = saveFile(seiralNo, patientInfo, seiralNo, "1", savePdfPath);//默认已签
+                fileId = caSignFile.getFileId();
+                String fileInfo = Base64Util.fileToBase64(outPdfFilePath);
+                OkHttpUtils.generateBase64StringToFile(fileInfo,pdfPath);
+            }else {
+                String image = addSignImageNew(patientInfo.getIdentityCard(), outPdfFilePath, pdfPath, patientInfo.getSignX(), patientInfo.getSignY(),patientInfo);
+                CaSignFile caSignFile = saveFile(seiralNo, patientInfo, seiralNo, "1", savePdfPath);//默认已签
+                fileId = caSignFile.getFileId();
+                new Thread(new Runnable() {
+                    @Override
+                    public void run() {
+                        try {
+                            if ("zy".equals(caSignFile.getPatientType())) {
+                                log.info(caSignFile.getIdentityCard() + "同步emr start....." + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
+                                uploadToEmr(caSignFile, image);
+                                log.info(caSignFile.getIdentityCard() + "同步emr end....." + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
+                            }
+                        } catch (Exception e) {
+                            log.error(caSignFile.getIdentityCard() + "同步EMR系统失败", e.getMessage());
+                        }
+                    }
+                }).start();
+            }
+            //报告已生成,删除临时文件0816
+            File tempPdf = new File(outPdfFilePath);
+            try{
+                tempPdf.delete();
+                log.info("删除临时文件成功"+outPdfFilePath);
+            }catch (Exception e){
+                log.error("删除临时文件失败",e);
+            }
+            File tempPdfTpl = new File(outFileHtmlPath+"/"+tplFileNameX);
+            try{
+                tempPdfTpl.delete();
+                log.info("删除临时文件模版成功"+outFileHtmlPath+"/"+tplFileNameX);
+            }catch (Exception e){
+                log.error("删除临时文件模版失败",e);
+            }
+            obj.put("pdfPath",savePdfPath);
+            obj.put("fileId",fileId);
+            result = "2";//代表签章
+            obj.put("result",result);
+            //add by chenqiwang 0426
+            deleteFileTmp(patientInfo);
+            return Result.success(obj);
+        }
+    }
+
+
+}

+ 103 - 0
src/main/java/com/qmrb/system/adatper/CaSignAdatper.java

@@ -0,0 +1,103 @@
+package com.qmrb.system.adatper;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.qmrb.system.common.result.Result;
+import com.qmrb.system.framework.security.CaConstants;
+import com.qmrb.system.pojo.entity.CaSignFile;
+import com.qmrb.system.pojo.query.PatientInfo;
+import com.qmrb.system.service.CaSignFileService;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+/**
+ * CA签名同意书适配器
+ */
+@Slf4j
+@Component
+public class CaSignAdatper extends AbstractPdfAdatper implements PdfAdatper{
+
+    @Autowired
+    private CaSignFileService caSignFileService;
+
+    @Override
+    public Result makePdf(PatientInfo patientInfo) {
+        return super.makePdf(patientInfo);
+    }
+
+    /**
+     * pdf上传路径
+     * @return
+     */
+    @Override
+    public String getBasePath(){
+        return CaConstants.basePath;
+    }
+    /**
+     * 删除临时文件
+     * @param patientInfo
+     */
+    @Override
+    public   void deleteFileTmp(PatientInfo patientInfo){
+        try {
+            if (StringUtils.isNotBlank(patientInfo.getFileId())) {
+                CaSignFile caSignFile = caSignFileService.getOne(new LambdaQueryWrapper<CaSignFile>()
+                        .eq(CaSignFile::getFileId, patientInfo.getFileId()));
+                if (caSignFile != null && caSignFile.getStatus().equals("2")) {
+                    caSignFileService.removeById(caSignFile.getId());
+                }
+            }
+        }catch (Exception e){
+            log.error("删除待提交内容失败#"+patientInfo.getIdentityCard(),e);
+        }
+    }
+    /**
+     * 保存签名文件
+     * @param fileId
+     * @param patientInfo
+     * @param seiralNo
+     * @param status
+     * @param pdfPath
+     */
+    @Override
+    public CaSignFile saveFile(String fileId, PatientInfo patientInfo, String seiralNo, String status, String pdfPath){
+        CaSignFile caSignFile = new CaSignFile();
+        if(caSignFile == null){
+            caSignFile =  new CaSignFile();
+        }
+        caSignFile.setFileId(fileId);
+        caSignFile.setIdentityCard(patientInfo.getIdentityCard());
+        caSignFile.setPhoneNum(patientInfo.getPhoneNum());
+        caSignFile.setFileName(patientInfo.getTplName());
+        caSignFile.setSeiralNo(seiralNo);
+        caSignFile.setStatus(status);//待签
+        caSignFile.setPdfPath(pdfPath);
+        caSignFile.setDeptCode(patientInfo.getDoctorDeptCode());
+        caSignFile.setFullName(patientInfo.getFullName());
+        caSignFile.setTplId(patientInfo.getTplId());
+        //add by chenqiwang 03-28 emr
+        caSignFile.setPatientType(patientInfo.getPatientType());
+        caSignFile.setInpatientNo(patientInfo.getInpatientNo());
+        if(StringUtils.isNotBlank(patientInfo.getInpatientTimes())){
+            caSignFile.setInpatientTimes(patientInfo.getInpatientTimes());
+        }
+        caSignFile.setOperationflow(patientInfo.getOperationflow());
+        caSignFile.setBedNo(patientInfo.getBedNo());
+        //add by chenqiwang 0426 暂存需求
+        if(StringUtils.isNotBlank(patientInfo.getContent())){
+            caSignFile.setContent(patientInfo.getContent());
+            caSignFile.setConfig(patientInfo.getConfig());
+        }
+        //0517
+        if(StringUtils.isNotBlank(patientInfo.getDoctorWorkno())){
+            caSignFile.setDoctorWorkno(patientInfo.getDoctorWorkno());
+        }
+        //0618报告文件
+        caSignFile.setFileType(patientInfo.getFileType());
+        caSignFile.setRtmsBizNo(patientInfo.getRtmsBizNo());
+        caSignFile.setActOrderNo(patientInfo.getActOrderNo());
+        caSignFileService.save(caSignFile);
+        return caSignFile;
+    }
+}

+ 111 - 0
src/main/java/com/qmrb/system/adatper/EmrAdatper.java

@@ -0,0 +1,111 @@
+package com.qmrb.system.adatper;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.qmrb.system.common.result.Result;
+import com.qmrb.system.framework.security.CaConstants;
+import com.qmrb.system.pojo.entity.EmrFile;
+import com.qmrb.system.pojo.entity.CaSignFile;
+import com.qmrb.system.pojo.query.PatientInfo;
+import com.qmrb.system.service.EmrFileService;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+/**
+ * 病历适配器
+ */
+@Component
+@Slf4j
+public class EmrAdatper extends AbstractPdfAdatper implements PdfAdatper{
+
+    @Autowired
+    private EmrFileService emrFileServiceS;
+//    @Override
+    public Result makePdf(PatientInfo patientInfo) {
+        return super.makePdf(patientInfo);
+    }
+
+    /**
+     * 病历上传路径
+     * @return
+     */
+    @Override
+    public String getBasePath(){
+        return CaConstants.pdfPath;
+    }
+
+    /**
+     * 删除临时文件
+     * @param patientInfo
+     */
+    @Override
+    public   void deleteFileTmp(PatientInfo patientInfo){
+        try {
+            if (StringUtils.isNotBlank(patientInfo.getFileId())) {
+                EmrFile caSignFile = emrFileServiceS.getOne(new LambdaQueryWrapper<EmrFile>()
+                        .eq(EmrFile::getFileId, patientInfo.getFileId()));
+                if (caSignFile != null && caSignFile.getStatus().equals("2")) {
+                    emrFileServiceS.removeById(caSignFile.getId());
+                }
+            }
+        }catch (Exception e){
+            log.error("删除待提交内容失败#"+patientInfo.getIdentityCard(),e);
+        }
+    }
+    /**
+     * 保存签名/病历文件
+     * @param fileId
+     * @param patientInfo
+     * @param seiralNo
+     * @param status
+     * @param pdfPath
+     */
+    @Override
+    public CaSignFile saveFile(String fileId, PatientInfo patientInfo, String seiralNo, String status, String pdfPath){
+        EmrFile caSignFile = new EmrFile();
+        if(caSignFile == null){
+            caSignFile =  new EmrFile();
+        }
+        caSignFile.setFileId(fileId);
+        caSignFile.setIdentityCard(patientInfo.getIdentityCard());
+        caSignFile.setPhoneNum(patientInfo.getPhoneNum());
+        caSignFile.setFileName(patientInfo.getTplName());
+        caSignFile.setSeiralNo(seiralNo);
+        caSignFile.setStatus(status);//待签
+        caSignFile.setPdfPath(pdfPath);
+        caSignFile.setDeptCode(patientInfo.getDoctorDeptCode());
+        caSignFile.setFullName(patientInfo.getFullName());
+        caSignFile.setTplId(patientInfo.getTplId());
+        //add by chenqiwang 03-28 emr
+        caSignFile.setPatientType(patientInfo.getPatientType());
+        caSignFile.setInpatientNo(patientInfo.getInpatientNo());
+        if(StringUtils.isNotBlank(patientInfo.getInpatientTimes())){
+            caSignFile.setInpatientTimes(patientInfo.getInpatientTimes());
+        }
+        caSignFile.setOperationflow(patientInfo.getOperationflow());
+        caSignFile.setBedNo(patientInfo.getBedNo());
+        //add by chenqiwang 0426 暂存需求
+        if(StringUtils.isNotBlank(patientInfo.getContent())){
+            caSignFile.setContent(patientInfo.getContent());
+            caSignFile.setConfig(patientInfo.getConfig());
+        }
+        //0517
+        if(StringUtils.isNotBlank(patientInfo.getDoctorWorkno())){
+            caSignFile.setDoctorWorkno(patientInfo.getDoctorWorkno());
+        }
+        //0618报告文件
+        caSignFile.setFileType(patientInfo.getFileType());
+        caSignFile.setRtmsBizNo(patientInfo.getRtmsBizNo());
+        caSignFile.setActOrderNo(patientInfo.getActOrderNo());
+        emrFileServiceS.save(caSignFile);
+        CaSignFile result = new CaSignFile();
+        try {
+            BeanUtils.copyProperties(caSignFile,result);
+        }catch (Exception e){
+            log.error(e.getMessage(),e);
+        }
+        return result;
+    }
+}

+ 27 - 0
src/main/java/com/qmrb/system/adatper/PdfAdatper.java

@@ -0,0 +1,27 @@
+package com.qmrb.system.adatper;
+
+import com.qmrb.system.common.result.Result;
+import com.qmrb.system.pojo.entity.CaSignFile;
+import com.qmrb.system.pojo.query.PatientInfo;
+
+/**
+ * pdf适配器
+ */
+public interface PdfAdatper {
+    /**
+     * 生成pdf
+     * @param patientInfo
+     * @return
+     */
+    public Result makePdf(PatientInfo patientInfo);
+
+    /**
+     * pdf上传路径
+     * @return
+     */
+    public String getBasePath();
+
+    public CaSignFile saveFile(String fileId, PatientInfo patientInfo, String seiralNo, String status, String pdfPath);
+
+    public   void deleteFileTmp(PatientInfo patientInfo);
+}

+ 39 - 0
src/main/java/com/qmrb/system/common/base/BaseEntity.java

@@ -0,0 +1,39 @@
+package com.qmrb.system.common.base;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+@Data
+public class BaseEntity implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @TableField(fill = FieldFill.INSERT)
+    @JsonInclude(value = JsonInclude.Include.NON_NULL)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime createTime;
+
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    @JsonInclude(value = JsonInclude.Include.NON_NULL)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime updateTime;
+    
+    
+    /**
+     * 新增用户
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private Long createBy;
+
+    /**
+     * 修改用户
+     */
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private Long updateBy;
+
+}

+ 22 - 0
src/main/java/com/qmrb/system/common/base/BasePageQuery.java

@@ -0,0 +1,22 @@
+package com.qmrb.system.common.base;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+/**
+ * 基础分页请求对象
+ *
+ * @author haoxr
+ * @date 2021/2/28
+ */
+@Data
+@Schema
+public class BasePageQuery {
+
+    @Schema(description = "页码", example = "1")
+    private int pageNum = 1;
+
+    @Schema(description = "每页记录数", example = "10")
+    private int pageSize = 10;
+}

+ 19 - 0
src/main/java/com/qmrb/system/common/base/BaseVO.java

@@ -0,0 +1,19 @@
+package com.qmrb.system.common.base;
+
+import lombok.Data;
+import lombok.ToString;
+
+import java.io.Serializable;
+
+/**
+ * 视图对象基类
+ *
+ * @author haoxr
+ * @date 2022/10/22
+ */
+@Data
+@ToString
+public class BaseVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+}

+ 91 - 0
src/main/java/com/qmrb/system/common/base/IBaseEnum.java

@@ -0,0 +1,91 @@
+package com.qmrb.system.common.base;
+
+
+import cn.hutool.core.util.ObjectUtil;
+
+import java.util.EnumSet;
+import java.util.Objects;
+
+/**
+ * 枚举通用接口
+ *
+ * @author haoxr
+ * @date 2022/3/27 12:06
+ */
+public interface IBaseEnum<T> {
+
+    T getValue();
+
+    String getLabel();
+
+    /**
+     * 根据值获取枚举
+     *
+     * @param value
+     * @param clazz
+     * @param <E>   枚举
+     * @return
+     */
+    @SuppressWarnings("rawtypes")
+	static <E extends Enum<E> & IBaseEnum> E getEnumByValue(Object value, Class<E> clazz) {
+        Objects.requireNonNull(value);
+        EnumSet<E> allEnums = EnumSet.allOf(clazz); // 获取类型下的所有枚举
+        E matchEnum = allEnums.stream()
+                .filter(e -> ObjectUtil.equal(e.getValue(), value))
+                .findFirst()
+                .orElse(null);
+        return matchEnum;
+    }
+
+    /**
+     * 根据文本标签获取值
+     *
+     * @param value
+     * @param clazz
+     * @param <E>
+     * @return
+     */
+    @SuppressWarnings("rawtypes")
+	static <E extends Enum<E> & IBaseEnum> String getLabelByValue(Object value, Class<E> clazz) {
+        Objects.requireNonNull(value);
+        EnumSet<E> allEnums = EnumSet.allOf(clazz); // 获取类型下的所有枚举
+        E matchEnum = allEnums.stream()
+                .filter(e -> ObjectUtil.equal(e.getValue(), value))
+                .findFirst()
+                .orElse(null);
+
+        String label = null;
+        if (matchEnum != null) {
+            label = matchEnum.getLabel();
+        }
+        return label;
+    }
+
+
+    /**
+     * 根据文本标签获取值
+     *
+     * @param label
+     * @param clazz
+     * @param <E>
+     * @return
+     */
+    @SuppressWarnings("rawtypes")
+	static <E extends Enum<E> & IBaseEnum> Object getValueByLabel(String label, Class<E> clazz) {
+        Objects.requireNonNull(label);
+        EnumSet<E> allEnums = EnumSet.allOf(clazz); // 获取类型下的所有枚举
+        String finalLabel = label;
+        E matchEnum = allEnums.stream()
+                .filter(e -> ObjectUtil.equal(e.getLabel(), finalLabel))
+                .findFirst()
+                .orElse(null);
+
+        Object value = null;
+        if (matchEnum != null) {
+            value = matchEnum.getValue();
+        }
+        return value;
+    }
+
+
+}

+ 681 - 0
src/main/java/com/qmrb/system/common/constant/DeptNameMapping.java

@@ -0,0 +1,681 @@
+package com.qmrb.system.common.constant;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class DeptNameMapping {
+    public static Map<String,String> map = new HashMap<>();
+    static {
+        map.put("1000001","综合内科");
+        map.put("1000002","心理精神门诊");
+        map.put("1000003","多学科门诊");
+        map.put("1000005","全科教学门诊");
+        map.put("1010000","普通内科");
+        map.put("1010100","消化内科");
+        map.put("1010101","胃镜室");
+        map.put("1010102","消化内科一病区");
+        map.put("1010103","消化内科二病区");
+        map.put("1010200","呼吸与危重症医学科");
+        map.put("1010201","肺功能室");
+        map.put("1010202","呼吸与危重症医学科二病区");
+        map.put("1010203","呼吸与危重症医学科一病区");
+        map.put("1010204","呼吸与危重症医学科重症室");
+        map.put("1010300","肾内科");
+        map.put("1010301","透析室");
+        map.put("1010310","肾内科病区");
+        map.put("1010400","风湿免疫科");
+        map.put("1010401","风湿免疫科病区");
+        map.put("1010500","耐药幽门螺旋杆菌感染门诊");
+        map.put("1010600","难治性痛风门诊");
+        map.put("1010700","阳康综合康复门诊");
+        map.put("1020000","外科");
+        map.put("1020100","普通外科");
+        map.put("1020101","普通外科(不可用)");
+        map.put("1020102","普通外科二病区");
+        map.put("1020103","普外儿外病区");
+        map.put("1020200","肝胆外科");
+        map.put("1020201","肝胆外科一病区");
+        map.put("1020202","肝胆外科二病区");
+        map.put("1020231","体检中心");
+        map.put("1020300","泌尿外科");
+        map.put("1020310","泌尿外科病区");
+        map.put("1020400","胸外科");
+        map.put("1020410","胸外科病区");
+        map.put("1020500","(停用)血管外科");
+        map.put("1020600","肿瘤微创与介入治疗科");
+        map.put("1020601","肿瘤微创与介入治疗科病区");
+        map.put("1020700","胰腺癌精准诊疗门诊");
+        map.put("1020800","肺小结节精准微创门诊");
+        map.put("1030000","骨科");
+        map.put("1030100","骨科一病区");
+        map.put("1030200","骨科二病区");
+        map.put("1030300","骨科三病区");
+        map.put("1031000","创伤骨科");
+        map.put("1031001","创伤骨科病区");
+        map.put("1031100","拇外翻微创门诊");
+        map.put("1032000","关节外科");
+        map.put("1032001","关节外科病区");
+        map.put("1032100","顽固性肩痛门诊");
+        map.put("1033000","脊柱外科");
+        map.put("1033001","脊柱外科病区");
+        map.put("1040000","妇产科");
+        map.put("1040002","产科一病区");
+        map.put("1040003","产科二病区");
+        map.put("1040005","妇产科(产)");
+        map.put("1040010","妇科一病区");
+        map.put("1040011","妇科二病区");
+        map.put("1040012","妇产科VIP");
+        map.put("1040013","家庭化产房");
+        map.put("1040014","围产营养咨询门诊");
+        map.put("1040015","助产士门诊");
+        map.put("1040105","产前咨询门诊");
+        map.put("1040205","遗传咨询门诊");
+        map.put("1040300","生殖健康与不孕症");
+        map.put("1050000","儿科");
+        map.put("1050001","儿科一病区");
+        map.put("1050002","儿科二病区");
+        map.put("1060000","老年医学科");
+        map.put("1060001","老年科一病区");
+        map.put("1060002","老年科二病区");
+        map.put("1060003","康复病区");
+        map.put("1060004","老年医学病区");
+        map.put("1070000","老年病研究所");
+        map.put("1080000","中医科");
+        map.put("1080100","推拿室");
+        map.put("1080200","针灸室");
+        map.put("1080300","中医科病区");
+        map.put("1080400","中医科(大内科一)");
+        map.put("1080410","中医科(大内科一)病区");
+        map.put("1081000","中医科(老年专病)");
+        map.put("1090000","眼科");
+        map.put("1090001","眼科病区");
+        map.put("1090002","口腔科病区(不可用)");
+        map.put("1100000","耳鼻咽喉头颈外科");
+        map.put("1100001","耳鼻喉科病区(作废)");
+        map.put("1100100","耳鼻咽喉头颈外科病区");
+        map.put("1100200","嗓音科(耳鼻咽喉头颈外科二病区)");
+        map.put("1100300","难治性眩晕门诊");
+        map.put("1108000","中医科病区(不可用)");
+        map.put("1110000","口腔科");
+        map.put("1110001","口腔颌面外科");
+        map.put("1110002","口腔正畸科");
+        map.put("1110003","口腔修复和种植科");
+        map.put("1110004","口腔内科(牙体牙髓)");
+        map.put("1110100","口腔科病区");
+        map.put("1120000","保健门诊");
+        map.put("1120001","保健病房一病区");
+        map.put("1120002","保健病房二病区");
+        map.put("1120003","保健病房三病区");
+        map.put("1120004","干部保健科四病区");
+        map.put("1120005","干部保健科五病区");
+        map.put("1120006","全科医学科病区");
+        map.put("1130000","麻醉手术室(旧编码)");
+        map.put("1130100","手术麻醉科");
+        map.put("1130110","复苏室");
+        map.put("1130200","急诊手术室");
+        map.put("1130300","心脏手术室");
+        map.put("1130400","疼痛门诊");
+        map.put("1130500","日间手术");
+        map.put("1130600","麻醉门诊");
+        map.put("1140000","神经内科");
+        map.put("1140001","神经内科一病区");
+        map.put("1140002","脑电室");
+        map.put("1140003","神经内科二病区");
+        map.put("1140100","神经电生理室");
+        map.put("1150000","神经外科");
+        map.put("1150001","神经外科一病区");
+        map.put("1150002","神经外科二病区");
+        map.put("115000A","脑胶质瘤多学科诊疗(MDT)");
+        map.put("1160000","皮肤科");
+        map.put("1160001","皮肤科病区");
+        map.put("1170000","预防保健科");
+        map.put("1170100","社区医疗服务中心");
+        map.put("1180000","(作废)特需诊疗");
+        map.put("1190000","急诊部");
+        map.put("1190001","急诊部观察室");
+        map.put("1190002","急诊部抢救室");
+        map.put("1190003","急诊部手术室");
+        map.put("1190004","急诊部-ICU病房");
+        map.put("1190005","急诊部病房");
+        map.put("1190006","急诊内科");
+        map.put("1190007","急诊外科");
+        map.put("1190008","急诊发热");
+        map.put("1190009","院前急救站");
+        map.put("1200000","肿瘤科");
+        map.put("1200100","肿瘤科一病区");
+        map.put("1200200","肿瘤科二病区");
+        map.put("1200300","肿瘤科三病区");
+        map.put("1210000","血液科");
+        map.put("1210100","血液科病区");
+        map.put("1210101","血液科(移植病房)");
+        map.put("1220000","内分泌科");
+        map.put("1220100","内分泌科病区");
+        map.put("1220200","全院血糖管理病区");
+        map.put("1230000","整形美容科");
+        map.put("1230001","整形美容科病区");
+        map.put("1240000","肿瘤放疗科");
+        map.put("1240100","肿瘤放疗科病区");
+        map.put("1250000","血管外科");
+        map.put("125000A","复杂糖尿病足及下肢溃疡门诊");
+        map.put("1250010","血管外科病区");
+        map.put("1250100","复杂动脉瘤微创门诊");
+        map.put("1260000","胃肠外科");
+        map.put("1260001","胃肠外科一病区");
+        map.put("1260002","胃肠外科二病区");
+        map.put("126000B","重症慢性便秘外科门诊");
+        map.put("1260100","重度痔疮及肛瘘门诊");
+        map.put("1270000","爱心科室");
+        map.put("1270001","爱心病区");
+        map.put("1280000","康复医学科");
+        map.put("1280100","康复医学科病区");
+        map.put("1280200","康复会诊管理病区");
+        map.put("1290000","总部心内科");
+        map.put("1300000","心血管外科");
+        map.put("1300100","心外ICU");
+        map.put("1310000","全科医学科");
+        map.put("1310001","全科医学科一病区");
+        map.put("1310002","全科医学科二病区");
+        map.put("1330000","异地心外科");
+        map.put("1400000","心血管内科");
+        map.put("1400001","心血管内科一病区");
+        map.put("1400003","心血管内科二病区");
+        map.put("1400100","心内CCU");
+        map.put("1400200","心内导管室");
+        map.put("1400300","心脏超声");
+        map.put("1400400","重症医学科");
+        map.put("1400401","重症医学科(病区)");
+        map.put("1410000","心血管科");
+        map.put("1410100","心血管科病区");
+        map.put("1420000","心脏大血管外科");
+        map.put("1420001","心脏大血管外科病区");
+        map.put("1420100","心血管内科(大内科二)");
+        map.put("1420200","心血管内科(大内科二)病区");
+        map.put("1440000","异地心内科");
+        map.put("1500000","体检一");
+        map.put("1500010","特需门诊");
+        map.put("1500020","体检二");
+        map.put("1500100","便民门诊1");
+        map.put("1500200","公务员体检");
+        map.put("1500300","职工体检");
+        map.put("1500400","居民体检");
+        map.put("1500500","保健体检");
+        map.put("1500600","便民门诊2");
+        map.put("1510000","小儿外科");
+        map.put("1510001","小儿外科病区");
+        map.put("1520000","乳腺外科");
+        map.put("1520001","乳腺外科病区");
+        map.put("1530000","介入导管室");
+        map.put("1530001","介入导管室病区");
+        map.put("1540000","核酸检测");
+        map.put("1550000","静脉治疗专科门诊");
+        map.put("1551000","伤口护理门诊");
+        map.put("1552000","神经心理门诊");
+        map.put("1553000","双通道药品注射(皮下)");
+        map.put("1554000","双通道药品注射(静脉)");
+        map.put("1600000","金榜分部");
+        map.put("1600100","金榜综合门诊部");
+        map.put("1600101","金榜门诊西药房");
+        map.put("1600102","金榜门诊中成药房");
+        map.put("1600103","金榜门诊中草药房");
+        map.put("1600104","金榜住院西药房");
+        map.put("1600105","金榜住院中成药房");
+        map.put("1600106","金榜检验");
+        map.put("1600107","金榜放射");
+        map.put("1600108","金榜B超");
+        map.put("1600109","金榜眼科");
+        map.put("1600110","金榜耳鼻喉");
+        map.put("1600111","厦北保健站");
+        map.put("1600112","金榜心功能室");
+        map.put("1600113","金榜收费处");
+        map.put("1600114","金榜口腔科");
+        map.put("1600115","金榜内科");
+        map.put("1600116","金榜外科");
+        map.put("1600117","金榜儿科");
+        map.put("1600118","金榜康复理疗科");
+        map.put("1600119","金榜妇科门诊");
+        map.put("1600120","金榜消化内科");
+        map.put("1600121","金榜内分泌科");
+        map.put("1600122","金榜老年科");
+        map.put("1600123","金榜神经内科");
+        map.put("1600124","金榜心血管内科");
+        map.put("1600125","厦北西药房");
+        map.put("1600126","厦北中成药房");
+        map.put("1600127","金榜疼痛门诊");
+        map.put("1600128","金榜体检");
+        map.put("1600200","金榜皮肤性病诊疗");
+        map.put("1600226","金榜肾内科");
+        map.put("1600300","整形美容(厦禾)");
+        map.put("1600400","金榜综合病房");
+        map.put("1600401","金榜妇科病房");
+        map.put("1610000","筼筜社区");
+        map.put("1610001","筼筜残联西药房");
+        map.put("1610002","筼筜残联中成药房");
+        map.put("1610003","筼筜残联中草药房");
+        map.put("1610004","筼筜残联收费处");
+        map.put("1610005","筼筜口腔科");
+        map.put("1610006","筼筜全科医疗科");
+        map.put("1610009","筼筜残联检验科");
+        map.put("1610010","筼筜中医科");
+        map.put("1610011","筼筜社区B超");
+        map.put("1610012","筼筜社区心电图");
+        map.put("1610013","筼筜社区理疗科");
+        map.put("1610014","筼筜内科");
+        map.put("1610015","筼筜外科");
+        map.put("1610016","筼筜妇产科");
+        map.put("1610017","筼筜儿科");
+        map.put("1610018","筼筜眼耳鼻喉科");
+        map.put("1610019","筼筜皮肤科");
+        map.put("1610020","筼筜精神科");
+        map.put("1610021","筼筜急诊科");
+        map.put("1610022","筼筜莲岳收费处");
+        map.put("1610023","筼筜莲岳中成药房");
+        map.put("1610024","筼筜莲岳西药房");
+        map.put("1610025","筼筜莲岳中草药房");
+        map.put("1610026","筼筜预防保健科");
+        map.put("1610027","筼筜妇女保健科");
+        map.put("1610028","筼筜儿童保健科");
+        map.put("1610029","筼筜康复医学科");
+        map.put("1610030","筼筜莲岳检验科");
+        map.put("1610031","筼筜医学影像科");
+        map.put("1610032","筼筜化学试剂库");
+        map.put("1610033","筼筜护理站");
+        map.put("1610034","莲岳妇产科");
+        map.put("1610035","莲岳全科医学科3");
+        map.put("1610036","莲岳中医科3");
+        map.put("1610037","市政府外科");
+        map.put("1610038","一里口腔科2");
+        map.put("1610039","屿后康复医学科");
+        map.put("1610040","屿后口腔科");
+        map.put("1610041","振兴内科2");
+        map.put("1610042","莲岳眼耳鼻喉科");
+        map.put("1610043","莲岳预防保健科");
+        map.put("1610044","市政府中医科");
+        map.put("1610045","振兴全科医疗科");
+        map.put("1610046","莲岳全科医学科2");
+        map.put("1610047","莲岳心内科");
+        map.put("1610048","市政府内科");
+        map.put("1610049","一里全科医学科2");
+        map.put("1610050","屿后全科医学科");
+        map.put("1610051","莲岳儿科");
+        map.put("1610052","莲岳口腔科2");
+        map.put("1610053","莲岳外科");
+        map.put("1610054","一里内科");
+        map.put("1610055","屿后中医科");
+        map.put("1610056","振兴外科2");
+        map.put("1610057","莲岳检验科");
+        map.put("1610058","莲岳康复医学科");
+        map.put("1610059","莲岳肾内科");
+        map.put("1610060","莲岳中医科");
+        map.put("1610061","一里外科");
+        map.put("1610062","一里中医科2");
+        map.put("1610063","儿童保健科");
+        map.put("1610064","莲岳内科");
+        map.put("1610065","莲岳医学影像科");
+        map.put("1610066","一里口腔科");
+        map.put("1610067","屿后妇产科");
+        map.put("1610068","莲岳妇产科2");
+        map.put("1610069","莲岳口腔科");
+        map.put("1610070","莲岳全科医学科");
+        map.put("1610071","社区办公室");
+        map.put("1610072","市政府全科");
+        map.put("1610073","一里全科医学科");
+        map.put("1610074","振兴全科医学科2");
+        map.put("1610075","振兴中医科2");
+        map.put("1620000","开元社区");
+        map.put("1620001","开元外科");
+        map.put("1620002","开元内科");
+        map.put("1620003","开元口腔科");
+        map.put("1620004","开元针灸科");
+        map.put("1620005","开元检验科");
+        map.put("1620006","开元西药房");
+        map.put("1620007","开元中成药房");
+        map.put("1620008","开元中草药房");
+        map.put("1620009","开元收费处");
+        map.put("1620010","开元儿科");
+        map.put("1620011","开元妇科");
+        map.put("1620012","开元中医科");
+        map.put("1620013","开元糖尿病科");
+        map.put("1620014","开元耳鼻喉科");
+        map.put("1620015","开元中医康复科");
+        map.put("1620016","开元中医痔疮科");
+        map.put("1620017","开元中医骨伤科");
+        map.put("1620018","开元心电B超");
+        map.put("1620019","开元放射科");
+        map.put("1620020","开元化学试剂库");
+        map.put("1620021","开元全科");
+        map.put("1620022","疫苗库");
+        map.put("1620023","开元全科医疗科");
+        map.put("1620024","信息科");
+        map.put("1620025","计免科");
+        map.put("1620026","社区工作室");
+        map.put("1630000","振兴点");
+        map.put("1630001","振兴收费处");
+        map.put("1630002","振兴西药房");
+        map.put("1630003","振兴中成药房");
+        map.put("1630004","振兴中草药房");
+        map.put("1630005","振兴全科医学");
+        map.put("1630006","振兴内科");
+        map.put("1630007","振兴理疗科");
+        map.put("1630008","振兴外科");
+        map.put("1630009","振兴中医科");
+        map.put("1640000","殿前社区");
+        map.put("1640001","殿前社区全科医学");
+        map.put("1640002","殿前西药房");
+        map.put("1640003","殿前中成药房");
+        map.put("1640004","殿前中草药房");
+        map.put("1640005","殿前收费处");
+        map.put("1640006","殿前外科");
+        map.put("1640007","殿前理疗科");
+        map.put("1640008","殿前内科");
+        map.put("1640009","殿前儿科");
+        map.put("1640010","殿前妇科");
+        map.put("1640011","殿前放射科");
+        map.put("1640012","殿前心电图室");
+        map.put("1640013","殿前B超室");
+        map.put("1640014","殿前检验科");
+        map.put("1640015","殿前口腔科");
+        map.put("1640016","殿前急诊抢救");
+        map.put("1640017","殿前儿保科");
+        map.put("1640018","殿前预防接种科");
+        map.put("1640019","殿前中医科");
+        map.put("1650000","嘉莲社区");
+        map.put("1650001","嘉莲全科");
+        map.put("1650002","嘉莲内科");
+        map.put("1650003","嘉莲妇科");
+        map.put("1650004","嘉莲口腔科");
+        map.put("1650005","嘉莲中医科");
+        map.put("1650006","嘉莲骨伤科");
+        map.put("1650007","嘉莲中医肛肠科");
+        map.put("1650008","嘉莲收费处");
+        map.put("1650009","嘉莲放射科");
+        map.put("1650010","嘉莲B超/心电图室");
+        map.put("1650011","嘉莲检验科");
+        map.put("1650012","嘉莲西药房");
+        map.put("1650013","嘉莲中成药房");
+        map.put("1650014","嘉莲中草药房");
+        map.put("1650015","嘉莲体检科");
+        map.put("1650016","嘉莲化学试剂库");
+        map.put("1650017","龙山中医科");
+        map.put("1650018","内科");
+        map.put("1650019","全科");
+        map.put("1650020","龙山内科");
+        map.put("1650021","龙山全科");
+        map.put("1650022","妇科");
+        map.put("1660000","梧村社区");
+        map.put("1660001","梧村全科医学");
+        map.put("1660002","梧村中医科");
+        map.put("1660003","梧村妇科");
+        map.put("1660004","梧村口腔科");
+        map.put("1660005","梧村B超");
+        map.put("1660006","梧村收费处");
+        map.put("1660007","梧村西药房");
+        map.put("1660008","梧村中成药房");
+        map.put("1660009","梧村内科");
+        map.put("1660010","梧村检验科");
+        map.put("1660011","梧村外科");
+        map.put("1660012","梧村化学试剂库");
+        map.put("1660013","万寿内科");
+        map.put("1660014","梧村儿科");
+        map.put("1660015","儿保科");
+        map.put("1660016","万寿康复医学科");
+        map.put("1660017","万寿中医科");
+        map.put("1660018","万寿全科");
+        map.put("1700000","厦禾分部");
+        map.put("1700100","厦禾分部办公室");
+        map.put("1700101","厦禾分部内科");
+        map.put("1700102","厦禾分部口腔科");
+        map.put("1700103","厦禾分部糖尿病科");
+        map.put("1700104","厦禾分部理疗科");
+        map.put("1700105","厦禾分部心电图室");
+        map.put("1700106","厦禾分部B超室");
+        map.put("1700107","厦禾分部放射科");
+        map.put("1700108","厦禾分部检验科");
+        map.put("1700109","驻干休点保健室");
+        map.put("1700110","厦禾分部收费处");
+        map.put("1700111","厦禾分部针推科");
+        map.put("1700112","厦禾分部西药房");
+        map.put("1700113","厦禾分部中成药房");
+        map.put("1700114","厦禾分部外科");
+        map.put("1700115","厦禾分部妇科");
+        map.put("1700116","厦禾分部中医内科");
+        map.put("1700117","厦禾分部五官眼科");
+        map.put("1700118","厦禾分部中草药房");
+        map.put("1700119","厦禾分部胃镜室");
+        map.put("1700120","厦禾分部护理组");
+        map.put("1700121","厦禾分部供应室");
+        map.put("1700122","厦禾分部体检科");
+        map.put("1700123","厦禾分部肠道科");
+        map.put("1700124","厦禾检验科(作废)");
+        map.put("1700125","厦禾分部心血管科");
+        map.put("1700126","厦禾分部皮肤科");
+        map.put("1700127","厦禾分部消化内科");
+        map.put("1700128","厦禾分部乳腺外科");
+        map.put("1800000","湖里分部");
+        map.put("1800001","湖里综合办");
+        map.put("1800002","湖里后勤物资");
+        map.put("1810100","湖里外科");
+        map.put("1810101","湖里乳腺美容室");
+        map.put("1810200","湖里骨科");
+        map.put("1811000","湖里综合外科");
+        map.put("1812000","湖里综合内科");
+        map.put("1812100","湖里内科");
+        map.put("1812200","湖里中医科");
+        map.put("1813000","湖里综合门诊");
+        map.put("1813001","湖里儿科");
+        map.put("1813002","湖里急诊科");
+        map.put("1813003","湖里五官科");
+        map.put("1813004","湖里口腔科");
+        map.put("1813005","湖里针灸理疗科");
+        map.put("1813006","湖里皮肤科");
+        map.put("1813007","湖里供应室");
+        map.put("1814000","湖里妇产科");
+        map.put("1814005","湖里产检");
+        map.put("1814010","湖里妇产科病区");
+        map.put("1815000","湖里体检科");
+        map.put("1816000","湖里麻醉手术室");
+        map.put("1816001","湖里疼痛门诊");
+        map.put("1820001","湖里检验科");
+        map.put("1820002","湖里心B超室");
+        map.put("1820003","湖里放射科");
+        map.put("1820004","湖里胃镜室");
+        map.put("1820005","湖里CT室");
+        map.put("1820006","湖里病理室");
+        map.put("1820007","湖里西药库");
+        map.put("1820008","湖里中成药库");
+        map.put("1820009","湖里中草药库");
+        map.put("1820010","湖里西药房");
+        map.put("1820011","湖里中成药房");
+        map.put("1820012","湖里草药房");
+        map.put("1820013","湖里中心药房西药");
+        map.put("1820014","湖里中心药房成药");
+        map.put("1820015","湖里化学试剂库");
+        map.put("1820016","湖里收费处");
+        map.put("1820017","湖里医务");
+        map.put("1820018","湖里信息");
+        map.put("1821001","湖里财务组");
+        map.put("1830000","湖里第一社区医疗服务中心");
+        map.put("1830001","东渡内科");
+        map.put("1830002","东渡外科");
+        map.put("1830003","东渡妇产科");
+        map.put("1830004","东渡儿科");
+        map.put("1830005","东渡中医科");
+        map.put("1830006","东渡糖尿病科");
+        map.put("1830007","东渡口腔科");
+        map.put("1830008","东渡康复治疗科");
+        map.put("1830009","东渡计生产后随访科");
+        map.put("1830010","东渡心理咨询科");
+        map.put("1830011","东渡健康教育科");
+        map.put("1830012","东渡慢性病管理科");
+        map.put("1830013","东渡全科");
+        map.put("1830014","海天预防保健科");
+        map.put("1830015","海天中医科");
+        map.put("1830016","海天内科");
+        map.put("1830017","康乐五官科");
+        map.put("1830018","兴华口腔科");
+        map.put("1830019","东渡妇科");
+        map.put("1830020","海天儿科");
+        map.put("1830021","海天精神科");
+        map.put("1830022","兴华全科");
+        map.put("1830023","海天妇产科");
+        map.put("1830024","海天口腔科");
+        map.put("1830025","海天五官科");
+        map.put("1830026","康乐口腔科");
+        map.put("1830027","康乐中医科");
+        map.put("1830028","社区工作室");
+        map.put("1830029","海天全科");
+        map.put("1830030","海天外科");
+        map.put("1830031","康乐全科");
+        map.put("1830032","兴华中医科");
+        map.put("1832000","东渡检验科");
+        map.put("1832001","东渡放射科");
+        map.put("1832002","东渡心电图B超室");
+        map.put("1832003","东渡西药房");
+        map.put("1832004","东渡中成药房");
+        map.put("1832005","东渡中草药房");
+        map.put("1832006","东渡挂号收费处");
+        map.put("1832007","东渡五官科");
+        map.put("1832008","湖里社区化学试剂库");
+        map.put("1832009","湖里社区西药房");
+        map.put("1832010","湖里社区中草药房");
+        map.put("1832011","湖里社区中成药房");
+        map.put("1832012","湖里社区收费处");
+        map.put("1832013","湖里社区检验科");
+        map.put("1832014","湖里社区放射科");
+        map.put("1832015","海天检验科");
+        map.put("1900000","演武分院");
+        map.put("1900001","演武放射科");
+        map.put("1900002","演武CT室");
+        map.put("1900003","演武心电图室");
+        map.put("1900004","演武检验科");
+        map.put("1900005","演武B超室");
+        map.put("1900006","演武胃镜室");
+        map.put("1900007","演武肠镜室");
+        map.put("2010000","营养科");
+        map.put("2010100","营养科病区");
+        map.put("2020000","检验科(作废)");
+        map.put("2020100","遗传室");
+        map.put("2020200","PCR室");
+        map.put("2030000","核医学科");
+        map.put("2040000","放射科");
+        map.put("2050000","CT室");
+        map.put("2050100","磁共振科(MRI)");
+        map.put("2060000","超声医学科");
+        map.put("2070000","病理科");
+        map.put("2080000","功能检查室");
+        map.put("2090000","药学部");
+        map.put("2090100","药库");
+        map.put("2090101","西药库");
+        map.put("2090102","中成药库");
+        map.put("2090103","草药库");
+        map.put("2090104","试剂库");
+        map.put("2090105","制剂库");
+        map.put("2090106","社区西药库");
+        map.put("2090107","社区中成药库");
+        map.put("2090108","社区中草药库");
+        map.put("2090109","演武西药库");
+        map.put("2090110","演武中成药库");
+        map.put("2090111","演武草药库");
+        map.put("2090112","湖里社区西药库");
+        map.put("2090113","湖里社区中成药库");
+        map.put("2090114","湖里社区中草药库");
+        map.put("2090115","筼筜社区西药库");
+        map.put("2090116","筼筜社区中成药库");
+        map.put("2090117","筼筜社区中草药库");
+        map.put("2090118","开元社区西药库");
+        map.put("2090119","开元社区中成药库");
+        map.put("2090120","开元社区中草药库");
+        map.put("2090121","梧村社区西药库");
+        map.put("2090122","梧村社区中成药库");
+        map.put("2090123","梧村社区中草药库");
+        map.put("2090124","嘉莲社区西药库");
+        map.put("2090125","嘉莲社区中成药库");
+        map.put("2090126","嘉莲社区中草药库");
+        map.put("2090127","双通道药库");
+        map.put("2090128","代煎草药库");
+        map.put("2090200","门诊西药房");
+        map.put("2090201","感染西药房");
+        map.put("2090300","门诊中成药房");
+        map.put("2090301","门诊中草药房");
+        map.put("2090302","感染中成药房");
+        map.put("2090303","颗粒药房");
+        map.put("2090304","代煎草药房");
+        map.put("2090305","厦禾代煎草药房");
+        map.put("2090400","中心药房1西药");
+        map.put("2090401","中心药房1中成药");
+        map.put("2090500","急诊药房");
+        map.put("2090501","急诊中成药");
+        map.put("2090600","中心药房2西药");
+        map.put("2090601","中心药房2中成药");
+        map.put("2090700","演武中心药房西药");
+        map.put("2090701","演武中心药房中成药");
+        map.put("2090702","演武门诊草药房");
+        map.put("2090800","发热西药");
+        map.put("2090801","发热中成药");
+        map.put("2090900","静配中心");
+        map.put("2100000","制剂室");
+        map.put("2110000","供应室");
+        map.put("2120000","理疗科");
+        map.put("2130000","氧疗");
+        map.put("2140000","中心实验室");
+        map.put("2150000","CO60");
+        map.put("2160000","检验科");
+        map.put("2170000","超伽科");
+        map.put("2180000","高压氧");
+        map.put("2190000","输血科");
+        map.put("2220000","DSA导管室");
+        map.put("3010000","院部");
+        map.put("3020000","党办");
+        map.put("3030000","工会团委");
+        map.put("3040000","院办公室");
+        map.put("3040100","打字室");
+        map.put("3040200","总机室");
+        map.put("3050000","人力资源部");
+        map.put("3060000","保卫科");
+        map.put("3070000","医务部");
+        map.put("3080000","科教部");
+        map.put("3080100","省慢性肝病肝癌重点实验室");
+        map.put("3080200","胃肠肿瘤研究所");
+        map.put("3090000","信息中心");
+        map.put("3090100","计算机中心");
+        map.put("3090200","病案室");
+        map.put("3090300","统计室");
+        map.put("3090400","图书馆");
+        map.put("3100000","护理部");
+        map.put("3110000","设备科");
+        map.put("3120000","质量管理科");
+        map.put("3130000","财务部");
+        map.put("3130100","住院处");
+        map.put("3130200","收费处");
+        map.put("3140000","门诊部");
+        map.put("3140100","门诊护理");
+        map.put("3140200","挂号室");
+        map.put("3150000","感染疾病科");
+        map.put("3150001","感染性疾病科病区");
+        map.put("3150002","感染性疾病科二病区");
+        map.put("3150003","普通发热一病区");
+        map.put("3150100","感染性疾病科(核酸采集)");
+        map.put("3160000","医院感染管理与预防保健科");
+        map.put("3170000","临床教学部");
+        map.put("3180000","审计科");
+        map.put("3190000","采购部");
+        map.put("3200000","医疗保险管理科");
+        map.put("3210000","纪检监察室");
+        map.put("5000000","其他");
+        map.put("6010000","保障部");
+        map.put("6010100","锅炉班");
+        map.put("6010200","汽车班");
+        map.put("6010300","基建");
+        map.put("6010400","电梯班");
+        map.put("6010500","缝纫班");
+        map.put("6010600","污水处理组");
+        map.put("6010700","电工班");
+        map.put("6010800","管道班");
+        map.put("6010900","木工班");
+        map.put("6011000","三产办");
+        map.put("6012000","招待所");
+        map.put("6020000","维修中心");
+        map.put("6030000","会议保障");
+        map.put("6040000","药学部新型冠状病毒(捐赠)");
+
+    }
+}

+ 16 - 0
src/main/java/com/qmrb/system/common/constant/ExcelConstants.java

@@ -0,0 +1,16 @@
+package com.qmrb.system.common.constant;
+
+/**
+ * Excel 常量
+ *
+ * @author: haoxr
+ * @date: 2023/03/24
+ */
+public interface ExcelConstants {
+
+    /**
+     * Excel 模板目录
+     */
+    String EXCEL_TEMPLATE_DIR="excel-templates";
+
+}

+ 40 - 0
src/main/java/com/qmrb/system/common/constant/SecurityConstants.java

@@ -0,0 +1,40 @@
+package com.qmrb.system.common.constant;
+
+/**
+ * Security 常量
+ *
+ * @author: haoxr
+ * @date: 2023/03/24
+ */
+public interface SecurityConstants {
+
+    /**
+     * 登录接口路径
+     */
+    String LOGIN_PATH = "/api/v1/auth/login";
+
+    /**
+     * Token 前缀
+     */
+    String TOKEN_PREFIX = "Bearer ";
+
+    /**
+     * 请求头Token的Key
+     */
+    String TOKEN_KEY = "Authorization";
+
+    /**
+     * 验证码缓存前缀
+     */
+    String VERIFY_CODE_CACHE_PREFIX = "AUTH:VERIFY_CODE:";
+
+    /**
+     * 用户权限集合缓存前缀
+     */
+    String USER_PERMS_CACHE_PREFIX = "AUTH:USER_PERMS:";
+
+    /**
+     * 黑名单Token缓存前缀
+     */
+    String BLACK_TOKEN_CACHE_PREFIX = "AUTH:BLACK_TOKEN:";
+}

+ 31 - 0
src/main/java/com/qmrb/system/common/constant/SystemConstants.java

@@ -0,0 +1,31 @@
+package com.qmrb.system.common.constant;
+
+/**
+ * 系统常量
+ *
+ * @author haoxr
+ * @since  2022/10/22
+ */
+public interface SystemConstants {
+
+    /**
+     * 根节点ID
+     */
+    Long ROOT_NODE_ID = 0L;
+
+
+    /**
+     * 系统默认密码
+     */
+    String DEFAULT_PASSWORD = "123456";
+
+    /**
+     * 超级管理员角色编码
+     */
+    String ROOT_ROLE_CODE = "ROOT";
+    String SYNC_SEUUCEE="同步成功";
+    String SYNC_FAIL="同步失败";
+    String SYNC_FIND="数据已存在";
+
+
+}

+ 33 - 0
src/main/java/com/qmrb/system/common/enums/DataScopeEnum.java

@@ -0,0 +1,33 @@
+package com.qmrb.system.common.enums;
+
+import com.qmrb.system.common.base.IBaseEnum;
+
+import lombok.Getter;
+
+/**
+ * 数据权限枚举
+ *
+ * @author haoxr
+ * @date 2022/10/14
+ */
+public enum DataScopeEnum implements IBaseEnum<Integer> {
+
+    /**
+     * value 越小,数据权限范围越大
+     */
+    ALL(0, "所有数据"),
+    DEPT_AND_SUB(1, "部门及子部门数据"),
+    DEPT(2, "本部门数据"),
+    SELF(3, "本人数据");
+
+    @Getter
+    private Integer value;
+
+    @Getter
+    private String label;
+
+    DataScopeEnum(Integer value, String label) {
+        this.value = value;
+        this.label = label;
+    }
+}

+ 30 - 0
src/main/java/com/qmrb/system/common/enums/GenderEnum.java

@@ -0,0 +1,30 @@
+package com.qmrb.system.common.enums;
+
+import com.qmrb.system.common.base.IBaseEnum;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+
+/**
+ * 性别枚举
+ *
+ * @author haoxr
+ * @date 2022/10/14
+ */
+@Schema(enumAsRef = true)
+public enum GenderEnum implements IBaseEnum<Integer> {
+
+    MALE(1, "男"),
+    FEMALE (2, "女");
+
+    @Getter
+    private Integer value;
+
+    @Getter
+    private String label;
+
+    GenderEnum(Integer value, String label) {
+        this.value = value;
+        this.label = label;
+    }
+}

+ 36 - 0
src/main/java/com/qmrb/system/common/enums/MenuTypeEnum.java

@@ -0,0 +1,36 @@
+package com.qmrb.system.common.enums;
+
+import com.baomidou.mybatisplus.annotation.EnumValue;
+import com.qmrb.system.common.base.IBaseEnum;
+
+import lombok.Getter;
+
+/**
+ * 菜单类型枚举
+ *
+ * @author haoxr
+ * @date 2022/4/23 9:36
+ */
+
+public enum MenuTypeEnum implements IBaseEnum<Integer> {
+
+    NULL(0, null),
+    MENU(1, "菜单"),
+    CATALOG(2, "目录"),
+    EXTLINK(3, "外链"),
+    BUTTON(4, "按钮");
+
+    @Getter
+    @EnumValue //  Mybatis-Plus 提供注解表示插入数据库时插入该值
+    private Integer value;
+
+    @Getter
+    // @JsonValue //  表示对枚举序列化时返回此字段
+    private String label;
+
+    MenuTypeEnum(Integer value, String label) {
+        this.value = value;
+        this.label = label;
+    }
+
+}

+ 28 - 0
src/main/java/com/qmrb/system/common/enums/StatusEnum.java

@@ -0,0 +1,28 @@
+package com.qmrb.system.common.enums;
+
+import com.qmrb.system.common.base.IBaseEnum;
+
+import lombok.Getter;
+
+/**
+ * 状态枚举
+ *
+ * @author haoxr
+ * @date 2022/10/14
+ */
+public enum StatusEnum implements IBaseEnum<Integer> {
+
+    ENABLE(1, "启用"),
+    DISABLE (0, "禁用");
+
+    @Getter
+    private Integer value;
+
+    @Getter
+    private String label;
+
+    StatusEnum(Integer value, String label) {
+        this.value = value;
+        this.label = label;
+    }
+}

+ 39 - 0
src/main/java/com/qmrb/system/common/exception/BusinessException.java

@@ -0,0 +1,39 @@
+package com.qmrb.system.common.exception;
+
+import com.qmrb.system.common.result.IResultCode;
+import lombok.Getter;
+
+/**
+ * 自定义业务异常
+ *
+ * @author haoxr
+ * @date 2022/7/31
+ */
+@Getter
+public class BusinessException extends RuntimeException {
+
+    /**
+	 * 
+	 */
+	private static final long serialVersionUID = 1L;
+	public IResultCode resultCode;
+
+    public BusinessException(IResultCode errorCode) {
+        super(errorCode.getMsg());
+        this.resultCode = errorCode;
+    }
+
+    public BusinessException(String message){
+        super(message);
+    }
+
+    public BusinessException(String message, Throwable cause){
+        super(message, cause);
+    }
+
+    public BusinessException(Throwable cause){
+        super(cause);
+    }
+
+
+}

+ 211 - 0
src/main/java/com/qmrb/system/common/exception/GlobalExceptionHandler.java

@@ -0,0 +1,211 @@
+package com.qmrb.system.common.exception;
+
+import cn.hutool.core.util.StrUtil;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.qmrb.system.common.result.Result;
+import com.qmrb.system.common.result.ResultCode;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.TypeMismatchException;
+import org.springframework.context.support.DefaultMessageSourceResolvable;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.jdbc.BadSqlGrammarException;
+import org.springframework.validation.BindException;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.MissingServletRequestParameterException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
+import org.springframework.web.servlet.NoHandlerFoundException;
+
+import jakarta.servlet.ServletException;
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.ConstraintViolationException;
+
+import java.sql.SQLSyntaxErrorException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+/**
+ * 全局系统异常处理器
+ * <p>
+ * 调整异常处理的HTTP状态码,丰富异常处理类型
+ *
+ * @author Gadfly
+ * @since 2020-02-25 13:54
+ **/
+@RestControllerAdvice
+@Slf4j
+public class GlobalExceptionHandler {
+
+    @ExceptionHandler(BindException.class)
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public <T> Result<T> processException(BindException e) {
+        log.error("BindException:{}", e.getMessage(), e);
+        String msg = e.getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining(";"));
+        return Result.failed(ResultCode.PARAM_ERROR, msg);
+    }
+
+    /**
+     * RequestParam参数的校验
+     *
+     * @param e
+     * @param <T>
+     * @return
+     */
+    @ExceptionHandler(ConstraintViolationException.class)
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public <T> Result<T> processException(ConstraintViolationException e) {
+        log.error("ConstraintViolationException:{}", e.getMessage(), e);
+        String msg = e.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(";"));
+        return Result.failed(ResultCode.PARAM_ERROR, msg);
+    }
+
+    /**
+     * RequestBody参数的校验
+     *
+     * @param e
+     * @param <T>
+     * @return
+     */
+    @ExceptionHandler(MethodArgumentNotValidException.class)
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public <T> Result<T> processException(MethodArgumentNotValidException e) {
+        log.error("MethodArgumentNotValidException:{}", e.getMessage(), e);
+        String msg = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining(";"));
+        return Result.failed(ResultCode.PARAM_ERROR, msg);
+    }
+
+    @ExceptionHandler(NoHandlerFoundException.class)
+    @ResponseStatus(HttpStatus.NOT_FOUND)
+    public <T> Result<T> processException(NoHandlerFoundException e) {
+        log.error(e.getMessage(), e);
+        return Result.failed(ResultCode.RESOURCE_NOT_FOUND);
+    }
+
+    /**
+     * MissingServletRequestParameterException
+     */
+    @ExceptionHandler(MissingServletRequestParameterException.class)
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public <T> Result<T> processException(MissingServletRequestParameterException e) {
+        log.error(e.getMessage(), e);
+        return Result.failed(ResultCode.PARAM_IS_NULL);
+    }
+
+    /**
+     * MethodArgumentTypeMismatchException
+     */
+    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public <T> Result<T> processException(MethodArgumentTypeMismatchException e) {
+        log.error(e.getMessage(), e);
+        return Result.failed(ResultCode.PARAM_ERROR, "类型错误");
+    }
+
+    /**
+     * ServletException
+     */
+    @ExceptionHandler(ServletException.class)
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public <T> Result<T> processException(ServletException e) {
+        log.error(e.getMessage(), e);
+        return Result.failed(e.getMessage());
+    }
+
+    @ExceptionHandler(IllegalArgumentException.class)
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public <T> Result<T> handleIllegalArgumentException(IllegalArgumentException e) {
+        log.error("非法参数异常,异常原因:{}", e.getMessage(), e);
+        return Result.failed(e.getMessage());
+    }
+
+    @ExceptionHandler(JsonProcessingException.class)
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public <T> Result<T> handleJsonProcessingException(JsonProcessingException e) {
+        log.error("Json转换异常,异常原因:{}", e.getMessage(), e);
+        return Result.failed(e.getMessage());
+    }
+
+    /**
+     * HttpMessageNotReadableException
+     */
+    @ExceptionHandler(HttpMessageNotReadableException.class)
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public <T> Result<T> processException(HttpMessageNotReadableException e) {
+        log.error(e.getMessage(), e);
+        String errorMessage = "请求体不可为空";
+        Throwable cause = e.getCause();
+        if (cause != null) {
+            errorMessage = convertMessage(cause);
+        }
+        return Result.failed(errorMessage);
+    }
+
+    @ExceptionHandler(TypeMismatchException.class)
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public <T> Result<T> processException(TypeMismatchException e) {
+        log.error(e.getMessage(), e);
+        return Result.failed(e.getMessage());
+    }
+
+    @ExceptionHandler(BadSqlGrammarException.class)
+    @ResponseStatus(HttpStatus.FORBIDDEN)
+    public <T> Result<T> handleBadSqlGrammarException(BadSqlGrammarException e) {
+        log.error(e.getMessage(), e);
+        String errorMsg = e.getMessage();
+        if (StrUtil.isNotBlank(errorMsg) && errorMsg.contains("denied to user")) {
+            return Result.failed(ResultCode.FORBIDDEN_OPERATION);
+        } else {
+            return Result.failed(e.getMessage());
+        }
+    }
+
+    @ExceptionHandler(SQLSyntaxErrorException.class)
+    @ResponseStatus(HttpStatus.FORBIDDEN)
+    public <T> Result<T> processSQLSyntaxErrorException(SQLSyntaxErrorException e) {
+        log.error(e.getMessage(), e);
+        return Result.failed(e.getMessage());
+    }
+
+
+    @ExceptionHandler(BusinessException.class)
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public <T> Result<T> handleBizException(BusinessException e) {
+        log.error("biz exception: {}", e.getMessage(), e);
+        if (e.getResultCode() != null) {
+            return Result.failed(e.getResultCode());
+        }
+        return Result.failed(e.getMessage());
+    }
+
+    @ExceptionHandler(Exception.class)
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public <T> Result<T> handleException(Exception e) {
+        log.error("unknown exception: {}", e.getMessage(), e);
+        return Result.failed(e.getLocalizedMessage());
+    }
+
+    /**
+     * 传参类型错误时,用于消息转换
+     *
+     * @param throwable 异常
+     * @return 错误信息
+     */
+    private String convertMessage(Throwable throwable) {
+        String error = throwable.toString();
+        String regulation = "\\[\"(.*?)\"]+";
+        Pattern pattern = Pattern.compile(regulation);
+        Matcher matcher = pattern.matcher(error);
+        String group = "";
+        if (matcher.find()) {
+            String matchString = matcher.group();
+            matchString = matchString.replace("[", "").replace("]", "");
+            matchString = "%s字段类型错误".formatted(matchString.replaceAll("\\\"", ""));
+            group += matchString;
+        }
+        return group;
+    }
+}

+ 41 - 0
src/main/java/com/qmrb/system/common/proc/CommonJDBCTemplate.java

@@ -0,0 +1,41 @@
+package com.qmrb.system.common.proc;
+
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
+import org.springframework.jdbc.core.namedparam.SqlParameterSource;
+import org.springframework.jdbc.core.simple.SimpleJdbcCall;
+import org.springframework.jdbc.datasource.DriverManagerDataSource;
+import org.springframework.stereotype.Service;
+
+import javax.sql.DataSource;
+import java.util.*;
+
+public class CommonJDBCTemplate implements CommonTemplate {
+    private DataSource dataSourceMssql;
+    private JdbcTemplate jdbcTemplateObject;
+
+    public void setDataSourceMssql(DataSource dataSourceMssql) {
+        this.dataSourceMssql = dataSourceMssql;
+        this.jdbcTemplateObject = new JdbcTemplate(dataSourceMssql);
+    }
+
+    public Map query(Map param,String procName) {
+        SimpleJdbcCall jdbcCall = new SimpleJdbcCall(dataSourceMssql).withProcedureName(procName);
+        MapSqlParameterSource msp = new MapSqlParameterSource();
+        SqlParameterSource in=null;
+        Set<String> set = param.keySet();
+        for (String key:set){
+            in = msp.addValue(key, param.get(key));
+        }
+        Map<String, Object> out = jdbcCall.execute(in);
+
+        for(Map.Entry<String, Object> entry:out.entrySet()){
+            List dataList = (ArrayList)entry.getValue();//linkmap默认有序
+            if(dataList.size()>0) {
+                out = (Map<String, Object>) (dataList).get(0);
+            }
+        }
+        return out;
+    }
+
+}

+ 18 - 0
src/main/java/com/qmrb/system/common/proc/CommonTemplate.java

@@ -0,0 +1,18 @@
+package com.qmrb.system.common.proc;
+
+import javax.sql.DataSource;
+import java.util.Map;
+
+public interface CommonTemplate {
+    /**
+     * This is the method to be used to initialize database resources ie.
+     * connection.
+     */
+    public void setDataSourceMssql(DataSource ds);
+
+    /**
+     * This is the method to be used to list down a record from the Student
+     * table corresponding to a passed student id.
+     */
+    public Map query(Map param,String procName);
+}

+ 21 - 0
src/main/java/com/qmrb/system/common/proc/MainApp.java

@@ -0,0 +1,21 @@
+package com.qmrb.system.common.proc;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.support.ClassPathXmlApplicationContext;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class MainApp {
+    public static void main(String[] args) {
+        ApplicationContext context = new ClassPathXmlApplicationContext("application-beans.xml");
+        CommonJDBCTemplate commonJDBCTemplate = (CommonJDBCTemplate) context.getBean("commonJDBCTemplate");
+        Map param = new HashMap<>();
+        param.put("in_xml","<root><request card_no=\"H64027281\" social_no=\"\" pid=\"\"/></root>");
+        for (int i = 0; i < 100; i++) {
+            Map out = commonJDBCTemplate.query(param,"AM_SP_GET_MZ_PATIENT");
+            System.out.println(out);
+        }
+
+    }
+}

+ 12 - 0
src/main/java/com/qmrb/system/common/result/IResultCode.java

@@ -0,0 +1,12 @@
+package com.qmrb.system.common.result;
+
+/**
+ * @author haoxr
+ **/
+public interface IResultCode {
+
+    String getCode();
+
+    String getMsg();
+
+}

+ 51 - 0
src/main/java/com/qmrb/system/common/result/PageResult.java

@@ -0,0 +1,51 @@
+package com.qmrb.system.common.result;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 分页响应结构体
+ *
+ * @author haoxr
+ * @date 2022/2/18 23:29
+ */
+@Data
+public class PageResult<T> implements Serializable {
+
+    /**
+	 * 
+	 */
+	private static final long serialVersionUID = 1L;
+
+	private String code;
+
+    private Data<T> data;
+
+    private String msg;
+
+    public static <T> PageResult<T> success(IPage<T> page) {
+        PageResult<T> result = new PageResult<>();
+        result.setCode(ResultCode.SUCCESS.getCode());
+
+        Data<T> data = new Data<T>();
+        data.setList(page.getRecords());
+        data.setTotal(page.getTotal());
+
+        result.setData(data);
+        result.setMsg(ResultCode.SUCCESS.getMsg());
+        return result;
+    }
+
+    @lombok.Data
+    public static class Data<T> {
+
+        private List<T> list;
+
+        private long total;
+
+    }
+
+}

+ 79 - 0
src/main/java/com/qmrb/system/common/result/Result.java

@@ -0,0 +1,79 @@
+package com.qmrb.system.common.result;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 统一响应结构体
+ *
+ * @author haoxr
+ * @date 2022/1/30
+ **/
+@Data
+public class Result<T> implements Serializable {
+
+    /**
+	 * 
+	 */
+	private static final long serialVersionUID = 1L;
+
+	private String code;
+
+    private T data;
+
+    private String msg;
+
+    public static <T> Result<T> success() {
+        return success(null);
+    }
+
+    public static <T> Result<T> success(T data) {
+        Result<T> result = new Result<>();
+        result.setCode(ResultCode.SUCCESS.getCode());
+        result.setMsg(ResultCode.SUCCESS.getMsg());
+        result.setData(data);
+        return result;
+    }
+
+    public static <T> Result<T> failed() {
+        return result(ResultCode.SYSTEM_EXECUTION_ERROR.getCode(), ResultCode.SYSTEM_EXECUTION_ERROR.getMsg(), null);
+    }
+
+    public static <T> Result<T> failed(String msg) {
+        return result(ResultCode.SYSTEM_EXECUTION_ERROR.getCode(), msg, null);
+    }
+
+    public static <T> Result<T> judge(boolean status) {
+        if (status) {
+            return success();
+        } else {
+            return failed();
+        }
+    }
+
+    public static <T> Result<T> failed(IResultCode resultCode) {
+        return result(resultCode.getCode(), resultCode.getMsg(), null);
+    }
+
+    public static <T> Result<T> failed(IResultCode resultCode, String msg) {
+        return result(resultCode.getCode(), msg, null);
+    }
+
+    @SuppressWarnings("unused")
+	private static <T> Result<T> result(IResultCode resultCode, T data) {
+        return result(resultCode.getCode(), resultCode.getMsg(), data);
+    }
+
+    private static <T> Result<T> result(String code, String msg, T data) {
+        Result<T> result = new Result<>();
+        result.setCode(code);
+        result.setData(data);
+        result.setMsg(msg);
+        return result;
+    }
+
+    public static boolean isSuccess(Result<?> result) {
+        return result != null && ResultCode.SUCCESS.getCode().equals(result.getCode());
+    }
+}

+ 114 - 0
src/main/java/com/qmrb/system/common/result/ResultCode.java

@@ -0,0 +1,114 @@
+package com.qmrb.system.common.result;
+
+import lombok.AllArgsConstructor;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 响应码枚举
+ *
+ * @author haoxr
+ * @date 2020-06-23
+ **/
+@AllArgsConstructor
+@NoArgsConstructor
+public enum ResultCode implements IResultCode, Serializable {
+
+    SUCCESS("00000", "请求成功"),
+
+    USER_ERROR("A0001", "用户端错误"),
+    REPEAT_SUBMIT_ERROR("A0002", "您的请求已提交,请不要重复提交或等待片刻再尝试。"),
+
+    USER_LOGIN_ERROR("A0200", "用户登录异常"),
+
+    USER_NOT_EXIST("A0201", "用户不存在"),
+    USER_ACCOUNT_LOCKED("A0202", "用户账户被冻结"),
+    USER_ACCOUNT_INVALID("A0203", "用户账户已作废"),
+
+    USERNAME_OR_PASSWORD_ERROR("A0210", "用户名或密码错误"),
+    PASSWORD_ENTER_EXCEED_LIMIT("A0211", "用户输入密码次数超限"),
+    CLIENT_AUTHENTICATION_FAILED("A0212", "客户端认证失败"),
+
+    VERIFY_CODE_TIMEOUT("A0213", "验证码已过期"),
+    VERIFY_CODE_ERROR("A0214", "验证码错误"),
+
+    TOKEN_INVALID("A0230", "token无效或已过期"),
+    TOKEN_ACCESS_FORBIDDEN("A0231", "token已被禁止访问"),
+
+    AUTHORIZED_ERROR("A0300", "访问权限异常"),
+    ACCESS_UNAUTHORIZED("A0301", "访问未授权"),
+    FORBIDDEN_OPERATION("A0302", "演示环境禁止新增、修改和删除数据,请本地部署后测试"),
+
+
+    PARAM_ERROR("A0400", "用户请求参数错误"),
+    RESOURCE_NOT_FOUND("A0401", "请求资源不存在"),
+    PARAM_IS_NULL("A0410", "请求必填参数为空"),
+
+    USER_UPLOAD_FILE_ERROR("A0700", "用户上传文件异常"),
+    USER_UPLOAD_FILE_TYPE_NOT_MATCH("A0701", "用户上传文件类型不匹配"),
+    USER_UPLOAD_FILE_SIZE_EXCEEDS("A0702", "用户上传文件太大"),
+    USER_UPLOAD_IMAGE_SIZE_EXCEEDS("A0703", "用户上传图片太大"),
+
+    SYSTEM_EXECUTION_ERROR("B0001", "系统执行出错"),
+    SYSTEM_EXECUTION_TIMEOUT("B0100", "系统执行超时"),
+    SYSTEM_ORDER_PROCESSING_TIMEOUT("B0100", "系统订单处理超时"),
+
+    SYSTEM_DISASTER_RECOVERY_TRIGGER("B0200", "系统容灾功能被出发"),
+    FLOW_LIMITING("B0210", "系统限流"),
+    DEGRADATION("B0220", "系统功能降级"),
+
+    SYSTEM_RESOURCE_ERROR("B0300", "系统资源异常"),
+    SYSTEM_RESOURCE_EXHAUSTION("B0310", "系统资源耗尽"),
+    SYSTEM_RESOURCE_ACCESS_ERROR("B0320", "系统资源访问异常"),
+    SYSTEM_READ_DISK_FILE_ERROR("B0321", "系统读取磁盘文件失败"),
+
+    CALL_THIRD_PARTY_SERVICE_ERROR("C0001", "调用第三方服务出错"),
+    MIDDLEWARE_SERVICE_ERROR("C0100", "中间件服务出错"),
+    INTERFACE_NOT_EXIST("C0113", "接口不存在"),
+
+    MESSAGE_SERVICE_ERROR("C0120", "消息服务出错"),
+    MESSAGE_DELIVERY_ERROR("C0121", "消息投递出错"),
+    MESSAGE_CONSUMPTION_ERROR("C0122", "消息消费出错"),
+    MESSAGE_SUBSCRIPTION_ERROR("C0123", "消息订阅出错"),
+    MESSAGE_GROUP_NOT_FOUND("C0124", "消息分组未查到"),
+
+    DATABASE_ERROR("C0300", "数据库服务出错"),
+    DATABASE_TABLE_NOT_EXIST("C0311", "表不存在"),
+    DATABASE_COLUMN_NOT_EXIST("C0312", "列不存在"),
+    DATABASE_DUPLICATE_COLUMN_NAME("C0321", "多表关联中存在多个相同名称的列"),
+    DATABASE_DEADLOCK("C0331", "数据库死锁"),
+    DATABASE_PRIMARY_KEY_CONFLICT("C0341", "主键冲突");
+
+    @Override
+    public String getCode() {
+        return code;
+    }
+
+    @Override
+    public String getMsg() {
+        return msg;
+    }
+
+    private String code;
+
+    private String msg;
+
+    @Override
+    public String toString() {
+        return "{" +
+                "\"code\":\"" + code + '\"' +
+                ", \"msg\":\"" + msg + '\"' +
+                '}';
+    }
+
+
+    public static ResultCode getValue(String code){
+        for (ResultCode value : values()) {
+            if (value.getCode().equals(code)) {
+                return value;
+            }
+        }
+        return SYSTEM_EXECUTION_ERROR; // 默认系统执行错误
+    }
+}

+ 186 - 0
src/main/java/com/qmrb/system/common/util/AesUtil.java

@@ -0,0 +1,186 @@
+package com.qmrb.system.common.util;
+
+import org.apache.commons.codec.binary.Base64;
+
+import javax.crypto.*;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.Charset;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.Random;
+
+public class AesUtil {
+
+    /**
+     * 注意key和加密用到的字符串是不一样的 加密还要指定填充的加密模式和填充模式 AES密钥可以是128或者256, 加密模式包括ECB, CBC等
+     * ECB模式是分组的模式, CBC是分块加密后,每块与前一块的加密结果异或后再加密 第一块加密的明文是与IV变量进行异或
+     */
+    static Charset CHARSET = Charset.forName("utf-8");
+    public static final String KEY_ALGORITHM        = "AES";
+    public static final String ECB_CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";
+    public static final String CBC_CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
+
+    /**
+     * IV(Initialization Value)是一个初始值,对于CBC模式来说,它必须是随机选取并且需要保密的
+     * 而且它的长度和密码分组相同(比如:对于AES 128为128位,即长度为16的byte类型数组)
+     */
+
+    public static final String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+    
+    public static String getRandomStr(int len) {
+        int baseStringLength = base.length();
+        Random random = new Random();
+        StringBuffer sb = new StringBuffer();
+        for (int i = 0; i < len; i++) {
+            int number = random.nextInt(baseStringLength);
+            sb.append(base.charAt(number));
+        }
+        return sb.toString();
+    }
+
+    public static void main(String[] arg) {
+        String password = "#123we#$%^fdhg34";
+//        password=getRandomStr(16);
+        String PLAIN_TEXT           = "MANUTD is the greatest club in the world";
+
+//        SecretKey key = generateSecretKey(password);
+//        System.out.println(key.getEncoded().length);
+//        byte[] encodedText = AesEcbEncode(PLAIN_TEXT.getBytes(), key);
+//        System.out.println("AES ECB encoded with Base64: " + Base64.encodeBase64String(encodedText));
+//        System.out.println("AES ECB decoded: " + AesEcbDecode(encodedText, key));
+
+        
+        byte[] iv = Arrays.copyOfRange(password.getBytes(), 0, 16);
+        byte[] encodedText = encrypt(PLAIN_TEXT.getBytes(), password, iv);
+        
+        System.out.println("AES CBC encoded with Base64: " + Base64.encodeBase64String(encodedText));
+        String content = "Uy9RChm5h26xPWz/a2lNTQOj+yUM0SGDUwVTvlhglP+PUqUlaXj6OOoBuX4ClqSysiedPhfTpkwVyQ9HPp/KBlVAZilK7gAxg8lRJ/Bo3JWGCtQDIAnybAHlpbRKNhh+y3COYs9u7+4HPohCVT1DK9/mauDOcxW1jKXIpcGfY9Y=";
+        byte[] cb = java.util.Base64.getDecoder().decode(content);
+        byte[] data=decrypt(cb, password, iv);
+        System.out.println("AES CBC decoded: " +new String(data));
+    }
+
+    /**
+     * 使用ECB模式进行加密。 加密过程三步走: 1. 传入算法,实例化一个加解密器 2. 传入加密模式和密钥,初始化一个加密器 3.
+     * 调用doFinal方法加密
+     * 
+     * @param plainText
+     * @return
+     */
+    public static byte[] AesEcbEncode(byte[] plainText, SecretKey key) {
+        try {
+            Cipher cipher = Cipher.getInstance(ECB_CIPHER_ALGORITHM);
+            cipher.init(Cipher.ENCRYPT_MODE, key);
+            return cipher.doFinal(plainText);
+        } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException
+                | BadPaddingException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    /**
+     * 使用ECB解密
+     * 
+     * @param decodedText
+     * @param key
+     * @return
+     */
+    public static String AesEcbDecode(byte[] decodedText, SecretKey key) {
+        try {
+            Cipher cipher = Cipher.getInstance(ECB_CIPHER_ALGORITHM);
+            cipher.init(Cipher.DECRYPT_MODE, key);
+            return new String(cipher.doFinal(decodedText));
+        } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException
+                | BadPaddingException e) {
+            e.printStackTrace();
+        }
+        return null;
+
+    }
+
+    /**
+     * CBC加密
+     * 
+     * @param plainText
+     * @param key
+     * @param IVParameter
+     * @return
+     */
+    public static byte[] encrypt(byte[] plainText, String aesKey, byte[] IVParameter) {
+        try {
+            Key key=generateSecretKey(aesKey);
+            IvParameterSpec ivParameterSpec = new IvParameterSpec(IVParameter);
+            Cipher cipher = Cipher.getInstance(CBC_CIPHER_ALGORITHM);
+            cipher.init(Cipher.ENCRYPT_MODE, key, ivParameterSpec);
+            return cipher.doFinal(plainText);
+
+        } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
+                | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    /**
+     * CBC 解密
+     * 
+     * @param decodedText
+     * @param key
+     * @param IVParameter
+     * @return
+     */
+    public static byte[] decrypt(byte[] data, String aesKey, byte[] ivParameter) {
+        SecretKey key=generateSecretKey(aesKey);
+        IvParameterSpec ivParameterSpec = new IvParameterSpec(ivParameter);
+//        IvParameterSpec ivParameterSpec = new IvParameterSpec(aesKey.getBytes(CHARSET),0,16);
+        try {
+            Cipher cipher = Cipher.getInstance(CBC_CIPHER_ALGORITHM);
+            cipher.init(Cipher.DECRYPT_MODE, key, ivParameterSpec);
+            return cipher.doFinal(data);
+        } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
+                | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    /**
+     * 1.创建一个KeyGenerator 2.调用KeyGenerator.generateKey方法
+     * 由于某些原因,这里只能是128,如果设置为256会报异常,需修改java jce文件
+     * 
+     * @return
+     */
+//    public static SecretKey generateAESSecretKey(String password) {
+//        KeyGenerator keyGenerator = null;
+//        try {
+//            keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM);
+//        } catch (NoSuchAlgorithmException e) {
+//            e.printStackTrace();
+//        }
+////        keyGenerator.init(256, new SecureRandom(password.getBytes()));
+//        keyGenerator.init(256);
+//        // keyGenerator.init(128, new SecureRandom(password.getBytes()));
+//        return keyGenerator.generateKey();// .getEncoded();
+//        // return new SecretKeySpec(password.getBytes(),"AES");
+//    }
+    
+    public static SecretKey generateSecretKey(String aesKey) {
+        return new SecretKeySpec(aesKey.getBytes(),"AES");
+    }
+
+    /**
+     * 还原密钥
+     * 
+     * @param secretBytes
+     * @return
+     */
+    public static SecretKey restoreSecretKey(byte[] secretBytes) {
+        SecretKey secretKey = new SecretKeySpec(secretBytes, KEY_ALGORITHM);
+        return secretKey;
+    }
+}

+ 163 - 0
src/main/java/com/qmrb/system/common/util/AesUtils.java

@@ -0,0 +1,163 @@
+package com.qmrb.system.common.util;
+
+import java.security.MessageDigest;
+import java.util.Arrays;
+import java.util.Base64;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * AES加解密
+ *
+ */
+public class AesUtils {
+
+    /**
+     * 加密
+     *
+     * @param content
+     *            消息内容
+     * @param key
+     *            密钥
+     * @return 密文
+     * @throws Exception
+     *             抛异常
+     */
+    public static String encrypt(String content, String key) throws Exception {
+        if (key == null || key.length() != 16 || content == null) {
+            return null;
+        }
+        byte[] aesKey = key.getBytes("UTF-8");
+        SecretKeySpec skeySpec = new SecretKeySpec(aesKey, "AES");
+        // 使用CBC模式,需要一个向量iv,可增加加密算法的强度
+        IvParameterSpec iv = new IvParameterSpec(aesKey, 0, 16);
+//        System.out.println("x"+new String(iv.getIV()));
+        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+        cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);
+        byte[] encrypted = cipher.doFinal(content.getBytes("UTF-8"));
+        // 此处使用BASE64做转码。
+        return Base64.getEncoder().encodeToString(encrypted);
+    }
+
+    /**
+     * 解密
+     *
+     * @param content
+     *            密文
+     * @param key
+     *            密钥
+     * @return 消息内容
+     * @throws Exception
+     *             抛异常
+     */
+    public static String decrypt(String content, String key) throws Exception {
+        if (key == null || key.length() != 16 || content == null) {
+            return null;
+        }
+        content = formatContent(content);
+
+        byte[] aesKey = key.getBytes("UTF-8");
+        SecretKeySpec skeySpec = new SecretKeySpec(aesKey, "AES");
+        // 使用CBC模式,需要一个向量iv,可增加加密算法的强度
+        IvParameterSpec iv = new IvParameterSpec(aesKey, 0, 16);
+        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+        cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);
+        // 先用base64解密
+        byte[] encrypted1 = Base64.getDecoder().decode(content);
+        byte[] original = cipher.doFinal(encrypted1);
+        String originalString = new String(original, "UTF-8");
+        return originalString;
+    }
+
+    /**
+     * 格式化加密的消息体去除换行符
+     *
+     * @param content
+     *            加密的消息体
+     * @return 格式化后加密的消息体
+     */
+    public static String formatContent(String content) {
+        String s = content;
+        // 1.去掉引号
+        if (content.contains("\"")) {
+            s = s.substring(1, s.length() - 1);
+        }
+        // 2.替换\r已经被转义的字符
+        if (content.contains("\\r")) {
+            s = s.replace("\\r", "\r");
+        }
+
+        // 3.替换\n已经被转义的字符
+        if (content.contains("\\n")) {
+            s = s.replace("\\n", "\n");
+        }
+        return s;
+    }
+
+    /**
+     * 用SHA1算法生成安全签名
+     *
+     * @param token
+     *            票据
+     * @param timestamp
+     *            时间戳
+     * @param nonce
+     *            随机字符串
+     * @param encrypt
+     *            密文
+     * @return 安全签名
+     * @throws Exception
+     */
+    public static String getSHA1(String token, String timestamp, String nonce, String encrypt) throws Exception {
+        String[] array = new String[] { token, timestamp, nonce, encrypt };
+        StringBuilder sb = new StringBuilder();
+        // 字符串排序
+        Arrays.sort(array);
+        for (int i = 0; i < 4; i++) {
+            sb.append(array[i]);
+        }
+        String str = sb.toString();
+        // SHA1签名生成
+        MessageDigest md = MessageDigest.getInstance("SHA-1");
+        md.update(str.getBytes());
+        byte[] digest = md.digest();
+
+        StringBuilder hexstr = new StringBuilder();
+        String shaHex = "";
+        for (int i = 0; i < digest.length; i++) {
+            shaHex = Integer.toHexString(digest[i] & 0xFF);
+            if (shaHex.length() < 2) {
+                hexstr.append(0);
+            }
+            hexstr.append(shaHex);
+        }
+        return hexstr.toString();
+    }
+
+    public static void main(String[] args) {
+        try {
+            String data = encrypt("350621196709296577","89H5q5EZSMFLVYJV");//350524199802267428
+            System.out.println(data);
+            System.out.println(decrypt("bwSU5ftkVc3cCmGSYj/VjZxY9kd/ZEKjZx8XNO+mIDk=","89H5q5EZSMFLVYJV"));
+            System.out.println();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+        /*try {
+            String data = encrypt("94728009","89H5q5EZSMFLVYJV");
+            System.out.println(data);
+            System.out.println(decrypt(data,"89H5q5EZSMFLVYJV"));
+
+            String data1 = encrypt("35020419630701652X","89H5q5EZSMFLVYJV");
+            System.out.println(data1);
+            System.out.println(decrypt(data1,"89H5q5EZSMFLVYJV"));
+
+            System.out.println(encrypt("51122119820","89H5q5EZSMFLVYJV"));
+            System.out.println(decrypt("84uq+l0y52nagxtWGab30AM9zVKTIrUR54P7oyL2fhs","89H5q5EZSMFLVYJV"));*/
+       /* } catch (Exception e) {
+            throw new RuntimeException(e);
+        }*/
+    }
+}

+ 246 - 0
src/main/java/com/qmrb/system/common/util/AuthUtils.java

@@ -0,0 +1,246 @@
+package com.qmrb.system.common.util;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.core.io.ClassPathResource;
+
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import javax.imageio.ImageIO;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Random;
+
+/**
+ * @author Gadfly
+ */
+@SuppressWarnings("SpellCheckingInspection")
+public class AuthUtils {
+
+    /**
+     * 验证码字符个数
+     */
+    public static final int RANDOM_STR_NUM = 4;
+    private static final Random RANDOM = new Random();
+    /**
+     * 验证码的宽
+     */
+    private static final int WIDTH = 80;
+    /**
+     * 验证码的高
+     */
+    private static final int HEIGHT = 40;
+    private static final String RANDOM_STRING = "23456789abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWSYZ";
+    private static final String AES = "AES";
+    private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    static {
+        java.security.Security.setProperty("crypto.policy", "unlimited");
+    }
+
+    /**
+     * 颜色的设置
+     */
+    private static Color getRandomColor(int fc, int bc) {
+
+        fc = Math.min(fc, 255);
+        bc = Math.min(bc, 255);
+
+        int r = fc + RANDOM.nextInt(bc - fc - 16);
+        int g = fc + RANDOM.nextInt(bc - fc - 14);
+        int b = fc + RANDOM.nextInt(bc - fc - 12);
+
+        return new Color(r, g, b);
+    }
+
+    /**
+     * 字体的设置
+     */
+    private static Font getFont() throws IOException, FontFormatException {
+        ClassPathResource dejavuSerifBold = new ClassPathResource("DejaVuSerif-Bold.ttf");
+        return Font.createFont(Font.TRUETYPE_FONT, dejavuSerifBold.getInputStream()).deriveFont(Font.BOLD, 24);
+    }
+
+    /**
+     * 随机字符的获取
+     */
+    public static String getRandomString(int num) {
+        num = num > 0 ? num : RANDOM_STRING.length();
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < num; i++) {
+            int number = RANDOM.nextInt(RANDOM_STRING.length());
+            sb.append(RANDOM_STRING.charAt(number));
+        }
+        return sb.toString();
+    }
+
+    /**
+     * 干扰线的绘制
+     */
+    private static void drawLine(Graphics2D g) {
+        int x = RANDOM.nextInt(WIDTH);
+        int y = RANDOM.nextInt(HEIGHT);
+        int xl = WIDTH;
+        int yl = HEIGHT;
+        g.setStroke(new BasicStroke(2.0f));
+        g.setColor(getRandomColor(98, 200));
+        g.drawLine(x, y, x + xl, y + yl);
+    }
+
+    /**
+     * 字符串的绘制
+     */
+    private static void drawString(Graphics2D g, String randomStr, int i) throws IOException, FontFormatException {
+        g.setFont(getFont());
+        g.setColor(getRandomColor(28, 130));
+        // 设置每个字符的随机旋转
+        double radianPercent = (RANDOM.nextBoolean() ? -1 : 1) * Math.PI * (RANDOM.nextInt(60) / 320D);
+        g.rotate(radianPercent, WIDTH * 0.8 / RANDOM_STR_NUM * i, HEIGHT / 2);
+        int y = (RANDOM.nextBoolean() ? -1 : 1) * RANDOM.nextInt(4) + 4;
+        g.translate(RANDOM.nextInt(3), y);
+        g.drawString(randomStr, WIDTH / RANDOM_STR_NUM * i, HEIGHT / 2);
+        g.rotate(-radianPercent, WIDTH * 0.8 / RANDOM_STR_NUM * i, HEIGHT / 2);
+        g.translate(0, -y);
+    }
+
+    private static BufferedImage getBufferedImage(String code) throws IOException, FontFormatException {
+        // BufferedImage类是具有缓冲区的Image类,Image类是用于描述图像信息的类
+        BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_BGR);
+        Graphics2D g = (Graphics2D) image.getGraphics();
+        g.fillRect(0, 0, WIDTH, HEIGHT);
+        g.setColor(getRandomColor(105, 189));
+        g.setFont(getFont());
+        int lineSize = RANDOM.nextInt(5);
+        // 干扰线
+        for (int i = 0; i < lineSize; i++) {
+            drawLine(g);
+        }
+        // 随机字符
+        for (int i = 0; i < code.length(); i++) {
+            drawString(g, String.valueOf(code.charAt(i)), i);
+        }
+        g.dispose();
+        return image;
+    }
+
+    /**
+     * 生成随机图片的base64编码字符串
+     *
+     * @param code 验证码
+     * @return base64
+     */
+    public static String getRandomCodeBase64(String code) throws IOException, FontFormatException {
+        BufferedImage image = getBufferedImage(code);
+        // 返回 base64
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        ImageIO.write(image, "PNG", bos);
+
+        byte[] bytes = bos.toByteArray();
+        Base64.Encoder encoder = Base64.getEncoder();
+
+        return encoder.encodeToString(bytes);
+    }
+
+
+
+    /**
+     * AES加密
+     */
+    public static String aesEncode(String secret, String iv, String content) throws GeneralSecurityException {
+        SecretKey secretKey = new SecretKeySpec(secret.getBytes(), AES);
+        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+        cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv.getBytes(StandardCharsets.US_ASCII)));
+        byte[] byteEncode = content.getBytes(StandardCharsets.UTF_8);
+        // 根据密码器的初始化方式加密
+        byte[] byteAES = cipher.doFinal(byteEncode);
+
+        // 将加密后的数据转换为字符串
+        return Base64.getEncoder().encodeToString(byteAES);
+    }
+
+    /**
+     * AES解密
+     */
+    public static String aesDecode(String secret, String iv, String content) throws GeneralSecurityException {
+        SecretKey secretKey = new SecretKeySpec(secret.getBytes(), AES);
+        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+        cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv.getBytes(StandardCharsets.US_ASCII)));
+        // 将加密并编码后的内容解码成字节数组
+        byte[] byteContent = Base64.getDecoder().decode(content);
+        // 解密
+        byte[] byteDecode = cipher.doFinal(byteContent);
+        return new String(byteDecode, StandardCharsets.UTF_8);
+    }
+
+    /**
+     * 格式化加密的消息体去除换⾏符
+     * @param content
+     * @return
+     */
+    public static String formatContent(String content) {
+        String s = content;
+        // 1.去掉引号
+        /*if (content.contains("\"")) {
+            s = s.substring(1, s.length() - 1);
+        }*/
+        // 2.替换\r已经被转义的字符
+        if (content.contains("\\r")) {
+            s = s.replace("\\r", "\r");
+        }
+
+        // 3.替换\n已经被转义的字符
+        if (content.contains("\\n")) {
+            s = s.replace("\\n", "\n");
+        }
+        return s;
+    }
+
+    /**
+     * ⽤SHA1算法⽣成安全签名
+     * @param authorization
+     * @param timestamp
+     * @param nonce
+     * @param encrypt
+     * @return
+     * @throws Exception
+     */
+    public static String getSHA1(String authorization, String timestamp, String nonce, String encrypt) throws Exception {
+        String[] array = new String[] { authorization, timestamp, nonce, encrypt };
+        StringBuilder sb = new StringBuilder();
+        // 字符串排序
+        Arrays.sort(array);
+        for (int i = 0; i < 4; i++) { sb.append(array[i]);
+        }
+        String str = sb.toString();
+        // SHA1签名⽣成
+        MessageDigest md = MessageDigest.getInstance("SHA-1"); md.update(str.getBytes());
+        byte[] digest = md.digest();
+
+        StringBuilder hexstr = new StringBuilder(); String shaHex = "";
+        for (int i = 0; i < digest.length; i++) {
+            shaHex = Integer.toHexString(digest[i] & 0xFF); if (shaHex.length() < 2) {
+                hexstr.append(0);
+            }
+            hexstr.append(shaHex);
+        }
+        return hexstr.toString();
+    }
+
+    public static void main(String[] args) throws GeneralSecurityException {
+        System.out.println(getRandomString(16));
+//        String data = aesEncode("1q2w3e4r","a979ed23","test");
+//        System.out.println(data);
+    }
+}

+ 23 - 0
src/main/java/com/qmrb/system/common/util/Base64Utils.java

@@ -0,0 +1,23 @@
+package com.qmrb.system.common.util;
+
+import java.io.*;
+import java.util.Base64;
+
+public class Base64Utils {
+    private static final int CACHE_SIZE = 1024;
+    private static final Base64.Decoder DECODER = Base64.getDecoder();
+    private static final Base64.Encoder ENCODER = Base64.getEncoder();
+
+    public Base64Utils() {
+    }
+
+    public static byte[] decode(String base64) {
+        return DECODER.decode(base64.getBytes());
+    }
+
+    public static String encode(byte[] bytes) {
+        return new String(ENCODER.encode(bytes));
+    }
+
+
+}

+ 21 - 0
src/main/java/com/qmrb/system/common/util/ExcelUtils.java

@@ -0,0 +1,21 @@
+package com.qmrb.system.common.util;
+
+import com.alibaba.excel.EasyExcel;
+import com.qmrb.system.framework.easyexcel.MyAnalysisEventListener;
+
+import java.io.InputStream;
+
+/**
+ * Excel 工具类
+ *
+ * @author: haoxr
+ * @date: 2023/03/01
+ */
+public class ExcelUtils {
+
+    public static <T> String importExcel(InputStream is, Class<?> clazz, MyAnalysisEventListener<T> listener) {
+        EasyExcel.read(is, clazz, listener).sheet().doRead();
+        String msg = listener.getMsg();
+        return msg;
+    }
+}

+ 261 - 0
src/main/java/com/qmrb/system/common/util/RSAUtil.java

@@ -0,0 +1,261 @@
+package com.qmrb.system.common.util;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.InvocationTargetException;
+import java.security.Key;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.Security;
+import java.security.Signature;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+import javax.crypto.Cipher;
+import java.io.ObjectInputStream;
+
+import org.apache.commons.codec.binary.Base64;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+
+public class RSAUtil {
+    public static final String KEY_ALGORITHM = "RSA";
+    public static final String SIGNATURE_ALGORITHM = "MD5withRSA";
+    private static final String PUBLIC_KEY = "RSAPublicKey";
+    private static final String PRIVATE_KEY = "RSAPrivateKey";
+    private static final int MAX_ENCRYPT_BLOCK = 117;
+    private static final int MAX_DECRYPT_BLOCK = 128;
+
+    public RSAUtil() {
+    }
+
+    public static Map<String, Object> genKeyPair() {
+        KeyPairGenerator keyPairGen = null;
+        try {
+            keyPairGen = KeyPairGenerator.getInstance("RSA");
+        } catch (NoSuchAlgorithmException var5) {
+            var5.printStackTrace();
+        }
+
+        keyPairGen.initialize(1024);
+        KeyPair keyPair = keyPairGen.generateKeyPair();
+        RSAPublicKey publicKey = (RSAPublicKey)keyPair.getPublic();
+        RSAPrivateKey privateKey = (RSAPrivateKey)keyPair.getPrivate();
+        Map<String, Object> keyMap = new HashMap(2);
+        keyMap.put("RSAPublicKey", publicKey);
+        keyMap.put("RSAPrivateKey", privateKey);
+        return keyMap;
+    }
+
+    public static String sign(byte[] data, String privateKey) {
+        byte[] keyBytes = Base64Utils.decode(privateKey);
+        PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
+
+        try {
+            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+            PrivateKey privateK = keyFactory.generatePrivate(pkcs8KeySpec);
+            Signature signature = Signature.getInstance("MD5withRSA");
+            signature.initSign(privateK);
+            signature.update(data);
+            return Base64Utils.encode(signature.sign());
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    public static boolean verify(byte[] data, String publicKey, String sign) {
+        byte[] keyBytes = Base64Utils.decode(publicKey);
+        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
+
+        try {
+            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+            PublicKey publicK = keyFactory.generatePublic(keySpec);
+            Signature signature = Signature.getInstance("MD5withRSA");
+            signature.initVerify(publicK);
+            signature.update(data);
+            return signature.verify(Base64Utils.decode(sign));
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return true;
+    }
+
+    public static byte[] decryptByPrivateKey(byte[] encryptedData, String privateKey) {
+        byte[] keyBytes = Base64Utils.decode(privateKey);
+        PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
+
+        try {
+            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+            Key privateK = keyFactory.generatePrivate(pkcs8KeySpec);
+            Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
+            cipher.init(2, privateK);
+            int inputLen = encryptedData.length;
+            ByteArrayOutputStream out = new ByteArrayOutputStream();
+            int offSet = 0;
+
+            for(int i = 0; inputLen - offSet > 0; offSet = i * 128) {
+                byte[] cache;
+                if (inputLen - offSet > 128) {
+                    cache = cipher.doFinal(encryptedData, offSet, 128);
+                } else {
+                    cache = cipher.doFinal(encryptedData, offSet, inputLen - offSet);
+                }
+
+                out.write(cache, 0, cache.length);
+                ++i;
+            }
+
+            byte[] decryptedData = out.toByteArray();
+            return decryptedData;
+        } catch (Exception var13) {
+
+        }
+        return null;
+    }
+
+    public static byte[] decryptByPublicKey(byte[] encryptedData, String publicKey) {
+        byte[] keyBytes = Base64Utils.decode(publicKey);
+        X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(keyBytes);
+
+        try {
+            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+            Key publicK = keyFactory.generatePublic(x509KeySpec);
+            Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
+            cipher.init(2, publicK);
+            int inputLen = encryptedData.length;
+            ByteArrayOutputStream out = new ByteArrayOutputStream();
+            int offSet = 0;
+
+            for(int i = 0; inputLen - offSet > 0; offSet = i * 128) {
+                byte[] cache;
+                if (inputLen - offSet > 128) {
+                    cache = cipher.doFinal(encryptedData, offSet, 128);
+                } else {
+                    cache = cipher.doFinal(encryptedData, offSet, inputLen - offSet);
+                }
+
+                out.write(cache, 0, cache.length);
+                ++i;
+            }
+
+            byte[] decryptedData = out.toByteArray();
+            return decryptedData;
+        } catch (Exception var13) {
+        }
+        return null;
+    }
+
+    public static byte[] encryptByPublicKey(byte[] data, String publicKey) {
+        byte[] keyBytes = Base64Utils.decode(publicKey);
+        X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(keyBytes);
+
+        try {
+            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+            Key publicK = keyFactory.generatePublic(x509KeySpec);
+            Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
+            cipher.init(1, publicK);
+            int inputLen = data.length;
+            ByteArrayOutputStream out = new ByteArrayOutputStream();
+            int offSet = 0;
+
+            for(int i = 0; inputLen - offSet > 0; offSet = i * 117) {
+                byte[] cache;
+                if (inputLen - offSet > 117) {
+                    cache = cipher.doFinal(data, offSet, 117);
+                } else {
+                    cache = cipher.doFinal(data, offSet, inputLen - offSet);
+                }
+
+                out.write(cache, 0, cache.length);
+                ++i;
+            }
+
+            byte[] encryptedData = out.toByteArray();
+            return encryptedData;
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    public static byte[] encryptByPrivateKey(byte[] data, String privateKey) {
+        byte[] keyBytes = Base64Utils.decode(privateKey);
+        PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
+
+        try {
+            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+            Key privateK = keyFactory.generatePrivate(pkcs8KeySpec);
+            Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
+            cipher.init(1, privateK);
+            int inputLen = data.length;
+            ByteArrayOutputStream out = new ByteArrayOutputStream();
+            int offSet = 0;
+
+            for(int i = 0; inputLen - offSet > 0; offSet = i * 117) {
+                byte[] cache;
+                if (inputLen - offSet > 117) {
+                    cache = cipher.doFinal(data, offSet, 117);
+                } else {
+                    cache = cipher.doFinal(data, offSet, inputLen - offSet);
+                }
+
+                out.write(cache, 0, cache.length);
+                ++i;
+            }
+
+            byte[] encryptedData = out.toByteArray();
+            return encryptedData;
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    public static String getPrivateKey(Map<String, Object> keyMap) {
+        Key key = (Key)keyMap.get("RSAPrivateKey");
+        return new String(Base64Utils.encode(key.getEncoded()));
+    }
+
+    public static String getPublicKey(Map<String, Object> keyMap) {
+        Key key = (Key)keyMap.get("RSAPublicKey");
+        return new String(Base64Utils.encode(key.getEncoded()));
+    }
+
+    static {
+        Security.addProvider(new BouncyCastleProvider());
+    }
+
+    public static void main(String[] args) {
+        String authKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC3//sR2tXw0wrC2DySx8vNGlqt3Y7ldU9+LBLI6e1KS5lfc5jlTGF7KBTSkCHBM3ouEHWqp1ZJ85iJe59aF5gIB2klBd6h4wrbbHA2XE1sq21ykja/Gqx7/IRia3zQfxGv/qEkyGOx+XALVoOlZqDwh76o2n1vP1D+tD3amHsK7QIDAQAB";
+        String authInfo = "S18+/CKG59rR/U7VuDcgR1AP4HTzXOYR7h5s5SDlFCu9SzHdO0i8Oxqvf+tjr3gKFZneMt9ihTc2jNpfHSWFTvWyZFDnPCcH/Sky602UAndtpXamUfuBZyovQkHgRm7M4hDHnFyxAGZVhzX3EuRH08n+pBlv6Q8k8h2aBJKPaHg=";
+        byte[] bytes = RSAUtil.decryptByPublicKey(Base64.decodeBase64(authInfo), authKey);
+        byte[] bytes2 = RSAUtil.decryptByPublicKey(Base64.decodeBase64("ANpLvgDdPoHrrQ41XhVtwTHq8F4QfWBGz4HpFpuzvoVbkkAgvc1aNE/oN9GTe2NNWfNTaBVA3ksiQ8AYpWJCbcK+c5w0YgeUjyGmDurowlSkBOsKCEtugcS0157W5Yh1M6hlgTDwb09hpVik6tHsOOe43tmIKM/c9UQC87XPoA0="), authKey);
+        try {
+            String s = new String(bytes, "UTF-8");
+            System.out.println(s);
+            s = new String(bytes2, "UTF-8");
+            System.out.println(s);
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException(e);
+        }
+        byte[] bytes1 = new byte[0];
+          String privateKey = "MIICXQIBAAKBgQC3//sR2tXw0wrC2DySx8vNGlqt3Y7ldU9+LBLI6e1KS5lfc5jlTGF7KBTSkCHBM3ouEHWqp1ZJ85iJe59aF5gIB2klBd6h4wrbbHA2XE1sq21ykja/Gqx7/IRia3zQfxGv/qEkyGOx+XALVoOlZqDwh76o2n1vP1D+tD3amHsK7QIDAQABAoGBAKH14bMitESqD4PYwODWmy7rrrvyFPEnJJTECLjvKB7IkrVxVDkp1XiJnGKH2h5syHQ5qslPSGYJ1M/XkDnGINwaLVHVD3BoKKgKg1bZn7ao5pXT+herqxaVwWs6ga63yVSIC8jcODxiuvxJnUMQRLaqoF6aUb/2VWc2T5MDmxLhAkEA3pwGpvXgLiWL3h7QLYZLrLrbFRuRN4CYl4UYaAKokkAvZly04Glle8ycgOc2DzL4eiL4l/+x/gaqdeJU/cHLRQJBANOZY0mEoVkwhU4bScSdnfM6usQowYBEwHYYh/OTv1a3SqcCE1f+qbAclCqeNiHajCcDmgYJ53LfIgyv0wCS54kCQAXaPkaHclRkQlAdqUV5IWYyJ25foiq+Y8SgCCs73qixrU1YpJy9yKA/meG9smsl4Oh9IOIGI+zUygh9YdSmEq0CQQC24G3IP2G3lNDRdZIm5NZ7PfnmyRabxk/UgVUWdk47IwTZHFkdhxKfC8QepUhBsAHLQjifGXY4eJKUBm3FpDGJAkAFwUxYssiJjvrHwnHFbg0rFkvvY63OSmnRxiL4X6EYyI9lblCsyfpl25l7l5zmJrAHn45zAiOoBrWqpM5edu7c";
+
+        try {
+            bytes1 = RSAUtil.encryptByPrivateKey("a:2:{s:4:\"date\";s:10:\"2026-12-15\";s:6:\"domain\";s:21:\"8.138.18.76,localhost,\";}".getBytes("UTF-8"), privateKey);
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException(e);
+        }
+        String encode = Base64Utils.encode(bytes1);
+        System.out.println(encode);
+    }
+}

+ 25 - 0
src/main/java/com/qmrb/system/common/util/RequestUtils.java

@@ -0,0 +1,25 @@
+package com.qmrb.system.common.util;
+
+import com.qmrb.system.common.constant.SecurityConstants;
+
+import cn.hutool.core.util.StrUtil;
+import jakarta.servlet.http.HttpServletRequest;
+
+/**
+ * 请求工具类
+ *
+ * @author haoxr
+ */
+public class RequestUtils {
+
+    /**
+     * 请求头解析获取 Token
+     */
+    public static String resolveToken(HttpServletRequest request) {
+        String bearerToken = request.getHeader(SecurityConstants.TOKEN_KEY);
+        if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith(SecurityConstants.TOKEN_PREFIX)) {
+            return bearerToken.substring(SecurityConstants.TOKEN_PREFIX.length());
+        }
+        return null;
+    }
+}

+ 46 - 0
src/main/java/com/qmrb/system/common/util/ResponseUtils.java

@@ -0,0 +1,46 @@
+package com.qmrb.system.common.util;
+
+import cn.hutool.json.JSONUtil;
+import com.qmrb.system.common.result.Result;
+import com.qmrb.system.common.result.ResultCode;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * 响应工具类
+ *
+ * @author haoxr
+ * @date 2022/10/18
+ */
+public class ResponseUtils {
+
+    /**
+     * 异常消息返回(适用过滤器中处理异常响应)
+     *
+     * @param response
+     * @param resultCode
+     */
+    public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) throws IOException {
+        switch (resultCode) {
+            case ACCESS_UNAUTHORIZED:
+            case TOKEN_INVALID:
+                response.setStatus(HttpStatus.UNAUTHORIZED.value());
+                break;
+            case TOKEN_ACCESS_FORBIDDEN:
+                response.setStatus(HttpStatus.FORBIDDEN.value());
+                break;
+            default:
+                response.setStatus(HttpStatus.BAD_REQUEST.value());
+                break;
+        }
+        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+        response.setCharacterEncoding("UTF-8");
+        response.getWriter().print(JSONUtil.toJsonStr(Result.failed(resultCode)));
+    }
+
+
+}

+ 45 - 0
src/main/java/com/qmrb/system/config/CorsConfig.java

@@ -0,0 +1,45 @@
+package com.qmrb.system.config;
+
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+import org.springframework.web.filter.CorsFilter;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+/**
+ * CORS资源共享配置
+ *
+ * @author haoxr
+ * @date 2023/4/17
+ */
+@Configuration
+public class CorsConfig {
+
+    @SuppressWarnings("rawtypes")
+	@Bean
+    public FilterRegistrationBean filterRegistrationBean() {
+        CorsConfiguration corsConfiguration = new CorsConfiguration();
+        //1.允许任何来源
+        corsConfiguration.setAllowedOriginPatterns(Arrays.asList("http://172.16.1.159:8080","http://192.10.33.95:8080","http://localhost:8080",
+                "http://localhost","https://172.16.1.159","https://etc.xmzsh.com","http://localhost:3000","http://192.0.33.228*","http://192.0.33.229*","http://192.6.33.187*","http://27.154.61.205*"));
+        //2.允许任何请求头
+        corsConfiguration.addAllowedHeader(CorsConfiguration.ALL);
+        //3.允许任何方法
+        corsConfiguration.addAllowedMethod(CorsConfiguration.ALL);
+        //4.允许凭证
+        corsConfiguration.setAllowCredentials(false);
+
+        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+        source.registerCorsConfiguration("/**", corsConfiguration);
+        CorsFilter corsFilter = new CorsFilter(source);
+
+        FilterRegistrationBean<CorsFilter> filterRegistrationBean=new FilterRegistrationBean<>(corsFilter);
+        filterRegistrationBean.setOrder(-101);  // 小于 SpringSecurity Filter的 Order(-100) 即可
+
+        return filterRegistrationBean;
+    }
+}

+ 20 - 0
src/main/java/com/qmrb/system/config/InsertBatchSqlInjector.java

@@ -0,0 +1,20 @@
+package com.qmrb.system.config;
+
+import java.util.List;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.core.injector.AbstractMethod;
+import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector;
+import com.baomidou.mybatisplus.core.metadata.TableInfo;
+import com.baomidou.mybatisplus.extension.injector.methods.InsertBatchSomeColumn;
+
+public class InsertBatchSqlInjector extends DefaultSqlInjector {
+	
+	@Override
+    public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {
+        // 注意:此SQL注入器继承了DefaultSqlInjector(默认注入器),调用了DefaultSqlInjector的getMethodList方法,保留了mybatis-plus的自带方法
+        List<AbstractMethod> methodList = super.getMethodList(mapperClass, tableInfo);
+        methodList.add(new InsertBatchSomeColumn(i -> i.getFieldFill() != FieldFill.UPDATE));
+        return methodList;
+    }
+}

+ 60 - 0
src/main/java/com/qmrb/system/config/MybatisPlusConfig.java

@@ -0,0 +1,60 @@
+package com.qmrb.system.config;
+
+import com.baomidou.mybatisplus.annotation.DbType;
+import com.baomidou.mybatisplus.core.config.GlobalConfig;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
+import com.qmrb.system.framework.mybatisplus.MyDataPermissionHandler;
+import com.qmrb.system.framework.mybatisplus.MyMetaObjectHandler;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+/**
+ * mybatis-plus 配置类
+ *
+ * @author haoxr
+ * @since 2022/7/2
+ */
+@Configuration
+@EnableTransactionManagement
+public class MybatisPlusConfig {
+
+
+    @Value("${system-config.data-permission.enabled}")
+    private Boolean dataPermissionEnabled;
+
+    /**
+     * 分页插件和数据权限插件
+     */
+    @Bean
+    public MybatisPlusInterceptor mybatisPlusInterceptor() {
+        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
+        //数据权限
+        if (dataPermissionEnabled) {
+            interceptor.addInnerInterceptor(new DataPermissionInterceptor(new MyDataPermissionHandler()));
+        }
+        //分页插件
+        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
+
+        return interceptor;
+    }
+
+    /**
+     * 自动填充数据库创建人、创建时间、更新人、更新时间
+     */
+    @Bean
+    public GlobalConfig globalConfig() {
+        GlobalConfig globalConfig = new GlobalConfig();
+        globalConfig.setMetaObjectHandler(new MyMetaObjectHandler());
+        return globalConfig;
+    }
+
+    @Bean
+    public InsertBatchSqlInjector insertBatchSqlInjector() {
+        return new InsertBatchSqlInjector();
+    }
+
+}

+ 42 - 0
src/main/java/com/qmrb/system/config/RedisConfig.java

@@ -0,0 +1,42 @@
+package com.qmrb.system.config;
+
+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.serializer.RedisSerializer;
+
+/**
+ * Redis 配置
+ *
+ * @author haoxr
+ * @since 2023/5/15
+ */
+@Configuration
+public class RedisConfig {
+
+    /**
+     * 自定义 RedisTemplate
+     * <p>
+     * 修改 Redis 序列化方式,默认 JdkSerializationRedisSerializer
+     *
+     * @param redisConnectionFactory {@link RedisConnectionFactory}
+     * @return {@link RedisTemplate}
+     */
+    @Bean
+    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
+
+        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
+        redisTemplate.setConnectionFactory(redisConnectionFactory);
+
+        redisTemplate.setKeySerializer(RedisSerializer.string());
+        redisTemplate.setValueSerializer(RedisSerializer.json());
+
+        redisTemplate.setHashKeySerializer(RedisSerializer.string());
+        redisTemplate.setHashValueSerializer(RedisSerializer.json());
+
+        redisTemplate.afterPropertiesSet();
+        return redisTemplate;
+    }
+
+}

+ 59 - 0
src/main/java/com/qmrb/system/config/SwaggerConfig.java

@@ -0,0 +1,59 @@
+package com.qmrb.system.config;
+
+import org.springdoc.core.models.GroupedOpenApi;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import io.swagger.v3.oas.models.Components;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.info.Info;
+import io.swagger.v3.oas.models.info.License;
+import io.swagger.v3.oas.models.security.SecurityScheme;
+
+@Configuration
+public class SwaggerConfig {
+
+    /**
+     * 接口信息
+     */
+    @Bean
+    public OpenAPI apiInfo() {
+        return new OpenAPI()
+                .components(new Components()
+                        .addSecuritySchemes("Authorization",
+                                new SecurityScheme().type(SecurityScheme.Type.HTTP)
+                                        .scheme("bearer").bearerFormat("JWT")
+                        )
+                )
+                .info(new Info()
+                        .title("接口文档")
+                        .version("2.0.0")
+                        .description("接口文档")
+                        .license(new License().name("Apache 2.0")
+                                .url("https://www.qmrb.tech"))
+                );
+    }
+
+    /**
+     * 接口分组-系统接口
+     *
+     * @return
+     */
+    @Bean
+    public GroupedOpenApi groupedOpenApi() {
+        String paths[] = {"/api/**"};
+
+        String packagesToScan[] = {"com.qmrb.system.controller"};
+        return GroupedOpenApi.builder().group("系统接口")
+                .packagesToScan(packagesToScan)
+                .pathsToMatch(paths)
+                .build();
+    }
+
+
+
+
+
+
+
+}

+ 60 - 0
src/main/java/com/qmrb/system/config/WebMvcConfig.java

@@ -0,0 +1,60 @@
+package com.qmrb.system.config;
+
+import java.math.BigInteger;
+import java.text.SimpleDateFormat;
+import java.util.List;
+import java.util.TimeZone;
+
+import org.hibernate.validator.HibernateValidator;
+import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.converter.StringHttpMessageConverter;
+import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
+import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+
+import jakarta.validation.Validation;
+import jakarta.validation.Validator;
+import jakarta.validation.ValidatorFactory;
+
+@Configuration
+public class WebMvcConfig implements WebMvcConfigurer {
+
+    @Override
+    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
+        converters.add(new StringHttpMessageConverter());
+
+        MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
+        ObjectMapper objectMapper = jackson2HttpMessageConverter.getObjectMapper();
+        objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
+        objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8"));
+        objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
+
+        // 后台Long值传递给前端精度丢失问题(JS最大精度整数是Math.pow(2,53))
+        SimpleModule simpleModule = new SimpleModule();
+        simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
+        simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance);
+        objectMapper.registerModule(simpleModule);
+
+        jackson2HttpMessageConverter.setObjectMapper(objectMapper);
+        converters.add(jackson2HttpMessageConverter);
+    }
+
+    @Bean
+    public Validator validator(final AutowireCapableBeanFactory autowireCapableBeanFactory) {
+        ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
+                .configure()
+                .failFast(true) // failFast=true 不校验所有参数,只要出现校验失败情况直接返回,不再进行后续参数校验
+                .constraintValidatorFactory(new SpringConstraintValidatorFactory(autowireCapableBeanFactory))
+                .buildValidatorFactory();
+
+        return validatorFactory.getValidator();
+    }
+}

+ 16 - 0
src/main/java/com/qmrb/system/config/WebSocketConfig.java

@@ -0,0 +1,16 @@
+package com.qmrb.system.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.socket.config.annotation.EnableWebSocket;
+import org.springframework.web.socket.server.standard.ServerEndpointExporter;
+
+@Configuration
+@EnableWebSocket
+public class WebSocketConfig {
+
+    @Bean
+    public ServerEndpointExporter serverEndpoint() {
+        return new ServerEndpointExporter();
+    }
+}

+ 329 - 0
src/main/java/com/qmrb/system/controller/AuthController.java

@@ -0,0 +1,329 @@
+package com.qmrb.system.controller;
+
+import java.io.IOException;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.qmrb.system.framework.security.CaConstants;
+import com.qmrb.system.pojo.dto.SoapReturnDTO;
+import com.qmrb.system.pojo.dto.SoapTemplateDTO;
+import lombok.extern.slf4j.Slf4j;
+import org.checkerframework.common.value.qual.StringVal;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.web.bind.annotation.DeleteMapping;
+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.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.qmrb.system.common.constant.SecurityConstants;
+import com.qmrb.system.common.result.Result;
+import com.qmrb.system.common.util.RequestUtils;
+import com.qmrb.system.framework.easycaptcha.service.EasyCaptchaService;
+import com.qmrb.system.framework.security.JwtTokenManager;
+import com.qmrb.system.framework.security.userdetails.SysUserDetails;
+import com.qmrb.system.pojo.bo.UserAuthInfo;
+import com.qmrb.system.pojo.dto.CaptchaResult;
+import com.qmrb.system.pojo.dto.LoginResult;
+import com.qmrb.system.pojo.entity.SysUser;
+import com.qmrb.system.service.SoapService;
+import com.qmrb.system.service.SysRoleService;
+import com.qmrb.system.service.SysUserRoleService;
+import com.qmrb.system.service.SysUserService;
+import com.qmrb.system.utils.WebServiceUtil;
+
+import cn.hutool.core.util.StrUtil;
+import io.jsonwebtoken.Claims;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import org.xml.sax.SAXException;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.xpath.XPathExpressionException;
+
+@Tag(name = "01.认证中心")
+@RestController
+@RequestMapping("/api/v1/auth")
+@RequiredArgsConstructor
+@Slf4j
+public class AuthController {
+    private final AuthenticationManager authenticationManager;
+    private final JwtTokenManager jwtTokenManager;
+    private final EasyCaptchaService easyCaptchaService;
+    private final SoapService soapService;
+
+    @SuppressWarnings("rawtypes")
+    private final RedisTemplate redisTemplate;
+    @Autowired
+    SysRoleService roleService;
+    @Autowired
+    SysUserRoleService userRoleService;
+    @Autowired
+    SysUserService userService;
+    @Value("${application.qr-url}")
+    private String qrUrl;
+
+    @Operation(summary = "登录")
+    @PostMapping("/login")
+    public Result<LoginResult> login(
+            @Parameter(description = "用户名", example = "admin") @RequestParam String username,
+            @Parameter(description = "密码", example = "123456") @RequestParam String password) throws Exception {
+        //超级管理员登录
+        UserAuthInfo userAuthInfoCheck = userService.getUserAuthInfo(username);
+        boolean isLocalLogin = false;
+        if(userAuthInfoCheck !=null ){
+            if(userAuthInfoCheck.getRoles().contains("MBGLY")){
+                isLocalLogin =  true;
+            }
+        }
+        if (CaConstants.ROOT_USER.equals(username)|| CaConstants.ADMIN_USER.equals(username)|| isLocalLogin){
+            LoginResult loginResult = doLogin(username,password);
+            if(loginResult == null ){
+                return Result.failed("登录失败");
+            }else {
+                return Result.success(loginResult);
+            }
+        }else{//医生登录
+            SysUser doctor = soapService.loadUserBySoapWithUserName(username, password);
+            if (doctor == null) {
+                return Result.failed("用户名不存在");
+            }
+
+            List<SimpleGrantedAuthority> roleList = userService.getUserAuthInfo(doctor.getUsername()).getRoles()
+                    .stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).toList();
+            UsernamePasswordAuthenticationToken authenticationToken =
+                    new UsernamePasswordAuthenticationToken(username, password,
+                            roleList);
+            UserAuthInfo userAuthInfo = userService.getUserAuthInfo(doctor.getUsername());
+            SysUserDetails sysUserDetails = new SysUserDetails(userAuthInfo);
+            authenticationToken.setDetails(sysUserDetails);
+            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
+            String accessToken = jwtTokenManager.createToken(authenticationToken);
+            LoginResult loginResult = LoginResult.builder()
+                    .tokenType("Bearer")
+                    .accessToken(accessToken)
+                    .build();
+            return Result.success(loginResult);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    @Operation(summary = "注销", security = {@SecurityRequirement(name = SecurityConstants.TOKEN_KEY)})
+    @DeleteMapping("/logout")
+    public Result<?> logout(HttpServletRequest request) {
+        String token = RequestUtils.resolveToken(request);
+        if (StrUtil.isNotBlank(token)) {
+            Claims claims = jwtTokenManager.getTokenClaims(token);
+            String jti = claims.get("jti", String.class);
+
+            Date expiration = claims.getExpiration();
+            if (expiration != null) {
+                // 有过期时间,在token有效时间内存入黑名单,超出时间移除黑名单节省内存占用
+                long ttl = (expiration.getTime() - System.currentTimeMillis());
+                redisTemplate.opsForValue().set(SecurityConstants.BLACK_TOKEN_CACHE_PREFIX + jti, null, ttl, TimeUnit.MILLISECONDS);
+            } else {
+                // 无过期时间,永久加入黑名单
+                redisTemplate.opsForValue().set(SecurityConstants.BLACK_TOKEN_CACHE_PREFIX + jti, null);
+            }
+        }
+        SecurityContextHolder.clearContext();
+        return Result.success("注销成功");
+    }
+
+    @Operation(summary = "获取验证码")
+    @GetMapping("/captcha")
+    public Result<?> getCaptcha() {
+        CaptchaResult captcha = easyCaptchaService.getCaptcha();
+        return Result.success(captcha);
+    }
+
+    @Operation(summary = "获取登录二维码")
+    @GetMapping("/his/code")
+    public Result<?> getLoginCode() throws Exception {
+        log.info("用户获取登录二维码");
+        String content = SoapTemplateDTO.QR_PICTURE;
+        String result = WebServiceUtil.httpClick(qrUrl,
+                "SOF_GetQRCodeBySys",
+                content);
+        JSONObject jsonObject = JSONUtil.parseObj(result);
+        if ("1000".equals(jsonObject.get("code"))) {
+//            log.info("二维码为:{}", jsonObject.get("data"));
+            return Result.success(jsonObject.get("data"));
+        }
+        return Result.failed(String.valueOf(jsonObject.get("msg")));
+    }
+
+    @Operation(summary = "获取二维码状态")
+    @GetMapping("/his/code/status")
+    public Result<?> getLoginCodeStatus(@RequestParam("code") String code) throws Exception {
+        log.info("用户获取二维码状态,qrcode为:{}", code);
+        String content = SoapTemplateDTO.getQrReturn(code);
+        String result = WebServiceUtil.httpClick(qrUrl, "SOF_QueryQRCode", content);
+        //将字符串转为json
+        JSONObject jsonData = JSONUtil.parseObj(JSONUtil.parseObj(result).get("data"));
+        //验证登录账号
+        if ("LoginQrCodeBeenScan".equals(jsonData.get("qrCodeStatus"))) {
+            //用户已扫码
+            UserAuthInfo doctor = userService.getUserAuthInfo(String.valueOf(jsonData.get("userJobNum")));
+            if (doctor == null) {
+                return Result.failed("用户不存在");
+            }
+            List<SimpleGrantedAuthority> roleList = doctor.getRoles()
+                    .stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).toList();
+            UsernamePasswordAuthenticationToken authenticationToken =
+                    new UsernamePasswordAuthenticationToken(doctor.getUsername(), doctor.getUsername(),
+                            roleList);
+            UserAuthInfo userAuthInfo =
+                    userService.getUserAuthInfo(doctor.getUsername());
+            SysUserDetails sysUserDetails = new SysUserDetails(userAuthInfo);
+            authenticationToken.setDetails(sysUserDetails);
+            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
+            String accessToken = jwtTokenManager.createToken(authenticationToken);
+            LoginResult loginResult = LoginResult.builder()
+                    .tokenType("Bearer")
+                    .accessToken(accessToken)
+                    .build();
+            return Result.success(loginResult);
+        } else if ("LoginQrCodeWaitScan".equals(jsonData.get("qrCodeStatus"))) {
+            return Result.success("等待扫码");
+        } else if ("LoginQrCodeExpire".equals(jsonData.get("qrCodeStatus")) ||
+                "LoginQrCodeInvalid".equals(jsonData.get("qrCodeStatus"))) {
+            return Result.success("二维码已失效");
+        }
+        return Result.failed("其他错误");
+    }
+
+    @Operation(summary = "自动登录")
+    @GetMapping("/autoLogin")
+    public Result autoLogin(@RequestParam String username) {
+        //验证当前医生是否存在
+        UserAuthInfo authInfo=userService.getUserAuthInfo(username);
+        if (authInfo == null){
+            return Result.success("工号不存在");
+        }
+        List<SimpleGrantedAuthority> roleList = authInfo.getRoles()
+                .stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).toList();
+        UsernamePasswordAuthenticationToken authenticationToken =
+                new UsernamePasswordAuthenticationToken(authInfo.getUsername(), authInfo.getUsername(),
+                        roleList);
+        SysUserDetails sysUserDetails = new SysUserDetails(authInfo);
+        authenticationToken.setDetails(sysUserDetails);
+        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
+        String accessToken = jwtTokenManager.createToken(authenticationToken);
+        LoginResult loginResult = LoginResult.builder()
+                .tokenType("Bearer")
+                .accessToken(accessToken)
+                .build();
+        return Result.success(loginResult);
+    }
+
+    @Operation(summary = "获取Token")
+    @GetMapping("/getToken")
+    public Result getToken(@RequestParam String username,@RequestParam String password,
+                           @RequestParam String client_id,@RequestParam String client_secret) {
+        try {
+            LoginResult loginResult = doLogin(username,password);
+            if(loginResult == null ){
+               return Result.failed("登录失败");
+            }
+            return Result.success(loginResult);
+        } catch (Exception e) {
+            log.error("授权失败", e);
+            return Result.failed(e.getMessage());
+        }
+    }
+
+    /**
+     * 内部系统登录操作
+     * @param username
+     * @param password
+     * @return
+     */
+    private LoginResult doLogin(String username,String password){
+
+        UserAuthInfo userAuthInfo = userService.getUserAuthInfo(username);
+        if(userAuthInfo == null){
+            return null;
+        }
+        List<SimpleGrantedAuthority> roleList = userAuthInfo.getRoles()
+                .stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).toList();
+        UsernamePasswordAuthenticationToken authenticationToken =
+                new UsernamePasswordAuthenticationToken(username, password, roleList);
+        SysUserDetails sysUserDetails = new SysUserDetails(userAuthInfo);
+        authenticationToken.setDetails(sysUserDetails);
+//        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
+        Authentication authentication = authenticationManager.authenticate(authenticationToken);
+        String accessToken = jwtTokenManager.createToken(authentication);
+        Claims claims = jwtTokenManager.parseAndValidateToken(accessToken);
+        LoginResult loginResult = LoginResult.builder()
+                .tokenType("Bearer")
+                .accessToken(accessToken)
+                .expires(claims.getExpiration().getTime())
+                .build();
+        return loginResult;
+    }
+    private String getSignImage(String userJobNum){
+        String content = SoapTemplateDTO.getUserInfo(userJobNum);
+        String method = "SOF_GetUserInfo";
+        String rs = null;
+        try {
+            rs = WebServiceUtil.httpClick(CaConstants.signDomain+"/PKISignPicture/services/v1",method, content);
+        } catch (IOException e) {
+            log.error(e.getMessage(),e);
+        }
+
+        ObjectMapper mapper = new ObjectMapper();
+        SoapReturnDTO<LinkedHashMap> soapReturnDTO=new SoapReturnDTO<>();
+        try{
+            soapReturnDTO = mapper.readValue(rs, SoapReturnDTO.class);
+        }catch (Exception e){
+            return e.getMessage();
+        }
+        if(!"1000".equals(soapReturnDTO.getCode()) ){
+            return "获取签名文件失败,"+soapReturnDTO.getMsg();
+        }
+        Map dataMap = (Map)soapReturnDTO.getData();
+        String image = (String) dataMap.get("image");
+        return image;
+    }
+    @Operation(summary = "获取二维码状态")
+    @GetMapping("/getScanStatus")
+    public Result<?> getScanStatus(@RequestParam("code") String code) throws Exception {
+        String content = SoapTemplateDTO.getQrReturn(code);
+        String result = WebServiceUtil.httpClick(qrUrl, "SOF_QueryQRCode", content);
+        //将字符串转为json
+        JSONObject jsonData = JSONUtil.parseObj(JSONUtil.parseObj(result).get("data"));
+        //验证登录账号
+        if ("LoginQrCodeBeenScan".equals(jsonData.get("qrCodeStatus"))) {
+            String userJobNum = jsonData.getStr("userJobNum");
+            String image = getSignImage(userJobNum);
+            return Result.success(image);
+        } else if ("LoginQrCodeWaitScan".equals(jsonData.get("qrCodeStatus"))) {
+            return Result.success("等待扫码");
+        } else if ("LoginQrCodeExpire".equals(jsonData.get("qrCodeStatus")) ||
+                "LoginQrCodeInvalid".equals(jsonData.get("qrCodeStatus"))) {
+            return Result.success("二维码已失效");
+        }
+        return Result.failed("其他错误");
+    }
+}

+ 109 - 0
src/main/java/com/qmrb/system/controller/CaPatientInfoController.java

@@ -0,0 +1,109 @@
+package com.qmrb.system.controller;
+
+import java.sql.Wrapper;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import cn.hutool.core.collection.CollectionUtil;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.qmrb.system.pojo.entity.CaPatientInfo;
+import org.springdoc.core.annotations.ParameterObject;
+import org.springframework.beans.BeanUtils;
+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 io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.qmrb.system.common.result.PageResult;
+import com.qmrb.system.common.result.Result;
+import com.qmrb.system.common.result.ResultCode;
+import com.qmrb.system.framework.resubmit.Resubmit;
+import com.qmrb.system.service.ICaPatientInfoService;
+import com.qmrb.system.pojo.form.CaPatientInfoForm;
+import com.qmrb.system.pojo.vo.CaPatientInfoVO;
+import com.qmrb.system.pojo.query.CaPatientInfoQuery;
+
+import cn.hutool.core.util.StrUtil;
+import org.springframework.web.bind.annotation.RestController;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * <p>
+ * CA电子签名患者默认信息 前端控制器
+ * </p>
+ *
+ * @author huangrj
+ * @since 2024-11-18
+ */
+@Tag(name = "CA电子签名患者默认信息接口")
+@RestController
+@RequestMapping("/api/v1/ca_patient_info")
+@RequiredArgsConstructor
+public class CaPatientInfoController{
+
+    private final ICaPatientInfoService caPatientInfoService;
+
+	@Operation(summary = "CA电子签名患者默认信息分页列表", security = {@SecurityRequirement(name = "Authorization")})
+    @GetMapping("/page")
+    public PageResult<CaPatientInfoVO> getPage(@ParameterObject CaPatientInfoQuery queryParams) {
+        Page<CaPatientInfoVO> result = caPatientInfoService.getPage(queryParams);
+        return PageResult.success(result);
+    }
+    
+    
+    @Operation(summary = "新增CA电子签名患者默认信息", security = {@SecurityRequirement(name = "Authorization")})
+    @PostMapping("/saveOrUpdate")
+    public Result<CaPatientInfoForm> saveOrUpdate(@RequestBody @Valid CaPatientInfoForm form) {
+        if (form.getIdentityCard() == null) {
+            return Result.failed("CA电子签名患者默认信息身份证号不能为空");
+        }else {
+            CaPatientInfoForm result =  caPatientInfoService.saveOrUpdateInfo(form);
+            return Result.success(result);
+        }
+
+    }
+
+    @Operation(summary = "CA电子签名患者默认信息表单数据", security = {@SecurityRequirement(name = "Authorization")})
+    @PostMapping("/getInfo")
+    public Result<CaPatientInfoForm> getForm(@RequestBody @Valid CaPatientInfoForm form) {
+        if (form.getIdentityCard() == null) {
+            return Result.failed("CA电子签名患者身份证号不能为空");
+        }
+    	CaPatientInfoForm formData = caPatientInfoService.getFormData(form.getIdentityCard());
+        return Result.success(formData);
+    }
+
+    @Operation(summary = "修改CA电子签名患者默认信息", security = {@SecurityRequirement(name = "Authorization")})
+    @PutMapping(value = "/{id}")
+    public Result<?> updateForm(@Parameter(description = "CA电子签名患者默认信息ID") @PathVariable Long id, @RequestBody @Validated CaPatientInfoForm form) {
+        boolean result = caPatientInfoService.updateForm(id,form);
+        return Result.judge(result);
+    }
+
+    @Operation(summary = "删除CA电子签名患者默认信息", security = {@SecurityRequirement(name = "Authorization")})
+    @DeleteMapping("/{ids}")
+    public Result<?> deleteUsers( @Parameter(description = "CA电子签名患者默认信息ID,多个以英文逗号(,)分割") @PathVariable String ids) {
+    	if(StrUtil.isBlank(ids)) {
+    		 return Result.failed(ResultCode.PARAM_ERROR, "删除的CA电子签名患者默认信息数据为空");
+    	}
+        // 逻辑删除
+        List<Long> idList = Arrays.asList(ids.split(",")).stream()
+                .map(idStr -> Long.parseLong(idStr)).collect(Collectors.toList());
+        boolean result = caPatientInfoService.removeByIds(idList);
+        return Result.judge(result);
+    }
+    
+
+
+}

File diff suppressed because it is too large
+ 167 - 0
src/main/java/com/qmrb/system/controller/CaSignController.java


+ 128 - 0
src/main/java/com/qmrb/system/controller/ElmtBaseCategoryController.java

@@ -0,0 +1,128 @@
+package com.qmrb.system.controller;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.springdoc.core.annotations.ParameterObject;
+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.CrossOrigin;
+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.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.qmrb.system.common.result.PageResult;
+import com.qmrb.system.common.result.Result;
+import com.qmrb.system.common.result.ResultCode;
+import com.qmrb.system.framework.resubmit.Resubmit;
+import com.qmrb.system.pojo.form.ElmtBaseCategoryForm;
+import com.qmrb.system.pojo.query.ElmtBaseCategoryQuery;
+import com.qmrb.system.pojo.vo.ElmtBaseCategoryVO;
+import com.qmrb.system.pojo.vo.Option;
+import com.qmrb.system.service.IElmtBaseCategoryService;
+
+import cn.hutool.core.util.StrUtil;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+
+/**
+ * <p>
+ * 元素分类树 前端控制器
+ * </p>
+ *
+ * @author huangrj
+ * @since 2023-06-06
+ */
+@Tag(name = "基本元素分类接口")
+@RestController
+@RequestMapping("/api/v1/base_category")
+@CrossOrigin
+public class ElmtBaseCategoryController{
+
+    @Autowired
+    private IElmtBaseCategoryService elmtBaseCategoryService;
+
+	@Operation(summary = "元素分类树分页列表", security = {@SecurityRequirement(name = "Authorization")})
+    @GetMapping("/page")
+    public PageResult<ElmtBaseCategoryVO> getPage(
+            @ParameterObject ElmtBaseCategoryQuery queryParams
+    ) {
+        Page<ElmtBaseCategoryVO> result = elmtBaseCategoryService.getPage(queryParams);
+        return PageResult.success(result);
+    }
+    
+    @Operation(summary = "新增元素分类树", security = {@SecurityRequirement(name = "Authorization")})
+    @PostMapping
+    @PreAuthorize("@ss.hasPerm('base_category:add')")
+    @Resubmit
+    public Result<?> saveForm(
+            @RequestBody @Valid ElmtBaseCategoryForm userForm
+    ) {
+        boolean result = elmtBaseCategoryService.saveForm(userForm);
+        return Result.judge(result);
+    }
+
+    @Operation(summary = "元素分类树表单数据", security = {@SecurityRequirement(name = "Authorization")})
+    @GetMapping("/{id}/form")
+    public Result<ElmtBaseCategoryForm> getForm(
+            @Parameter(description = "元素分类树ID") @PathVariable Long id
+    ) {
+    	ElmtBaseCategoryForm formData = elmtBaseCategoryService.getFormData(id);
+        return Result.success(formData);
+    }
+
+    @Operation(summary = "修改元素分类树", security = {@SecurityRequirement(name = "Authorization")})
+    @PutMapping(value = "/{id}")
+    @PreAuthorize("@ss.hasPerm('base_category:edit')")
+    public Result<?> updateForm(
+            @Parameter(description = "元素分类树ID") @PathVariable Long id,
+            @RequestBody @Validated ElmtBaseCategoryForm form) {
+    	
+        boolean result = elmtBaseCategoryService.updateForm(id,form);
+        return Result.judge(result);
+    }
+
+    @Operation(summary = "删除元素分类树", security = {@SecurityRequirement(name = "Authorization")})
+    @DeleteMapping("/{ids}")
+    @PreAuthorize("@ss.hasPerm('base_category:delete')")
+    public Result<?> deleteUsers(
+            @Parameter(description = "元素分类树ID,多个以英文逗号(,)分割") @PathVariable String ids
+    ) {
+    	if(StrUtil.isBlank(ids)) {
+    		 return Result.failed(ResultCode.PARAM_ERROR, "删除的元素分类树数据为空");
+    	}
+        // 逻辑删除
+        List<Long> idList = Arrays.asList(ids.split(",")).stream()
+                .map(idStr -> Long.parseLong(idStr)).collect(Collectors.toList());
+        boolean result = elmtBaseCategoryService.removeByIds(idList);
+        return Result.judge(result);
+    }
+    
+	@Operation(summary = "获取元素分类选项", security = {@SecurityRequirement(name = "Authorization")})
+    @GetMapping("/options")
+    public Result<List<Option<Long>>> listDeptOptions() {
+        List<Option<Long>> list = elmtBaseCategoryService.listOptions();
+        return Result.success(list);
+    }
+	
+	@Operation(summary = "获取元素分类树形列表", security = {@SecurityRequirement(name = "Authorization")})
+    @GetMapping("/treeList")
+    public Result<List<ElmtBaseCategoryVO>> listDepartments(
+            @ParameterObject ElmtBaseCategoryQuery queryParams
+    ) {
+        List<ElmtBaseCategoryVO> list = elmtBaseCategoryService.treeList(queryParams);
+        return Result.success(list);
+    }
+
+}

+ 111 - 0
src/main/java/com/qmrb/system/controller/ElmtBaseElementController.java

@@ -0,0 +1,111 @@
+package com.qmrb.system.controller;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.springdoc.core.annotations.ParameterObject;
+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.CrossOrigin;
+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 io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.qmrb.system.common.result.PageResult;
+import com.qmrb.system.common.result.Result;
+import com.qmrb.system.common.result.ResultCode;
+import com.qmrb.system.framework.resubmit.Resubmit;
+import com.qmrb.system.service.IElmtBaseElementService;
+import com.qmrb.system.pojo.form.ElmtBaseElementForm;
+import com.qmrb.system.pojo.vo.ElmtBaseElementVO;
+import com.qmrb.system.pojo.query.ElmtBaseElementQuery;
+
+import cn.hutool.core.util.StrUtil;
+import org.springframework.web.bind.annotation.RestController;
+import jakarta.validation.Valid;
+
+/**
+ * <p>
+ * 基本元素 前端控制器
+ * </p>
+ *
+ * @author huangrj
+ * @since 2023-06-07
+ */
+@Tag(name = "基本元素接口")
+@RestController
+@RequestMapping("/api/v1/base_element")
+@CrossOrigin
+public class ElmtBaseElementController{
+
+    @Autowired
+    private IElmtBaseElementService elmtBaseElementService;
+
+	@Operation(summary = "基本元素分页列表", security = {@SecurityRequirement(name = "Authorization")})
+    @GetMapping("/page")
+    public PageResult<ElmtBaseElementVO> getPage(
+            @ParameterObject ElmtBaseElementQuery queryParams
+    ) {
+        Page<ElmtBaseElementVO> result = elmtBaseElementService.getPage(queryParams);
+        return PageResult.success(result);
+    }
+    
+    @Operation(summary = "新增基本元素", security = {@SecurityRequirement(name = "Authorization")})
+    @PostMapping
+    @PreAuthorize("@ss.hasPerm('base_element:add')")
+    @Resubmit
+    public Result<?> saveForm(
+            @RequestBody @Valid ElmtBaseElementForm userForm
+    ) {
+        boolean result = elmtBaseElementService.saveForm(userForm);
+        return Result.judge(result);
+    }
+
+    @Operation(summary = "基本元素表单数据", security = {@SecurityRequirement(name = "Authorization")})
+    @GetMapping("/{id}/form")
+    public Result<ElmtBaseElementForm> getForm(
+            @Parameter(description = "基本元素ID") @PathVariable Long id
+    ) {
+    	ElmtBaseElementForm formData = elmtBaseElementService.getFormData(id);
+        return Result.success(formData);
+    }
+
+    @Operation(summary = "修改基本元素", security = {@SecurityRequirement(name = "Authorization")})
+    @PutMapping(value = "/{id}")
+    @PreAuthorize("@ss.hasPerm('base_element:edit')")
+    public Result<?> updateForm(
+            @Parameter(description = "基本元素ID") @PathVariable Long id,
+            @RequestBody @Validated ElmtBaseElementForm form) {
+    	
+        boolean result = elmtBaseElementService.updateForm(id,form);
+        return Result.judge(result);
+    }
+
+    @Operation(summary = "删除基本元素", security = {@SecurityRequirement(name = "Authorization")})
+    @DeleteMapping("/{ids}")
+    @PreAuthorize("@ss.hasPerm('base_element:delete')")
+    public Result<?> deleteUsers(
+            @Parameter(description = "基本元素ID,多个以英文逗号(,)分割") @PathVariable String ids
+    ) {
+    	if(StrUtil.isBlank(ids)) {
+    		 return Result.failed(ResultCode.PARAM_ERROR, "删除的基本元素数据为空");
+    	}
+        // 逻辑删除
+        List<Long> idList = Arrays.asList(ids.split(",")).stream()
+                .map(idStr -> Long.parseLong(idStr)).collect(Collectors.toList());
+        boolean result = elmtBaseElementService.removeByIds(idList);
+        return Result.judge(result);
+    }
+
+
+}

+ 133 - 0
src/main/java/com/qmrb/system/controller/ElmtBaseRangeCodeController.java

@@ -0,0 +1,133 @@
+package com.qmrb.system.controller;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.springdoc.core.annotations.ParameterObject;
+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.CrossOrigin;
+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.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.qmrb.system.common.result.PageResult;
+import com.qmrb.system.common.result.Result;
+import com.qmrb.system.common.result.ResultCode;
+import com.qmrb.system.framework.resubmit.Resubmit;
+import com.qmrb.system.pojo.form.ElmtBaseRangeCodeForm;
+import com.qmrb.system.pojo.query.ElmtBaseRangeCodeQuery;
+import com.qmrb.system.pojo.vo.ElmtBaseRangeCodeVO;
+import com.qmrb.system.service.IElmtBaseRangeCodeService;
+
+import cn.hutool.core.util.StrUtil;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+
+/**
+ * <p>
+ * 基本元素值域代码信息 前端控制器
+ * </p>
+ *
+ * @author huangrj
+ * @since 2023-06-12
+ */
+@Tag(name = "基本元素值域代码信息接口")
+@RestController
+@RequestMapping("/api/v1/base_range_code")
+@CrossOrigin
+public class ElmtBaseRangeCodeController{
+
+    @Autowired
+    private IElmtBaseRangeCodeService elmtBaseRangeCodeService;
+
+	@Operation(summary = "基本元素值域代码信息分页列表", security = {@SecurityRequirement(name = "Authorization")})
+    @GetMapping("/page")
+    public PageResult<ElmtBaseRangeCodeVO> getPage(
+            @ParameterObject ElmtBaseRangeCodeQuery queryParams
+    ) {
+        Page<ElmtBaseRangeCodeVO> result = elmtBaseRangeCodeService.getPage(queryParams);
+        return PageResult.success(result);
+    }
+	
+	@Operation(summary = "基本元素值域代码信息列表,不分页", security = {@SecurityRequirement(name = "Authorization")})
+    @GetMapping("/list")
+    public Result<List<ElmtBaseRangeCodeVO>> getList(
+            @ParameterObject ElmtBaseRangeCodeQuery queryParams
+    ) {
+		List<ElmtBaseRangeCodeVO> result = elmtBaseRangeCodeService.getList(queryParams);
+        return Result.success(result);
+    }
+    
+    @Operation(summary = "新增基本元素值域代码信息", security = {@SecurityRequirement(name = "Authorization")})
+    @PostMapping
+    @PreAuthorize("@ss.hasPerm('base_range_code:add')")
+    @Resubmit
+    public Result<?> saveForm(
+            @RequestBody @Valid ElmtBaseRangeCodeForm userForm
+    ) {
+        boolean result = elmtBaseRangeCodeService.saveForm(userForm);
+        return Result.judge(result);
+    }
+    
+    @Operation(summary = "批量新增基本元素值域代码信息", security = {@SecurityRequirement(name = "Authorization")})
+    @PostMapping("/batchAdd")
+//    @PreAuthorize("@ss.hasPerm('base_range_code:add')")
+    @Resubmit
+    public Result<String> batchSaveForm(
+            @RequestBody @Valid ElmtBaseRangeCodeForm[] list
+    ) {
+        String result = elmtBaseRangeCodeService.batchSaveForm(list);
+        return Result.success(result);
+    }
+
+    @Operation(summary = "基本元素值域代码信息表单数据", security = {@SecurityRequirement(name = "Authorization")})
+    @GetMapping("/{id}/form")
+    public Result<ElmtBaseRangeCodeForm> getForm(
+            @Parameter(description = "基本元素值域代码信息ID") @PathVariable Long id
+    ) {
+    	ElmtBaseRangeCodeForm formData = elmtBaseRangeCodeService.getFormData(id);
+        return Result.success(formData);
+    }
+
+    @Operation(summary = "修改基本元素值域代码信息", security = {@SecurityRequirement(name = "Authorization")})
+    @PutMapping(value = "/{id}")
+    @PreAuthorize("@ss.hasPerm('base_range_code:edit')")
+    public Result<?> updateForm(
+            @Parameter(description = "基本元素值域代码信息ID") @PathVariable Long id,
+            @RequestBody @Validated ElmtBaseRangeCodeForm form) {
+    	
+        boolean result = elmtBaseRangeCodeService.updateForm(id,form);
+        return Result.judge(result);
+    }
+
+    @Operation(summary = "删除基本元素值域代码信息", security = {@SecurityRequirement(name = "Authorization")})
+    @DeleteMapping("/{ids}")
+    @PreAuthorize("@ss.hasPerm('base_range_code:delete')")
+    public Result<?> deleteUsers(
+            @Parameter(description = "基本元素值域代码信息ID,多个以英文逗号(,)分割") @PathVariable String ids
+    ) {
+    	if(StrUtil.isBlank(ids)) {
+    		 return Result.failed(ResultCode.PARAM_ERROR, "删除的基本元素值域代码信息数据为空");
+    	}
+        // 逻辑删除
+        List<Long> idList = Arrays.asList(ids.split(",")).stream()
+                .map(idStr -> Long.parseLong(idStr)).collect(Collectors.toList());
+        boolean result = elmtBaseRangeCodeService.removeByIds(idList);
+        return Result.judge(result);
+    }
+    
+
+
+}

+ 111 - 0
src/main/java/com/qmrb/system/controller/ElmtBaseRangeController.java

@@ -0,0 +1,111 @@
+package com.qmrb.system.controller;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.springdoc.core.annotations.ParameterObject;
+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.CrossOrigin;
+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 io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.qmrb.system.common.result.PageResult;
+import com.qmrb.system.common.result.Result;
+import com.qmrb.system.common.result.ResultCode;
+import com.qmrb.system.framework.resubmit.Resubmit;
+import com.qmrb.system.service.IElmtBaseRangeService;
+import com.qmrb.system.pojo.form.ElmtBaseRangeForm;
+import com.qmrb.system.pojo.vo.ElmtBaseRangeVO;
+import com.qmrb.system.pojo.query.ElmtBaseRangeQuery;
+
+import cn.hutool.core.util.StrUtil;
+import org.springframework.web.bind.annotation.RestController;
+import jakarta.validation.Valid;
+
+/**
+ * <p>
+ * 基本元素值域 前端控制器
+ * </p>
+ *
+ * @author huangrj
+ * @since 2023-06-07
+ */
+@Tag(name = "基本元素值域接口")
+@RestController
+@RequestMapping("/api/v1/base_range")
+@CrossOrigin
+public class ElmtBaseRangeController{
+
+    @Autowired
+    private IElmtBaseRangeService elmtBaseRangeService;
+
+	@Operation(summary = "基本元素值域分页列表", security = {@SecurityRequirement(name = "Authorization")})
+    @GetMapping("/page")
+    public PageResult<ElmtBaseRangeVO> getPage(
+            @ParameterObject ElmtBaseRangeQuery queryParams
+    ) {
+        Page<ElmtBaseRangeVO> result = elmtBaseRangeService.getPage(queryParams);
+        return PageResult.success(result);
+    }
+    
+    @Operation(summary = "新增基本元素值域", security = {@SecurityRequirement(name = "Authorization")})
+    @PostMapping
+    @PreAuthorize("@ss.hasPerm('base_range:add')")
+    @Resubmit
+    public Result<?> saveForm(
+            @RequestBody @Valid ElmtBaseRangeForm userForm
+    ) {
+        boolean result = elmtBaseRangeService.saveForm(userForm);
+        return Result.judge(result);
+    }
+
+    @Operation(summary = "基本元素值域表单数据", security = {@SecurityRequirement(name = "Authorization")})
+    @GetMapping("/{id}/form")
+    public Result<ElmtBaseRangeForm> getForm(
+            @Parameter(description = "基本元素值域ID") @PathVariable Long id
+    ) {
+    	ElmtBaseRangeForm formData = elmtBaseRangeService.getFormData(id);
+        return Result.success(formData);
+    }
+
+    @Operation(summary = "修改基本元素值域", security = {@SecurityRequirement(name = "Authorization")})
+    @PutMapping(value = "/{id}")
+    @PreAuthorize("@ss.hasPerm('base_range:edit')")
+    public Result<?> updateForm(
+            @Parameter(description = "基本元素值域ID") @PathVariable Long id,
+            @RequestBody @Validated ElmtBaseRangeForm form) {
+    	
+        boolean result = elmtBaseRangeService.updateForm(id,form);
+        return Result.judge(result);
+    }
+
+    @Operation(summary = "删除基本元素值域", security = {@SecurityRequirement(name = "Authorization")})
+    @DeleteMapping("/{ids}")
+    @PreAuthorize("@ss.hasPerm('base_range:delete')")
+    public Result<?> deleteUsers(
+            @Parameter(description = "基本元素值域ID,多个以英文逗号(,)分割") @PathVariable String ids
+    ) {
+    	if(StrUtil.isBlank(ids)) {
+    		 return Result.failed(ResultCode.PARAM_ERROR, "删除的基本元素值域数据为空");
+    	}
+        // 逻辑删除
+        List<Long> idList = Arrays.asList(ids.split(",")).stream()
+                .map(idStr -> Long.parseLong(idStr)).collect(Collectors.toList());
+        boolean result = elmtBaseRangeService.removeByIds(idList);
+        return Result.judge(result);
+    }
+
+
+}

+ 128 - 0
src/main/java/com/qmrb/system/controller/ElmtCpstCategoryController.java

@@ -0,0 +1,128 @@
+package com.qmrb.system.controller;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.springdoc.core.annotations.ParameterObject;
+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.CrossOrigin;
+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 io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.qmrb.system.common.result.PageResult;
+import com.qmrb.system.common.result.Result;
+import com.qmrb.system.common.result.ResultCode;
+import com.qmrb.system.framework.resubmit.Resubmit;
+import com.qmrb.system.service.IElmtCpstCategoryService;
+import com.qmrb.system.pojo.form.ElmtCpstCategoryForm;
+import com.qmrb.system.pojo.vo.ElmtCpstCategoryVO;
+import com.qmrb.system.pojo.query.ElmtCpstCategoryQuery;
+import com.qmrb.system.pojo.vo.Option;
+
+import cn.hutool.core.util.StrUtil;
+import org.springframework.web.bind.annotation.RestController;
+import jakarta.validation.Valid;
+
+/**
+ * <p>
+ * 复合元素类别 前端控制器
+ * </p>
+ *
+ * @author huangrj
+ * @since 2023-06-12
+ */
+@Tag(name = "复合元素类别接口")
+@RestController
+@RequestMapping("/api/v1/cpst_category")
+@CrossOrigin
+public class ElmtCpstCategoryController{
+
+    @Autowired
+    private IElmtCpstCategoryService elmtCpstCategoryService;
+
+	@Operation(summary = "复合元素类别分页列表", security = {@SecurityRequirement(name = "Authorization")})
+    @GetMapping("/page")
+    public PageResult<ElmtCpstCategoryVO> getPage(
+            @ParameterObject ElmtCpstCategoryQuery queryParams
+    ) {
+        Page<ElmtCpstCategoryVO> result = elmtCpstCategoryService.getPage(queryParams);
+        return PageResult.success(result);
+    }
+    
+    @Operation(summary = "新增复合元素类别", security = {@SecurityRequirement(name = "Authorization")})
+    @PostMapping
+    @PreAuthorize("@ss.hasPerm('cpst_category:add')")
+    @Resubmit
+    public Result<?> saveForm(
+            @RequestBody @Valid ElmtCpstCategoryForm userForm
+    ) {
+        boolean result = elmtCpstCategoryService.saveForm(userForm);
+        return Result.judge(result);
+    }
+
+    @Operation(summary = "复合元素类别表单数据", security = {@SecurityRequirement(name = "Authorization")})
+    @GetMapping("/{id}/form")
+    public Result<ElmtCpstCategoryForm> getForm(
+            @Parameter(description = "复合元素类别ID") @PathVariable Long id
+    ) {
+    	ElmtCpstCategoryForm formData = elmtCpstCategoryService.getFormData(id);
+        return Result.success(formData);
+    }
+
+    @Operation(summary = "修改复合元素类别", security = {@SecurityRequirement(name = "Authorization")})
+    @PutMapping(value = "/{id}")
+    @PreAuthorize("@ss.hasPerm('cpst_category:edit')")
+    public Result<?> updateForm(
+            @Parameter(description = "复合元素类别ID") @PathVariable Long id,
+            @RequestBody @Validated ElmtCpstCategoryForm form) {
+    	
+        boolean result = elmtCpstCategoryService.updateForm(id,form);
+        return Result.judge(result);
+    }
+
+    @Operation(summary = "删除复合元素类别", security = {@SecurityRequirement(name = "Authorization")})
+    @DeleteMapping("/{ids}")
+    @PreAuthorize("@ss.hasPerm('cpst_category:delete')")
+    public Result<?> deleteUsers(
+            @Parameter(description = "复合元素类别ID,多个以英文逗号(,)分割") @PathVariable String ids
+    ) {
+    	if(StrUtil.isBlank(ids)) {
+    		 return Result.failed(ResultCode.PARAM_ERROR, "删除的复合元素类别数据为空");
+    	}
+        // 逻辑删除
+        List<Long> idList = Arrays.asList(ids.split(",")).stream()
+                .map(idStr -> Long.parseLong(idStr)).collect(Collectors.toList());
+        boolean result = elmtCpstCategoryService.removeByIds(idList);
+        return Result.judge(result);
+    }
+    
+    @Operation(summary = "获取复合元素类别下拉选项", security = {@SecurityRequirement(name = "Authorization")})
+    @GetMapping("/options")
+    public Result<List<Option<Long>>> listDeptOptions() {
+        List<Option<Long>> list = elmtCpstCategoryService.listOptions();
+        return Result.success(list);
+    }
+	
+	@Operation(summary = "获取复合元素类别树形列表", security = {@SecurityRequirement(name = "Authorization")})
+    @GetMapping("/treeList")
+    public Result<List<ElmtCpstCategoryVO>> listDepartments(
+            @ParameterObject ElmtCpstCategoryQuery queryParams
+    ) {
+        List<ElmtCpstCategoryVO> list = elmtCpstCategoryService.treeList(queryParams);
+        return Result.success(list);
+    }
+
+
+}

+ 130 - 0
src/main/java/com/qmrb/system/controller/ElmtCpstElementController.java

@@ -0,0 +1,130 @@
+package com.qmrb.system.controller;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.springdoc.core.annotations.ParameterObject;
+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.CrossOrigin;
+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.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.qmrb.system.common.result.PageResult;
+import com.qmrb.system.common.result.Result;
+import com.qmrb.system.common.result.ResultCode;
+import com.qmrb.system.framework.resubmit.Resubmit;
+import com.qmrb.system.pojo.form.ElmtCpstElementForm;
+import com.qmrb.system.pojo.query.ElmtCpstElementQuery;
+import com.qmrb.system.pojo.vo.ElmtCpstElementVO;
+import com.qmrb.system.pojo.vo.TplTreeVO;
+import com.qmrb.system.service.IElmtCpstElementService;
+
+import cn.hutool.core.util.StrUtil;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+
+/**
+ * <p>
+ * 复合元素 前端控制器
+ * </p>
+ *
+ * @author huangrj
+ * @since 2023-06-07
+ */
+@Tag(name = "复合元素接口")
+@RestController
+@RequestMapping("/api/v1/cpst_element")
+@CrossOrigin
+public class ElmtCpstElementController{
+
+    @Autowired
+    private IElmtCpstElementService elmtCpstElementService;
+
+	@Operation(summary = "复合元素分页列表", security = {@SecurityRequirement(name = "Authorization")})
+    @GetMapping("/page")
+    public PageResult<ElmtCpstElementVO> getPage(
+            @ParameterObject ElmtCpstElementQuery queryParams
+    ) {
+        Page<ElmtCpstElementVO> result = elmtCpstElementService.getPage(queryParams);
+        return PageResult.success(result);
+    }
+    
+    @Operation(summary = "新增复合元素", security = {@SecurityRequirement(name = "Authorization")})
+    @PostMapping
+    @PreAuthorize("@ss.hasPerm('cpst_element:add')")
+    @Resubmit
+    public Result<ElmtCpstElementForm> saveForm(
+            @RequestBody @Valid ElmtCpstElementForm userForm
+    ) {
+    	ElmtCpstElementForm result = elmtCpstElementService.saveForm(userForm);
+        return Result.success(result);
+    }
+
+    @Operation(summary = "复合元素表单数据", security = {@SecurityRequirement(name = "Authorization")})
+    @GetMapping("/{id}/form")
+    public Result<ElmtCpstElementForm> getForm(
+            @Parameter(description = "复合元素ID") @PathVariable Long id
+    ) {
+    	ElmtCpstElementForm formData = elmtCpstElementService.getFormData(id);
+        return Result.success(formData);
+    }
+
+    @Operation(summary = "修改复合元素", security = {@SecurityRequirement(name = "Authorization")})
+    @PutMapping(value = "/{id}")
+    @PreAuthorize("@ss.hasPerm('cpst_element:edit')")
+    public Result<?> updateForm(
+            @Parameter(description = "复合元素ID") @PathVariable Long id,
+            @RequestBody @Validated ElmtCpstElementForm form) {
+    	
+        boolean result = elmtCpstElementService.updateForm(id,form);
+        return Result.judge(result);
+    }
+
+    @Operation(summary = "删除复合元素", security = {@SecurityRequirement(name = "Authorization")})
+    @DeleteMapping("/{ids}")
+    @PreAuthorize("@ss.hasPerm('cpst_element:delete')")
+    public Result<?> deleteUsers(
+            @Parameter(description = "复合元素ID,多个以英文逗号(,)分割") @PathVariable String ids
+    ) {
+    	if(StrUtil.isBlank(ids)) {
+    		 return Result.failed(ResultCode.PARAM_ERROR, "删除的复合元素数据为空");
+    	}
+        // 逻辑删除
+        List<Long> idList = Arrays.asList(ids.split(",")).stream()
+                .map(idStr -> Long.parseLong(idStr)).collect(Collectors.toList());
+        boolean result = elmtCpstElementService.removeByIds(idList);
+        return Result.judge(result);
+    }
+
+
+    @Operation(summary = "修改复合元素,设计时使用,只修改内容content与配置config", security = {@SecurityRequirement(name = "Authorization")})
+    @PutMapping(value = "/cpst/{id}")
+    public Result<?> updateCpst(
+            @Parameter(description = "复合元素ID") @PathVariable Long id,
+            @RequestBody @Validated ElmtCpstElementForm form) {
+    	
+        boolean result = elmtCpstElementService.updateCpst(id,form);
+        return Result.judge(result);
+    }
+    
+    
+    @Operation(summary = "获取复合元素的树形列表,包含分类", security = {@SecurityRequirement(name = "Authorization")})
+	@GetMapping("/cpstTreeList")
+	public Result<List<TplTreeVO>> tplTreeList() {
+		List<TplTreeVO> list = elmtCpstElementService.cpstTreeList();
+		return Result.success(list);
+	}
+}

Some files were not shown because too many files changed in this diff