- 一、引言
- 二、数据库设计
- 三、动态发表模块设计
- 1、介绍
- 2、Redis结构选择
- 四、评论模块设计
- 1、介绍
- 2、对象类型转换工具
- 3、多级评论树型拼接
- 4、评论简单过滤
- 五、点赞模块设计
- 1、问题描述
- 2、Redis数据结构选择
- 1. Set结构存储
- 2. Hash结构存储
- 3、编码实现
- 1. 配置redis
- 2. Redis工具类编写
- 3. 使用Set结构存储点赞
- 4. 使用Hash结构存储点赞
- 5. Quartz定时任务持久化
社交模块作为热点数据来说,可能会频繁改动字段,因此用Mysql是肯定不现实的,一般使用Redis。这里我以发表朋友圈动态为例,社交模块包括发表动态,点赞、评论、收藏、关注以及签到统计等模块,这里我简单实现了动态发表,点赞、评论这三个模块。
关注功能模块,使用Redis集合Set,一个人两个集合数据,定时更新到数据库
https://blog.csdn.net/INGNIGHT/article/details/107066022
https://www.cnblogs.com/linjiqin/p/12828315.html
点赞、收藏模块,Set(点赞视频、点赞人评论)和Hash(like::url =1或0)结构都比较合适
https://juejin.cn/post/6904816415912493069#heading-10
https://juejin.cn/post/6895185457110319118#heading-20
https://juejin.cn/post/6844903967168675847
评论模块,可以选择list,用list和zset存储id,其他存储内容
https://juejin.cn/post/6844903709374169102
https://blog.csdn.net/qq171563857/article/details/107406409
https://symonlin.github.io/2019/07/29/redis-1/
登录统计、签到,使用Redis的Bitmap
二、数据库设计https://juejin.cn/post/6990152493099384869
数据库自行参考,可以考虑持久化到数据库。这里说一下我的设计思路:
动态分为视频动态和图片形式的动态,类似于抖音和微信朋友圈,该模块单独编写,需要信息从其他模块获取;评论为二级评论,后端包装后返回,评论可以点赞等 *** 作;点赞优先经过Redis,若没有查询数据库
create database if not exists lamp_social; use lamp_social; -- 评论表 drop table if exists social_comment; CREATE TABLE social_comment ( comment_id int(11) NOT NULL AUTO_INCREMENT COMMENT '评论表id', owner_id int(11) NOT NULL COMMENT '文章或视频id', user_id int(11) NOT NULL COMMENT '用户id', content text COMMENT '评论内容', star_num int(11) not null default 0 COMMENT '点赞数量', p_comment_id int(11) NOT NULL DEFAULT 0 COMMENT '若父评论则为0,默认一级评论;子评论对应其相应的评论父Id', state int(2) NOT NULL DEFAULT 0 COMMENT '默认0,表示未审核,1表示审核通过,2表示不通过', type int(2) NOT NULL DEFAULT 0 COMMENT '评论类型,默认为0,可以是对人、对资源、对视频等,暂时不用', create_time timestamp not null default CURRENT_TIMESTAMP comment '创建时间', update_time timestamp not null default CURRENT_TIMESTAMP comment '修改时间', deleted tinyint not null default 0 comment '数据删除位 0正常 1逻辑删除', primary key(comment_id) )AUTO_INCREMENT=1 ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评论表'; -- 个人动态表 drop table if exists social_dynamic; CREATE TABLE social_dynamic ( dynamic_id int(11) NOT NULL AUTO_INCREMENT COMMENT '动态id', dynamic_url varchar(5000) default '' COMMENT '视频地址', user_id int(11) NOT NULL COMMENT '用户id', content text COMMENT '朋友圈内容', star_num int(11) NOT NULL default 0 COMMENT '点赞数量', collection_num int(11) NOT NULL default 0 COMMENT '收藏数', state int(2) NOT NULL DEFAULT 0 COMMENT '默认0,表示未审核,1表示审核通过,2表示不通过', type int(2) NOT NULL DEFAULT 0 COMMENT '动态类型,默认为0,表示视频,1表示图片朋友圈,每个数字可以对应不同视频类型', create_time timestamp not null default CURRENT_TIMESTAMP comment '创建时间', update_time timestamp not null default CURRENT_TIMESTAMP comment '修改时间', deleted tinyint not null default 0 comment '数据删除位 0正常 1逻辑删除', primary key(dynamic_id) )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='个人动态表'; -- 朋友圈图片表 drop table if exists social_pic; CREATE TABLE social_pic ( pic_id int(11) NOT NULL AUTO_INCREMENT COMMENT '图片id', pic_url varchar(5000) default '' COMMENT '图片地址', user_id int(11) NOT NULL COMMENT '用户id', dynamic_id int(11) NOT NULL COMMENT '动态id', create_time timestamp not null default CURRENT_TIMESTAMP comment '创建时间', update_time timestamp not null default CURRENT_TIMESTAMP comment '修改时间', primary key(pic_id) )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='朋友圈图片表'; -- 用户表可以添加一个点赞数字段,可选 drop table if exists social_user; CREATE TABLE social_user ( user_id int(11) NOT NULL comment '用户id', school_id int(11) NOT NULL comment '学校id', star_num int(11) NOT NULL default 0 COMMENT '点赞数量', focus_num int(11) NOT NULL default 0 COMMENT '关注数量', fan_num int(11) NOT NULL default 0 COMMENT '粉丝数量', create_time timestamp not null default CURRENT_TIMESTAMP comment '创建时间', update_time timestamp not null default CURRENT_TIMESTAMP comment '修改时间', primary key(user_id) )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户点赞表'; -- 用户点赞表 drop table if exists social_user_like_dynamic; CREATE TABLE social_user_like_dynamic ( liked_id int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id', dynamic_id int(11) NOT NULL COMMENT '动态id', user_id int(11) NOT NULL COMMENT '用户id', state int(2) NOT NULL DEFAULT 0 COMMENT '默认0,表示点赞,1表示取消点赞', create_time timestamp not null default CURRENT_TIMESTAMP comment '创建时间', update_time timestamp not null default CURRENT_TIMESTAMP comment '修改时间', primary key(liked_id) )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户点赞表'; -- 用户收藏表 drop table if exists social_user_collect_dynamic; CREATE TABLE social_user_collect_dynamic ( collection_id int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id', dynamic_id int(11) NOT NULL COMMENT '动态id', user_id int(11) NOT NULL COMMENT '用户id', state int(2) NOT NULL DEFAULT 0 COMMENT '默认0,表示收藏,1表示取消收藏', create_time timestamp not null default CURRENT_TIMESTAMP comment '创建时间', update_time timestamp not null default CURRENT_TIMESTAMP comment '修改时间', primary key(collection_id) )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户收藏表'; -- 用户关注与粉丝表 drop table if exists social_user_focus; CREATE TABLE social_user_focus ( focus_id int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id', user_id int(11) NOT NULL COMMENT '用户id', focus_user_id int(11) NOT NULL COMMENT '关注用户id', state int(2) NOT NULL DEFAULT 0 COMMENT '默认0,表示关注,1表示取消关注', create_time timestamp not null default CURRENT_TIMESTAMP comment '创建时间', update_time timestamp not null default CURRENT_TIMESTAMP comment '修改时间', primary key(focus_id) )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户关注与粉丝表';三、动态发表模块设计 1、介绍
Feed流产品在我们手机APP中几乎无处不在,常见的Feed流比如微信朋友圈、新浪微博、今日头条等。对Feed流的定义,可以简单理解为只要大拇指不停地往下划手机屏幕,就有一条条的信息不断涌现出来。
大多数Feed流产品都包含两种Feed流,一种是基于算法推荐,另一种是基于关注(好友关系)。例如下图中的微博和知乎,顶栏的页卡都包含“关注”和“推荐”这两种。两种Feed流背后用到的技术差别会比较大(读扩散、写扩散)。
2、Redis结构选择参考:https://cloud.tencent.com/developer/article/1744756
动态发布因为考虑到先缓存到Redis,在异步保存到MySql,因此动态主键使用Redis的自增函数,通过Redis生成MySql的动态主键;
对于动态数据的存储,我使用了list存储结构,新的数据从左边压入list,考虑到feed流查询,我还设置了一个伴生list列表,用来与动态同步存储主键值,首先通过lastid查询上一次浏览的值,查询list的index,在通过存储动态的列表返回一个列表;同时使用了读写锁,是为了保证原子性;
最后异步或定时检查列表长度,若过长可以从右边舍弃,或者设置列表过期时间,插入的时候重新刷新过期时间
mysql表字段,评论父表和字表存储在同一个数据表,根据p_comment_id字段分辨,返回的时候先查询出总的list,在使用JDK8的Stream流形成树形结构返回。ORM映射使用了Fluent MyBatis ,树形结构格式转换;
2、对象类型转换工具首先创建转换工具类,这里先将对象转化为json,在通过解析json进行复制 *** 作
import com.alibaba.fastjson.JSON; import java.util.List; public class ObjectConversion { public static3、多级评论树型拼接List copy(List> list,Class clazz){ String oldOb = JSON.toJSONString(list); return JSON.parseArray(oldOb, clazz); } public static T copy(Object ob,Class clazz){ String oldOb = JSON.toJSONString(ob); return JSON.parseObject(oldOb, clazz); } }
我的VO类,主要用将数据库的评论拼装返回前端
@Data @Accessors(chain = true) public class VideoCommentVO { private Integer commentId; private Date createTime; private String content; private Integer ownerId; private Integer pCommentId; private Integer starNum; private Integer userId; private Listchild; }
树形结构拼装,用了jdk8新特性
@Service public class VideoCommentService { @Autowired CommentMapper commentMapper; //就先二级评论吧 public ListgetVideoComment(Integer videoId) { CommentQuery query = new CommentQuery() .where().ownerId().eq(videoId).end() .where().state().eq(0).end() .where().deleted().eq(0).end(); List commentEntities = commentMapper.listEntity(query); //列表拷贝 List videoCommentVOList = ObjectConversion.copy(commentEntities, VideoCommentVO.class); //列表通过pcommentid进行分组 Map > collect = videoCommentVOList.stream().collect(Collectors.groupingBy(VideoCommentVO::getPCommentId)); //分组后遍历每一个数组设置孩子 videoCommentVOList.forEach( videoComment->videoComment.setChild(collect.get(videoComment.getCommentId())) ); System.out.println(videoCommentVOList); //找出父结点并返回,排序默认从小到大 List result = videoCommentVOList.stream() .filter(s -> s.getPCommentId().equals(0)) .sorted(Comparator.comparing(VideoCommentVO::getStarNum).reversed()) .collect(Collectors.toList()); return result; } }
如果遇到下面问题,回退版本号,我当时遇到了
// fastJson1.2.78版本会概率性出现该错误,回退到1.2.76即可 Comparison method violates its general contract4、评论简单过滤
简单原理如上图所示,创建结点类,里面包含是否是敏感词结束符,以及一个HashMap,哈希里key值存储的是敏感词的一个词,value指向下一个结点(即指向下一个词),一个哈希表中可以存放多个值,比如赌博、赌黄这两个都是敏感词。
敏感词文件存在在resources文件夹下,通过类加载器获取里面的敏感词。在springboot中,被@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器执行一次。PostConstruct在构造函数之后执行,init()方法之前执行。
@Component public class SensitiveFilter { private static final Logger logger = LoggerFactory.getLogger(SensitiveFilter.class); private static final String REPLACEMENT = "***"; private final TreeNode rootNode = new TreeNode(); @PostConstruct public void init(){ // 带资源的try语句,try块退出时,会自动调用res.close()方法,关闭资源。 try ( InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt"); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(resourceAsStream)); ){ String keyword; while((keyword=bufferedReader.readLine())!=null){ this.addKeyWord(keyword); } } catch(IOException e){ logger.error("资源文件加载失败 ==> {}",e.getMessage()); } } private void addKeyWord(@NotNull String keyword){ TreeNode tempNode = rootNode; for(int i = 0 ;i五、点赞模块设计 1、问题描述0x9FFF); } private static class TreeNode{ // 关键词结束标识,默认是不是非结束结点 private boolean isKeywordEnd = false; // map的key存储一个敏感词,value指向下一个敏感词结点 HashMap nodeMap = new HashMap<>(); // 返回是否本次词语结束 public boolean isKeyWordEnd(){ return isKeywordEnd; } // 设置是否是结束词 public void setKeywordEnd(boolean keywordEnd){ isKeywordEnd = keywordEnd; } // 添加敏感词,key表示字符 public void addKeywordNode(Character c, TreeNode treeNode){ nodeMap.put(c,treeNode); } // 获取当前词是否是敏感词,若没有在表中,则返回null public TreeNode getKeywordNode(Character c){ return nodeMap.get(c); } } }
考虑到点赞是字段频繁变动的,用Mysql肯定不合适,使用需要使用Redis内存数据库。这里以动态点赞为例子,点赞模块需要解决的几个问题
- 用户对某个动态点赞/取消点赞
- 该动态获得了多少赞
- 用户是否已经点赞该动态
- 用户的总点赞数是多少
- 数据的持久化
对于点赞来说,Set和Hash结构都可以选择。set中的值不能重复,是无序不重复的,Hash相当于Map集合,相当于key-Map,通常来存储经常变动的对象。对于点赞,两种结构都可以,根据业务自由选择
1. Set结构存储这里我选择了一种较为简单的存储方案,不过这种方案很难进行MySql持久化,用Set结构存储某视频点赞的用户,用String结构存储用户点赞数量,查询用户是否点赞只需查询用户是否在这个Set集合里,点赞/取消点赞加入/移除Set,查询某视频点赞数只需统计Set集合中的用户数量,两个存储结构为
- 视频点赞Set的存储结构 like:dynamic:{dynamicType}:{dynamicId}={userId}
- 用户点赞数量的String存储结构 like:user:{userId}=value
这种Hash结构可以记录点赞人和被点赞产品,还有点赞状态(点赞/取消点赞设置值为1/0),还可以固定时间间隔取出 Redis 中所有点赞数据
- 视频点赞Hash的Key结构like:dynamic:{dynamicType},里面的键值对为{userId}::{dynamicId}=1
- 视频点赞数Hash的Key结构like:count:dynamic,里面的键值对为{dynamicId}={count}
- 用户点咱叔Hash的Key结构like:count:user,里面的键值对为{userId}={count}
首先进行redis配置,实现序列化,否则不能正常显示
@Configuration public class RedisConfig { // 编写自己的RedisTemplate @Bean @SuppressWarnings("all") public RedisTemplate2. Redis工具类编写redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory); //序列化配置 FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class); ObjectMapper om = new ObjectMapper(); //String的序列化 StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); // key采用String的序列化方式 template.setKeySerializer(stringRedisSerializer); // hash采用String序列方式 template.setHashKeySerializer(stringRedisSerializer); // value采用jackson template.setValueSerializer(fastJsonRedisSerializer); // hash的value采用jackson template.setHashValueSerializer(fastJsonRedisSerializer); template.afterPropertiesSet(); return template; } }
新建RedisKeyUtil,进行key的拼接
package com.zstu.social.redisutils; public class RedisKeyUtil { private static final String SPLIT = ":"; private static final String PREFIX_DYNAMIC_LIKE = "like:dynamic"; private static final String PREFIX_DYNAMIC_LIKE_COUNT = "like:count:dynamic"; private static final String PREFIX_USER_LIKE = "like:count:user"; public static String getDynamicLikeKey(int dynamicType,int dynamicId){ return PREFIX_DYNAMIC_LIKE + SPLIT + dynamicType + SPLIT + dynamicId; } public static String getDynamicLikeHashKey(int dynamicType){ return PREFIX_DYNAMIC_LIKE + SPLIT + dynamicType; } public static String getDynamicLikeCountHashKey(){ return PREFIX_DYNAMIC_LIKE_COUNT ; } public static String getDynamicUserLikeHashKey(int userId, int dynamicId){ return userId + SPLIT + SPLIT + dynamicId ; } public static String getUserLikeKey(int userId){ return PREFIX_USER_LIKE + SPLIT + userId; } public static String getUserLikeHashKey(){ return PREFIX_USER_LIKE; } }3. 使用Set结构存储点赞
redis点赞模块service代码,自己写的,没有持久化,仅供参考
@Autowired private RedisTemplate redisTemplate; public Boolean getDynamicIsLikeByUser(int dynamicType, int userId,int dynamicId){ String dynamicLikeKey = RedisKeyUtil.getDynamicLikeKey(dynamicType,dynamicId); Boolean member = redisTemplate.opsForSet().isMember(dynamicLikeKey, userId); return member; } public boolean putDynamicLikedByRedis(int dynamicType, int userId,int dynamicId){ redisTemplate.execute(new SessionCallback() { @Override public Object execute(RedisOperations operations) throws DataAccessException { String dynamicLikeKey = RedisKeyUtil.getDynamicLikeKey(dynamicType,dynamicId); String userLikeKey = RedisKeyUtil.getUserLikeKey(userId); Boolean member = redisTemplate.opsForSet().isMember(dynamicLikeKey, userId); //开启redis事务 redisTemplate.multi(); //如果已经点过赞了,就去除 if(Boolean.TRUE.equals(member)){ redisTemplate.opsForSet().remove(dynamicLikeKey,userId); redisTemplate.opsForValue().decrement(userLikeKey); }else{ //如果没有点赞,就点赞 redisTemplate.opsForSet().add(dynamicLikeKey,userId); redisTemplate.opsForValue().increment(userLikeKey); } // 返回每条成功执行的记录 redisTemplate.exec(); return true; } } ); return true; } public Long getDynamicLikeCount(int dynamicType,int dynamicId){ String dynamicLikeKey = RedisKeyUtil.getDynamicLikeKey(dynamicType,dynamicId); Long size = redisTemplate.opsForSet().size(dynamicLikeKey); return size; } public Integer getUserLikeCount(int userId){ String userLikeKey = RedisKeyUtil.getUserLikeKey(userId); Integer result = (Integer) redisTemplate.opsForValue().get(userLikeKey); return result==null ? 0 : result; }4. 使用Hash结构存储点赞
@Autowired private RedisTemplate redisTemplate; public Boolean getDynamicIsLikeByUser1(int dynamicType, int userId,int dynamicId){ try { String dynamicLikeKey = RedisKeyUtil.getDynamicLikeHashKey(dynamicType); String dynamicLikeHashkey = RedisKeyUtil.getDynamicUserLikeHashKey(userId,dynamicId); // redis Set的值 String dynamicLikeSetKey = RedisKeyUtil.getDynamicLikeKey(dynamicType,dynamicId); // 首先查询缓存 Object redisResult = redisTemplate.opsForHash().get(dynamicLikeKey, dynamicLikeHashkey); // 查询缓存 Boolean member = redisTemplate.opsForSet().isMember(dynamicLikeSetKey, userId); // 缓存没有查询数据库 if(redisResult == null && member == false){ log.info("redis查询失败"); throw new Exception("redis查询失败"); } }catch (Exception e){ UserLikeDynamicQuery userLikeDynamicQuery = new UserLikeDynamicQuery() .where().dynamicId().eq(dynamicId).end() .where().userId().eq(userId).end() .where().state().eq(0).end(); UserLikeDynamicEntity one = userLikeDynamicMapper.findOne(userLikeDynamicQuery); if(null == one){ // 两边都没有,没有点赞 return false; } } return true; } @SuppressWarnings("all") public int putDynamicLikedByRedis1(int dynamicType, int userId, int dynamicId){ String dynamicLikeKey = RedisKeyUtil.getDynamicLikeHashKey(dynamicType); String dynamicLikeHashkey = RedisKeyUtil.getDynamicUserLikeHashKey(userId,dynamicId); // 首先查询缓存 Object redisResult = redisTemplate.opsForHash().get(dynamicLikeKey, dynamicLikeHashkey); if(null != redisResult && redisResult.equals(1)){ return -1; } Long result = (Long) redisTemplate.execute(new SessionCallback() { @Override public Object execute(RedisOperations operations) throws DataAccessException { String dynamicLikeKey = RedisKeyUtil.getDynamicLikeHashKey(dynamicType); String dynamicLikeHashkey = RedisKeyUtil.getDynamicUserLikeHashKey(userId, dynamicId); String userLikeHashKey = RedisKeyUtil.getUserLikeHashKey(); // 点赞数记数 String dynamicLikeCountHashKey = RedisKeyUtil.getDynamicLikeCountHashKey(); redisTemplate.multi(); redisTemplate.opsForHash().put(dynamicLikeKey, dynamicLikeHashkey, 1); redisTemplate.opsForHash().increment(userLikeHashKey,String.valueOf(userId),1); // 自增 Long increment = redisTemplate.opsForHash().increment(dynamicLikeCountHashKey, String.valueOf(dynamicId), 1); List exec = redisTemplate.exec(); return exec.get(exec.size()-1); } }); return result.intValue(); }
@SuppressWarnings("all") public int putDynamicDislikedByRedis1(int dynamicType, int userId,int dynamicId){ String dynamicLikeKey = RedisKeyUtil.getDynamicLikeHashKey(dynamicType); String dynamicLikeHashkey = RedisKeyUtil.getDynamicUserLikeHashKey(userId,dynamicId); // 首先查询缓存 Object redisResult = redisTemplate.opsForHash().get(dynamicLikeKey, dynamicLikeHashkey); if(null == redisResult || redisResult.equals(0)){ return -1; } Long result = (Long) redisTemplate.execute(new SessionCallback() { @Override public Object execute(RedisOperations operations) throws DataAccessException { String dynamicLikeKey = RedisKeyUtil.getDynamicLikeHashKey(dynamicType); String dynamicLikeHashkey = RedisKeyUtil.getDynamicUserLikeHashKey(userId, dynamicId); String userLikeHashKey = RedisKeyUtil.getUserLikeHashKey(); // 点赞数记数 String dynamicLikeCountHashKey = RedisKeyUtil.getDynamicLikeCountHashKey(); redisTemplate.multi(); redisTemplate.opsForHash().put(dynamicLikeKey, dynamicLikeHashkey, 0); redisTemplate.opsForHash().increment(userLikeHashKey,String.valueOf(userId),-1); // 自增 Long increment = redisTemplate.opsForHash().increment(dynamicLikeCountHashKey, String.valueOf(dynamicId), -1); List exec = redisTemplate.exec(); return exec.get(exec.size()-1); } }); return result.intValue(); } public Integer getDynamicLikeCount1(int dynamicId) throws Exception { Integer result; try { // 点赞数记数 String dynamicLikeCountHashKey = RedisKeyUtil.getDynamicLikeCountHashKey(); // Hash这里 *** 作都需要String Object o = redisTemplate.opsForHash().get(dynamicLikeCountHashKey, String.valueOf(dynamicId)); // 如果缓存挂了,查询数据库 if(null == o){ log.info("redis查询失败"); throw new Exception("redis查询为空"); } result = (Integer) o; }catch (Exception e){ DynamicEntity byId = dynamicMapper.findById(dynamicId); if(null == byId){ throw new Exception("没有该id动态"); } Integer starNum = byId.getStarNum(); result=starNum; } return result; }5. Quartz定时任务持久化
对于Hash结构存储的,还可以根据::分离出点赞人和被赞动态,拆分后进行持久化,下面举例
// 返回需要插入数据库的列表 public ListgetDBList(int dynamicType) { List userLikeDynamicEntityList = new ArrayList<>(); try { Cursor > cursor = redisTemplate.opsForHash() .scan(RedisKeyUtil.getDynamicLikeHashKey(dynamicType), ScanOptions.NONE); while(cursor.hasNext()){ Map.Entry
以上是我暂时做的,可能有很多问题,如果有问题,希望能够指出
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)