using EARSystem.Core; using EARSystem.Infrastructure; using EARSystem.Web.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace EARSystem.Web.Controllers; [Authorize(Roles = "Admin")] public class AdminController(AppDbContext db) : Controller { public async Task Index(CancellationToken ct) { ViewBag.Users = await db.Users.CountAsync(ct); ViewBag.Recommendations = await db.Recommendations.CountAsync(ct); var durations = await db.Recommendations.Where(x => x.Mode == DataMode.Demo).OrderByDescending(x => x.CreatedAt) .Select(x => x.DurationMs).Take(100).ToArrayAsync(ct); ViewBag.Latency = durations.Length > 0 ? Math.Round(durations.Average(), 1) : 0; ViewBag.Rule = await db.Rules.AsNoTracking().SingleAsync(ct); return View(await db.Places.AsNoTracking().Include(x => x.Atmosphere).OrderBy(x => x.Source).ThenBy(x => x.Name).ToArrayAsync(ct)); } [HttpGet] public async Task Edit(Guid? id, CancellationToken ct) { if (id == null) return View(new AdminPlaceInput()); var p = await db.Places.AsNoTracking().Include(x => x.Atmosphere).SingleOrDefaultAsync(x => x.Id == id, ct); if (p == null) return NotFound(); var a = p.Atmosphere ?? new(); return View(new AdminPlaceInput { Id = p.Id, Name = p.Name, Address = p.Address, Description = p.Description, Category = p.Category, Source = p.Source, Longitude = p.Longitude, Latitude = p.Latitude, Price = p.Price, Rating = p.Rating, OpenHour = p.OpenHour, CloseHour = p.CloseHour, Quiet = a.Quiet, Comfort = a.Comfort, Nature = a.Nature, Liveliness = a.Liveliness, Social = a.Social, Focus = a.Focus, BaselineCount = a.BaselineCount, Evidence = a.Evidence }); } [HttpPost] public async Task Edit(AdminPlaceInput input, CancellationToken ct) { if (!Enum.IsDefined(input.Source) || !Enum.IsDefined(input.Category)) ModelState.AddModelError("", "类别或来源无效。"); if (input.Source == PlaceSource.Collected && input.BaselineCount > 0 && string.IsNullOrWhiteSpace(input.Evidence)) ModelState.AddModelError("", "真实采集基线必须填写采集来源及样本说明。"); if (!ModelState.IsValid) return View(input); var p = input.Id.HasValue ? await db.Places.Include(x => x.Atmosphere).SingleOrDefaultAsync(x => x.Id == input.Id, ct) : new Place(); if (p == null) return NotFound(); if (input.Id.HasValue && p.Source != input.Source) { ModelState.AddModelError("", "已有地点的数据来源不可更改,请新建独立地点以保持数据隔离。"); return View(input); } p.Name = input.Name; p.Address = input.Address; p.Description = input.Description; p.Category = input.Category; p.Source = input.Source; p.Longitude = input.Longitude; p.Latitude = input.Latitude; p.Price = input.Price; p.Rating = input.Rating; p.OpenHour = input.OpenHour; p.CloseHour = input.CloseHour; p.Atmosphere ??= new() { PlaceId = p.Id }; p.Atmosphere.Quiet = input.Quiet; p.Atmosphere.Comfort = input.Comfort; p.Atmosphere.Nature = input.Nature; p.Atmosphere.Liveliness = input.Liveliness; p.Atmosphere.Social = input.Social; p.Atmosphere.Focus = input.Focus; p.Atmosphere.BaselineCount = input.BaselineCount; p.Atmosphere.Evidence = input.Evidence; if (!input.Id.HasValue) db.Places.Add(p); await db.SaveChangesAsync(ct); TempData["Notice"] = "地点已保存。"; return RedirectToAction(nameof(Index)); } [HttpPost] public async Task Rules(RuleInput input, CancellationToken ct) { if (!input.Weights.IsValid()) return BadRequest(new { title = "权重须为 0–1,且六项之和等于 1。" }); var rule = await db.Rules.SingleAsync(ct); if (rule.ConcurrencyStamp != input.ConcurrencyStamp) return Conflict(new { title = "配置已修改,请刷新后重试。" }); rule.WeightsJson = Labels.Json(input.Weights); rule.Version = DateTimeOffset.UtcNow.ToString("yyyyMMddHHmmssfff"); rule.ConcurrencyStamp = Guid.NewGuid(); await db.SaveChangesAsync(ct); TempData["Notice"] = "推荐权重已更新,新推荐将使用新版本。"; return RedirectToAction(nameof(Index)); } [HttpPost] public async Task Delete(Guid id, CancellationToken ct) { var p = await db.Places.FindAsync([id], ct); if (p == null) return NotFound(); if (await db.Experiences.AnyAsync(x => x.PlaceId == id, ct) || await db.Favorites.AnyAsync(x => x.PlaceId == id, ct)) { TempData["Notice"] = "地点已有收藏或体验,请保留记录并编辑信息。"; return RedirectToAction(nameof(Index)); } db.Places.Remove(p); await db.SaveChangesAsync(ct); return RedirectToAction(nameof(Index)); } }