如何设计用户评论表

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

上一篇提到树形结构是非常经典的一种表设计模式,看似平平无奇,实则包罗万象。今天,我们借助“用户评论”的需求,再来领略一把树形结构的魅力。

二级评论与盖楼

下面是两张评论相关的截图,请大家观察一下结构上有什么不同:

第一张图的评论形式俗称“二级评论”,第二张图俗称“盖楼”。

“二级评论”和“盖楼”最大的不同是 :

二级评论只需要关注当前评论的上一级,而“盖楼”则需要把当前评论之前的所有评论按顺序展示出来。

没有太多表设计经验的同学可能已经晕了:我上面两张图啥区别都还没整明白呢,被你这么一说,更晕了。

我们逐个分析。

先看“盖楼”。我们把“我是煎饼侠”的上一条评论也显示出来:

“我是煎饼侠”的评论其实是对“bravo1988”评论的评论,但它并没有直接回复“bravo1988”,而是另起一层并把前面的评论引用过来,然后在最下面显示自己的评论内容。

再看“二级评论”:

针对“谢函”的“想咨询一个...”的评论,“程大治”是直接在该评论下显示自己的评论内容,并不会另起一层(用户Shayne_xxy那种才叫另起一层)。整个评论区的所有评论其实就两大类:一级评论、二级评论。

关于“一级评论”、“二级评论”的定义:

  • 一级评论:针对内容(文章、图片、视频)本身的评论
  • 二级评论:针对一级评论的回复,也就是“对评论的评论”

上图中,“想咨询一个...”和“这个好”属于一级评论,“程大治”的两条评论属于二级评论(大家移上去再看一遍)。一级评论下无论再怎么复杂(A评论B,B评论A,C评论B...),都是二级评论,而不是三级评论、四级评论。

如果把上图改为“盖楼”,就是这样:

“盖楼”和“二级评论”两种评论形式看起来好像大相径庭,其实数据库表设计是差不多的,区别在SQL查询以及前端展示,其中“盖楼”的难度要大一些。

大家也看到了,“盖楼”这种形式的评论不如“二级评论”来得直观,且实现较为复杂(每一条评论都要找到在它前面的所有评论,d评论找c评论,c评论找b评论,最终找到a评论),所以现在已经很少采用 “盖楼”的评论形式了。

本文主要讨论如何设计“二级评论表”。

分析需求,确定表字段

请大家停下来重新观察并思考:如果让你来做这个需求,你会如何设计表结构,后端大概需要返回哪些字段呢?

需要几张表?

我们最直观的感受是:既然评论总共有2级,那么我们设计两张表吧,用逻辑外键关联一级评论表和二级评论表。

但实际上,这个问题可以用一张表“自关联”解决,只需要在表中设计一个pid,让secondLevel.pid = firstLevel.id即可。

所以,结论是二级评论可以用一张表解决。

一级评论和二级评论怎么摆放?

这个问题,其实是从前端展示的角度提出来的。我们知道,数据库存放的评论都是一条条独立的:

怎么最终在前端展示成这样呢:

还是树形结构,“二级评论”的树只有两级:

归根到底,用户评论这个需求还是对树形结构的实际应用。二级评论可以通过pid找到自己所属的一级评论,页面展示时,先遍历一级评论,直接展示在文章下方,再遍历一级评论的子评论(replies),把二级评论展示在一级评论下方即可。至于某个一级评论下的排序,可以默认id排序(一般等于时间排序)。如果有其他需要,可自定排序规则。

张三@李四

现在只剩最后一个问题了,怎么处理张三@李四这种展示效果?比如下图:

无论一级评论还是二级评论,除了展示评论本身,还要展示评论相关的用户信息、评论时间等。

而二级评论还多了一个属性:这条评论是谁对谁的回复。通常会用 “A 回复 B:xxx”或者“张三:@李四 xxx”这两种形式。

换句话说,后端接口需要返回:评论、用户(评论的作者)、评论时间、对谁的回复(被评论人)。

你可能会想,一级评论和二级评论的字段好像不同啊,二级评论还多了个“谁对谁的回复”,果然,还是要拆开两张表。

其实不用,设计评论表时统一设计content、user_id、to_user_id、create_time字段即可,一级评论如果to_user_id用不上,可以空着(也可以认为一级评论是对文章作者的回复,或者干脆to_user_id字段设置为0或null)。

总之,同一张表在兼容多种类型时,应该以多的一方考虑。在表设计时,可以“多退”(用不上就空着呗),但不能“少补”(没有就真的没有了,除非修改表结构)。

这里给出较为可行的表设计:

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for t_comment
-- ----------------------------
DROP TABLE IF EXISTS `t_comment`;
CREATE TABLE `t_comment` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '评论id',
  `pid` int(11) DEFAULT NULL COMMENT '所属一级评论的id,如果当前评论为一级,则为0',
  `target_id` int(11) NOT NULL COMMENT '评论所属文章id',
  `content` varchar(255) COLLATE utf8mb4_bin NOT NULL COMMENT '评论内容',
  `user_id` varchar(255) COLLATE utf8mb4_bin NOT NULL COMMENT '该条评论的作者',
  `to_user_id` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '对谁回复,一级评论可以为null',
  `likes_count` int(11) DEFAULT '0' COMMENT '当前评论的点赞数',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `key_target_id` (`target_id`) USING BTREE,
  KEY `key_pid` (`pid`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

-- ----------------------------
-- Records of t_comment
-- ----------------------------
BEGIN;
INSERT INTO `t_comment` VALUES (1, 0, 10086, '这是第一条评论。', 'zhangsan', NULL, 1, '2020-03-17 11:06:39', '2020-03-17 14:06:13');
INSERT INTO `t_comment` VALUES (2, 0, 10086, '这是第二条评论。', 'lisi', NULL, 0, '2020-03-17 11:08:10', '2020-03-17 14:06:16');
INSERT INTO `t_comment` VALUES (3, 2, 10086, '你好啊,第二条评论。', 'zhangsan', 'lisi', 2, '2020-03-17 11:08:56', '2020-03-17 11:43:37');
INSERT INTO `t_comment` VALUES (4, 2, 10086, '哇,谢谢你的回复!', 'lisi', 'zhangsan', 0, '2020-03-17 11:09:57', '2020-03-17 12:02:40');
INSERT INTO `t_comment` VALUES (5, 0, 10086, '楼上两个细佬...', 'wangwu', NULL, 0, '2020-03-17 11:10:24', '2020-03-17 14:06:20');
INSERT INTO `t_comment` VALUES (6, 2, 10086, '回复一下而已,需要这么激动吗...', 'zhaoliu', 'lisi', 1, '2020-03-17 11:11:40', '2020-03-17 12:02:48');
COMMIT;

SET FOREIGN_KEY_CHECKS = 1;

为了方便展示时理清关系,这里我把user_id设置为VARCHAR,这样user_id就可以填入zhangsan、lisi,直观一些。

代码示例(通用Mapper)

Comment

@Data
@Table(name = "t_comment")
public class Comment {
    /**
     * 评论id
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY, generator = "SELECT LAST_INSERT_ID()")
    private Integer id;

    /**
     * 所属一级评论的id,如果当前评论为一级,则为0
     */
    private Integer pid;

    /**
     * 评论所属文章id
     */
    @Column(name = "target_id")
    private Integer targetId;

    /**
     * 评论内容
     */
    private String content;

    /**
     * 该条评论的作者
     */
    @Column(name = "user_id")
    private String userId;

    /**
     * 对谁回复,一级评论可以为null
     */
    @Column(name = "to_user_id")
    private String toUserId;

    /**
     * 当前评论的点赞数
     */
    @Column(name = "likes_count")
    private Integer likesCount;

    /**
     * 创建时间
     */
    @Column(name = "create_time")
    private Date createTime;

    /**
     * 更新时间
     */
    @Column(name = "update_time")
    private Date updateTime;

    /**
     * 该评论下的回复,非数据库字段,用 @Transient
     */
    @Transient
    private List<Comment> replies = new ArrayList<>();
}

CommentMapper

public interface CommentMapper extends Mapper<Comment> {
}

CommentService

@Service
public class CommentService {

    @Autowired
    private CommentMapper commentMapper;

    public List<Comment> getAllCommentsByTargetId(Integer targetId) {
        Example example = new Example(Comment.class);
        Example.Criteria criteria = example.createCriteria();
        criteria.andEqualTo("targetId", targetId);
        example.setOrderByClause("id asc");

        List<Comment> commentList = commentMapper.selectByExample(example);

        return commentList;
    }
}

启动类

@SpringBootApplication
@MapperScan("com.bravo")// 记得加扫描
public class SpringbootDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringbootDemoApplication.class, args);
    }
}

测试类

@RunWith(SpringRunner.class)
@SpringBootTest
public class CommentTest {
    @Autowired
    private CommentService commentService;
    @Autowired
    private ObjectMapper objectMapper;

    @Test
    public void testComment() throws JsonProcessingException {
        // =========查出targetId下所有评论(一篇文章下的所有评论)==========
        List<Comment> commentList = commentService.getAllCommentsByTargetId(10086);

        // =========对平铺数据进行嵌套整理==========
        // 最终结果
        List<Comment> result = new ArrayList<>();

        // list转map,建立索引
        Map<Integer, Comment> commentMap = new HashMap<>();
        for (Comment comment : commentList) {
            commentMap.put(comment.getId(), comment);
        }
        
        // 嵌套数据
        for (Comment comment : commentList) {
            /**
             * 归纳评论:对文章的评论是第一级,对文章的评论的评论是第二级,把第二级评论塞到对应的第一级评论下,作为replies
             *
             * 《静夜思》
             * 床前明月光
             * 疑似地上霜
             * -----------------------
             * a:第一级评论1
             *   a 回复 b:第二级评论1
             *   b 回复 a:第二级评论2
             *
             * c:第一级评论2
             *   c 回复 d:第二级评论3
             *   d 回复 c:第二级评论4
             */
            if (comment.getPid() == 0) {
                // 一级评论
                result.add(comment);
            } else{
                // 二级评论,那么肯定有一级评论且firstComment一定不为null
                Comment firstComment = commentMap.get(comment.getPid());
                // 把二级评论塞到一级评论下
                firstComment.getReplies().add(comment);
            }
        }

        prettyPrint(result);
    }

    private void prettyPrint(List<Comment> commentList) throws JsonProcessingException {
        System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(commentList));
    }
}

结果展示

JSON返回值(顺序和层级结构已经处理好了,前端只要展示即可):

[

   {

       "id": 1,

       "pid": 0,

       "targetId": 10086,

       "content": "这是第一条评论。",

       "userId": "zhangsan",

       "toUserId": null,

       "likesCount": 1,

       "createTime": "2020-03-17T03:06:39.000+0000",

       "updateTime": "2020-03-17T06:06:13.000+0000",

       "replies": []

   },

   {

       "id": 2,

       "pid": 0,

       "targetId": 10086,

       "content": "这是第二条评论。",

       "userId": "lisi",

       "toUserId": null,

       "likesCount": 0,

       "createTime": "2020-03-17T03:08:10.000+0000",

       "updateTime": "2020-03-17T06:06:16.000+0000",

       "replies": [

           {

               "id": 3,

               "pid": 2,

               "targetId": 10086,

               "content": "你好啊,第二条评论。",

               "userId": "zhangsan",

               "toUserId": "lisi",

               "likesCount": 2,

               "createTime": "2020-03-17T03:08:56.000+0000",

               "updateTime": "2020-03-17T03:43:37.000+0000",

               "replies": []

           },

           {

               "id": 4,

               "pid": 2,

               "targetId": 10086,

               "content": "哇,谢谢你的回复!",

               "userId": "lisi",

               "toUserId": "zhangsan",

               "likesCount": 0,

               "createTime": "2020-03-17T03:09:57.000+0000",

               "updateTime": "2020-03-17T04:02:40.000+0000",

               "replies": []

           },

           {

               "id": 6,

               "pid": 2,

               "targetId": 10086,

               "content": "回复一下而已,需要这么激动吗...",

               "userId": "zhaoliu",

               "toUserId": "lisi",

               "likesCount": 1,

               "createTime": "2020-03-17T03:11:40.000+0000",

               "updateTime": "2020-03-17T04:02:48.000+0000",

               "replies": []

           }

       ]

   },

   {

       "id": 5,

       "pid": 0,

       "targetId": 10086,

       "content": "楼上两个细佬...",

       "userId": "wangwu",

       "toUserId": null,

       "likesCount": 0,

       "createTime": "2020-03-17T03:10:24.000+0000",

       "updateTime": "2020-03-17T06:06:20.000+0000",

       "replies": []

   }

]

扩展

之前提到了过,每条评论除了内容本身,还有用户信息:头像、昵称、简介等等:

而上面为了简单,JSON返回值中只有userId,并没有用户头像、昵称及个人简介。

解决办法也简单,一般评论肯定会做分页,所以一次查询的数量是有限的,我们可以查询出commentList后,用之前封装的ConvertUtil#resultToList收集所有评论的userId,再调用UserService.listUserInfoByIdList()查询所有用户信息,此时内存中有commentList和userList,而它们都有userId,不用我说大家也知道怎么做啦。

我们来看看掘金网站是怎么做的:

我们发现,把鼠标移到任意用户头像上时,会弹出一个tab页显示用户的信息,并且仔细观察的话,此时并没有触发异步请求,说明是后端嵌套好的。

[
    {
        "id": 2,
        "pid": 0,
        "targetId": 10086,
        "content": "想咨询一个问题,就是如何获取跟 listview 一样某一个 item 的 view?",
        "userId": "xiehan",
        "toUserId": null,
        "likesCount": 0,
        "userInfo": {
            "objectId": "57ab4807d342d30057867209",
            "username": "谢函",
            "avatarLarge": "",
            "selfDescription": "",
            "jobTitle": "",
            "company": "",
            "viewedEntriesCount": 423,
            "collectedEntriesCount": 64,
            "level": 0,
            "isFollow": false
        },
        "toUserInfo": null,
        "createTime": "2020-03-17T03:08:10.000+0000",
        "updateTime": "2020-03-17T06:06:16.000+0000",
        "replies": [
            {
                "id": 3,
                "pid": 2,
                "targetId": 10086,
                "content": "RecyclerView.getChildAt",
                "userId": "chengdazhi",
                "toUserId": "xiehan",
                "likesCount": 2,
                "userInfo": {
                    "objectId": "56a9a4941532bc005304ab60",
                    "username": "程大治",
                    "avatarLarge": "https://user-gold-cdn.xitu.io/2016/11/29/f74e01b6a8cb2ced5da81e1aceac5e40",
                    "selfDescription": "计算机视觉研究 前Android开发",
                    "jobTitle": "科研实习",
                    "company": "微软亚洲研究院",
                    "viewedEntriesCount": 497,
                    "collectedEntriesCount": 41,
                    "level": 0,
                    "isFollow": false
                },
                "toUserInfo": {
                    "objectId": "57ab4807d342d30057867209",
                    "username": "谢函",
                    "avatarLarge": "",
                    "selfDescription": "",
                    "jobTitle": "",
                    "company": "",
                    "viewedEntriesCount": 423,
                    "collectedEntriesCount": 64,
                    "level": 0,
                    "isFollow": false
                },
                "createTime": "2020-03-17T03:08:56.000+0000",
                "updateTime": "2020-03-17T03:43:37.000+0000",
                "replies": []
            }
        ]
    }
]

当鼠标触发hover事件时,直接从当前Comment中取出UserInfo展示。

当然,掘金的这种做法会使得同一个作者的多条评论中带有相同的UserInfo,前后端传递到数据有很大的冗余。

如果是你,会怎么改进呢?

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C++_动态二维数组的两种方法

介绍 本文主要介绍使用 动态二维数组的两种方法 (PS:仅作创建 动态二维数组参考,详细使用方法根据需求自行改变) 第一种&#xff1a;连续存储结构的 二维动态数组(需固定 列 大小&#xff0c;可通过下标访问) 缺点: 1.需要在设计二维数组前写死 列 的大小 2.空间利用率不高 优点…

netty源码:(29)ChannelInboundHandlerAdapter

它实现的方法都有一个ChannelHandlerContext参数&#xff0c;它的方法都是直接调用ChannelHandlerContext参数对应的方法&#xff0c;该方法会调用下一个handler对应的方法。 可以继承这个类&#xff0c;重写感兴趣的方法,比如channelRead. 这个类有个子类&#xff1a;SimpleC…

调度算法(一)-第二十一天

目录 各种调度算法的学习思路 先来先服务&#xff08;FCFS&#xff09; 短作业优先&#xff08;SJF&#xff09; 各种调度算法的学习思路 1、算法思想 2、算法规则 3、算法用于作业调度还是进程调度 5、抢占式还是非抢占式 6、优点和缺点 7、是否会导致饥饿&#xff08;进程…

【音视频 | AAC】AAC格式音频文件解析

&#x1f601;博客主页&#x1f601;&#xff1a;&#x1f680;https://blog.csdn.net/wkd_007&#x1f680; &#x1f911;博客内容&#x1f911;&#xff1a;&#x1f36d;嵌入式开发、Linux、C语言、C、数据结构、音视频&#x1f36d; &#x1f923;本文内容&#x1f923;&a…

C++刷题 -- KMP算法

C刷题 – KMP算法 文章目录 C刷题 -- KMP算法1.算法讲解2.算法实现 https://leetcode.cn/problems/find-the-index-of-the-first-occurrence-in-a-string/description/ 1.算法讲解 KMP算法是一种字符串匹配算法&#xff0c;当出现字符串不匹配时&#xff0c;可以记录一部分之…

PSP - 结构生物学中的机器学习 (NIPS MLSB Workshop 2023.12)

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/135120094 Machine Learning in Structural Biology (机器学习在结构生物学中) 网址&#xff1a;https://www.mlsb.io/ Workshop at the 37th Co…

计算机网络-进阶

目录 易混淆物理层数据链路层网络层nat如何实现私有ip通信IP数据报 格式解析tcp 连接tcp流量控制滑动窗口拥塞控制 报文捕获 wireshark路由模拟器 enspcdn代理服务器 VS cdn VS web cache 计算机有了物理地址&#xff0c;为什么还要有ip地址&#xff1f;单播 多播 广播 传输层会…

基于Java+SpringBoot+Mybaties-plus+Vue+ElementUI+Vant 电影院订票管理系统 的设计与实现

一.项目介绍 基于SpringBootVue 电影院订票管理系统 分为前端和后端。 前端&#xff08;用户&#xff09;&#xff1a; 登录后支持查看首页、电影、影院和我的信息 支持查看正在热映和即将上映的电影信息 支持购票&#xff08;需选择影院座位&#xff09;、看过&#xff08;评论…

力扣:203. 移除链表元素(Python3)

题目&#xff1a; 给你一个链表的头节点 head 和一个整数 val &#xff0c;请你删除链表中所有满足 Node.val val 的节点&#xff0c;并返回 新的头节点 。 来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 链接&#xff1a;力扣&#xff08;LeetCode&#xff09;官网 …

基于springboot+mybatis+mysql+jsp房屋租赁管理系统

基于springbootmybatismysqljsp房屋租赁管理系统 一、系统介绍二、功能展示1.项目内容2.项目骨架3.数据库3.登录4.首页5.房源管理6.个人中心7.房屋详情 四、其它1.其他系统实现五.获取源码 一、系统介绍 项目名称&#xff1a;基于Spring boot的房屋租赁管理系统 项目架构&…

对77,539个基因组进行的遗传关联分析揭示了罕见疾病的病因

今天给同学们分享一篇实验文章“Genetic association analysis of 77,539 genomes reveals rare disease etiologies”&#xff0c;这篇文章发表在Nat Med期刊上&#xff0c;影响因子为82.9。 结果解读&#xff1a; 稀有水库 关系型数据库&#xff08;RDB&#xff09;提供了一…

vue关闭当前路由页面并跳转到其父页面

1.dom中添加关闭或取消按钮 <el-button type"primary" class"blueLinearbg cancelBtn" click"cancel" >取 消</el-button>2.cancel方法中 /*取消或关闭*/cancel(){this.$store.dispatch("tagsView/delView", this.$route)…

echarts 实现x轴文字倾斜显示

显示效果 关键代码 xAxis: {axisLabel: {show: true,rotate: 35,//35度角倾斜显示},}, 完全代码 var optSaleType {title: {text: ,textStyle: {color: #000,fontSize: 14}},tooltip: {},grid: {left: 0,right: 0,bottom: 0,containLabel: true,},xAxis: {axisLabel: {show:…

苹果cms论坛多播放源自动采集 /采集在线影视网站/苹果CMS影视站采集器

源码介绍&#xff1a; 苹果cms论坛多播放源自动采集、采集在线影视网站&#xff0c;作为苹果CMS影视站采集器&#xff0c;它能轻松获取在线影视网站资源。 苹果 cms 论坛这是一个基于Vue和Gin实现的在线观影网站。项目采用 vite vue 作为前端技术栈, 使用 ElementPlus 作为 …

基于mpvue的小程序项目搭建的步骤(附精选源码32套,涵盖商城团购等)

mpvue 是美团开源的一套语法与vue.js一致的、快速开发小程序的前端框架&#xff0c;按官网说可以达到小程序与H5界面使用一套代码。使用此框架&#xff0c;开发者将得到完整的 Vue.js 开发体验&#xff0c;同时为 H5 和小程序提供了代码复用的能力。如果想将 H5 项目改造为小程…

ES排错命令

GET _cat/indices?v&healthred GET _cat/indices?v&healthyellow GET _cat/indices?v&healthgreen确定哪些索引有问题&#xff0c;多少索引有问题。_cat API 可以通过返回结果告诉我们这一点 查看有问题的分片以及原因。 这与索引列表有关&#xff0c;但是索引…

海康rtsp拉流,rtmp推流,nginx部署转flv集成

海康rtsp拉流&#xff0c;rtmp推流&#xff0c;nginx部署转flv集成 项目实际使用并测试经正式使用无问题&#xff0c;有问题欢迎评论留言 核心后台java代码&#xff1a; try {// FFmpeg命令String command "ffmpeg -re -i my_video.mp4 -c copy -f flv rtmp://localho…

opencv入门到精通——鼠标事件和Trackbar控件的使用

目标 了解如何在OpenCV中处理鼠标事件 您将学习以下功能&#xff1a;cv.setMouseCallback() 了解将轨迹栏固定到OpenCV窗口 您将学习以下功能&#xff1a;cv.getTrackbarPos&#xff0c;cv.createTrackbar等。 简单演示 在这里&#xff0c;我们创建一个简单的应用程序&am…

飞天使-k8s知识点4-验证安装好后功能

文章目录 接k8s知识点2之验证集群功能创建dashboard 接k8s知识点2之验证集群功能 [rootkubeadm-master2 tmp]# kubectl run net-test1 --imagealpine sleep 36000 pod/net-test1 created [rootkubeadm-master2 tmp]# kubectl get pod NAME READY STATUS RESTART…

小型洗衣机好用吗?目前口碑最好的四款迷你洗衣机分享

作为一个上班族&#xff0c;每天回到家中真的不愿意再动了&#xff0c;市面上也越来越多懒人福利神器&#xff0c;而内衣洗衣机可以称得上是人类最幸福的小家电&#xff0c;它不仅可以释放我们的双手&#xff0c;而且还比我们自己手洗得干净&#xff0c;功能和清洁力都比我们传…
最新文章