feat: 项目初始化 + 3D方块世界原型 + AI助搭系统
CI / Go Backend (push) Canceled after 0s

初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器

HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块

Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚

AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
xyou
2026-08-08 14:07:56 +08:00
parent 9500c4c80a
commit f70b061d1a
1972 changed files with 159760 additions and 6 deletions
@@ -0,0 +1,35 @@
using System.Collections.Generic;
using FluentValidation;
using FluentValidation.Results;
namespace PCL.Core.Utils.Validate;
public class BlacklistValidator(List<string> contains) : AbstractValidator<string>
{
public List<string> Blacklist { get; set; } = contains;
public BlacklistValidator() : this([])
{
}
private void _BuildRules()
{
RuleFor(x => x)
.Custom((input, context) =>
{
foreach (var items in Blacklist)
{
if (input.Contains(items))
{
context.AddFailure($"输入内容不能包含 {items}");
}
}
});
}
protected override bool PreValidate(ValidationContext<string> context, ValidationResult result)
{
_BuildRules();
return base.PreValidate(context, result);
}
}
@@ -0,0 +1,74 @@
using System;
using System.IO;
using System.Linq;
using FluentValidation;
using FluentValidation.Results;
using PCL.Core.Utils.Exts;
namespace PCL.Core.Utils.Validate;
public class FileNameValidator(
string? parentFolder = null,
bool ignoreCase = true,
bool useMinecraftCharCheck = true,
bool requireParentFolderExists = true)
: FileSystemValidator
{
public bool UseMinecraftCharCheck { get; set; } = useMinecraftCharCheck;
public bool IgnoreCase { get; set; } = ignoreCase;
public string? ParentFolder { get; set; } = parentFolder;
public bool RequireParentFolderExists { get; set; } = requireParentFolderExists;
private bool? _isParentFolderExists;
public FileNameValidator() : this(null)
{
}
private void _BuildRules()
{
RuleFor(x => x)
.Must(x => !string.IsNullOrWhiteSpace(x)).WithMessage("输入内容不能为空!")
.Must(x => !x.StartsWith(' ')).WithMessage("文件名不能以空格开头!")
.Must(x => !x.EndsWith(' ')).WithMessage("文件名不能以空格结尾!")
.Must(x => !x.EndsWith('.')).WithMessage("文件名不能以小数点结尾!")
.Custom((fileName, context) =>
{
var invalidChar = CheckInvalidStrings(fileName, UseMinecraftCharCheck ? ["!;"] : []);
if (invalidChar is not null)
{
context.AddFailure($"文件名不可包含 {invalidChar} 字符!");
}
})
.Custom((fileName, context) =>
{
var reservedWord = CheckReservedWord(fileName, []);
if (reservedWord is not null)
{
context.AddFailure($"文件名不可为 {reservedWord}");
}
})
.Must(x => !x.IsMatch(RegexPatterns.Ntfs83FileName)).WithMessage("文件名不能包含这一特殊格式!")
.Must(x =>
{
if (ParentFolder is null) return true;
var dirInfo = new DirectoryInfo(ParentFolder);
if (dirInfo.Exists)
{
return !dirInfo.EnumerateFiles().Select(f => f.Name).Contains(x,
IgnoreCase ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal);
}
_isParentFolderExists = false;
return !RequireParentFolderExists;
}).WithMessage(_isParentFolderExists is not null ? $"父文件夹不存在:{ParentFolder}" : "不可与现有文件重名!");
}
protected override bool PreValidate(ValidationContext<string> context, ValidationResult result)
{
_BuildRules();
return base.PreValidate(context, result);
}
}
@@ -0,0 +1,45 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using FluentValidation;
namespace PCL.Core.Utils.Validate;
public abstract class FileSystemValidator : AbstractValidator<string>
{
protected static string? CheckInvalidStrings(string input, string[] extraInvalidStrings)
{
if (string.IsNullOrEmpty(input)) return null;
var invalidStrings = Path.GetInvalidFileNameChars().Select(c => c.ToString());
invalidStrings = invalidStrings.Concat(extraInvalidStrings);
// 找出字符串中包含的非法字符
var found = invalidStrings
.Where(input.Contains)
.Distinct()
.ToArray();
return found.Length != 0 ? string.Join(" ", found) : null;
}
protected static string? CheckReservedWord(string input, string[] extraReservedWords)
{
if (string.IsNullOrEmpty(input)) return null;
var nameWithoutExtension = Path.GetFileNameWithoutExtension(input);
IEnumerable<string> reserved =
[
"CON", "PRN", "AUX", "CLOCK$", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7",
"COM8", "COM9", "COM¹", "COM²", "COM³", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7",
"LPT8", "LPT9", "LPT¹", "LPT²", "LPT³"
];
reserved = reserved.Concat(extraReservedWords);
// 找出匹配的保留字
var matched = reserved.FirstOrDefault(r => r.Equals(nameWithoutExtension));
return matched ?? null;
}
}
@@ -0,0 +1,69 @@
using System;
using System.IO;
using System.Linq;
using FluentValidation;
using FluentValidation.Results;
using PCL.Core.Utils.Exts;
namespace PCL.Core.Utils.Validate;
public class FolderNameValidator(
string? parentFolder = null,
bool useMinecraftCharCheck = true,
bool ignoreCase = true,
bool ignoreSameNameInParentFolder = false)
: FileSystemValidator
{
public bool UseMinecraftCharCheck { get; set; } = useMinecraftCharCheck;
public bool IgnoreCase { get; set; } = ignoreCase;
public bool IgnoreSameNameInParentFolder { get; set; } = ignoreSameNameInParentFolder;
public string? ParentFolder { get; set; } = parentFolder;
public FolderNameValidator() : this(null)
{
}
private void _BuildRules()
{
RuleFor(x => x)
.Must(x => !string.IsNullOrWhiteSpace(x)).WithMessage("输入内容不能为空!")
.Must(x => !x.StartsWith(' ')).WithMessage("文件名不能以空格开头!")
.Must(x => !x.EndsWith(' ')).WithMessage("文件名不能以空格结尾!")
.Must(x => !x.EndsWith('.')).WithMessage("文件名不能以小数点结尾!")
.Custom((fileName, context) =>
{
var invalidChar = CheckInvalidStrings(fileName, UseMinecraftCharCheck ? ["!;"] : []);
if (invalidChar is not null)
{
context.AddFailure($"文件名不可包含 {invalidChar} 字符!");
}
})
.Custom((fileName, context) =>
{
var reservedWord = CheckReservedWord(fileName, []);
if (reservedWord is not null)
{
context.AddFailure($"文件名不可为 {reservedWord}");
}
})
.Must(x => !x.IsMatch(RegexPatterns.Ntfs83FileName)).WithMessage("文件名不能包含这一特殊格式!")
.Must(x =>
{
if (ParentFolder is null) return true;
var dirInfo = new DirectoryInfo(ParentFolder);
if (!dirInfo.Exists) return true;
if (IgnoreSameNameInParentFolder) return true;
return !dirInfo.EnumerateDirectories().Select(f => f.Name).Contains(x,
IgnoreCase ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal);
}).WithMessage("不可与现有文件夹重名!");
}
protected override bool PreValidate(ValidationContext<string> context, ValidationResult result)
{
_BuildRules();
return base.PreValidate(context, result);
}
}
@@ -0,0 +1,67 @@
using System;
using System.IO;
using FluentValidation;
using FluentValidation.Results;
using PCL.Core.Utils.Exts;
namespace PCL.Core.Utils.Validate;
public class FolderPathValidator(bool useMinecraftCharCheck) : FileSystemValidator
{
public bool UseMinecraftCharCheck { get; set; } = useMinecraftCharCheck;
public FolderPathValidator() : this(true)
{
}
private void _BuildRules()
{
RuleFor(x => x)
.NotEmpty().WithMessage("输入内容不能为空!")
.Must(x => !x.EndsWith(' ')).WithMessage("文件夹名不能以空格结尾!")
.Must(x => !x.EndsWith('.')).WithMessage("文件夹名不能以小数点结尾!");
RuleForEach(x => _GetSubPaths(x))
.Must(x => !string.IsNullOrWhiteSpace(x)).WithMessage("文件夹路径存在错误!")
.Must(x => !x.StartsWith(' ')).WithMessage("文件夹名不能以空格开头!")
.Must(x => !x.EndsWith(' ')).WithMessage("文件夹名不能以空格结尾!")
.Must(x => !x.EndsWith('.')).WithMessage("文件夹名不能以小数点结尾!")
.Custom((fileName, context) =>
{
var invalidChar = CheckInvalidStrings(fileName, UseMinecraftCharCheck ? ["!;"] : []);
if (invalidChar is not null)
{
context.AddFailure($"文件夹名不可包含 {invalidChar} 字符!");
}
})
.Custom((fileName, context) =>
{
var reservedWord = CheckReservedWord(fileName, []);
if (reservedWord is not null)
{
context.AddFailure($"文件夹名不可为 {reservedWord}");
}
})
.Must(x => !x.IsMatch(RegexPatterns.Ntfs83FileName)).WithMessage("文件夹名不能包含这一特殊格式!")
.OverridePropertyName("PathSegments");
}
protected override bool PreValidate(ValidationContext<string> context, ValidationResult result)
{
_BuildRules();
return base.PreValidate(context, result);
}
private static string[] _GetSubPaths(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
return [];
}
var fullPath = new DirectoryInfo(path).FullName;
return fullPath[Path.GetPathRoot(fullPath)!.Length..]
.TrimEnd(Path.DirectorySeparatorChar)
.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries);
}
}
@@ -0,0 +1,34 @@
using FluentValidation;
using FluentValidation.Results;
using PCL.Core.Utils.Exts;
namespace PCL.Core.Utils.Validate;
public class HttpAndUncValidator(bool allowNullOrEmpty) : AbstractValidator<string>
{
public bool AllowsNullOrEmpty { get; set; } = allowNullOrEmpty;
public HttpAndUncValidator() : this(false)
{
}
private void _BuildRules()
{
RuleFor(x => x)
.Must(x =>
{
if (AllowsNullOrEmpty && string.IsNullOrEmpty(x))
{
return true;
}
return x.IsMatch(RegexPatterns.HttpUri) || x.IsMatch(RegexPatterns.UncPath);
}).WithMessage("输入的网址无效!");
}
protected override bool PreValidate(ValidationContext<string> context, ValidationResult result)
{
_BuildRules();
return base.PreValidate(context, result);
}
}
@@ -0,0 +1,34 @@
using FluentValidation;
using FluentValidation.Results;
using PCL.Core.Utils.Exts;
namespace PCL.Core.Utils.Validate;
public class HttpValidator(bool allowNullOrEmpty) : AbstractValidator<string>
{
public bool AllowsNullOrEmpty { get; set; } = allowNullOrEmpty;
public HttpValidator() : this(false)
{
}
private void _BuildRules()
{
RuleFor(x => x)
.Must(x =>
{
if (AllowsNullOrEmpty && string.IsNullOrEmpty(x))
{
return true;
}
return x.IsMatch(RegexPatterns.HttpUri);
}).WithMessage("输入的网址无效!");
}
protected override bool PreValidate(ValidationContext<string> context, ValidationResult result)
{
_BuildRules();
return base.PreValidate(context, result);
}
}
@@ -0,0 +1,29 @@
using FluentValidation;
using FluentValidation.Results;
namespace PCL.Core.Utils.Validate;
public class IntValidator(int max = int.MaxValue, int min = int.MinValue) : AbstractValidator<string>
{
public int Max { get; set; } = max;
public int Min { get; set; } = min;
public IntValidator() : this(int.MaxValue)
{
}
private void _BuildRules()
{
RuleFor(x => x)
.Must(x => x.Length < 9).WithMessage("请输入一个大小合理的数字!")
.Must(x => int.TryParse(x, out _)).WithMessage("请输入一个整数!")
.Must(x => int.TryParse(x, out var value) && value <= Max).WithMessage($"不可超过 {Max}")
.Must(x => int.TryParse(x, out var value) && value >= Min).WithMessage($"不可低于 {Min}");
}
protected override bool PreValidate(ValidationContext<string> context, ValidationResult result)
{
_BuildRules();
return base.PreValidate(context, result);
}
}
@@ -0,0 +1,12 @@
using FluentValidation;
namespace PCL.Core.Utils.Validate;
public class NullOrEmptyValidator : AbstractValidator<string>
{
public NullOrEmptyValidator()
{
RuleFor(x => x)
.Must(x => !string.IsNullOrEmpty(x)).WithMessage("输入内容不能为空!");
}
}
@@ -0,0 +1,12 @@
using FluentValidation;
namespace PCL.Core.Utils.Validate;
public class NullOrWhiteSpaceValidator : AbstractValidator<string>
{
public NullOrWhiteSpaceValidator()
{
RuleFor(x => x)
.Must(x => !string.IsNullOrWhiteSpace(x)).WithMessage("输入内容不能为空!");
}
}
@@ -0,0 +1,30 @@
using System.Text.RegularExpressions;
using FluentValidation;
using FluentValidation.Results;
using PCL.Core.App.Localization;
namespace PCL.Core.Utils.Validate;
public class RegexValidator(string pattern = "", string errorMessage = "正则检查失败!") : AbstractValidator<string>
{
public RegexValidator() : this(string.Empty)
{
}
public string Pattern { get; set; } = pattern;
public string ErrorMessage { get; set; } = errorMessage;
public string ErrorKey { get; set; } = "";
private void _BuildRules()
{
var message = string.IsNullOrEmpty(ErrorKey) ? ErrorMessage : Lang.Text(ErrorKey);
RuleFor(x => x)
.Must(x => Regex.IsMatch(x, Pattern)).WithMessage(message);
}
protected override bool PreValidate(ValidationContext<string> context, ValidationResult result)
{
_BuildRules();
return base.PreValidate(context, result);
}
}
@@ -0,0 +1,28 @@
using FluentValidation;
using FluentValidation.Results;
namespace PCL.Core.Utils.Validate;
public class StringLengthValidator(int min = 0, int max = int.MaxValue) : AbstractValidator<string>
{
public int Min { get; set; } = min;
public int Max { get; set; } = max;
public StringLengthValidator() : this(0)
{
}
private void _BuildRules()
{
RuleFor(x => x)
.Must(x => x.Length != Max || Max == Min).WithMessage($"长度必须为 {Max} 个字符!")
.Must(x => x.Length >= Min).WithMessage($"长度至少为 {Min} 个字符!")
.Must(x => x.Length <= Max).WithMessage($"长度最长为 {Max} 个字符!");
}
protected override bool PreValidate(ValidationContext<string> context, ValidationResult result)
{
_BuildRules();
return base.PreValidate(context, result);
}
}