using System.Security.Claims;
using EARSystem.Core;
using EARSystem.Infrastructure;
using EARSystem.Web.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.EntityFrameworkCore;
namespace EARSystem.Web.Controllers;
[Authorize, ApiController, Route("api")]
public class ApiController(AppDbContext db, IRecommendationService recommendations, IExperienceService experiences,
EmotionTagAnalyzer tags, TimeProvider clock, AMapClient map) : ControllerBase
{
private string UserId => User.FindFirstValue(ClaimTypes.NameIdentifier)!;
private static object MoodDto(MoodEntry x) => new { x.Id, x.Mood, x.Energy, x.Stress, x.Thoughts,
Tags = Labels.Read<string>(x.TagsJson), x.RecordedAt, x.Version, x.ExperienceId };
[HttpGet("moods")]
public async Task<IActionResult> Moods([FromQuery] DateOnly? date, [FromQuery] int days = 30, CancellationToken ct = default) {
if (days is < 1 or > 365) return BadRequest(new { title = "时间范围须为 1–365 天。" });
var start = date.HasValue ? ChinaTime.Start(date.Value) : clock.GetUtcNow().AddDays(-days);
var end = date.HasValue ? start.AddDays(1) : clock.GetUtcNow().AddMinutes(1);
var items = await db.Moods.AsNoTracking().Where(x => x.UserId == UserId && x.RecordedAt >= start && x.RecordedAt < end)
.OrderByDescending(x => x.RecordedAt).ThenByDescending(x => x.CreatedAt).Take(500).ToArrayAsync(ct);
return Ok(items.Select(MoodDto));
}
[HttpPost("moods")]
public async Task<IActionResult> CreateMood(MoodInput input, CancellationToken ct) {
var item = new MoodEntry { UserId = UserId, Mood = input.Mood, Energy = input.Energy, Stress = input.Stress,
Thoughts = input.Thoughts, TagsJson = Labels.Json(input.Tags.Distinct()), RecordedAt = input.RecordedAt, CreatedAt = clock.GetUtcNow() };
db.Moods.Add(item); await db.SaveChangesAsync(ct);
return Created("/api/moods/" + item.Id, MoodDto(item));
}
[HttpPut("moods/{id:guid}")]
public async Task<IActionResult> UpdateMood(Guid id, MoodInput input, CancellationToken ct) {
var item = await db.Moods.SingleOrDefaultAsync(x => x.Id == id && x.UserId == UserId, ct);
if (item == null) return NotFound();
if (input.Version != item.Version) return Conflict(new { title = "记录已修改,请保留输入并重新加载最新记录。" });
item.Mood = input.Mood; item.Energy = input.Energy; item.Stress = input.Stress;
item.Thoughts = input.Thoughts; item.TagsJson = Labels.Json(input.Tags.Distinct()); item.RecordedAt = input.RecordedAt;
item.Version = Guid.NewGuid(); await db.SaveChangesAsync(ct);
return Ok(MoodDto(item));
}
[HttpDelete("moods/{id:guid}")]
public async Task<IActionResult> DeleteMood(Guid id, [FromQuery] Guid version, CancellationToken ct) {
var item = await db.Moods.SingleOrDefaultAsync(x => x.Id == id && x.UserId == UserId, ct);
if (item == null) return NotFound();
if (version != item.Version) return Conflict(new { title = "记录已修改,请刷新后再删除。" });
db.Moods.Remove(item); await db.SaveChangesAsync(ct); return NoContent();
}
[HttpPost("emotion-tags/preview")]
public IActionResult Tags(TagInput input) => Ok(new { suggestions = tags.Analyze(input.Text), basis = "文字规则提示,请自行确认后添加。" });
[HttpPost("recommendations"), EnableRateLimiting("maps")]
public async Task<IActionResult> Recommend(RecommendationInput input, CancellationToken ct) =>
Ok(await recommendations.RecommendAsync(UserId, input.Query(), ct));
[HttpGet("experiences")]
public async Task<IActionResult> Experiences(CancellationToken ct) {
var items = await db.Experiences.AsNoTracking().Include(x => x.Place).Where(x => x.UserId == UserId)
.OrderByDescending(x => x.VisitedAt).Take(250).ToArrayAsync(ct);
var synced = await db.Moods.AsNoTracking().Where(x => x.UserId == UserId && x.ExperienceId != null)
.ToDictionaryAsync(x => x.ExperienceId!.Value, ct);
return Ok(items.Select(x => new { x.Id, x.PlaceId, placeName = x.Place.Name, source = x.Place.Source.ToString(),
x.VisitedAt, x.Satisfaction, x.MoodBefore, x.MoodAfter, x.Quiet, x.Comfort, x.Nature, x.Liveliness, x.Social, x.Focus,
x.Thoughts, x.Contribute, x.Version, syncDiary = synced.ContainsKey(x.Id),
energy = synced.TryGetValue(x.Id, out var mood) ? mood.Energy : 3,
stress = synced.TryGetValue(x.Id, out var stressMood) ? stressMood.Stress : 3 }));
}
[HttpPost("experiences")]
public async Task<IActionResult> CreateExperience(ExperienceInput input, CancellationToken ct) {
var item = await experiences.SaveAsync(UserId, input.Entity(), input.SyncDiary, input.Energy, input.Stress, cancellationToken: ct);
return Created("/api/experiences/" + item.Id, new { item.Id, item.Version });
}
[HttpPut("experiences/{id:guid}")]
public async Task<IActionResult> UpdateExperience(Guid id, ExperienceInput input, CancellationToken ct) {
if (!await db.Experiences.AnyAsync(x => x.Id == id && x.UserId == UserId, ct)) return NotFound();
var item = await experiences.SaveAsync(UserId, input.Entity(id), input.SyncDiary, input.Energy, input.Stress, input.Version, ct);
return Ok(new { item.Id, item.Version });
}
[HttpDelete("experiences/{id:guid}")]
public async Task<IActionResult> DeleteExperience(Guid id, [FromQuery] Guid version, CancellationToken ct) {
var existing = await db.Experiences.AsNoTracking().SingleOrDefaultAsync(x => x.UserId == UserId && x.Id == id, ct);
if (existing == null) return NotFound();
if (version != existing.Version) return Conflict(new { title = "体验已修改,请刷新后重试。" });
return await experiences.DeleteAsync(UserId, id, version, ct) ? NoContent() : NotFound();
}
[HttpGet("favorites")]
public async Task<IActionResult> Favorites(CancellationToken ct) {
var items = await db.Favorites.AsNoTracking().Include(x => x.Place).Where(x => x.UserId == UserId).OrderByDescending(x => x.CreatedAt).ToArrayAsync(ct);
var visited = await db.Experiences.Where(x => x.UserId == UserId).Select(x => x.PlaceId).Distinct().ToArrayAsync(ct);
return Ok(items.Select(x => new { x.PlaceId, x.Place.Name, category = x.Place.Category.ToString(), x.Place.Price, x.Place.Rating,
source = x.Place.Source.ToString(), visited = visited.Contains(x.PlaceId) }));
}
[HttpPost("favorites")]
public async Task<IActionResult> Favorite(FavoriteInput input, CancellationToken ct) {
if (!await db.Places.AnyAsync(x => x.Id == input.PlaceId, ct)) return BadRequest(new { title = "请收藏独立目录中的地点。实时第三方地点默认不入库。" });
if (!await db.Favorites.AnyAsync(x => x.UserId == UserId && x.PlaceId == input.PlaceId, ct)) {
db.Favorites.Add(new() { UserId = UserId, PlaceId = input.PlaceId }); await db.SaveChangesAsync(ct);
}
return Ok(new { saved = true });
}
[HttpDelete("favorites/{placeId:guid}")]
public async Task<IActionResult> RemoveFavorite(Guid placeId, CancellationToken ct) {
var item = await db.Favorites.SingleOrDefaultAsync(x => x.UserId == UserId && x.PlaceId == placeId, ct);
if (item == null) return NotFound();
db.Favorites.Remove(item); await db.SaveChangesAsync(ct); return NoContent();
}
[HttpGet("preferences")]
public async Task<IActionResult> Preferences(CancellationToken ct) {
var p = await db.Preferences.AsNoTracking().SingleOrDefaultAsync(x => x.UserId == UserId, ct) ?? new();
return Ok(new { favoriteCategories = Labels.Read<PlaceCategory>(p.FavoriteCategoriesJson),
blockedCategories = Labels.Read<PlaceCategory>(p.BlockedCategoriesJson), blockedPlaces = Labels.Read<Guid>(p.BlockedPlacesJson),
p.Budget, p.RadiusMeters });
}
[HttpPut("preferences")]
public async Task<IActionResult> SavePreferences(PreferenceInput input, CancellationToken ct) {
var p = await db.Preferences.SingleOrDefaultAsync(x => x.UserId == UserId, ct);
if (p == null) { p = new() { UserId = UserId }; db.Preferences.Add(p); }
p.FavoriteCategoriesJson = Labels.Json(input.FavoriteCategories.Distinct());
p.BlockedCategoriesJson = Labels.Json(input.BlockedCategories.Distinct());
p.BlockedPlacesJson = Labels.Json(input.BlockedPlaces.Distinct());
p.Budget = input.Budget; p.RadiusMeters = input.RadiusMeters;
await db.SaveChangesAsync(ct); return Ok(new { saved = true });
}
[HttpGet("reports/weekly")]
public async Task<IActionResult> Weekly([FromQuery] DateOnly? week, CancellationToken ct) {
var monday = ChinaTime.Monday(week ?? ChinaTime.Day(clock.GetUtcNow()));
var start = ChinaTime.Start(monday); var end = start.AddDays(7);
var moods = await db.Moods.AsNoTracking().Where(x => x.UserId == UserId && x.RecordedAt >= start && x.RecordedAt < end).ToArrayAsync(ct);
var daily = moods.GroupBy(x => ChinaTime.Day(x.RecordedAt)).Select(g => g.OrderByDescending(x => x.RecordedAt)
.ThenByDescending(x => x.CreatedAt).First()).ToArray();
var visits = await db.Experiences.AsNoTracking().Include(x => x.Place).Where(x => x.UserId == UserId && x.VisitedAt >= start && x.VisitedAt < end).ToArrayAsync(ct);
var favorite = visits.GroupBy(x => x.Place.Category).OrderByDescending(g => g.Count()).Select(g => Labels.Category(g.Key)).FirstOrDefault();
return Ok(new { week = monday.ToString("yyyy-MM-dd"), recordedDays = daily.Length,
mood = daily.Length > 0 ? (double?)Math.Round(daily.Average(x => x.Mood), 1) : null,
energy = daily.Length > 0 ? (double?)Math.Round(daily.Average(x => x.Energy), 1) : null,
stress = daily.Length > 0 ? (double?)Math.Round(daily.Average(x => x.Stress), 1) : null,
visits = visits.Length, satisfaction = visits.Length > 0 ? (double?)Math.Round(visits.Average(x => x.Satisfaction), 1) : null,
topCategory = favorite,
summary = daily.Length == 0 ? "这周还没有情绪记录。写下一个小片段,开始了解自己的节奏。"
: $"这周记录了 {daily.Length} 天,心情均值 {daily.Average(x => x.Mood):F1},到访 {visits.Length} 次。给每一次认真生活留一个印记。",
notice = "体验前后心情变化仅描述记录,不表示确定的因果关系。" });
}
[HttpGet("reports/trend")]
public async Task<IActionResult> Trend([FromQuery] int days = 30, CancellationToken ct = default) {
if (days is not (7 or 30)) return BadRequest();
var today = ChinaTime.Day(clock.GetUtcNow());
var start = ChinaTime.Start(today.AddDays(1 - days));
var items = await db.Moods.AsNoTracking().Where(x => x.UserId == UserId && x.RecordedAt >= start && x.RecordedAt <= clock.GetUtcNow()).ToArrayAsync(ct);
var daily = items.GroupBy(x => ChinaTime.Day(x.RecordedAt)).ToDictionary(g => g.Key, g => g.OrderByDescending(x => x.RecordedAt)
.ThenByDescending(x => x.CreatedAt).First());
return Ok(Enumerable.Range(0, days).Select(i => {
var day = today.AddDays(1 - days + i); daily.TryGetValue(day, out var mood);
return new { date = day.ToString("yyyy-MM-dd"), mood = mood?.Mood, energy = mood?.Energy, stress = mood?.Stress };
}));
}
[HttpPost("map/geocode"), EnableRateLimiting("maps")]
public async Task<IActionResult> Geocode([FromBody] TagInput input, CancellationToken ct) {
if (string.IsNullOrWhiteSpace(input.Text) || input.Text.Length > 200) return BadRequest();
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct); timeout.CancelAfter(TimeSpan.FromSeconds(8));
return Ok(await map.GeocodeAsync(input.Text, timeout.Token));
}
}
using System.Security.Claims;
using EARSystem.Core;
using EARSystem.Infrastructure;
using EARSystem.Web.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.EntityFrameworkCore;
namespace EARSystem.Web.Controllers;
[Authorize, ApiController, Route("api")]
public class ApiController(AppDbContext db, IRecommendationService recommendations, IExperienceService experiences,
EmotionTagAnalyzer tags, TimeProvider clock, AMapClient map) : ControllerBase
{
private string UserId => User.FindFirstValue(ClaimTypes.NameIdentifier)!;
private static object MoodDto(MoodEntry x) => new { x.Id, x.Mood, x.Energy, x.Stress, x.Thoughts,
Tags = Labels.Read<string>(x.TagsJson), x.RecordedAt, x.Version, x.ExperienceId };
[HttpGet("moods")]
public async Task<IActionResult> Moods([FromQuery] DateOnly? date, [FromQuery] int days = 30, CancellationToken ct = default) {
if (days is < 1 or > 365) return BadRequest(new { title = "时间范围须为 1–365 天。" });
var start = date.HasValue ? ChinaTime.Start(date.Value) : clock.GetUtcNow().AddDays(-days);
var end = date.HasValue ? start.AddDays(1) : clock.GetUtcNow().AddMinutes(1);
var items = await db.Moods.AsNoTracking().Where(x => x.UserId == UserId && x.RecordedAt >= start && x.RecordedAt < end)
.OrderByDescending(x => x.RecordedAt).ThenByDescending(x => x.CreatedAt).Take(500).ToArrayAsync(ct);
return Ok(items.Select(MoodDto));
}
[HttpPost("moods")]
public async Task<IActionResult> CreateMood(MoodInput input, CancellationToken ct) {
var item = new MoodEntry { UserId = UserId, Mood = input.Mood, Energy = input.Energy, Stress = input.Stress,
Thoughts = input.Thoughts, TagsJson = Labels.Json(input.Tags.Distinct()), RecordedAt = input.RecordedAt, CreatedAt = clock.GetUtcNow() };
db.Moods.Add(item); await db.SaveChangesAsync(ct);
return Created("/api/moods/" + item.Id, MoodDto(item));
}
[HttpPut("moods/{id:guid}")]
public async Task<IActionResult> UpdateMood(Guid id, MoodInput input, CancellationToken ct) {
var item = await db.Moods.SingleOrDefaultAsync(x => x.Id == id && x.UserId == UserId, ct);
if (item == null) return NotFound();
if (input.Version != item.Version) return Conflict(new { title = "记录已修改,请保留输入并重新加载最新记录。" });
item.Mood = input.Mood; item.Energy = input.Energy; item.Stress = input.Stress;
item.Thoughts = input.Thoughts; item.TagsJson = Labels.Json(input.Tags.Distinct()); item.RecordedAt = input.RecordedAt;
item.Version = Guid.NewGuid(); await db.SaveChangesAsync(ct);
return Ok(MoodDto(item));
}
[HttpDelete("moods/{id:guid}")]
public async Task<IActionResult> DeleteMood(Guid id, [FromQuery] Guid version, CancellationToken ct) {
var item = await db.Moods.SingleOrDefaultAsync(x => x.Id == id && x.UserId == UserId, ct);
if (item == null) return NotFound();
if (version != item.Version) return Conflict(new { title = "记录已修改,请刷新后再删除。" });
db.Moods.Remove(item); await db.SaveChangesAsync(ct); return NoContent();
}
[HttpPost("emotion-tags/preview")]
public IActionResult Tags(TagInput input) => Ok(new { suggestions = tags.Analyze(input.Text), basis = "文字规则提示,请自行确认后添加。" });
[HttpPost("recommendations"), EnableRateLimiting("maps")]
public async Task<IActionResult> Recommend(RecommendationInput input, CancellationToken ct) =>
Ok(await recommendations.RecommendAsync(UserId, input.Query(), ct));
[HttpGet("experiences")]
public async Task<IActionResult> Experiences(CancellationToken ct) {
var items = await db.Experiences.AsNoTracking().Include(x => x.Place).Where(x => x.UserId == UserId)
.OrderByDescending(x => x.VisitedAt).Take(250).ToArrayAsync(ct);
var synced = await db.Moods.AsNoTracking().Where(x => x.UserId == UserId && x.ExperienceId != null)
.ToDictionaryAsync(x => x.ExperienceId!.Value, ct);
return Ok(items.Select(x => new { x.Id, x.PlaceId, placeName = x.Place.Name, source = x.Place.Source.ToString(),
x.VisitedAt, x.Satisfaction, x.MoodBefore, x.MoodAfter, x.Quiet, x.Comfort, x.Nature, x.Liveliness, x.Social, x.Focus,
x.Thoughts, x.Contribute, x.Version, syncDiary = synced.ContainsKey(x.Id),
energy = synced.TryGetValue(x.Id, out var mood) ? mood.Energy : 3,
stress = synced.TryGetValue(x.Id, out var stressMood) ? stressMood.Stress : 3 }));
}
[HttpPost("experiences")]
public async Task<IActionResult> CreateExperience(ExperienceInput input, CancellationToken ct) {
var item = await experiences.SaveAsync(UserId, input.Entity(), input.SyncDiary, input.Energy, input.Stress, cancellationToken: ct);
return Created("/api/experiences/" + item.Id, new { item.Id, item.Version });
}
[HttpPut("experiences/{id:guid}")]
public async Task<IActionResult> UpdateExperience(Guid id, ExperienceInput input, CancellationToken ct) {
if (!await db.Experiences.AnyAsync(x => x.Id == id && x.UserId == UserId, ct)) return NotFound();
var item = await experiences.SaveAsync(UserId, input.Entity(id), input.SyncDiary, input.Energy, input.Stress, input.Version, ct);
return Ok(new { item.Id, item.Version });
}
[HttpDelete("experiences/{id:guid}")]
public async Task<IActionResult> DeleteExperience(Guid id, [FromQuery] Guid version, CancellationToken ct) {
var existing = await db.Experiences.AsNoTracking().SingleOrDefaultAsync(x => x.UserId == UserId && x.Id == id, ct);
if (existing == null) return NotFound();
if (version != existing.Version) return Conflict(new { title = "体验已修改,请刷新后重试。" });
return await experiences.DeleteAsync(UserId, id, version, ct) ? NoContent() : NotFound();
}
[HttpGet("favorites")]
public async Task<IActionResult> Favorites(CancellationToken ct) {
var items = await db.Favorites.AsNoTracking().Include(x => x.Place).Where(x => x.UserId == UserId).OrderByDescending(x => x.CreatedAt).ToArrayAsync(ct);
var visited = await db.Experiences.Where(x => x.UserId == UserId).Select(x => x.PlaceId).Distinct().ToArrayAsync(ct);
return Ok(items.Select(x => new { x.PlaceId, x.Place.Name, category = x.Place.Category.ToString(), x.Place.Price, x.Place.Rating,
source = x.Place.Source.ToString(), visited = visited.Contains(x.PlaceId) }));
}
[HttpPost("favorites")]
public async Task<IActionResult> Favorite(FavoriteInput input, CancellationToken ct) {
if (!await db.Places.AnyAsync(x => x.Id == input.PlaceId, ct)) return BadRequest(new { title = "请收藏独立目录中的地点。实时第三方地点默认不入库。" });
if (!await db.Favorites.AnyAsync(x => x.UserId == UserId && x.PlaceId == input.PlaceId, ct)) {
db.Favorites.Add(new() { UserId = UserId, PlaceId = input.PlaceId }); await db.SaveChangesAsync(ct);
}
return Ok(new { saved = true });
}
[HttpDelete("favorites/{placeId:guid}")]
public async Task<IActionResult> RemoveFavorite(Guid placeId, CancellationToken ct) {
var item = await db.Favorites.SingleOrDefaultAsync(x => x.UserId == UserId && x.PlaceId == placeId, ct);
if (item == null) return NotFound();
db.Favorites.Remove(item); await db.SaveChangesAsync(ct); return NoContent();
}
[HttpGet("preferences")]
public async Task<IActionResult> Preferences(CancellationToken ct) {
var p = await db.Preferences.AsNoTracking().SingleOrDefaultAsync(x => x.UserId == UserId, ct) ?? new();
return Ok(new { favoriteCategories = Labels.Read<PlaceCategory>(p.FavoriteCategoriesJson),
blockedCategories = Labels.Read<PlaceCategory>(p.BlockedCategoriesJson), blockedPlaces = Labels.Read<Guid>(p.BlockedPlacesJson),
p.Budget, p.RadiusMeters });
}
[HttpPut("preferences")]
public async Task<IActionResult> SavePreferences(PreferenceInput input, CancellationToken ct) {
var p = await db.Preferences.SingleOrDefaultAsync(x => x.UserId == UserId, ct);
if (p == null) { p = new() { UserId = UserId }; db.Preferences.Add(p); }
p.FavoriteCategoriesJson = Labels.Json(input.FavoriteCategories.Distinct());
p.BlockedCategoriesJson = Labels.Json(input.BlockedCategories.Distinct());
p.BlockedPlacesJson = Labels.Json(input.BlockedPlaces.Distinct());
p.Budget = input.Budget; p.RadiusMeters = input.RadiusMeters;
await db.SaveChangesAsync(ct); return Ok(new { saved = true });
}
[HttpGet("reports/weekly")]
public async Task<IActionResult> Weekly([FromQuery] DateOnly? week, CancellationToken ct) {
var monday = ChinaTime.Monday(week ?? ChinaTime.Day(clock.GetUtcNow()));
var start = ChinaTime.Start(monday); var end = start.AddDays(7);
var moods = await db.Moods.AsNoTracking().Where(x => x.UserId == UserId && x.RecordedAt >= start && x.RecordedAt < end).ToArrayAsync(ct);
var daily = moods.GroupBy(x => ChinaTime.Day(x.RecordedAt)).Select(g => g.OrderByDescending(x => x.RecordedAt)
.ThenByDescending(x => x.CreatedAt).First()).ToArray();
var visits = await db.Experiences.AsNoTracking().Include(x => x.Place).Where(x => x.UserId == UserId && x.VisitedAt >= start && x.VisitedAt < end).ToArrayAsync(ct);
var favorite = visits.GroupBy(x => x.Place.Category).OrderByDescending(g => g.Count()).Select(g => Labels.Category(g.Key)).FirstOrDefault();
return Ok(new { week = monday.ToString("yyyy-MM-dd"), recordedDays = daily.Length,
mood = daily.Length > 0 ? (double?)Math.Round(daily.Average(x => x.Mood), 1) : null,
energy = daily.Length > 0 ? (double?)Math.Round(daily.Average(x => x.Energy), 1) : null,
stress = daily.Length > 0 ? (double?)Math.Round(daily.Average(x => x.Stress), 1) : null,
visits = visits.Length, satisfaction = visits.Length > 0 ? (double?)Math.Round(visits.Average(x => x.Satisfaction), 1) : null,
topCategory = favorite,
summary = daily.Length == 0 ? "这周还没有情绪记录。写下一个小片段,开始了解自己的节奏。"
: $"这周记录了 {daily.Length} 天,心情均值 {daily.Average(x => x.Mood):F1},到访 {visits.Length} 次。给每一次认真生活留一个印记。",
notice = "体验前后心情变化仅描述记录,不表示确定的因果关系。" });
}
[HttpGet("reports/trend")]
public async Task<IActionResult> Trend([FromQuery] int days = 30, CancellationToken ct = default) {
if (days is not (7 or 30)) return BadRequest();
var today = ChinaTime.Day(clock.GetUtcNow());
var start = ChinaTime.Start(today.AddDays(1 - days));
var items = await db.Moods.AsNoTracking().Where(x => x.UserId == UserId && x.RecordedAt >= start && x.RecordedAt <= clock.GetUtcNow()).ToArrayAsync(ct);
var daily = items.GroupBy(x => ChinaTime.Day(x.RecordedAt)).ToDictionary(g => g.Key, g => g.OrderByDescending(x => x.RecordedAt)
.ThenByDescending(x => x.CreatedAt).First());
return Ok(Enumerable.Range(0, days).Select(i => {
var day = today.AddDays(1 - days + i); daily.TryGetValue(day, out var mood);
return new { date = day.ToString("yyyy-MM-dd"), mood = mood?.Mood, energy = mood?.Energy, stress = mood?.Stress };
}));
}
[HttpPost("map/geocode"), EnableRateLimiting("maps")]
public async Task<IActionResult> Geocode([FromBody] TagInput input, CancellationToken ct) {
if (string.IsNullOrWhiteSpace(input.Text) || input.Text.Length > 200) return BadRequest();
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct); timeout.CancelAfter(TimeSpan.FromSeconds(8));
return Ok(await map.GeocodeAsync(input.Text, timeout.Token));
}
}