日志系统项目(2)项目实现(实用工具类、日志等级类、日志消息类、日志格式化输出类)

news2025/2/26 18:59:48

前面的文章中我们讲述了日志系统项目的前置知识点,再本文中我们将开始日志项目的细节实现。

日志系统框架设计

本项目实现的是一个多日志器日志系统,主要实现的功能是让程序员能够轻松的将程序运行日志信息落地到指定的位置,且支持同步与异步两种方式的日志落地方式。
项目的框架设计将项目分为以下几个模块来实现。

模块划分

  • 日志等级模块:对输出日志的等级进行划分,以便于控制日志的输出,并提供等级枚举转字符串功能。

    • OFF:关闭
    • DEBUG:调试,调试时的关键信息输出。
    • INFO:提示,普通的提示型日志信息。
    • WARN:警告,不影响运行,但是需要注意一下的日志。
    • ERROR:错误,程序运行出现错误的日志。
    • FATAL:致命,一般是代码异常导致程序无法继续推进运行的日志。
  • 日志消息模块:中间存储日志输出所需的各项要素信息

    • 时间:描述本条日志的输出时间。
    • 线程ID:描述本条日志是哪个线程输出的。
    • 日志等级:描述本条日志的等级。
    • 日志数据:本条日志的有效载荷数据。
    • 日志文件名:描述本条日志在哪个源码文件中输出的。
    • 日志行号:描述本条日志在源码文件的哪一行输出的。
  • 日志消息格式化模块:设置日志输出格式,并提供对日志消息进行格式化功能。

    • 系统的默认日志输出格式:%d{%H:%M:%S}%T[9%t]%T[%p]%T[%c]%T%f:%1%T%m%no
      ->13:26:32 [2343223321] [FATAL] [root] main.c:76 套接字创建失败\n
    • %d{%H:%M:%S}:表示日期时间,花括号中的内容表示日期时间的格式。
    • %T:表示制表符缩进。
    • %t:表示线程ID。
    • %p:表示日志级别。
    • %c:表示日志器名称,不同的开发组可以创建自己的日志器进行日志输出,小组之间互不影响。
    • %f:表示日志输出时的源代码文件名。
    • %l:表示日志输出时的源代码行号。
    • %m:表示给与的日志有效载荷数据。
    • %n:表示换行。
    • 设计思想:设计不同的子类,不同的子类从日志消息中取出不同的数据进行处理。
  • 日志消息落地模块∶决定了日志的落地方向,可以是标准输出,也可以是日志文件,也可以滚动文件输出…

    • 标准输出:表示将日志进行标准输出的打印。
    • 日志文件输出:表示将日志写入指定的文件末尾。
    • 滚动文件输出:当前以文件大小进行控制,当一个日志文件大小达到指定大小,则切换下一个文件进行输出
    • 后期,也可以扩展远程日志输出,创建客户端,将日志消息发送给远程的日志分析服务器。
    • 设计思想:设计不同的子类,不同的子类控制不同的日志落地方向。
  • 日志器模块:

    • 此模块是对以上几个模块的整合模块,用户通过日志器进行日志的输出,有效降低用户的使用难度。
    • 包含有:日志消息落地模块对象,日志消息格式化模块对象,日志输出等级
  • 日志器管理模块:

    • 为了降低项目开发的日志耦合,不同的项目组可以有自己的日志器来控制输出格式以及落地方向,因此本项目是一个多日志器的日志系统。
    • 管理模块就是对创建的所有日志器进行统一管理。并提供一个默认日志器提供标准输出的日志输出。
  • 异步线程模块:

    • 实现对日志的异步输出功能,用户只需要将输出日志任务放入任务池,异步线程负责日志的落地输出功能,以此提供更加高效的非阻塞日志输出。

模块关系图

在这里插入图片描述

代码设计

实用类设计

完成一些零碎的功能接口,以便于后面会用到。

  • 获取系统时间信息
  • 判断文件是否存在
  • 获取文件所在路径
  • 创建目录
/*实用工具类的实现:
    1. 获取系统时间
    2. 判断文件是否存在
    3. 获取文件所在目录
    4. 创建目录
*/
namespace zyqlog
{
    namespace util
    {
        class Date 
        {
        public:
            static size_t now() // 获取当前的系统时间
            {
                return (size_t)time(nullptr);
            }
        };

        class File
        {
        public:
            static bool exists(const std::string &pathname) // 判断当前的文件是否存在
            {
                // return (access(pathname.c_str(), F_OK) == 0); // Linux下的接口

                struct stat st;
                if (stat(pathname.c_str(), &st) < 0)
                {
                    return false;
                }
                return true;
            }
            static std::string path(const std::string &pathname) // 获取当前的文件路径
            {
                // ./abc/a.txt
                size_t pos = pathname.find_last_of("/\\"); // 从文件路径最后的一个/或者\\开始获取文件路径
                if (pos == std::string::npos) return ".";
                return pathname.substr(0, pos + 1);
            }
            static void createDirectory(const std::string &pathname)
            {
                // ./abc/bcd/a.txt
                size_t pos = 0, idx = 0;
                while (idx < pathname.size())
                {
                    pos = pathname.find_first_of("/\\", idx); // 从文件路径开始处的/或者\\开始获取文件路径
                    if (pos == std::string::npos) // 获取到的结果如果已到文件末尾则说明传入的整个路径名都是需要创建的目录,直接进行创建
                    {
                        mkdir(pathname.c_str(), 0777);
                    }
                    std::string parent_dir = pathname.substr(0, pos + 1); // 将获取到的每级目录进行截取
                    if (parent_dir == "." || parent_dir == "..") // 如果截取的目录是当前目录或者是上一级目录则继续进行截取。
                    {
                        idx = pos + 1;
                        continue;
                    }
                    if (exists(parent_dir) == true) // 如果目录已经存在则继续进行截取
                    {
                        idx = pos + 1;
                        continue;
                    }
                    mkdir(parent_dir.c_str(), 0777); // 创建截取得到的未创建目录
                    idx = pos + 1;
                }
            }
        };
    }
}
/*test*/
std::cout << zyqlog::util::Date().now() << std::endl;
std::string pathname = "./abc/bcd/a.txt";
zyqlog::util::File().createDirectory(zyqlog::util::File::path(pathname));

日志等级类设计

日志等级共分为7个等级,分别为:

  • OFF 关闭所有日志输出
  • DRBUG 进行debug时候打印日志的等级
  • INFO 打印一些用户提示信息
  • WARN 打印警告信息
  • ERROR 打印错误信息
  • FATAL 打印致命信息-导致程序崩溃的信息
/*
    1. 定义枚举类,枚举出日志等级
    2. 提供转换接口,将枚举转换为对应的字符串
*/
namespace zyqlog
{
    class LogLevel
    {
    private:
    public:
        enum class value // 枚举类实现不同的日志等级
        {
            UNKNOW = 0,
            DEBUG,
            INFO,
            WARNING,
            ERROR,
            FATAL,
            OFF
        };

        static const char *toString(LogLevel::value level) // 将获取到的日志等级转换为字符串
        {
            switch (level)
            {
            case LogLevel::value::DEBUG:
                return "DEBUG";
            case LogLevel::value::INFO:
                return "INFO";
            case LogLevel::value::WARNING:
                return "WARNING";
            case LogLevel::value::ERROR:
                return "ERROR";
            case LogLevel::value::FATAL:
                return "FATAL";
            case LogLevel::value::OFF:
                return "OFF";
            }
            return "UNKNOW";
        }
    };
}

日志消息类设计

/*
    定义日志消息类,进行日志中间信息的存储:
    1. 日志的输出时间--用于过滤日志输出时间
    2. 日志等级--用于进行日志过滤分析
    3. 源文件名称
    4. 源代码行号--用于定位出现错误的代码的位置
    5. 线程ID--用于过滤出错的线程
    6. 日志主体消息
    7. 日志器名称--支持多日志器同时使用
*/
namespace zyqlog
{
    struct LogMsg
    {
        time_t _ctime; // 日志产生的时间戳
        LogLevel::value _level; // 日志等级
        std::thread::id _tid; // 线程ID
        size_t _line; // 行号
        std::string _file; // 源码文件名
        std::string _logger; // 日志器名称
        std::string _payload; // 有效消息数据
        LogMsg() {}
        LogMsg(LogLevel::value level, size_t line, const std::string file, const std::string logger, const std::string msg) 
            : _ctime(util::Date::now())
            , _level(level)
            , _line(line)
            , _tid(std::this_thread::get_id())
            , _file(file)
            , _logger(logger)
            , _payload(msg)
        {}
    };
}

日志格式化输出类

日志格式化(Formatter)类主要负责格式化日志消息。其主要包含以下内容

  • pattern成员:保存日志输出的格式字符串
    • %d 日期
    • %T 缩进
    • %t 线程id
    • %p 日志级别
    • %c 日志器名称
    • %f 文件名
    • %l 行号
    • %m 日志消息
    • %n 换行
  • std::vector< FormatItem::ptr > items成员:用于按序保存格式化字符串对应的子格式化对象
    FormatItem类主要负责日志消息子项的获取及格式化。其包含以下子类
  • MsgFormatItem:表示要从LogMsg中取出有效日志数据
  • LevelFormatItem:表示要从LogMsg中取出日志等级
  • ThreadFormatItem:表示要从LogMsg中取出线程ID
  • TimeFormatItem:表示要从LogMsg中取出时间戳并按照指定格式进行格式化
  • LineFormatItem:表示要从LogMsg中取出源码所在行号
  • TabFormatItem:表示⼀个制表符缩进
  • NLineFormatItem:表示⼀个换行
  • OtherFormatItem:表示非格式化的原始字符串

格式化的过程其实就是按次序从Msg中取出需要的数据进行字符串的连接的过程。

// 设计思想:
// 1. 抽象一个格式化子项基类
// 2. 基于基类, 派生出格式化子项子类
    // 在父类中定义父类指针数组,指向不同格式化子类对象
namespace zyqlog
{
    // 抽象格式化子项基类
    class FormatItem
    {
    public:
        using ptr = std::shared_ptr<FormatItem>;
        virtual void format(std::ostream &out, const LogMsg &msg) = 0;
    };
    
    // 派生类格式化子项子类--消息,等级,时间,文件名,行号,线程ID,日志器名,制表符,换行吗,其他
    class MsgFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, const LogMsg &msg) override
        {
            out << msg._payload;
        }
    };

    class LevelFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, const LogMsg &msg) override
        {
            out << LogLevel::toString(msg._level);
        }
    };

    class TimeFormatItem : public FormatItem
    {
    public:
        TimeFormatItem(const std::string &fmt = "%H:%M:%S") :_time_fmt(fmt) {}
        void format(std::ostream &out, const LogMsg &msg) override
        {
            struct tm t;
            localtime_r(&msg._ctime, &t);
            char tmp[32] = {0};
            strftime(tmp, 31, _time_fmt.c_str(), &t); // strftime()函数根据格式字符串将给定的日期和时间从给定的日历时间转换为以空结尾的多字节字符串。
            out << tmp;
        }
    private:
        std::string _time_fmt; // %H:%M:%S
    };

    class FileFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, const LogMsg &msg) override
        {
            out << msg._file;
        }
    };

    class LineFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, const LogMsg &msg) override
        {
            out << msg._line;
        }
    };

    class ThreadFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, const LogMsg &msg) override
        {
            out << msg._tid;
        }
    };

    class LoggerFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, const LogMsg &msg) override
        {
            out << msg._logger;
        }
    };

    class TabFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, const LogMsg &msg) override
        {
            out << "\t";
        }
    };

    class NLineFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, const LogMsg &msg) override
        {
            out << "\n";
        }
    };

    class OtherFormatItem : public FormatItem
    {
    public:
        OtherFormatItem(const std::string &str) :_str(str) {}
        void format(std::ostream &out, const LogMsg &msg) override
        {
            out << _str;
        }
    private:
        std::string _str;
    };

    /*
        %d 表示日期 包含子格式{%H:%M:%S}
        %t 表示线程ID 
        %c 表示日志器名称
        %f 表示源码文件名
        %l 表示源码行号
        %p 表示日志级别
        %T 表示制表符缩进
        %m 日志消息
        %n 表示换行
    */
    class ForMatter
    {
    public:
        using ptr = std::shared_ptr<ForMatter>;

        ForMatter(const std::string &pattern = "[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n") : _pattern(pattern) 
        {
            assert(parsePattern()); // 对格式化规则字符串进行解析
        }
        // 对msg进行格式化
        void format(std::ostream &out, LogMsg &msg)
        {
            for(auto &item : _items)
            {
                item->format(out, msg);
            }
        }
        std::string format(LogMsg &msg)
        {
            std::stringstream ss;
            format(ss, msg);
            return ss.str();
        }

    private:
        // 对格式化规则字符串进行解析
        bool parsePattern()
        {
            // 1. 对格式化规则字符串进行解析
            // abcd[ % d {%H:%M:%S} ][ %t][%c][%f:%l][%p]%T%m%n
            std::vector<std::pair<std::string, std::string>> fmt_order;
            size_t pos = 0;
            std::string key, val;
            while (pos < _pattern.size())
            {
                // 1. 处理原始字符串--判断是否是%,不是就是原始字符
                if (_pattern[pos] != '%')
                {
                    val.push_back(_pattern[pos++]);
                    continue;
                }
                // 能进行到此说明pos位置是%字符,%%处理称为一个%字符
                if (pos + 1 < _pattern.size() && _pattern[pos + 1] == '%')
                {
                    val.push_back('%');
                    pos += 2;
                    continue;
                }
                // 这时候原始字符串处理完毕
                if (!val.empty())
                {
                    fmt_order.push_back(std::make_pair("", val));
                    val.clear();
                }

                //代表%后面是一个格式化字符,格式化字符的处理
                pos += 1; // pos指向格式化字符的位置
                if (pos == _pattern.size()) 
                {
                    std::cerr << "%之后没有对应的字符!\n";
                    return false;
                }
                key = _pattern[pos]; // 确定key格式化字符的位置
                // 此时pos指向格式化字符后的位置
                pos += 1;
                if (pos < _pattern.size() && _pattern[pos] == '{') 
                {
                    pos += 1; // pos指向子规则的起始位置
                    while (pos < _pattern.size() && _pattern[pos] != '}')
                    {
                        val.push_back(_pattern[pos++]);
                    }
                    // 走到末尾跳出循环,则代表没有遇到},代表格式是错误的
                    if (pos == _pattern.size()) 
                    {
                        std::cerr << "子规则{}匹配出错!\n";
                        return false;
                    }
                    pos += 1; // pos指向}位置,向后走一步,到了下一步的位置
                }
                fmt_order.push_back(std::make_pair(key, val));
                key.clear();
                val.clear();
            }
            /*
				这个处理的过程以 abcd[%d{%H:%M:%S}][ %t][%c][%f:%l][%p]%T%m%n 为例子进行解析
				key = nullptr,val = abcd[
				key = d,val = %H:%M:%S
				key = nullptr,val = ][
				...
				得到数组内容之后,根据数组内容,创建格式化子项对象,添加到items成员数组中。
			*/
            // 2. 根据解析得到的数据初始化格式化子项数组成员
            for (auto &it : fmt_order)
            {
                _items.push_back(createItem(it.first, it.second));
            }

            return true;
        }

        // 根据不同格式化字符创建不同的格式化子项对象
        FormatItem::ptr createItem(const std::string &key, const std::string &val)
        {
            if (key == "d") return std::make_shared<TimeFormatItem>(val);
            if (key == "t") return std::make_shared<ThreadFormatItem>();
            if (key == "c") return std::make_shared<LoggerFormatItem>();
            if (key == "f") return std::make_shared<FileFormatItem>();
            if (key == "l") return std::make_shared<LineFormatItem>();
            if (key == "p") return std::make_shared<LevelFormatItem>();
            if (key == "T") return std::make_shared<TabFormatItem>();
            if (key == "m") return std::make_shared<MsgFormatItem>();
            if (key == "n") return std::make_shared<NLineFormatItem>();
            if (key.empty()) return std::make_shared<OtherFormatItem>(val);
            std::cerr << "没有对应的格式化字符: %" << key << std::endl;
            abort();
            return FormatItem::ptr();
        }
    private:
        std::string _pattern; // 格式化规则字符串
        std::vector<FormatItem::ptr> _items;
    };
}

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

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

相关文章

20240223-2092.查找所有有秘密的人

题目要求 给你一个整数 n&#xff0c;表示有 n 个人&#xff0c;编号从 0 到 n - 1。你还给你一个 0 索引的二维整数数组 meetings&#xff0c;其中 meetings[i] [xi, yi, timei] 表示 xi 和 yi 在 timei 有一个会议。一个人可以同时参加多个会议。最后&#xff0c;给你一个整…

【Flutter/Android】运行到安卓手机上一直卡在 Running Gradle task ‘assembleDebug‘... 的终极解决办法

方法步骤简要 查看你的Flutter项目需要什么版本的 Gradle 插件&#xff1a; 下载这个插件&#xff1a; 方法一&#xff1a;浏览器输入&#xff1a;https://services.gradle.org/distributions/gradle-7.6.3-all.zip 方法二&#xff1a;去Gradle官网找对应的版本&#xff1a;h…

个人机器人课程中最棘手的问题

爱好 22年&#xff0c;观点。当然现在自动驾驶研究路径已经不是22年的模式了。 23年&#xff0c;观点&#xff1a; 一个热爱自动驾驶但妥妥外行之人的思考-2023-CSDN博客 前篇 不合格机器人工程讲师为何不分享成功的案例-CSDN博客 在这篇文章中&#xff0c;有一段&#x…

Maya笔记 设置工作目录

Maya会把素材场景等自动保存在工作目录里&#xff0c;我们可以自己定义工作目录 步骤1 创建workspace.mel文件 文件/设置项目 ——>选择一个文件夹&#xff0c;点击设置——>创建默认工作区 这一个后&#xff0c;可以在文件夹里看到.mel文件 步骤2 自动创建文件夹…

进程的学习

进程基本概念: 1.进程: 程序&#xff1a;存放在外存中的一段数据组成的文件 进程&#xff1a;是一个程序动态执行的过程,包括进程的创建、进程的调度、进程的消亡 2.进程相关命令: 1.top 动态查看当前系统中的所有进程信息&#xff08;根据CPU占用率排序&#xf…

python中那些双下划线开头得函数和变量

嗨喽~大家好呀&#xff0c;这里是魔王呐 ❤ ~! python更多源码/资料/解答/教程等 点击此处跳转文末名片免费获取 Python中下划线—完全解读 Python 用下划线作为变量前缀和后缀指定特殊变量 _xxx 不能用from module import *导入 __xxx__ 系统定义名字 __xxx 类中的私有变量…

PC蓝牙通信

一、基本概念 蓝牙用于不同设备之间建立联系。利用无线电波在短距离间发送数据&#xff0c;wifi是在路由器跟设备间传输数据。蓝牙是在设备之间。蓝牙跟wifi同在2.4GHz频率下工作。蓝牙信号比wifi信号弱很多功率仅为1mW。传输距离有限。最初的1.0版本传输距离只有10m。现在5.0…

快速启动-后台管理系统

目录 Gitee人人开源 后端快速启动 1.clone仓库到本地 2.初始化数据库 3.更改数据库连接 4.启动项目验证 前端快速启动 1.克隆仓库 2.vsCode打开 3.控制台npm install 4.验证测试 时代已然不同&#xff0c;后台管理也可以使用脚手架方式快速启动。 Gitee人人开源 地…

JavaWeb——006MYSQL(DDLDML)

这里写目录标题 数据库开发-MySQL首先来了解一下什么是数据库。1. MySQL概述1.1 安装1.1.1 版本1.1.2 安装1.1.3 连接1.1.4 企业使用方式(了解) 1.2 数据模型1.3 SQL简介1.3.1 SQL通用语法1.3.2 分类 2. 数据库设计-DDL2.1 项目开发流程2.2 数据库操作2.2.1 查询数据库2.2.2 创…

Linux编程 1.2 系统文件IO- 使用

系统文件IO使用 1、open函数 #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> int open(const char* pathname,int flags); int open(const char* pathname,int flags,mode_t mode); 返回&#xff1a;若成功为文件描述符&#xff0c;若出错…

操作系统——处理机调度

文章目录 进程调度0.概念1.调度分类高级调度低级调度中级调度七状态模型调度对比 2.进程调度进程调度的时机进程调度的方式进程的切换方式调度器/调度程序闲逛进程 3. 调度算法的评价指标CPU利用率系统吞吐量周转时间等待时间响应时间 4. 调度算法先来先服务(FCFS)短作业优先(S…

微服务基础环境搭建

一.创建父工程 用于聚合其他微服务模块 1 新建 Maven 项目 JDK8Maven 项目Web 2 项目设置 编码的选择 UTF8JDK 版本的选择 3 删除 src 目录 4 配置父级 pom.xml SpringBoot&#xff1a;模块探究之spring-boot-dependencies-CSDN博客 子模块能够依赖当前父级 pom.xml 配置 【My…

LDR6020双盲插音频随便插充电听歌随便插

随着智能手机的普及和功能的日益丰富&#xff0c;手机已经成为我们日常生活中不可或缺的一部分。音乐、电影、游戏等娱乐内容更是丰富了手机的使用体验。而在这其中&#xff0c;音频转接器的作用愈发凸显&#xff0c;特别是在边听边充的场景下&#xff0c;一款高效且便捷的手机…

python脚本调用bitcoin-cli接口命令

脚本需求 1、python一个对外接口 2、不同的bitcoin命令通过传不同的参数实现 3、接口及接口的参数依次往后传递 4、日志全部打印到日志文件中并且日志文件按天进行切割 #!/usr/bin/python3from flask import Flask, request, jsonify import subprocess import json import os …

ROS中简单实现讯飞星火大模型API调用

文章目录 前言一、申请试用二、ROS中使用1.配置环境变量2.编写ros功能包总结前言 讯飞星火认知大模型是由科大讯飞自主研发的认知智能大模型,通过学习海量的文本、代码和图像,具备跨领域的知识和语言理解能力,能基于自然对话方式理解和执行任务。目前开放了API接口供用户使…

主数据管理是数字化转型成功的基石——江淮汽车案例分享

汽车行业数字化转型的背景 在新冠疫情导火索的影响下&#xff0c;经济全球化政治基础逐渐动摇。作为全球最大的汽车市场&#xff0c;我国的汽车市场逐渐由增量转为存量市场。 在数字化改革大背景下&#xff0c;随着工业4.0时代的到来&#xff0c;江淮汽车集团力争实现十四五数…

【汽车电子】万字详解汽车标定与XCP协议

XCP协议基础 文章目录 XCP协议基础一、引言1.1 什么是标定1.2 什么时候进行标定1.3 标定的意义 二、XCP协议简介2.1 xcp简介2.2 XCP如何加快开发过程&#xff1f;2.3 XCP的主要作用 三、XCP工作过程3.1 工作过程3.2 通讯模型3.3 测量与标定 四、XCP报文解析4.1 数据包报文格式4…

会分期完成2.3亿元C轮融资,它们都使用了拼音域名

易名科技()1月10日报道&#xff0c;致力于帮助租户缓解资金压力的汇分公司近日宣布完成2.3亿元C轮融资&#xff0c;由联络互动领投。 值得注意的是&#xff0c;它们都使用拼音域名。 他们分别是。 图&#xff1a;接触互动 汇分是汇房房旗下的租房分期平台。 打破了传统的押一付…

如何进行单元测试和集成测试

前端开发&#xff1a;如何进行单元测试和集成测试 在前端开发中&#xff0c;单元测试和集成测试是确保代码质量和稳定性不可或缺的一环。单元测试用于测试代码中的最小单元&#xff0c;而集成测试则是测试各个单元之间的交互和整体功能。通过合理的测试策略和工具&#xff0c;…

芯品荟 | 电动牙刷应用介绍

PART ONE 市场简介 - Market Profile - 电动牙刷个护是小家电最炙手可热的细分赛道之一。 随着居民的消费水平不断提升&#xff0c;口腔保健意识也逐步增强&#xff0c;中国电动牙刷市场迎来高速发展阶段。目前电动牙刷全球年用量在1亿只左右。 PART TWO 产品应用框图 - Bl…