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

日志系统的介绍及前置技术

目录

1、项目介绍

1.1 开发环境

1.2 核心技术

2、日志系统的介绍

2.1 日志系统的必要性

2.2 日志系统的技术实现

3、不定参数函数

3.1 C语言不定参宏函数

3.2 C语言不定参函数

3.3 C++不定参函数

4、设计模式

4.1 六大原则(SOLID 原则)

4.2 单例模式

4.2.1 饿汉模式

4.2.2 懒汉模式

4.2.3 适用场景的对比

4.3 工厂模式

4.3.1 简单工厂模式

4.3.2 工厂方法模式

4.3.3 抽象工厂模式

4.3.4 适用场景的对比

4.4 建造者模式

4.5 代理模式


完整代码:PreTech。

1、项目介绍

  • 本项目主要实现一个日志系统,其主要支持以下功能:
    • 支持多级别日志消息。
    • 支持同步(自己做)日志和异步(给别人做)日志。
    • 支持可靠写入日志到控制台文件以及滚动文件(当日志文件达到预设条件,创建新文件写入,同时管理旧文件)中。
    • 支持多线程并发写日志。
    • 支持扩展不同的日志落地日标地。

1.1 开发环境

  • Ubuntu 22.04
  • vscode/vim
  • g++/gdb
  • Makefile

1.2 核心技术

  • 类层次设计(继承多态的应用)。
  • C++11(多线程智能指针右值引用auto等)。
  • 双缓冲区
  • 生产者消费者模型
  • 设计模式(单例工厂代理建造者等)。

2、日志系统的介绍

2.1 日志系统的必要性

  • 出问题时,查日志找原因
  • 运行时,靠日志看状态
  • 要追责时,用日志留证据

2.2 日志系统的技术实现

  • 日志系统的技术实现,按输出方式和复杂度可分为几类:

    • 基础控制台输出:直接用 printfstd::cout 等原生函数,将日志打印到控制台(适合调试简单程序)。

    • 文件 / 数据库落地:针对大型项目,为便于排查和分析,将日志写入文件数据库,核心实现方式分两种:

      • 同步写日志:业务线程直接完成日志写入写完再继续执行
      • 异步写日志:业务线程仅提交日志到队列由后台线程异步写入不阻塞业务

3、不定参数函数

  • 在初学C语言的时候,我们都用过printf函数进行打印。其中printf函数就是一个不定参函数不定参函数在实际的使用中比较常见,这里简单做一下介绍。

3.1 C语言不定参宏函数

#include <stdio.h>// 当可变参数为空时,##会消除前面的逗号,避免语法错误
#define Print_1(fmt, ...) printf("[%s:%d] " fmt "\n", __FILE__, __LINE__, 
##__VA_ARGS__)int main()
{Print_1();Print_1("%s","Hello Log");return 0;
}
  • 输出结果:

3.2 C语言不定参函数

  • va_list ap;  // 声明一个可变参数列表指针
  • void va_start(va_list ap, last)
    • 功能初始化 va_list 变量 ap,让它指向函数中第一个可变参数
    • 参数
      • ap待初始化va_list 变量
      • last:函数中最后一个固定参数的名称(即可变参数 ... 前的最后一个参数)。
  • type va_arg(va_list ap, type)
    • 功能从 ap 获取下一个参数,并返回该参数的值类型type),同时自动将 ap 移动到下一个参数
    • 参数
      • ap已初始化va_list 变量
      • type要获取的参数类型(如 int、double 等,必须是完整类型)。
  • void va_end(va_list ap)
    • 功能:结束对可变参数列表的访问,清理 ap(释放可能的资源)。
  • int vasprintf(char **strp, const char *format, va_list ap);
  • 功能格式化字符串动态分配内存存储结果
  • 参数
  • strp输出型参数,是一个指向 char* 的指针。函数成功执行后,会将动态分配的字符串地址存入 *strp(即 *strp 指向格式化后的字符串)。
    • format:格式字符串(printf 的格式字符串规则相同,如 %d、%s 等)。
    • ap已初始化va_list 变量;。
  • 返回值
    • 成功:返回格式化后字符串的长度
    • 失败:返回 -1
  • 注意:在使用完毕后必须通过 free(*strp) 释放内存,否则会导致内存泄漏。
#include <iostream>
#include <stdarg.h>void PrintNum(int n, ...)
{va_list ap;va_start(ap, n);for(int i = 0; i < n; ++i){int num = va_arg(ap, int); // 拿到一个int类型的值std::cout << num << std::endl;}va_end(ap);
}void Print_2(const char* fmt, ...)
{va_list ap;va_start(ap, fmt);char* str;vasprintf(&str, fmt, ap);std::cout << str << std::endl;va_end(ap);free(str);
}int main()
{PrintNum(3, 1, 2, 3);Print_2("%s\n", "Hello Log");return 0;
}
  • 输出结果:

3.3 C++不定参函数

#include <iostream>void Print_3()
{// 编译时递归的终止条件,参数包是0个时,直接匹配这个函数。std::cout << "end" << std::endl;
}template<typename T, typename ...Args>
void Print_3(const T& val, Args&& ...args)
{std::cout << val << std::endl;Print_3(std::forward<Args>(args)...); // 保留原始实参的 “左值 / 右值身份”
}int main()
{Print_3();Print_3(1, 2, 3, "Hello Log");return 0;
}
  • 输出结果:

4、设计模式

  • 设计模式软件开发中总结的可复用解决方案,针对常见问题,提高代码可维护性可扩展性等。
  • 下面我们介绍日志系统中用到的几个设计模式。

4.1 六大原则(SOLID 原则)

  • 六大原则是 “道”(设计思想),设计模式是 “术”(具体解法)。
  • 单一职责原则(Single Responsibility Principle, SRP)
    • 核心一个类 / 模块只负责一个明确的职责(只做一件事)。
    • 目的减少类的复杂度,降低修改风险(修改一个职责不会影响其他职责)。
    • 例子:一个 “用户类” 不应同时负责 “用户信息管理” 和 “支付逻辑”,应拆分为 User 和 Payment 两个类。
  • 开闭原则(Open-Closed Principle, OCP)
    • 核心对扩展开放(新增功能时可新增代码),对修改关闭(不改动已有代码)。
    • 目的避免修改旧代码导致的风险(如引入新 bug)。
    • 例子:用 “抽象类 / 接口” 定义规范,新增功能时通过 “子类继承 / 实现接口” 扩展,而非修改抽象类本身(如用 Shape 接口定义图形,新增 Circle/Rectangle 只需实现接口,无需改 Shape)。
  • 里氏替换原则(Liskov Substitution Principle, LSP)
    • 核心:子类对象可以替换父类对象(在任何使用父类的地方,用子类替代后逻辑仍成立)。
    • 目的保证继承关系的合理性,避免子类破坏父类的预期行为
    • 反例:正方形继承长方形,重写 setWidth 和 setHeight 时强制宽高相等,导致 “长方形的宽高独立” 的父类逻辑被破坏,此时用正方形替换长方形会出错。
  • 依赖倒置原则(Dependency Inversion Principle, DIP)
    • 核心客户端不应依赖它不需要的接口接口小而专,避免 “大而全”)。
    • 目的:防止接口臃肿导致的实现负担(类只需实现自己需要的方法)。
    • 反例:一个 “动物接口” 包含 fly()、swim()、run(),让 “狗” 实现时被迫空实现 fly() 和 swim(),应拆分为 Flyable、Swimmable、Runnable 多个小接口。
  • 迪米特法则(Law of Demeter, LoD,又称 “最少知道原则”)
    • 核心一个对象应尽可能少地了解其他对象(只与直接朋友通信,避免间接依赖)。
    • 目的减少对象间的耦合降低系统复杂度
    • 例子:学生(A)想知道班级平均分,应直接问班长(B),而非让班长去问班主任(C)后再转告 A(A 只需依赖 B,无需知道 C 的存在)。

4.2 单例模式

  • 单例模式是一种创建型设计模式,核心目标是:保证一个类在程序中只有唯一实例,并提供一个全局访问点
  • 核心实现要点
    • 私有构造函数禁止外部通过 new 或 直接实例化 创建对象
    • 静态成员变量在类内部存储唯一的实例(全局仅一份);
    • 静态访问方法提供全局唯一的接口(如 GetInstance()),供外部获取这个实例。
  • 单例模式有两种实现模式:饿汉模式和懒汉模式。
4.2.1 饿汉模式
  • 饿汉模式程序启动时就会创建一个唯一的实例对象。因为单例对象已经确定,所以没有 “多线程并发创建实例” 的安全问题
  • #include <iostream>class Singleton
    {
    private:Singleton() {std::cout << "单例对象构造成功!" << std::endl;};~Singleton() {};
    public:Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;static Singleton& GetInstance(){return _ston;}
    private:static Singleton _ston;
    };
    Singleton Singleton::_ston;int main()
    {return 0;
    }
  • 输出结果:
4.2.2 懒汉模式
  • 懒汉模式:第一次使用单例对象的时候创建实例对象存在 多线程并发创建实例” 的安全问题
  • 这里介绍一下,Scott Meyers 在《Effective C++》中提出Meyers' Singleton迈耶斯单例)。在全局访问函数 getInstance() 中定义一个局部静态变量,该变量会在函数第一次被调用时初始化,且 C++11 标准明确规定 “局部静态变量的初始化是线程安全的”。
  • class Singleton
    {
    private:Singleton(){std::cout << "单例对象构造成功!" << std::endl;}~Singleton(){}
    public:Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;static Singleton& GetInstance(){static Singleton _ston;return _ston;}
    };int main()
    {Singleton::GetInstance();return 0;
    }
  • 输出结果:

4.2.3 适用场景的对比
  • 饿汉模式适合实例初始化成本低、且程序必然会用到的场景(如全局配置管理器)。
  • 懒汉模式适合实例初始化成本高(如大内存对象)、或可能不被使用的场景(如可选功能的管理器)。

4.3 工厂模式

  • 工厂模式是一种创建型设计模式,核心思想是将对象的创建过程封装在专门的工厂类。目的是解耦对象的创建” 和 “对象的使用”。
  • 工厂模式主要分为三种常见形式:简单工厂,工厂方法,抽象工厂。
4.3.1 简单工厂模式
  • 最基础工厂模式,通过一个单一工厂类根据输入的 “类型参数创建不同的产品实例
  • 核心结构
    • 抽象产品(Product):定义所有产品的公共接口(通常是纯虚类)。
    • 具体产品(ConcreteProduct):实现抽象产品接口具体类(如 “圆形”“矩形”)。
    • 具体工厂(Factory):提供静态或成员方法根据参数创建并返回具体产品的实例
  • // 简单工厂模式// 抽象产品:形状
    class Shape
    {
    public:virtual void Draw() = 0; // 公共接口// 基类析构函数设为虚函数后,子类析构函数会自动参与多态,// 确保通过基类指针销毁子类对象时,析构函数的调用符合 “实际对象类型”。virtual ~Shape() = default;
    };// 具体产品1:圆形
    class Circle : public Shape
    {
    public:void Draw() override{std::cout << "draw circle" << std::endl;}
    };// 具体产品2:矩阵
    class Rectangle : public Shape
    {
    public:void Draw() override{std::cout << "draw rectangle" << std::endl;}
    };// 具体工厂
    class ShapeFactory 
    {
    public:static std::unique_ptr<Shape> CreateShape(const std::string& type){if(type == "circle")return std::make_unique<Circle>();else if(type == "rectangle")return std::make_unique<Rectangle>();elsereturn nullptr;}
    };int main()
    {std::unique_ptr<Shape> circle = ShapeFactory::CreateShape("circle");std::unique_ptr<Shape> rect = ShapeFactory::CreateShape("rectangle");if (circle) circle->Draw();if (rect) rect->Draw();return 0;
    }
  • 输出结果:
  • 优点简单直观,用户只需关注 “用什么”,无需关心 “怎么创建”。
  • 缺点新增产品时,需要修改工厂类的CreateShape方法违反 开闭原则”:对扩展开放,对修改关闭),适合产品类型固定很少扩展的场景。
4.3.2 工厂方法模式
  • 为解决简单工厂的 “开闭原则问题,将工厂也抽象化抽象工厂定义创建产品的接口(通常是纯虚类),具体工厂负责创建对应产品每个具体产品对应一个具体工厂
  • 核心结构
    • 抽象产品(Product):同上。
    • 具体产品(ConcreteProduct):同上。
    • 抽象工厂(AbstractFactory):定义创建产品的纯虚方法(返回抽象产品)。
    • 具体工厂(ConcreteFactory):继承抽象工厂,实现创建方法返回具体产品
  • // 抽象产品:形状
    class Shape
    {
    public:virtual void Draw() = 0; // 公共接口// 基类析构函数设为虚函数后,子类析构函数会自动参与多态,// 确保通过基类指针销毁子类对象时,析构函数的调用符合 “实际对象类型”。virtual ~Shape() = default;
    };// 具体产品1:圆形
    class Circle : public Shape
    {
    public:void Draw() override{std::cout << "draw circle" << std::endl;}
    };// 具体产品2:矩阵
    class Rectangle : public Shape
    {
    public:void Draw() override{std::cout << "draw rectangle" << std::endl;}
    };// 抽象工厂
    class ShapeFactory
    {
    public:virtual std::unique_ptr<Shape> CreateShape() = 0;virtual ~ShapeFactory() = default;
    };// 具体工厂1:创建圆形
    class CircleFactory : public ShapeFactory 
    {
    public:std::unique_ptr<Shape> CreateShape() override{return std::make_unique<Circle>();}
    };// 具体工厂2:创建矩形
    class RectangleFactory : public ShapeFactory 
    {
    public:std::unique_ptr<Shape> CreateShape() override{return std::make_unique<Rectangle>();}
    };int main()
    {// 工厂本身也用智能指针,彻底避免手动释放std::unique_ptr<ShapeFactory> circle_factory = std::make_unique<CircleFactory>();std::unique_ptr<ShapeFactory> rect_factory = std::make_unique<RectangleFactory>();std::unique_ptr<Shape> circle = circle_factory->CreateShape();std::unique_ptr<Shape> rect = rect_factory->CreateShape();circle->Draw();rect->Draw();return 0;
    }
  • 输出结果:
  • 优点新增产品时,只需新增具体产品类” 和 “对应具体工厂类”,无需修改原有代码符合开闭原则)。
  • 缺点每增加一个产品,需额外增加一个工厂类,可能导致类数量过多
4.3.3 抽象工厂模式
  • 用于创建一系列相互关联或依赖的产品族(如 “华为手机 + 华为平板”“苹果手机 + 苹果平板”)。抽象工厂定义创建整个产品族的接口具体工厂负责创建一整个产品族
  • 核心结构
    • 抽象产品族多个相关抽象产品(如Phone、Tablet)。
    • 具体产品族每个品牌下的具体产品(如HuaweiPhone、HuaweiTablet)。
    • 抽象工厂:定义创建整个产品族接口(如createPhone()、createTablet())。
    • 具体工厂:实现抽象工厂,创建对应品牌的所有产品
  • // 抽象产品族:手机和平板
    class Phone
    {
    public:virtual void Call() = 0;virtual ~Phone() = default;
    };
    class Tablet
    {
    public:virtual void Watch() = 0;virtual ~Tablet() = default;
    };// 具体产品族1:华为系列
    class HuaweiPhone : public Phone
    {
    public:void Call() override{std::cout << "Huawei Call" << std::endl;}
    };
    class HuaweiTablet : public Tablet
    {
    public:void Watch() override{std::cout << "Huawei videos" << std::endl;}
    };// 具体产品族2:苹果系列
    class IPhone : public Phone
    {
    public:void Call() override{std::cout << "IPhone Call" << std::endl;}
    };
    class Ipad : public Tablet
    {
    public:void Watch() override{std::cout << "Ipad videos" << std::endl;}
    };// 抽象工厂:创建整个产品族的接口
    class AbstractFactory
    {
    public:virtual std::unique_ptr<Phone> CreatePhone() = 0;virtual std::unique_ptr<Tablet> CreateTablet() = 0;virtual ~AbstractFactory() = default;
    };// 具体工厂1:华为工厂
    class HuaweiFactory : public AbstractFactory
    {
    public:std::unique_ptr<Phone> CreatePhone() override{return std::make_unique<HuaweiPhone>();}std::unique_ptr<Tablet> CreateTablet() override{return std::make_unique<HuaweiTablet>();}
    };// 具体工厂2:苹果工厂
    class AppleFactory : public AbstractFactory
    {
    public:std::unique_ptr<Phone> CreatePhone() override{return std::make_unique<IPhone>();}std::unique_ptr<Tablet> CreateTablet() override{return std::make_unique<Ipad>();}
    };int main()
    {std::unique_ptr<AbstractFactory> huawei_factory = std::make_unique<HuaweiFactory>();std::unique_ptr<AbstractFactory> apple_factory = std::make_unique<AppleFactory>();std::unique_ptr<Phone> huawei_phone = huawei_factory->CreatePhone();std::unique_ptr<Tablet> huawei_tablet = huawei_factory->CreateTablet();std::unique_ptr<Phone> iphone = apple_factory->CreatePhone();std::unique_ptr<Tablet> ipad = apple_factory->CreateTablet();huawei_phone->Call();huawei_tablet->Watch();iphone->Call();ipad->Watch();return 0;
    }
  • 输出结果:
  • 优点:能保证同一产品族的产品相互匹配新增具体产品族只需新增具体工厂符合开闭原则)。
  • 缺点:若新增产品族需要的某个新产品(如新增 “手表”),需新增抽象产品+具体产品+修改所有工厂接口和实现扩展性较差
4.3.4 适用场景的对比
  • 简单工厂:适合产品少变化少的场景,直接通过参数创建对象
  • 工厂方法:适合产品可能扩展的场景, “一对一”(一个工厂造一种产品),遵循开闭原则。
  • 抽象工厂:适合需要创建产品族” 的场景(如同一品牌的系列产品),“一对多”(一个工厂造一族产品)

4.4 建造者模式

  • 建造者模式(Builder Pattern) 是一种创建型设计模式,核心思想是:复杂对象的构建过程与它的表示分离
  • 工厂模式是 “一键生成”,建造者模式是 “分步定制”。
  • 核心结构
  • 产品(Product):需要构建的复杂对象(如 “电脑”“汽车”)。
  • 抽象建造者(Builder):定义构建产品各部件的接口(如 “装 CPU”“装内存”),以及返回最终产品的方法
  • 具体建造者(ConcreteBuilder):实现抽象建造者的接口,负责具体部件的构建(如 “游戏本建造者” 用高性能 CPU,“办公本建造者” 用低功耗 CPU)。
  • 指挥者(Director):负责调用建造者的方法,按固定步骤组织构建过程(如 “先装 CPU→再装内存→最后装硬盘”),不关心具体部件细节
  • #include <iostream>
    #include <string>
    #include <memory>// 复杂产品:电脑(cpu+memory+disk)
    class Computer
    {
    public:using ptr = std::shared_ptr<Computer>;void SetCPU(const std::string& cpu){_cpu = cpu;}void SetMemory(const std::string& memory){_memory = memory;}void SetDisk(const std::string& disk){_disk = disk;}void Show(){std::cout << "电脑配置: "<< "\n\tCPU: " << _cpu<< "\n\tMemory: " << _memory<< "\n\tDisk: " << _disk<< std::endl;}private:std::string _cpu;std::string _memory;std::string _disk;
    };// 抽象建造者:定义构建电脑各组件的接口
    class ComputerBuilder
    {
    public:using ptr = std::shared_ptr<ComputerBuilder>;virtual void BuildCPU() = 0;virtual void BuildMemory() = 0;virtual void BuildDisk() = 0;virtual Computer::ptr GetResult() = 0;virtual ~ComputerBuilder() = default;
    };// 具体建造者1:游戏本建造者
    class GamingComputerBuilder : public ComputerBuilder
    {
    public:GamingComputerBuilder():_computer(std::make_shared<Computer>()){}void BuildCPU() override{_computer->SetCPU("Intel i9");}void BuildMemory() override{_computer->SetMemory("32GB DDR5");}void BuildDisk() override{_computer->SetDisk("1TB SSD");}Computer::ptr GetResult() override{return _computer;}private:Computer::ptr _computer;
    };// 具体建造者2:办公本建造者
    class OfficeComputerBuilder : public ComputerBuilder
    {
    public:OfficeComputerBuilder():_computer(std::make_shared<Computer>()){}void BuildCPU() override{_computer->SetCPU("Intel i5");}void BuildMemory() override{_computer->SetMemory("16GB DDR4");}void BuildDisk() override{_computer->SetDisk("512GB SSD");}Computer::ptr GetResult() override{return _computer;}private:Computer::ptr _computer;
    };// 指挥者:控制构建步骤
    class Director
    {
    public:Director(ComputerBuilder::ptr computer_builder):_computer_builder(computer_builder){}void Construct(){_computer_builder->BuildCPU();_computer_builder->BuildMemory();_computer_builder->BuildDisk();}private:ComputerBuilder::ptr _computer_builder;
    };int main()
    {std::shared_ptr<ComputerBuilder> gaming_computer_builder = std::make_shared<GamingComputerBuilder>();std::shared_ptr<ComputerBuilder> office_computer_builder = std::make_shared<OfficeComputerBuilder>();std::unique_ptr<Director> gaming_computer_director = std::make_unique<Director>(gaming_computer_builder);std::unique_ptr<Director> office_computer_director = std::make_unique<Director>(office_computer_builder);gaming_computer_director->Construct();office_computer_director->Construct();Computer::ptr gaming_computer = gaming_computer_builder->GetResult();Computer::ptr office_computer = office_computer_builder->GetResult();gaming_computer->Show();office_computer->Show();return 0;
    }
  • 输出结果:
  • 已经存在的智能指针,如果要传递,就使用std::shared_ptr

4.5 代理模式

  • 代理模式(Proxy Pattern) 是一种结构型设计模式,核心思想是:为一个对象提供代理”(中间层),通过代理间接访问原对象,从而在不修改原对象的前提下,对访问过程增加控制逻辑(如权限校验、日志记录、延迟加载等)。
  • 核心结构
  • 抽象主题(Subject):定义真实对象代理共同接口(纯虚类),确保代理可替代真实对象。因为要通过代理访问,代理应该要有真实对象的所有操作。
  • 真实主题(RealSubject):实际执行业务逻辑的对象(被代理的对象)。
  • 代理(Proxy):实现抽象主题接口内部持有真实主题的引用在调用真实主题的方法前后,可添加额外操作(如权限检查)。
  • 代理模式分为静态代理(编译时确定)、动态代理(运行时确定),动态代理目前有点复杂,下面介绍静态代理。
  • #include <iostream>class RentHouse
    {
    public:virtual void rentHouse() = 0;virtual ~RentHouse() = default;
    };class LandLord : public RentHouse
    {
    public:void rentHouse() override{std::cout << "rent house" << std::endl;}
    };class Intermediary : public RentHouse
    {
    public:void rentHouse() override{std::cout << "发布招租启事\n";std::cout << "带人看房\n";_landlord.rentHouse();std::cout << "负责租后维修\n";}private:LandLord _landlord;
    };int main()
    {Intermediary intermediary;intermediary.rentHouse();return 0;
    }
  • 输出结果:

http://www.dtcms.com/a/556687.html

相关文章:

  • 安居客做网站广州建设网站公司哪家好
  • 【JUnit实战3_22】 第十三章:用 JUnit 5 做持续集成(下):Jenkins + JUnit 5 + Git 持续集成本地实战演练完整复盘
  • 【Linux】 CI/CD 管道优化:使用 GitHub Actions/GitLab CI 提速构建和部署
  • XML 与 XSLT:深入解析与实际应用
  • 关于maven中pom依赖冲突问题记录
  • 360提交网站入口怎么做能够让网站流量大
  • 三亚做网站哪家好做网站推广的难点、
  • 做一家购物网站要多少钱天津网站建设哪家好
  • ps制作网站效果图有没有做任务拿佣金的网站
  • 国内网站设计案例欣赏自己的网站怎么做商城
  • 建设好的网站怎么分享门户cms
  • h5语言网站制作网站应急响应机制建设情况
  • qq刷赞网站怎么做的石家庄栾城区建设局网站
  • 滁州seo网站排名优化湛江网站建设皆选小罗24专业
  • 门户网站建设评标办法wordpress页面链接跳转
  • 做代码的网站做展示型网站多少钱
  • 国外网站后缀WordPress无法删除插件
  • 泗阳做网站公司做网站服务器配置
  • 站长统计软件网站换空间 怎么下载
  • 哪家房屋设计公司网站食品包装设计展开图片
  • 做网站如何赚广费广州app开发和制作
  • 如何把网站放到空间别人可以访问义乌seo快速排名
  • 做网站背景全覆盖的代码域名后缀html是怎样的网站
  • 潍坊网站设计好处自己开发小程序多少钱
  • 做网站排名优化有用吗老哥们给个手机能看的2020
  • 网站建设公司推荐乐云seo网站如何做内部链接
  • 常州建站软件网站开发后台指什么
  • 中国企业网官方网站查询铜仁建设集团招聘信息网站
  • 个人网站开发协议汽车网站建设多少钱
  • 二级网站怎么建前端做网站使用的软件工具