本文目录
- 1. 一、台达PLC Modbus通讯概述
- 2. 二、C# Modbus通讯类实现
- 3. 三、完整使用示例
- 4. 四、Windows Forms上位机界面示例
- 5. 五、常见问题与排查
- 6. 六、最佳实践
- 7. 基于C#的上位机实例,用于与台达PLC进行Modbus通信源码¥8.99
台达(Delta)PLC是国内工业自动化领域应用最广泛的PLC品牌之一,支持Modbus RTU和Modbus TCP通讯协议。C#作为Windows平台上位机开发的主流语言,与台达PLC的Modbus通讯是工业自动化项目中的常见需求。本文提供完整的C#上位机与台达PLC Modbus通信实例,包括连接管理、寄存器读写、数据解析、断线重连、多设备管理以及Windows Forms界面示例。
一、台达PLC Modbus通讯概述
1.1 台达PLC支持的Modbus功能
| 功能码 | 功能 | 台达PLC对应区域 |
|---|---|---|
| 0x01 | 读线圈 | M(辅助继电器)、Y(输出继电器) |
| 0x02 | 读离散输入 | X(输入继电器) |
| 0x03 | 读保持寄存器 | D(数据寄存器) |
| 0x05 | 写单个线圈 | M、Y |
| 0x06 | 写单个寄存器 | D |
| 0x0F | 写多个线圈 | M、Y |
| 0x10 | 写多个寄存器 | D |
1.2 台达PLC地址映射
台达PLC的Modbus地址与内部软元件的对应关系(以DVP系列为例):
| 软元件 | Modbus地址(十进制) | 说明 |
|---|---|---|
| D0-D9999 | 0-9999 | 数据寄存器,保持寄存器 |
| M0-M9999 | 0-9999 | 辅助继电器,线圈 |
| Y0-Y255 | 0-255(偏移2048) | 输出继电器,线圈 |
| X0-X255 | 0-255(偏移2048) | 输入继电器,离散输入 |
| S0-S999 | 0-999(偏移4096) | 状态继电器,线圈 |
| T0-T255触点 | 1024-1279 | 定时器触点状态,线圈(功能码01/05) |
| C0-C255触点 | 1536-1791 | 计数器触点状态,线圈(功能码01/05) |
| T/C当前值 | 视型号而定 | 部分型号支持通过D寄存器映射读取,需查阅对应手册 |
1.3 通讯参数设置
- Modbus TCP:PLC IP地址 + 端口502,从站地址默认1
- Modbus RTU(COM1):9600bps,8数据位,无校验,1停止位(默认)
- Modbus RTU(COM2):9600bps,8数据位,无校验,1停止位(默认)
- 从站地址:可在PLC中设置,默认1
- 通讯协议:需在PLC编程软件中设置为Modbus RTU Slave或Modbus TCP
二、C# Modbus通讯类实现
2.1 Modbus TCP客户端类
using System;
using System.Net.Sockets;
using System.Threading;
namespace DeltaPlcModbus
{
public class ModbusTcpClient : IDisposable
{
private TcpClient _client;
private NetworkStream _stream;
private ushort _transactionId = 0;
private readonly object _lock = new object();
public string IpAddress { get; }
public int Port { get; }
public byte SlaveId { get; set; } = 1;
public int Timeout { get; set; } = 3000;
public bool IsConnected => _client?.Connected ?? false;
public ModbusTcpClient(string ip, int port = 502)
{
IpAddress = ip;
Port = port;
}
/// <summary>
/// 连接PLC(带重试)
/// </summary>
public bool Connect(int retryCount = 3)
{
for (int i = 0; i < retryCount; i++)
{
try
{
_client = new TcpClient();
_client.ReceiveTimeout = Timeout;
_client.SendTimeout = Timeout;
_client.Connect(IpAddress, Port);
_stream = _client.GetStream();
Console.WriteLine($"已连接到PLC:{IpAddress}:{Port}");
return true;
}
catch (Exception ex)
{
Console.WriteLine($"连接失败(第{i + 1}次):{ex.Message}");
Thread.Sleep(1000);
}
}
return false;
}
/// <summary>
/// 断开连接
/// </summary>
public void Disconnect()
{
_stream?.Close();
_client?.Close();
_client = null;
_stream = null;
}
/// <summary>
/// 读保持寄存器(功能码03)- 读取D寄存器
/// </summary>
public ushort[] ReadHoldingRegisters(ushort startAddr, ushort count)
{
lock (_lock)
{
byte[] request = BuildReadRequest(0x03, startAddr, count);
byte[] response = SendAndReceive(request);
if (response[7] != 0x03)
throw new ModbusException(response[7], response[8]);
byte byteCount = response[8];
ushort[] result = new ushort[byteCount / 2];
for (int i = 0; i < result.Length; i++)
{
result[i] = (ushort)((response[9 + i * 2] << 8) | response[9 + i * 2 + 1]);
}
return result;
}
}
/// <summary>
/// 读线圈(功能码01)- 读取M/Y继电器
/// </summary>
public bool[] ReadCoils(ushort startAddr, ushort count)
{
lock (_lock)
{
byte[] request = BuildReadRequest(0x01, startAddr, count);
byte[] response = SendAndReceive(request);
if (response[7] != 0x01)
throw new ModbusException(response[7], response[8]);
byte byteCount = response[8];
bool[] result = new bool[count];
for (int i = 0; i < count; i++)
{
result[i] = (response[9 + i / 8] & (1 << (i % 8))) != 0;
}
return result;
}
}
/// <summary>
/// 读离散输入(功能码02)- 读取X输入
/// </summary>
public bool[] ReadDiscreteInputs(ushort startAddr, ushort count)
{
lock (_lock)
{
byte[] request = BuildReadRequest(0x02, startAddr, count);
byte[] response = SendAndReceive(request);
if (response[7] != 0x02)
throw new ModbusException(response[7], response[8]);
byte byteCount = response[8];
bool[] result = new bool[count];
for (int i = 0; i < count; i++)
{
result[i] = (response[9 + i / 8] & (1 << (i % 8))) != 0;
}
return result;
}
}
/// <summary>
/// 写单个寄存器(功能码06)- 写D寄存器
/// </summary>
public void WriteSingleRegister(ushort addr, ushort value)
{
lock (_lock)
{
byte[] request = new byte[12];
BuildMBAPHeader(request, 6);
request[7] = 0x06;
request[8] = (byte)(addr >> 8);
request[9] = (byte)(addr & 0xFF);
request[10] = (byte)(value >> 8);
request[11] = (byte)(value & 0xFF);
byte[] response = SendAndReceive(request);
if (response[7] != 0x06)
throw new ModbusException(response[7], response[8]);
}
}
/// <summary>
/// 写单个线圈(功能码05)- 写M/Y继电器
/// </summary>
public void WriteSingleCoil(ushort addr, bool value)
{
lock (_lock)
{
byte[] request = new byte[12];
BuildMBAPHeader(request, 6);
request[7] = 0x05;
request[8] = (byte)(addr >> 8);
request[9] = (byte)(addr & 0xFF);
request[10] = (byte)(value ? 0xFF : 0x00);
request[11] = 0x00;
byte[] response = SendAndReceive(request);
if (response[7] != 0x05)
throw new ModbusException(response[7], response[8]);
}
}
/// <summary>
/// 写多个寄存器(功能码16)- 批量写D寄存器
/// </summary>
public void WriteMultipleRegisters(ushort startAddr, ushort[] values)
{
lock (_lock)
{
byte byteCount = (byte)(values.Length * 2);
byte[] request = new byte[13 + byteCount];
BuildMBAPHeader(request, (ushort)(7 + byteCount));
request[7] = 0x10;
request[8] = (byte)(startAddr >> 8);
request[9] = (byte)(startAddr & 0xFF);
request[10] = (byte)(values.Length >> 8);
request[11] = (byte)(values.Length & 0xFF);
request[12] = byteCount;
for (int i = 0; i < values.Length; i++)
{
request[13 + i * 2] = (byte)(values[i] >> 8);
request[13 + i * 2 + 1] = (byte)(values[i] & 0xFF);
}
byte[] response = SendAndReceive(request);
if (response[7] != 0x10)
throw new ModbusException(response[7], response[8]);
}
}
/// <summary>
/// 写多个线圈(功能码15)- 批量写M/Y继电器
/// </summary>
public void WriteMultipleCoils(ushort startAddr, bool[] values)
{
lock (_lock)
{
byte byteCount = (byte)((values.Length + 7) / 8);
byte[] request = new byte[13 + byteCount];
BuildMBAPHeader(request, (ushort)(7 + byteCount));
request[7] = 0x0F;
request[8] = (byte)(startAddr >> 8);
request[9] = (byte)(startAddr & 0xFF);
request[10] = (byte)(values.Length >> 8);
request[11] = (byte)(values.Length & 0xFF);
request[12] = byteCount;
for (int i = 0; i < values.Length; i++)
{
if (values[i])
request[13 + i / 8] |= (byte)(1 << (i % 8));
}
byte[] response = SendAndReceive(request);
if (response[7] != 0x0F)
throw new ModbusException(response[7], response[8]);
}
}
// 构建读请求
private byte[] BuildReadRequest(byte funcCode, ushort startAddr, ushort count)
{
byte[] request = new byte[12];
BuildMBAPHeader(request, 6);
request[7] = funcCode;
request[8] = (byte)(startAddr >> 8);
request[9] = (byte)(startAddr & 0xFF);
request[10] = (byte)(count >> 8);
request[11] = (byte)(count & 0xFF);
return request;
}
// 构建MBAP头
private void BuildMBAPHeader(byte[] buffer, ushort length)
{
_transactionId++;
buffer[0] = (byte)(_transactionId >> 8);
buffer[1] = (byte)(_transactionId & 0xFF);
buffer[2] = 0x00;
buffer[3] = 0x00;
buffer[4] = (byte)(length >> 8);
buffer[5] = (byte)(length & 0xFF);
buffer[6] = SlaveId;
}
// 发送并接收
private byte[] SendAndReceive(byte[] request)
{
if (_stream == null || !_client.Connected)
throw new InvalidOperationException("未连接到PLC");
_stream.Write(request, 0, request.Length);
_stream.Flush();
byte[] buffer = new byte[260];
int bytesRead = _stream.Read(buffer, 0, buffer.Length);
byte[] response = new byte[bytesRead];
Array.Copy(buffer, response, bytesRead);
return response;
}
public void Dispose()
{
Disconnect();
}
}
public class ModbusException : Exception
{
public byte FunctionCode { get; }
public byte ExceptionCode { get; }
public ModbusException(byte funcCode, byte exceptionCode)
: base($"Modbus异常:功能码=0x{funcCode:X2},异常码=0x{exceptionCode:X2}")
{
FunctionCode = funcCode;
ExceptionCode = exceptionCode;
}
}
}
2.2 台达PLC专用操作类
using System;
namespace DeltaPlcModbus
{
/// <summary>
/// 台达PLC专用操作类,封装D/M/X/Y软元件的读写
/// </summary>
public class DeltaPlcClient
{
private readonly ModbusTcpClient _modbus;
public DeltaPlcClient(string ip, int port = 502, byte slaveId = 1)
{
_modbus = new ModbusTcpClient(ip, port);
_modbus.SlaveId = slaveId;
}
public bool Connect() => _modbus.Connect();
public void Disconnect() => _modbus.Disconnect();
public bool IsConnected => _modbus.IsConnected;
/// <summary>
/// 读取D寄存器值
/// </summary>
public ushort ReadD(int address)
{
return _modbus.ReadHoldingRegisters((ushort)address, 1)[0];
}
/// <summary>
/// 批量读取D寄存器
/// </summary>
public ushort[] ReadD(int startAddress, int count)
{
return _modbus.ReadHoldingRegisters((ushort)startAddress, (ushort)count);
}
/// <summary>
/// 写入D寄存器值
/// </summary>
public void WriteD(int address, ushort value)
{
_modbus.WriteSingleRegister((ushort)address, value);
}
/// <summary>
/// 批量写入D寄存器
/// </summary>
public void WriteD(int startAddress, ushort[] values)
{
_modbus.WriteMultipleRegisters((ushort)startAddress, values);
}
/// <summary>
/// 读取32位整数(占用2个D寄存器)
/// </summary>
public int ReadD32(int startAddress)
{
ushort[] regs = _modbus.ReadHoldingRegisters((ushort)startAddress, 2);
return (regs[0] << 16) | regs[1];
}
/// <summary>
/// 读取32位浮点数(占用2个D寄存器,ABCD大端序)
/// </summary>
public float ReadDFloat(int startAddress)
{
ushort[] regs = _modbus.ReadHoldingRegisters((ushort)startAddress, 2);
// ABCD大端序:高字在前,高字节在前
uint bits = ((uint)regs[0] << 16) | regs[1];
byte[] bytes = BitConverter.GetBytes(bits);
// BitConverter.GetBytes返回的是小端序数组,需要反转
if (BitConverter.IsLittleEndian) Array.Reverse(bytes);
return BitConverter.ToSingle(bytes, 0);
}
/// <summary>
/// 读取32位浮点数(CDAB字交换序)
/// </summary>
public float ReadDFloatCDAB(int startAddress)
{
ushort[] regs = _modbus.ReadHoldingRegisters((ushort)startAddress, 2);
// CDAB:低字在前,高字节在前
uint bits = ((uint)regs[1] << 16) | regs[0];
byte[] bytes = BitConverter.GetBytes(bits);
if (BitConverter.IsLittleEndian) Array.Reverse(bytes);
return BitConverter.ToSingle(bytes, 0);
}
/// <summary>
/// 读取M辅助继电器状态
/// </summary>
public bool ReadM(int address)
{
return _modbus.ReadCoils((ushort)address, 1)[0];
}
/// <summary>
/// 批量读取M辅助继电器
/// </summary>
public bool[] ReadM(int startAddress, int count)
{
return _modbus.ReadCoils((ushort)startAddress, (ushort)count);
}
/// <summary>
/// 写入M辅助继电器
/// </summary>
public void WriteM(int address, bool value)
{
_modbus.WriteSingleCoil((ushort)address, value);
}
/// <summary>
/// 读取X输入继电器状态
/// 注意:X输入在台达PLC中Modbus地址偏移为2048
/// </summary>
public bool ReadX(int address)
{
return _modbus.ReadDiscreteInputs((ushort)(address + 2048), 1)[0];
}
/// <summary>
/// 读取Y输出继电器状态
/// 注意:Y输出在台达PLC中Modbus地址偏移为2048
/// </summary>
public bool ReadY(int address)
{
return _modbus.ReadCoils((ushort)(address + 2048), 1)[0];
}
/// <summary>
/// 写入Y输出继电器
/// </summary>
public void WriteY(int address, bool value)
{
_modbus.WriteSingleCoil((ushort)(address + 2048), value);
}
/// <summary>
/// 读取定时器当前值(T寄存器,偏移1024)
/// </summary>
public ushort ReadT(int address)
{
return _modbus.ReadHoldingRegisters((ushort)(address + 1024), 1)[0];
}
/// <summary>
/// 读取计数器当前值(C寄存器,偏移1536)
/// </summary>
public ushort ReadC(int address)
{
return _modbus.ReadHoldingRegisters((ushort)(address + 1536), 1)[0];
}
}
}
三、完整使用示例
using System;
using System.Threading;
namespace DeltaPlcModbus
{
class Program
{
static void Main(string[] args)
{
// 创建台达PLC客户端
var plc = new DeltaPlcClient("192.168.1.10", 502, slaveId: 1);
try
{
// 连接PLC
if (!plc.Connect())
{
Console.WriteLine("无法连接到PLC");
return;
}
Console.WriteLine("PLC连接成功!\n");
// 1. 读取D寄存器
Console.WriteLine("=== 读取D寄存器 ===");
ushort d0 = plc.ReadD(0);
ushort d1 = plc.ReadD(1);
ushort d2 = plc.ReadD(2);
Console.WriteLine($"D0 = {d0}");
Console.WriteLine($"D1 = {d1}");
Console.WriteLine($"D2 = {d2}");
// 批量读取
ushort[] dValues = plc.ReadD(0, 10);
Console.WriteLine("\nD0-D9:");
for (int i = 0; i < dValues.Length; i++)
{
Console.WriteLine($" D{i} = {dValues[i]}");
}
// 2. 写入D寄存器
Console.WriteLine("\n=== 写入D寄存器 ===");
plc.WriteD(10, 1234);
Console.WriteLine("D10 写入 1234");
ushort check = plc.ReadD(10);
Console.WriteLine($"D10 回读 = {check}");
// 批量写入
plc.WriteD(20, new ushort[] { 100, 200, 300, 400, 500 });
Console.WriteLine("D20-D24 批量写入完成");
// 3. 读取32位数据
Console.WriteLine("\n=== 读取32位数据 ===");
int d32 = plc.ReadD32(0);
Console.WriteLine($"D0-D1(32位整数)= {d32}");
// 4. 读取M继电器
Console.WriteLine("\n=== 读取M继电器 ===");
bool m0 = plc.ReadM(0);
bool m1 = plc.ReadM(1);
Console.WriteLine($"M0 = {(m0 ? "ON" : "OFF")}");
Console.WriteLine($"M1 = {(m1 ? "ON" : "OFF")}");
// 批量读取
bool[] mValues = plc.ReadM(0, 8);
Console.WriteLine("\nM0-M7:");
for (int i = 0; i < mValues.Length; i++)
{
Console.WriteLine($" M{i} = {(mValues[i] ? "ON" : "OFF")}");
}
// 5. 写入M继电器
Console.WriteLine("\n=== 写入M继电器 ===");
plc.WriteM(0, true);
Console.WriteLine("M0 置为 ON");
Thread.Sleep(500);
plc.WriteM(0, false);
Console.WriteLine("M0 置为 OFF");
// 6. 读取X输入
Console.WriteLine("\n=== 读取X输入 ===");
bool x0 = plc.ReadX(0);
bool x1 = plc.ReadX(1);
Console.WriteLine($"X0 = {(x0 ? "ON" : "OFF")}");
Console.WriteLine($"X1 = {(x1 ? "ON" : "OFF")}");
// 7. 读取Y输出
Console.WriteLine("\n=== 读取Y输出 ===");
bool y0 = plc.ReadY(0);
Console.WriteLine($"Y0 = {(y0 ? "ON" : "OFF")}");
// 8. 控制Y输出
Console.WriteLine("\n=== 控制Y输出 ===");
plc.WriteY(0, true);
Console.WriteLine("Y0 置为 ON");
Thread.Sleep(1000);
plc.WriteY(0, false);
Console.WriteLine("Y0 置为 OFF");
// 9. 读取定时器/计数器触点状态
Console.WriteLine("\n=== 读取定时器/计数器触点 ===");
bool t0 = plc.ReadTContact(0);
bool c0 = plc.ReadCContact(0);
Console.WriteLine($"T0 触点 = {(t0 ? "ON" : "OFF")}");
Console.WriteLine($"C0 触点 = {(c0 ? "ON" : "OFF")}");
Console.WriteLine("注:定时器/计数器当前值请查阅对应型号手册的Modbus映射");
// 10. 持续监控(10秒)
Console.WriteLine("\n=== 持续监控D0-D3(10秒)===");
for (int i = 0; i < 10; i++)
{
ushort[] monitor = plc.ReadD(0, 4);
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] D0={monitor[0]}, D1={monitor[1]}, D2={monitor[2]}, D3={monitor[3]}");
Thread.Sleep(1000);
}
}
catch (ModbusException ex)
{
Console.WriteLine($"Modbus异常:{ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"系统异常:{ex.Message}");
}
finally
{
plc.Disconnect();
Console.WriteLine("\nPLC连接已关闭");
}
Console.WriteLine("\n按任意键退出...");
Console.ReadKey();
}
}
}
四、Windows Forms上位机界面示例
using System;
using System.Windows.Forms;
using System.Threading;
namespace DeltaPlcModbus
{
public partial class MainForm : Form
{
private DeltaPlcClient _plc;
private Thread _monitorThread;
private volatile bool _monitoring = false;
public MainForm()
{
InitializeComponent();
}
private void btnConnect_Click(object sender, EventArgs e)
{
try
{
_plc = new DeltaPlcClient(txtIp.Text, int.Parse(txtPort.Text), byte.Parse(txtSlaveId.Text));
if (_plc.Connect())
{
lblStatus.Text = "已连接";
lblStatus.ForeColor = System.Drawing.Color.Green;
btnConnect.Enabled = false;
btnDisconnect.Enabled = true;
_monitoring = true;
_monitorThread = new Thread(MonitorLoop);
_monitorThread.IsBackground = true;
_monitorThread.Start();
}
else
{
MessageBox.Show("连接失败!");
}
}
catch (Exception ex)
{
MessageBox.Show($"连接异常:{ex.Message}");
}
}
private void btnDisconnect_Click(object sender, EventArgs e)
{
_monitoring = false;
_plc?.Disconnect();
lblStatus.Text = "未连接";
lblStatus.ForeColor = System.Drawing.Color.Red;
btnConnect.Enabled = true;
btnDisconnect.Enabled = false;
}
private void MonitorLoop()
{
while (_monitoring && _plc.IsConnected)
{
try
{
// 读取D0-D9
ushort[] values = _plc.ReadD(0, 10);
// 读取M0-M7
bool[] mValues = _plc.ReadM(0, 8);
// 更新UI(跨线程调用)
this.Invoke(new Action(() =>
{
txtD0.Text = values[0].ToString();
txtD1.Text = values[1].ToString();
txtD2.Text = values[2].ToString();
txtD3.Text = values[3].ToString();
lblM0.Text = mValues[0] ? "ON" : "OFF";
lblM1.Text = mValues[1] ? "ON" : "OFF";
lblM2.Text = mValues[2] ? "ON" : "OFF";
lblM3.Text = mValues[3] ? "ON" : "OFF";
}));
}
catch
{
// 读取失败,忽略
}
Thread.Sleep(500);
}
}
private void btnWriteD_Click(object sender, EventArgs e)
{
try
{
int addr = int.Parse(txtWriteAddr.Text);
ushort value = ushort.Parse(txtWriteValue.Text);
_plc.WriteD(addr, value);
MessageBox.Show($"D{addr} 写入 {value} 成功");
}
catch (Exception ex)
{
MessageBox.Show($"写入失败:{ex.Message}");
}
}
private void btnM0On_Click(object sender, EventArgs e)
{
_plc.WriteM(0, true);
}
private void btnM0Off_Click(object sender, EventArgs e)
{
_plc.WriteM(0, false);
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
_monitoring = false;
_plc?.Disconnect();
base.OnFormClosing(e);
}
}
}
五、常见问题与排查
5.1 连接失败
- 检查IP地址:确认PLC的IP地址设置正确,与电脑在同一网段
- 检查端口:台达PLC Modbus TCP默认端口502,确认未被防火墙拦截
- 检查网线:确认网线连接正常,PLC网口指示灯亮
- 检查PLC设置:在ISPSoft中确认PLC已启用Modbus TCP通讯
- Ping测试:在命令行中ping PLC的IP地址,确认网络连通
5.2 读取数据为0或异常
- 地址错误:确认Modbus地址与PLC软元件地址的对应关系,注意偏移量
- 从站地址错误:确认SlaveId与PLC中设置的一致
- 功能码不支持:部分台达PLC型号可能不支持某些功能码
- 地址越界:确认读取的地址范围在PLC支持的范围内
- PLC未运行:确认PLC处于RUN模式,STOP模式下部分地址不可读
5.3 写入不生效
- 寄存器被程序覆盖:PLC程序中可能正在写入该地址,导致写入被覆盖
- 地址只读:部分寄存器(如X输入)是只读的,不能写入
- 权限限制:部分PLC需要设置通讯写入权限
- 数据类型错误:确认写入的数据类型与PLC中定义的一致
六、最佳实践
- 批量读取:尽量一次读取多个连续寄存器,减少通讯次数,提高效率
- 合理轮询间隔:建议轮询间隔不小于100ms,避免过快轮询导致PLC通讯过载
- 断线重连:添加自动重连机制,网络恢复后自动恢复通讯
- 超时设置:合理设置通讯超时,建议2-3秒
- 异常处理:捕获ModbusException,区分不同异常码进行处理
- 线程安全:多线程访问时使用lock保护,避免并发冲突
- 日志记录:记录通讯日志,便于排查问题
- 地址规划:提前规划D/M寄存器的使用,避免地址冲突
本文提供的C#代码实现了台达PLC的完整Modbus TCP通讯功能,包括D寄存器、M继电器、X输入、Y输出、定时器、计数器的读写操作,以及Windows Forms上位机界面示例。代码可直接用于工业自动化项目开发,建议先使用台达ISPSoft软件监控PLC数据,确认通讯正常后再集成到实际项目中。
发表回复