上架流程
上架
下单流程
流程
退款流程
退款
上架
流程
退款
若依java服务端添加websocket 包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
业务类
package com.ruoyi.project.websocket;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.security.service.TokenService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@Component
@ServerEndpoint("/websocket")
public class WebSocketServer {
private static final Logger log = LoggerFactory.getLogger(WebSocketServer.class);
private static final AtomicInteger onlineCount = new AtomicInteger(0);
private static final Map<String, Session> sessionMap = new ConcurrentHashMap<>();
private static final Map<String, Long> lastActiveTimeMap = new ConcurrentHashMap<>();
private static final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private static final long HEARTBEAT_TIMEOUT = 30000;
private static TokenService tokenService;
@Autowired
public void setTokenService(TokenService tokenService) {
WebSocketServer.tokenService = tokenService;
}
private String token;
private LoginUser loginUser;
static {
scheduler.scheduleAtFixedRate(() -> {
long currentTime = System.currentTimeMillis();
lastActiveTimeMap.forEach((token, lastActiveTime) -> {
if (currentTime - lastActiveTime > HEARTBEAT_TIMEOUT) {
Session session = sessionMap.get(token);
if (session != null) {
try {
session.close(new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, "心跳超时"));
} catch (IOException e) {
log.error("关闭WebSocket连接失败", e);
}
}
}
});
}, 0, HEARTBEAT_TIMEOUT / 2, TimeUnit.MILLISECONDS);
}
@OnOpen
public void onOpen(Session session) {
log.info("有新连接加入,等待认证...");
}
@OnClose
public void onClose() {
if (this.token != null) {
sessionMap.remove(this.token);
lastActiveTimeMap.remove(this.token);
subOnlineCount();
log.info("连接关闭:用户ID={},当前在线人数为:{}",
loginUser != null ? loginUser.getUserId() : "unknown",
getOnlineCount());
}
}
@OnMessage
public void onMessage(String message, Session session) {
try {
ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = mapper.readTree(message);
// 处理认证消息
if (this.token == null) {
if (!jsonNode.has("type") || !"AUTH".equals(jsonNode.get("type").asText())) {
sendError(session, "请先发送认证消息");
return;
}
String token = jsonNode.get("token").asText();
LoginUser user = tokenService.getLoginUser(token);
if (user == null) {
sendError(session, "无效的token");
session.close(new CloseReason(CloseReason.CloseCodes.VIOLATED_POLICY, "认证失败"));
return;
}
this.token = token;
this.loginUser = user;
sessionMap.put(token, session);
lastActiveTimeMap.put(token, System.currentTimeMillis());
addOnlineCount();
log.info("认证成功:用户ID={},用户名={}", user.getUserId(), user.getUsername());
// 发送认证成功响应
session.getBasicRemote().sendText(mapper.writeValueAsString(Map.of(
"type", "AUTH_RESPONSE",
"success", true,
"message", "认证成功",
"userId", user.getUserId()
)));
return;
}
// 更新最后活跃时间
lastActiveTimeMap.put(this.token, System.currentTimeMillis());
// 处理心跳消息
if (jsonNode.has("type") && "HEARTBEAT".equals(jsonNode.get("type").asText())) {
session.getBasicRemote().sendText(mapper.writeValueAsString(Map.of(
"type", "HEARTBEAT_ACK"
)));
return;
}
// 处理业务消息
if (jsonNode.has("type") && "MESSAGE".equals(jsonNode.get("type").asText())) {
String content = jsonNode.get("content").asText();
log.info("收到来自用户{}的消息: {}", loginUser.getUserId(), content);
// 业务处理逻辑...
session.getBasicRemote().sendText(mapper.writeValueAsString(Map.of(
"type", "MESSAGE_RESPONSE",
"content", "服务器收到消息: " + content,
"timestamp", System.currentTimeMillis()
)));
}
} catch (Exception e) {
log.error("处理消息异常", e);
try {
sendError(session, "处理消息时发生错误: " + e.getMessage());
} catch (IOException ex) {
log.error("发送错误消息失败", ex);
}
}
}
@OnError
public void onError(Session session, Throwable error) {
log.error("WebSocket发生错误", error);
}
private void sendError(Session session, String errorMessage) throws IOException {
session.getBasicRemote().sendText(new ObjectMapper().writeValueAsString(Map.of(
"type", "ERROR",
"message", errorMessage
)));
}
public static void sendToUser(String token, String message) {
Session session = sessionMap.get(token);
if (session != null && session.isOpen()) {
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
log.error("发送消息失败", e);
}
}
}
public static void sendToUserByUserId(Long userId, String message) {
sessionMap.forEach((token, session) -> {
try {
LoginUser user = tokenService.getLoginUser(token);
if (user != null && userId.equals(user.getUserId())) {
session.getBasicRemote().sendText(message);
}
} catch (Exception e) {
log.error("通过用户ID发送消息异常", e);
}
});
}
public static int getOnlineCount() {
return onlineCount.get();
}
public static void addOnlineCount() {
onlineCount.incrementAndGet();
}
public static void subOnlineCount() {
onlineCount.decrementAndGet();
}
}
前端添加websocket操作类 且作为全局单例模式 任何文件都可以调用
import { getToken } from '@/utils/auth'
import { Message, MessageBox } from 'element-ui'
class WebSocketClient {
constructor(options = {}) {
const defaultOptions = {
url: '',
heartBeat: 30000, // 心跳间隔30秒
reconnectDelay: 5000, // 重连延迟5秒
maxReconnectAttempts: 5, // 最大重连次数
onOpen: () => {},
onMessage: () => {},
onClose: () => {},
onError: () => {},
onAuthenticated: () => {} // 新增认证成功回调
}
this.options = { ...defaultOptions, ...options }
this.ws = null
this.reconnectAttempts = 0
this.heartBeatTimer = null
this.isManualClose = false
this.isAuthenticated = false // 认证状态
}
connect() {
if (this.ws) {
this.close()
}
this.ws = new WebSocket(this.options.url)
this.ws.onopen = (event) => {
this.reconnectAttempts = 0
this.options.onOpen(event)
// 连接建立后立即发送认证消息
this.sendAuthMessage()
this.startHeartBeat()
}
this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data)
// 处理认证响应
if (data.type === 'AUTH_RESPONSE') {
if (data.success) {
this.isAuthenticated = true
this.options.onAuthenticated(data)
Message.success('WebSocket认证成功')
} else {
Message.error(data.message || 'WebSocket认证失败')
this.close()
}
return
}
// 其他消息处理
this.options.onMessage(event)
this.resetHeartBeat()
} catch (e) {
console.error('消息解析失败:', e)
}
}
this.ws.onclose = (event) => {
this.isAuthenticated = false
this.options.onClose(event)
this.stopHeartBeat()
if (!this.isManualClose) {
this.reconnect()
}
}
this.ws.onerror = (error) => {
this.options.onError(error)
this.stopHeartBeat()
if (!this.isManualClose) {
this.reconnect()
}
}
}
// 发送认证消息
sendAuthMessage() {
const token = getToken()
if (!token) {
Message.error('未获取到登录Token,请重新登录')
this.close()
return
}
this.send({
type: 'AUTH',
token: token,
timestamp: new Date().getTime()
})
}
send(data) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data))
} else {
console.error('WebSocket未连接')
}
}
// 其他方法保持不变...
close() { /* ... */ }
reconnect() { /* ... */ }
startHeartBeat() { /* ... */ }
stopHeartBeat() { /* ... */ }
resetHeartBeat() { /* ... */ }
}
// 全局单例
let wsInstance = null
export function initWebSocket() {
if (wsInstance) {
return wsInstance
}
wsInstance = new WebSocketClient({
url: process.env.VUE_APP_WS_API,
onAuthenticated: (data) => {
// 认证成功后的处理
console.log('WebSocket认证成功', data)
},
onMessage: (event) => {
try {
const data = JSON.parse(event.data)
if (data.type === 'NOTIFICATION') {
MessageBox.alert(data.content, data.title || '系统通知', {
confirmButtonText: '确定',
type: data.level || 'info'
})
}
} catch (e) {
console.error('消息处理错误:', e)
}
},
onError: (error) => {
Message.error('WebSocket连接错误: ' + error.message)
}
})
return wsInstance
}
export function getWebSocket() {
return wsInstance
}
export function closeWebSocket() {
if (wsInstance) {
wsInstance.close()
wsInstance = null
}
}
docker-compose
version: '3'
services:
jenkins:
image: jenkins/jenkins
container_name: jenkins
restart: unless-stopped #指定容器退出后的重启策略为始终重启,但是不考虑在Docker守护进程启动时就已经停止了的容器
volumes:
- "/usr/bin/docker:/usr/bin/docker"
- "/var/run/docker.sock:/var/run/docker.sock"
- "/usr/lib64/libltdl.so.7:/usr/lib/x86_64-linux-gnu/libltdl.so.7"
- "./jenkins/jenkins_home:/var/jenkins_home"
- "./jenkins/jenkins_config:/var/jenkins_config"
environment:
http_proxy: 'http://192.168.0.12:33333'
https_proxy: 'http://192.168.0.12:33333'
TZ: Asia/Shanghai
LANG: en_US.UTF-8
JAVA_OPTS: '-Xmx2048M -Xms2048M -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:MaxNewSize=128m -Djava.util.logging.config.file=/var/jenkins_home/log.properties -Duser.timezone=Asia/Shanghai'
user: root
ports:
- "10000:8080"
由于默认插件地址是国外的 没有插件jenkins可就没啥用了
所以安装完成后在插件市场 plugin manager设置 插件更新地址
https://eastamerica.cloudflare.jenkins.io/current/update-center.json
JWT和Sa-Token在功能、使用场景和优缺点等方面存在显著差异。
Dockerfile :
FROM docker.1ms.run/openjdk:8
MAINTAINER Robin Luo
RUN mkdir -p /ruoyi/server/logs \
/ruoyi/server/temp \
/ruoyi/skywalking/agent
WORKDIR /ruoyi/server
ENV SERVER_PORT=8080
EXPOSE ${SERVER_PORT}
ADD ./ruoyi-admin.jar ./app.jar
ENTRYPOINT ["java", \
"-Djava.security.egd=file:/dev/./urandom", \
"-Dserver.port=${SERVER_PORT}", \
# 应用名称 如果想区分集群节点监控 改成不同的名称即可
# "-Dskywalking.agent.service_name=ruoyi-server", \
# "-javaagent:/ruoyi/skywalking/agent/skywalking-agent.jar", \
"-jar", "app.jar"]
docker-compose.yaml
version: '3.8'
services:
ruoyi-his-server:
build:
context: .
dockerfile: Dockerfile
container_name: ruoyi-his-server
ports:
- "8009:8080"
environment:
- SPRING_PROFILES_ACTIVE=druid,dev
restart: always
再该目录下 执行 docker-compose up -d 启动服务
执行 dock-compose down 停止服务
DWR 全称 direct web remote
可将java的一个对象当成js一个对象执行方法返回结果
可以理解为js域通向java服务端的一种RPC(远程方法调用)
技术执行流 为 js-> js 对象执行方法-> post 异步请求->DWRServlet->java 对象
中间post 异步请求与 DWRServlet 均被封装屏蔽
用户只需要调用DWRServlet 动态生成的js脚本里面生成的js对象的方法 就可以发起通讯
gradle import local jar as dependency
dependencies{
// 依赖某个jar文件
implementation files(‘lib/xxx.jar’)
// 依赖libs目录下所有以.jar结尾的文件
implementation fileTree(dir: ‘lib’, include: [‘.jar’])
// 依赖libs目录下除了xxx.jar以外的所有以.jar结尾的文件 implementation fileTree(dir: ‘lib’, exclude: [‘xxx.jar’], include: [‘.jar’])
}
仓库名称 | 阿里云仓库地址 | 阿里云仓库地址(老版) | 源地址 |
---|---|---|---|
central | https://maven.aliyun.com/repository/central | https://maven.aliyun.com/nexus/content/repositories/central | https://repo1.maven.org/maven2/ |
jcenter | https://maven.aliyun.com/repository/public | https://maven.aliyun.com/nexus/content/repositories/jcenter | http://jcenter.bintray.com/ |
public | https://maven.aliyun.com/repository/public | https://maven.aliyun.com/nexus/content/groups/public | central仓和jcenter仓的聚合仓 |
https://maven.aliyun.com/repository/google | https://maven.aliyun.com/nexus/content/repositories/google | https://maven.google.com/ | |
gradle-plugin | https://maven.aliyun.com/repository/gradle-plugin | https://maven.aliyun.com/nexus/content/repositories/gradle-plugin | https://plugins.gradle.org/m2/ |
spring | https://maven.aliyun.com/repository/spring | https://maven.aliyun.com/nexus/content/repositories/spring | http://repo.spring.io/libs-milestone/ |
spring-plugin | https://maven.aliyun.com/repository/spring-plugin | https://maven.aliyun.com/nexus/content/repositories/spring-plugin | http://repo.spring.io/plugins-release/ |
grails-core | https://maven.aliyun.com/repository/grails-core | https://maven.aliyun.com/nexus/content/repositories/grails-core | https://repo.grails.org/grails/core |
apache snapshots | https://maven.aliyun.com/repository/apache-snapshots | https://maven.aliyun.com/nexus/content/repositories/apache-snapshots | https://repository.apache.org/snapshots/ |
maven 版本
<repository>
<id>spring</id>
<url>https://maven.aliyun.com/repository/spring</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
gradle 版本
allProjects {
repositories {
maven {
url 'https://maven.aliyun.com/repository/public/'
}
maven {
url 'https://maven.aliyun.com/repository/spring/'
}
mavenLocal()
mavenCentral()
}
}
流程定义模块-> 设计-> 部署 删除
流程定义历史模块
流程实例模块
任务实例模块
<dependency> <groupId>org.activiti</groupId> <artifactId>activiti-spring-boot-starter</artifactId> <version>7.1.0.M6</version> </dependency> 注入activiti 组件 初始化activiti 表 以下单元测试用例 用来初始化 activiti 表
@SpringBootTest
class ActivitiApplicationTests {
@Test
void contextLoads() {
ProcessEngineConfiguration processEngineConfiguration = ProcessEngineConfiguration.createProcessEngineConfigurationFromResource("activiti.cfg.xml");
processEngineConfiguration.buildProcessEngine();
}
}
activiti.cfg.xml 是在类目录下的 一个spring beans 配置文件
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="dataSource" class="com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceWrapper" >
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver" />
<property name="url" value="jdbc:mysql://mysql.robinluo.top/activiti?characterEncoding=UTF-8&nullCatalogMeansCurrent=true&serverTimezone=GMT&useSSL=false" />
<property name="username" value="admin" />
<property name="password" value="RobinLuo@2021" />
</bean>
<bean id="processEngineConfiguration" class="org.activiti.engine.impl.cfg.StandaloneProcessEngineConfiguration">
<!-- ... -->
<property name="databaseSchemaUpdate" value="true" />
<property name="dataSource" ref="dataSource" />
</bean>
</beans>