合并云商品分支到 master 并适配插件结构
This commit is contained in:
commit
1fa91a9cfb
15 files changed
+968
-48
No files matched your search
@@ -0,0 +1,119 @@
|
||||
using MAX.Plugins;
|
||||
using Microsoft.Extensions.Hosting.Internal;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using MAX.Framework.Commons;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using OpenSearch.Client;
|
||||
using OpenSearch.Net;
|
||||
using Store.Modules.CloudProduct.Controllers;
|
||||
using Store.Modules.CloudProduct.Dtos;
|
||||
using Store.Modules.CloudProduct.Entities;
|
||||
using Store.Modules.CloudProduct.Services;
|
||||
|
||||
if (args.Contains("--integration"))
|
||||
{
|
||||
await IntegrationChecks.RunAsync(args.Contains("--apply-migration"), args.Contains("--migrate-violation-rows"));
|
||||
return;
|
||||
}
|
||||
|
||||
var checks = 0;
|
||||
void Check(bool condition, string message)
|
||||
{
|
||||
if (!condition) throw new Exception(message);
|
||||
checks++;
|
||||
}
|
||||
void Reject(Action action)
|
||||
{
|
||||
try { action(); }
|
||||
catch (AppException) { checks++; return; }
|
||||
throw new Exception("Expected validation failure");
|
||||
}
|
||||
var grouped = GoodsViolationService.ValidateAndGroup([
|
||||
new() { GoodsId = 12, Reasons = " 图片违规 " },
|
||||
new() { GoodsId = 12, Reasons = "描述违规" },
|
||||
new() { GoodsId = 12, Reasons = "图片违规" },
|
||||
new() { GoodsId = 12, Reasons = "涉嫌侵权" },
|
||||
new() { GoodsId = 12, Reasons = "描述违规" },
|
||||
new() { GoodsId = 13, Reasons = "中文原因" }
|
||||
]);
|
||||
Check(grouped.Count == 2 && grouped[12].ToHashSet().SetEquals(new[] { "图片违规", "描述违规", "涉嫌侵权" }), "Duplicate goods/reasons must merge");
|
||||
Check(grouped[13][0] == "中文原因", "Chinese reason text must survive");
|
||||
Reject(() => GoodsViolationService.ValidateAndGroup(null));
|
||||
Reject(() => GoodsViolationService.ValidateAndGroup([]));
|
||||
Reject(() => GoodsViolationService.ValidateAndGroup([null!]));
|
||||
Reject(() => GoodsViolationService.ValidateAndGroup([new() { GoodsId = -1, Reasons = "违规" }]));
|
||||
Reject(() => GoodsViolationService.ValidateAndGroup([new() { GoodsId = 1, Reasons = "" }]));
|
||||
Reject(() => GoodsViolationService.ValidateAndGroup([new() { GoodsId = 1, Reasons = " " }]));
|
||||
Reject(() => GoodsViolationService.ValidateAndGroup([new() { GoodsId = 1, Reasons = null! }]));
|
||||
Reject(() => GoodsViolationService.ValidateAndGroup(new ReportGoodsViolationItem[1001]));
|
||||
|
||||
Reject(() => GoodsViolationService.ValidateAndGroup([new() { GoodsId = 1, Reasons = new string('中', 101) }]));
|
||||
Reject(() => GoodsViolationService.ValidateAndGroup([new() { GoodsId = 1, Reasons = null! }]));
|
||||
|
||||
// SQL generation only: no database connection or production data access.
|
||||
using var db = new FreeSql.FreeSqlBuilder().UseConnectionString(FreeSql.DataType.MySql,
|
||||
"Server=127.0.0.1;Database=checks;User Id=unused;Password=unused").Build();
|
||||
var sql = db.Select<GoodsEntity>().As("g")
|
||||
.Where(x => x.Status == 0 && x.IsSegmented == 0)
|
||||
.Where("NOT EXISTS (SELECT 1 FROM goods_violation v WHERE v.goods_id = g.goods_id)")
|
||||
.OrderBy(x => x.GoodsId).Limit(1000).ToSql();
|
||||
Check(sql.Contains("NOT EXISTS") && sql.IndexOf("NOT EXISTS") < sql.IndexOf("limit", StringComparison.OrdinalIgnoreCase),
|
||||
"Violation filter must precede paging");
|
||||
Check(sql.Contains("FROM `goods` g", StringComparison.OrdinalIgnoreCase), "Anti-join alias must match: " + sql);
|
||||
var pendingSql = db.Select<GoodsViolationEntity>().Where(x => !x.IsIndexDeleted).ToSql();
|
||||
Check(pendingSql.Contains("is_index_deleted") && !pendingSql.Contains("off_shelf_status"), "Renamed column mapping");
|
||||
var insertSql = db.Insert(new GoodsViolationEntity { GoodsId = 1, ReasonId = 2 }).NoneParameter().ToSql();
|
||||
Check(insertSql.Contains("reason_id") && !insertSql.Contains("reason_ids"), "Reason must be stored as a scalar ID");
|
||||
|
||||
var request = JsonSerializer.Deserialize<ReportGoodsViolationItem>("""{"goodsId":1,"reasons":"图片违规"}""", new JsonSerializerOptions(JsonSerializerDefaults.Web))!;
|
||||
Check(request.Reasons == "图片违规", "JSON request accepts reason names");
|
||||
var reportJson = JsonSerializer.Serialize(new ReportGoodsViolationsResult { GoodsId = 1, IsOffShelf = true },
|
||||
new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
Check(reportJson == """{"goodsId":1,"isOffShelf":true}""", "Report response contains only goods ID and off-shelf status");
|
||||
try
|
||||
{
|
||||
JsonSerializer.Deserialize<ReportGoodsViolationItem>("""{"goodsId":1,"reasons":["图片违规"]}""", new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
throw new Exception("Array reason accepted");
|
||||
}
|
||||
catch (JsonException) { checks++; }
|
||||
Check(!typeof(GoodsController).GetMethods().Any(x => x.Name.Contains("ByReason")), "Reason query must not be exposed");
|
||||
var removalService = new GoodsViolationService(null!, null!, null!, NullLogger<GoodsViolationService>.Instance);
|
||||
Reject(() => removalService.RemoveAsync(null).GetAwaiter().GetResult());
|
||||
Reject(() => removalService.RemoveAsync([]).GetAwaiter().GetResult());
|
||||
Reject(() => removalService.RemoveAsync([0]).GetAwaiter().GetResult());
|
||||
Reject(() => removalService.RemoveAsync([-1]).GetAwaiter().GetResult());
|
||||
Reject(() => removalService.RemoveAsync(new long[1001]).GetAwaiter().GetResult());
|
||||
|
||||
// Exercise deletion with an in-memory OpenSearch transport, without a real server.
|
||||
using var context = new PluginContext("CloudProduct", Path.Combine(Directory.GetCurrentDirectory(), "Plugins/CloudProduct"), new HostingEnvironment { EnvironmentName = "Testing" });
|
||||
context.Configuration["Search:Url"] = "http://localhost:9200";
|
||||
context.Configuration["Search:IndexName"] = "goods";
|
||||
var serviceType = typeof(IOpenSearchService).Assembly.GetType("Store.Modules.CloudProduct.Services.OpenSearchService")!;
|
||||
var logger = Activator.CreateInstance(typeof(NullLogger<>).MakeGenericType(serviceType));
|
||||
async Task TestDelete(int status, string body, bool shouldFail)
|
||||
{
|
||||
var service = (IOpenSearchService)Activator.CreateInstance(serviceType, context, logger, db, new GoodsIndexLock(context))!;
|
||||
using var transport = new InMemoryConnection(Encoding.UTF8.GetBytes(body), status);
|
||||
var client = new OpenSearchClient(new ConnectionSettings(new SingleNodeConnectionPool(new Uri("http://localhost:9200")), transport));
|
||||
serviceType.GetField("_client", BindingFlags.Instance | BindingFlags.NonPublic)!.SetValue(service, client);
|
||||
try
|
||||
{
|
||||
await service.DeleteDocumentsAsync([123]);
|
||||
Check(!shouldFail, $"HTTP {status}: expected delete failure");
|
||||
}
|
||||
catch (AppException)
|
||||
{
|
||||
Check(shouldFail, $"HTTP {status}: unexpected delete failure");
|
||||
}
|
||||
}
|
||||
await TestDelete(403, "{}", true);
|
||||
await TestDelete(500, "{}", true);
|
||||
await TestDelete(404, "{}", false);
|
||||
await TestDelete(200, """{"errors":false,"items":[{"delete":{"_index":"goods","_id":"123","status":200}}]}""", false);
|
||||
await TestDelete(200, """{"errors":true,"items":[{"delete":{"_index":"goods","_id":"123","status":404,"result":"not_found"}}]}""", false);
|
||||
await TestDelete(200, """{"errors":true,"items":[{"delete":{"_index":"goods","_id":"123","status":429,"error":{"type":"rejected","reason":"busy"}}}]}""", true);
|
||||
await TestDelete(200, """{"errors":false,"items":[]}""", true);
|
||||
Console.WriteLine($"PASS: {checks} checks (no external services accessed)");
|
||||
Reference in new issue
Block a user