Commit fde99e6e by 何阳

实现了FTP代理转发的功能

parent 3aa8de5c
......@@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.6.33815.320
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CaptureAgent", "CaptureAgent\CaptureAgent.csproj", "{039679C3-A869-4F7B-9540-E875EF016323}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FtpAgent", "FtpAgent\FtpAgent.csproj", "{3759FB30-4BFC-4864-A671-477776057F04}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
......@@ -11,10 +11,10 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{039679C3-A869-4F7B-9540-E875EF016323}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{039679C3-A869-4F7B-9540-E875EF016323}.Debug|Any CPU.Build.0 = Debug|Any CPU
{039679C3-A869-4F7B-9540-E875EF016323}.Release|Any CPU.ActiveCfg = Release|Any CPU
{039679C3-A869-4F7B-9540-E875EF016323}.Release|Any CPU.Build.0 = Release|Any CPU
{3759FB30-4BFC-4864-A671-477776057F04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3759FB30-4BFC-4864-A671-477776057F04}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3759FB30-4BFC-4864-A671-477776057F04}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3759FB30-4BFC-4864-A671-477776057F04}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
......
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net7.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
\ No newline at end of file
namespace CaptureAgent
{
partial class Main
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(800, 450);
this.Text = "Form1";
}
#endregion
}
}
\ No newline at end of file
namespace CaptureAgent
{
public partial class Main : Form
{
public Main()
{
InitializeComponent();
}
}
}
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>
\ No newline at end of file
namespace CaptureAgent
{
internal static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
// To customize application configuration such as set high DPI settings or default font,
// see https://aka.ms/applicationconfiguration.
ApplicationConfiguration.Initialize();
Application.Run(new Main());
}
}
}
\ No newline at end of file
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace FtpAgent
{
public class ApiError
{
public int Code { get; set; }
public string? Msg { get; set; }
}
}
using CSharpFunctionalExtensions;
using Rougamo;
using Rougamo.Context;
using Serilog;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace FtpAgent
{
[System.AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
public class ErrorHandlerAttribute<T> : MoAttribute
{
public ErrorHandlerAttribute(string msg)
{
Msg = msg;
}
public string Msg { get; }
public override void OnException(MethodContext context)
{
Log.Error(context.Exception, Msg);
context.HandledException(this, returnValue:Result.Failure<T,ApiError>( new ApiError()
{
Code = 600,
Msg = $"{Msg} 程序错误:{context?.Exception?.Message}"
}));
}
}
}
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Rougamo />
</Weavers>
\ No newline at end of file
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CSharpFunctionalExtensions" Version="2.40.3" />
<PackageReference Include="FubarDev.FtpServer" Version="3.1.2" />
<PackageReference Include="FubarDev.FtpServer.FileSystem.DotNet" Version="3.1.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Rougamo.Fody" Version="2.1.1" />
<PackageReference Include="Serilog.Formatting.Compact" Version="2.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="Serilog.Sinks.Trace" Version="3.0.0" />
</ItemGroup>
</Project>
using Serilog;
using Serilog.Formatting.Compact;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CaptureAgent
{
public static class Logging
{
public static void InitLogging()
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.Enrich.FromLogContext()
.WriteTo.File(
path: "log/log-.log",
rollingInterval: RollingInterval.Day,
fileSizeLimitBytes: 1_000_000,
rollOnFileSizeLimit: true,
outputTemplate: "[{Timestamp:HH:mm:ss.fff}] [{Level}] {Message:lj}{NewLine} {Exception}")
.WriteTo.Trace(outputTemplate: "[{Timestamp:HH:mm:ss.fff}] [{Level}] {Message:lj}{NewLine} {Exception}")
.WriteTo.Console( outputTemplate: "[{Timestamp:HH:mm:ss.fff}] [{Level}] {Message:lj}{NewLine} {Exception}")
.CreateLogger();
Serilog.Debugging.SelfLog.Enable(Console.Error);
}
}
}
using CSharpFunctionalExtensions;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
namespace FtpAgent
{
public static class MultipartFormDataContentEx
{
public static void AddFile(this MultipartFormDataContent @this, string fileName )
{
@this.Headers.Add("ContentType", "multipart/form-data");
var ocrFileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
Byte[] bytes = new Byte[ocrFileStream.Length];
ocrFileStream.Read(bytes, 0, bytes.Length);
ocrFileStream.Close();
ByteArrayContent contents = new ByteArrayContent(bytes);
contents.Headers.ContentType = new MediaTypeHeaderValue("image/jpg");
@this.Add(contents, "ocrFile", Path.GetFileName(fileName));
}
public static void AddJson(this MultipartFormDataContent @this, object obj)
{
string jsondata = JsonConvert.SerializeObject(obj, Formatting.None, new JsonSerializerSettings()
{ NullValueHandling = NullValueHandling.Ignore });
Dictionary<string, string> jsondic = JsonConvert.DeserializeObject<Dictionary<string, string>>(jsondata);
foreach (var keyValuePair in jsondic)
{
@this.Add(new StringContent(keyValuePair.Value, Encoding.UTF8, "text/plain"), keyValuePair.Key);
}
}
}
}
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace FtpAgent
{
public static class ObjectEx
{
public static string ToJson(this object @this)
{
return JsonConvert.SerializeObject(@this, Formatting.Indented);
}
}
}
// See https://aka.ms/new-console-template for more information
using Newtonsoft.Json;
public class OcrRequest
{
/// <summary>
/// 客户名称
/// </summary>
public string customerName { get; set; } = "无境创新";
/// <summary>
/// 设备号
/// </summary>
public string deviceNo { get; set; } = "CS";
/// <summary>
/// 识别图片,类型body
/// </summary>
[JsonIgnore]
public string ocrFile { get; set; }
public OcrRequest(string ocrFile)
{
this.ocrFile = ocrFile;
uniqueNo = Guid.NewGuid().ToString();
}
/// <summary>
/// 识别类型:1-脊柱、2-创伤、3-棒版、4-合格证照片
/// </summary>
public Int32 ocrType { get; set; } = 4;
/// <summary>
/// 订单号
/// </summary>
public string orderNo { get; set; } = "1";
/// <summary>
/// 订单类型:1-收货、2-换货
/// </summary>
public Int32 orderType { get; set; } = 1;
/// <summary>
/// 拍照模式:1=拍照;2=重拍;3=连拍
/// </summary>
public Int32 photoMode { get; set; } = 1;
/// <summary>
/// 秘钥;固定值
/// </summary>
public string secret { get; set; } = "knz2r23rdhnbnhc7zr9b9ux7evkvzvdm";
/// <summary>
/// 租户ID
/// </summary>
public string tenantId { get; set; } = "1";
/// <summary>
/// 拍照唯一键
/// </summary>
public string uniqueNo { get; set; }
}
\ No newline at end of file
// See https://aka.ms/new-console-template for more information
public partial class OcrServer
{
public class LinesItem
{
/// <summary>
/// 返回文本内容
/// </summary>
public string? text { get; set; }
}
/// <summary>
/// /识别数据类
/// </summary>
public class OcrResultData
{
/// <summary>
/// OCR识别结果
/// </summary>
public List<LinesItem> ?lines { get; set; }
/// <summary>
/// OCR记录ID,后续需要传输匹配接口和提交结果
/// </summary>
public long ocrLogId { get; set; }
}
}
\ No newline at end of file
// See https://aka.ms/new-console-template for more information
using Newtonsoft.Json;
using static System.Runtime.InteropServices.JavaScript.JSType;
using System.Net.Http.Headers;
using System.Text;
using Serilog;
using CSharpFunctionalExtensions;
using CSharpFunctionalExtensions.ValueTasks;
using FtpAgent;
using static OcrServer;
public partial class OcrServer
{
public static string OCR_SERVER_URL { get; private set; } = "https://hardware-test.guke.tech";
public static string OCR_API_URL { get; private set; } = "/api/service/ocr/v2/recognize";
[ErrorHandler<OcrResultData>("上传图片到OCR服务器失败!")]
public static async Task<Result<OcrResultData, ApiError>> UploadFile(OcrRequest result)
{
string url = $"{OCR_SERVER_URL}{OCR_API_URL}?ocrChannel=1&ocrType=4&remark=certificate test";
using (var httpclient = new HttpClient())
{
var formData = new MultipartFormDataContent();
formData.AddFile(result.ocrFile);
formData.AddJson(result);
Log.Information("Post请求:{url},上传文件:{file},请求参数:", url, result.ocrFile);
await LogFormDataContent(formData);
var response = await httpclient.PostAsync(url, formData);
var responseContent = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
Log.Error($"请求远程OCR服务器失败,失败code=${response.StatusCode}, msg={responseContent}");
return Result.Failure<OcrResultData, ApiError>(new ApiError() { Code = (int)response.StatusCode, Msg = responseContent });
}
Log.Information("Post请求:{url},请求结果:{responseContent}", url, responseContent);
var respond = JsonConvert.DeserializeObject<Respond<OcrResultData>>(responseContent);
if (respond.code != 200)
{
Log.Error($"请求远程OCR API失败,失败code=${respond.code}, msg={respond.detail}");
return Result.Failure<OcrResultData, ApiError>(new ApiError() { Code = respond.code, Msg = respond.msg }) ;
}
var test = respond?.data?.lines.Select(item => item.text).Aggregate((l, f) => l + f);
Log.Information($"返回结果文本长度:{test.Length}");
return Result.Success<OcrResultData, ApiError>(respond.data);
}
}
public static async Task LogFormDataContent(MultipartFormDataContent formData)
{
var logData = new Dictionary<string, object>();
foreach (var content in formData)
{
ContentDispositionHeaderValue disposition = content.Headers.ContentDisposition;
string name = disposition.Name?.Trim('"');
string? fileName = disposition.FileName?.Trim('"');
if (content is StringContent || content.Headers.ContentType?.MediaType == "text/plain")
{
string value = await content.ReadAsStringAsync();
logData.Add(name, value);
}
else if (fileName != null)
{
logData.Add(name, $"(File: {fileName})");
}
else
{
logData.Add(name, $"(Content-Type: {content.Headers.ContentType})");
}
}
string json = JsonConvert.SerializeObject(logData, Formatting.Indented);
Log.Information("Form Data Content: {FormData}", json);
}
}
\ No newline at end of file
// See https://aka.ms/new-console-template for more information
using CaptureAgent;
using FubarDev.FtpServer;
using FubarDev.FtpServer.FileSystem.DotNet;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
HashSet<string> FileChangeCnt = new();
Logging.InitLogging();
// Setup dependency injection
var services = new ServiceCollection();
// use %TEMP%/TestFtpServer as root folder
services.Configure<DotNetFileSystemOptions>(opt => opt
.RootPath = Path.Combine(Environment.CurrentDirectory, "Test"));
// Add FTP server services
// DotNetFileSystemProvider = Use the .NET file system functionality
// AnonymousMembershipProvider = allow only anonymous logins
services.AddFtpServer(builder => builder
.UseDotNetFileSystem() // Use the .NET file system functionality
.EnableAnonymousAuthentication()); // allow anonymous logins
// Configure the FTP server
services.Configure<FtpServerOptions>(opt =>
{
opt.ServerAddress = "127.0.0.1";
opt.Port = 9527;
});
// Build the service provider
using (var serviceProvider = services.BuildServiceProvider())
{
Log.Information("开始运行FTP服务");
Directory.CreateDirectory("Test");
// Initialize the FTP server
var ftpServerHost = serviceProvider.GetRequiredService<IFtpServerHost>();
// 设置文件系统监控器
var watcher = new FileSystemWatcher("Test");
// 监控的文件类型
watcher.Filter = "*.*";
// 添加事件处理器
watcher.Created += OnFileCreated;
watcher.Changed += Watcher_Changed;
Log.Information("添加文件监控器");
// 开始监控
watcher.EnableRaisingEvents = true;
// Start the FTP server
await ftpServerHost.StartAsync(CancellationToken.None);
Console.WriteLine("输入q退出");
while (Console.ReadKey().Key != ConsoleKey.Q)
{
}
Log.Information("停止FTP服务");
// Stop the FTP server
ftpServerHost?.StopAsync(CancellationToken.None).Wait();
watcher.Dispose();
}
async void Watcher_Changed(object sender, FileSystemEventArgs e)
{
// FTP文件被修改两次,才算完成上传
if (!FileChangeCnt.TryGetValue(e.Name, out string res))
{
FileChangeCnt.Add(e.Name);
return;
}
Log.Information("{Name} 文件接收完成!",e.Name);
//如果被修改的是jpg文件,则转发到OCR服务器
if (Path.GetExtension(e.Name) == ".jpg")
{
Log.Information("{Name} 文件转发到服务器", e.Name);
OcrRequest result = new OcrRequest(e.FullPath);
await OcrServer.UploadFile(result);
}
FileChangeCnt.Remove(e.Name);
}
void OnFileCreated(object sender, FileSystemEventArgs e)
{
Log.Information($"文件:{e.Name} 被上传");
}
// See https://aka.ms/new-console-template for more information
public partial class OcrServer
{
public record Respond<T> where T: class
{
public Respond(int code, string msg, T data)
{
this.code = code;
this.msg = msg;
this.data = data;
}
/// <summary>
/// 状态码
/// </summary>
public int code { get; set; }
/// <summary>
/// 错误信息
/// </summary>
public string msg { get; set; }
/// <summary>
/// 成功状态
/// </summary>
public bool success { get; set; }
/// <summary>
/// OCR识别结果
/// </summary>
public T data { get; set; }
/// <summary>
/// 异常信息
/// </summary>
public string ?detail { get; set; }
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment