5839b3508f
Add the discovery layer: the denormalized nurse_search_index read model (one row per bookable variant x covered service area), maintained inline inside each source write's transaction, plus the single public search query behind the INurseSearch seam. - Entity + EF config + migration (search schema): covering search index, filtered-unique (variant_id, city_id, district_id) pair with NULL district participating, nurse_id index, soft-delete. - ISearchIndexMaintainer (write seam) + SearchIndexMaintainer: reindex variant / nurse / fan-out / remove-area / full rebuild, staged in the owning source write's unit of work; wired into the b3/b4/b5/b6 handlers. - INurseSearch (read seam) + SqlNurseSearch (real MVP backend): reads only is_searchable=1, category/city/district(NULL-aware)/gender/price filters, rating sort, pagination. Elasticsearch deferred (config Search:Backend). - SearchNursesQuery (+ validator) and RebuildSearchIndexCommand; public SearchController (GET search/nurses) + admin AdminSearchController (POST admin_search/rebuild_index). - Tests: 9 DB-backed maintainer/search + 4 WebApplicationFactory; updated affected b3/b4/b5/b6 handler tests. Build clean, 167 tests green. - Docs: server CLAUDE.md project map, contract search.md, swagger refresh, handoff, report, mocks-registry rows, STATUS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
44 lines
1.9 KiB
C#
44 lines
1.9 KiB
C#
#nullable enable
|
|
using System.ComponentModel.DataAnnotations;
|
|
using Asp.Versioning;
|
|
using Baya.Application.Features.Search.Queries.SearchNurses;
|
|
using Baya.Application.Models.Common;
|
|
using Baya.Application.Models.Search;
|
|
using Baya.WebFramework.Attributes;
|
|
using Baya.WebFramework.BaseController;
|
|
using Mediator;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
namespace Baya.Web.Api.Controllers.V1;
|
|
|
|
/// <summary>
|
|
/// Public nurse discovery. Pre-auth (families browse before signing in) and covered by the per-IP global
|
|
/// rate limiter. Reads only searchable (verified + accepting + active) rows via the <c>INurseSearch</c> seam.
|
|
/// </summary>
|
|
[ApiVersion("1")]
|
|
[ApiController]
|
|
[Route("api/v{version:apiVersion}/[controller]")]
|
|
[Display(Description = "Public nurse search: category + city/district geo, same-gender filter, price range, rating sort")]
|
|
public sealed class SearchController(ISender sender) : BaseController
|
|
{
|
|
[HttpGet("[action]")]
|
|
[ProducesOkApiResponseType<PagedResult<NurseSearchResultDto>>]
|
|
public async Task<IActionResult> Nurses(
|
|
[FromQuery(Name = "service_category_id")] long serviceCategoryId,
|
|
[FromQuery(Name = "city_id")] long cityId,
|
|
[FromQuery(Name = "district_id")] long? districtId,
|
|
[FromQuery(Name = "nurse_gender")] string? nurseGender,
|
|
[FromQuery(Name = "min_price")] long? minPrice,
|
|
[FromQuery(Name = "max_price")] long? maxPrice,
|
|
[FromQuery(Name = "price_unit")] string? priceUnit,
|
|
[FromQuery(Name = "page")] int page,
|
|
[FromQuery(Name = "page_size")] int pageSize,
|
|
CancellationToken cancellationToken)
|
|
=> OperationResult(await sender.Send(
|
|
new SearchNursesQuery(
|
|
serviceCategoryId, cityId, districtId, nurseGender, minPrice, maxPrice, priceUnit,
|
|
page <= 0 ? 1 : page,
|
|
pageSize <= 0 ? Application.Common.Pagination.DefaultPageSize : pageSize),
|
|
cancellationToken));
|
|
}
|