使用 Vue3 + Pinia + Ant Design Vue3 搭建后台管理系统

Vue3 & Ant Design Vue3基础


nodejs版本要求:node-v18.16.0-x64
nodejs基础配置

npm -v
node -vnpm config set prefix "D:\software\nodejs\node_global"
npm config set cache "D:\software\nodejs\node_cache"npm config get registry
npm config set registry https://registry.npm.taobao.org

安装Vue3

npm install @vue/cli -g
vue --version#npm install @vue/cli@5.0.8 -g	安装指定版本
#npm uninstall @vue/cli -g

使用Vue创建前端项目

npm create vue@latest
√ Project name: ...web
√ Add TypeScript? ... No 
√ Add JSX Support? ... No 
√ Add Vue Router for Single Page Application development? ...  Yes
√ Add Pinia for state management? ...  Yes
√ Add Vitest for Unit Testing? ... No 
√ Add an End-to-End Testing Solution? » No
√ Add ESLint for code quality? ... Yes
√ Add Prettier for code formatting? ... Yes

启动前端项目

cd web
npm install
npm run dev

浏览器访问:http://localhost:5173
修改端口号,修改配置 vite.config.js

export default defineConfig({server: {port: 9000},plugins: [vue(),],resolve: {alias: {'@': fileURLToPath(new URL('./src', import.meta.url))}}
})

再次访问:http://localhost:9000/


https://antdv.com/components/overview-cn
Ant Design Vue官方文档

安装Ant Design Vue

npm install ant-design-vue --save
npm install --save @ant-design/icons-vue#自动按需引入组件
npm install unplugin-vue-components -D

修改配置文件vite.config.js

import {fileURLToPath, URL} from 'node:url'import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
import Components from 'unplugin-vue-components/vite';
import {AntDesignVueResolver} from 'unplugin-vue-components/resolvers';// https://vitejs.dev/config/
export default defineConfig({server: {port: 9000},plugins: [vue(),Components({resolvers: [AntDesignVueResolver({importStyle: false, // css in js}),],}),],resolve: {alias: {'@': fileURLToPath(new URL('./src', import.meta.url))}}
})

修改main.js

import './assets/main.css'import { createApp } from 'vue'
import { createPinia } from 'pinia'
import Antd from 'ant-design-vue';import App from './App.vue'
import router from './router'
import 'ant-design-vue/dist/reset.css';const app = createApp(App)app.use(createPinia())
app.use(router)
app.use(Antd)app.mount('#app')

添加一个测试页面

<script setup>
import {ZoomOutOutlined} from "@ant-design/icons-vue";
</script><template><div class="about"><h1>This is an about page</h1><a-button>测试</a-button><br><ZoomOutOutlined /></div>
</template><style>
</style>

Antd栅格把页面平均分成24份

<template><a-row><a-col :span="24">col</a-col></a-row><a-row><a-col :span="12">col-12</a-col><a-col :span="12">col-12</a-col></a-row><a-row><a-col :span="8">col-8</a-col><a-col :span="8">col-8</a-col><a-col :span="8">col-8</a-col></a-row><a-row><a-col :span="6">col-6</a-col><a-col :span="6">col-6</a-col><a-col :span="6">col-6</a-col><a-col :span="6">col-6</a-col></a-row>
</template>

使用Pinia管理用户状态

刷新页面,Pinia中的数据会丢失,使用Pinia插件做数据持久化

npm install --save zipson
npm install --save pinia-plugin-persistedstate

修改main.js

import './assets/main.css'import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import Antd from 'ant-design-vue';import App from './App.vue'
import router from './router'
import 'ant-design-vue/dist/reset.css';const app = createApp(App)
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate);//pinia数据持久化app.use(pinia)
app.use(router)
app.use(Antd)app.mount('#app')

使用Pinia保存用户状态,添加文件src/stores/user.js

import {reactive} from 'vue'
import {defineStore} from 'pinia'
import {stringify, parse} from 'zipson'const MEMBER = "MEMBER"
export const useUserStore = defineStore('user', () => {const userInfo = reactive({id: '',mobile: '',token: ''})function setUserInfo({id, mobile, token}) {userInfo.id = iduserInfo.mobile = mobileuserInfo.token = token}function clearUserInfo() {userInfo.id = ''userInfo.mobile = ''userInfo.token = ''}return {userInfo, setUserInfo, clearUserInfo}
}, {persist: {key: MEMBER,storage: sessionStorage,// paths: ['count'],serializer: {deserialize: parse,serialize: stringify},beforeRestore: (ctx) => {console.log(`about to restore '${ctx.store.$id}'`)},afterRestore: (ctx) => {console.log(`just restored '${ctx.store.$id}'`)},debug: true,}
})

ite多环境配置

https://vitejs.cn/vite3-cn/guide/env-and-mode.html#env-variables
官方配置文档

在根目录创建文件 .env.development

NODE_ENV=development
#自定义变量需要以VITE_开头
VITE_APP_BASE_URL=http://localhost:8000

生产环境 .env.production

NODE_ENV=production
VITE_APP_BASE_URL=http://train.intmall.com

使用环境变量

axios.defaults.baseURL = import.meta.env.VITE_APP_BASE_URL;
console.log(process.env.NODE_ENV)
console.log(import.meta.env.VITE_APP_BASE_URL)

封装网络请求工具类Axios

npm install axios --save

封装网络请求工具类 src/utils/request.js

import axios from 'axios'
import {notification} from 'ant-design-vue';
import {useUserStore} from '@/stores/user';
import router from '@/router'const {userInfo, clearUserInfo} = useUserStore()
export const serverUrl = import.meta.env.VITE_APP_BASE_URLconst service = axios.create({baseURL: serverUrl,timeout: 5000
})// Add a request interceptor 全局请求拦截
service.interceptors.request.use(function (config) {// Do something before request is sentconst token = userInfo.tokenif (token) {config.headers['token'] = token}// 此处还可以设置tokenreturn config},function (error) {// Do something with request errorreturn Promise.reject(error)}
)// Add a response interceptor 全局相应拦截
service.interceptors.response.use(function (response) {// Any status code that lie within the range of 2xx cause this function to trigger// Do something with response data// 如果是固定的数据返回模式,此处可以做继续完整的封装const resData = response.data || {}if (resData.success) {return resData}notification.error({description: resData.message});return Promise.reject(resData.message)},function (error) {// Any status codes that falls outside the range of 2xx cause this function to trigger// Do something with response error// 此处需要对返回的状态码或者异常信息作统一处理console.log('error', error)const response = error.response;const status = response.status;if (status === 401) {// 判断状态码是401 跳转到登录页console.log("未登录或登录超时,跳到登录页");clearUserInfo()notification.error({description: "未登录或登录超时"});router.push('/login')}return Promise.reject(error)}
)export const get = (url, params) => {return service.get(url, {params})
}export const post = (url, data) => service.post(url, data)export const put = (url, data) => service.put(url, data)export const del = (url, data) => service.delete(url)

遇到的问题:
useRouter失效,router无法跳转页面
https://blog.csdn.net/qq_57700056/article/details/133530562

后台接口调用示例: src/api/userApi.js

import { get, post, put, del } from "../utils/request";// 用户登录
export async function login(data) {return post('/member/member/login', data)
}export async function sendCode(data) {return post('/member/member/sendCode', data)
}export async function getUserCount() {return get('/member/member/count')
}export async function savePassenger(data) {return post('/member/passenger/save', data)
}export async function queryPassengerList(data) {return post('/member/passenger/queryList', data)
}export async function deletePassenger(id) {return del(`/member/passenger/delete/${id}`)
}// 导出 userApi 方法
export default {login,sendCode,getUserCount,savePassenger,deletePassenger,queryPassengerList
}

前端页面路由配置

增加路由防卫,判断要跳转的页面是否需要登录
src/router/index.js
由于router挂载比pinia要早,守卫在在使用pinia时,pinia还没有挂载,把pinia写在守卫里面即可解决问题

import {createRouter, createWebHistory} from 'vue-router'
import {notification} from 'ant-design-vue';
import {useUserStore} from '@/stores/user';const router = createRouter({history: createWebHistory(import.meta.env.BASE_URL),routes: [{path: '/',name: 'main',component: () => import('../views/MainView.vue'),children: [{path: '/welcome',name: 'welcome',component: () => import('../views/main/WelcomeView.vue')}, {path: '/passenger',name: 'passenger',component: () => import('../views/main/PassengerView.vue')}]},{path: '/login',name: 'login',component: () => import('../views/LoginView.vue'),meta: {noToken: true}}, {path: '',redirect: '/welcome'}]
})// 路由登录拦截
router.beforeEach((to, from, next) => {// 要不要对meta.noToken属性做监控拦截if (to.matched.some(function (item) {console.log(item, "是否不需要登录校验:", item.meta.noToken || false);return !item.meta.noToken})) {const {userInfo} = useUserStore()console.log("页面登录校验开始:", userInfo);if (!userInfo.token) {console.log("用户未登录或登录超时!");notification.error({description: "未登录或登录超时"});next('/login');} else {next();}} else {next();}
});export default router

登录页面

<script setup>
import {reactive} from 'vue';
import {useRouter} from 'vue-router'
import {CodepenCircleOutlined} from "@ant-design/icons-vue";
import {notification} from 'ant-design-vue';
import userApi from '../api/userApi';
import { useUserStore } from '@/stores/user';
const { setUserInfo } = useUserStore()const router = useRouter();const loginForm = reactive({mobile: '',code: '',
});
const onFinish = async (value) => {// 执行登录逻辑const respData = await userApi.login(value);const data = respData.datasetUserInfo(data)console.log('Success:', value, data);notification.success({ description: '登录成功!' });router.push("/welcome");
};
const onFinishFailed = errorInfo => {console.log('Failed:', errorInfo);
};const sendCode = async () => {await userApi.sendCode({mobile: loginForm.mobile})notification.success({ description: '发送验证码成功!' });loginForm.code = "8888";
}
</script><template><a-row class="login"><a-col :span="8" :offset="8" class="login-main"><h1 style="text-align: center"><CodepenCircleOutlined/>&nbsp;模拟12306售票系统</h1><a-form:model="loginForm"name="basic"autocomplete="off"@finish="onFinish"@finishFailed="onFinishFailed"><a-form-itemlabel=""name="mobile":rules="[{ required: true, message: '请输入手机号!' }]"><a-input v-model:value="loginForm.mobile" placeholder="手机号"/></a-form-item><a-form-itemlabel=""name="code":rules="[{ required: true, message: '请输入验证码!' }]"><a-input v-model:value="loginForm.code"><template #addonAfter><a @click="sendCode">获取验证码</a></template></a-input></a-form-item><a-form-item :wrapper-col="{ offset: 8, span: 16 }"><a-button type="primary" html-type="submit">登录</a-button></a-form-item></a-form></a-col></a-row></template><style scoped>
.login-main h1 {font-size: 25px;font-weight: bold;
}.login-main {margin-top: 100px;padding: 30px 30px 20px;border: 2px solid grey;border-radius: 10px;background-color: #fcfcfc;
}
</style>

页面增删改查操作

<script setup>
import {ref, reactive} from 'vue';
import {notification} from "ant-design-vue";
import {cloneDeep} from 'lodash-es';
import userApi from '@/api/userApi';
import {PASSENGER_TYPE_ARRAY} from '@/assets/js/enums'const visible = ref(false);
const loading = ref(false);let passenger = ref({id: undefined,memberId: undefined,name: undefined,idCard: undefined,type: undefined,createTime: undefined,updateTime: undefined,
});const passengers = ref([]);
const pagination = reactive({total: 0,current: 1,pageSize: 2
})const columns = [{title: '姓名',dataIndex: 'name',key: 'name',},{title: '身份证',dataIndex: 'idCard',key: 'idCard',},{title: '旅客类型',dataIndex: 'type',key: 'type',},{title: '操作',dataIndex: 'operation'},
]
const handleQuery = (param) => {if (!param) {param = {"page": 1,"size": pagination.pageSize}}loading.value = true;userApi.queryPassengerList({"page": param.page,"limit": param.size}).then(res => {console.log('res', res)loading.value = false;passengers.value = res.data;pagination.total = res.count;pagination.current = res.page})
}const handleTableChange = (pagination) => {handleQuery({page: pagination.current,size: pagination.pageSize})
}handleQuery()const onAdd = () => {passenger.value = {};visible.value = true;
}const onEdit = (record) => {console.log("record", record)passenger.value = cloneDeep(record);visible.value = true;
}const onDelete = (record) => {console.log("delete record", record)userApi.deletePassenger(record.id).then(() => {notification.success({description: "删除成功!"});handleQuery({page: pagination.current,size: pagination.pageSize,});})
}const handleOk = () => {userApi.savePassenger(passenger.value).then(resp => {notification.success({description: "保存成功!"});visible.value = false;handleQuery({page: pagination.current,size: pagination.pageSize})})
}
</script><template><p><a-space><a-button type="primary" @click="handleQuery()">刷新</a-button><a-button type="primary" @click="onAdd">新增</a-button></a-space></p><a-table :dataSource="passengers" :columns="columns" :pagination="pagination" @change="handleTableChange":loading="loading"><template #bodyCell="{ column, text, record }"><template v-if="column.dataIndex === 'operation'"><a-space><a @click="onEdit(record)">编辑</a><a-popconfirmtitle="删除后不可恢复,确认删除?"ok-text="确认" cancel-text="取消"@confirm="onDelete(record)"><a style="color: red">删除</a></a-popconfirm></a-space></template><template v-else-if="column.dataIndex === 'type'"><span v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code"><span v-if="item.code === record.type">{{item.desc}}</span></span></template></template></a-table><a-modal v-model:open="visible" title="乘车人" @ok="handleOk"ok-text="确认" cancel-text="取消"><a-form:model="passenger" :label-col="{span: 4}" :wrapper-col="{ span: 20 }"><a-form-item label="姓名"><a-input v-model:value="passenger.name"/></a-form-item><a-form-item label="身份证"><a-input v-model:value="passenger.idCard"/></a-form-item><a-form-item label="旅客类型"><a-select v-model:value="passenger.type"><a-select-option v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code" :value="item.code">{{ item.desc }}</a-select-option></a-select></a-form-item></a-form></a-modal></template>

前端跨域问题

前后端分离项目,前端在请求后台接口时会出现跨域问题
这个后端项目使用到了gateway,在配置文件中加入:

# 允许请求来源(老版本叫allowedOrigin)
spring.cloud.gateway.globalcors.cors-configurations.[/**].allowedOriginPatterns=*
# 允许携带的头信息
spring.cloud.gateway.globalcors.cors-configurations.[/**].allowedHeaders=*
# 允许的请求方式
spring.cloud.gateway.globalcors.cors-configurations.[/**].allowedMethods=*
# 是否允许携带cookie
spring.cloud.gateway.globalcors.cors-configurations.[/**].allowCredentials=true
# 跨域检测的有效期,会发起一个OPTION请求
spring.cloud.gateway.globalcors.cors-configurations.[/**].maxAge=3600

后端分页查询方法

#CommonPageParam.java
@Data
public class CommonPageParam {@NotNull(message = "页码不能为空")private Integer page;@NotNull(message = "每页数量不能为空")@Max(value = 100, message = "分页条数不能超过100")private Integer limit;
}#PassengerQueryReq.java
@Data
public class PassengerQueryReq extends CommonPageParam {private Long memberId;
}#CommonPageResp.java
@Data
@NoArgsConstructor
public class CommonPageResp<T> {/** 默认每页的条数 */public static final int PAGE_SIZE_DEFAULT = 10;/*** 业务上的成功或失败*/private boolean success = true;/*** 返回信息*/private String message;/*** 返回泛型数据,自定义类型*/private List<T> data;/*** 总数*/private Long count;/*** 页码*/private Integer page;/*** 每页数量*/private Integer limit;public Integer getPage() {if (page == null || page < 1) {return 1;}return page;}public Integer getLimit() {if (limit == null) {return PAGE_SIZE_DEFAULT;}return limit;}public static <T> CommonPageResp<T> SUCCESS(String message, List<T> data, PageInfo pageInfo) {return new CommonPageResp<>(true, message, data, pageInfo.getTotal(), pageInfo.getPageNum(), pageInfo.getPageSize());}public CommonPageResp(boolean success, String message, List<T> data, Long count, Integer page, Integer limit) {this.success = success;this.message = message;this.data = data;this.count = count;this.page = page;this.limit = limit;}
}#PassengerService.java
@Service
@Slf4j
public class PassengerService {@Resourceprivate PassengerMapper passengerMapper;public CommonPageResp<PassengerQueryResp> queryList(PassengerQueryReq req) {PassengerExample passengerExample = new PassengerExample();passengerExample.setOrderByClause("id desc");PassengerExample.Criteria criteria = passengerExample.createCriteria();if (ObjectUtil.isNotNull(req.getMemberId())) {criteria.andMemberIdEqualTo(req.getMemberId());}PageHelper.startPage(req.getPage(), req.getLimit());List<Passenger> passengerList = passengerMapper.selectByExample(passengerExample);PageInfo<Passenger> pageInfo = new PageInfo<>(passengerList);List<PassengerQueryResp> list = BeanUtil.copyToList(passengerList, PassengerQueryResp.class);return CommonPageResp.SUCCESS("", list, pageInfo);}
}

源码地址

完整代码参考:
https://gitee.com/galen.zhang/train

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/165123.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2023亚太赛数学建模A题:采果机器人的图像识别技术思路模型代码

亚太A题&#xff1a;采果机器人的图像识别技术 A题完整思路获取 &#xff1a;获取见文末名片&#xff0c;第一时间更新 中国是世界上最大的苹果生产国&#xff0c;年产量约为3500万吨。与此同时&#xff0c;中国也是世 界上最大的苹果出口国&#xff0c;全球每两个苹果中就有…

Android设计模式--装饰模式

千淘万漉虽辛苦&#xff0c;吹尽黄沙始到金 一&#xff0c;定义 动态地给一个对象添加一些额外的职责。就增加功能来说&#xff0c;装饰模式相比生成子类更为灵活。 装饰模式也叫包装模式&#xff0c;结构型设计模式之一&#xff0c;其使用一种对客户端透明的方式来动态地扩展…

QT 中的元对象系统

作为一名十几年的 C 程序员&#xff0c;最近一段时间使用 QT 开发程序&#xff0c;发现 QT 中还是有许多值得深入理解的技术。QT 不仅仅是一个应用程序开发框架&#xff0c;还有一些对标准 C 的扩充。本文和大家一起探讨 QT 中的元对象系统。 在分析 QT 中的元对象系统之前&…

<JavaEE> 什么是线程(Thread)?进程和线程有什么区别?

目录 一、线程&#xff08;Thread&#xff09;的概念 二、线程存在的意义 2.1 并发编程 2.2 比进程更“轻量” 三、使用线程时应该注意 四、进程和线程的区别 五、Java中的线程和操作系统中的线程是不同的概念 六、多线程编程 一、线程&#xff08;Thread&#xff09;的…

蓝桥杯官网练习题(奇怪的数列)

题目描述 从 X 星截获一份电码&#xff0c;是一些数字&#xff0c;如下&#xff1a; 13 1113 3113132113 1113122113 ⋯ YY 博士经彻夜研究&#xff0c;发现了规律&#xff1a; 第一行的数字随便是什么&#xff0c;以后每一行都是对上一行"读出来" 比如第 2…

图神经网络的数学原理总结

图深度学习(Graph Deep Learning) 多年来一直在加速发展。许多现实生活问题使GDL成为万能工具&#xff1a;在社交媒体、药物发现、芯片植入、预测、生物信息学等方面都显示出了很大的前景。 本文将流行的图神经网络及其数学细微差别的进行详细的梳理和解释&#xff0c;图深度学…

Linux中flask项目开启https访问

1.下载阿里云免费证书 2.项目添加https配置 3.服务器开启https访问 3.1 重新安装OpenSSL 3.2.重新安装Python 上一次已经讲过Linux安装部署Python: Linux安装Python3.10与部署flask项目实战详细记录,今天记录一下Python项目如何支持https访问…

《计算机中的程序》(灵魂六问)

目录 1、程序是什么&#xff1f; 2、程序是由什么组成的&#xff1f; 3、什么是机器语言&#xff1f; 4、正在运行的程序存储在什么位置&#xff1f; 5、什么是内存地址&#xff1f; 6、计算机的构成元件中&#xff0c;负责程序的解释和运行是哪个&#xff1f; 1、程序是什…

性能相关的闪存特性

一、多Plane操作 上章提到若干个Plane组成Die或者叫LUN,即一个Die上有多个Plane 每次进行写操作时&#xff0c;控制器先将数据写入页缓存中&#xff0c;等同一个Die上另一个Plane也写数据的时候&#xff0c;再同时写入&#xff0c;原来单独操作一个Plane的时间变成了可以同时做…

Springmvc实现增删改差

一、包结构 二、各层代码 (1)数据User public class User {private Integer id;private String userName;private String note;public User() {super();}public User(Integer i, String userName, String note) {super();this.id i;this.userName userName;this.note note;…

Qt实现自定义IP地址输入控件(百分百还原Windows 10网络地址输入框)

在开发网络相关的程序时,我们经常需要输入IP地址,例如源地址和目标地址。Qt提供了一些基础的控件,如QLineEdit,但是它们并不能满足我们对IP地址输入的要求,例如限制输入的格式、自动跳转到下一个输入框、处理回车和退格键等。因此,我们需要自己编写一个自定义的IP地址输入…

AI AIgents时代- Autogen

由微软开发的 Autogen 是一个新的 Agents 项目&#xff0c;刚一上线就登上GitHub热榜&#xff0c;狂揽11k星✨✨✨ 项目地址&#xff1a;https://github.com/microsoft/autogen Autogen 允许你根据需要创建任意数量的Agents&#xff0c;并让它们协同工作以执行任务。它的独特…

外观模式 (Facade Pattern)

定义&#xff1a; 外观模式&#xff08;Facade Pattern&#xff09;是一种结构型设计模式&#xff0c;它通过提供一个统一的高层接口来简化复杂子系统或库的访问。这种模式的关键在于&#xff0c;它创建了一个外观类&#xff0c;这个类封装了对子系统的一系列复杂交互&#xf…

【赠书第7期】从零基础到精通Flutter开发

文章目录 前言 1 安装Flutter和Dart 2 了解Flutter的基础概念 2.1 Widget 2.2 MaterialApp和Scaffold 2.3 Hot Reload 3 编写你的第一个Flutter应用 3.1 创建一个Flutter项目 3.2 修改默认页面 3.3 添加交互 4 深入学习Flutter高级特性 4.1 路由和导航 4.2 状态管…

python之TCP的网络应用程序开发

文章目录 版权声明python3编码转换socket类的使用创建Socket对象Socket对象常用方法和参数使用示例服务器端代码客户端代码 TCP客户端程序开发流程TCP服务端程序开发流程TCP网络应用程序注意点socket之send和recv原理剖析send原理剖析recv原理剖析send和recv原理剖析图 多任务版…

浅谈C#在unity应用中的工厂模式

文章目录 前言简单工厂模式工厂方法模式抽象工厂模式Unity实战 前言 工厂模式是一种创建型设计模式&#xff0c;它提供了一种将对象的实例化过程封装起来的方法&#xff0c;使得客户端代码不必直接依赖于具体类。这有助于降低代码的耦合度&#xff0c;提高代码的可维护性和可扩…

Python项目打包

Python项目如何打包&#xff1f; 本指南总结了Python项目打包的最佳实践&#xff0c;主要涉及代码的打包和分发&#xff0c;以及环境和依赖的管理。 0. 一般项目清单 源代码&#xff08;可使用git托管&#xff09;数据包&#xff08;可使用DVC托管&#xff09;Docker环境镜像…

Python进行threading多线程编程及高级并发处理机制

threading 模块是 Python 中用于进行多线程编程的标准库之一。通过 threading 模块&#xff0c;你可以创建和管理线程&#xff0c;使得程序能够并发执行多个任务。以下是一些基本的 threading 模块的用法&#xff1a; 1. 创建线程&#xff1a; 使用 threading.Thread 类可以创…

在两个java项目中实现Redis的发布订阅模式

如何在两个java项目中实现Redis的发布订阅模式&#xff1f; 1. Redis简介2. 发布订阅模式介绍3. 实现思路4. 代码实现及详细解释4.1. RedisUtil4.2. Publisher4.3. Subscriber4.4. 运行程序 目录&#xff1a; Redis简介发布订阅模式介绍实现思路代码实现及详细解释 1. Redis简…

HTB Napper WriteUp

Napper 2023年11月12日 14:58:35User Nmap ➜ Napper nmap -sCV -A -p- 10.10.11.240 --min-rate 10000 Starting Nmap 7.80 ( https://nmap.org ) at 2023-11-12 13:58 CST Nmap scan report for app.napper.htb (10.10.11.240) Host is up (0.15s latency). Not shown: …