添加项目文件。

This commit is contained in:
2025-12-02 13:10:10 +08:00
parent 93a2382a16
commit 289aa4cbe7
400 changed files with 91177 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="IdGen" Version="3.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.3.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,188 @@
namespace Atomx.Utils.Extension
{
public static class DateTimeExtension
{
/// <summary>
/// 时间距离现在多久
/// </summary>
/// <param name="time"></param>
/// <returns></returns>
public static string DateFromNow(this DateTime time)
{
if (time == DateTime.MinValue)
{
return string.Empty;
}
TimeSpan timeSpan = DateTime.Now - time;
if (Math.Floor(timeSpan.TotalDays) > 365)
{
return Math.Floor(timeSpan.TotalDays) / 365 + "年前";
}
else if (Math.Floor(timeSpan.TotalDays) > 30)
{
return Math.Floor(timeSpan.TotalDays) / 30 + "月前";
}
else if (Math.Floor(timeSpan.TotalDays) > 7)
{
return Math.Floor(timeSpan.TotalDays) / 7 + "周前";
}
else if (Math.Floor(timeSpan.TotalDays) > 1)
{
return Math.Floor(timeSpan.TotalDays) + "天前";
}
else if (Math.Floor(timeSpan.TotalHours) > 1)
{
return Math.Floor(timeSpan.TotalHours) + "小时前";
}
else if (Math.Floor(timeSpan.TotalMinutes) > 1)
{
return Math.Floor(timeSpan.TotalMinutes) + "分钟前";
}
else
{
return Math.Floor(timeSpan.TotalSeconds) + "秒前";
}
}
/// <summary>
/// 字符串时间转换DateTime
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
public static DateTime ToDateTime(this string text)
{
DateTime.TryParse(text, out DateTime time);
return time;
}
/// <summary>
/// 字符串时间转换DateTime
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
public static DateTime ToUtcDateTime(this string text)
{
var state = DateTime.TryParse(text, out DateTime time);
if (state)
{
return DateTime.SpecifyKind(time, DateTimeKind.Utc);
}
return DateTime.MinValue;
}
/// <summary>
/// 获取当月第一天0时0分0秒
/// </summary>
/// <returns></returns>
public static DateTime GetMonthStartDate()
{
return DateTime.Now.AddDays(1 - DateTime.Now.Day).Date;
}
/// <summary>
/// 获取当月最后一天23时59分59秒
/// </summary>
/// <returns></returns>
public static DateTime GetMonthEndDate()
{
return DateTime.Now.AddDays(1 - DateTime.Now.Day).Date.AddMonths(1).AddSeconds(-1);
}
/// <summary>
/// 获取当前时间的周一开始时间
/// </summary>
/// <returns></returns>
public static DateTime GetWeekStartDate()
{
DateTime today = DateTime.Today;
return today.AddDays(-(int)today.DayOfWeek + (int)DayOfWeek.Monday).Date;
}
/// <summary>
/// 获取当前时间所在的周的最后一天结束时间
/// </summary>
/// <returns></returns>
public static DateTime GetWeekEndDate()
{
DateTime today = DateTime.Today;
return today.AddDays(-(int)today.DayOfWeek + (int)DayOfWeek.Monday).AddDays(7).AddSeconds(-1);
}
/// <summary>
/// 转换成每日开始时间
/// </summary>
/// <param name="time"></param>
/// <returns></returns>
public static DateTime ConvertStartDateTime(this DateTime time)
{
return time.Date;
}
/// <summary>
/// 转换成每日开始UTC时间
/// </summary>
/// <param name="time"></param>
/// <returns></returns>
public static DateTime ConvertStartDateUTCTime(this DateTime time)
{
return DateTime.SpecifyKind(time.Date, DateTimeKind.Utc);
}
/// <summary>
/// 转换成每日结束时间
/// </summary>
/// <param name="time"></param>
/// <returns></returns>
public static DateTime ConvertEndDateTime(this DateTime time)
{
return time.Date.AddDays(1).AddSeconds(-1);
}
/// <summary>
/// 转换成每日结束UTC时间
/// </summary>
/// <param name="time"></param>
/// <returns></returns>
public static DateTime ConvertEndDateUTCTime(this DateTime time)
{
return DateTime.SpecifyKind(time.Date.AddDays(1).AddSeconds(-1), DateTimeKind.Utc);
}
/// <summary>
/// long --> DateTime
/// </summary>
/// <param name="d"></param>
/// <returns></returns>
public static DateTime ConvertLongToDateTime(this long d)
{
DateTime dtStart = TimeZoneInfo.ConvertTimeToUtc(new DateTime(1970, 1, 1));
long lTime = long.Parse(d + "0000");
TimeSpan toNow = new TimeSpan(lTime);
DateTime dtResult = dtStart.Add(toNow);
return dtResult;
}
public static DateTime NumberToDateTime(this string number)
{
var year = number.Substring(0, 4);
//Console.WriteLine($"年:{year}");
var month = number.Substring(4, 2);
//Console.WriteLine($"月:{month}");
var day = number.Substring(6, 2);
//Console.WriteLine($"日:{day}");
var hour = number.Substring(8, 2);
//Console.WriteLine($"时:{hour}");
var minute = number.Substring(10, 2);
//Console.WriteLine($"分:{minute}");
var second = number.Substring(12);
//Console.WriteLine($"秒:{second}");
var datetime = $"{year}-{month}-{day} {hour}:{minute}:{second}";
DateTime date = DateTime.Parse(datetime);
return date;
}
}
}

View File

@@ -0,0 +1,37 @@
using System.Text;
namespace Atomx.Utils.Extension
{
public static class IdCodeExtension
{
private static readonly char[] Base62Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".ToCharArray();
/// <summary>
/// 将long类型ID编码为10位Base62邀请码
/// </summary>
public static string IdToCode(this long id)
{
ulong value = (ulong)id;
var sb = new StringBuilder();
for (int i = 0; i < 10; i++)
{
sb.Insert(0, Base62Chars[(int)(value % 62)]);
value /= 62;
}
return sb.ToString();
}
/// <summary>
/// 将10位Base62邀请码还原为long类型ID
/// </summary>
public static long CodeToId(this string code)
{
ulong value = 0;
foreach (var c in code)
{
value = value * 62 + (ulong)Array.IndexOf(Base62Chars, c);
}
return (long)value;
}
}
}

View File

@@ -0,0 +1,81 @@
namespace Atomx.Utils.Extension
{
public static class NumberExtension
{
/// <summary>
/// 字符串转 int64位数字,转换失败则返回0
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
public static long ToLong(this string text)
{
long.TryParse(text, out long num);
return num;
}
/// <summary>
/// 字符转Int32位数字转换失败则返回0
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
public static int ToInt(this string text)
{
int.TryParse(text, out int num);
return num;
}
public static decimal ToDecimal(this string text)
{
decimal.TryParse(text, out decimal num);
return num;
}
/// <summary>
/// 字符串转 int64位数字,转换失败则返回0
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
public static double ToDouble(this string text)
{
double.TryParse(text, out double num);
return num;
}
/// <summary>
/// 移除小数点后面的0
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static decimal RemoveZero(this decimal value)
{
var text = value.ToString();
text = text.TrimEnd('0').TrimStart('.');
return Convert.ToDecimal(text);
}
/// <summary>
/// 删除无意义的0
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static string RemoveTrailingZeros(this decimal value)
{
string str = value.ToString();
if (str.Contains('.'))
{
str = str.TrimEnd('0');
if (str.EndsWith("."))
{
str = str.Substring(0, str.Length - 1);
}
}
return str;
}
public static long DateToNumber(this DateTime data)
{
return data.ToString("yyyyMMddHHmmss").ToLong();
}
}
}

View File

@@ -0,0 +1,46 @@
using System.Reflection;
using System.Xml.Serialization;
namespace Atomx.Utils.Extension
{
public static class ObjectExtension
{
/// <summary>
/// 利用反射实现深拷贝
/// </summary>
/// <param name="_object"></param>
/// <returns></returns>
public static object DeepCopy(object _object)
{
Type T = _object.GetType();
object o = Activator.CreateInstance(T);
PropertyInfo[] PI = T.GetProperties();
for (int i = 0; i < PI.Length; i++)
{
PropertyInfo P = PI[i];
P.SetValue(o, P.GetValue(_object));
}
return o;
}
/// <summary>
/// 深度克隆对象
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="obj"></param>
/// <returns></returns>
public static T Clone<T>(this T obj)
{
T ret = default;
if (obj != null)
{
XmlSerializer cloner = new XmlSerializer(typeof(T));
MemoryStream stream = new MemoryStream();
cloner.Serialize(stream, obj);
stream.Seek(0, SeekOrigin.Begin);
ret = (T)cloner.Deserialize(stream);
}
return ret;
}
}
}

View File

@@ -0,0 +1,86 @@
using System.Text;
namespace Atomx.Utils.Extension
{
public static class PathExtension
{
/// <summary>
/// 是否是绝对路径
/// windows下判断 路径是否包含 ":"
/// Mac OS、Linux下判断 路径是否包含 "\"
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public static bool IsAbsolute(this string path)
{
return Path.VolumeSeparatorChar == ':' ? path.IndexOf(Path.VolumeSeparatorChar) > 0 : path.IndexOf('\\') > 0;
}
/// <summary>
/// 获取文件绝对路径
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public static string MapPath(string root, string path)
{
return path.IsAbsolute() ? path : Path.Combine(root, path.TrimStart('~', '/').Replace('/', Path.DirectorySeparatorChar));
}
/// <summary>
/// 检测指定路径是否存在
/// </summary>
/// <param name="path">路径</param>
/// <param name="isDirectory">是否是目录</param>
/// <returns></returns>
public static bool IsExist(string root, string path, bool isDirectory)
{
if (isDirectory)
{
return Directory.Exists(path.IsAbsolute() ? path : MapPath(root, path));
}
else
{
return File.Exists(path.IsAbsolute() ? path : MapPath(root, path));
}
}
/// <summary>
/// 根据指定目录,当前日期和扩展名,获取一个完整的存储路径和名称
/// </summary>
/// <param name="path">指定存储文件根目录,默认 upload</param>
/// <param name="fixedpath">是否固定存储在根目录,如果不固定,将自动根据时间生成子文件夹进行存储</param>
/// <param name="name">存储文件名,如果为空将自动生成文件名</param>
/// <param name="extension">文件存储后缀,不能为空</param>
/// <returns></returns>
public static string GetFileNameByData(string path, bool fixedpath, string name, string extension)
{
if (string.IsNullOrEmpty(extension)) throw new ArgumentNullException("extension");
if (!extension.StartsWith(".")) extension = "." + extension;
if (string.IsNullOrEmpty(path))
{
path = "/uploads";
}
if (path.StartsWith("/")) path = path.Substring(1, path.Length - 1);
if (path.EndsWith("/")) path = path.Substring(0, path.Length - 1);
StringBuilder fileName = new StringBuilder(path);
if (!fixedpath)
{
fileName.AppendFormat("/{0}/{1}/", DateTime.Now.ToString("yyyyMM"), DateTime.Now.ToString("dd"));
}
if (string.IsNullOrEmpty(name))
{
Random random = new Random(Guid.NewGuid().GetHashCode());
fileName.Append(DateTime.Now.ToString("yyyyMMddHHmmss") + random.Next(1000, 10000));
}
else
{
fileName.Append("/" + name);
}
return fileName.ToString() + extension;
}
}
}

View File

@@ -0,0 +1,60 @@
using System.Text.RegularExpressions;
namespace Atomx.Utils.Extension
{
public class RegexExtension
{
/// <summary>
/// 判断是否是电子邮件格式
/// </summary>
/// <param name="email"></param>
/// <returns></returns>
public static bool IsEmail(string email)
{
return Regex.IsMatch(email, @"^\S+@\S+\.\S+$");
}
/// <summary>
/// 判断手机号码是否符合规则,不考虑国家代码的情况下
/// </summary>
/// <param name="mobile"></param>
/// <returns></returns>
public static bool IsValidMobile(string mobile)
{
string pattern = "^\\d{7,15}$";
return Regex.IsMatch(mobile, pattern);
}
/// <summary>
/// 判断手机号码是否符合规则,不考虑国家代码的情况下
/// </summary>
/// <param name="mobile"></param>
/// <returns></returns>
public static bool IsValidGlobalMobile(string mobile)
{
// 正则表达式规则解释:
// ^ 开始
// \\+ 匹配一个加号 (E.164要求号码以加号开始)
// \\d{1,3} 匹配1到3位数字即国家代码
// \\d{7,15} 匹配7到15位数字即电话号码
// $ 结束
string pattern = "^\\+\\d{1,3}\\d{7,15}$";
return Regex.IsMatch(mobile, pattern);
}
/// <summary>
/// 验证账号是否是邮箱地址或者手机号
/// </summary>
/// <param name="account"></param>
/// <returns></returns>
public static bool IsValidAccount(string account)
{
string pattern = @"^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$";
if (Regex.IsMatch(account, pattern))
{
return true;
}
return IsValidMobile(account);
}
}
}

View File

@@ -0,0 +1,195 @@
using System.Security.Cryptography;
using System.Text;
using System.Web;
namespace Atomx.Utils.Extension
{
public static class StringExtension
{
// <summary>
/// MD5加密输出的字母是大写
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
public static string ToMD5(this string text)
{
MD5 md5 = MD5.Create();
return BitConverter.ToString(md5.ComputeHash(Encoding.UTF8.GetBytes(text))).Replace("-", "");
}
/// <summary>
/// 字符串加密为通用的密码
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
public static string ToMd5Password(this string text)
{
var md5 = text.ToMD5().ToMD5();
return md5;
}
/// <summary>
/// 随机数字字符串
/// </summary>
/// <param name="length"></param>
/// <returns></returns>
public static string GetRandomNumberString(int length)
{
string buffer = "0123456789";
StringBuilder sb = new StringBuilder();
Random r = new Random();
int range = buffer.Length;
for (var i = 0; i < length; i++)
{
sb.Append(buffer.Substring(r.Next(range), 1));
}
return sb.ToString();
}
/// <summary>
/// 随机数字字符串
/// </summary>
/// <param name="length"></param>
/// <returns></returns>
public static string GetRandomString(int length)
{
string buffer = "abcdefghijklmnopqrstuvwxyz";
StringBuilder sb = new StringBuilder();
Random r = new Random();
int range = buffer.Length;
for (var i = 0; i < length; i++)
{
sb.Append(buffer.Substring(r.Next(range), 1));
}
return sb.ToString();
}
/// <summary>
/// 获取URL参数字段值
/// </summary>
/// <param name="queryString"></param>
/// <param name="paramName"></param>
/// <returns></returns>
public static string GetQueryString(this string queryString, string paramName)
{
if (string.IsNullOrEmpty(queryString) || string.IsNullOrEmpty(paramName))
{
return "";
}
string paramValue = HttpUtility.ParseQueryString(queryString).Get(paramName)?.ToString() ?? string.Empty;
return paramValue ?? "";
}
/// <summary>
/// 转换URL参数
/// </summary>
/// <param name="source"></param>
/// <returns></returns>
public static string BuildQueryString(this object source)
{
var buff = new StringBuilder(string.Empty);
if (source == null)
return buff.ToString();
var modelDict = source.GetType().GetProperties().ToDictionary(a => a.Name.ToLower());
foreach (var item in modelDict)
{
if (item.Value.PropertyType != typeof(DateTime?[]))
{
System.Reflection.PropertyInfo? pi = null;
if (modelDict.TryGetValue(item.Key, out pi))
{
object? obj = pi.GetValue(source, null);
if (pi != null && obj != null)
{
var value = obj.ToString();
if (!string.IsNullOrEmpty(value))
{
buff.Append($"{item.Key}={value}&");
}
}
}
}
else
{
System.Reflection.PropertyInfo? pi = null;
if (modelDict.TryGetValue(item.Key, out pi))
{
object? obj = pi.GetValue(source, null);
if (pi != null && obj != null)
{
var data = obj as DateTime?[];
if (data != null)
{
long start = 0;
long end = 0;
if (data[0].HasValue) {
var time1 = data[0].Value.ConvertStartDateTime();
start = time1.DateToNumber();
}
if (data[1].HasValue)
{
var time1 = data[1].Value.ConvertEndDateTime();
end = time1.DateToNumber();
}
if (start > 0 && end > 0)
{
buff.Append($"{item.Key}={start}-{end}&");
}
}
}
}
}
}
return buff.ToString().Trim('&');
}
/// <summary>
/// url参数转换成字典
/// </summary>
/// <param name="formData"></param>
/// <returns></returns>
public static Dictionary<string, object> ToUrlDictionary(this string formData)
{
if (string.IsNullOrEmpty(formData))
{
return new Dictionary<string, object>();
}
try
{
//将参数存入字符数组
string[] dataArry = formData.Split('&');
//定义字典,将参数按照键值对存入字典中
Dictionary<string, object> dataDic = new Dictionary<string, object>();
//遍历字符数组
for (int i = 0; i <= dataArry.Length - 1; i++)
{
//当前参数值
string dataParm = dataArry[i];
//"="的索引值
int dIndex = dataParm.IndexOf("=");
//参数名作为key
string key = dataParm.Substring(0, dIndex);
//参数值作为Value
string value = dataParm.Substring(dIndex + 1, dataParm.Length - dIndex - 1);
//将编码后的Value解码
string deValue = HttpUtility.UrlDecode(value, Encoding.GetEncoding("utf-8"));
if (key != "__VIEWSTATE")
{
//将参数以键值对存入字典
dataDic.Add(key, deValue);
}
}
return dataDic;
}
catch (Exception)
{
return new Dictionary<string, object>();
}
}
}
}

View File

@@ -0,0 +1,189 @@
using Atomx.Utils.Json;
using Atomx.Utils.Models;
using Microsoft.AspNetCore.Http;
using System.Net;
namespace Atomx.Utils.Extension
{
public class UploadExtension
{
/// <summary>
/// 上传
/// </summary>
/// <param name="file">文件</param>
/// <param name="rootPath">站点根目录</param>
/// <param name="path">存储目录</param>
/// <param name="fixedPath">是否固定存储在根目录,如果不固定,将自动根据时间生成子文件夹进行存储</param>
/// <param name="fileName">文件名</param>
/// <param name="currentPath">覆盖上传的路径</param>
/// <param name="extension">文件类型</param>
/// <returns></returns>
public Result<string> LocalUpload(IFormFile file, string rootPath, string path, bool fixedPath, string fileName, string currentPath, string extension)
{
var result = new Result<string>();
var filepath = string.Empty;
if (!string.IsNullOrEmpty(currentPath))
{
filepath = currentPath;
}
else
{
filepath = PathExtension.GetFileNameByData(path, fixedPath, fileName, extension);
}
var savepath = Path.Combine(rootPath, filepath.Replace("/", "\\"));
var newpath = Path.GetDirectoryName(savepath);
if (string.IsNullOrEmpty(newpath))
{
result.Message = "Storage path exception";
result.Success = false;
return result;
}
try
{
if (!Directory.Exists(newpath))
{
Directory.CreateDirectory(newpath);
}
}
catch
{
result.Message = "Folder failure is established";
result.Success = false;
return result;
}
try
{
using (FileStream fs = File.Create(savepath))
{
file.CopyTo(fs);
fs.Flush();
}
}
catch (Exception ex)
{
result.Message = ex.Message;
result.Success = false;
}
result.Data = filepath;
return result;
}
/// <summary>
/// 上传
/// </summary>
/// <param name="file">文件</param>
/// <param name="rootPath">站点根目录</param>
/// <param name="path">存储目录</param>
/// <param name="fixedPath">是否固定存储在根目录,如果不固定,将自动根据时间生成子文件夹进行存储</param>
/// <param name="fileName">文件名</param>
/// <param name="currentPath">覆盖上传的路径</param>
/// <param name="extension">文件类型</param>
/// <returns></returns>
public async Task<Result<string>> LocalUploadEncryptAsync(IFormFile file, string rootPath, string path, bool fixedPath, string fileName, string currentPath, string extension)
{
var result = new Result<string>();
var filepath = string.Empty;
if (!string.IsNullOrEmpty(currentPath))
{
filepath = currentPath;
}
else
{
filepath = PathExtension.GetFileNameByData(path, fixedPath, fileName, extension);
}
var savepath = Path.Combine(rootPath, filepath.Replace("/", "\\"));
var newpath = Path.GetDirectoryName(savepath);
if(string.IsNullOrEmpty(newpath))
{
result.Message = "Storage path exception";
result.Success = false;
return result;
}
try
{
if (!Directory.Exists(newpath))
{
Directory.CreateDirectory(newpath);
}
}
catch
{
result.Message = "Folder failure is established";
result.Success = false;
return result;
}
try
{
var stream = file.OpenReadStream();
byte[] bytes = new byte[stream.Length];
await stream.ReadAsync(bytes, 0, bytes.Length);
for (int i = 0; i < bytes.Length; i++)
{
++bytes[i];
}
var fsw = new FileStream(savepath, FileMode.Create, FileAccess.Write);
fsw.Write(bytes, 0, bytes.Length);
fsw.Close();
}
catch (Exception ex)
{
result.Message = ex.Message;
result.Success = false;
}
result.Data = filepath;
return result;
}
/// <summary>
/// 远程上传文件
/// </summary>
/// <param name="file"></param>
/// <param name="url"></param>
/// <param name="rootpath"></param>
/// <param name="fixedpath"></param>
/// <param name="filename"></param>
/// <param name="currentpath"></param>
/// <returns></returns>
public async Task<UploadResult> RemoteUploadAsync(IFormFile file, string url, string rootpath, bool fixedpath, string filename, string currentpath)
{
var result = new UploadResult();
var dic = new Dictionary<string, string>();
dic.Add("rootpath", rootpath);
dic.Add("fixedpath", fixedpath.ToString().ToLower());
dic.Add("filename", filename);
dic.Add("currentpath", currentpath);
using (var client = new HttpClient())
{
using (var content = new MultipartFormDataContent())
{
content.Add(new StreamContent(file.OpenReadStream()), "file", file.FileName);
foreach (var item in dic)
{
content.Add(new StringContent(item.Value), item.Key);
}
var response = await client.PostAsync(url, content);
if (response.StatusCode == HttpStatusCode.OK)
{
var uploadresult = await response.Content.ReadAsStringAsync();
result = uploadresult.FromJson<UploadResult>();
}
else
{
//result.Code = UploadCodes.Failure;
//result.Message = "remote server is error";
}
}
}
return result;
}
}
}

View File

@@ -0,0 +1,285 @@
using System.Text;
using System.Text.RegularExpressions;
namespace Atomx.Utils.Files
{
/// <summary>
/// 常用文件头识别工具(改进版:修复编译器警告、改善流读取安全性与类型判断)
/// </summary>
public static class FileTypes
{
// 保持原有 public 字段签名以兼容现有代码
public static readonly Dictionary<string, byte[]> ImageHeader = new();
public static readonly Dictionary<string, object> FilesHeader = new();
public static readonly Dictionary<string, object> VideoHeader = new();
static FileTypes()
{
ImageHeader.Add("gif", new byte[] { 71, 73, 70, 56, 57, 97 });
ImageHeader.Add("bmp", new byte[] { 66, 77 });
ImageHeader.Add("jpg", new byte[] { 255, 216, 255 });
ImageHeader.Add("png", new byte[] { 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82 });
FilesHeader.Add("pdf", new byte[] { 37, 80, 68, 70, 45, 49, 46, 53 });
FilesHeader.Add("docx", new object[] { new byte[] { 80, 75, 3, 4, 20, 0, 6, 0, 8, 0, 0, 0, 33 }, new Regex(@"word/_rels/document\.xml\.rels", RegexOptions.IgnoreCase) });
FilesHeader.Add("xlsx", new object[] { new byte[] { 80, 75, 3, 4, 20, 0, 6, 0, 8, 0, 0, 0, 33 }, new Regex(@"xl/_rels/workbook\.xml\.rels", RegexOptions.IgnoreCase) });
FilesHeader.Add("pptx", new object[] { new byte[] { 80, 75, 3, 4, 20, 0, 6, 0, 8, 0, 0, 0, 33 }, new Regex(@"ppt/_rels/presentation\.xml\.rels", RegexOptions.IgnoreCase) });
FilesHeader.Add("doc", new object[] { new byte[] { 208, 207, 17, 224, 161, 177, 26, 225 }, new Regex(@"microsoft( office)? word(?![\s\S]*?microsoft)", RegexOptions.IgnoreCase) });
FilesHeader.Add("xls", new object[] { new byte[] { 208, 207, 17, 224, 161, 177, 26, 225 }, new Regex(@"microsoft( office)? excel(?![\s\S]*?microsoft)", RegexOptions.IgnoreCase) });
FilesHeader.Add("ppt", new object[] { new byte[] { 208, 207, 17, 224, 161, 177, 26, 225 }, new Regex(@"c.u.r.r.e.n.t. .u.s.e.r(?![\s\S]*?[a-z])", RegexOptions.IgnoreCase) });
FilesHeader.Add("avi", new byte[] { 65, 86, 73, 32 });
FilesHeader.Add("mpg", new byte[] { 0, 0, 1, 0xBA });
FilesHeader.Add("mpeg", new byte[] { 0, 0, 1, 0xB3 });
FilesHeader.Add("rar", new byte[] { 82, 97, 114, 33, 26, 7 });
FilesHeader.Add("zip", new byte[] { 80, 75, 3, 4 });
VideoHeader.Add("avi", new byte[] { 65, 86, 73, 32 });
VideoHeader.Add("mpg", new byte[] { 0, 0, 1, 0xBA });
VideoHeader.Add("mpeg", new byte[] { 0, 0, 1, 0xB3 });
}
/// <summary>
/// 检测视频类型扩展方法
/// </summary>
public static string VideoType(this Stream stream)
{
if (stream == null) throw new ArgumentNullException(nameof(stream));
foreach (var kv in VideoHeader)
{
if (TryMatchHeader(stream, kv.Value, out var matched))
{
if (matched)
return kv.Key;
}
}
return string.Empty;
}
/// <summary>
/// 检测常规文件类型扩展方法
/// </summary>
public static string FileType(this Stream stream)
{
if (stream == null) throw new ArgumentNullException(nameof(stream));
foreach (var kv in FilesHeader)
{
if (TryMatchHeader(stream, kv.Value, out var matched))
{
if (matched)
return kv.Key;
}
}
return string.Empty;
}
/// <summary>
/// 检测图片类型扩展方法
/// </summary>
public static string ImageType(this Stream stream)
{
if (stream == null) throw new ArgumentNullException(nameof(stream));
foreach (var kv in ImageHeader)
{
var header = kv.Value;
if (header == null || header.Length == 0) continue;
if (ReadAndCompare(stream, header))
return kv.Key;
}
// 额外尝试判断是否是纯文本(防止误判),若为纯文本则返回空
// 只读取流内容进行检查,谨慎处理大流(可能会分配较大内存)
try
{
var content = stream.ReadAllBytesAndRestorePosition();
if (content.Length > 0)
{
var encodings = new[] { Encoding.ASCII, Encoding.UTF8 };
foreach (var enc in encodings)
{
var text = enc.GetString(content);
if (Regex.IsMatch(text, @"^[^\u0000-\u0008\u000B-\u000C\u000E-\u001F]*$"))
{
// 认为是文本,非图片
return string.Empty;
}
}
// Windows-936 编码仅在支持 CodePages 的情况下尝试使用
try
{
var cp936 = Encoding.GetEncoding(936);
var text936 = cp936.GetString(content);
if (Regex.IsMatch(text936, @"^[^\u0000-\u0008\u000B-\u000C\u000E-\u001F]*$"))
return string.Empty;
}
catch
{
// 在不支持 CodePages 的平台上忽略
}
}
}
catch
{
// 忽略检查错误,回退为无法识别
}
return string.Empty;
}
/// <summary>
/// 将流完整读取为字节数组(更高效且保持原位置)
/// </summary>
public static byte[] StreamToBytes(this Stream stream)
{
if (stream == null) throw new ArgumentNullException(nameof(stream));
return stream.ReadAllBytesAndRestorePosition();
}
// ---------- 辅助方法 ----------
/// <summary>
/// 根据字节头或复杂描述object[])进行匹配判断。
/// object 类型支持:
/// - byte[] : 仅比较头部字节
/// - object[] : 第一个元素为 byte[] 头部;第二个元素可以为 Regex 或 int(表示尾部偏移),后面可跟要比较的尾部 byte[] 列表
/// </summary>
private static bool TryMatchHeader(Stream stream, object value, out bool matched)
{
matched = false;
if (value == null) return false;
if (value is byte[] headerOnly)
{
matched = ReadAndCompare(stream, headerOnly);
return true;
}
if (value is object[] arr && arr.Length > 0 && arr[0] is byte[] header)
{
// 先比较头部
if (!ReadAndCompare(stream, header))
return true; // header no match -> not this type
// 若仅头部匹配,后续根据第二元素进一步校验
if (arr.Length >= 2)
{
var second = arr[1];
if (second is Regex regex)
{
var content = stream.ReadAllBytesAndRestorePosition();
var text = Encoding.ASCII.GetString(content);
matched = regex.IsMatch(text);
return true;
}
else if (second is int tailOffset)
{
// arr[2..] 为若干尾部字节数组,比对任一相等则通过
for (int i = 2; i < arr.Length; i++)
{
if (arr[i] is byte[] tailBytes)
{
if (ReadAndCompareTail(stream, tailOffset, tailBytes))
{
matched = true;
return true;
}
}
}
matched = false;
return true;
}
else
{
// 未知第二元素类型,视为仅头匹配
matched = true;
return true;
}
}
matched = true;
return true;
}
return false;
}
/// <summary>
/// 从流头部读取与目标字节数组比较,比较完成后恢复流位置
/// </summary>
private static bool ReadAndCompare(Stream stream, byte[] target)
{
if (target == null || target.Length == 0) return false;
var originalPos = stream.CanSeek ? stream.Position : (long?)null;
try
{
var buffer = new byte[target.Length];
if (!ReadFull(stream, buffer, 0, buffer.Length))
return false;
return buffer.SequenceEqual(target);
}
finally
{
if (originalPos.HasValue)
stream.Position = originalPos.Value;
}
}
/// <summary>
/// 从流尾部根据偏移读取并比较
/// </summary>
private static bool ReadAndCompareTail(Stream stream, int offsetFromEnd, byte[] target)
{
if (target == null || target.Length == 0) return false;
if (!stream.CanSeek) return false;
var originalPos = stream.Position;
try
{
if (stream.Length < offsetFromEnd + target.Length) return false;
stream.Position = stream.Length - offsetFromEnd;
var buffer = new byte[target.Length];
if (!ReadFull(stream, buffer, 0, buffer.Length)) return false;
return buffer.SequenceEqual(target);
}
finally
{
stream.Position = originalPos;
}
}
/// <summary>
/// 从流中读取指定长度的数据,直到读取到要求长度或 EOF返回是否读取到完整长度
/// </summary>
private static bool ReadFull(Stream stream, byte[] buffer, int offset, int count)
{
if (count <= 0) return true;
int read;
int total = 0;
while (total < count && (read = stream.Read(buffer, offset + total, count - total)) > 0)
{
total += read;
}
return total == count;
}
/// <summary>
/// 将流全部读取为字节数组并尝试恢复原始 Position若支持
/// </summary>
private static byte[] ReadAllBytesAndRestorePosition(this Stream stream)
{
var originalPos = stream.CanSeek ? stream.Position : (long?)null;
try
{
using var ms = new MemoryStream();
stream.CopyTo(ms);
return ms.ToArray();
}
finally
{
if (originalPos.HasValue)
stream.Position = originalPos.Value;
}
}
}
}

View File

@@ -0,0 +1,47 @@
using System.ComponentModel;
namespace Atomx.Utils.Images
{
/// <summary>
/// 图片类型
/// </summary>
[Obsolete("废弃")]
public enum ImageType
{
/// <summary>
/// .jpg
/// </summary>
[Description(".jpg")]
Jpg,
/// <summary>
/// .jpeg
/// </summary>
[Description(".jpeg")]
Jpeg,
/// <summary>
/// .png
/// </summary>
[Description(".png")]
Png,
/// <summary>
/// .gif
/// </summary>
[Description(".gif")]
Gif,
/// <summary>
/// .svg
/// </summary>
[Description(".svg")]
Svg,
/// <summary>
/// .bmp
/// </summary>
[Description(".bmp")]
Bmp,
/// <summary>
/// .webp
/// </summary>
[Description(".webp")]
Webp
}
}

View File

@@ -0,0 +1,36 @@
using System.Security.Claims;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Atomx.Utils.Json.Converts
{
public class ClaimConverter : JsonConverter<Claim>
{
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert == typeof(Claim);
}
public override Claim Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using (var jsonDocument = JsonDocument.ParseValue(ref reader))
{
var text = jsonDocument.RootElement.GetRawText();
var data = JsonSerializer.Deserialize<ClaimLite>(text, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
return new Claim(data?.Type ?? string.Empty, data?.Value ?? string.Empty, data?.ValueType ?? string.Empty);
}
}
public override void Write(Utf8JsonWriter writer, Claim value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value);
}
}
public class ClaimLite
{
public string Type { get; set; } = string.Empty;
public string Value { get; set; } = string.Empty;
public string ValueType { get; set; } = string.Empty;
}
}

View File

@@ -0,0 +1,33 @@
using System.Text.Json;
namespace Atomx.Utils.Json.Converts
{
public class DateTimeJsonConverter : System.Text.Json.Serialization.JsonConverter<DateTime>
{
readonly string _dateFormatString;
public DateTimeJsonConverter()
{
_dateFormatString = "yyyy-MM-dd HH:mm:ss";
}
public DateTimeJsonConverter(string dateFormatString)
{
_dateFormatString = dateFormatString;
}
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var text = reader.GetString();
if (string.IsNullOrEmpty(text))
{
return default;
}
return DateTime.Parse(text);
}
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString("s"));
}
}
}

View File

@@ -0,0 +1,33 @@
using System.Buffers;
using System.Buffers.Text;
using System.Text.Json;
namespace Atomx.Utils.Json.Converts
{
public class LongJsonConverter : System.Text.Json.Serialization.JsonConverter<long>
{
public override long Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
ReadOnlySpan<byte> span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan;
if (Utf8Parser.TryParse(span, out long number, out int bytesConsumed) && span.Length == bytesConsumed)
{
return number;
}
if (long.TryParse(reader.GetString(), out number))
{
return number;
}
}
return reader.GetInt64();
}
public override void Write(Utf8JsonWriter writer, long value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString());
}
}
}

View File

@@ -0,0 +1,26 @@
using Atomx.Utils.Json.Converts;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Unicode;
namespace Atomx.Utils.Json
{
public class JsonOptions
{
public static JsonSerializerOptions DefaultOptions()
{
var opts = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true,
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
};
opts.Converters.Add(new LongJsonConverter());
opts.Converters.Add(new DateTimeJsonConverter());
opts.Converters.Add(new ClaimConverter());
return opts;
}
}
}

View File

@@ -0,0 +1,30 @@
using System.Text.Json;
namespace Atomx.Utils.Json
{
public static class JsonSerialize
{
public static string ToJson(this object obj, JsonSerializerOptions? options = null)
{
var opts = JsonOptions.DefaultOptions();
if (options != null)
{
opts = options;
}
var data = JsonSerializer.Serialize(obj, opts);
return data;
}
public static T? FromJson<T>(this string json, JsonSerializerOptions? options = null)
{
var opts = JsonOptions.DefaultOptions();
if (options != null)
{
opts = options;
}
var data = JsonSerializer.Deserialize<T>(json, opts);
return data;
}
}
}

View File

@@ -0,0 +1,25 @@
namespace Atomx.Utils.Models
{
public class Result<T>
{
/// <summary>
/// 数据
/// </summary>
public T? Data { get; set; }
/// <summary>
/// 消息
/// </summary>
public string Message { get; set; } = string.Empty;
/// <summary>
/// 业务代码
/// </summary>
public int Code { get; set; }
/// <summary>
/// 业务是否成功
/// </summary>
public bool Success { get; set; }
}
}

View File

@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Atomx.Utils.Models
{
public class UploadResult
{
/// <summary>
///
/// </summary>
public string Url { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public long Size { get; set; }
public string Type { get; set; } = string.Empty;
}
}