Linux——日志的编写与线程池

目录

前言

一、日志的编写

二、线程池

1.线程池基本原理

2.线程池作用 

3.线程池的实现 


前言

学了很多线程相关的知识点,线程控制、线程互斥、线程同步,今天我们将他们做一个总结,运用所学知识写一个较为完整的线程池,同时把日志编写也学一下。

一、日志的编写

在企业开发过程中,经常会通过打印日志来查看当前项目的运行情况。写一个日志难度并不大,用到的都是之前学的知识,注释写得比较详细,代码如下。

Log.hpp

#pragma once

#include<iostream>
#include<cstdarg>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
using namespace std;
enum{
    Debug = 0,
    Info,
    Warnig,
    Error,
    Fatal
};

enum{
    Screen = 10,
    OneFile,
    ClassFile
};

string LevelToString(int level)
{
    switch (level)
    {
    case Debug:
        return "Debug";
    case Info:
        return "Info";
    case Warnig:
        return "Warning";
    case Error:
        return "Error";
    case Fatal:
        return "Fatal";
    
    default:
        return "Unkonw"; 
    }
}
 
const int default_style = Screen;
const string default_filename = "Log.";
const string logdir = "log";

class Log  
{
public:
    Log(int style = default_style,string filename = default_filename)
        :_style(style),_filename(filename)
    {
        mkdir(logdir.c_str(),0775);
    }

    //更改打印方式
    void Enable(int style)
    {
        _style = style;
    }

    //时间戳转化为年月日时分秒
    string GetTime()
    {
        time_t currtime = time(nullptr);
        struct tm* curr = localtime(&currtime);
        char time_buffer[128];
        snprintf(time_buffer,sizeof(time_buffer),"%d-%d-%d %d:%d:%d",
        curr->tm_year+1900,curr->tm_mon+1,curr->tm_mday,curr->tm_hour,curr->tm_min,curr->tm_sec);
        return time_buffer;
    }

    //写入到文件中
    void WriteLogToOneFile(const string& logname,const string& message)
    {
        FILE* fp = fopen(logname.c_str(),"a");
        if(fp==nullptr)
        {
            perror("fopen filed");
            exit(-1);
        }
        fprintf(fp, "%s\n", message.c_str());

        fclose(fp);
    }

    //打印日志
    void WriteLogToClassFile(const string& levelstr,const string& message)
    {
        string logname = logdir;
        logname+="/";
        logname+=_filename;
        logname+=levelstr;
        WriteLogToOneFile(logname,message);
    }

    void WriteLog(const string& levelstr,const string& message)
    {
        switch (_style) 
        {
        case Screen:
            cout<<message<<endl;//打印到屏幕中
            break;
        case OneFile:
            WriteLogToClassFile("all",message);//给定all,直接写到all里
            break;
        case ClassFile:
            WriteLogToClassFile(levelstr,message);//写入levelstr里
            break;
        default:
            break;
        }
    }

    //打印日志
    void LogMessage(int level,const char* format,...)
    {
        char rightbuffer[1024];//处理消息
        va_list args;   //va_list 是指针
        va_start(args,format);//初始化va_list对象,format是最后一个确定的参数
        //现在args指向了可变参数部分
        vsnprintf(rightbuffer,sizeof(rightbuffer),format,args);//写入到leftbuffer中
        
        va_end(args);

        char leftbuffer[1024];//处理日志等级、pid、时间
        string levelstr = LevelToString(level);
        string currtime = GetTime();
        string idstr = to_string(getpid());

        snprintf(leftbuffer,sizeof(leftbuffer),"[%s][%s][%s]",levelstr.c_str()
        ,currtime.c_str(),idstr.c_str());

        string loginfo = leftbuffer;
        loginfo+=rightbuffer;
        WriteLog(levelstr,loginfo);
    }

    //提供接口给运算符重载使用
    void _LogMessage(int level,char* rightbuffer)
    {
        char leftbuffer[1024];
        string levelstr = LevelToString(level);
        string currtime = GetTime();
        string idstr = to_string(getpid());

        snprintf(leftbuffer,sizeof(leftbuffer),"[%s][%s][%s]",levelstr.c_str()
        ,currtime.c_str(),idstr.c_str());

        string messages = leftbuffer;
        messages+=rightbuffer;
        WriteLog(levelstr,messages);
    }

    //运算符重载
    void operator()(int level,const char* format,...)
    {
        char rightbuffer[1024];
        va_list args;   //va_list 是指针
        va_start(args,format);//初始化va_list对象,format是最后一个确定的参数
        vsnprintf(rightbuffer,sizeof(rightbuffer),format,args);//写入到leftbuffer中
        va_end(args);
        _LogMessage(level,rightbuffer);
    }

    ~Log() 
    {}
private:
    int _style;
    string _filename;
};

 Main.cc

#include "Log.hpp"

int main()
{
    Log log;
    log.Enable(ClassFile);
    log(Debug, "%d %s %f", 10, "test", 3.14);
    log(Info, "%d %s %f", 10, "test", 3.14);
    log(Warnig, "%d %s %f", 10, "test", 3.14);
    log(Error, "%d %s %f", 10, "test", 3.14);
    log(Fatal, "%d %s %f", 10, "test", 3.14);
    log(Debug, "%d %s %f", 10, "test", 3.14);
    log(Info, "%d %s %f", 10, "test", 3.14);
    log(Warnig, "%d %s %f", 10, "test", 3.14);
    log(Error, "%d %s %f", 10, "test", 3.14);
    log(Fatal, "%d %s %f", 10, "test", 3.14);
    log(Debug, "%d %s %f", 10, "test", 3.14);
    log(Info, "%d %s %f", 10, "test", 3.14);
    log(Warnig, "%d %s %f", 10, "test", 3.14);
    log(Error, "%d %s %f", 10, "test", 3.14);
    log(Fatal, "%d %s %f", 10, "test", 3.14);
}

数据就被我们分门别类的写入到了文件中,后面查询起来就很方便。 

二、线程池

之前我们还学习过生产者消费者模型,创建一批线程去生产,还有一批线程去消费。线程池也类似于如此。只不过生产是主线程自己做。有了任务他只需要将内容分配给已经被创建好进程,让这些进程去消费就行了,而这些被提前创建好的进程,就叫做进程池

1.线程池基本原理

  • 线程池由一组预先创建的线程组成,这些线程等待接收并执行任务。
  • 当需要执行任务时,而且线程池中有空闲线程时,任务被分配给其中一个空闲线程执行。
  • 如果没有空闲线程,任务将被放入任务队列中等待执行,直到有线程空闲为止。

2.线程池作用 

  • 降低了线程创建和销毁的开销:线程的创建和销毁是比较昂贵的操作,线程池可以避免频繁地创建和销毁线程。
  • 提高了性能:通过重用线程,可以减少线程的上下文切换和内存占用,提高了系统的整体性能。
  • 控制并发度:可以限制并发执行的任务数量,防止系统资源被耗尽。

3.线程池的实现 

我们需要如下变量:

  1. 一个任务队列,用于存储待执行的任务。
  2. 一个容器,来存放线程。
  3. 整形变量:存放线程最多的数量
  4. 保护临界资源:互斥锁
  5. 通知线程去处理任务:条件变量

具体代码如下

LockGuard.hpp   不生产锁,做线程的守护者,作用域到了自动释放锁

#pragma once
#include <pthread.h>

// 不定义锁,外部会传递锁
class Mutex
{
public:
    Mutex(pthread_mutex_t *lock)
        : _lock(lock)
    {
    }
    void Lock()
    {
        pthread_mutex_lock(_lock);
    }
    void UnLock()
    {
        pthread_mutex_unlock(_lock);
    }
    ~Mutex()
    {
    }

private:
    pthread_mutex_t *_lock;
};

class LockGuard
{
public:
    LockGuard(pthread_mutex_t *lock)
        : _mutex(lock)
    {
        _mutex.Lock();
    }
    ~LockGuard()
    {
        _mutex.UnLock();
    }
private:
    Mutex _mutex;
};

 Thread.hpp   模拟C++实现的线程,封装了一下

#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>

// 设计方的视角
//typedef std::function<void()> func_t;
template<class T>
using func_t = std::function<void(T&)>;

template<class T>
class Thread
{
public:
    Thread(const std::string &threadname, func_t<T> func, T &data)
    :_tid(0), _threadname(threadname), _isrunning(false), _func(func), _data(data)
    {}

    // 不加static会有this指针,无法调用pthread_creadte
    static void *ThreadRoutine(void *args) // 类内方法,
    {
        // (void)args; // 仅仅是为了防止编译器有告警
        Thread *ts = static_cast<Thread *>(args);

        ts->_func(ts->_data);

        return nullptr;
    }

    //运行线程
    bool Start()
    {
        int n = pthread_create(&_tid, nullptr, ThreadRoutine, this/*?*/);
        if(n == 0) 
        {
            _isrunning = true;
            return true;
        }
        else return false;
    }

    //等待线程
    bool Join()
    {
        if(!_isrunning) return true;
        int n = pthread_join(_tid, nullptr);
        if(n == 0)
        {
            _isrunning = false;
            return true;
        }
        return false;
    }
    std::string ThreadName()
    {
        return _threadname;
    }
    bool IsRunning()
    {
        return _isrunning; 
    }
    ~Thread()
    {}
private:
    pthread_t _tid;
    std::string _threadname;
    bool _isrunning;
    func_t<T> _func;
    T _data;
};

 Log.hpp   前面写的日志

#pragma once

#include<iostream>
#include<cstdarg>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
using namespace std;
enum{
    Debug = 0,
    Info,
    Warnig,
    Error,
    Fatal
};

enum{
    Screen = 10,
    OneFile,
    ClassFile
};

string LevelToString(int level)
{
    switch (level)
    {
    case Debug:
        return "Debug";
    case Info:
        return "Info";
    case Warnig:
        return "Warning";
    case Error:
        return "Error";
    case Fatal:
        return "Fatal";
    
    default:
        return "Unkonw"; 
    }
}
 
const int default_style = Screen;
const string default_filename = "Log.";
const string logdir = "log";

class Log  
{
public:
    Log(int style = default_style,string filename = default_filename)
        :_style(style),_filename(filename)
    {
        mkdir(logdir.c_str(),0775);
    }

    //更改打印方式
    void Enable(int style)
    {
        _style = style;
    }

    //时间戳转化为年月日时分秒
    string GetTime()
    {
        time_t currtime = time(nullptr);
        struct tm* curr = localtime(&currtime);
        char time_buffer[128];
        snprintf(time_buffer,sizeof(time_buffer),"%d-%d-%d %d:%d:%d",
        curr->tm_year+1900,curr->tm_mon+1,curr->tm_mday,curr->tm_hour,curr->tm_min,curr->tm_sec);
        return time_buffer;
    }

    //写入到文件中
    void WriteLogToOneFile(const string& logname,const string& message)
    {
        FILE* fp = fopen(logname.c_str(),"a");
        if(fp==nullptr)
        {
            perror("fopen filed");
            exit(-1);
        }
        fprintf(fp, "%s\n", message.c_str());

        fclose(fp);
    }

    //打印日志
    void WriteLogToClassFile(const string& levelstr,const string& message)
    {
        string logname = logdir;
        logname+="/";
        logname+=_filename;
        logname+=levelstr;
        WriteLogToOneFile(logname,message);
    }

    void WriteLog(const string& levelstr,const string& message)
    {
        switch (_style) 
        {
        case Screen:
            cout<<message<<endl;//打印到屏幕中
            break;
        case OneFile:
            WriteLogToClassFile("all",message);//给定all,直接写到all里
            break;
        case ClassFile:
            WriteLogToClassFile(levelstr,message);//写入levelstr里
            break;
        default:
            break;
        }
    }

    //打印日志
    void LogMessage(int level,const char* format,...)
    {
        char rightbuffer[1024];//处理消息
        va_list args;   //va_list 是指针
        va_start(args,format);//初始化va_list对象,format是最后一个确定的参数
        //现在args指向了可变参数部分
        vsnprintf(rightbuffer,sizeof(rightbuffer),format,args);//写入到leftbuffer中
        
        va_end(args);

        char leftbuffer[1024];//处理日志等级、pid、时间
        string levelstr = LevelToString(level);
        string currtime = GetTime();
        string idstr = to_string(getpid());

        snprintf(leftbuffer,sizeof(leftbuffer),"[%s][%s][%s]",levelstr.c_str()
        ,currtime.c_str(),idstr.c_str());

        string loginfo = leftbuffer;
        loginfo+=rightbuffer;
        WriteLog(levelstr,loginfo);
    }

    //提供接口给运算符重载使用
    void _LogMessage(int level,char* rightbuffer)
    {
        char leftbuffer[1024];
        string levelstr = LevelToString(level);
        string currtime = GetTime();
        string idstr = to_string(getpid());

        snprintf(leftbuffer,sizeof(leftbuffer),"[%s][%s][%s]",levelstr.c_str()
        ,currtime.c_str(),idstr.c_str());

        string messages = leftbuffer;
        messages+=rightbuffer;
        WriteLog(levelstr,messages);
    }

    //运算符重载
    void operator()(int level,const char* format,...)
    {
        char rightbuffer[1024];
        va_list args;   //va_list 是指针
        va_start(args,format);//初始化va_list对象,format是最后一个确定的参数
        vsnprintf(rightbuffer,sizeof(rightbuffer),format,args);//写入到leftbuffer中
        va_end(args);
        _LogMessage(level,rightbuffer);
    }

    ~Log() 
    {}
private:
    int _style;
    string _filename;
};

Log lg;

class Conf
{
public:
    Conf()
    {
        lg.Enable(ClassFile); 
    }
    ~Conf()
    {}
};

Conf conf;

 ThreadPool.hpp   线程池的实现

#pragma once

#include <pthread.h>
#include <vector>
#include <functional>
#include <queue>
#include "Log.hpp"
#include "Thread.hpp"
#include "LockGuard.hpp"
using namespace std;

static const int default_num = 5;

class ThreadData
{
public:
    ThreadData(string name)
        :thread_name(name)
    {}
    string thread_name;
};

template <class T>
class ThreadPool
{
public:
    ThreadPool(int thread_num = default_num)
        : _thread_num(thread_num)
    {
        pthread_mutex_init(&_mutex, nullptr); // 初始化
        pthread_cond_init(&_cond, nullptr);

        // 创建指定个数的线程
        for (int i = 0; i < _thread_num; i++)
        {
            string thread_name = "thread_";
            thread_name += to_string(i + 1);

            ThreadData td(thread_name);//ThreadData为线程数据类型
            
            Thread<ThreadData> t(thread_name, bind(&ThreadPool<T>::ThreadRun, this, 
            placeholders::_1), td);
            _threads.emplace_back(t);

            lg(Info,"%s 被创建...",thread_name.c_str());//写入
        }
    }

    //线程运行
    bool Start()
    {
        for (auto &thread : _threads)
        {
            thread.Start();
            lg.LogMessage(Info,"%s 正在运行!",thread.ThreadName().c_str());
        }
    }

    //线程条件变量等待
    void ThreadWait(ThreadData &td)
    {
        lg.LogMessage(Debug,"没有任务,%s休眠了",td.thread_name.c_str());
        pthread_cond_wait(&_cond,&_mutex);
    }

    //线程条件变量唤醒
    void ThreadWakeUp()
    {
        pthread_cond_signal(&_cond);
    }

    //执行任务
    void ThreadRun(ThreadData &td)
    {
        while (1)
        {
            T t;
            // 取出任务
            {
                LockGuard lockguard(&_mutex);//代码块中自动加锁与解锁
                while (_q.empty())
                {
                    ThreadWait(td);
                    lg.LogMessage(Debug,"有任务了,%s去执行任务了",td.thread_name.c_str());
                }
                t = _q.front();
                _q.pop();
            }
            //处理任务 我们通过打印消息来模拟任务
            // cout<<t<<endl;
            lg.LogMessage(Debug,"%s 计算结果为:%d",td.thread_name.c_str(),t);
        }
    }

    //将任务放到队列中
    void Push(const T &in)
    {
        {
            LockGuard lockguard(&_mutex);
            _q.push(in);
        }
        lg.LogMessage(Debug,"任务push成功,任务是: %d",in);
        ThreadWakeUp();
    }

    ~ThreadPool()
    {
        pthread_mutex_destroy(&_mutex); // 销毁
        pthread_cond_destroy(&_cond);
    }

    //进程等待
    void Wait()
    {
        for (auto &thread : _threads)
        {
            thread.Join();
        }
    }

private:
    queue<T> _q;
    vector<Thread<ThreadData>> _threads;
    int _thread_num;
    pthread_mutex_t _mutex;
    pthread_cond_t _cond;
};

 Main.cc

#include <memory>
#include "Threadpool.hpp"


int main()
{
    unique_ptr<ThreadPool<int>> tp(new ThreadPool<int>());
    tp->Start();
    int i = 1;
    while(true)
    {
        //放入数据模拟任务
        tp->Push(i++);
        sleep(1);
    }
    tp->Wait();
    return 0;
}

执行后,就将内容写入到了日志里面。 

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

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

相关文章

mac: docker安装及其Command not found: docker

已经安装了docker desktop&#xff0c;没安装的 点击安装 傻瓜式安装即可 接着打开终端&#xff1a;好一个 Comand not found:docker 看我不把你整顿&#xff0c;解决如下&#xff1a; 如果你在 macOS 上安装了 Docker Desktop&#xff0c;但是终端无法识别 docker 命令&…

目标检测——多模态人体动作数据集

一、重要性及意义 连续多模态人体动作识别检测的重要性及意义主要体现在以下几个方面&#xff1a; 首先&#xff0c;它极大地提升了人体动作识别的准确性和稳定性。由于人体动作具有复杂性和多样性&#xff0c;单一模态的数据往往难以全面、准确地描述动作的特征。而连续多模…

深度学习数据处理——对比标签文件与图像文件,把没有打标签的图像文件标记并删除

要对比目录下的jpg文件与json文件&#xff0c;并删除那些没有对应json文件的jpg文件&#xff0c;这个在深度学习或者机器学习时常会遇到。比如对一个数据集做处理时&#xff0c;往往会有些图像不用标注&#xff0c;那么这张图像是没有对应的标签文件的&#xff0c;这个时候又不…

python-django企业设备配件检修系统flask+vue

本课题使用Python语言进行开发。代码层面的操作主要在PyCharm中进行&#xff0c;将系统所使用到的表以及数据存储到MySQL数据库中&#xff0c;方便对数据进行操作本课题基于WEB的开发平台&#xff0c;设计的基本思路是&#xff1a; 前端&#xff1a;vue.jselementui 框架&#…

玄子Share-LVM与磁盘配额

玄子Share-LVM与磁盘配额 LVM概述 Logical Volume Manager&#xff0c;逻辑卷管理 Linux系统中对磁盘分区进行管理的一种逻辑机制&#xff0c;是建立在硬盘和分区之上的一个逻辑层动态调整磁盘容量&#xff0c;从而提高磁盘管理的灵活性 /boot分区用于存放引导文件&#xff…

服务器中查看CPU/GPU使用情况的常用命令

1、查看显卡 nvidia-smi2、间隔查看GPU使用情况 间隔5s刷新信息 watch -n 5 nvidia-smiCtrlC退出 参考博文&#xff1a;https://mbd.baidu.com/ug_share/mbox/4a83aa9e65/share?productsmartapp&tk6ff15196d305c4dd3daab94b4abb81a4&share_urlhttps%3A%2F%2Fyebd1h…

JavaSE备忘录(未完)

文章目录 基本数据类型println 小知识除法( / ) 和 Infinity(无穷) 小知识除法InfinityInfinity 在除法中正负判断 求余(%) 小知识 基本数据类型 除 int、char 的包装类分别为 Integer、Character 外&#xff0c;其余基本数据类型的第一个字母大写就是它的包装类。 println 小…

vscode自动生成返回值的快捷键

vscode中类似idea的altenter功能&#xff0c;可以添加返回值 idea中是Introduce local variable&#xff0c; vscode中按下command.(句号) 然后选extract to local variable或者 Assign statement to new local variable都行&#xff0c; 光标在分号前如图&#xff1a; 光标在…

Redis快速入门操作

启动Redis 进入命令行客户端 字符串命令常用操作&#xff08;redis默认使用字符串来存储数据&#xff09; 列表&#xff08;Lists&#xff09;常用操作 集合&#xff08;Sets&#xff09;常用操作 &#xff08;无序集合且元素不可重复&#xff09; 有序集合&#xff08;So…

云原生虚拟数仓 PieCloudDB Database 4月更新盘点

第一部分 PieCloudDB Database 最新动态 增强本地缓存文件生命周期管理 PieCloudDB 在最新版本中增强了本地缓存文件生命周期管理&#xff0c;执行器节点重启之后可以继续使用之前缓存在本地的数据文件&#xff0c;从而节约重新从远端下载数据文件的带宽资源&#xff0c;提升…

DFS之剪枝(上交考研题目--正方形数组的数目)

题目 给定一个非负整数数组 A A A&#xff0c;如果该数组每对相邻元素之和是一个完全平方数&#xff0c;则称这一数组为正方形数组。 返回 A A A 的正方形排列的数目。 两个排列 A 1 A1 A1 和 A 2 A2 A2 不同的充要条件是存在某个索引 i i i&#xff0c;使得 A 1 [ i …

C语言简单的数据结构:双向链表的实现

目录&#xff1a; 1.双向链表的结构和初始化1.1双向链表的结构1.2双向链表的初始化 2.双向链表的相关操作2.1双向链表的尾插、打印和头插2.11双向链表的尾插2.12双向链表的打印2.13双向链表的头插 2.2双向链表的尾删和头删2.21双向链表的尾删2.22双向链表的头删 2.3双向链表查找…

实力认证!亚数产品入选《中国网络安全行业全景图(第十一版)》

2024年4月12日&#xff0c;安全牛第十一版《中国网络安全行业全景图》&#xff08;以下简称“全景图”&#xff09;正式发布。 亚数信息科技&#xff08;上海&#xff09;有限公司&#xff08;以下简称“亚数”&#xff09;成功入选数字证书、加解密、密钥管理三项细分领域。 此…

开发同城O2O跑腿系统源码:构建高效便捷的本地服务平台教程

为了满足用户对便捷的需求&#xff0c;今天我们将一同探讨如何开发一个高效便捷的同城O2O跑腿系统&#xff0c;以构建一个功能全面、操作简单的本地服务平台。 一、确定需求和功能 在开发同城O2O跑腿系统之前&#xff0c;首先需要明确系统的需求和功能。用户可以通过该系统发布…

使用LangChain和Llama-Index实现多重检索RAG

大家好&#xff0c;在信息检索的世界里&#xff0c;查询扩展技术正引领着一场效率革命。本文将介绍这一技术的核心多查询检索&#xff0c;以及其是如何在LangChain和Llama-Index中得到应用的。 1.查询扩展 查询扩展是一种信息检索技术&#xff0c;通过在原始查询的基础上增加…

python辅助QQ登入

python辅助QQ登入 import pyautogui import time import random from pyautogui import ImageNotFoundException# 生成随机等待时间&#xff0c;范围在1到3秒之间 random_time random.uniform(1, 3)def find_and_click(image_path, moveFalse, execute_nextTrue):try:image_l…

达梦数据库:安装达梦数据库客户端并配置python调用

前言 本文主要介绍了达梦数据库的客户端安装方案、python调用方案。本文使用的达梦数据库版本为 V8&#xff0c;如果使用的是其他版本&#xff0c;操作可能会有些许差异。 下载 前往官网安装&#xff1a;产品下载 | 达梦数据库 根据自己的系统版本进行选择&#xff0c;而后点击…

基于SpringBoot的“论坛管理系统”的设计与实现(源码+数据库+文档+PPT)

基于SpringBoot的“论坛管理系统”的设计与实现&#xff08;源码数据库文档PPT) 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;SpringBoot 工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 系统展示 论坛管理系统结构图 前台首页功能界面图 用户登录…

高速公路信息化大会 | 云轴科技ZStack分享云原生超融合在高速公路行业的应用

近日&#xff0c;作为第二十六届高速公路信息化大会分论坛之一&#xff0c;由中国公路学会邀请、英特尔支持和协办《第四届英特尔智慧化方案助力高速新基建升级论坛》在合肥顺利召开。来自智慧交通建设领域的创新企业和技术专家共聚一堂&#xff0c;围绕改扩建高速公路提升和数…

Odoo|手把手教你Odoo集成drools,完成物料规则配置与报价单自动审核!

一、背景介绍 在实际业务中&#xff0c;售前根据客户需求选择相应的产品和对应的物料来生成报价单。然而&#xff0c;在填写报价单的过程中&#xff0c;可能会出现物料漏选或数量不准确的情况&#xff0c;这会对后续备货和生产效率造成重大影响。此外&#xff0c;由于产品和物料…
最新文章