Browse Source

first commit

master
zhouhaibin 5 months ago
commit
602b1fbd08
  1. 35
      .gitignore
  2. 21
      LICENSE
  3. 144
      README.md
  4. 110
      cip-client/pom.xml
  5. 73
      cip-client/src/main/java/tech/popsoft/cip/client/CipClientApplication.java
  6. 112
      cip-client/src/main/java/tech/popsoft/cip/client/framework/MessageClient.java
  7. 142
      cip-client/src/main/java/tech/popsoft/cip/client/framework/MessageClientChannelInitializer.java
  8. 88
      cip-client/src/main/java/tech/popsoft/cip/client/framework/MessageClientConfig.java
  9. 18
      cip-client/src/main/java/tech/popsoft/cip/client/framework/MessageClientGlobalHolder.java
  10. 49
      cip-client/src/main/java/tech/popsoft/cip/client/framework/ResendMessage.java
  11. 48
      cip-client/src/main/java/tech/popsoft/cip/client/framework/customhandler/CustomReadTimeoutHandler.java
  12. 72
      cip-client/src/main/java/tech/popsoft/cip/client/framework/customhandler/DistinctMessageHandler.java
  13. 52
      cip-client/src/main/java/tech/popsoft/cip/client/framework/customhandler/RequestMessageBusinessHandler.java
  14. 54
      cip-client/src/main/java/tech/popsoft/cip/client/framework/customhandler/ResponseMessageBusinessHandler.java
  15. 120
      cip-client/src/main/java/tech/popsoft/cip/client/framework/customhandler/WebSocketClientHandshakerHandler.java
  16. 45
      cip-client/src/main/java/tech/popsoft/cip/client/framework/handler/MessageHandler.java
  17. 35
      cip-client/src/main/java/tech/popsoft/cip/client/framework/handler/MessageHandlerFactory.java
  18. 73
      cip-client/src/main/java/tech/popsoft/cip/client/framework/handler/RequestMessageHandler.java
  19. 47
      cip-client/src/main/java/tech/popsoft/cip/client/framework/handler/ResponseMessageHandler.java
  20. 69
      cip-client/src/main/java/tech/popsoft/cip/client/framework/sender/MessageSender.java
  21. 38
      cip-client/src/main/java/tech/popsoft/cip/client/framework/sender/MessageSenderFactory.java
  22. 105
      cip-client/src/main/java/tech/popsoft/cip/client/framework/sender/RequestMessageSender.java
  23. 74
      cip-client/src/main/java/tech/popsoft/cip/client/framework/sender/ResponseMessageSender.java
  24. 24
      cip-client/src/main/java/tech/popsoft/cip/client/handler/request/lms/transportbill/ConsignmentBillCreateRequestHandler.java
  25. 22
      cip-client/src/main/java/tech/popsoft/cip/client/handler/response/system/ErrorResponseHandler.java
  26. 67
      cip-client/src/main/java/tech/popsoft/cip/client/handler/response/system/LoginResponseHandler.java
  27. 15
      cip-client/src/main/java/tech/popsoft/cip/client/handler/response/system/MessageConfirmResponseHandler.java
  28. 185
      cip-client/src/main/java/tech/popsoft/cip/client/manage/controller/ApiMessageLogController.java
  29. 224
      cip-client/src/main/java/tech/popsoft/cip/client/manage/controller/ApiMessageTopicController.java
  30. 116
      cip-client/src/main/java/tech/popsoft/cip/client/manage/entity/ApiMessageLog.java
  31. 93
      cip-client/src/main/java/tech/popsoft/cip/client/manage/entity/ApiMessageTopic.java
  32. 28
      cip-client/src/main/java/tech/popsoft/cip/client/manage/exception/ApiMessageLogExceptionEnum.java
  33. 39
      cip-client/src/main/java/tech/popsoft/cip/client/manage/exception/ApiMessageTopicExceptionEnum.java
  34. 15
      cip-client/src/main/java/tech/popsoft/cip/client/manage/mapper/ApiMessageLogMapper.java
  35. 5
      cip-client/src/main/java/tech/popsoft/cip/client/manage/mapper/ApiMessageLogMapper.xml
  36. 15
      cip-client/src/main/java/tech/popsoft/cip/client/manage/mapper/ApiMessageTopicMapper.java
  37. 5
      cip-client/src/main/java/tech/popsoft/cip/client/manage/mapper/ApiMessageTopicMapper.xml
  38. 92
      cip-client/src/main/java/tech/popsoft/cip/client/manage/service/ApiMessageLogService.java
  39. 72
      cip-client/src/main/java/tech/popsoft/cip/client/manage/service/ApiMessageTopicService.java
  40. 146
      cip-client/src/main/java/tech/popsoft/cip/client/manage/service/impl/ApiMessageLogServiceImpl.java
  41. 150
      cip-client/src/main/java/tech/popsoft/cip/client/manage/service/impl/ApiMessageTopicServiceImpl.java
  42. 83
      cip-client/src/main/java/tech/popsoft/cip/client/manage/vo/ApiMessageLogVO.java
  43. 78
      cip-client/src/main/java/tech/popsoft/cip/client/manage/vo/ApiMessageTopicVO.java
  44. 27
      cip-client/src/main/java/tech/popsoft/cip/client/platform/exception/CustomException.java
  45. 16
      cip-client/src/main/java/tech/popsoft/cip/client/platform/exception/ExceptionInterface.java
  46. 37
      cip-client/src/main/java/tech/popsoft/cip/client/receiver/ReceiveMessageController.java
  47. 17
      cip-client/src/main/java/tech/popsoft/cip/client/sender/request/lms/transportbill/ConsignmentBillCreateSender.java
  48. 31
      cip-client/src/main/java/tech/popsoft/cip/client/sender/request/system/LoginRequestSender.java
  49. 25
      cip-client/src/main/java/tech/popsoft/cip/client/sender/response/system/ErrorResponseSender.java
  50. 20
      cip-client/src/main/java/tech/popsoft/cip/client/sender/response/system/MessageConfirmResponseSender.java
  51. 29
      cip-client/src/main/resources/application-dev.yml
  52. 94
      cip-client/src/main/resources/application.yml
  53. 84
      cip-client/src/main/resources/init.sql
  54. 18
      cip-client/src/test/java/tech/popsoft/AppTest.java
  55. 16
      platform-app/.gitignore
  56. 34
      platform-app/App.vue
  57. 21
      platform-app/LICENSE
  58. 50
      platform-app/api/login.js
  59. 41
      platform-app/api/system/user.js
  60. 167
      platform-app/components/uni-section/uni-section.vue
  61. 26
      platform-app/config.js
  62. 17
      platform-app/main.js
  63. 69
      platform-app/manifest.json
  64. 102
      platform-app/pages.json
  65. 43
      platform-app/pages/common/textview/index.vue
  66. 34
      platform-app/pages/common/webview/index.vue
  67. 43
      platform-app/pages/index.vue
  68. 167
      platform-app/pages/login.vue
  69. 75
      platform-app/pages/mine/about/index.vue
  70. 631
      platform-app/pages/mine/avatar/index.vue
  71. 112
      platform-app/pages/mine/help/index.vue
  72. 195
      platform-app/pages/mine/index.vue
  73. 127
      platform-app/pages/mine/info/edit.vue
  74. 44
      platform-app/pages/mine/info/index.vue
  75. 85
      platform-app/pages/mine/pwd/index.vue
  76. 78
      platform-app/pages/mine/setting/index.vue
  77. 196
      platform-app/pages/register.vue
  78. 181
      platform-app/pages/work/index.vue
  79. 39
      platform-app/permission.js
  80. 60
      platform-app/plugins/auth.js
  81. 14
      platform-app/plugins/index.js
  82. 74
      platform-app/plugins/modal.js
  83. 30
      platform-app/plugins/tab.js
  84. BIN
      platform-app/static/favicon.ico
  85. 90
      platform-app/static/font/iconfont.css
  86. BIN
      platform-app/static/font/iconfont.ttf
  87. BIN
      platform-app/static/images/banner/banner01.jpg
  88. BIN
      platform-app/static/images/banner/banner02.jpg
  89. BIN
      platform-app/static/images/banner/banner03.jpg
  90. BIN
      platform-app/static/images/profile.jpg
  91. BIN
      platform-app/static/images/tabbar/home.png
  92. BIN
      platform-app/static/images/tabbar/home_.png
  93. BIN
      platform-app/static/images/tabbar/mine.png
  94. BIN
      platform-app/static/images/tabbar/mine_.png
  95. BIN
      platform-app/static/images/tabbar/work.png
  96. BIN
      platform-app/static/images/tabbar/work_.png
  97. 20
      platform-app/static/index.html
  98. BIN
      platform-app/static/logo.png
  99. BIN
      platform-app/static/logo200.png
  100. 5142
      platform-app/static/scss/colorui.css

35
.gitignore

@ -0,0 +1,35 @@
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
/output/
/file-online-preview/server/src/main/file

21
LICENSE

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 风行
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

144
README.md

@ -0,0 +1,144 @@
### 系统简介
企业级通用开发平台,前后端分离架构,单工程,多模块,部署形态为单体应用。
前端基于vue3.2.47,element-plus 2.1.0,前端框架vue-element-plus-admin1.9.4深度整合改造。
后端SSM+MybatisPlus,使用SpringBoot 2.3.0。
数据库使用MySql 8.0.
重度使用MybatisPlus,包括主键策略、逻辑删除、乐观锁、自动填充、数据分页、CURD接口、条件构造器等,
二次封装和扩展代码生成器,实现entity、dao、service、controller、vo及前端vue页面生成。
整体架构图如下:
![输入图片说明](resource/1.png)
技术选型,详见专栏博客:https://blog.csdn.net/seawaving/article/details/130015830
### 后端架构
到目前为止,整个工程项目,后端共计19个模块,架构图和依赖关系如下图所示:
![输入图片说明](resource/2.jpg)
模块分成三类,一类是平台内核模块,命名规则是platform+模块功能名称,图中用蓝色标示;一类是能力扩展模块,命名规则是platform-boot-starter+模块功能名称,图中用绿色标示;剩下的一类是接口平台,命名规则是cip
+模块功能名称,图中用紫色标示,相对平台独立,但又作为平台的重要组成部分。
#### 平台内核模块
platform-common作为公用基础模块,主要包括工具类、公用注解、公共父类、公共常量、公共枚举值,与前端UI交互定义的vo类,该模块为最基础的模块,无前置依赖。
platform-system是平台最核心的模块,主要包括组织机构、人员、角色(用户组)、权限、日志、系统参数、模块这些实体和服务的实现,需要注意的是,权限控制、日志记录,并不是在该模块实现,而是在platform-framework平台框架中实现,该模块依赖于platform-common。
platform-framework是平台框架,负责身份认证、权限控制、全局配置、数据分页、日志处理、自动填充(创建人、创建时间、修改人、修改时间),因为身份认证、权限控制等功能,不可避免需要使用处于platform-system模块中的人员、用户组等实体和服务,因此依赖于platform-system。
platform-support是一个业务支撑模块,基于技术组件进行功能设计与封装,实现一些通用的功能设计,更方便业务逻辑的实现,提供附件管理、通知公告、内容模板(用于短信、邮件、消息)、单据流水号、门户等功能。这些支撑模块同样需要位于platform-system模块中的人员、组织机构等实体和服务,因此依赖于platform-system。
platform-entityconfig属于低代码配置范畴,定义了业务实体的元数据,通过模块、实体、模型、视图多级配置,结合模板技术,实现细粒度的代码生成控制。
platform-workflow集成了工作流组件activiti的分支Camunda,实现了流程建模、流程模板,以及我的待办、我的已办和我的申请等功能。
platform-businessflow是业务流程的集中存储模块,实现了流程导航功能和请求申请示例流程。
platform-boot-starter是平台启动项目,整合平台基础功能,类似于spring-boot-starter,业务系统引入该包进行依赖。该模块自身没有实体与服务,而是汇总整合,把platform-framework
引用进来,同时进行配置。配置分两方面,一方面是做一个配置类,加一些注解(如:@EnableRetry、@ServletComponentScan、@EnableTransactionManagement),使用开发平台实现的业务系统,就不需要在启动类上重复添加这些注解;另一方面,是位于yml配置文件中的配置信息,也分为两部分,一部分是三方组件自身的,如数据源、连接池、redis、quartz、logback,另一方面是自定义的系统参数,如用户默认密码、导出excel数据的批次最大行数量。
platform-boot-starter-demo是示例项目,实际是模拟业务系统如何使用开发平台,用于平台自身功能开发与调试。
#### 能力扩展模块
绿色标示的五个模块,比较好理解,通常是对第三方组件的封装与整合,依赖于公共基础模块platform-common,这些模块可以不断扩展的,业务系统按需引入即可,这样就实现了核心模块必选、扩展模块可选的目的。
* platform-boot-starter-mail:邮件,集成springmail组件,实现邮件的发送功能封装
* platform-boot-starter-oss: 对象存储,用于文件存储封装,底层可基于多种模式,如本地磁盘、对象存储系统等
* platform-boot-starter-scheduler:任务调度,集成quartz组件,实现任务调度可视化配置
* platform-boot-starter-notification:消息通知,基于netty实现的websocket,用于系统内置消息
* platform-boot-starter-elasticsearch:全文搜索,集成elasticsearch组件
对于扩展模块,平台的核心模块实际也可能会用到,例如platform-support中的附件功能,就会用到platform-boot-starter-oss;platform-system中的自动解锁用户功能,就会用到platform-boot-starter-scheduler。
#### 接口平台
将自己之前开源的通用接口平台进行了改造,将其作为一个模块,整合到应用开发平台当中来,由接口平台统一对外暴露应用系统的API数据接口以及推送事件消息。
platform-cip-common:公共基础
platform-cip-api:对外提供API数据接口,提供API服务
platform-cip-message:基于netty的web socket服务端提供消息服务
platform-cip-manage:平台自身基础数据的维护,如应用、API服务、消息服务、数据权限管理等。
4个模块内关系为manage依赖common,api和message相互独立,但都依赖于manage。
### 如何运行
以下为简要说明,详细的开发环境搭建手册参见https://blog.csdn.net/seawaving/article/details/134895546
#### 1. 准备工作
预装redis、nodejs、mysql、ide
#### 2. 初始化数据库
执行/resource目录下的init.sql,创建名字为abc的数据库。
#### 3 .前端
nodejs >=14.6
执行npm install pnpm -g,安装pnpm包
执行pnpm install命令,若nodejs版本过低会提示
使用vscode打开platform-web目录,执行pnpm install安装npm module
执行结束会提示如下错误,不用理会,因为把husky移除导致的,不影响系统正常运行,进行下步dev脚本即可
husky install
'husky' 不是内部或外部命令,也不是可运行的程序
或批处理文件。
执行dev脚本,默认打开localhost:4000
默认管理员账号密码:admin/12345678
#### 4 .后端
标准SpringBoot项目,多模块,启动类位于platform-boot-starter-demo下,默认端口8080。
注:系统的下拉数据源,也即数据字典使用redis缓存,按上述步骤构建后,部分查询界面不显示中文名称,可在系统登录后,访问系统管理-》系统维护菜单下的“重建缓存”按钮,系统会自动将数据库的字典数据写入到redis中。
#### 5 .接口平台对接客户端
cip-client是一个模拟的接口平台客户端,是一个独立的springboot,相当于第三方系统,有自己独立的数据库,数据库脚本参见\cip-client\src\main\resources\init.sql
#### 6 .minio启用说明
平台对于文件存储除了支持本地磁盘模式外,还实现了minio对象存储组件的集成。如需启用,需安装minio服务端,版本2021-04-22T15-44-28Z(最后一个基于apache
2.0开源协议的版本),并修改平台配置文件。
### 未来规划
客观地说,目前开发平台已经实现了大部分常用常见功能,可以投入使用了。
由于是一路狂奔模式,速度提升,时间缩短,但不可避免一些功能遗留了待办项,以及未充分测试导致存在bug,后面需要再循着功能过一遍,进行重构、测试,完善功能,输出设计。特别是低代码配置部分,需要持续完善与改进,简化配置,进一步提升开发效率。
后面几块是平台欠缺的,需要补全和完善,每一块都是硬骨头,难度和工作量都不小,做了一些简单初步的了解,具体如下:
输出系统使用手册(进行中)
集成图表组件(已完成)
集成工作流(常用功能已完成,进度80%,已可用)
实现可视化表单(常用功能已完成,进度90%,待打磨)
移动端实现(整合了一个移动端框架,打通了认证,尚未有具体的功能,暂挂起)
自定义查询(未开始)
实现数据权限(未开始)
### 基于平台开发的应用
#### 文档管理系统
文档管理系统是基于平台实现的文档库管理应用,可作为企业内部文档库、知识库使用,主要包括以下功能:
* 文件夹管理:创建、更名、删除、复制、移动、授权;
* 文档管理:上传、下载、更名、复制、移动、预览、分享;
* 权限控制:按组织机构和按用户组两种授权模式;
* 在线预览:无需下载,上百种格式文件在线预览;
* 收藏夹:支持将文件夹和文档加入收藏、查看和移除;
* 全文搜索:对文本类、office文档和pdf文档等进行全文搜索;
### 系统设计资料
参见csdn博客专栏 [https://blog.csdn.net/seawaving/category_12230971.html](https://blog.csdn.net/seawaving/category_12230971.html)
平台研发过程中的设计思路、遇到的问题和方案的选择等一并分享出来,欢迎交流与讨论。

110
cip-client/pom.xml

@ -0,0 +1,110 @@
<?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>tech.popsoft.cip</groupId>
<artifactId>cip-client</artifactId>
<version>5.1.0</version>
<name>cip-client</name>
<properties>
<!-- 文件拷贝时的编码 -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<!-- 编译时的编码 -->
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>
<java.version>1.8</java.version>
</properties>
<!--版本依赖管理-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.3.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!--######开发平台核心模块依赖 区域开始#####-->
<dependency>
<groupId>tech.abc</groupId>
<artifactId>platform-framework</artifactId>
<version>5.1.0</version>
</dependency>
<!--######开发平台核心模块依赖 区域开始#####-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</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>tech.abc</groupId>
<artifactId>platform-cip-common</artifactId>
<version>5.1.0</version>
</dependency>
<!--mysql 驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.20</version>
</dependency>
<!--数据库连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.5</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>

73
cip-client/src/main/java/tech/popsoft/cip/client/CipClientApplication.java

@ -0,0 +1,73 @@
package tech.popsoft.cip.client;
import lombok.extern.slf4j.Slf4j;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Import;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.util.StopWatch;
import tech.abc.platform.common.utils.SpringUtil;
import tech.abc.platform.framework.config.PlatformConfig;
import tech.popsoft.cip.client.framework.MessageClient;
import tech.popsoft.cip.client.framework.MessageClientConfig;
/**
* @author wqliu
*/
@Slf4j
@Import(SpringUtil.class)
@EnableRetry
@ServletComponentScan
@EnableTransactionManagement
@EnableConfigurationProperties({MessageClientConfig.class, PlatformConfig.class})
@SpringBootApplication(scanBasePackages = {"tech.abc.platform.**", "tech.popsoft.cip.**"})
@MapperScan({"tech.abc.platform.**.mapper", "tech.popsoft.cip.**.mapper"})
public class CipClientApplication implements CommandLineRunner {
@Autowired
private MessageClient messageClient;
public static void main(String[] args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = new SpringApplicationBuilder(CipClientApplication.class)
.logStartupInfo(false)
.run(args);
stopWatch.stop();
Integer port = context.getBean(ServerProperties.class).getPort();
log.info("服务启动完成,耗时:{}s,端口号:{}", stopWatch.getTotalTimeSeconds(), port);
}
@Override
public void run(String... args) throws Exception {
// 此处通过单线程启动netty,是为了不堵塞主应用,不需要线程池
// noinspection AlibabaAvoidManuallyCreateThread
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
messageClient.start();
}
});
// 启动netty服务
thread.start();
}
}

112
cip-client/src/main/java/tech/popsoft/cip/client/framework/MessageClient.java

@ -0,0 +1,112 @@
package tech.popsoft.cip.client.framework;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import tech.popsoft.cip.client.framework.customhandler.WebSocketClientHandshakerHandler;
import java.util.concurrent.TimeUnit;
/**
* @author wqliu
* @date 2021-9-28
**/
@Slf4j
@Component
public class MessageClient {
@Autowired
private MessageClientConfig config;
@Autowired
private MessageClientChannelInitializer messageClientChannelInitializer;
@Autowired
private ResendMessage resendMessage;
/**
* 启动客户端方法
*/
public void start() {
EventLoopGroup workerGroup = new NioEventLoopGroup(1);
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(workerGroup);
bootstrap.channel(NioSocketChannel.class);
bootstrap.handler(messageClientChannelInitializer);
// 客户端与服务端连接的通道,final修饰表示只会有一个
ChannelFuture channelFuture = bootstrap.connect(config.getHost(), config.getPort());
channelFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (!future.isSuccess()) {
// 未成功
log.error("连接失败", future.cause());
// 执行重连
reconnect();
} else {
log.info("连接成功");
Channel channel = future.channel();
// 将channel保存到全局变量
MessageClientGlobalHolder.channel = channel;
// 发起握手
WebSocketClientHandshakerHandler handler = (WebSocketClientHandshakerHandler) channel.pipeline().get("hookedHandler");
handler.handshake(MessageClientGlobalHolder.channel);
}
}
});
// 消息重发
if (config.isEnableResend()) {
log.info("启动消息重发机制");
// 延迟30秒启动,给建立连接预留充足的时间
workerGroup.scheduleAtFixedRate(() -> {
resendMessage.resend();
}, 30, config.getSendMessageSpan(), TimeUnit.SECONDS);
}
// 等待服务器端关闭
channelFuture.channel().closeFuture().sync();
} catch (Exception e) {
log.error("消息客户端启动失败:{}" + e.getMessage(), e);
// 执行重连
reconnect();
} finally {
workerGroup.shutdownGracefully();
}
}
/**
* 重连
*/
public void reconnect() {
try {
// 重连期间将连接置空
MessageClientGlobalHolder.channel = null;
// 休眠5秒
Thread.sleep(5000);
// 执行重连
log.info("消息客户端进行重连");
start();
} catch (InterruptedException e) {
log.error("消息客户端重连过程中线程休眠失败", e);
}
}
}

142
cip-client/src/main/java/tech/popsoft/cip/client/framework/MessageClientChannelInitializer.java

@ -0,0 +1,142 @@
package tech.popsoft.cip.client.framework;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.ssl.SslHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import tech.abc.platform.cip.common.handler.JsonEncodeHandler;
import tech.abc.platform.cip.common.handler.MessageTypeDecodeHandler;
import tech.abc.platform.cip.common.handler.TextWebSocketFrameEncodeHandler;
import tech.abc.platform.cip.common.handler.ValidateMessageHandler;
import tech.popsoft.cip.client.framework.customhandler.*;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import java.io.InputStream;
import java.security.KeyStore;
/**
* 初始化通道
*
* @author wqliu
* @date 2021-2-5
*/
@Slf4j
@Component
public class MessageClientChannelInitializer extends ChannelInitializer<SocketChannel> {
@Autowired
private MessageClientConfig config;
@Autowired
private Environment environment;
/**
* 生产运行模式
*/
private final String PRD_MODE = "prd";
/**
* 初始化channel
*/
@Override
public void initChannel(SocketChannel socketChannel) throws Exception {
// 获取通道链路
ChannelPipeline pipeline = socketChannel.pipeline();
// 仅在生产模式下加载ssl过滤器
String mode = environment.getProperty("spring.profiles.active");
if (PRD_MODE.equals(mode)) {
// ssl
SSLContext sslContext = createSslContext();
SSLEngine engine = sslContext.createSSLEngine();
engine.setNeedClientAuth(false);
engine.setUseClientMode(false);
// 客户端第一个消息,即发起握手的消息不要加密
pipeline.addLast(new SslHandler(engine, true));
}
// HTTP 编解码
pipeline.addLast(new HttpClientCodec());
// 聚合为单个 FullHttpRequest 或者 FullHttpResponse
pipeline.addLast(new HttpObjectAggregator(64 * 1024));
// 读超时处理
pipeline.addLast(new CustomReadTimeoutHandler(config.getReadIdleTimeOut()));
// 处理web socket协议与握手
pipeline.addLast("hookedHandler", new WebSocketClientHandshakerHandler());
// 数据基本验证
pipeline.addLast(new ValidateMessageHandler());
// 去重
pipeline.addLast(new DistinctMessageHandler());
// 将文本按消息类型转换为请求消息或响应消息
pipeline.addLast(new MessageTypeDecodeHandler());
// 请求消息业务逻辑处理器
pipeline.addLast(new RequestMessageBusinessHandler());
// 响应消息业务逻辑处理器
pipeline.addLast(new ResponseMessageBusinessHandler());
// 编码为TextWebSocketFrame
pipeline.addLast(new TextWebSocketFrameEncodeHandler());
// json序列化
pipeline.addLast(new JsonEncodeHandler());
}
/**
* 创建ssl上下文对象
*
* @return
* @throws Exception
*/
public SSLContext createSslContext() throws Exception {
// 读取配置信息
String path = environment.getProperty("server.ssl.key-store");
String password = environment.getProperty("server.ssl.key-store-password");
String type = environment.getProperty("server.ssl.key-store-type");
// 构建证书上下文对象
KeyStore ks = KeyStore.getInstance(type);
path = path.replace("classpath:", "");
ClassPathResource resource = new ClassPathResource(path);
InputStream ksInputStream = resource.getInputStream();
ks.load(ksInputStream, password.toCharArray());
// KeyManagerFactory充当基于密钥内容源的密钥管理器的工厂。
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(ks, password.toCharArray());
// SSLContext的实例表示安全套接字协议的实现,它充当用于安全套接字工厂或 SSLEngine 的工厂。
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), null, null);
return sslContext;
}
}

88
cip-client/src/main/java/tech/popsoft/cip/client/framework/MessageClientConfig.java

@ -0,0 +1,88 @@
package tech.popsoft.cip.client.framework;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.text.MessageFormat;
/**
* 客户端配置参数
* 使用spring单例模式
*
* @author wqliu
* @date 2021-10-5
*/
@Data
@ConfigurationProperties(prefix = "params")
public class MessageClientConfig {
/**
* 应用编码
*/
private String appCode;
/**
* 应用密钥
*/
private String appSecret;
/**
* 消息中心主机
*/
private String host;
/**
* 消息中心端口
*/
private int port = 8997;
/**
* 心跳频率单位秒
*/
private int heartbeatRate = 5;
/**
* 最大发送次数
*/
private int maxSendCount = 4;
/**
* 定时发送消息间隔时间单位秒
*/
private int sendMessageSpan = 30;
/**
* 每次定时发送消息的数量
*/
private int sendMessageCount = 10;
/**
* 是否启用消息重发
*/
private boolean enableResend;
/**
* web socket的路径
*/
private String webSocketPath;
/**
* 触发读空闲事件的时间单位秒
*/
private int readIdleTimeOut;
/**
* 获取web socket的url地址
*/
public String getWebSocketUrl() {
String url = MessageFormat.format("ws://{0}:{1}/{2}", this.getHost(), String.valueOf(this.getPort()),
this.getWebSocketPath());
return url;
}
}

18
cip-client/src/main/java/tech/popsoft/cip/client/framework/MessageClientGlobalHolder.java

@ -0,0 +1,18 @@
package tech.popsoft.cip.client.framework;
import io.netty.channel.Channel;
import lombok.experimental.UtilityClass;
/**
* 客户端全局容器
*
* @author wqliu
* @date 2022-2-3 9:28
**/
@UtilityClass
public class MessageClientGlobalHolder {
/**
* 通道
*/
public static Channel channel = null;
}

49
cip-client/src/main/java/tech/popsoft/cip/client/framework/ResendMessage.java

@ -0,0 +1,49 @@
package tech.popsoft.cip.client.framework;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import tech.popsoft.cip.client.framework.sender.MessageSenderFactory;
import tech.popsoft.cip.client.framework.sender.RequestMessageSender;
import tech.popsoft.cip.client.manage.entity.ApiMessageLog;
import tech.popsoft.cip.client.manage.service.ApiMessageLogService;
import java.util.List;
/**
* 重发消息
*
* @author wqliu
* @date 2022-1-24
**/
@Component
@Slf4j
public class ResendMessage {
@Autowired
private ApiMessageLogService apiMessageLogService;
@Autowired
private MessageClientConfig config;
public void resend() {
// 需要进行异常处理,否则某次异常会导致定时器停止运行
try {
// 判断连接状态
if (MessageClientGlobalHolder.channel != null) {
// 查找待重发的消息
List<ApiMessageLog> list =
apiMessageLogService.getResendMessage(config.getSendMessageCount(), config.getMaxSendCount());
for (int i = 0; i < list.size(); i++) {
ApiMessageLog log = list.get(i);
// 根据消息主题构建发送器
RequestMessageSender sender = (RequestMessageSender) MessageSenderFactory.createSender(log.getRequestTopicCode());
// 传入原请求的消息标识和消息内容
sender.sendMessage(log.getRequestData(), log.getRequestId());
}
}
} catch (Exception e) {
log.error("消息重发处理异常", e);
}
}
}

48
cip-client/src/main/java/tech/popsoft/cip/client/framework/customhandler/CustomReadTimeoutHandler.java

@ -0,0 +1,48 @@
package tech.popsoft.cip.client.framework.customhandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.timeout.ReadTimeoutHandler;
import lombok.extern.slf4j.Slf4j;
import tech.abc.platform.common.utils.SpringUtil;
import tech.popsoft.cip.client.framework.MessageClient;
import tech.popsoft.cip.client.framework.MessageClientGlobalHolder;
/**
* 自定义的读超时处理器
*
* @author wqliu
* @date 2022-2-10
**/
@Slf4j
public class CustomReadTimeoutHandler extends ReadTimeoutHandler {
public CustomReadTimeoutHandler(int timeoutSeconds) {
super(timeoutSeconds);
}
@Override
protected void readTimedOut(ChannelHandlerContext ctx) throws Exception {
log.info("读空闲");
// 关闭连接
ctx.channel().close();
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
// 置空连接通道
MessageClientGlobalHolder.channel = null;
log.info("客户端检测通道失效,启动重连");
MessageClient messageClient = SpringUtil.getBean(MessageClient.class);
messageClient.reconnect();
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.info("与服务端建立连接,通道开启!");
// 必须调用父类方法,避免其他处理器的channelActive事件不再触发
super.channelActive(ctx);
}
}

72
cip-client/src/main/java/tech/popsoft/cip/client/framework/customhandler/DistinctMessageHandler.java

@ -0,0 +1,72 @@
package tech.popsoft.cip.client.framework.customhandler;
import com.alibaba.fastjson.JSON;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.ReferenceCountUtil;
import tech.abc.platform.cip.common.entity.BaseMessage;
import tech.abc.platform.cip.common.entity.RequestMessage;
import tech.abc.platform.cip.common.enums.MessageTypeEnum;
import tech.abc.platform.common.utils.SpringUtil;
import tech.popsoft.cip.client.framework.sender.MessageSenderFactory;
import tech.popsoft.cip.client.framework.sender.ResponseMessageSender;
import tech.popsoft.cip.client.manage.entity.ApiMessageLog;
import tech.popsoft.cip.client.manage.service.ApiMessageLogService;
import tech.popsoft.cip.client.manage.service.ApiMessageTopicService;
/**
* 去重处理器
*
* @author wqliu
* @date 2022-1-19
**/
public class DistinctMessageHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
private ApiMessageLogService apiMessageLogService = SpringUtil.getBean(ApiMessageLogService.class);
private ApiMessageTopicService apiMessageTopicService = SpringUtil.getBean(ApiMessageTopicService.class);
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame textWebSocketFrame) throws Exception {
String content = textWebSocketFrame.text();
BaseMessage message = JSON.parseObject(content, BaseMessage.class);
String messageType = message.getMessageType();
String messageId = message.getId();
String topic = message.getTopic();
boolean hasReceived = false;
if (messageType.equals(MessageTypeEnum.REQUEST.name())) {
hasReceived = apiMessageLogService.checkRequestMessageExist(messageId);
if (hasReceived) {
// 发送响应,终止流程
String responseTopic = apiMessageTopicService.getResponseTopicCodeByCode(topic);
ResponseMessageSender sender = (ResponseMessageSender) MessageSenderFactory.createSender(responseTopic);
ApiMessageLog log = apiMessageLogService.getByRequestMessageId(messageId);
RequestMessage reqeustMessage = JSON.parseObject(content, RequestMessage.class);
sender.sendMessage(ctx.channel(), reqeustMessage);
} else {
// 继续往下传递
ReferenceCountUtil.retain(textWebSocketFrame);
ctx.fireChannelRead(textWebSocketFrame);
}
} else if (messageType.equals(MessageTypeEnum.RESPONSE.name())) {
hasReceived = apiMessageLogService.checkResponseMessageExist(messageId);
if (hasReceived) {
// 不做处理,终止流程
} else {
// 继续往下传递
ReferenceCountUtil.retain(textWebSocketFrame);
ctx.fireChannelRead(textWebSocketFrame);
}
}
}
}

52
cip-client/src/main/java/tech/popsoft/cip/client/framework/customhandler/RequestMessageBusinessHandler.java

@ -0,0 +1,52 @@
package tech.popsoft.cip.client.framework.customhandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import tech.abc.platform.cip.common.entity.RequestMessage;
import tech.abc.platform.common.utils.SpringUtil;
import tech.popsoft.cip.client.framework.handler.MessageHandlerFactory;
import tech.popsoft.cip.client.framework.handler.RequestMessageHandler;
import tech.popsoft.cip.client.manage.service.ApiMessageLogService;
import tech.popsoft.cip.client.sender.response.system.ErrorResponseSender;
/**
* 客户端请求消息业务逻辑处理
*
* @author wqliu
* @date 2021-2-5
**/
@Slf4j
public class RequestMessageBusinessHandler extends SimpleChannelInboundHandler<RequestMessage> {
private ApiMessageLogService apiMessageLogService = SpringUtil.getBean(ApiMessageLogService.class);
@Override
protected void channelRead0(ChannelHandlerContext ctx, RequestMessage message) throws Exception {
String requestMessageId = StringUtils.EMPTY;
String topic = StringUtils.EMPTY;
try {
// 获取消息主题
topic = message.getTopic();
// 获取消息标识
requestMessageId = message.getId();
// 转具体的消息处理器进行处理
RequestMessageHandler handler = (RequestMessageHandler) MessageHandlerFactory.createHandler(topic);
handler.handleMessage(message, ctx.channel());
} catch (Exception e) {
// 记录消息请求日志
apiMessageLogService.createRequestPart(message);
String errorMessage = e.getMessage();
// 统一响应错误
ErrorResponseSender errorResponseSender = new ErrorResponseSender();
errorResponseSender.setErrorMessage(e.getMessage());
errorResponseSender.sendMessage(ctx.channel(), message);
log.error("发生异常:", e);
}
}
}

54
cip-client/src/main/java/tech/popsoft/cip/client/framework/customhandler/ResponseMessageBusinessHandler.java

@ -0,0 +1,54 @@
package tech.popsoft.cip.client.framework.customhandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import tech.abc.platform.cip.common.entity.ResponseMessage;
import tech.abc.platform.common.utils.SpringUtil;
import tech.popsoft.cip.client.framework.handler.MessageHandlerFactory;
import tech.popsoft.cip.client.framework.handler.ResponseMessageHandler;
import tech.popsoft.cip.client.manage.service.ApiMessageLogService;
/**
* 客户端响应消息业务逻辑处理
*
* @author wqliu
* @date 2021-2-5
**/
@Slf4j
public class ResponseMessageBusinessHandler extends SimpleChannelInboundHandler<ResponseMessage> {
private ApiMessageLogService apiMessageLogService = SpringUtil.getBean(ApiMessageLogService.class);
@Override
protected void channelRead0(ChannelHandlerContext ctx, ResponseMessage message) throws Exception {
String requestMessageId = StringUtils.EMPTY;
String topic = StringUtils.EMPTY;
try {
// 获取消息主题
topic = message.getTopic();
// 获取消息标识
requestMessageId = message.getId();
// 转具体的消息处理器进行处理
ResponseMessageHandler handler = (ResponseMessageHandler) MessageHandlerFactory.createHandler(topic);
handler.handleMessage(message, ctx.channel());
} catch (Exception e) {
// 更新消息日志
apiMessageLogService.updateResponsePart(message);
// 对于错误响应,只记录日志,不再向发送方反馈响应
log.error("收到客户端的响应消息有误:", e);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
log.error("发生异常", cause);
}
}

120
cip-client/src/main/java/tech/popsoft/cip/client/framework/customhandler/WebSocketClientHandshakerHandler.java

@ -0,0 +1,120 @@
package tech.popsoft.cip.client.framework.customhandler;
import io.netty.channel.*;
import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.websocketx.*;
import io.netty.util.CharsetUtil;
import io.netty.util.ReferenceCountUtil;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
import tech.abc.platform.common.utils.SpringUtil;
import tech.popsoft.cip.client.framework.MessageClientConfig;
import tech.popsoft.cip.client.sender.request.system.LoginRequestSender;
import java.net.URI;
import java.net.URISyntaxException;
/**
* 处理web socket握手
*
* @author wqliu
* @date 2021-9-28
**/
@EqualsAndHashCode(callSuper = true)
@Slf4j
@Data
public class WebSocketClientHandshakerHandler extends SimpleChannelInboundHandler<Object> {
/**
* 握手
*/
private WebSocketClientHandshaker handshaker;
/**
* 握手 异步处理
*/
private ChannelPromise handshakeFuture;
public WebSocketClientHandshakerHandler() {
// 初始化握手处理者
MessageClientConfig config = SpringUtil.getBean(MessageClientConfig.class);
URI webSocketUri = null;
try {
webSocketUri = new URI(config.getWebSocketUrl());
} catch (URISyntaxException e) {
log.error("解析远程服务器地址出错", e);
}
WebSocketClientHandshaker webSocketClientHandshaker = WebSocketClientHandshakerFactory.newHandshaker(webSocketUri,
WebSocketVersion.V13, (String) null, true, new DefaultHttpHeaders());
this.setHandshaker(webSocketClientHandshaker);
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
Channel ch = ctx.channel();
FullHttpResponse response;
// 进行握手操作
if (!this.handshaker.isHandshakeComplete()) {
try {
response = (FullHttpResponse) msg;
// 握手协议返回,设置结束握手
this.handshaker.finishHandshake(ch, response);
// 设置成功
this.handshakeFuture.setSuccess();
} catch (WebSocketHandshakeException var7) {
FullHttpResponse res = (FullHttpResponse) msg;
String errorMsg = String.format("握手失败,status:%s,reason:%s", res.status(), res.content().toString(CharsetUtil.UTF_8));
this.handshakeFuture.setFailure(new Exception(errorMsg));
}
} else if (msg instanceof FullHttpResponse) {
// 已握手成功并将http协议升级为了WebSocket协议,不应再收到Http消息,发生这种情况则抛出异常
response = (FullHttpResponse) msg;
throw new IllegalStateException("Unexpected FullHttpResponse (getStatus=" + response.status() + ", content=" + response.content().toString(CharsetUtil.UTF_8) + ')');
} else if (msg instanceof CloseWebSocketFrame) {
log.info("收到关闭信息");
} else if (msg instanceof TextWebSocketFrame) {
ReferenceCountUtil.retain(msg);
ctx.fireChannelRead(msg);
}
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) {
this.handshakeFuture = ctx.newPromise();
this.handshakeFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (future.isSuccess()) {
// 发送登录请求
log.info("握手成功");
LoginRequestSender login = new LoginRequestSender();
login.sendMessage(null);
} else {
// 握手失败
log.error("握手失败", future.cause());
}
}
});
}
/**
* 发起握手
*/
public void handshake(Channel channel) {
this.getHandshaker().handshake(channel);
}
}

45
cip-client/src/main/java/tech/popsoft/cip/client/framework/handler/MessageHandler.java

@ -0,0 +1,45 @@
package tech.popsoft.cip.client.framework.handler;
import tech.abc.platform.cip.common.entity.MessageException;
import tech.abc.platform.common.enums.StatusEnum;
import tech.abc.platform.common.utils.SpringUtil;
import tech.popsoft.cip.client.manage.entity.ApiMessageTopic;
import tech.popsoft.cip.client.manage.service.ApiMessageLogService;
import tech.popsoft.cip.client.manage.service.ApiMessageTopicService;
/**
* 消息处理抽象类
*
* @author wqliu
* @date 2021-10-13
**/
public class MessageHandler {
protected ApiMessageLogService apiMessageLogService = SpringUtil.getBean(ApiMessageLogService.class);
protected ApiMessageTopicService apiMessageTopicService = SpringUtil.getBean(ApiMessageTopicService.class);
/**
* 验证主题编码
*
* @param topicCode 主题编码
*/
protected void validateTopic(String topicCode) {
try {
ApiMessageTopic messageTopic = apiMessageTopicService.getByCode(topicCode);
if (messageTopic.getStatus().equals(StatusEnum.DEAD.name())) {
throw new MessageException("S102", "消息主题不可用");
}
} catch (Exception ex) {
throw new MessageException("S101", "消息主题不存在");
}
}
}

35
cip-client/src/main/java/tech/popsoft/cip/client/framework/handler/MessageHandlerFactory.java

@ -0,0 +1,35 @@
package tech.popsoft.cip.client.framework.handler;
import tech.abc.platform.cip.common.entity.MessageException;
import tech.abc.platform.common.utils.SpringUtil;
import tech.popsoft.cip.client.manage.service.ApiMessageTopicService;
import tech.popsoft.cip.client.platform.exception.CustomException;
/**
* 消息处理器工厂
*
* @author wqliu
* @date 2021-10-13
**/
public class MessageHandlerFactory {
public static MessageHandler createHandler(String topic) {
// 使用反射技术获取类
Class<MessageHandler> messageHandler = null;
try {
// 根据消息主题获取对应的消息处理类名
ApiMessageTopicService service = SpringUtil.getBean(ApiMessageTopicService.class);
String handlerName = service.getHandlerByCode(topic);
messageHandler = (Class<MessageHandler>) Class.forName(handlerName);
// 返回消息处理类的实例
return messageHandler.newInstance();
} catch (CustomException e) {
throw new MessageException("S101", e.getMessage());
} catch (Exception e) {
throw new MessageException("S102", "消息处理器不存在");
}
}
}

73
cip-client/src/main/java/tech/popsoft/cip/client/framework/handler/RequestMessageHandler.java

@ -0,0 +1,73 @@
package tech.popsoft.cip.client.framework.handler;
import io.netty.channel.Channel;
import lombok.extern.slf4j.Slf4j;
import tech.abc.platform.cip.common.entity.RequestMessage;
import tech.popsoft.cip.client.framework.sender.MessageSenderFactory;
import tech.popsoft.cip.client.framework.sender.ResponseMessageSender;
/**
* 请求消息处理器
*
* @author wqliu
* @date 2022-1-8
**/
@Slf4j
public class RequestMessageHandler extends MessageHandler {
/**
* 消息处理
*
* @param message 消息
* @param channel 通道
*/
public void handleMessage(RequestMessage requestMessage, Channel channel) {
// 记录消息请求日志
apiMessageLogService.createRequestPart(requestMessage);
// 消息主题验证(是否存在及是否可用)
validateTopic(requestMessage.getTopic());
// 特殊处理
messageOperation(requestMessage, channel);
// 发送响应至消息发送者
sendResponse(requestMessage, channel);
}
private void sendResponse(RequestMessage requestMessage, Channel channel) {
// 获取响应消息的消息主题
String responseTopicCode = getResponseTopicCode(requestMessage.getTopic());
// 根据消息主题构建发送器
ResponseMessageSender responseMessageSender = (ResponseMessageSender) MessageSenderFactory.createSender(responseTopicCode);
// 发送消息
responseMessageSender.sendMessage(channel, requestMessage);
}
/**
* 获取响应消息主题
*
* @return
*/
protected String getResponseTopicCode(String topic) {
// 默认从消息主题实体类中获取
return apiMessageTopicService.getResponseTopicCodeByCode(topic);
}
/**
* 请求消息处理
*
* @param message
* @param channel
*/
protected void messageOperation(RequestMessage message, Channel channel) {
}
}

47
cip-client/src/main/java/tech/popsoft/cip/client/framework/handler/ResponseMessageHandler.java

@ -0,0 +1,47 @@
package tech.popsoft.cip.client.framework.handler;
import io.netty.channel.Channel;
import lombok.extern.slf4j.Slf4j;
import tech.abc.platform.cip.common.entity.ResponseMessage;
/**
* 响应消息处理器
*
* @author wqliu
* @date 2022-1-8 11:07
**/
@Slf4j
public class ResponseMessageHandler extends MessageHandler {
/**
* 消息处理
*
* @param message 消息
* @param channel 通道
*/
public void handleMessage(ResponseMessage responseMessage, Channel channel) {
// 消息主题验证(是否存在及是否可用)
validateTopic(responseMessage.getTopic());
// 更新消息日志
apiMessageLogService.updateResponsePart(responseMessage);
// 特殊处理
messageOperation(responseMessage, channel);
}
/**
* 响应消息处理
*
* @param message
* @param channel
*/
protected void messageOperation(ResponseMessage message, Channel channel) {
}
}

69
cip-client/src/main/java/tech/popsoft/cip/client/framework/sender/MessageSender.java

@ -0,0 +1,69 @@
package tech.popsoft.cip.client.framework.sender;
import lombok.extern.slf4j.Slf4j;
import tech.abc.platform.common.utils.SpringUtil;
import tech.popsoft.cip.client.framework.MessageClientConfig;
import tech.popsoft.cip.client.manage.service.ApiMessageLogService;
/**
* 消息发送器基类
*
* @author wqliu
* @date 2021-10-5 14:02
*/
@Slf4j
public class MessageSender {
public MessageSender() {
}
/**
* 消息主题
*/
private String topic;
/**
* 消息内容
*/
private String content;
public MessageSender(String topic) {
this.topic = topic;
}
/**
* 获取消息主题
*/
public String getTopic() {
return this.topic;
}
/**
* 设置消息内容
*
* @param content
*/
public void setContent(String content) {
this.content = content;
}
/**
* 获取消息内容
*/
public String getContent() {
return this.content;
}
/**
* 应用程序配置
*/
protected MessageClientConfig messageClientConfig = SpringUtil.getBean(MessageClientConfig.class);
/**
* 消息日志服务
*/
protected ApiMessageLogService apiMessageLogService = SpringUtil.getBean(ApiMessageLogService.class);
}

38
cip-client/src/main/java/tech/popsoft/cip/client/framework/sender/MessageSenderFactory.java

@ -0,0 +1,38 @@
package tech.popsoft.cip.client.framework.sender;
import tech.abc.platform.cip.common.entity.MessageException;
import tech.abc.platform.common.exception.CustomException;
import tech.abc.platform.common.utils.SpringUtil;
import tech.popsoft.cip.client.manage.service.ApiMessageTopicService;
/**
* 消息发送器工厂
*
* @author wqliu
* @date 2022-01-29 16:30
**/
public class MessageSenderFactory {
private MessageSenderFactory() {
}
;
public static MessageSender createSender(String topic) {
// 使用反射技术获取类
Class<MessageSender> messageSender = null;
try {
// 根据消息主题获取对应的消息处理类名
ApiMessageTopicService service = SpringUtil.getBean(ApiMessageTopicService.class);
String senderName = service.getSenderByCode(topic);
messageSender = (Class<MessageSender>) Class.forName(senderName);
// 返回消息发送器类的实例
return messageSender.newInstance();
} catch (CustomException e) {
throw new MessageException("S101", e.getMessage());
} catch (Exception e) {
throw new MessageException("S102", "消息发送器不存在");
}
}
}

105
cip-client/src/main/java/tech/popsoft/cip/client/framework/sender/RequestMessageSender.java

@ -0,0 +1,105 @@
package tech.popsoft.cip.client.framework.sender;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import tech.abc.platform.cip.common.entity.RequestMessage;
import tech.abc.platform.cip.common.enums.MessageStatusEnum;
import tech.popsoft.cip.client.framework.MessageClientGlobalHolder;
import tech.popsoft.cip.client.manage.entity.ApiMessageLog;
/**
* 请求消息发送器
*
* @author wqliu
* @date 2021-10-5
*/
@Slf4j
public class RequestMessageSender extends MessageSender {
public RequestMessageSender(String topic) {
super(topic);
}
/**
* 发送消息
*
* @param content 消息内容
* @param id 消息标识
*/
public void sendMessage(String content, String id) {
// 生成请求消息
RequestMessage message = new RequestMessage();
// 使用已有ID重置默认生成的ID,用于关联消息
if (StringUtils.isNotBlank(id)) {
message.setId(id);
}
// 设置相关属性
message.setTopic(super.getTopic());
// 参数中消息内容优先,如为空,取对象属性的值
if (StringUtils.isNotBlank(content)) {
message.setContent(content);
} else {
message.setContent(this.getContent());
}
message.setPublishAppCode(messageClientConfig.getAppCode());
// 获取发送通道
Channel channel = MessageClientGlobalHolder.channel;
if (channel != null && channel.isActive()) {
ChannelFuture channelFuture = channel.writeAndFlush(message);
channelFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (StringUtils.isBlank(id)) {
// 创建日志
ApiMessageLog log = apiMessageLogService.createRequestPart(message);
// 设置状态为已请求
apiMessageLogService.updateStatus(MessageStatusEnum.REQUESTED.name(), log.getRequestId());
// 发送次数加1
apiMessageLogService.incrementSendCount(log.getRequestId());
} else {
// 更新发送次数
apiMessageLogService.incrementSendCount(id);
}
}
});
} else {
// 创建日志
apiMessageLogService.createRequestPart(message);
}
}
/**
* 发送请求消息
*
* @param content 消息内容
*/
public void sendMessage(String content) {
// 发送消息
sendMessage(content, null);
}
/**
* 发送请求消息
*
* @param content 消息内容
*/
public void sendMessage() {
// 发送消息
sendMessage(null, null);
}
}

74
cip-client/src/main/java/tech/popsoft/cip/client/framework/sender/ResponseMessageSender.java

@ -0,0 +1,74 @@
package tech.popsoft.cip.client.framework.sender;
import io.netty.channel.Channel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import tech.abc.platform.cip.common.entity.RequestMessage;
import tech.abc.platform.cip.common.entity.ResponseMessage;
import tech.abc.platform.cip.common.enums.MessageResponseResultEnum;
/**
* 响应消息发送器
*
* @author wqliu
* @date 2022-1-31 8:14
**/
@EqualsAndHashCode(callSuper = true)
@Data
public class ResponseMessageSender extends MessageSender {
private String result;
private String errorCode;
private String errorMessage;
public ResponseMessageSender(String topic) {
super(topic);
// 默认设置响应结果为成功
this.result = MessageResponseResultEnum.SUCCESS.name();
}
/**
* 发送响应
*
* @param channel 通道
* @param requestMessage 请求消息
*/
public void sendMessage(Channel channel, RequestMessage requestMessage) {
// 组织响应消息
ResponseMessage responseMessage = new ResponseMessage();
responseMessage.setPublishAppCode(messageClientConfig.getAppCode());
responseMessage.setRequestMessageId(requestMessage.getId());
// 设置主题
responseMessage.setTopic(this.getTopic());
// 设置默认值
responseMessage.setResult(this.result);
responseMessage.setErrorCode(this.errorCode);
responseMessage.setErrorMessage(this.errorMessage);
// 设置响应
setResponseContent(requestMessage, responseMessage, channel);
// 发送响应给请求方
channel.writeAndFlush(responseMessage);
// 更新消息日志
apiMessageLogService.updateResponsePart(responseMessage);
}
/**
* 设置响应消息内容
*
* @param requestMessage 请求消息
* @param responseMessage 响应消息
* @param channel 通道
*/
protected void setResponseContent(RequestMessage requestMessage, ResponseMessage responseMessage, Channel channel) {
}
}

24
cip-client/src/main/java/tech/popsoft/cip/client/handler/request/lms/transportbill/ConsignmentBillCreateRequestHandler.java

@ -0,0 +1,24 @@
package tech.popsoft.cip.client.handler.request.lms.transportbill;
import io.netty.channel.Channel;
import lombok.extern.slf4j.Slf4j;
import tech.abc.platform.cip.common.entity.RequestMessage;
import tech.popsoft.cip.client.framework.handler.RequestMessageHandler;
/**
* 委托单创建请求消息处理器
*
* @author wqliu
* @date 2022-1-22
**/
@Slf4j
public class ConsignmentBillCreateRequestHandler extends RequestMessageHandler {
@Override
protected void messageOperation(RequestMessage message, Channel channel) {
// 进行业务处理
}
}

22
cip-client/src/main/java/tech/popsoft/cip/client/handler/response/system/ErrorResponseHandler.java

@ -0,0 +1,22 @@
package tech.popsoft.cip.client.handler.response.system;
import io.netty.channel.Channel;
import lombok.extern.slf4j.Slf4j;
import tech.abc.platform.cip.common.entity.ResponseMessage;
import tech.popsoft.cip.client.framework.handler.ResponseMessageHandler;
/**
* 错误响应处理器
*
* @author wqliu
* @date 2022-1-16 8:53
**/
@Slf4j
public class ErrorResponseHandler extends ResponseMessageHandler {
@Override
protected void messageOperation(ResponseMessage message, Channel channel) {
log.info("收到错误响应");
}
}

67
cip-client/src/main/java/tech/popsoft/cip/client/handler/response/system/LoginResponseHandler.java

@ -0,0 +1,67 @@
package tech.popsoft.cip.client.handler.response.system;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoop;
import io.netty.handler.codec.http.websocketx.PingWebSocketFrame;
import lombok.extern.slf4j.Slf4j;
import tech.abc.platform.cip.common.entity.ResponseMessage;
import tech.abc.platform.cip.common.enums.MessageResponseResultEnum;
import tech.abc.platform.common.utils.SpringUtil;
import tech.popsoft.cip.client.framework.MessageClientConfig;
import tech.popsoft.cip.client.framework.MessageClientGlobalHolder;
import tech.popsoft.cip.client.framework.handler.ResponseMessageHandler;
import java.util.concurrent.TimeUnit;
/**
* 登录响应处理器
*
* @author wqliu
* @date 2022-1-16
**/
@Slf4j
public class LoginResponseHandler extends ResponseMessageHandler {
@Override
protected void messageOperation(ResponseMessage message, Channel channel) {
log.info("收到登录响应");
if (message.getResult().equals(MessageResponseResultEnum.SUCCESS.name())) {
// 登录成功,将连接保存到全局变量
MessageClientGlobalHolder.channel = channel;
// 启动心跳
startHeartbeat(channel);
} else {
// 登录失败,关闭通道,重新连接
log.error("登录失败:{}", message.getErrorMessage());
channel.close();
}
}
private void startHeartbeat(Channel channel) {
// 获取心跳发送频率
MessageClientConfig config = SpringUtil.getBean(MessageClientConfig.class);
int heartbeatRate = config.getHeartbeatRate();
// 启动心跳
EventLoop eventLoop = channel.eventLoop();
eventLoop.scheduleWithFixedDelay(new Runnable() {
private Channel channel;
@Override
public void run() {
// log.info("发送心跳");
PingWebSocketFrame frame = new PingWebSocketFrame();
ChannelFuture channelFuture = channel.writeAndFlush(frame);
}
public Runnable setChannel(Channel channel) {
this.channel = channel;
return this;
}
}.setChannel(channel), 15, heartbeatRate, TimeUnit.SECONDS);
}
}

15
cip-client/src/main/java/tech/popsoft/cip/client/handler/response/system/MessageConfirmResponseHandler.java

@ -0,0 +1,15 @@
package tech.popsoft.cip.client.handler.response.system;
import lombok.extern.slf4j.Slf4j;
import tech.popsoft.cip.client.framework.handler.ResponseMessageHandler;
/**
* 消息确认 响应处理器
*
* @author wqliu
* @date 2022-1-16
**/
@Slf4j
public class MessageConfirmResponseHandler extends ResponseMessageHandler {
}

185
cip-client/src/main/java/tech/popsoft/cip/client/manage/controller/ApiMessageLogController.java

@ -0,0 +1,185 @@
package tech.popsoft.cip.client.manage.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import tech.abc.platform.common.annotation.SystemLog;
import tech.abc.platform.common.base.BaseController;
import tech.abc.platform.common.query.QueryGenerator;
import tech.abc.platform.common.utils.ResultUtil;
import tech.abc.platform.common.vo.PageInfo;
import tech.abc.platform.common.vo.Result;
import tech.abc.platform.common.vo.SortInfo;
import tech.popsoft.cip.client.manage.entity.ApiMessageLog;
import tech.popsoft.cip.client.manage.service.ApiMessageLogService;
import tech.popsoft.cip.client.manage.vo.ApiMessageLogVO;
import java.util.ArrayList;
import java.util.List;
/**
* 消息日志 前端控制器
*
* @author wqliu
* @date 2021-08-21
*/
@RestController
@RequestMapping("/cip/apiMessageLog")
@Slf4j
public class ApiMessageLogController extends BaseController {
@Autowired
private ApiMessageLogService apiMessageLogService;
// region 基本操作
/**
* 初始化
*/
@ApiOperation(value = "初始化")
@GetMapping("/init")
public ResponseEntity<Result> init() {
ApiMessageLog entity = apiMessageLogService.init();
ApiMessageLogVO vo = convert2VO(entity);
return ResultUtil.success(vo);
}
/**
* 新增
*/
@ApiOperation(value = "新增")
@PostMapping("/")
@SystemLog(value = "消息日志-新增")
@PreAuthorize("hasPermission(null,'cip:apiMessageLog:add')")
public ResponseEntity<Result> add(@Validated @RequestBody ApiMessageLogVO vo) {
ApiMessageLog entity = convert2Entity(vo);
apiMessageLogService.add(entity);
ApiMessageLogVO newVO = convert2VO(entity);
return ResultUtil.success(newVO);
}
/**
* 修改
*/
@ApiOperation(value = "修改")
@PutMapping("/")
@SystemLog(value = "消息日志-修改")
@PreAuthorize("hasPermission(null,'cip:apiMessageLog:modify')")
public ResponseEntity<Result> modify(@Validated @RequestBody ApiMessageLogVO vo) {
ApiMessageLog entity = convert2Entity(vo);
// 此处数据库会更新 更新人和更新时间,但数据模型不会刷新
// 个别需展示的该类信息的地方可以重新查询后返回
apiMessageLogService.modify(entity);
ApiMessageLogVO newVO = convert2VO(entity);
return ResultUtil.success(newVO);
}
/**
* 删除数据单条数据标识或多条数据标识用逗号间隔拼成的字符串
*/
@ApiOperation(value = "删除")
@ApiImplicitParam(name = "id", value = "标识", required = true, dataType = "String", paramType = "path")
@DeleteMapping("/{id}")
@SystemLog(value = "消息日志-删除")
@PreAuthorize("hasPermission(null,'cip:apiMessageLog:remove')")
public ResponseEntity<Result> remove(@PathVariable("id") String id) {
apiMessageLogService.remove(id);
return ResultUtil.success();
}
/**
* 分页
*/
@ApiOperation(value = "分页查询")
@GetMapping("/page")
@SystemLog(value = "消息日志-分页")
@PreAuthorize("hasPermission(null,'cip:apiMessageLog:query')")
public ResponseEntity<Result> page(ApiMessageLogVO queryVO, PageInfo pageInfo, SortInfo sortInfo) {
// 构造分页对象
IPage<ApiMessageLog> page = new Page<ApiMessageLog>(pageInfo.getPageNum(), pageInfo.getPageSize());
// 构造查询条件
QueryWrapper<ApiMessageLog> queryWrapper = QueryGenerator.generateQueryWrapper(ApiMessageLog.class, queryVO, sortInfo);
// 查询数据
apiMessageLogService.page(page, queryWrapper);
// 转换vo
IPage<ApiMessageLogVO> pageVO = mapperFacade.map(page, IPage.class);
List<ApiMessageLogVO> apiMessageLogVOList = new ArrayList<>();
for (int i = 0; i < page.getRecords().size(); i++) {
ApiMessageLogVO vo = convert2VO(page.getRecords().get(i));
apiMessageLogVOList.add(vo);
}
pageVO.setRecords(apiMessageLogVOList);
;
return ResultUtil.success(pageVO);
}
/**
* 列表
*/
@ApiOperation(value = "获取列表")
@GetMapping("/list")
@SystemLog(value = "消息日志-列表")
@PreAuthorize("hasPermission(null,'cip:apiMessageLog:query')")
public ResponseEntity<Result> list(ApiMessageLogVO queryVO, SortInfo sortInfo) {
// 构造查询条件
QueryWrapper<ApiMessageLog> queryWrapper = QueryGenerator.generateQueryWrapper(ApiMessageLog.class, queryVO, sortInfo);
List<ApiMessageLog> list = apiMessageLogService.list(queryWrapper);
// 转换vo
List<ApiMessageLogVO> apiMessageLogVOList = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
ApiMessageLogVO vo = convert2VO(list.get(i));
apiMessageLogVOList.add(vo);
}
return ResultUtil.success(apiMessageLogVOList);
}
/**
* 获取单条数据
*/
@ApiOperation(value = "获取单条数据")
@ApiImplicitParam(name = "id", value = "标识", required = true, dataType = "String", paramType = "path")
@GetMapping("/{id}")
@SystemLog(value = "消息日志-详情")
@PreAuthorize("hasPermission(null,'cip:apiMessageLog:view')")
public ResponseEntity<Result> get(@PathVariable("id") String id) {
ApiMessageLog entity = apiMessageLogService.query(id);
ApiMessageLogVO vo = convert2VO(entity);
return ResultUtil.success(vo);
}
// endregion
// region 扩展操作
// endregion
// region 辅助操作
private ApiMessageLogVO convert2VO(ApiMessageLog entity) {
ApiMessageLogVO vo = mapperFacade.map(entity, ApiMessageLogVO.class);
String name = dictionaryUtil.getNameByCode("MessageResponseResult", entity.getResponseResult());
vo.setResponseResultName(name);
String statusName = dictionaryUtil.getNameByCode("MessageStatus", entity.getResponseResult());
vo.setStatusName(statusName);
return vo;
}
private ApiMessageLog convert2Entity(ApiMessageLogVO vo) {
ApiMessageLog entity = mapperFacade.map(vo, ApiMessageLog.class);
return entity;
}
// endregion
}

224
cip-client/src/main/java/tech/popsoft/cip/client/manage/controller/ApiMessageTopicController.java

@ -0,0 +1,224 @@
package tech.popsoft.cip.client.manage.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import tech.abc.platform.common.annotation.SystemLog;
import tech.abc.platform.common.base.BaseController;
import tech.abc.platform.common.query.QueryGenerator;
import tech.abc.platform.common.utils.ResultUtil;
import tech.abc.platform.common.vo.PageInfo;
import tech.abc.platform.common.vo.Result;
import tech.abc.platform.common.vo.SortInfo;
import tech.popsoft.cip.client.manage.entity.ApiMessageTopic;
import tech.popsoft.cip.client.manage.service.ApiMessageTopicService;
import tech.popsoft.cip.client.manage.vo.ApiMessageTopicVO;
import java.util.ArrayList;
import java.util.List;
/**
* 消息主题 前端控制器
*
* @author wqliu
* @date 2021-08-21
*/
@RestController
@RequestMapping("/cip/apiMessageTopic")
@Slf4j
public class ApiMessageTopicController extends BaseController {
@Autowired
private ApiMessageTopicService apiMessageTopicService;
// region 基本操作
/**
* 初始化
*/
@ApiOperation(value = "初始化")
@GetMapping("/init")
public ResponseEntity<Result> init() {
ApiMessageTopic entity = apiMessageTopicService.init();
ApiMessageTopicVO vo = convert2VO(entity);
return ResultUtil.success(vo);
}
/**
* 新增
*/
@ApiOperation(value = "新增")
@PostMapping("/")
@SystemLog(value = "消息主题-新增")
@PreAuthorize("hasPermission(null,'cip:apiMessageTopic:add')")
public ResponseEntity<Result> add(@Validated @RequestBody ApiMessageTopicVO vo) {
ApiMessageTopic entity = convert2Entity(vo);
apiMessageTopicService.add(entity);
ApiMessageTopicVO newVO = convert2VO(entity);
return ResultUtil.success(newVO);
}
/**
* 修改
*/
@ApiOperation(value = "修改")
@PutMapping("/")
@SystemLog(value = "消息主题-修改")
@PreAuthorize("hasPermission(null,'cip:apiMessageTopic:modify')")
public ResponseEntity<Result> modify(@Validated @RequestBody ApiMessageTopicVO vo) {
ApiMessageTopic entity = convert2Entity(vo);
// 此处数据库会更新 更新人和更新时间,但数据模型不会刷新
// 个别需展示的该类信息的地方可以重新查询后返回
apiMessageTopicService.modify(entity);
ApiMessageTopicVO newVO = convert2VO(entity);
return ResultUtil.success(newVO);
}
/**
* 删除数据单条数据标识或多条数据标识用逗号间隔拼成的字符串
*/
@ApiOperation(value = "删除")
@ApiImplicitParam(name = "id", value = "标识", required = true, dataType = "String", paramType = "path")
@DeleteMapping("/{id}")
@SystemLog(value = "消息主题-删除")
@PreAuthorize("hasPermission(null,'cip:apiMessageTopic:remove')")
public ResponseEntity<Result> remove(@PathVariable("id") String id) {
apiMessageTopicService.remove(id);
return ResultUtil.success();
}
/**
* 分页
*/
@ApiOperation(value = "分页查询")
@GetMapping("/page")
@SystemLog(value = "消息主题-分页")
@PreAuthorize("hasPermission(null,'cip:apiMessageTopic:query')")
public ResponseEntity<Result> page(ApiMessageTopicVO queryVO, PageInfo pageInfo, SortInfo sortInfo) {
// 构造分页对象
IPage<ApiMessageTopic> page = new Page<ApiMessageTopic>(pageInfo.getPageNum(), pageInfo.getPageSize());
// 构造查询条件
QueryWrapper<ApiMessageTopic> queryWrapper = QueryGenerator.generateQueryWrapper(ApiMessageTopic.class, queryVO, sortInfo);
// 查询数据
apiMessageTopicService.page(page, queryWrapper);
// 转换vo
IPage<ApiMessageTopicVO> pageVO = mapperFacade.map(page, IPage.class);
List<ApiMessageTopicVO> apiMessageTopicVOList = new ArrayList<>();
for (int i = 0; i < page.getRecords().size(); i++) {
ApiMessageTopicVO vo = convert2VO(page.getRecords().get(i));
apiMessageTopicVOList.add(vo);
}
pageVO.setRecords(apiMessageTopicVOList);
;
return ResultUtil.success(pageVO);
}
/**
* 列表
*/
@ApiOperation(value = "获取列表")
@GetMapping("/list")
@SystemLog(value = "消息主题-列表")
@PreAuthorize("hasPermission(null,'cip:apiMessageTopic:query')")
public ResponseEntity<Result> list(ApiMessageTopicVO queryVO, SortInfo sortInfo) {
// 构造查询条件
QueryWrapper<ApiMessageTopic> queryWrapper = QueryGenerator.generateQueryWrapper(ApiMessageTopic.class, queryVO, sortInfo);
List<ApiMessageTopic> list = apiMessageTopicService.list(queryWrapper);
// 转换vo
List<ApiMessageTopicVO> apiMessageTopicVOList = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
ApiMessageTopicVO vo = convert2VO(list.get(i));
apiMessageTopicVOList.add(vo);
}
return ResultUtil.success(apiMessageTopicVOList);
}
/**
* 获取单条数据
*/
@ApiOperation(value = "获取单条数据")
@ApiImplicitParam(name = "id", value = "标识", required = true, dataType = "String", paramType = "path")
@GetMapping("/{id}")
@SystemLog(value = "消息主题-详情")
@PreAuthorize("hasPermission(null,'cip:apiMessageTopic:view')")
public ResponseEntity<Result> get(@PathVariable("id") String id) {
ApiMessageTopic entity = apiMessageTopicService.query(id);
ApiMessageTopicVO vo = convert2VO(entity);
return ResultUtil.success(vo);
}
// endregion
// region 扩展操作
/**
* 启用
*/
@ApiOperation(value = "启用")
@PutMapping("/{id}/enable")
@ApiImplicitParam(name = "id", value = "标识", required = true, dataType = "String", paramType = "path")
@SystemLog(value = "消息主题-启用")
@PreAuthorize("hasPermission(null,'cip:apiMessageTopic:enable')")
public ResponseEntity<Result> enable(@PathVariable("id") String id) {
apiMessageTopicService.enable(id);
return ResultUtil.success();
}
/**
* 停用
*/
@ApiOperation(value = "停用")
@PutMapping("/{id}/disable")
@ApiImplicitParam(name = "id", value = "标识", required = true, dataType = "String", paramType = "path")
@SystemLog(value = "消息主题-停用")
@PreAuthorize("hasPermission(null,'cip:apiMessageTopic:disable')")
public ResponseEntity<Result> disable(@PathVariable("id") String id) {
apiMessageTopicService.disable(id);
return ResultUtil.success();
}
// endregion
// region 辅助操作
private ApiMessageTopicVO convert2VO(ApiMessageTopic entity) {
ApiMessageTopicVO vo = mapperFacade.map(entity, ApiMessageTopicVO.class);
String statusName = dictionaryUtil.getNameByCode("Status", entity.getStatus());
vo.setStatusName(statusName);
String categoryName = dictionaryUtil.getNameByCode("ApiMessageTopicCategory", entity.getCategory());
vo.setCategoryName(categoryName);
if (StringUtils.isNotBlank(entity.getHasPermission())) {
String hasPermission = dictionaryUtil.getNameByCode("YesOrNo", entity.getHasPermission());
vo.setHasPermissionName(hasPermission);
}
if (StringUtils.isNotBlank(entity.getHasSubscription())) {
String hasSubscription = dictionaryUtil.getNameByCode("YesOrNo", entity.getHasSubscription());
vo.setHasSubscriptionName(hasSubscription);
}
return vo;
}
private ApiMessageTopic convert2Entity(ApiMessageTopicVO vo) {
ApiMessageTopic entity = mapperFacade.map(vo, ApiMessageTopic.class);
return entity;
}
// endregion
}

116
cip-client/src/main/java/tech/popsoft/cip/client/manage/entity/ApiMessageLog.java

@ -0,0 +1,116 @@
package tech.popsoft.cip.client.manage.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import tech.abc.platform.common.base.BaseEntity;
import java.time.LocalDateTime;
/**
* 消息日志
*
* @author wqliu
* @date 2021-08-21
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@TableName("cip_message_log")
public class ApiMessageLog extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
* 请求消息标识
*/
@TableField("request_id")
private String requestId;
/**
* 请求应用编码
*/
@TableField("request_app_code")
private String requestAppCode;
/**
* 请求消息主题编码
*/
@TableField("request_topic_code")
private String requestTopicCode;
/**
* 请求时间
*/
@TableField("request_time")
private LocalDateTime requestTime;
/**
* 请求内容
*/
@TableField("request_data")
private String requestData;
/**
* 响应消息标识
*/
@TableField("response_id")
private String responseId;
/**
* 响应应用编码
*/
@TableField("response_app_code")
private String responseAppCode;
/**
* 响应消息主题编码
*/
@TableField("response_topic_code")
private String responseTopicCode;
/**
* 响应时间
*/
@TableField("response_time")
private LocalDateTime responseTime;
/**
* 响应内容
*/
@TableField("response_data")
private String responseData;
/**
* 响应结果
*/
@TableField("response_result")
private String responseResult;
/**
* 错误编码
*/
@TableField("error_code")
private String errorCode;
/**
* 错误信息
*/
@TableField("error_message")
private String errorMessage;
/**
* 当前状态
*/
@TableField("status")
private String status;
/**
* 发送次数
*/
@TableField("send_count")
private Integer sendCount;
}

93
cip-client/src/main/java/tech/popsoft/cip/client/manage/entity/ApiMessageTopic.java

@ -0,0 +1,93 @@
package tech.popsoft.cip.client.manage.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import tech.abc.platform.common.base.BaseEntity;
/**
* 消息主题
*
* @author wqliu
* @date 2021-08-21
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@TableName("cip_message_topic")
public class ApiMessageTopic extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
* 编码
*/
@TableField("code")
private String code;
/**
* 名称
*/
@TableField("name")
private String name;
/**
* 处理器
*/
@TableField("handler")
private String handler;
/**
* 发送器
*/
@TableField("sender")
private String sender;
/**
* 响应主题编码
*/
@TableField("response_topic_code")
private String responseTopicCode;
/**
* 分类
*/
@TableField("category")
private String category;
/**
* 状态
*/
@TableField("status")
private String status;
/**
* 备注
*/
@TableField("remark")
private String remark;
/**
* 排序号
*/
@TableField("order_no")
private String orderNo;
/**
* 是否已授权
*/
@TableField(exist = false)
private String hasPermission;
/**
* 是否已订阅
*/
@TableField(exist = false)
private String hasSubscription;
}

28
cip-client/src/main/java/tech/popsoft/cip/client/manage/exception/ApiMessageLogExceptionEnum.java

@ -0,0 +1,28 @@
package tech.popsoft.cip.client.manage.exception;
import lombok.Getter;
import tech.abc.platform.common.exception.ExceptionInterface;
/**
* 消息主题相关异常
*
* @author wqliu
* @date: 2020-03-29 19:11
*/
@Getter
public enum ApiMessageLogExceptionEnum implements ExceptionInterface {
/**
* 消息日志不存在
*/
MESSAGE_LOG_NOT_EXIST("消息日志不存在"),
;
private String message;
ApiMessageLogExceptionEnum(String message) {
this.message = message;
}
}

39
cip-client/src/main/java/tech/popsoft/cip/client/manage/exception/ApiMessageTopicExceptionEnum.java

@ -0,0 +1,39 @@
package tech.popsoft.cip.client.manage.exception;
import lombok.Getter;
import tech.abc.platform.common.exception.ExceptionInterface;
/**
* 消息主题相关异常
*
* @author wqliu
* @date: 2020-03-29 19:11
*/
@Getter
public enum ApiMessageTopicExceptionEnum implements ExceptionInterface {
/**
* 消息主题不存在
*/
TOPIC_NOT_EXIST("消息主题不存在"),
/**
* 消息主题未设置处理器
*/
TOPIC_NOT_SET_HANDLER("消息主题未设置处理器"),
/**
* 消息主题未设置发送器
*/
TOPIC_NOT_SET_SENDER("消息主题未设置发送器"),
/**
* 响应消息主题未设置处理器
*/
RESPONSE_TOPIC_NOT_SET_HANDLER("响应消息主题未设置处理器"),
;
private String message;
ApiMessageTopicExceptionEnum(String message) {
this.message = message;
}
}

15
cip-client/src/main/java/tech/popsoft/cip/client/manage/mapper/ApiMessageLogMapper.java

@ -0,0 +1,15 @@
package tech.popsoft.cip.client.manage.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import tech.popsoft.cip.client.manage.entity.ApiMessageLog;
/**
* 消息日志 Mapper 接口
*
* @author wqliu
* @date 2021-08-21
*/
public interface ApiMessageLogMapper extends BaseMapper<ApiMessageLog> {
}

5
cip-client/src/main/java/tech/popsoft/cip/client/manage/mapper/ApiMessageLogMapper.xml

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="tech.popsoft.cip.client.manage.mapper.ApiMessageLogMapper">
</mapper>

15
cip-client/src/main/java/tech/popsoft/cip/client/manage/mapper/ApiMessageTopicMapper.java

@ -0,0 +1,15 @@
package tech.popsoft.cip.client.manage.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import tech.popsoft.cip.client.manage.entity.ApiMessageTopic;
/**
* 消息主题 Mapper 接口
*
* @author wqliu
* @date 2021-08-21
*/
public interface ApiMessageTopicMapper extends BaseMapper<ApiMessageTopic> {
}

5
cip-client/src/main/java/tech/popsoft/cip/client/manage/mapper/ApiMessageTopicMapper.xml

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="tech.popsoft.cip.client.manage.mapper.ApiMessageTopicMapper">
</mapper>

92
cip-client/src/main/java/tech/popsoft/cip/client/manage/service/ApiMessageLogService.java

@ -0,0 +1,92 @@
package tech.popsoft.cip.client.manage.service;
import tech.abc.platform.cip.common.entity.RequestMessage;
import tech.abc.platform.cip.common.entity.ResponseMessage;
import tech.abc.platform.common.base.BaseService;
import tech.popsoft.cip.client.manage.entity.ApiMessageLog;
import java.util.List;
/**
* 消息日志 服务类
*
* @author wqliu
* @date 2021-08-21
*/
public interface ApiMessageLogService extends BaseService<ApiMessageLog> {
/**
* 检查请求消息是否已存在
*
* @param requestMessageId 请求消息标识
* @return true 存在 false 不存在
*/
boolean checkRequestMessageExist(String requestMessageId);
/**
* 检查响应消息是否已存在
*
* @param responseMessageId 响应消息标识
* @return true 存在 false 不存在
*/
boolean checkResponseMessageExist(String responseMessageId);
/**
* 根据请求消息标识获取消息日志对象
*
* @param requestMessageId 请求消息标识
* @return 消息日志对象
*/
ApiMessageLog getByRequestMessageId(String requestMessageId);
/**
* 更新状态
*
* @param status 状态
* @param messageId 消息标识
*/
void updateStatus(String status, String messageId);
/**
* 递增发送计数
*
* @param messageId 消息标识
*/
void incrementSendCount(String messageId);
/**
* 创建日志填充请求消息部分
*
* @param message
*/
ApiMessageLog createRequestPart(RequestMessage message);
/**
* 创建日志填充请求消息部分
*
* @param message
* @param responseAppCode 响应应用编码
*/
ApiMessageLog createRequestPart(RequestMessage message, String responseAppCode);
/**
* 更新日志填充响应消息部分
*
* @param message
*/
void updateResponsePart(ResponseMessage message);
/**
* 获取重发消息
*
* @param messageCount 消息数量
* @param maxSendCount 最大发送次数
* @return {@link List}<{@link ApiMessageLog}>
*/
List<ApiMessageLog> getResendMessage(int messageCount, int maxSendCount);
}

72
cip-client/src/main/java/tech/popsoft/cip/client/manage/service/ApiMessageTopicService.java

@ -0,0 +1,72 @@
package tech.popsoft.cip.client.manage.service;
import tech.abc.platform.common.base.BaseService;
import tech.popsoft.cip.client.manage.entity.ApiMessageTopic;
/**
* 消息主题 服务类
*
* @author wqliu
* @date 2021-08-21
*/
public interface ApiMessageTopicService extends BaseService<ApiMessageTopic> {
/**
* 启用
*
* @param id 标识
*/
void enable(String id);
/**
* 停用
*
* @param id 标识
*/
void disable(String id);
/**
* 根据消息主题编码获取处理类
*
* @param code 消息主题
* @return 处理类名含路径
*/
String getHandlerByCode(String code);
/**
* 根据消息主题编码获取响应主题编码
*
* @param code 消息主题
* @return 响应主题编码
*/
String getResponseTopicCodeByCode(String code);
/**
* 根据消息主题编码获取消息主题标识
*
* @param code 消息主题编码
* @return 消息主题标识
*/
String getIdByCode(String code);
/**
* 根据消息主题编码获取消息主题对象
*
* @param code 消息主题编码
* @return 消息主题对象
*/
ApiMessageTopic getByCode(String code);
/**
* 根据消息主题编码获取发送类
*
* @param code 消息主题
* @return 发送类名含路径
*/
String getSenderByCode(String code);
}

146
cip-client/src/main/java/tech/popsoft/cip/client/manage/service/impl/ApiMessageLogServiceImpl.java

@ -0,0 +1,146 @@
package tech.popsoft.cip.client.manage.service.impl;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.stereotype.Service;
import tech.abc.platform.cip.common.entity.RequestMessage;
import tech.abc.platform.cip.common.entity.ResponseMessage;
import tech.abc.platform.cip.common.enums.MessageStatusEnum;
import tech.abc.platform.common.base.BaseServiceImpl;
import tech.abc.platform.common.exception.CustomException;
import tech.popsoft.cip.client.manage.entity.ApiMessageLog;
import tech.popsoft.cip.client.manage.exception.ApiMessageLogExceptionEnum;
import tech.popsoft.cip.client.manage.mapper.ApiMessageLogMapper;
import tech.popsoft.cip.client.manage.service.ApiMessageLogService;
import java.time.LocalDateTime;
import java.util.List;
/**
* 消息日志 服务实现类
*
* @author wqliu
* @date 2021-08-21
*/
@Service
@Slf4j
public class ApiMessageLogServiceImpl extends BaseServiceImpl<ApiMessageLogMapper, ApiMessageLog> implements ApiMessageLogService {
@Override
public ApiMessageLog init() {
ApiMessageLog entity = new ApiMessageLog();
return entity;
}
@Override
public boolean checkRequestMessageExist(String requestMessageId) {
long count = this.lambdaQuery().eq(ApiMessageLog::getRequestId, requestMessageId).count();
return count > 0;
}
@Override
public boolean checkResponseMessageExist(String responseMessageId) {
long count = this.lambdaQuery().eq(ApiMessageLog::getResponseId, responseMessageId).count();
return count > 0;
}
@Override
public ApiMessageLog getByRequestMessageId(String requestMessageId) {
List<ApiMessageLog> list = this.lambdaQuery().eq(ApiMessageLog::getRequestId, requestMessageId).list();
if (CollectionUtils.isNotEmpty(list)) {
return list.get(0);
} else {
throw new CustomException(ApiMessageLogExceptionEnum.MESSAGE_LOG_NOT_EXIST);
}
}
@Override
public void updateStatus(String status, String messageId) {
ApiMessageLog apiMessageLog = getByRequestMessageId(messageId);
apiMessageLog.setStatus(status);
modify(apiMessageLog);
}
@Override
public ApiMessageLog createRequestPart(RequestMessage message, String responseAppCode) {
ApiMessageLog log = new ApiMessageLog();
log.setRequestId(message.getId());
log.setRequestAppCode(message.getPublishAppCode());
log.setRequestTopicCode(message.getTopic());
log.setRequestTime(LocalDateTime.now());
log.setRequestData(message.getContent());
log.setResponseAppCode(responseAppCode);
// 发送次数设置为0
log.setSendCount(0);
add(log);
return log;
}
@Override
public ApiMessageLog createRequestPart(RequestMessage message) {
return createRequestPart(message, null);
}
@Override
public void updateResponsePart(ResponseMessage message) {
ApiMessageLog log = getByRequestMessageId(message.getRequestMessageId());
// 响应消息的发布者,对应请求消息的响应
log.setResponseAppCode(message.getPublishAppCode());
log.setResponseTopicCode(message.getTopic());
log.setResponseTime(LocalDateTime.now());
log.setResponseData(message.getContent());
log.setResponseResult(message.getResult());
log.setErrorMessage(message.getErrorMessage());
log.setErrorCode(message.getErrorCode());
log.setResponseId(message.getId());
// 将消息更新为已响应
log.setStatus(MessageStatusEnum.RESPONSED.name());
modify(log);
}
@Override
public List<ApiMessageLog> getResendMessage(int messageCount, int maxSendCount) {
LocalDateTime now = LocalDateTime.now();
// 获取当前时间之前15秒的时间点
LocalDateTime beforeNow = now.minusSeconds(15);
try {
LambdaQueryChainWrapper<ApiMessageLog> query = this.lambdaQuery()
// 消息状态为待发送或已发送
.and(x -> x.eq(ApiMessageLog::getStatus, MessageStatusEnum.WAIT_REQUEST.name())
.or(y -> y.eq(ApiMessageLog::getStatus, MessageStatusEnum.REQUESTED.name())))
// 排除掉登录请求
.ne(ApiMessageLog::getRequestTopicCode, "framework.login.request")
// 发送次数小于设置的最大发送次数
.lt(ApiMessageLog::getSendCount, maxSendCount)
// 请求时间小于当前时间15秒,避免刚产生的消息尚未收到服务端响应时就进行重发
.lt(ApiMessageLog::getRequestTime, beforeNow)
// 按照请求时间升序排列
.orderByAsc(ApiMessageLog::getRequestTime)
// 只取指定数量的消息
.last("limit " + messageCount);
return query.list();
} catch (Exception e) {
log.error("获取发送信息失败", e);
return null;
}
}
@Override
public void incrementSendCount(String messageId) {
ApiMessageLog apiMessageLog = getByRequestMessageId(messageId);
apiMessageLog.setSendCount(apiMessageLog.getSendCount() + 1);
modify(apiMessageLog);
}
}

150
cip-client/src/main/java/tech/popsoft/cip/client/manage/service/impl/ApiMessageTopicServiceImpl.java

@ -0,0 +1,150 @@
package tech.popsoft.cip.client.manage.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import tech.abc.platform.common.base.BaseServiceImpl;
import tech.abc.platform.common.enums.StatusEnum;
import tech.abc.platform.common.exception.CommonException;
import tech.abc.platform.common.exception.CustomException;
import tech.popsoft.cip.client.manage.entity.ApiMessageTopic;
import tech.popsoft.cip.client.manage.exception.ApiMessageTopicExceptionEnum;
import tech.popsoft.cip.client.manage.mapper.ApiMessageTopicMapper;
import tech.popsoft.cip.client.manage.service.ApiMessageTopicService;
import java.util.List;
/**
* 消息主题 服务实现类
*
* @author wqliu
* @date 2021-08-21
*/
@Service
public class ApiMessageTopicServiceImpl extends BaseServiceImpl<ApiMessageTopicMapper, ApiMessageTopic> implements ApiMessageTopicService {
@Override
public ApiMessageTopic init() {
ApiMessageTopic entity = new ApiMessageTopic();
entity.setStatus(StatusEnum.NORMAL.name());
return entity;
}
@Override
public void beforeAdd(ApiMessageTopic entity) {
// 验证编码全局唯一
QueryWrapper<ApiMessageTopic> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(ApiMessageTopic::getCode, entity.getCode());
long count = count(queryWrapper);
if (count > 0) {
throw new CustomException(CommonException.CODE_EXIST);
}
// 验证名称全局唯一
queryWrapper.lambda().eq(ApiMessageTopic::getName, entity.getName());
count = count(queryWrapper);
if (count > 0) {
throw new CustomException(CommonException.NAME_EXIST);
}
}
@Override
public void beforeModify(ApiMessageTopic entity) {
// 验证编码全局唯一
QueryWrapper<ApiMessageTopic> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(ApiMessageTopic::getCode, entity.getCode())
.ne(ApiMessageTopic::getId, entity.getId());
long count = count(queryWrapper);
if (count > 0) {
throw new CustomException(CommonException.CODE_EXIST);
}
// 验证名称全局唯一
queryWrapper.lambda().eq(ApiMessageTopic::getName, entity.getName())
.ne(ApiMessageTopic::getId, entity.getId());
count = count(queryWrapper);
if (count > 0) {
throw new CustomException(CommonException.NAME_EXIST);
}
}
@Override
public void enable(String id) {
ApiMessageTopic entity = getEntity(id);
entity.setStatus(StatusEnum.NORMAL.name());
modify(entity);
}
@Override
public void disable(String id) {
ApiMessageTopic entity = getEntity(id);
entity.setStatus(StatusEnum.DEAD.name());
modify(entity);
}
@Override
public String getHandlerByCode(String code) {
List<ApiMessageTopic> list = this.lambdaQuery().eq(ApiMessageTopic::getCode, code).list();
if (CollectionUtils.isEmpty(list)) {
throw new CustomException(ApiMessageTopicExceptionEnum.TOPIC_NOT_EXIST);
}
String handler = list.get(0).getHandler();
if (StringUtils.isBlank(handler)) {
throw new CustomException(ApiMessageTopicExceptionEnum.TOPIC_NOT_SET_HANDLER);
}
return handler;
}
@Override
public String getResponseTopicCodeByCode(String code) {
List<ApiMessageTopic> list = this.lambdaQuery().eq(ApiMessageTopic::getCode, code).list();
if (CollectionUtils.isEmpty(list)) {
throw new CustomException(ApiMessageTopicExceptionEnum.TOPIC_NOT_EXIST);
}
String responseTopicCode = list.get(0).getResponseTopicCode();
if (StringUtils.isBlank(responseTopicCode)) {
throw new CustomException(ApiMessageTopicExceptionEnum.RESPONSE_TOPIC_NOT_SET_HANDLER);
}
return responseTopicCode;
}
@Override
public String getIdByCode(String code) {
return getByCode(code).getId();
}
@Override
public ApiMessageTopic getByCode(String code) {
List<ApiMessageTopic> list = this.lambdaQuery().eq(ApiMessageTopic::getCode, code).list();
if (CollectionUtils.isNotEmpty(list)) {
return list.get(0);
} else {
throw new CustomException(ApiMessageTopicExceptionEnum.TOPIC_NOT_EXIST);
}
}
@Override
public String getSenderByCode(String code) {
ApiMessageTopic apiMessageTopic = getByCode(code);
String sender = apiMessageTopic.getSender();
if (StringUtils.isBlank(sender)) {
throw new CustomException(ApiMessageTopicExceptionEnum.TOPIC_NOT_SET_SENDER);
}
return sender;
}
}

83
cip-client/src/main/java/tech/popsoft/cip/client/manage/vo/ApiMessageLogVO.java

@ -0,0 +1,83 @@
package tech.popsoft.cip.client.manage.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import tech.abc.platform.common.base.BaseVO;
import java.time.LocalDateTime;
/**
* 消息日志 视图对象
*
* @author wqliu
* @date 2021-08-21
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@ApiModel(value = "消息日志对象")
public class ApiMessageLogVO extends BaseVO {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "请求消息标识")
private String requestId;
@ApiModelProperty(value = "请求应用编码")
private String requestAppCode;
@ApiModelProperty(value = "请求消息主题编码")
private String requestTopicCode;
@ApiModelProperty(value = "请求时间")
private LocalDateTime requestTime;
@ApiModelProperty(value = "请求内容")
private String requestData;
@ApiModelProperty(value = "响应消息标识")
private String responseId;
@ApiModelProperty(value = "响应应用编码")
private String responseAppCode;
@ApiModelProperty(value = "响应消息主题编码")
private String responseTopicCode;
@ApiModelProperty(value = "响应时间")
private LocalDateTime responseTime;
@ApiModelProperty(value = "响应内容")
private String responseData;
@ApiModelProperty(value = "响应结果")
private String responseResult;
@ApiModelProperty(value = "错误编码")
private String errorCode;
@ApiModelProperty(value = "错误信息")
private String errorMessage;
@ApiModelProperty(value = "当前状态")
private String status;
@ApiModelProperty(value = "发送次数")
private Integer sendCount;
/********自定义扩展*****/
/********字典类*****/
@ApiModelProperty(value = "消息状态")
private String responseResultName;
@ApiModelProperty(value = "响应结果")
private String statusName;
/********子对象*****/
}

78
cip-client/src/main/java/tech/popsoft/cip/client/manage/vo/ApiMessageTopicVO.java

@ -0,0 +1,78 @@
package tech.popsoft.cip.client.manage.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import tech.abc.platform.common.base.BaseVO;
/**
* 消息主题 视图对象
*
* @author wqliu
* @date 2021-08-21
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@ApiModel(value = "消息主题对象")
public class ApiMessageTopicVO extends BaseVO {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "编码")
private String code;
@ApiModelProperty(value = "名称")
private String name;
@ApiModelProperty(value = "处理器")
private String handler;
@ApiModelProperty(value = "发送器")
private String sender;
@ApiModelProperty(value = "响应主题编码")
private String responseTopicCode;
@ApiModelProperty(value = "分类")
private String category;
@ApiModelProperty(value = "状态")
private String status;
@ApiModelProperty(value = "备注")
private String remark;
@ApiModelProperty(value = "排序号")
private String orderNo;
/********自定义扩展*****/
@ApiModelProperty(value = "应用标识")
private String appId;
@ApiModelProperty(value = "是否已授权")
private String hasPermission;
@ApiModelProperty(value = "是否已订阅")
private String hasSubscription;
/********字典类*****/
@ApiModelProperty(value = "状态")
private String statusName;
@ApiModelProperty(value = "分类")
private String categoryName;
@ApiModelProperty(value = "是否已授权")
private String hasPermissionName;
@ApiModelProperty(value = "是否已订阅")
private String hasSubscriptionName;
/********子对象*****/
}

27
cip-client/src/main/java/tech/popsoft/cip/client/platform/exception/CustomException.java

@ -0,0 +1,27 @@
package tech.popsoft.cip.client.platform.exception;
import java.text.MessageFormat;
/**
* 自定义异常
*
* @author wqliu
*/
public class CustomException extends RuntimeException {
/**
* 继承exception
*/
public CustomException(ExceptionInterface exceptionInterface) {
super(exceptionInterface.getMessage());
}
/**
* 错误信息带参数
*/
public CustomException(ExceptionInterface exceptionInterface, Object... args) {
super(MessageFormat.format(exceptionInterface.getMessage(), args));
}
}

16
cip-client/src/main/java/tech/popsoft/cip/client/platform/exception/ExceptionInterface.java

@ -0,0 +1,16 @@
package tech.popsoft.cip.client.platform.exception;
/**
* 异常接口
*
* @author wqliu
* @date 2022-8-1
*/
public interface ExceptionInterface {
/**
* 获取异常信息
*
* @return 异常信息
*/
String getMessage();
}

37
cip-client/src/main/java/tech/popsoft/cip/client/receiver/ReceiveMessageController.java

@ -0,0 +1,37 @@
package tech.popsoft.cip.client.receiver;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.abc.platform.cip.common.entity.MessageException;
import tech.popsoft.cip.client.framework.sender.MessageSenderFactory;
import tech.popsoft.cip.client.framework.sender.RequestMessageSender;
/**
* 接收业务系统产生的消息
*
* @author wqliu
* @date 2022-1-18
**/
@RestController
@RequestMapping("/message")
public class ReceiveMessageController {
@GetMapping
public String receive(String topic, String id) {
// 数据验证
if (StringUtils.isBlank(topic)) {
throw new RuntimeException("消息主题不能为空");
}
// 根据消息主题查找发送器
try {
RequestMessageSender messageSender = (RequestMessageSender) MessageSenderFactory.createSender(topic);
messageSender.sendMessage(id);
return "ok";
} catch (MessageException e) {
throw new RuntimeException(e.getMessage());
}
}
}

17
cip-client/src/main/java/tech/popsoft/cip/client/sender/request/lms/transportbill/ConsignmentBillCreateSender.java

@ -0,0 +1,17 @@
package tech.popsoft.cip.client.sender.request.lms.transportbill;
import tech.popsoft.cip.client.framework.sender.RequestMessageSender;
/**
* 委托单创建发送器
*
* @author wqliu
* @date 2022-1-18
**/
public class ConsignmentBillCreateSender extends RequestMessageSender {
public ConsignmentBillCreateSender() {
super("lms.transportbill.consignmentbill.create");
}
}

31
cip-client/src/main/java/tech/popsoft/cip/client/sender/request/system/LoginRequestSender.java

@ -0,0 +1,31 @@
package tech.popsoft.cip.client.sender.request.system;
import com.alibaba.fastjson.JSONObject;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import tech.popsoft.cip.client.framework.sender.RequestMessageSender;
/**
* 登录请求发送器
*
* @author wqliu
* @date 2021-10-6
**/
public class LoginRequestSender extends RequestMessageSender {
public LoginRequestSender() {
super("framework.login.request");
// 使用账号密钥构建消息内容
JSONObject jsonObject = new JSONObject();
jsonObject.put("appCode", messageClientConfig.getAppCode());
// 对密钥进行加密处理
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String password = encoder.encode(messageClientConfig.getAppSecret());
jsonObject.put("appSecret", password);
super.setContent(jsonObject.toJSONString());
}
}

25
cip-client/src/main/java/tech/popsoft/cip/client/sender/response/system/ErrorResponseSender.java

@ -0,0 +1,25 @@
package tech.popsoft.cip.client.sender.response.system;
import tech.abc.platform.cip.common.enums.MessageResponseResultEnum;
import tech.popsoft.cip.client.framework.sender.ResponseMessageSender;
/**
* 向请求方发送发生错误的响应
*
* @author wqliu
* @date 2021-10-14 10:38
**/
public class ErrorResponseSender extends ResponseMessageSender {
public ErrorResponseSender() {
super("framework.error.response");
// 默认设置结果为出错
this.setResult(MessageResponseResultEnum.ERROR.name());
// 默认设置错误编码
this.setErrorCode("500");
}
}

20
cip-client/src/main/java/tech/popsoft/cip/client/sender/response/system/MessageConfirmResponseSender.java

@ -0,0 +1,20 @@
package tech.popsoft.cip.client.sender.response.system;
import tech.popsoft.cip.client.framework.sender.ResponseMessageSender;
/**
* 向请求方发送发生消息确认的响应
*
* @author wqliu
* @date 2021-10-14 10:38
**/
public class MessageConfirmResponseSender extends ResponseMessageSender {
public MessageConfirmResponseSender() {
super("framework.message.confirm.response");
}
}

29
cip-client/src/main/resources/application-dev.yml

@ -0,0 +1,29 @@
server:
port: 20001
params:
appCode: tms
appSecret: 8543692d-7ea7-4797-a746-47891bff2bec
host: localhost
port: 8997
heartbeatRate: 5
maxSendCount: 4
sendMessageSpan: 30
sendMessageCount: 10
enableResend: true
webSocketPath: webSocket
readIdleTimeOut: 30
spring:
# 数据库设置
datasource:
url: jdbc:mysql://localhost:3306/cip_client?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
# redis设置
redis:
host: localhost
port: 6379
password: test123
platform-config:
message:
serverPort: 9998

94
cip-client/src/main/resources/application.yml

@ -0,0 +1,94 @@
server:
port: 8080
#数据连接
spring:
datasource:
# 使用druid数据源
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/abc?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowMultiQueries=true
username: root
password: root
druid:
# 连接池的配置信息
# 初始化大小,最小,最大
initial-size: 5
min-idle: 5
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 600000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1
testWhileIdle: false
testOnBorrow: true
testOnReturn: false
usePingMethod: false
devtools:
livereload:
enabled: true
redis:
host: localhost
port: 6379
password:
#新版本redis的timeout是一个duration,需使用如下写法
timeout: 10s
database: 0
lettuce:
pool:
# 连接池中的最小空闲连接
min-idle: 2
# 连接池中的最大空闲连接
max-idle: 2
# 连接池的最大连接数
max-active: 16
#连接池最大阻塞等待时间
max-wait: 30s
profiles:
active: dev
# session:
# store-type: REDIS
# timeout: 8h
servlet:
# 文件上传设置
multipart:
#设置单个文件的大小,-1代表不限制
max-file-size: -1
#设置单次请求文件的大小,-1代表不限制
max-request-size: -1
freemarker:
# 不检测是否存在模板,避免启动时控制台输出警告信息
checkTemplateLocation: false
#mybatis-plus配置
mybatis-plus:
# mapper-locations: classpath*:/system/**.xml
global-config:
db-config:
logic-delete-value: "YES" # 逻辑已删除值
logic-not-delete-value: "NO" # 逻辑未删除值
#平台配置
platform-config:
system:
enablePermission: false
userInitPassword: 12345678
tokenSecret: wqliu
exportDataPageSize: 2
message:
serverPort: 8997
messageServerAppCode: CIP
readIdleTimeOut: 60
maxSendCount: 10
sendMessageSpan: 30
sendMessageCount: 10
enableResend: true

84
cip-client/src/main/resources/init.sql

@ -0,0 +1,84 @@
-- --------------------------------------------------------
-- 主机: 127.0.0.1
-- 服务器版本: 8.0.19 - MySQL Community Server - GPL
-- 服务器操作系统: Win64
-- HeidiSQL 版本: 12.5.0.6677
-- --------------------------------------------------------
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET NAMES utf8 */;
/*!50503 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
-- 导出 cip_client 的数据库结构
CREATE DATABASE IF NOT EXISTS `cip_client` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin */ /*!80016 DEFAULT ENCRYPTION='N' */;
USE `cip_client`;
-- 导出 表 cip_client.cip_message_log 结构
CREATE TABLE IF NOT EXISTS `cip_message_log` (
`id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '标识',
`request_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '请求消息标识',
`request_app_code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '请求应用编码',
`request_topic_code` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '请求消息主题编码',
`request_time` datetime DEFAULT NULL COMMENT '请求时间',
`request_data` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '请求内容',
`response_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '响应消息标识',
`response_app_code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '响应应用编码',
`response_topic_code` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '响应消息主题编码',
`response_time` datetime DEFAULT NULL COMMENT '响应时间',
`response_data` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '响应内容',
`response_result` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '响应结果',
`error_code` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '错误编码',
`error_message` varchar(400) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '错误信息',
`status` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '当前状态',
`send_count` int DEFAULT NULL COMMENT '发送次数',
`create_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '创建人标识',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '更新人标识',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`version` int DEFAULT NULL COMMENT '版本',
`delete_flag` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '删除标志',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='消息日志';
-- 正在导出表 cip_client.cip_message_log 的数据:~0 rows (大约)
-- 导出 表 cip_client.cip_message_topic 结构
CREATE TABLE IF NOT EXISTS `cip_message_topic` (
`id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '标识',
`code` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '编码',
`name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '名称',
`handler` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '处理器',
`sender` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '发送器',
`response_topic_code` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '响应主题编码',
`category` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '分类',
`status` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '状态',
`remark` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '备注',
`order_no` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '排序号',
`create_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '创建人标识',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '更新人标识',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`version` int DEFAULT NULL COMMENT '版本',
`delete_flag` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '删除标志',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=DYNAMIC COMMENT='消息主题';
-- 正在导出表 cip_client.cip_message_topic 的数据:~5 rows (大约)
INSERT INTO `cip_message_topic` (`id`, `code`, `name`, `handler`, `sender`, `response_topic_code`, `category`, `status`, `remark`, `order_no`, `create_id`, `create_time`, `update_id`, `update_time`, `version`, `delete_flag`) VALUES
('1448247847417774081', 'framework.login.response', '登录响应', 'tech.popsoft.cip.client.handler.response.system.LoginResponseHandler', NULL, '', 'USER', 'NORMAL', NULL, '01', '1', '2021-10-13 19:22:49', '1', '2021-10-13 19:22:49', 1, 'NO'),
('1448247847417774082', 'lms.transportbill.consignmentbill.create', '委托单创建', 'tech.popsoft.cip.client.handler.request.lms.transportbill.ConsignmentBillCreateRequestHandler', 'tech.popsoft.cip.client.sender.request.lms.transportbill.ConsignmentBillCreateSender', 'framework.message.confirm.response', 'USER', 'NORMAL', NULL, '01', '1', '2021-10-13 19:22:49', '1', '2021-10-13 19:22:49', 1, 'NO'),
('1448247847417774083', 'framework.error.response', '错误响应', 'tech.popsoft.cip.client.handler.response.system.ErrorResponseHandler', 'tech.popsoft.cip.client.sender.response.system.ErrorResponseSender', '', 'USER', 'NORMAL', NULL, '01', '1', '2021-10-13 19:22:49', '1', '2021-10-13 19:22:49', 1, 'NO'),
('1448247847417774084', 'framework.message.confirm.response', '消息确认响应', 'tech.popsoft.cip.client.handler.response.system.MessageConfirmResponseHandler', 'tech.popsoft.cip.client.sender.response.system.MessageConfirmResponseSender', '', 'USER', 'NORMAL', NULL, '01', '1', '2021-10-13 19:22:49', '1', '2021-10-13 19:22:49', 1, 'NO'),
('1448247847417774086', 'framework.login.request', '登录请求', '', 'tech.popsoft.cip.client.sender.request.system.LoginRequestSender', '', 'USER', 'NORMAL', NULL, '01', '1', '2021-10-13 19:22:49', '1', '2021-10-13 19:22:49', 1, 'NO');
/*!40103 SET TIME_ZONE=IFNULL(@OLD_TIME_ZONE, 'system') */;
/*!40101 SET SQL_MODE=IFNULL(@OLD_SQL_MODE, '') */;
/*!40014 SET FOREIGN_KEY_CHECKS=IFNULL(@OLD_FOREIGN_KEY_CHECKS, 1) */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40111 SET SQL_NOTES=IFNULL(@OLD_SQL_NOTES, 1) */;

18
cip-client/src/test/java/tech/popsoft/AppTest.java

@ -0,0 +1,18 @@
package tech.popsoft;
import org.junit.Test;
import static org.junit.Assert.assertTrue;
/**
* Unit test for simple App.
*/
public class AppTest {
/**
* Rigorous Test :-)
*/
@Test
public void shouldAnswerWithTrue() {
assertTrue(true);
}
}

16
platform-app/.gitignore

@ -0,0 +1,16 @@
######################################################################
# Build Tools
/unpackage/*
/node_modules/*
######################################################################
# Development Tools
/.idea/*
/.vscode/*
/.hbuilderx/*
package-lock.json
yarn.lock

34
platform-app/App.vue

@ -0,0 +1,34 @@
<script>
import config from './config'
import store from '@/store'
import { getToken } from '@/utils/auth'
export default {
onLaunch: function() {
this.initApp()
},
methods: {
//
initApp() {
//
this.initConfig()
//
//#ifdef H5
this.checkLogin()
//#endif
},
initConfig() {
this.globalData.config = config
},
checkLogin() {
if (!getToken()) {
this.$tab.reLaunch('/pages/login')
}
}
}
}
</script>
<style lang="scss">
@import '@/static/scss/index.scss'
</style>

21
platform-app/LICENSE

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

50
platform-app/api/login.js

@ -0,0 +1,50 @@
import request from '@/utils/request'
// 登录方法
export function login(username, password) {
return request({
'url': '/system/user/login?username='+username+'&password='+password,
'method': 'post'
})
}
// 注册方法
export function register(data) {
return request({
url: '/register',
headers: {
isToken: false
},
method: 'post',
data: data
})
}
// 获取用户详细信息
export function getInfo() {
return request({
'url': '/getInfo',
'method': 'get'
})
}
// 退出方法
export function logout() {
return request({
'url': '/logout',
'method': 'post'
})
}
// 获取验证码
export function getCodeImg() {
return request({
'url': '/captchaImage',
headers: {
isToken: false
},
method: 'get',
timeout: 20000
})
}

41
platform-app/api/system/user.js

@ -0,0 +1,41 @@
import upload from '@/utils/upload'
import request from '@/utils/request'
// 用户密码重置
export function updateUserPwd(oldPassword, newPassword) {
const data = {
oldPassword,
newPassword
}
return request({
url: '/system/user/profile/updatePwd',
method: 'put',
params: data
})
}
// 查询用户个人信息
export function getUserProfile() {
return request({
url: '/system/user/profile',
method: 'get'
})
}
// 修改用户个人信息
export function updateUserProfile(data) {
return request({
url: '/system/user/profile',
method: 'put',
data: data
})
}
// 用户头像上传
export function uploadAvatar(data) {
return upload({
url: '/system/user/profile/avatar',
name: data.name,
filePath: data.filePath
})
}

167
platform-app/components/uni-section/uni-section.vue

@ -0,0 +1,167 @@
<template>
<view class="uni-section">
<view class="uni-section-header" @click="onClick">
<view class="uni-section-header__decoration" v-if="type" :class="type" />
<slot v-else name="decoration"></slot>
<view class="uni-section-header__content">
<text :style="{'font-size':titleFontSize,'color':titleColor}" class="uni-section__content-title" :class="{'distraction':!subTitle}">{{ title }}</text>
<text v-if="subTitle" :style="{'font-size':subTitleFontSize,'color':subTitleColor}" class="uni-section-header__content-sub">{{ subTitle }}</text>
</view>
<view class="uni-section-header__slot-right">
<slot name="right"></slot>
</view>
</view>
<view class="uni-section-content" :style="{padding: _padding}">
<slot />
</view>
</view>
</template>
<script>
/**
* Section 标题栏
* @description 标题栏
* @property {String} type = [line|circle|square] 标题装饰类型
* @value line 竖线
* @value circle 圆形
* @value square 正方形
* @property {String} title 主标题
* @property {String} titleFontSize 主标题字体大小
* @property {String} titleColor 主标题字体颜色
* @property {String} subTitle 副标题
* @property {String} subTitleFontSize 副标题字体大小
* @property {String} subTitleColor 副标题字体颜色
* @property {String} padding 默认插槽 padding
*/
export default {
name: 'UniSection',
emits:['click'],
props: {
type: {
type: String,
default: ''
},
title: {
type: String,
required: true,
default: ''
},
titleFontSize: {
type: String,
default: '14px'
},
titleColor:{
type: String,
default: '#333'
},
subTitle: {
type: String,
default: ''
},
subTitleFontSize: {
type: String,
default: '12px'
},
subTitleColor: {
type: String,
default: '#999'
},
padding: {
type: [Boolean, String],
default: false
}
},
computed:{
_padding(){
if(typeof this.padding === 'string'){
return this.padding
}
return this.padding?'10px':''
}
},
watch: {
title(newVal) {
if (uni.report && newVal !== '') {
uni.report('title', newVal)
}
}
},
methods: {
onClick() {
this.$emit('click')
}
}
}
</script>
<style lang="scss" >
$uni-primary: #2979ff !default;
.uni-section {
background-color: #fff;
.uni-section-header {
position: relative;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
align-items: center;
padding: 12px 10px;
font-weight: normal;
&__decoration{
margin-right: 6px;
background-color: $uni-primary;
&.line {
width: 4px;
height: 12px;
border-radius: 10px;
}
&.circle {
width: 8px;
height: 8px;
border-top-right-radius: 50px;
border-top-left-radius: 50px;
border-bottom-left-radius: 50px;
border-bottom-right-radius: 50px;
}
&.square {
width: 8px;
height: 8px;
}
}
&__content {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
flex: 1;
color: #333;
.distraction {
flex-direction: row;
align-items: center;
}
&-sub {
margin-top: 2px;
}
}
&__slot-right{
font-size: 14px;
}
}
.uni-section-content{
font-size: 14px;
}
}
</style>

26
platform-app/config.js

@ -0,0 +1,26 @@
// 应用全局配置
module.exports = {
baseUrl: 'http://localhost:8080',
// baseUrl: 'http://localhost:8080',
// 应用信息
appInfo: {
// 应用名称
name: "platform-app",
// 应用版本
version: "1.0.0",
// 应用logo
logo: "/static/logo.png",
// 官方网站
site_url: "https://popsoft.tech",
// 政策协议
agreements: [{
title: "隐私政策",
url: "https://popsoft.tech/protocol.html"
},
{
title: "用户服务协议",
url: "https://popsoft.tech/protocol.html"
}
]
}
}

17
platform-app/main.js

@ -0,0 +1,17 @@
import Vue from 'vue'
import App from './App'
import store from './store' // store
import plugins from './plugins' // plugins
import './permission' // permission
Vue.use(plugins)
Vue.config.productionTip = false
Vue.prototype.$store = store
App.mpType = 'app'
const app = new Vue({
...App
})
app.$mount()

69
platform-app/manifest.json

@ -0,0 +1,69 @@
{
"name" : "移动端",
"appid" : "__UNI__25A9D80",
"description" : "",
"versionName" : "1.1.0",
"versionCode" : "100",
"transformPx" : false,
"app-plus" : {
"usingComponents" : true,
"nvueCompiler" : "uni-app",
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : true,
"autoclose" : true,
"delay" : 0
},
"modules" : {},
"distribute" : {
"android" : {
"permissions" : [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
"ios" : {},
"sdkConfigs" : {}
}
},
"quickapp" : {},
"mp-weixin" : {
"appid" : "wxccd7e2a0911b3397",
"setting" : {
"urlCheck" : false,
"es6" : false,
"minified" : true,
"postcss" : true
},
"optimization" : {
"subPackages" : true
},
"usingComponents" : true
},
"vueVersion" : "2",
"h5" : {
"template" : "static/index.html",
"devServer" : {
"port" : 9090,
"https" : false
},
"title" : "Platform-App",
"router" : {
"mode" : "hash",
"base" : "./"
}
}
}

102
platform-app/pages.json

@ -0,0 +1,102 @@
{
"pages": [{
"path": "pages/login",
"style": {
"navigationBarTitleText": "登录"
}
}, {
"path": "pages/register",
"style": {
"navigationBarTitleText": "注册"
}
}, {
"path": "pages/index",
"style": {
"navigationBarTitleText": "移动端框架",
"navigationStyle": "custom"
}
}, {
"path": "pages/work/index",
"style": {
"navigationBarTitleText": "工作台"
}
}, {
"path": "pages/mine/index",
"style": {
"navigationBarTitleText": "我的"
}
}, {
"path": "pages/mine/avatar/index",
"style": {
"navigationBarTitleText": "修改头像"
}
}, {
"path": "pages/mine/info/index",
"style": {
"navigationBarTitleText": "个人信息"
}
}, {
"path": "pages/mine/info/edit",
"style": {
"navigationBarTitleText": "编辑资料"
}
}, {
"path": "pages/mine/pwd/index",
"style": {
"navigationBarTitleText": "修改密码"
}
}, {
"path": "pages/mine/setting/index",
"style": {
"navigationBarTitleText": "应用设置"
}
}, {
"path": "pages/mine/help/index",
"style": {
"navigationBarTitleText": "常见问题"
}
}, {
"path": "pages/mine/about/index",
"style": {
"navigationBarTitleText": "关于我们"
}
}, {
"path": "pages/common/webview/index",
"style": {
"navigationBarTitleText": "浏览网页"
}
}, {
"path": "pages/common/textview/index",
"style": {
"navigationBarTitleText": "浏览文本"
}
}],
"tabBar": {
"color": "#000000",
"selectedColor": "#000000",
"borderStyle": "white",
"backgroundColor": "#ffffff",
"list": [{
"pagePath": "pages/index",
"iconPath": "static/images/tabbar/home.png",
"selectedIconPath": "static/images/tabbar/home_.png",
"text": "首页"
}, {
"pagePath": "pages/work/index",
"iconPath": "static/images/tabbar/work.png",
"selectedIconPath": "static/images/tabbar/work_.png",
"text": "工作台"
}, {
"pagePath": "pages/mine/index",
"iconPath": "static/images/tabbar/mine.png",
"selectedIconPath": "static/images/tabbar/mine_.png",
"text": "我的"
}
]
},
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "App",
"navigationBarBackgroundColor": "#FFFFFF"
}
}

43
platform-app/pages/common/textview/index.vue

@ -0,0 +1,43 @@
<template>
<view>
<uni-card class="view-title" :title="title">
<text class="uni-body view-content">{{ content }}</text>
</uni-card>
</view>
</template>
<script>
export default {
data() {
return {
title: '',
content: ''
}
},
onLoad(options) {
this.title = options.title
this.content = options.content
uni.setNavigationBarTitle({
title: options.title
})
}
}
</script>
<style scoped>
page {
background-color: #ffffff;
}
.view-title {
font-weight: bold;
}
.view-content {
font-size: 26rpx;
padding: 12px 5px 0;
color: #333;
line-height: 24px;
font-weight: normal;
}
</style>

34
platform-app/pages/common/webview/index.vue

@ -0,0 +1,34 @@
<template>
<view v-if="params.url">
<web-view :webview-styles="webviewStyles" :src="`${params.url}`"></web-view>
</view>
</template>
<script>
export default {
data() {
return {
params: {},
webviewStyles: {
progress: {
color: "#FF3333"
}
}
}
},
props: {
src: {
type: [String],
default: null
}
},
onLoad(event) {
this.params = event
if (event.title) {
uni.setNavigationBarTitle({
title: event.title
})
}
}
}
</script>

43
platform-app/pages/index.vue

@ -0,0 +1,43 @@
<template>
<view class="content">
<image class="logo" src="@/static/logo.png"></image>
<view class="text-area">
<text class="title">Hello ABC</text>
</view>
</view>
</template>
<script>
export default {
onLoad: function() {
}
}
</script>
<style>
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.logo {
height: 200rpx;
width: 200rpx;
margin-top: 200rpx;
margin-left: auto;
margin-right: auto;
margin-bottom: 50rpx;
}
.text-area {
display: flex;
justify-content: center;
}
.title {
font-size: 36rpx;
color: #8f8f94;
}
</style>

167
platform-app/pages/login.vue

@ -0,0 +1,167 @@
<template>
<view class="normal-login-container">
<view class="logo-content align-center justify-center flex">
<image style="width: 100rpx;height: 100rpx;" :src="globalConfig.appInfo.logo" mode="widthFix">
</image>
<text class="title">移动端登录</text>
</view>
<view class="login-form-content">
<view class="input-item flex align-center">
<view class="iconfont icon-user icon"></view>
<input v-model="loginForm.username" class="input" type="text" placeholder="请输入账号" maxlength="30" />
</view>
<view class="input-item flex align-center">
<view class="iconfont icon-password icon"></view>
<input v-model="loginForm.password" type="password" class="input" placeholder="请输入密码" maxlength="20" />
</view>
<view class="action-btn">
<button @click="handleLogin" class="login-btn cu-btn block bg-blue lg round">登录</button>
</view>
<view class="xieyi text-center">
<text class="text-grey1">登录即代表同意</text>
<text @click="handleUserAgrement" class="text-blue">用户协议</text>
<text @click="handlePrivacy" class="text-blue">隐私协议</text>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
codeUrl: "",
captchaEnabled: true,
//
register: false,
globalConfig: getApp().globalData.config,
loginForm: {
username: "admin",
password: "12345678"
}
}
},
created() {
},
methods: {
//
handleUserRegister() {
this.$tab.redirectTo(`/pages/register`)
},
//
handlePrivacy() {
let site = this.globalConfig.appInfo.agreements[0]
this.$tab.navigateTo(`/pages/common/webview/index?title=${site.title}&url=${site.url}`)
},
//
handleUserAgrement() {
let site = this.globalConfig.appInfo.agreements[1]
this.$tab.navigateTo(`/pages/common/webview/index?title=${site.title}&url=${site.url}`)
},
//
async handleLogin() {
if (this.loginForm.username === "") {
this.$modal.msgError("请输入您的账号")
} else if (this.loginForm.password === "") {
this.$modal.msgError("请输入您的密码")
}else {
this.$modal.loading("登录中,请耐心等待...")
this.pwdLogin()
}
},
//
async pwdLogin() {
this.$store.dispatch('Login', this.loginForm).then((res) => {
this.$modal.closeLoading()
this.$tab.reLaunch('/pages/index')
})
}
}
}
</script>
<style lang="scss">
page {
background-color: #ffffff;
}
.normal-login-container {
width: 100%;
.logo-content {
width: 100%;
font-size: 21px;
text-align: center;
padding-top: 15%;
image {
border-radius: 4px;
}
.title {
margin-left: 10px;
}
}
.login-form-content {
text-align: center;
margin: 20px auto;
margin-top: 15%;
width: 80%;
.input-item {
margin: 20px auto;
background-color: #f5f6f7;
height: 45px;
border-radius: 20px;
.icon {
font-size: 38rpx;
margin-left: 10px;
color: #999;
}
.input {
width: 100%;
font-size: 14px;
line-height: 20px;
text-align: left;
padding-left: 15px;
}
}
.login-btn {
margin-top: 40px;
height: 45px;
}
.reg {
margin-top: 15px;
}
.xieyi {
color: #333;
margin-top: 20px;
}
.login-code {
height: 38px;
float: right;
.login-code-img {
height: 38px;
position: absolute;
margin-left: 10px;
width: 200rpx;
}
}
}
}
</style>

75
platform-app/pages/mine/about/index.vue

@ -0,0 +1,75 @@
<template>
<view class="about-container">
<view class="header-section text-center">
<image style="width: 150rpx;height: 150rpx;" src="/static/logo200.png" mode="widthFix">
</image>
<uni-title type="h2" title="移动端"></uni-title>
</view>
<view class="content-section">
<view class="menu-list">
<view class="list-cell list-cell-arrow">
<view class="menu-item-box">
<view>版本信息</view>
<view class="text-right">v{{version}}</view>
</view>
</view>
<view class="list-cell list-cell-arrow">
<view class="menu-item-box">
<view>官方邮箱</view>
<view class="text-right">sealy321@126.com</view>
</view>
</view>
<view class="list-cell list-cell-arrow">
<view class="menu-item-box">
<view>服务热线</view>
<view class="text-right">400-999-9999</view>
</view>
</view>
<view class="list-cell list-cell-arrow">
<view class="menu-item-box">
<view>公司网站</view>
<view class="text-right">
<uni-link :href="url" :text="url" showUnderLine="false"></uni-link>
</view>
</view>
</view>
</view>
</view>
<view class="copyright">
<view>Copyright &copy; 2022 popsoft.tech All Rights Reserved.</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
url: getApp().globalData.config.appInfo.site_url,
version: getApp().globalData.config.appInfo.version
}
}
}
</script>
<style lang="scss">
page {
background-color: #f8f8f8;
}
.copyright {
margin-top: 50rpx;
text-align: center;
line-height: 60rpx;
color: #999;
}
.header-section {
display: flex;
padding: 30rpx 0 0;
flex-direction: column;
align-items: center;
}
</style>

631
platform-app/pages/mine/avatar/index.vue

@ -0,0 +1,631 @@
<template>
<view class="container">
<view class="page-body uni-content-info">
<view class='cropper-content'>
<view v-if="isShowImg" class="uni-corpper" :style="'width:'+cropperInitW+'px;height:'+cropperInitH+'px;background:#000'">
<view class="uni-corpper-content" :style="'width:'+cropperW+'px;height:'+cropperH+'px;left:'+cropperL+'px;top:'+cropperT+'px'">
<image :src="imageSrc" :style="'width:'+cropperW+'px;height:'+cropperH+'px'"></image>
<view class="uni-corpper-crop-box" @touchstart.stop="contentStartMove" @touchmove.stop="contentMoveing" @touchend.stop="contentTouchEnd"
:style="'left:'+cutL+'px;top:'+cutT+'px;right:'+cutR+'px;bottom:'+cutB+'px'">
<view class="uni-cropper-view-box">
<view class="uni-cropper-dashed-h"></view>
<view class="uni-cropper-dashed-v"></view>
<view class="uni-cropper-line-t" data-drag="top" @touchstart.stop="dragStart" @touchmove.stop="dragMove"></view>
<view class="uni-cropper-line-r" data-drag="right" @touchstart.stop="dragStart" @touchmove.stop="dragMove"></view>
<view class="uni-cropper-line-b" data-drag="bottom" @touchstart.stop="dragStart" @touchmove.stop="dragMove"></view>
<view class="uni-cropper-line-l" data-drag="left" @touchstart.stop="dragStart" @touchmove.stop="dragMove"></view>
<view class="uni-cropper-point point-t" data-drag="top" @touchstart.stop="dragStart" @touchmove.stop="dragMove"></view>
<view class="uni-cropper-point point-tr" data-drag="topTight"></view>
<view class="uni-cropper-point point-r" data-drag="right" @touchstart.stop="dragStart" @touchmove.stop="dragMove"></view>
<view class="uni-cropper-point point-rb" data-drag="rightBottom" @touchstart.stop="dragStart" @touchmove.stop="dragMove"></view>
<view class="uni-cropper-point point-b" data-drag="bottom" @touchstart.stop="dragStart" @touchmove.stop="dragMove" @touchend.stop="dragEnd"></view>
<view class="uni-cropper-point point-bl" data-drag="bottomLeft"></view>
<view class="uni-cropper-point point-l" data-drag="left" @touchstart.stop="dragStart" @touchmove.stop="dragMove"></view>
<view class="uni-cropper-point point-lt" data-drag="leftTop"></view>
</view>
</view>
</view>
</view>
</view>
<view class='cropper-config'>
<button type="primary reverse" @click="getImage" style='margin-top: 30rpx;'> 选择头像 </button>
<button type="warn" @click="getImageInfo" style='margin-top: 30rpx;'> 提交 </button>
</view>
<canvas canvas-id="myCanvas" :style="'position:absolute;border: 1px solid red; width:'+imageW+'px;height:'+imageH+'px;top:-9999px;left:-9999px;'"></canvas>
</view>
</view>
</template>
<script>
import config from '@/config'
import store from "@/store"
import { uploadAvatar } from "@/api/system/user"
const baseUrl = config.baseUrl
let sysInfo = uni.getSystemInfoSync()
let SCREEN_WIDTH = sysInfo.screenWidth
let PAGE_X, // x
PAGE_Y, // y
PR = sysInfo.pixelRatio, // dpi
T_PAGE_X, // x
T_PAGE_Y, // Y
CUT_L, // left
CUT_T, // top
CUT_R, //
CUT_B, //
CUT_W, //
CUT_H, //
IMG_RATIO, //
IMG_REAL_W, //
IMG_REAL_H, //
DRAFG_MOVE_RATIO = 1, //,
INIT_DRAG_POSITION = 100, //
DRAW_IMAGE_W = sysInfo.screenWidth //
export default {
/**
* 页面的初始数据
*/
data() {
return {
imageSrc: store.getters.avatar,
isShowImg: false,
//
cropperInitW: SCREEN_WIDTH,
cropperInitH: SCREEN_WIDTH,
//
cropperW: SCREEN_WIDTH,
cropperH: SCREEN_WIDTH,
// left top
cropperL: 0,
cropperT: 0,
transL: 0,
transT: 0,
//
scaleP: 0,
imageW: 0,
imageH: 0,
//
cutL: 0,
cutT: 0,
cutB: SCREEN_WIDTH,
cutR: '100%',
qualityWidth: DRAW_IMAGE_W,
innerAspectRadio: DRAFG_MOVE_RATIO
}
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady: function () {
this.loadImage()
},
methods: {
setData: function (obj) {
let that = this
Object.keys(obj).forEach(function (key) {
that.$set(that.$data, key, obj[key])
})
},
getImage: function () {
var _this = this
uni.chooseImage({
success: function (res) {
_this.setData({
imageSrc: res.tempFilePaths[0],
})
_this.loadImage()
},
})
},
loadImage: function () {
var _this = this
uni.getImageInfo({
src: _this.imageSrc,
success: function success(res) {
IMG_RATIO = 1 / 1
if (IMG_RATIO >= 1) {
IMG_REAL_W = SCREEN_WIDTH
IMG_REAL_H = SCREEN_WIDTH / IMG_RATIO
} else {
IMG_REAL_W = SCREEN_WIDTH * IMG_RATIO
IMG_REAL_H = SCREEN_WIDTH
}
let minRange = IMG_REAL_W > IMG_REAL_H ? IMG_REAL_W : IMG_REAL_H
INIT_DRAG_POSITION = minRange > INIT_DRAG_POSITION ? INIT_DRAG_POSITION : minRange
//
if (IMG_RATIO >= 1) {
let cutT = Math.ceil((SCREEN_WIDTH / IMG_RATIO - (SCREEN_WIDTH / IMG_RATIO - INIT_DRAG_POSITION)) / 2)
let cutB = cutT
let cutL = Math.ceil((SCREEN_WIDTH - SCREEN_WIDTH + INIT_DRAG_POSITION) / 2)
let cutR = cutL
_this.setData({
cropperW: SCREEN_WIDTH,
cropperH: SCREEN_WIDTH / IMG_RATIO,
// left right
cropperL: Math.ceil((SCREEN_WIDTH - SCREEN_WIDTH) / 2),
cropperT: Math.ceil((SCREEN_WIDTH - SCREEN_WIDTH / IMG_RATIO) / 2),
cutL: cutL,
cutT: cutT,
cutR: cutR,
cutB: cutB,
//
imageW: IMG_REAL_W,
imageH: IMG_REAL_H,
scaleP: IMG_REAL_W / SCREEN_WIDTH,
qualityWidth: DRAW_IMAGE_W,
innerAspectRadio: IMG_RATIO
})
} else {
let cutL = Math.ceil((SCREEN_WIDTH * IMG_RATIO - (SCREEN_WIDTH * IMG_RATIO)) / 2)
let cutR = cutL
let cutT = Math.ceil((SCREEN_WIDTH - INIT_DRAG_POSITION) / 2)
let cutB = cutT
_this.setData({
cropperW: SCREEN_WIDTH * IMG_RATIO,
cropperH: SCREEN_WIDTH,
// left right
cropperL: Math.ceil((SCREEN_WIDTH - SCREEN_WIDTH * IMG_RATIO) / 2),
cropperT: Math.ceil((SCREEN_WIDTH - SCREEN_WIDTH) / 2),
cutL: cutL,
cutT: cutT,
cutR: cutR,
cutB: cutB,
//
imageW: IMG_REAL_W,
imageH: IMG_REAL_H,
scaleP: IMG_REAL_W / SCREEN_WIDTH,
qualityWidth: DRAW_IMAGE_W,
innerAspectRadio: IMG_RATIO
})
}
_this.setData({
isShowImg: true
})
uni.hideLoading()
}
})
},
// touchStart
contentStartMove(e) {
PAGE_X = e.touches[0].pageX
PAGE_Y = e.touches[0].pageY
},
// touchMove
contentMoveing(e) {
var _this = this
var dragLengthX = (PAGE_X - e.touches[0].pageX) * DRAFG_MOVE_RATIO
var dragLengthY = (PAGE_Y - e.touches[0].pageY) * DRAFG_MOVE_RATIO
//
if (dragLengthX > 0) {
if (this.cutL - dragLengthX < 0) dragLengthX = this.cutL
} else {
if (this.cutR + dragLengthX < 0) dragLengthX = -this.cutR
}
if (dragLengthY > 0) {
if (this.cutT - dragLengthY < 0) dragLengthY = this.cutT
} else {
if (this.cutB + dragLengthY < 0) dragLengthY = -this.cutB
}
this.setData({
cutL: this.cutL - dragLengthX,
cutT: this.cutT - dragLengthY,
cutR: this.cutR + dragLengthX,
cutB: this.cutB + dragLengthY
})
PAGE_X = e.touches[0].pageX
PAGE_Y = e.touches[0].pageY
},
contentTouchEnd() {
},
//
getImageInfo() {
var _this = this
uni.showLoading({
title: '图片生成中...',
})
//
const ctx = uni.createCanvasContext('myCanvas')
ctx.drawImage(_this.imageSrc, 0, 0, IMG_REAL_W, IMG_REAL_H)
ctx.draw(true, () => {
// * canvasT = (_this.cutT / _this.cropperH) * (_this.imageH / pixelRatio)
var canvasW = ((_this.cropperW - _this.cutL - _this.cutR) / _this.cropperW) * IMG_REAL_W
var canvasH = ((_this.cropperH - _this.cutT - _this.cutB) / _this.cropperH) * IMG_REAL_H
var canvasL = (_this.cutL / _this.cropperW) * IMG_REAL_W
var canvasT = (_this.cutT / _this.cropperH) * IMG_REAL_H
uni.canvasToTempFilePath({
x: canvasL,
y: canvasT,
width: canvasW,
height: canvasH,
destWidth: canvasW,
destHeight: canvasH,
quality: 0.5,
canvasId: 'myCanvas',
success: function (res) {
uni.hideLoading()
let data = {name: 'avatarfile', filePath: res.tempFilePath}
uploadAvatar(data).then(response => {
store.commit('SET_AVATAR', baseUrl + response.imgUrl)
uni.showToast({ title: "修改成功", icon: 'success' })
uni.navigateBack()
})
}
})
})
},
// touchStart
dragStart(e) {
T_PAGE_X = e.touches[0].pageX
T_PAGE_Y = e.touches[0].pageY
CUT_L = this.cutL
CUT_R = this.cutR
CUT_B = this.cutB
CUT_T = this.cutT
},
// touchMove
dragMove(e) {
var _this = this
var dragType = e.target.dataset.drag
switch (dragType) {
case 'right':
var dragLength = (T_PAGE_X - e.touches[0].pageX) * DRAFG_MOVE_RATIO
if (CUT_R + dragLength < 0) dragLength = -CUT_R
this.setData({
cutR: CUT_R + dragLength
})
break
case 'left':
var dragLength = (T_PAGE_X - e.touches[0].pageX) * DRAFG_MOVE_RATIO
if (CUT_L - dragLength < 0) dragLength = CUT_L
if ((CUT_L - dragLength) > (this.cropperW - this.cutR)) dragLength = CUT_L - (this.cropperW - this.cutR)
this.setData({
cutL: CUT_L - dragLength
})
break
case 'top':
var dragLength = (T_PAGE_Y - e.touches[0].pageY) * DRAFG_MOVE_RATIO
if (CUT_T - dragLength < 0) dragLength = CUT_T
if ((CUT_T - dragLength) > (this.cropperH - this.cutB)) dragLength = CUT_T - (this.cropperH - this.cutB)
this.setData({
cutT: CUT_T - dragLength
})
break
case 'bottom':
var dragLength = (T_PAGE_Y - e.touches[0].pageY) * DRAFG_MOVE_RATIO
if (CUT_B + dragLength < 0) dragLength = -CUT_B
this.setData({
cutB: CUT_B + dragLength
})
break
case 'rightBottom':
var dragLengthX = (T_PAGE_X - e.touches[0].pageX) * DRAFG_MOVE_RATIO
var dragLengthY = (T_PAGE_Y - e.touches[0].pageY) * DRAFG_MOVE_RATIO
if (CUT_B + dragLengthY < 0) dragLengthY = -CUT_B
if (CUT_R + dragLengthX < 0) dragLengthX = -CUT_R
let cutB = CUT_B + dragLengthY
let cutR = CUT_R + dragLengthX
this.setData({
cutB: cutB,
cutR: cutR
})
break
default:
break
}
}
}
}
</script>
<style>
/* pages/uni-cropper/index.wxss */
.uni-content-info {
/* position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: block;
align-items: center;
flex-direction: column; */
}
.cropper-config {
padding: 20rpx 40rpx;
}
.cropper-content {
min-height: 750rpx;
width: 100%;
}
.uni-corpper {
position: relative;
overflow: hidden;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-tap-highlight-color: transparent;
-webkit-touch-callout: none;
box-sizing: border-box;
}
.uni-corpper-content {
position: relative;
}
.uni-corpper-content image {
display: block;
width: 100%;
min-width: 0 !important;
max-width: none !important;
height: 100%;
min-height: 0 !important;
max-height: none !important;
image-orientation: 0deg !important;
margin: 0 auto;
}
/* 移动图片效果 */
.uni-cropper-drag-box {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
cursor: move;
background: rgba(0, 0, 0, 0.6);
z-index: 1;
}
/* 内部的信息 */
.uni-corpper-crop-box {
position: absolute;
background: rgba(255, 255, 255, 0.3);
z-index: 2;
}
.uni-corpper-crop-box .uni-cropper-view-box {
position: relative;
display: block;
width: 100%;
height: 100%;
overflow: visible;
outline: 1rpx solid #69f;
outline-color: rgba(102, 153, 255, .75)
}
/* 横向虚线 */
.uni-cropper-dashed-h {
position: absolute;
top: 33.33333333%;
left: 0;
width: 100%;
height: 33.33333333%;
border-top: 1rpx dashed rgba(255, 255, 255, 0.5);
border-bottom: 1rpx dashed rgba(255, 255, 255, 0.5);
}
/* 纵向虚线 */
.uni-cropper-dashed-v {
position: absolute;
left: 33.33333333%;
top: 0;
width: 33.33333333%;
height: 100%;
border-left: 1rpx dashed rgba(255, 255, 255, 0.5);
border-right: 1rpx dashed rgba(255, 255, 255, 0.5);
}
/* 四个方向的线 为了之后的拖动事件*/
.uni-cropper-line-t {
position: absolute;
display: block;
width: 100%;
background-color: #69f;
top: 0;
left: 0;
height: 1rpx;
opacity: 0.1;
cursor: n-resize;
}
.uni-cropper-line-t::before {
content: '';
position: absolute;
top: 50%;
right: 0rpx;
width: 100%;
-webkit-transform: translate3d(0, -50%, 0);
transform: translate3d(0, -50%, 0);
bottom: 0;
height: 41rpx;
background: transparent;
z-index: 11;
}
.uni-cropper-line-r {
position: absolute;
display: block;
background-color: #69f;
top: 0;
right: 0rpx;
width: 1rpx;
opacity: 0.1;
height: 100%;
cursor: e-resize;
}
.uni-cropper-line-r::before {
content: '';
position: absolute;
top: 0;
left: 50%;
width: 41rpx;
-webkit-transform: translate3d(-50%, 0, 0);
transform: translate3d(-50%, 0, 0);
bottom: 0;
height: 100%;
background: transparent;
z-index: 11;
}
.uni-cropper-line-b {
position: absolute;
display: block;
width: 100%;
background-color: #69f;
bottom: 0;
left: 0;
height: 1rpx;
opacity: 0.1;
cursor: s-resize;
}
.uni-cropper-line-b::before {
content: '';
position: absolute;
top: 50%;
right: 0rpx;
width: 100%;
-webkit-transform: translate3d(0, -50%, 0);
transform: translate3d(0, -50%, 0);
bottom: 0;
height: 41rpx;
background: transparent;
z-index: 11;
}
.uni-cropper-line-l {
position: absolute;
display: block;
background-color: #69f;
top: 0;
left: 0;
width: 1rpx;
opacity: 0.1;
height: 100%;
cursor: w-resize;
}
.uni-cropper-line-l::before {
content: '';
position: absolute;
top: 0;
left: 50%;
width: 41rpx;
-webkit-transform: translate3d(-50%, 0, 0);
transform: translate3d(-50%, 0, 0);
bottom: 0;
height: 100%;
background: transparent;
z-index: 11;
}
.uni-cropper-point {
width: 5rpx;
height: 5rpx;
background-color: #69f;
opacity: .75;
position: absolute;
z-index: 3;
}
.point-t {
top: -3rpx;
left: 50%;
margin-left: -3rpx;
cursor: n-resize;
}
.point-tr {
top: -3rpx;
left: 100%;
margin-left: -3rpx;
cursor: n-resize;
}
.point-r {
top: 50%;
left: 100%;
margin-left: -3rpx;
margin-top: -3rpx;
cursor: n-resize;
}
.point-rb {
left: 100%;
top: 100%;
-webkit-transform: translate3d(-50%, -50%, 0);
transform: translate3d(-50%, -50%, 0);
cursor: n-resize;
width: 36rpx;
height: 36rpx;
background-color: #69f;
position: absolute;
z-index: 1112;
opacity: 1;
}
.point-b {
left: 50%;
top: 100%;
margin-left: -3rpx;
margin-top: -3rpx;
cursor: n-resize;
}
.point-bl {
left: 0%;
top: 100%;
margin-left: -3rpx;
margin-top: -3rpx;
cursor: n-resize;
}
.point-l {
left: 0%;
top: 50%;
margin-left: -3rpx;
margin-top: -3rpx;
cursor: n-resize;
}
.point-lt {
left: 0%;
top: 0%;
margin-left: -3rpx;
margin-top: -3rpx;
cursor: n-resize;
}
/* 裁剪框预览内容 */
.uni-cropper-viewer {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
.uni-cropper-viewer image {
position: absolute;
z-index: 2;
}
</style>

112
platform-app/pages/mine/help/index.vue

@ -0,0 +1,112 @@
<template>
<view class="help-container">
<view v-for="(item, findex) in list" :key="findex" :title="item.title" class="list-title">
<view class="text-title">
<view :class="item.icon"></view>{{ item.title }}
</view>
<view class="childList">
<view v-for="(child, zindex) in item.childList" :key="zindex" class="question" hover-class="hover"
@click="handleText(child)">
<view class="text-item">{{ child.title }}</view>
<view class="line" v-if="zindex !== item.childList.length - 1"></view>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
list: [{
icon: 'iconfont icon-github',
title: '问题',
childList: [{
title: '开源吗?',
content: '开源'
}, {
title: '可以商用吗?',
content: '可以'
}, {
title: '官网地址多少?',
content: 'http://popsoft.tech'
}, {
title: '文档地址多少?',
content: 'http://popsoft.tech'
}]
},
{
icon: 'iconfont icon-help',
title: '其他问题',
childList: [{
title: '如何退出登录?',
content: '请点击[我的] - [应用设置] - [退出登录]即可退出登录',
}, {
title: '如何修改用户头像?',
content: '请点击[我的] - [选择头像] - [点击提交]即可更换用户头像',
}, {
title: '如何修改登录密码?',
content: '请点击[我的] - [应用设置] - [修改密码]即可修改登录密码',
}]
}
]
}
},
methods: {
handleText(item) {
this.$tab.navigateTo(`/pages/common/textview/index?title=${item.title}&content=${item.content}`)
}
}
}
</script>
<style lang="scss" scoped>
page {
background-color: #f8f8f8;
}
.help-container {
margin-bottom: 100rpx;
padding: 30rpx;
}
.list-title {
margin-bottom: 30rpx;
}
.childList {
background: #ffffff;
box-shadow: 0px 0px 10rpx rgba(193, 193, 193, 0.2);
border-radius: 16rpx;
margin-top: 10rpx;
}
.line {
width: 100%;
height: 1rpx;
background-color: #F5F5F5;
}
.text-title {
color: #303133;
font-size: 32rpx;
font-weight: bold;
margin-left: 10rpx;
.iconfont {
font-size: 16px;
margin-right: 10rpx;
}
}
.text-item {
font-size: 28rpx;
padding: 24rpx;
}
.question {
color: #606266;
font-size: 28rpx;
}
</style>

195
platform-app/pages/mine/index.vue

@ -0,0 +1,195 @@
<template>
<view class="mine-container" :style="{height: `${windowHeight}px`}">
<!--顶部个人信息栏-->
<view class="header-section">
<view class="flex padding justify-between">
<view class="flex align-center">
<view v-if="!avatar" class="cu-avatar xl round bg-white">
<view class="iconfont icon-people text-gray icon"></view>
</view>
<image v-if="avatar" @click="handleToAvatar" :src="avatar" class="cu-avatar xl round" mode="widthFix">
</image>
<view v-if="!name" @click="handleToLogin" class="login-tip">
点击登录
</view>
<view v-if="name" @click="handleToInfo" class="user-info">
<view class="u_title">
{{ name }}
</view>
</view>
</view>
</view>
</view>
<view class="content-section">
<view class="mine-actions grid col-4 text-center">
<view class="action-item" @click="handleJiaoLiuQun">
<view class="iconfont icon-friendfill text-pink icon"></view>
<text class="text">交流群</text>
</view>
<view class="action-item" @click="handleBuilding">
<view class="iconfont icon-service text-blue icon"></view>
<text class="text">在线客服</text>
</view>
<view class="action-item" @click="handleBuilding">
<view class="iconfont icon-community text-mauve icon"></view>
<text class="text">反馈社区</text>
</view>
<view class="action-item" @click="handleBuilding">
<view class="iconfont icon-dianzan text-green icon"></view>
<text class="text">点赞我们</text>
</view>
</view>
<view class="menu-list">
<view class="list-cell list-cell-arrow" @click="handleToEditInfo">
<view class="menu-item-box">
<view class="iconfont icon-user menu-icon"></view>
<view>编辑资料</view>
</view>
</view>
<view class="list-cell list-cell-arrow" @click="handleHelp">
<view class="menu-item-box">
<view class="iconfont icon-help menu-icon"></view>
<view>常见问题</view>
</view>
</view>
<view class="list-cell list-cell-arrow" @click="handleAbout">
<view class="menu-item-box">
<view class="iconfont icon-aixin menu-icon"></view>
<view>关于我们</view>
</view>
</view>
<view class="list-cell list-cell-arrow" @click="handleToSetting">
<view class="menu-item-box">
<view class="iconfont icon-setting menu-icon"></view>
<view>应用设置</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import storage from '@/utils/storage'
export default {
data() {
return {
name: this.$store.state.user.name,
version: getApp().globalData.config.appInfo.version
}
},
computed: {
avatar() {
return this.$store.state.user.avatar
},
windowHeight() {
return uni.getSystemInfoSync().windowHeight - 50
}
},
methods: {
handleToInfo() {
this.$tab.navigateTo('/pages/mine/info/index')
},
handleToEditInfo() {
this.$tab.navigateTo('/pages/mine/info/edit')
},
handleToSetting() {
this.$tab.navigateTo('/pages/mine/setting/index')
},
handleToLogin() {
this.$tab.reLaunch('/pages/login')
},
handleToAvatar() {
this.$tab.navigateTo('/pages/mine/avatar/index')
},
handleLogout() {
this.$modal.confirm('确定注销并退出系统吗?').then(() => {
this.$store.dispatch('LogOut').then(() => {
this.$tab.reLaunch('/pages/index')
})
})
},
handleHelp() {
this.$tab.navigateTo('/pages/mine/help/index')
},
handleAbout() {
this.$tab.navigateTo('/pages/mine/about/index')
},
handleJiaoLiuQun() {
this.$modal.showToast('模块建设中~')
},
handleBuilding() {
this.$modal.showToast('模块建设中~')
}
}
}
</script>
<style lang="scss">
page {
background-color: #f5f6f7;
}
.mine-container {
width: 100%;
height: 100%;
.header-section {
padding: 15px 15px 45px 15px;
background-color: #3c96f3;
color: white;
.login-tip {
font-size: 18px;
margin-left: 10px;
}
.cu-avatar {
border: 2px solid #eaeaea;
.icon {
font-size: 40px;
}
}
.user-info {
margin-left: 15px;
.u_title {
font-size: 18px;
line-height: 30px;
}
}
}
.content-section {
position: relative;
top: -50px;
.mine-actions {
margin: 15px 15px;
padding: 20px 0px;
border-radius: 8px;
background-color: white;
.action-item {
.icon {
font-size: 28px;
}
.text {
display: block;
font-size: 13px;
margin: 8px 0px;
}
}
}
}
}
</style>

127
platform-app/pages/mine/info/edit.vue

@ -0,0 +1,127 @@
<template>
<view class="container">
<view class="example">
<uni-forms ref="form" :model="user" labelWidth="80px">
<uni-forms-item label="用户昵称" name="nickName">
<uni-easyinput v-model="user.nickName" placeholder="请输入昵称" />
</uni-forms-item>
<uni-forms-item label="手机号码" name="phonenumber">
<uni-easyinput v-model="user.phonenumber" placeholder="请输入手机号码" />
</uni-forms-item>
<uni-forms-item label="邮箱" name="email">
<uni-easyinput v-model="user.email" placeholder="请输入邮箱" />
</uni-forms-item>
<uni-forms-item label="性别" name="sex" required>
<uni-data-checkbox v-model="user.sex" :localdata="sexs" />
</uni-forms-item>
</uni-forms>
<button type="primary" @click="submit">提交</button>
</view>
</view>
</template>
<script>
import { getUserProfile } from "@/api/system/user"
import { updateUserProfile } from "@/api/system/user"
export default {
data() {
return {
user: {
nickName: "",
phonenumber: "",
email: "",
sex: ""
},
sexs: [{
text: '男',
value: "0"
}, {
text: '女',
value: "1"
}],
rules: {
nickName: {
rules: [{
required: true,
errorMessage: '用户昵称不能为空'
}]
},
phonenumber: {
rules: [{
required: true,
errorMessage: '手机号码不能为空'
}, {
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
errorMessage: '请输入正确的手机号码'
}]
},
email: {
rules: [{
required: true,
errorMessage: '邮箱地址不能为空'
}, {
format: 'email',
errorMessage: '请输入正确的邮箱地址'
}]
}
}
}
},
onLoad() {
this.getUser()
},
onReady() {
this.$refs.form.setRules(this.rules)
},
methods: {
getUser() {
getUserProfile().then(response => {
this.user = response.data
})
},
submit(ref) {
this.$refs.form.validate().then(res => {
updateUserProfile(this.user).then(response => {
this.$modal.msgSuccess("修改成功")
})
})
}
}
}
</script>
<style lang="scss">
page {
background-color: #ffffff;
}
.example {
padding: 15px;
background-color: #fff;
}
.segmented-control {
margin-bottom: 15px;
}
.button-group {
margin-top: 15px;
display: flex;
justify-content: space-around;
}
.form-item {
display: flex;
align-items: center;
flex: 1;
}
.button {
display: flex;
align-items: center;
height: 35px;
line-height: 35px;
margin-left: 10px;
}
</style>

44
platform-app/pages/mine/info/index.vue

@ -0,0 +1,44 @@
<template>
<view class="container">
<uni-list>
<uni-list-item showExtraIcon="true" :extraIcon="{type: 'person-filled'}" title="昵称" :rightText="user.nickName" />
<uni-list-item showExtraIcon="true" :extraIcon="{type: 'phone-filled'}" title="手机号码" :rightText="user.phonenumber" />
<uni-list-item showExtraIcon="true" :extraIcon="{type: 'email-filled'}" title="邮箱" :rightText="user.email" />
<uni-list-item showExtraIcon="true" :extraIcon="{type: 'auth-filled'}" title="岗位" :rightText="postGroup" />
<uni-list-item showExtraIcon="true" :extraIcon="{type: 'staff-filled'}" title="角色" :rightText="roleGroup" />
<uni-list-item showExtraIcon="true" :extraIcon="{type: 'calendar-filled'}" title="创建日期" :rightText="user.createTime" />
</uni-list>
</view>
</template>
<script>
import { getUserProfile } from "@/api/system/user"
export default {
data() {
return {
user: {},
roleGroup: "",
postGroup: ""
}
},
onLoad() {
this.getUser()
},
methods: {
getUser() {
getUserProfile().then(response => {
this.user = response.data
this.roleGroup = response.roleGroup
this.postGroup = response.postGroup
})
}
}
}
</script>
<style lang="scss">
page {
background-color: #ffffff;
}
</style>

85
platform-app/pages/mine/pwd/index.vue

@ -0,0 +1,85 @@
<template>
<view class="pwd-retrieve-container">
<uni-forms ref="form" :value="user" labelWidth="80px">
<uni-forms-item name="oldPassword" label="旧密码">
<uni-easyinput type="password" v-model="user.oldPassword" placeholder="请输入旧密码" />
</uni-forms-item>
<uni-forms-item name="newPassword" label="新密码">
<uni-easyinput type="password" v-model="user.newPassword" placeholder="请输入新密码" />
</uni-forms-item>
<uni-forms-item name="confirmPassword" label="确认密码">
<uni-easyinput type="password" v-model="user.confirmPassword" placeholder="请确认新密码" />
</uni-forms-item>
<button type="primary" @click="submit">提交</button>
</uni-forms>
</view>
</template>
<script>
import { updateUserPwd } from "@/api/system/user"
export default {
data() {
return {
user: {
oldPassword: undefined,
newPassword: undefined,
confirmPassword: undefined
},
rules: {
oldPassword: {
rules: [{
required: true,
errorMessage: '旧密码不能为空'
}]
},
newPassword: {
rules: [{
required: true,
errorMessage: '新密码不能为空',
},
{
minLength: 6,
maxLength: 20,
errorMessage: '长度在 6 到 20 个字符'
}
]
},
confirmPassword: {
rules: [{
required: true,
errorMessage: '确认密码不能为空'
}, {
validateFunction: (rule, value, data) => data.newPassword === value,
errorMessage: '两次输入的密码不一致'
}
]
}
}
}
},
onReady() {
this.$refs.form.setRules(this.rules)
},
methods: {
submit() {
this.$refs.form.validate().then(res => {
updateUserPwd(this.user.oldPassword, this.user.newPassword).then(response => {
this.$modal.msgSuccess("修改成功")
})
})
}
}
}
</script>
<style lang="scss">
page {
background-color: #ffffff;
}
.pwd-retrieve-container {
padding-top: 36rpx;
padding: 15px;
}
</style>

78
platform-app/pages/mine/setting/index.vue

@ -0,0 +1,78 @@
<template>
<view class="setting-container" :style="{height: `${windowHeight}px`}">
<view class="menu-list">
<view class="list-cell list-cell-arrow" @click="handleToPwd">
<view class="menu-item-box">
<view class="iconfont icon-password menu-icon"></view>
<view>修改密码</view>
</view>
</view>
<view class="list-cell list-cell-arrow" @click="handleToUpgrade">
<view class="menu-item-box">
<view class="iconfont icon-refresh menu-icon"></view>
<view>检查更新</view>
</view>
</view>
<view class="list-cell list-cell-arrow" @click="handleCleanTmp">
<view class="menu-item-box">
<view class="iconfont icon-clean menu-icon"></view>
<view>清理缓存</view>
</view>
</view>
</view>
<view class="cu-list menu">
<view class="cu-item item-box">
<view class="content text-center" @click="handleLogout">
<text class="text-black">退出登录</text>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
windowHeight: uni.getSystemInfoSync().windowHeight
}
},
methods: {
handleToPwd() {
this.$tab.navigateTo('/pages/mine/pwd/index')
},
handleToUpgrade() {
this.$modal.showToast('模块建设中~')
},
handleCleanTmp() {
this.$modal.showToast('模块建设中~')
},
handleLogout() {
this.$modal.confirm('确定注销并退出系统吗?').then(() => {
this.$store.dispatch('LogOut').then(() => {
this.$tab.reLaunch('/pages/index')
})
})
}
}
}
</script>
<style lang="scss" scoped>
.page {
background-color: #f8f8f8;
}
.item-box {
background-color: #FFFFFF;
margin: 30rpx;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
padding: 10rpx;
border-radius: 8rpx;
color: #303133;
font-size: 32rpx;
}
</style>

196
platform-app/pages/register.vue

@ -0,0 +1,196 @@
<template>
<view class="normal-login-container">
<view class="logo-content align-center justify-center flex">
<image style="width: 100rpx;height: 100rpx;" :src="globalConfig.appInfo.logo" mode="widthFix">
</image>
<text class="title">移动端注册</text>
</view>
<view class="login-form-content">
<view class="input-item flex align-center">
<view class="iconfont icon-user icon"></view>
<input v-model="registerForm.username" class="input" type="text" placeholder="请输入账号" maxlength="30" />
</view>
<view class="input-item flex align-center">
<view class="iconfont icon-password icon"></view>
<input v-model="registerForm.password" type="password" class="input" placeholder="请输入密码" maxlength="20" />
</view>
<view class="input-item flex align-center">
<view class="iconfont icon-password icon"></view>
<input v-model="registerForm.confirmPassword" type="password" class="input" placeholder="请输入重复密码" maxlength="20" />
</view>
<view class="input-item flex align-center" style="width: 60%;margin: 0px;" v-if="captchaEnabled">
<view class="iconfont icon-code icon"></view>
<input v-model="registerForm.code" type="number" class="input" placeholder="请输入验证码" maxlength="4" />
<view class="login-code">
<image :src="codeUrl" @click="getCode" class="login-code-img"></image>
</view>
</view>
<view class="action-btn">
<button @click="handleRegister()" class="register-btn cu-btn block bg-blue lg round">注册</button>
</view>
</view>
<view class="xieyi text-center">
<text @click="handleUserLogin" class="text-blue">使用已有账号登录</text>
</view>
</view>
</template>
<script>
import { getCodeImg, register } from '@/api/login'
export default {
data() {
return {
codeUrl: "",
captchaEnabled: true,
globalConfig: getApp().globalData.config,
registerForm: {
username: "",
password: "",
confirmPassword: "",
code: "",
uuid: ''
}
}
},
created() {
this.getCode()
},
methods: {
//
handleUserLogin() {
this.$tab.navigateTo(`/pages/login`)
},
//
getCode() {
getCodeImg().then(res => {
this.captchaEnabled = res.captchaEnabled === undefined ? true : res.captchaEnabled
if (this.captchaEnabled) {
this.codeUrl = 'data:image/gif;base64,' + res.img
this.registerForm.uuid = res.uuid
}
})
},
//
async handleRegister() {
if (this.registerForm.username === "") {
this.$modal.msgError("请输入您的账号")
} else if (this.registerForm.password === "") {
this.$modal.msgError("请输入您的密码")
} else if (this.registerForm.confirmPassword === "") {
this.$modal.msgError("请再次输入您的密码")
} else if (this.registerForm.password !== this.registerForm.confirmPassword) {
this.$modal.msgError("两次输入的密码不一致")
} else if (this.registerForm.code === "" && this.captchaEnabled) {
this.$modal.msgError("请输入验证码")
} else {
this.$modal.loading("注册中,请耐心等待...")
this.register()
}
},
//
async register() {
register(this.registerForm).then(res => {
this.$modal.closeLoading()
uni.showModal({
title: "系统提示",
content: "恭喜你,您的账号 " + this.registerForm.username + " 注册成功!",
success: function (res) {
if (res.confirm) {
uni.redirectTo({ url: `/pages/login` });
}
}
})
}).catch(() => {
if (this.captchaEnabled) {
this.getCode()
}
})
},
//
registerSuccess(result) {
//
this.$store.dispatch('GetInfo').then(res => {
this.$tab.reLaunch('/pages/index')
})
}
}
}
</script>
<style lang="scss">
page {
background-color: #ffffff;
}
.normal-login-container {
width: 100%;
.logo-content {
width: 100%;
font-size: 21px;
text-align: center;
padding-top: 15%;
image {
border-radius: 4px;
}
.title {
margin-left: 10px;
}
}
.login-form-content {
text-align: center;
margin: 20px auto;
margin-top: 15%;
width: 80%;
.input-item {
margin: 20px auto;
background-color: #f5f6f7;
height: 45px;
border-radius: 20px;
.icon {
font-size: 38rpx;
margin-left: 10px;
color: #999;
}
.input {
width: 100%;
font-size: 14px;
line-height: 20px;
text-align: left;
padding-left: 15px;
}
}
.register-btn {
margin-top: 40px;
height: 45px;
}
.xieyi {
color: #333;
margin-top: 20px;
}
.login-code {
height: 38px;
float: right;
.login-code-img {
height: 38px;
position: absolute;
margin-left: 10px;
width: 200rpx;
}
}
}
}
</style>

181
platform-app/pages/work/index.vue

@ -0,0 +1,181 @@
<template>
<view class="work-container">
<!-- 轮播图 -->
<uni-swiper-dot class="uni-swiper-dot-box" :info="data" :current="current" field="content">
<swiper class="swiper-box" :current="swiperDotIndex" @change="changeSwiper">
<swiper-item v-for="(item, index) in data" :key="index">
<view class="swiper-item" @click="clickBannerItem(item)">
<image :src="item.image" mode="aspectFill" :draggable="false" />
</view>
</swiper-item>
</swiper>
</uni-swiper-dot>
<!-- 宫格组件 -->
<uni-section title="系统管理" type="line"></uni-section>
<view class="grid-body">
<uni-grid :column="4" :showBorder="false" @change="changeGrid">
<uni-grid-item>
<view class="grid-item-box">
<uni-icons type="person-filled" size="30"></uni-icons>
<text class="text">用户管理</text>
</view>
</uni-grid-item>
<uni-grid-item>
<view class="grid-item-box">
<uni-icons type="staff-filled" size="30"></uni-icons>
<text class="text">角色管理</text>
</view>
</uni-grid-item>
<uni-grid-item>
<view class="grid-item-box">
<uni-icons type="color" size="30"></uni-icons>
<text class="text">菜单管理</text>
</view>
</uni-grid-item>
<uni-grid-item>
<view class="grid-item-box">
<uni-icons type="settings-filled" size="30"></uni-icons>
<text class="text">部门管理</text>
</view>
</uni-grid-item>
<uni-grid-item>
<view class="grid-item-box">
<uni-icons type="heart-filled" size="30"></uni-icons>
<text class="text">岗位管理</text>
</view>
</uni-grid-item>
<uni-grid-item>
<view class="grid-item-box">
<uni-icons type="bars" size="30"></uni-icons>
<text class="text">字典管理</text>
</view>
</uni-grid-item>
<uni-grid-item>
<view class="grid-item-box">
<uni-icons type="gear-filled" size="30"></uni-icons>
<text class="text">参数设置</text>
</view>
</uni-grid-item>
<uni-grid-item>
<view class="grid-item-box">
<uni-icons type="chat-filled" size="30"></uni-icons>
<text class="text">通知公告</text>
</view>
</uni-grid-item>
<uni-grid-item>
<view class="grid-item-box">
<uni-icons type="wallet-filled" size="30"></uni-icons>
<text class="text">日志管理</text>
</view>
</uni-grid-item>
</uni-grid>
</view>
</view>
</template>
<script>
export default {
data() {
return {
current: 0,
swiperDotIndex: 0,
data: [
{
image: '/static/images/banner/banner02.jpg'
},
{
image: '/static/images/banner/banner03.jpg'
}
]
}
},
methods: {
clickBannerItem(item) {
console.info(item)
},
changeSwiper(e) {
this.current = e.detail.current
},
changeGrid(e) {
this.$modal.showToast('模块建设中~')
}
}
}
</script>
<style lang="scss">
/* #ifndef APP-NVUE */
page {
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: #fff;
min-height: 100%;
height: auto;
}
view {
font-size: 14px;
line-height: inherit;
}
/* #endif */
.text {
text-align: center;
font-size: 26rpx;
margin-top: 10rpx;
}
.grid-item-box {
flex: 1;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
align-items: center;
justify-content: center;
padding: 15px 0;
}
.uni-margin-wrap {
width: 690rpx;
width: 100%;
;
}
.swiper {
height: 300rpx;
}
.swiper-box {
height: 150px;
}
.swiper-item {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
justify-content: center;
align-items: center;
color: #fff;
height: 300rpx;
line-height: 300rpx;
}
@media screen and (min-width: 500px) {
.uni-swiper-dot-box {
width: 400px;
/* #ifndef APP-NVUE */
margin: 0 auto;
/* #endif */
margin-top: 8px;
}
.image {
width: 100%;
}
}
</style>

39
platform-app/permission.js

@ -0,0 +1,39 @@
import { getToken } from '@/utils/auth'
// 登录页面
const loginPage = "/pages/login"
// 页面白名单
const whiteList = [
'/pages/login', '/pages/register', '/pages/common/webview/index'
]
// 检查地址白名单
function checkWhite(url) {
const path = url.split('?')[0]
return whiteList.indexOf(path) !== -1
}
// 页面跳转验证拦截器
let list = ["navigateTo", "redirectTo", "reLaunch", "switchTab"]
list.forEach(item => {
uni.addInterceptor(item, {
invoke(to) {
if (getToken()) {
if (to.url === loginPage) {
uni.reLaunch({ url: "/" })
}
return true
} else {
if (checkWhite(to.url)) {
return true
}
uni.reLaunch({ url: loginPage })
return false
}
},
fail(err) {
console.log(err)
}
})
})

60
platform-app/plugins/auth.js

@ -0,0 +1,60 @@
import store from '@/store'
function authPermission(permission) {
const all_permission = "*:*:*"
const permissions = store.getters && store.getters.permissions
if (permission && permission.length > 0) {
return permissions.some(v => {
return all_permission === v || v === permission
})
} else {
return false
}
}
function authRole(role) {
const super_admin = "admin"
const roles = store.getters && store.getters.roles
if (role && role.length > 0) {
return roles.some(v => {
return super_admin === v || v === role
})
} else {
return false
}
}
export default {
// 验证用户是否具备某权限
hasPermi(permission) {
return authPermission(permission)
},
// 验证用户是否含有指定权限,只需包含其中一个
hasPermiOr(permissions) {
return permissions.some(item => {
return authPermission(item)
})
},
// 验证用户是否含有指定权限,必须全部拥有
hasPermiAnd(permissions) {
return permissions.every(item => {
return authPermission(item)
})
},
// 验证用户是否具备某角色
hasRole(role) {
return authRole(role)
},
// 验证用户是否含有指定角色,只需包含其中一个
hasRoleOr(roles) {
return roles.some(item => {
return authRole(item)
})
},
// 验证用户是否含有指定角色,必须全部拥有
hasRoleAnd(roles) {
return roles.every(item => {
return authRole(item)
})
}
}

14
platform-app/plugins/index.js

@ -0,0 +1,14 @@
import tab from './tab'
import auth from './auth'
import modal from './modal'
export default {
install(Vue) {
// 页签操作
Vue.prototype.$tab = tab
// 认证对象
Vue.prototype.$auth = auth
// 模态框对象
Vue.prototype.$modal = modal
}
}

74
platform-app/plugins/modal.js

@ -0,0 +1,74 @@
export default {
// 消息提示
msg(content) {
uni.showToast({
title: content,
icon: 'none'
})
},
// 错误消息
msgError(content) {
uni.showToast({
title: content,
icon: 'error'
})
},
// 成功消息
msgSuccess(content) {
uni.showToast({
title: content,
icon: 'success'
})
},
// 隐藏消息
hideMsg(content) {
uni.hideToast()
},
// 弹出提示
alert(content, title) {
uni.showModal({
title: title || '系统提示',
content: content,
showCancel: false
})
},
// 确认窗体
confirm(content, title) {
return new Promise((resolve, reject) => {
uni.showModal({
title: title || '系统提示',
content: content,
cancelText: '取消',
confirmText: '确定',
success: function(res) {
if (res.confirm) {
resolve(res.confirm)
}
}
})
})
},
// 提示信息
showToast(option) {
if (typeof option === "object") {
uni.showToast(option)
} else {
uni.showToast({
title: option,
icon: "none",
duration: 2500
})
}
},
// 打开遮罩层
loading(content) {
uni.showLoading({
title: content,
icon: 'none'
})
},
// 关闭遮罩层
closeLoading() {
uni.hideLoading()
}
}

30
platform-app/plugins/tab.js

@ -0,0 +1,30 @@
export default {
// 关闭所有页面,打开到应用内的某个页面
reLaunch(url) {
return uni.reLaunch({
url: url
})
},
// 跳转到tabBar页面,并关闭其他所有非tabBar页面
switchTab(url) {
return uni.switchTab({
url: url
})
},
// 关闭当前页面,跳转到应用内的某个页面
redirectTo(url) {
return uni.redirectTo({
url: url
})
},
// 保留当前页面,跳转到应用内的某个页面
navigateTo(url) {
return uni.navigateTo({
url: url
})
},
// 关闭当前页面,返回上一页面或多级页面
navigateBack() {
return uni.navigateBack()
}
}

BIN
platform-app/static/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

90
platform-app/static/font/iconfont.css

@ -0,0 +1,90 @@
@font-face {
font-family: "iconfont";
src: url('@/static/font/iconfont.ttf') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
display: inline-block;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-user:before {
content: "\e7ae";
}
.icon-password:before {
content: "\e8b2";
}
.icon-code:before {
content: "\e699";
}
.icon-setting:before {
content: "\e6cc";
}
.icon-share:before {
content: "\e739";
}
.icon-edit:before {
content: "\e60c";
}
.icon-version:before {
content: "\e63f";
}
.icon-service:before {
content: "\e6ff";
}
.icon-friendfill:before {
content: "\e726";
}
.icon-community:before {
content: "\e741";
}
.icon-people:before {
content: "\e736";
}
.icon-dianzan:before {
content: "\ec7f";
}
.icon-right:before {
content: "\e7eb";
}
.icon-logout:before {
content: "\e61d";
}
.icon-help:before {
content: "\e616";
}
.icon-github:before {
content: "\e628";
}
.icon-aixin:before {
content: "\e601";
}
.icon-clean:before {
content: "\e607";
}
.icon-refresh:before {
content: "\e604";
}

BIN
platform-app/static/font/iconfont.ttf

Binary file not shown.

BIN
platform-app/static/images/banner/banner01.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
platform-app/static/images/banner/banner02.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
platform-app/static/images/banner/banner03.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
platform-app/static/images/profile.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
platform-app/static/images/tabbar/home.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
platform-app/static/images/tabbar/home_.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
platform-app/static/images/tabbar/mine.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
platform-app/static/images/tabbar/mine_.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
platform-app/static/images/tabbar/work.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
platform-app/static/images/tabbar/work_.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

20
platform-app/static/index.html

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="renderer" content="webkit">
<title><%= htmlWebpackPlugin.options.title %></title>
<link rel="shortcut icon" type="image/x-icon" href="<%= BASE_URL %>static/favicon.ico">
<script>
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') || CSS.supports('top: constant(a)'))
document.write('<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' + (coverSupport ? ', viewport-fit=cover' : '') + '" />')
</script>
<link rel="stylesheet" href="<%= BASE_URL %>static/index.<%= VUE_APP_INDEX_CSS_HASH %>.css" />
</head>
<body>
<noscript>
<strong>本站点必须要开启JavaScript才能运行.</strong>
</noscript>
<div id="app"></div>
</html>

BIN
platform-app/static/logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 975 B

BIN
platform-app/static/logo200.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

5142
platform-app/static/scss/colorui.css

File diff suppressed because one or more lines are too long

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

Loading…
Cancel
Save