当前位置: 首页 > news >正文

众云网联做的网站效果好吗/太原seo网站优化

众云网联做的网站效果好吗,太原seo网站优化,wordpress评论去掉邮箱,滨江区建设局网站本文主要讲 Redis 的使用,如何与 SpringBoot 项目整合,如何使用注解方式和 RedisTemplate 方式实现缓存。最后会给一个用 Redis 实现分布式锁,用在秒杀系统中的案例。 更多 Redis 的实际运用场景请关注开源项目 coderiver 项目地址&#xff…

本文主要讲 Redis 的使用,如何与 SpringBoot 项目整合,如何使用注解方式和 RedisTemplate 方式实现缓存。最后会给一个用 Redis 实现分布式锁,用在秒杀系统中的案例。

更多 Redis 的实际运用场景请关注开源项目 coderiver

项目地址:https://github.com/cachecats/...

一、NoSQL 概述

什么是 NoSQL ?

NoSQL(NoSQL = Not Only SQL ),意即“不仅仅是SQL”,泛指非关系型的数据库。

为什么需要 NoSQL ?

随着互联网web2.0网站的兴起,传统的关系数据库在应付web2.0网站,特别是超大规模和高并发的SNS类型的web2.0纯动态网站已经显得力不从心,暴露了很多难以克服的问题,而非关系型的数据库则由于其本身的特点得到了非常迅速的发展。NoSQL数据库的产生就是为了解决大规模数据集合多重数据种类带来的挑战,尤其是大数据应用难题。 -- 百度百科

NoSQL 数据库的四大分类

  • 键值(key-value)存储
  • 列存储
  • 文档数据库
  • 图形数据库
分类相关产品典型应用数据模型优点缺点
键值(key-value)Tokyo、 Cabinet/Tyrant、Redis、Voldemort、Berkeley DB内容缓存,主要用于处理大量数据的高访问负载一系列键值对快速查询存储的数据缺少结构化
列存储数据库Cassandra, HBase, Riak分布式的文件系统以列簇式存储,将同一列数据存在一起查找速度快,可扩展性强,更容易进行分布式扩展功能相对局限
文档数据库CouchDB, MongoDBWeb应用(与Key-Value类似,value是结构化的)一系列键值对数据结构要求不严格查询性能不高,而且缺乏统一的查询语法
图形(Graph)数据库Neo4J, InfoGrid, Infinite Graph社交网络,推荐系统等。专注于构建关系图谱图结构利用图结构相关算法需要对整个图做计算才能得出结果,不容易做分布式集群方案

NoSQL 的特点

  • 易扩展
  • 灵活的数据模型
  • 大数据量,高性能
  • 高可用

二、Redis 概述

Redis的应用场景

  • 缓存
  • 任务队列
  • 网站访问统计
  • 应用排行榜
  • 数据过期处理
  • 分布式集群架构中的 session 分离

Redis 安装

网上有很多 Redis 的安装教程,这里就不多说了,只说下 Docker 的安装方法:

Docker 安装运行 Redis

docker run -d -p 6379:6379 redis:4.0.8

如果以后想启动 Redis 服务,打开命令行,输入以下命令即可。

redis-server

使用前先引入依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

三、注解方式使用 Redis 缓存

使用缓存有两个前置步骤

  1. pom.xml 引入依赖

    <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
  2. 在启动类上加注解 @EnableCaching

    @SpringBootApplication
    @EnableCaching
    public class SellApplication {public static void main(String[] args) {SpringApplication.run(SellApplication.class, args);}
    }

常用的注解有以下几个

  • @Cacheable

    属性如下图

用于查询和添加缓存,第一次查询的时候返回该方法返回值,并向 Redis 服务器保存数据。

以后调用该方法先从 Redis 中查是否有数据,如果有直接返回 Redis 缓存的数据,而不执行方法里的代码。如果没有则正常执行方法体中的代码。

value 或 cacheNames 属性做键,key 属性则可以看作为 value 的子键, 一个 value 可以有多个 key 组成不同值存在 Redis 服务器。

验证了下,value 和 cacheNames 的作用是一样的,都是标识主键。两个属性不能同时定义,只能定义一个,否则会报错。

condition 和 unless 是条件,后面会讲用法。其他的几个属性不常用,其实我也不知道怎么用…

  • @CachePut

    更新 Redis 中对应键的值。属性和 @Cacheable 相同

  • @CacheEvict

    删除 Redis 中对应键的值。

3.1 添加缓存

在需要加缓存的方法上添加注解 @Cacheable(cacheNames = "product", key = "123"),

cacheNameskey 都必须填,如果不填 key ,默认的 key 是当前的方法名,更新缓存时会因为方法名不同而更新失败。

如在订单列表上加缓存

    @RequestMapping(value = "/list", method = RequestMethod.GET)@Cacheable(cacheNames = "product", key = "123")public ResultVO list() {// 1.查询所有上架商品List<ProductInfo> productInfoList = productInfoService.findUpAll();// 2.查询类目(一次性查询)//用 java8 的特性获取到上架商品的所有类型List<Integer> categoryTypes = productInfoList.stream().map(e -> e.getCategoryType()).collect(Collectors.toList());List<ProductCategory> productCategoryList = categoryService.findByCategoryTypeIn(categoryTypes);List<ProductVO> productVOList = new ArrayList<>();//数据拼装for (ProductCategory category : productCategoryList) {ProductVO productVO = new ProductVO();//属性拷贝BeanUtils.copyProperties(category, productVO);//把类型匹配的商品添加进去List<ProductInfoVO> productInfoVOList = new ArrayList<>();for (ProductInfo productInfo : productInfoList) {if (productInfo.getCategoryType().equals(category.getCategoryType())) {ProductInfoVO productInfoVO = new ProductInfoVO();BeanUtils.copyProperties(productInfo, productInfoVO);productInfoVOList.add(productInfoVO);}}productVO.setProductInfoVOList(productInfoVOList);productVOList.add(productVO);}return ResultVOUtils.success(productVOList);}

可能会报如下错误

对象未序列化。让对象实现 Serializable 方法即可

@Data
public class ProductVO implements Serializable {private static final long serialVersionUID = 961235512220891746L;@JsonProperty("name")private String categoryName;@JsonProperty("type")private Integer categoryType;@JsonProperty("foods")private List<ProductInfoVO> productInfoVOList ;
}

生成唯一的 id 在 IDEA 里有一个插件:GenerateSerialVersionUID 比较方便。

重启项目访问订单列表,在 rdm 里查看 Redis 缓存,有 product::123 说明缓存成功。

3.2 更新缓存

在需要更新缓存的方法上加注解: @CachePut(cacheNames = "prodcut", key = "123")

注意

  1. cacheNameskey 要跟 @Cacheable() 里的一致,才会正确更新。
  2. @CachePut()@Cacheable() 注解的方法返回值要一致

3.3 删除缓存

在需要删除缓存的方法上加注解:@CacheEvict(cacheNames = "prodcut", key = "123"),执行完这个方法之后会将 Redis 中对应的记录删除。

3.4 其他常用功能

  1. cacheNames 也可以统一写在类上面, @CacheConfig(cacheNames = "product") ,具体的方法上就不用写啦。

    @CacheConfig(cacheNames = "product")
    public class BuyerOrderController {@PostMapping("/cancel")@CachePut(key = "456")public ResultVO cancel(@RequestParam("openid") String openid,@RequestParam("orderId") String orderId){buyerService.cancelOrder(openid, orderId);return ResultVOUtils.success();}
    }
  2. Key 也可以动态设置为方法的参数

    @GetMapping("/detail")
    @Cacheable(cacheNames = "prodcut", key = "#openid")
    public ResultVO<OrderDTO> detail(@RequestParam("openid") String openid,@RequestParam("orderId") String orderId){OrderDTO orderDTO = buyerService.findOrderOne(openid, orderId);return ResultVOUtils.success(orderDTO);
    }

    如果参数是个对象,也可以设置对象的某个属性为 key。比如其中一个参数是 user 对象,key 可以写成 key="#user.id"

  3. 缓存还可以设置条件。

    设置当 openid 的长度大于3时才缓存

    @GetMapping("/detail")
    @Cacheable(cacheNames = "prodcut", key = "#openid", condition = "#openid.length > 3")
    public ResultVO<OrderDTO> detail(@RequestParam("openid") String openid,@RequestParam("orderId") String orderId){OrderDTO orderDTO = buyerService.findOrderOne(openid, orderId);return ResultVOUtils.success(orderDTO);
    }

    还可以指定 unless 即条件不成立时缓存。#result 代表返回值,意思是当返回码不等于 0 时不缓存,也就是等于 0 时才缓存。

    
    @GetMapping("/detail")
    @Cacheable(cacheNames = "prodcut", key = "#openid", condition = "#openid.length > 3", unless = "#result.code != 0")
    public ResultVO<OrderDTO> detail(@RequestParam("openid") String openid,@RequestParam("orderId") String orderId){OrderDTO orderDTO = buyerService.findOrderOne(openid, orderId);return ResultVOUtils.success(orderDTO);
    }

四、RedisTemplate 使用 Redis 缓存

与使用注解方式不同,注解方式可以零配置,只需引入依赖并在启动类上加上 @EnableCaching 注解就可以使用;而使用 RedisTemplate 方式麻烦些,需要做一些配置。

4.1 Redis 配置

第一步还是引入依赖和在启动类上加上 @EnableCaching 注解。

然后在 application.yml 文件中配置 Redis

spring:redis:port: 6379database: 0host: 127.0.0.1password:jedis:pool:max-active: 8max-wait: -1msmax-idle: 8min-idle: 0timeout: 5000ms

然后写个 RedisConfig.java 配置类

package com.solo.coderiver.user.config;import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;import java.net.UnknownHostException;@Configuration
public class RedisConfig {@Bean@ConditionalOnMissingBean(name = "redisTemplate")public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)throws UnknownHostException {Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);ObjectMapper om = new ObjectMapper();om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(om);RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();template.setConnectionFactory(redisConnectionFactory);template.setKeySerializer(jackson2JsonRedisSerializer);template.setValueSerializer(jackson2JsonRedisSerializer);template.setHashKeySerializer(jackson2JsonRedisSerializer);template.setHashValueSerializer(jackson2JsonRedisSerializer);template.afterPropertiesSet();return template;}@Bean@ConditionalOnMissingBean(StringRedisTemplate.class)public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)throws UnknownHostException {StringRedisTemplate template = new StringRedisTemplate();template.setConnectionFactory(redisConnectionFactory);return template;}
}

Redis 的配置就完成了。

4.2 Redis 的数据结构类型

Redis 可以存储键与5种不同数据结构类型之间的映射,这5种数据结构类型分别为String(字符串)、List(列表)、Set(集合)、Hash(散列)和 Zset(有序集合)。

下面来对这5种数据结构类型作简单的介绍:

结构类型结构存储的值结构的读写能力
String可以是字符串、整数或者浮点数对整个字符串或者字符串的其中一部分执行操作;对象和浮点数执行自增(increment)或者自减(decrement)
List一个链表,链表上的每个节点都包含了一个字符串从链表的两端推入或者弹出元素;根据偏移量对链表进行修剪(trim);读取单个或者多个元素;根据值来查找或者移除元素
Set包含字符串的无序收集器(unorderedcollection),并且被包含的每个字符串都是独一无二的、各不相同添加、获取、移除单个元素;检查一个元素是否存在于某个集合中;计算交集、并集、差集;从集合里卖弄随机获取元素
Hash包含键值对的无序散列表添加、获取、移除单个键值对;获取所有键值对
Zset字符串成员(member)与浮点数分值(score)之间的有序映射,元素的排列顺序由分值的大小决定添加、获取、删除单个元素;根据分值范围(range)或者成员来获取元素

4.3 StringRedisTemplate 与 RedisTemplate

RedisTemplate 对五种数据结构分别定义了操作

  • redisTemplate.opsForValue();

    操作字符串

  • redisTemplate.opsForHash();

    操作hash

  • redisTemplate.opsForList();

    操作list

  • redisTemplate.opsForSet();

    操作set

  • redisTemplate.opsForZSet();

    操作有序set

如果操作字符串的话,建议用 StringRedisTemplate

StringRedisTemplate 与 RedisTemplate 的区别

  1. StringRedisTemplate 继承了 RedisTemplate。
  2. RedisTemplate 是一个泛型类,而 StringRedisTemplate 则不是。
  3. StringRedisTemplate 只能对 key=String,value=String 的键值对进行操作,RedisTemplate 可以对任何类型的 key-value 键值对操作。
  4. 他们各自序列化的方式不同,但最终都是得到了一个字节数组,殊途同归,StringRedisTemplate 使用的是 StringRedisSerializer 类;RedisTemplate 使用的是 JdkSerializationRedisSerializer 类。反序列化,则是一个得到 String,一个得到 Object
  5. 两者的数据是不共通的,StringRedisTemplate 只能管理 StringRedisTemplate 里面的数据,RedisTemplate 只能管理 RedisTemplate中 的数据。

4.4 项目中使用

在需要使用 Redis 的地方,用 @Autowired 注入进来

@Autowired
RedisTemplate redisTemplate;@Autowired
StringRedisTemplate stringRedisTemplate;

由于项目中暂时仅用到了 StringRedisTemplate 与 RedisTemplate 的 Hash 结构,StringRedisTemplate 比较简单就不贴代码了,下面仅对操作 Hash 进行举例。

关于 RedisTemplate 的详细用法,有一篇文章已经讲的很细很好了,我觉得没必要再去写了。传送门

用 RedisTemplate 操作 Hash

package com.solo.coderiver.user.service.impl;import com.solo.coderiver.user.dataobject.UserLike;
import com.solo.coderiver.user.dto.LikedCountDTO;
import com.solo.coderiver.user.enums.LikedStatusEnum;
import com.solo.coderiver.user.service.LikedService;
import com.solo.coderiver.user.service.RedisService;
import com.solo.coderiver.user.utils.RedisKeyUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;import java.util.ArrayList;
import java.util.List;
import java.util.Map;@Service
@Slf4j
public class RedisServiceImpl implements RedisService {@AutowiredRedisTemplate redisTemplate;@AutowiredLikedService likedService;@Overridepublic void saveLiked2Redis(String likedUserId, String likedPostId) {String key = RedisKeyUtils.getLikedKey(likedUserId, likedPostId);redisTemplate.opsForHash().put(RedisKeyUtils.MAP_KEY_USER_LIKED, key, LikedStatusEnum.LIKE.getCode());}@Overridepublic void unlikeFromRedis(String likedUserId, String likedPostId) {String key = RedisKeyUtils.getLikedKey(likedUserId, likedPostId);redisTemplate.opsForHash().put(RedisKeyUtils.MAP_KEY_USER_LIKED, key, LikedStatusEnum.UNLIKE.getCode());}@Overridepublic void deleteLikedFromRedis(String likedUserId, String likedPostId) {String key = RedisKeyUtils.getLikedKey(likedUserId, likedPostId);redisTemplate.opsForHash().delete(RedisKeyUtils.MAP_KEY_USER_LIKED, key);}@Overridepublic void incrementLikedCount(String likedUserId) {redisTemplate.opsForHash().increment(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, likedUserId, 1);}@Overridepublic void decrementLikedCount(String likedUserId) {redisTemplate.opsForHash().increment(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, likedUserId, -1);}@Overridepublic List<UserLike> getLikedDataFromRedis() {Cursor<Map.Entry<Object, Object>> cursor = redisTemplate.opsForHash().scan(RedisKeyUtils.MAP_KEY_USER_LIKED, ScanOptions.NONE);List<UserLike> list = new ArrayList<>();while (cursor.hasNext()) {Map.Entry<Object, Object> entry = cursor.next();String key = (String) entry.getKey();//分离出 likedUserId,likedPostIdString[] split = key.split("::");String likedUserId = split[0];String likedPostId = split[1];Integer value = (Integer) entry.getValue();//组装成 UserLike 对象UserLike userLike = new UserLike(likedUserId, likedPostId, value);list.add(userLike);//存到 list 后从 Redis 中删除redisTemplate.opsForHash().delete(RedisKeyUtils.MAP_KEY_USER_LIKED, key);}return list;}@Overridepublic List<LikedCountDTO> getLikedCountFromRedis() {Cursor<Map.Entry<Object, Object>> cursor = redisTemplate.opsForHash().scan(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, ScanOptions.NONE);List<LikedCountDTO> list = new ArrayList<>();while (cursor.hasNext()) {Map.Entry<Object, Object> map = cursor.next();//将点赞数量存储在 LikedCountDTString key = (String) map.getKey();LikedCountDTO dto = new LikedCountDTO(key, (Integer) map.getValue());list.add(dto);//从Redis中删除这条记录redisTemplate.opsForHash().delete(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, key);}return list;}
}

五、Redis 实现分布式锁

讲完了基础操作,再说个实战运用,用Redis 实现分布式锁 。

实现分布式锁之前先看两个 Redis 命令:

  • SETNX

    key设置值为value,如果key不存在,这种情况下等同SET命令。 当key存在时,什么也不做。SETNX是”SET if Not eXists”的简写。

    返回值

    Integer reply, 特定值:

    • 1 如果key被设置了
    • 0 如果key没有被设置

例子

redis> SETNX mykey "Hello"
(integer) 1
redis> SETNX mykey "World"
(integer) 0
redis> GET mykey
"Hello"
redis> 
  • GETSET

    自动将key对应到value并且返回原来key对应的value。如果key存在但是对应的value不是字符串,就返回错误。

    设计模式

    GETSET可以和INCR一起使用实现支持重置的计数功能。举个例子:每当有事件发生的时候,一段程序都会调用INCR给key mycounter加1,但是有时我们需要获取计数器的值,并且自动将其重置为0。这可以通过GETSET mycounter “0”来实现:

    INCR mycounter
    GETSET mycounter "0"
    GET mycounter

    返回值

    bulk-string-reply: 返回之前的旧值,如果之前Key不存在将返回nil

    例子

    redis> INCR mycounter
    (integer) 1
    redis> GETSET mycounter "0"
    "1"
    redis> GET mycounter
    "0"
    redis>

这两个命令在 java 中对应为 setIfAbsentgetAndSet

分布式锁的实现:

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;@Component
@Slf4j
public class RedisLock {@AutowiredStringRedisTemplate redisTemplate;/*** 加锁* @param key* @param value 当前时间 + 超时时间* @return*/public boolean lock(String key, String value){if (redisTemplate.opsForValue().setIfAbsent(key, value)){return true;}//解决死锁,且当多个线程同时来时,只会让一个线程拿到锁String currentValue = redisTemplate.opsForValue().get(key);//如果过期if (!StringUtils.isEmpty(currentValue) &&Long.parseLong(currentValue) < System.currentTimeMillis()){//获取上一个锁的时间String oldValue = redisTemplate.opsForValue().getAndSet(key, value);if (StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)){return true;}}return false;}/*** 解锁* @param key* @param value*/public void unlock(String key, String value){try {String currentValue = redisTemplate.opsForValue().get(key);if (!StringUtils.isEmpty(currentValue) && currentValue.equals(value)){redisTemplate.opsForValue().getOperations().delete(key);}}catch (Exception e){log.error("【redis锁】解锁失败, {}", e);}}
}

使用:


/*** 模拟秒杀*/
public class SecKillService {@AutowiredRedisLock redisLock;//超时时间10sprivate static final int TIMEOUT = 10 * 1000;public void secKill(String productId){long time = System.currentTimeMillis() + TIMEOUT;//加锁if (!redisLock.lock(productId, String.valueOf(time))){throw new SellException(101, "人太多了,等会儿再试吧~");}//具体的秒杀逻辑//解锁redisLock.unlock(productId, String.valueOf(time));}
}

更多 Redis 的具体使用场景请关注开源项目 CodeRiver,致力于打造全平台型全栈精品开源项目。

coderiver 中文名 河码,是一个为程序员和设计师提供项目协作的平台。无论你是前端、后端、移动端开发人员,或是设计师、产品经理,都可以在平台上发布项目,与志同道合的小伙伴一起协作完成项目。

coderiver河码 类似程序员客栈,但主要目的是方便各细分领域人才之间技术交流,共同成长,多人协作完成项目。暂不涉及金钱交易。

计划做成包含 pc端(Vue、React)、移动H5(Vue、React)、ReactNative混合开发、Android原生、微信小程序、java后端的全平台型全栈项目,欢迎关注。

项目地址:https://github.com/cachecats/...


您的鼓励是我前行最大的动力,欢迎点赞,欢迎送小星星✨ ~

http://www.lbrq.cn/news/1304281.html

相关文章:

  • 哪个网站可以做代销/百度推广登录网址
  • 做网站租空间/seo关键词排名优化的方法
  • 单页的网站怎么做的/百度投流运营
  • 网站广告推广哪家好/百度seo推广方案
  • 湖南智能网站建设费用/哪里有永久免费建站
  • 广州疫情防控最新消息/站长工具seo查询
  • wordpress 导航站点/短信广告投放软件
  • 做网站选关键词/十大营销策略
  • 东莞网站推广策划活动/seo是做什么工作的
  • 品牌设计案例网站/手机百度高级搜索入口
  • 如何的找网站建设公司/渠道网络
  • 网站是不是用cms做的/广西seo搜索引擎优化
  • 如何做网站meta设置/百度卖货平台
  • 织梦网站怎么重新安装教程/2022最近的新闻大事10条
  • 东莞网站优化软件/360收录查询
  • 做视频网站要多大的主机/百度网盘怎么用
  • 高端网站建设文案/有没有免费的写文案的软件
  • 群晖wordpress图片/北京seo业务员
  • 北京婚纱摄影网站/软文街官网
  • 乐山做网站的公司/湖南省人民政府官网
  • 推送者seo/网站关键词推广优化
  • 个人备案的网站涉及到资金/2345浏览器导航页
  • 西安网站建设云阔/国外免费推广平台有哪些
  • 搜索引擎广告形式有/海口关键词优化报价
  • 直接做那个视频网站/网站内容优化方法
  • 网站的模糊搜索怎么做/今日足球赛事推荐
  • vs2012 做网站教程/百度一下网址是多少
  • 企业网站制作免费下载/头条新闻 最新消息条
  • 毕设做购物网站/南宁seo怎么做优化团队
  • 男女做男个真实视频网站/网络营销方案设计
  • 【C语言进阶】题目练习(2)
  • AE MDX L6 L12 L18 电源手侧操作使用说明
  • CentOS7下的ElasticSearch部署
  • 消息队列 2.RabbitMQ
  • Linux C 进程基本操作
  • 医院各类不良事件上报,PHP+vscode+vue2+element+laravel8+mysql5.7不良事件管理系统源代码,成品源码,不良事件管理系统