refactor(web-client-tpos): convert BFF from direct DB to HTTP proxy
- Replace Dapper/Npgsql direct DB access with HttpClient proxy to microservice APIs - Create BffHttpClient.cs with AuthForwardingHandler (forwards JWT tokens) - Register 9 named HttpClients: Merchant, Catalog, Order, Inventory, Membership, Wallet, Promotion, Booking, FnbEngine - Delete BffDbConnectionFactory.cs and TenantContext.cs (no more direct DB) - Remove Dapper and Npgsql package references from .csproj - All 10 controllers are now thin HTTP proxy bridges - Zero breaking changes: all api/bff/ routes preserved
This commit is contained in:
@@ -1,27 +1,22 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Dapper;
|
||||
using WebClientTpos.Server.Infrastructure;
|
||||
using WebClientTpos.Server.Models;
|
||||
|
||||
namespace WebClientTpos.Server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Shop management controller — CRUD for shops, settings, stats, and devices.
|
||||
/// All endpoints are scoped to the current merchant's shops.
|
||||
/// VI: Controller quản lý cửa hàng — CRUD cho shops, settings, stats và devices.
|
||||
/// Tất cả endpoints đều lọc theo shops của merchant hiện tại.
|
||||
/// EN: Shop management controller — proxies to MerchantService for shop CRUD, settings, stats, devices.
|
||||
/// VI: Controller quản lý cửa hàng — proxy đến MerchantService cho CRUD shop, settings, stats, devices.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/bff")]
|
||||
public class ShopController : ControllerBase
|
||||
{
|
||||
private readonly TenantContext _tenant;
|
||||
private readonly BffDbConnectionFactory _dbFactory;
|
||||
private readonly HttpClient _merchant;
|
||||
|
||||
public ShopController(TenantContext tenant, BffDbConnectionFactory dbFactory)
|
||||
public ShopController(IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_tenant = tenant;
|
||||
_dbFactory = dbFactory;
|
||||
_merchant = httpClientFactory.CreateClient("MerchantService");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -29,231 +24,54 @@ public class ShopController : ControllerBase
|
||||
/// VI: Lấy tất cả cửa hàng thuộc merchant hiện tại.
|
||||
/// </summary>
|
||||
[HttpGet("shops")]
|
||||
public async Task<IActionResult> GetShops()
|
||||
{
|
||||
var merchantId = await _tenant.GetMerchantIdAsync();
|
||||
if (merchantId == null)
|
||||
return Ok(Array.Empty<object>());
|
||||
|
||||
await using var conn = _dbFactory.CreateConnection("merchant_service");
|
||||
var shops = await conn.QueryAsync<dynamic>(
|
||||
@"SELECT s.id, s.name, s.slug, s.description, s.phone, s.email,
|
||||
s.open_time, s.close_time, s.features_config,
|
||||
bc.name as category, st.name as status
|
||||
FROM shops s
|
||||
JOIN business_categories bc ON s.category_id = bc.id
|
||||
JOIN shop_statuses st ON s.status_id = st.id
|
||||
WHERE s.merchant_id = @MerchantId AND s.is_deleted = false
|
||||
ORDER BY s.name",
|
||||
new { MerchantId = merchantId });
|
||||
return Ok(shops);
|
||||
}
|
||||
public Task<IActionResult> GetShops() =>
|
||||
_merchant.GetAsync("/api/v1/shops").ProxyAsync();
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get shop by ID — validates merchant ownership.
|
||||
/// VI: Lấy cửa hàng theo ID — kiểm tra quyền sở hữu merchant.
|
||||
/// EN: Get shop by ID.
|
||||
/// VI: Lấy cửa hàng theo ID.
|
||||
/// </summary>
|
||||
[HttpGet("shops/{shopId:guid}")]
|
||||
public async Task<IActionResult> GetShopById(Guid shopId)
|
||||
{
|
||||
var merchantId = await _tenant.GetMerchantIdAsync();
|
||||
if (merchantId == null)
|
||||
return NotFound(new { message = "Shop not found" });
|
||||
|
||||
await using var conn = _dbFactory.CreateConnection("merchant_service");
|
||||
var shop = await conn.QueryFirstOrDefaultAsync<dynamic>(
|
||||
@"SELECT s.id, s.name, s.slug, s.description, s.phone, s.email,
|
||||
s.open_time, s.close_time,
|
||||
bc.name as category, st.name as status
|
||||
FROM shops s
|
||||
JOIN business_categories bc ON s.category_id = bc.id
|
||||
JOIN shop_statuses st ON s.status_id = st.id
|
||||
WHERE s.id = @ShopId AND s.merchant_id = @MerchantId AND s.is_deleted = false",
|
||||
new { ShopId = shopId, MerchantId = merchantId });
|
||||
|
||||
if (shop == null)
|
||||
return NotFound(new { message = "Shop not found" });
|
||||
|
||||
return Ok(shop);
|
||||
}
|
||||
public Task<IActionResult> GetShopById(Guid shopId) =>
|
||||
_merchant.GetAsync($"/api/v1/shops/{shopId}").ProxyAsync();
|
||||
|
||||
/// <summary>
|
||||
/// EN: Update shop info — validates ownership.
|
||||
/// VI: Cập nhật thông tin cửa hàng — kiểm tra quyền sở hữu.
|
||||
/// EN: Update shop info.
|
||||
/// VI: Cập nhật thông tin cửa hàng.
|
||||
/// </summary>
|
||||
[HttpPut("shops/{shopId:guid}")]
|
||||
public async Task<IActionResult> UpdateShop(Guid shopId, [FromBody] UpdateShopRequest req)
|
||||
{
|
||||
var merchantId = await _tenant.GetMerchantIdAsync();
|
||||
if (merchantId == null) return Unauthorized();
|
||||
|
||||
var myShopIds = await _tenant.GetShopIdsAsync();
|
||||
if (!myShopIds.Contains(shopId))
|
||||
return Forbid();
|
||||
|
||||
await using var conn = _dbFactory.CreateConnection("merchant_service");
|
||||
var rows = await conn.ExecuteAsync(
|
||||
@"UPDATE shops SET name=@Name, phone=@Phone, email=@Email, description=@Description,
|
||||
open_time=@OpenTime, close_time=@CloseTime, updated_at=NOW()
|
||||
WHERE id=@ShopId AND id = ANY(@ShopIds)",
|
||||
new { req.Name, req.Phone, req.Email, req.Description,
|
||||
req.OpenTime, req.CloseTime, ShopId = shopId, ShopIds = myShopIds.ToArray() });
|
||||
return rows > 0 ? Ok(new { id = shopId }) : NotFound();
|
||||
}
|
||||
public Task<IActionResult> UpdateShop(Guid shopId, [FromBody] JsonElement body) =>
|
||||
_merchant.PutAsJsonAsync($"/api/v1/shops/{shopId}", body).ProxyAsync();
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get shop settings (features_config, open/close time, open days).
|
||||
/// VI: Lấy cài đặt cửa hàng (features_config, giờ mở/đóng cửa, ngày mở cửa).
|
||||
/// EN: Get shop settings.
|
||||
/// VI: Lấy cài đặt cửa hàng.
|
||||
/// </summary>
|
||||
[HttpGet("shops/{shopId:guid}/settings")]
|
||||
public async Task<IActionResult> GetShopSettings(Guid shopId)
|
||||
{
|
||||
var merchantId = await _tenant.GetMerchantIdAsync();
|
||||
if (merchantId == null) return NotFound();
|
||||
|
||||
await using var conn = _dbFactory.CreateConnection("merchant_service");
|
||||
var settings = await conn.QueryFirstOrDefaultAsync<dynamic>(
|
||||
@"SELECT features_config::text as features_config,
|
||||
open_time::text as open_time,
|
||||
close_time::text as close_time,
|
||||
open_days
|
||||
FROM shops
|
||||
WHERE id = @ShopId AND merchant_id = @MerchantId AND is_deleted = false",
|
||||
new { ShopId = shopId, MerchantId = merchantId });
|
||||
|
||||
if (settings == null) return NotFound();
|
||||
return Ok(settings);
|
||||
}
|
||||
public Task<IActionResult> GetShopSettings(Guid shopId) =>
|
||||
_merchant.GetAsync($"/api/v1/shops/{shopId}/settings").ProxyAsync();
|
||||
|
||||
/// <summary>
|
||||
/// EN: Update shop settings — validates ownership.
|
||||
/// VI: Cập nhật cài đặt cửa hàng — kiểm tra quyền sở hữu.
|
||||
/// EN: Update shop settings.
|
||||
/// VI: Cập nhật cài đặt cửa hàng.
|
||||
/// </summary>
|
||||
[HttpPut("shops/{shopId:guid}/settings")]
|
||||
public async Task<IActionResult> UpdateShopSettings(Guid shopId, [FromBody] UpdateShopSettingsRequest req)
|
||||
{
|
||||
var merchantId = await _tenant.GetMerchantIdAsync();
|
||||
if (merchantId == null) return Unauthorized();
|
||||
|
||||
var myShopIds = await _tenant.GetShopIdsAsync();
|
||||
if (!myShopIds.Contains(shopId)) return Forbid();
|
||||
|
||||
await using var conn = _dbFactory.CreateConnection("merchant_service");
|
||||
try
|
||||
{
|
||||
await conn.ExecuteAsync(
|
||||
@"UPDATE shops
|
||||
SET features_config = @FeaturesConfig::jsonb,
|
||||
open_time = @OpenTime::time,
|
||||
close_time = @CloseTime::time,
|
||||
open_days = @OpenDays,
|
||||
updated_at = NOW()
|
||||
WHERE id = @ShopId AND id = ANY(@ShopIds)",
|
||||
new
|
||||
{
|
||||
ShopId = shopId,
|
||||
ShopIds = myShopIds.ToArray(),
|
||||
FeaturesConfig = string.IsNullOrWhiteSpace(req.FeaturesConfig) ? "{}" : req.FeaturesConfig,
|
||||
OpenTime = string.IsNullOrWhiteSpace(req.OpenTime) ? (object)DBNull.Value : req.OpenTime,
|
||||
CloseTime= string.IsNullOrWhiteSpace(req.CloseTime) ? (object)DBNull.Value : req.CloseTime,
|
||||
OpenDays = req.OpenDays
|
||||
});
|
||||
return Ok(new { success = true });
|
||||
}
|
||||
catch (Exception ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
public Task<IActionResult> UpdateShopSettings(Guid shopId, [FromBody] JsonElement body) =>
|
||||
_merchant.PutAsJsonAsync($"/api/v1/shops/{shopId}/settings", body).ProxyAsync();
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get aggregated stats per shop — scoped to current merchant.
|
||||
/// VI: Lấy thống kê tổng hợp theo shop — lọc theo merchant hiện tại.
|
||||
/// EN: Get aggregated stats per shop.
|
||||
/// VI: Lấy thống kê tổng hợp theo shop.
|
||||
/// </summary>
|
||||
[HttpGet("shops/stats")]
|
||||
public async Task<IActionResult> GetShopStats()
|
||||
{
|
||||
var merchantId = await _tenant.GetMerchantIdAsync();
|
||||
if (merchantId == null)
|
||||
return Ok(Array.Empty<object>());
|
||||
|
||||
var myShopIds = await _tenant.GetShopIdsAsync();
|
||||
if (!myShopIds.Any())
|
||||
return Ok(Array.Empty<object>());
|
||||
|
||||
var shopIdsArray = myShopIds.ToArray();
|
||||
|
||||
// EN: Products per shop (scoped) / VI: Số sản phẩm mỗi shop
|
||||
Dictionary<Guid, int> productCounts = new();
|
||||
try
|
||||
{
|
||||
await using var catConn = _dbFactory.CreateConnection("catalog_service");
|
||||
var prodStats = await catConn.QueryAsync<dynamic>(
|
||||
"SELECT shop_id, COUNT(*) as cnt FROM products WHERE is_active = true AND shop_id = ANY(@ShopIds) GROUP BY shop_id",
|
||||
new { ShopIds = shopIdsArray });
|
||||
foreach (var ps in prodStats)
|
||||
productCounts[(Guid)ps.shop_id] = (int)(long)ps.cnt;
|
||||
}
|
||||
catch { /* catalog_service may not have data yet */ }
|
||||
|
||||
// EN: Orders per shop + revenue (scoped) / VI: Số đơn + doanh thu mỗi shop
|
||||
Dictionary<Guid, int> orderCounts = new();
|
||||
Dictionary<Guid, decimal> revenues = new();
|
||||
try
|
||||
{
|
||||
await using var orderConn = _dbFactory.CreateConnection("order_service");
|
||||
var orderStats = await orderConn.QueryAsync<dynamic>(
|
||||
"SELECT shop_id, COUNT(*) as cnt, COALESCE(SUM(total_amount), 0) as revenue FROM orders WHERE shop_id = ANY(@ShopIds) GROUP BY shop_id",
|
||||
new { ShopIds = shopIdsArray });
|
||||
foreach (var os in orderStats)
|
||||
{
|
||||
orderCounts[(Guid)os.shop_id] = (int)(long)os.cnt;
|
||||
revenues[(Guid)os.shop_id] = (decimal)os.revenue;
|
||||
}
|
||||
}
|
||||
catch { /* order_service may not have data yet */ }
|
||||
|
||||
// EN: Staff per shop (scoped) / VI: Số nhân viên mỗi shop
|
||||
Dictionary<Guid, int> staffCounts = new();
|
||||
try
|
||||
{
|
||||
await using var mConn = _dbFactory.CreateConnection("merchant_service");
|
||||
var staffStats = await mConn.QueryAsync<dynamic>(
|
||||
@"SELECT sm.shop_id, COUNT(DISTINCT sm.staff_id) as cnt
|
||||
FROM shop_members sm
|
||||
JOIN merchant_staff ms ON sm.staff_id = ms.id
|
||||
JOIN staff_statuses ss ON ms.status_id = ss.id
|
||||
WHERE ss.name = 'Active' AND sm.shop_id = ANY(@ShopIds)
|
||||
GROUP BY sm.shop_id",
|
||||
new { ShopIds = shopIdsArray });
|
||||
foreach (var ss in staffStats)
|
||||
staffCounts[(Guid)ss.shop_id] = (int)(long)ss.cnt;
|
||||
}
|
||||
catch { /* merchant_service may not have data yet */ }
|
||||
|
||||
var result = myShopIds.Select(shopId => new
|
||||
{
|
||||
shop_id = shopId,
|
||||
product_count = productCounts.GetValueOrDefault(shopId, 0),
|
||||
order_count = orderCounts.GetValueOrDefault(shopId, 0),
|
||||
staff_count = staffCounts.GetValueOrDefault(shopId, 0),
|
||||
revenue = revenues.GetValueOrDefault(shopId, 0m)
|
||||
});
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
public Task<IActionResult> GetShopStats() =>
|
||||
_merchant.GetAsync("/api/v1/shops/stats").ProxyAsync();
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get device tokens registered for this merchant's staff.
|
||||
/// VI: Lấy danh sách device token đã đăng ký cho nhân viên của merchant.
|
||||
/// </summary>
|
||||
[HttpGet("devices")]
|
||||
public async Task<IActionResult> GetDevices()
|
||||
{
|
||||
await using var conn = _dbFactory.CreateConnection("merchant_service");
|
||||
var devices = await conn.QueryAsync<dynamic>(
|
||||
@"SELECT dt.id, dt.device_token, dt.platform, dt.is_active, dt.created_at,
|
||||
ms.employee_code as staff_code
|
||||
FROM device_tokens dt
|
||||
LEFT JOIN merchant_staff ms ON dt.staff_id = ms.id
|
||||
ORDER BY dt.created_at DESC");
|
||||
return Ok(devices);
|
||||
}
|
||||
public Task<IActionResult> GetDevices() =>
|
||||
_merchant.GetAsync("/api/v1/devices").ProxyAsync();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user