C#上位机与三菱PLC的通信08---开发自己的通讯库(A-1E版)

1、A-1E报文回顾

 

具体细节请看:

C#上位机与三菱PLC的通信03--MC协议之A-1E报文解析

C#上位机与三菱PLC的通信04--MC协议之A-1E报文测试

2、为何要开发自己的通讯库

前面使用了第3方的通讯库实现了与三菱PLC的通讯,实现了数据的读写,对于通讯库,我们只要引用并调用相关的方法即可实现目的,为什么别人可以封装通讯库dll文件,自己能不能做到?当然可以,但写一个通讯库需要非凡的技术,需要考虑的东西很多,比如扩展性,通用性,等等之类的。通过封装通讯库达到更高的层次,想想,别人使用自己的东西,说明自己牛XXXX啊,大师就是这样锻造出来的,接下来马上安排,鸿鹄之志从小事做起,振兴工业自动化,匹夫有责。

3、空谈误国,实干兴邦

1、创建vs项目

2、添加类库项目

3、创建目录及基础类 

 

     

 

 

  AreaCode.cs代码

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Mitsubishi.Communication.MC.Mitsubishi.Base
{
    /// <summary>
    /// 存储区枚举
    /// </summary>
    public enum AreaCode
    {
        D = 0xA8,
        X = 0x9C,
        Y = 0x9D,
        M = 0x90,
        R = 0xAF,
        S = 0x98,
        TS = 0xC1,
        CN = 0xC5
    }
}

MelsecBase.cs代码

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Mitsubishi.Communication.MC.Mitsubishi.Base
{
    /// <summary>
    /// mc协议基类
    /// </summary>
    public class MelsecBase
    {
        /// <summary>
        /// plc的ip地址
        /// </summary>
        public string _ip;
        /// <summary>
        /// plc的端口号
        /// </summary>
        public int _port;
        /// <summary>
        /// socket对象
        /// </summary>
        public Socket socket = null;
        /// <summary>
        /// 超时事件
        /// </summary>
        ManualResetEvent TimeoutObject = new ManualResetEvent(false);
        /// <summary>
        /// 连接状态
        /// </summary>
        bool connectState = false;

        /// <summary>
        /// 构造方法
        /// </summary>
        /// <param name="ip"></param>
        /// <param name="port"></param>
        public MelsecBase(string ip, short port)
        {
            _ip = ip;
            _port = port;
            // 初始化一个通信对象
            socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        }

        /// <summary>
        /// 连接PLC
        /// </summary>
        /// <param name="timeout">超时时间</param>
        /// <returns></returns>
        public Result Connect(int timeout = 50)
        {
            TimeoutObject.Reset();
            Result result = new Result();
            try
            {
                if (socket == null)
                {
                    socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                }
                int count = 0;
                while (count < timeout)
                {
                    if (!(!socket.Connected || (socket.Poll(200, SelectMode.SelectRead) && (socket.Available == 0))))
                    {
                        return result;
                    }
                    try
                    {
                        socket?.Close();
                        socket.Dispose();
                        socket = null;

                        socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                        //异步连接 
                        socket.BeginConnect(_ip, _port, callback =>
                        {
                            connectState = false;
                            var cbSocket = callback.AsyncState as Socket;
                            if (cbSocket != null)
                            {
                                connectState = cbSocket.Connected;
                                if (cbSocket.Connected)
                                {
                                    cbSocket.EndConnect(callback);
                                }
                            }
                            TimeoutObject.Set();
                        }, socket);
                        TimeoutObject.WaitOne(2000, false);
                        if (!connectState)
                        {
                            throw new Exception();
                        }
                        else
                        {
                            break;
                        }
                    }
                    catch (SocketException ex)
                    {
                        if (ex.ErrorCode == 10060)
                        {
                            throw new Exception(ex.Message);
                        }
                    }
                    catch (Exception ex)
                    {
                        throw new Exception(ex.Message);
                    }
                    finally
                    {
                        count++;
                    }
                }
                if (socket == null || !socket.Connected || ((socket.Poll(200, SelectMode.SelectRead) && (socket.Available == 0))))
                {
                    throw new Exception("网络连接失败");
                }
            }
            catch (Exception ex)
            {
                result.IsSuccessed = false;
                result.Message = ex.Message;
            }
            return result;
        }

        /// <summary>
        /// 构建开始地址
        /// </summary>
        /// <param name="areaCode">存储区</param>
        /// <param name="startAddr">开始地址</param>
        /// <returns></returns>
        /// <exception cref="Exception"></exception>
        public List<byte> StartToBytes(AreaCode areaCode, string startAddr)
        {
            List<byte> startBytes = new List<byte>();
            if (areaCode == AreaCode.X || areaCode == AreaCode.Y)
            {
                string str = startAddr.ToString().PadLeft(8, '0');
                for (int i = str.Length - 2; i >= 0; i -= 2)
                {
                    string v = str[i].ToString() + str[i + 1].ToString();
                    startBytes.Add(Convert.ToByte(v, 16));
                }
            }
            else
            {
                int addr = 0;
                if (!int.TryParse(startAddr, out addr))
                {
                    throw new Exception("软元件地址不支持!");
                }
                startBytes.Add((byte)(addr % 256));
                startBytes.Add((byte)(addr / 256 % 256));
                startBytes.Add((byte)(addr / 256 / 256 % 256));
                startBytes.Add((byte)(addr / 256 / 256 / 256 % 256));
            }

            return startBytes;
        }

        /// <summary>
        /// 发送报文
        /// </summary>
        /// <param name="reqBytes">字节集合</param>
        /// <param name="count">字节长度</param>
        /// <returns></returns>
        public virtual List<byte> Send(List<byte> reqBytes, int count)
        {
            return null;
        }

        /// <summary>
        /// 数据解析
        /// </summary>
        /// <typeparam name="T">读取的数据类型</typeparam>
        /// <param name="datas">数据列表</param>
        /// <param name="typeLen">类型长度</param>
        /// <returns></returns>
        /// <exception cref="Exception"></exception>
        public List<T> AnalysisDatas<T>(List<byte> datas, int typeLen)
        {
            List<T> resultDatas = new List<T>();
            if (typeof(T) == typeof(bool))//bool类型
            {
                for (int i = 0; i < datas.Count; i++)
                {
                    // 10 10 10 10 10
                    string binaryStr = Convert.ToString(datas[i], 2).PadLeft(8, '0');
                    dynamic state = binaryStr.Substring(0, 4) == "0001";
                    resultDatas.Add(state);
                    state = binaryStr.Substring(4) == "0001";
                    resultDatas.Add(state);
                }
            }
            else//其他类型:ushort,short,float
            {
                for (int i = 0; i < datas.Count;)
                {
                    List<byte> valueByte = new List<byte>();
                    for (int sit = 0; sit < typeLen * 2; sit++)
                    {
                        valueByte.Add(datas[i++]);
                    }
                    Type tBitConverter = typeof(BitConverter);
                    MethodInfo method = tBitConverter.GetMethods(BindingFlags.Public | BindingFlags.Static).FirstOrDefault(mi => mi.ReturnType == typeof(T)) as MethodInfo;
                    if (method == null)
                    {
                        throw new Exception("未找到匹配的数据类型转换方法");
                    }
                    resultDatas.Add((T)method?.Invoke(tBitConverter, new object[] { valueByte.ToArray(), 0 }));
                }
            }

            return resultDatas;
        }

        /// <summary>
        /// 计算长度
        /// </summary>
        /// <typeparam name="T">读取的数据类型</typeparam>
        /// <returns></returns>
        public int CalculatLength<T>()
        {
            int typeLen = 1;
            if (!typeof(T).Equals(typeof(bool)))
            {
                typeLen = Marshal.SizeOf<T>() / 2;// 每一个数据需要多少个寄存器
            }
            return typeLen;
        }

        /// <summary>
        /// 获取数据的字节列表
        /// </summary>
        /// <typeparam name="T">数据类型</typeparam>
        /// <param name="values">数据列表</param>
        /// <returns></returns>
        public List<byte> GetDataBytes<T>(List<T> values)
        {
            List<byte> datas = new List<byte>();
            int count = values.Count;
            if (typeof(T) == typeof(bool))//bool类型的数据
            {
                dynamic value = false;
                // 添加一个填充数据,保存一个完整字节
                if (values.Count % 2 > 0)
                {
                    values.Add(value);
                }

                for (int i = 0; i < values.Count; i += 2)
                {
                    byte valueByte = 0;
                    if (bool.Parse(values[i].ToString()))
                    {
                        valueByte |= 16;
                    }
                    if (bool.Parse(values[i + 1].ToString()))
                    {
                        valueByte |= 1;
                    }
                    datas.Add(valueByte);
                }
            }
            else //其他类型:float,short,int16
            {
                for (int i = 0; i < values.Count; i++)
                {
                    dynamic value = values[i];
                    datas.AddRange(BitConverter.GetBytes(value)); // MC不需要字节的颠倒
                }
            }

            return datas;
        }
    }
}

Result.cs代码

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Mitsubishi.Communication.MC.Mitsubishi.Base
{
    /// <summary>
    /// 结果类
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class Result<T>
    {
        /// <summary>
        /// 状态
        /// </summary>
        public bool IsSuccessed { get; set; }
        /// <summary>
        /// 对应的消息
        /// </summary>
        public string Message { get; set; }
        /// <summary>
        /// 数据列表
        /// </summary>
        public List<T> Datas { get; set; }


        public Result() : this(true, "OK") { }
        public Result(bool state, string msg) : this(state, msg, new List<T>()) { }
        public Result(bool state, string msg, List<T> datas)
        {
            this.IsSuccessed = state; Message = msg; Datas = datas;
        }
    }

    public class Result : Result<bool> { }
}

确保上面的三个类文件编译成功,继续干

4、编写核心的通信类A1E.cs 

  

A1E.cs完整代码: 

using Mitsubishi.Communication.MC.Mitsubishi.Base;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

namespace Mitsubishi.Communication.MC.Mitsubishi
{
    /// <summary>
    /// A1E报文通讯库
    /// </summary>
    public class A1E : MelsecBase
    {
        /// <summary>
        /// 构造方法
        /// </summary>
        /// <param name="ip"></param>
        /// <param name="port"></param>
        public A1E(string ip, short port) : base(ip, port)
        {

        }

        #region 读取数据

        /// <summary>
        /// 读取数据
        /// </summary>
        /// <typeparam name="T">读取的数据类型</typeparam>
        /// <param name="address">开始地址</param>
        /// <param name="count">读取长度</param>
        /// <returns></returns>
        public Result<T> Read<T>(string address, short count)
        {
            AreaCode areaCode;
            string start;
            (areaCode, start) = this.AnalysisAddress(address);
            return Read<T>(areaCode, start, count);
        }

        /// <summary>
        /// 读取数据
        /// </summary>
        /// <typeparam name="T">读取的数据类型</typeparam>
        /// <param name="areaCode">存储区代码</param>
        /// <param name="startAddr">开始地址</param>
        /// <param name="count">读取长度</param>
        /// <returns></returns>
        public Result<T> Read<T>(AreaCode areaCode, string startAddr, short count)
        {
            Result<T> result = new Result<T>();
            try
            {
                var connectState = this.Connect();
                if (!connectState.IsSuccessed)
                {
                    throw new Exception(connectState.Message);
                }

                //读取类型
                byte readCode = (byte)(typeof(T) == typeof(bool) ? 0x00 : 0x01);
                //起始地址
                List<byte> startBytes = this.StartToBytes(areaCode, startAddr);
                //存储区代码
                List<byte> areaBytes = this.AreaToBytes(areaCode);
                //读取长度
                int typeLen = this.CalculatLength<T>();
                //组装报文
                List<byte> command = new List<byte> {
                    readCode,///读取类型
                    0xFF,0x0A,0x00,//0xFF指PLC编号,0x0A,0x00指超时时间,超时时间是以250ms为单位 
                    startBytes[0],startBytes[1],startBytes[2],startBytes[3], // 起始地址,占4个字节
                    areaBytes[0],areaBytes[1], // 存储区,占2个字节
                    (byte)(typeLen*count%256),// 读取长度,低位
                    (byte)(typeLen*count/256%256) // 读取长度,高位
                };
                //计算响应报文的长度
                int respLen = typeLen * 2 * count;
                if (typeof(T) == typeof(bool))
                {
                    respLen = (int)Math.Ceiling(typeLen * count * 1.0 / 2);
                }
                //发送报文
                List<byte> respBytes = this.Send(command, respLen);
                //数据解析
                result.Datas = this.AnalysisDatas<T>(respBytes, typeLen);
            }
            catch (Exception ex)
            {
                result = new Result<T>(false, ex.Message);
            }

            return result;
        }

        #endregion 

        #region 写入数据

        /// <summary>
        /// 写数据
        /// </summary>
        /// <typeparam name="T">写入的数据类型</typeparam>
        /// <param name="values">数据值列表</param>
        /// <param name="addr">开始地址</param>
        /// <returns></returns>
        public Result Write<T>(List<T> values, string addr)
        {
            AreaCode areaCode; string start;
            (areaCode, start) = this.AnalysisAddress(addr);
            return this.Write<T>(values, areaCode, start);
        }

        /// <summary>
        /// 写数据
        /// </summary>
        /// <typeparam name="T">写入的数据类型</typeparam>
        /// <param name="values">数据值列表</param>
        /// <param name="areaCode">存储区代码</param>
        /// <param name="startAddr">开始地址</param>
        /// <returns></returns>
        public Result Write<T>(List<T> values, AreaCode areaCode, string startAddr)
        {
            Result result = new Result();

            try
            {
                var connectState = this.Connect();
                if (!connectState.IsSuccessed)
                {
                    throw new Exception(connectState.Message);
                }

                // 写操作的类型 //0x00 批量位读取 //0x01 批量字读取 //0x02 批量位写入 //0x03 批量字写入 //0x04 随机位写入 //0x05 随机字写入
                byte writeCode = (byte)(typeof(T) == typeof(bool) ? 0x02 : 0x03);
                //开始地址
                List<byte> startBytes = this.StartToBytes(areaCode, startAddr);
                //存储区代码
                List<byte> areaBytes = this.AreaToBytes(areaCode);
                //构建数据的字节列表
                int count = values.Count;
                List<byte> datas = this.GetDataBytes<T>(values);
                //判断写入的长度,如果是float类型则长度要扩大2倍
                int length = count;//长度等于值的个数 
                if (typeof(T) == typeof(float))
                {
                    length = length * 2;
                }
                //拼装报文
                List<byte> command = new List<byte> {
                    writeCode,
                    0xFF, 0x0A, 0x00,//0xFF指PLC编号,0x0A,0x00指超时时间,超时时间是以250ms为单位 
                    startBytes[0], startBytes[1], startBytes[2], startBytes[3], // 起始地址
                    areaBytes[0], areaBytes[1], // 存储区 
                    //写入的长度的低位和高位 
                    (byte)(length % 256),
                    (byte)(length / 256 % 256),
                };
                command.AddRange(datas);//写入的具体数据
                //发送报文
                socket.Send(command.ToArray());

                // 判断写入的结果 
                byte[] respBytes = new byte[2];
                socket.Receive(respBytes);
                if (respBytes[0] != (writeCode |= 0x80))
                {
                    throw new Exception("响应报文结构异常。" + respBytes[0].ToString());
                }
                if (respBytes[1] != 0x00)
                {
                    throw new Exception("响应异常。" + respBytes[1].ToString());
                }
            }
            catch (Exception ex)
            {
                result.IsSuccessed = false;
                result.Message = ex.Message;
            }

            return result;
        }

        #endregion

        #region PLC启停,区别功能码  0x13,0x14
        public Result Run()
        {
            return PlcState(0x13);
        }
        public Result Stop()
        {
            return PlcState(0x14);
        }
        private Result PlcState(byte commandCode)
        {
            Result result = new Result();
            try
            {
                var connectState = this.Connect();
                if (!connectState.IsSuccessed)
                {
                    throw new Exception(connectState.Message);
                }

                List<byte> commandBytes = new List<byte>
                {
                    commandCode,0xFF,0x0A,0x00
                };
                socket.Send(commandBytes.ToArray());

                // 先判断响应状态
                byte[] respBytes = new byte[2];
                socket.Receive(respBytes);

                if (respBytes[0] != (commandCode |= 0x80))
                {
                    throw new Exception("响应报文结构异常。" + respBytes[0].ToString());
                }
                if (respBytes[1] != 0x00)
                {
                    throw new Exception("响应异常。" + respBytes[1].ToString());
                }
            }
            catch (Exception ex)
            {
                result.IsSuccessed = false;
                result.Message = ex.Message;
            }

            return result;
        }
        #endregion

        #region 内部方法

        /// <summary>
        /// 构建存储区代码
        /// </summary>
        /// <param name="areaCode">存储区代码</param>
        /// <returns></returns>
        private List<byte> AreaToBytes(AreaCode areaCode)
        {
            List<byte> areaBytes = new List<byte>();
            string areaStr = areaCode.ToString();
            areaBytes.AddRange(Encoding.ASCII.GetBytes(areaStr));
            if (areaBytes.Count == 1)
            {
                areaBytes.Add(0x20);
            }
            areaBytes.Reverse(); //字节反转
            return areaBytes;
        }

        /// <summary>
        /// 发送报文
        /// </summary>
        /// <param name="reqBytes">报文字节集合</param>
        /// <param name="len">报文字节长度</param>
        /// <returns></returns>
        /// <exception cref="Exception"></exception>
        public override List<byte> Send(List<byte> reqBytes, int len)
        {
            socket.Send(reqBytes.ToArray()); //发送报文
            // 先判断响应状态
            byte[] respBytes = new byte[2];
            socket.Receive(respBytes);

            if (respBytes[0] != (reqBytes[0] |= 0x80))
            {
                throw new Exception("响应报文结构异常。" + respBytes[0].ToString());
            }
            if (respBytes[1] != 0x00)
            {
                throw new Exception("响应异常。" + respBytes[1].ToString());
            }
            respBytes = new byte[len];
            socket.Receive(respBytes, 0, len, SocketFlags.None);
            return new List<byte>(respBytes);
        }


        /// <summary>
        /// 地址解析,输入的地址:X100    X1A0    M100    D100   TN10
        /// </summary>
        /// <param name="address">地址字符串</param>
        /// <returns>返回元组</returns>
        public Tuple<AreaCode, string> AnalysisAddress(string address)
        {
            // 取两个字符
            string area = address.Substring(0, 2);
            if (!new string[] { "TN", "TS", "CS", "CN" }.Contains(area))
            {
                area = address.Substring(0, 1);
            }
            string start = address.Substring(area.Length);
            // 返回元组(一个对象,该对象包括编号和地址)
            var obj = new Tuple<AreaCode, string>((AreaCode)Enum.Parse(typeof(AreaCode), area), start);
            return obj;
        }

        #endregion 


    }
}

确保项目编译成功,可以进行下一步

4、测试通讯库

1、添加项目引用    

 2、启动MC服务器 

3、利用通讯库读写数据

 1 读取X区100开始的4个bool数据

 

 2、读取D区100开始的5个float数据

 3、读取M区200开始的2个short数据

4、写入M区200开始的2个short数据 

 

5、写入D区200开始的5个float数据 

 4、完整代码

using Mitsubishi.Communication.MC.Mitsubishi;
using Mitsubishi.Communication.MC.Mitsubishi.Base;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Mitsubishi.Communication.Test
{
    internal class Program
    {
        static void Main(string[] args)
        {
            MCLibTestA1E(); 
            Console.WriteLine("执行完成!");
            Console.ReadKey();
        }

        /// <summary>
        /// 测试A-1E通讯库
        /// </summary>
        static void MCLibTestA1E()
        {
            A1E a1E = new A1E("192.168.1.7", 6000);
            #region 读数据
            Console.WriteLine("读取X区100开始的4个bool数据");
            var result1 = a1E.Read<bool>(AreaCode.X, "100", 4);
            if (result1.IsSuccessed)
            {
                result1.Datas.ForEach(d => Console.WriteLine(d));
            }
            else
            {
                Console.WriteLine(result1.Message);
            }
            Console.WriteLine("读取D区200开始的5个float数据");
            var result2 = a1E.Read<float>(AreaCode.D, "200", 5);
            if (result2.IsSuccessed)
            {
                result2.Datas.ForEach(d => Console.WriteLine(d));
            }
            else
            {
                Console.WriteLine(result2.Message);
            }
            Console.WriteLine("读取M区200开始的2个short数据");
            var result3 = a1E.Read<short>(AreaCode.M, "200", 2);
            if (result3.IsSuccessed)
            {
                result3.Datas.ForEach(d => Console.WriteLine(d));
            }
            else
            {
                Console.WriteLine(result3.Message);
            }

            #endregion

            #region 写数据
            Console.WriteLine("写入M区200开始的2个short数据");
            var result4 = a1E.Write<short>(new List<short> { 61, 72 }, "M200");
            if (result4.IsSuccessed)
            {
                Console.WriteLine(result4.Message);
            }
            Console.WriteLine("写入D区200开始的5个float数据");
            var result5 = a1E.Write<float>(new List<float> { 3.2f, -2.5f, 0, 35, -98 }, "D200");
            if (result5.IsSuccessed)
            {
                Console.WriteLine(result5.Message);
            }

            #endregion

        }
    }
}

5、小结

原创真的不容易,走过路过不要错过,点赞关注收藏又圈粉,共同致富。

原创真的不容易,走过路过不要错过,点赞关注收藏又圈粉,共同致富。

原创真的不容易,走过路过不要错过,点赞关注收藏又圈粉,共同致富

  • 27
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: Mitsubishi CNC网络是一个用于Mitsubishi CNC机床的网络系统。这个系统可以连接多台机床,并通过网络实现数据的传输和监控。 Mitsubishi CNC网络使用高性能的通信设备和协议,确保数据的高速传输和实时监控。通过网络连接,可以实现多台机床之间的数据共享和协作。例如,可以将加工程序和工艺参数从一个机床传输到另一个机床,从而实现生产线的灵活调度和协同工作。 Mitsubishi CNC网络还可以通过远程监控功能,实时监控机床的工作状态。机床的运行情况、加工状态以及故障信息都可以通过网络传输到中央控制中心,方便操作员进行远程监控和故障排除。 此外,Mitsubishi CNC网络还提供了远程诊断和维护的功能。通过网络连接,厂商可以远程诊断机床的故障,并及时提供修复方案。这大大提高了设备的可靠性和维护效率。 总而言之,Mitsubishi CNC网络是一个强大的网络系统,为Mitsubishi CNC机床提供了高效的数据传输、实时监控和远程维护功能。它有助于提高机床的生产效率和设备的可靠性,满足现代制造业对于智能化网络化的需求。 ### 回答2: Mitsubishi CNC网络是三菱公司的一种计算机数控网络系统,用于控制和监控其数控机床。这个网络系统具有高度可靠性和稳定性,可以实现机床之间的数据共享和远程监控。通过该网络,操作人员可以在中央控制台上集中管理多个数控机床,并且可以对机床的运行状态进行实时监控和远程操作。 Mitsubishi CNC网络具有许多优势。首先,它可以提高生产效率和质量控制。通过实现数据共享和远程监控,操作人员可以更快速地进行生产计划和调度,减少机床的闲置时间。其次,该网络系统还可以减少人力资源的消耗。操作人员可以在中央控制台上远程监控和操作多个机床,从而减少了在现场操作的需求,节省了人力资源和成本。此外,Mitsubishi CNC网络还具有高度可定制性,可以根据用户需求进行定制和升级,以实现更高的生产效率和灵活性。 总而言之,Mitsubishi CNC网络是一种高度可靠、稳定、高效的数控网络系统,适用于各种规模的制造企业。它能够提高生产效率、质量控制和人力资源利用率,为企业带来更高的经济效益。同时,这个网络系统还具有可定制性和升级性,以满足不同企业的需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hqwest

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值