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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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));
}
}
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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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));
}
}