forked from xiongyuxing/tiku-backend.net
feat: add operations catalog readonly endpoints
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using Tiku.Domain.Catalog;
|
||||
using Tiku.Domain.Commerce;
|
||||
using Tiku.Domain.Common;
|
||||
using Tiku.Domain.Learning;
|
||||
using Tiku.Domain.Operations;
|
||||
using Tiku.Domain.Tenancy;
|
||||
|
||||
namespace Tiku.IntegrationTests.Api;
|
||||
@@ -162,6 +165,316 @@ public sealed class CatalogEndpointTests
|
||||
Assert.Equal("tenant_not_found", body.RootElement.GetProperty("code").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Banners_are_filtered_by_tenant_region_and_active_status()
|
||||
{
|
||||
var tenantId = Guid.NewGuid();
|
||||
var otherTenantId = Guid.NewGuid();
|
||||
var regionId = Guid.NewGuid();
|
||||
var otherRegionId = Guid.NewGuid();
|
||||
await using var factory = new ApiTestFactory();
|
||||
await factory.SeedAsync(
|
||||
Tenant(tenantId, "master"),
|
||||
Tenant(otherTenantId, "other"),
|
||||
new Region { Id = regionId, TenantId = tenantId, Name = "浙江" },
|
||||
new Region { Id = otherRegionId, TenantId = tenantId, Name = "江苏" },
|
||||
new Banner
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenantId,
|
||||
RegionId = regionId,
|
||||
Title = "第二张",
|
||||
SortOrder = 2,
|
||||
IsActive = true
|
||||
},
|
||||
new Banner
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenantId,
|
||||
RegionId = regionId,
|
||||
Title = "第一张",
|
||||
SortOrder = 1,
|
||||
IsActive = true
|
||||
},
|
||||
new Banner
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenantId,
|
||||
RegionId = regionId,
|
||||
Title = "停用横幅",
|
||||
IsActive = false
|
||||
},
|
||||
new Banner
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenantId,
|
||||
RegionId = otherRegionId,
|
||||
Title = "其他地区横幅",
|
||||
IsActive = true
|
||||
},
|
||||
new Banner
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = otherTenantId,
|
||||
Title = "其他租户横幅",
|
||||
IsActive = true
|
||||
});
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
using var response = await client.GetAsync($"/api/catalog/banners?tenantCode=master®ionId={regionId}");
|
||||
var items = await ReadItemsAsync(response);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal(["第一张", "第二张"], items.Select(item => item.GetProperty("title").GetString()!).ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Faqs_and_announcements_only_return_active_current_tenant_items()
|
||||
{
|
||||
var tenantId = Guid.NewGuid();
|
||||
var otherTenantId = Guid.NewGuid();
|
||||
await using var factory = new ApiTestFactory();
|
||||
await factory.SeedAsync(
|
||||
Tenant(tenantId, "master"),
|
||||
Tenant(otherTenantId, "other"),
|
||||
new Faq
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenantId,
|
||||
Question = "如何购买?",
|
||||
Answer = "在线购买",
|
||||
IsActive = true
|
||||
},
|
||||
new Faq
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenantId,
|
||||
Question = "停用 FAQ",
|
||||
IsActive = false
|
||||
},
|
||||
new Faq
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = otherTenantId,
|
||||
Question = "其他租户 FAQ",
|
||||
IsActive = true
|
||||
},
|
||||
new Announcement
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenantId,
|
||||
Content = "暑期活动",
|
||||
IsActive = true
|
||||
},
|
||||
new Announcement
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenantId,
|
||||
Content = "停用公告",
|
||||
IsActive = false
|
||||
},
|
||||
new Announcement
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = otherTenantId,
|
||||
Content = "其他租户公告",
|
||||
IsActive = true
|
||||
});
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
using var faqResponse = await client.GetAsync("/api/catalog/faqs?tenantCode=master");
|
||||
using var announcementResponse = await client.GetAsync("/api/catalog/announcements?tenantCode=master");
|
||||
var faqs = await ReadItemsAsync(faqResponse);
|
||||
var announcements = await ReadItemsAsync(announcementResponse);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, faqResponse.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.OK, announcementResponse.StatusCode);
|
||||
Assert.Equal("如何购买?", Assert.Single(faqs).GetProperty("question").GetString());
|
||||
Assert.Equal("暑期活动", Assert.Single(announcements).GetProperty("content").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Exam_dates_support_region_school_filters_and_days_left()
|
||||
{
|
||||
var tenantId = Guid.NewGuid();
|
||||
var regionId = Guid.NewGuid();
|
||||
var schoolId = Guid.NewGuid();
|
||||
var futureExamAt = new DateTimeOffset(DateTimeOffset.UtcNow.Date.AddDays(10), TimeSpan.Zero);
|
||||
await using var factory = new ApiTestFactory();
|
||||
await factory.SeedAsync(
|
||||
Tenant(tenantId, "master"),
|
||||
new Region { Id = regionId, TenantId = tenantId, Name = "浙江" },
|
||||
new School { Id = schoolId, TenantId = tenantId, RegionId = regionId, Name = "测试院校" },
|
||||
new ExamDate
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenantId,
|
||||
RegionId = regionId,
|
||||
SchoolId = schoolId,
|
||||
ExamName = "校考",
|
||||
ExamAt = futureExamAt,
|
||||
IsActive = true
|
||||
},
|
||||
new ExamDate
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenantId,
|
||||
ExamName = "通用考试",
|
||||
ExamAt = futureExamAt.AddDays(1),
|
||||
IsActive = true
|
||||
},
|
||||
new ExamDate
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenantId,
|
||||
RegionId = Guid.NewGuid(),
|
||||
SchoolId = Guid.NewGuid(),
|
||||
ExamName = "其他地区学校考试",
|
||||
ExamAt = futureExamAt.AddDays(2),
|
||||
IsActive = true
|
||||
},
|
||||
new ExamDate
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenantId,
|
||||
RegionId = regionId,
|
||||
SchoolId = schoolId,
|
||||
ExamName = "停用考试",
|
||||
IsActive = false
|
||||
});
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
using var response = await client.GetAsync(
|
||||
$"/api/catalog/exam-dates?tenantCode=master®ionId={regionId}&schoolId={schoolId}");
|
||||
var items = await ReadItemsAsync(response);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal(["校考", "通用考试"], items.Select(item => item.GetProperty("examName").GetString()!).ToArray());
|
||||
Assert.Equal(10, items[0].GetProperty("daysLeft").GetInt32());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Products_filter_active_region_type_keyword_and_limit()
|
||||
{
|
||||
var tenantId = Guid.NewGuid();
|
||||
var regionId = Guid.NewGuid();
|
||||
await using var factory = new ApiTestFactory();
|
||||
await factory.SeedAsync(
|
||||
Tenant(tenantId, "master"),
|
||||
new Region { Id = regionId, TenantId = tenantId, Name = "浙江" },
|
||||
new Product
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenantId,
|
||||
RegionId = regionId,
|
||||
Title = "联考资料 B",
|
||||
Type = ProductType.Material,
|
||||
SortOrder = 2,
|
||||
LegacyPriceText = "¥20",
|
||||
IsActive = true
|
||||
},
|
||||
new Product
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenantId,
|
||||
RegionId = regionId,
|
||||
Title = "联考资料 A",
|
||||
Type = ProductType.Material,
|
||||
SortOrder = 1,
|
||||
LegacyPriceText = "¥10",
|
||||
IsActive = true
|
||||
},
|
||||
new Product
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenantId,
|
||||
RegionId = regionId,
|
||||
Title = "联考课程",
|
||||
Type = ProductType.Course,
|
||||
IsActive = true
|
||||
},
|
||||
new Product
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenantId,
|
||||
RegionId = regionId,
|
||||
Title = "停用资料",
|
||||
Type = ProductType.Material,
|
||||
IsActive = false
|
||||
});
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
using var response = await client.GetAsync(
|
||||
$"/api/catalog/products?tenantCode=master®ionId={regionId}&type=material&keyword=联考&limit=1");
|
||||
var items = await ReadItemsAsync(response);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var item = Assert.Single(items);
|
||||
Assert.Equal("联考资料 A", item.GetProperty("title").GetString());
|
||||
Assert.Equal("¥10", item.GetProperty("priceLabel").GetString());
|
||||
Assert.True(item.GetProperty("isActive").GetBoolean());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Svip_plans_filter_active_region_and_include_global_plans()
|
||||
{
|
||||
var tenantId = Guid.NewGuid();
|
||||
var regionId = Guid.NewGuid();
|
||||
await using var factory = new ApiTestFactory();
|
||||
await factory.SeedAsync(
|
||||
Tenant(tenantId, "master"),
|
||||
new Region { Id = regionId, TenantId = tenantId, Name = "浙江" },
|
||||
new SvipPlan
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenantId,
|
||||
RegionId = regionId,
|
||||
Name = "地区月卡",
|
||||
PriceCents = 3000,
|
||||
Days = 30,
|
||||
SortOrder = 2,
|
||||
IsActive = true
|
||||
},
|
||||
new SvipPlan
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenantId,
|
||||
Name = "通用周卡",
|
||||
PriceCents = 900,
|
||||
Days = 7,
|
||||
SortOrder = 1,
|
||||
IsActive = true
|
||||
},
|
||||
new SvipPlan
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenantId,
|
||||
RegionId = Guid.NewGuid(),
|
||||
Name = "其他地区卡",
|
||||
PriceCents = 1000,
|
||||
Days = 7,
|
||||
IsActive = true
|
||||
},
|
||||
new SvipPlan
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenantId,
|
||||
RegionId = regionId,
|
||||
Name = "停用卡",
|
||||
PriceCents = 1,
|
||||
Days = 1,
|
||||
IsActive = false
|
||||
});
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
using var response = await client.GetAsync($"/api/catalog/svip-plans?tenantCode=master®ionId={regionId}");
|
||||
var items = await ReadItemsAsync(response);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal(["通用周卡", "地区月卡"], items.Select(item => item.GetProperty("name").GetString()!).ToArray());
|
||||
Assert.Equal(9m, items[0].GetProperty("price").GetDecimal());
|
||||
}
|
||||
|
||||
private static Tenant Tenant(Guid id, string slug)
|
||||
{
|
||||
return new Tenant
|
||||
|
||||
@@ -556,6 +556,23 @@ public sealed class PersistenceModelTests
|
||||
{
|
||||
using var context = new TikuDbContext(Options);
|
||||
|
||||
var productType = context.Model.FindEntityType(typeof(Product))!;
|
||||
var isActive = productType.FindProperty(nameof(Product.IsActive))!;
|
||||
var productsTable = StoreObjectIdentifier.Table("products", null);
|
||||
Assert.Equal("is_active", isActive.GetColumnName(productsTable));
|
||||
Assert.Contains(
|
||||
productType.GetIndexes(),
|
||||
index => !index.IsUnique
|
||||
&& index.Properties
|
||||
.Select(property => property.Name)
|
||||
.SequenceEqual([
|
||||
nameof(Product.TenantId),
|
||||
nameof(Product.RegionId),
|
||||
nameof(Product.Type),
|
||||
nameof(Product.IsActive),
|
||||
nameof(Product.SortOrder)
|
||||
]));
|
||||
|
||||
AssertHasUniqueIndex<Order>(nameof(Order.TenantId), nameof(Order.OrderNo));
|
||||
AssertHasUniqueIndex<ActivationCode>(nameof(ActivationCode.TenantId), nameof(ActivationCode.Code));
|
||||
AssertHasUniqueIndex<TenantPaymentAccount>(
|
||||
|
||||
Reference in New Issue
Block a user