(六)RestAPI 毛子(外部导入打卡/游标分页/Refit/Http resilience/批量提交/Quartz后台任务/Hateoas Driven)


文章目录

  • 项目地址
  • 一、外部导入打卡功能
    • 1.1 创建实体
      • 1. Entry实体
      • 2. EntryImport实体
      • 3. 添加数据库配置
      • 4. 创建表
    • 1.2 创建DTOs
    • 1.3 创建GetEnties Controller
  • 二、游标分页
    • 2.1 创建所需要的DTOs
      • 1. 创建游标分页的请求参数
      • 2. 创建CollectionResponse
      • 3. 添加游标编码和解码的DTO
    • 2.2 创建游标查询的Controller
      • 1. 传入带游标的query
      • 2. 对query里的游标解码查询
      • 3. 数据查询逻辑
      • 2.3 测试
  • 三、Refit
    • 3.1 安装需要的包
    • 3.2 创建接口IGitHubApi
    • 3.3 创建RefitGitHubService
    • 3.4 修改使用方法
  • 四、Http resilience
    • 4.1 安装所需要的包
    • 4.2 创建resilience pipeline简单版
    • 4.3 创建全局的resilience处理
      • 1. 创建清理全局ResilienceHandler
      • 2. 添加全局resilience
      • 3. 添加自定义的resilience策略
      • 4. 使用自定义策略
  • XXX
    • 1.5 创建缓存属性过滤器Idempotent Request
      • 1. 创建户型过滤器
      • 2. 创建CreateEntry Controller
    • 1.6 批量提交Entry
      • 1. 创建Dto
      • 2. 创建批量提交的Controller
    • 1.7 Quartz后台任务调度
      • 1. 给Habit添加AutomationSource
      • 2. 创建定时任务
      • 3. 创建GitHubHabitProcessorJob
    • 1.8 Hateoas Driven
      • 1. 前后端解耦
      • 2. 后端控制前端页面


项目地址

  • 教程作者:
  • 教程地址:
  • 代码仓库地址:
  • 所用到的框架和插件:
dbt 
airflow

一、外部导入打卡功能

1.1 创建实体

1. Entry实体

用来记录打卡

public sealed class Entry
{public string Id { get; set; } // 条目的唯一标识符,例如 "e_xxxxx"public string HabitId { get; set; } // 所属的习惯 ID(外键,关联 Habit 表)public string UserId { get; set; } // 该条目所属用户的 IDpublic int Value { get; set; } // 数值,比如某习惯的完成次数、时长等public string? Notes { get; set; } // 备注信息,可选填写public EntrySource Source { get; init; } // 条目的来源(手动、自动、文件导入等)public string? ExternalId { get; init; } // 外部系统的 ID(用于去重或跟踪导入来源)public bool IsArchived { get; set; } // 是否被归档,不再显示在活跃列表中public DateOnly Date { get; set; } // 条目的日期(例如 2025-04-21)public DateTime CreatedAtUtc { get; set; } // 创建时间(UTC 时间)public DateTime? UpdatedAtUtc { get; set; } // 最后更新时间(可为空)public Habit Habit { get; set; } // 导航属性,用于 Entity Framework 中的关联查询public static string NewId(){return $"e_{Guid.CreateVersion7()}"; // 使用 Guid v7 创建带前缀的唯一 ID,例如 "e_018fa..."}
}public enum EntrySource
{Manual = 0,      // 手动添加Automation = 1,  // 通过系统自动生成(例如 GitHub 数据等)FileImport = 2   // 通过导入文件生成
}

2. EntryImport实体

  • 通过文件上传打卡记录,
public sealed class EntryImportJob
{public string Id { get; set; } // 导入任务的唯一标识符,例如 "ei_xxxxx"public string UserId { get; set; } // 执行导入任务的用户 IDpublic EntryImportStatus Status { get; set; } // 当前导入任务的状态(等待/处理中/完成/失败)public string FileName { get; set; } // 上传的文件名称(如 habits.csv)public byte[] FileContent { get; set; } // 文件的原始内容(二进制格式,便于存储/处理)public int TotalRecords { get; set; } // 文件中预期要导入的记录总数public int ProcessedRecords { get; set; } // 实际已处理的记录数量public int SuccessfulRecords { get; set; } // 成功导入的记录数public int FailedRecords { get; set; } // 导入失败的记录数public List<string> Errors { get; set; } = []; // 导入过程中的错误信息集合public DateTime CreatedAtUtc { get; set; } // 创建时间(上传时间,使用 UTC)public DateTime? CompletedAtUtc { get; set; } // 完成时间(仅在导入完成后有值)public static string NewId(){return $"ei_{Guid.CreateVersion7()}"; // 使用 Guid v7 创建带前缀的唯一 ID,例如 "ei_018fa..."}
}public enum EntryImportStatus
{Pending,     // 等待开始处理Processing,  // 正在处理Completed,   // 处理完成Failed       // 处理失败(可能是文件损坏、格式错误等)
}

3. 添加数据库配置

  • Entry表
public sealed class EntryConfiguration : IEntityTypeConfiguration<Entry>
{public void Configure(EntityTypeBuilder<Entry> builder){builder.HasKey(e => e.Id);builder.Property(e => e.Id).HasMaxLength(500);builder.Property(e => e.HabitId).HasMaxLength(500);builder.Property(e => e.UserId).HasMaxLength(500);builder.Property(e => e.Notes).HasMaxLength(1000);builder.Property(e => e.ExternalId).HasMaxLength(1000);builder.HasOne(e => e.Habit).WithMany().HasForeignKey(e => e.HabitId);builder.HasOne<User>().WithMany().HasForeignKey(e => e.UserId);// We have to match snake_case naming convention for the column namebuilder.HasIndex(e => e.ExternalId).IsUnique().HasFilter("external_id IS NOT NULL");}
}

4. 创建表

在这里插入图片描述

  • 进行迁移
Add-Migration Add_Entry -Context ApplicationDbContext -o Migrations/Application

1.2 创建DTOs

1.3 创建GetEnties Controller

  • 创建GetEntries流程梳理
1. 传入请求的参数 [FromQuery] EntriesQueryParameters query以及需要的服务
2. 获取当前用户信息并且判断用户是否存在
3. 验证EntriesQueryParameters 里的Sort和Fields是否合法
4. 根据排序信息,进行排序映射
5. 使用IQueryable 延迟查询
6. 获取所有数据条数,用于分页
7. 进行排序,查询
8. 对查询结果进行分页
9. 判断是否有Hateoas
10.返回结果 
    [HttpGet]public async Task<IActionResult> GetEntries([FromQuery] EntriesQueryParameters query,SortMappingProvider sortMappingProvider,DataShapingService dataShapingService){string? userId = await userContext.GetUserIdAsync();if (string.IsNullOrWhiteSpace(userId)){return Unauthorized();}if (!sortMappingProvider.ValidateMappings<EntryDto, Entry>(query.Sort)){return Problem(statusCode: StatusCodes.Status400BadRequest,detail: $"The provided sort parameter isn't valid: '{query.Sort}'");}if (!dataShapingService.Validate<EntryDto>(query.Fields)){return Problem(statusCode: StatusCodes.Status400BadRequest,detail: $"The provided data shaping fields aren't valid: '{query.Fields}'");}SortMapping[] sortMappings = sortMappingProvider.GetMappings<EntryDto, Entry>();IQueryable<Entry> entriesQuery = dbContext.Entries.Where(e => e.UserId == userId).Where(e => query.HabitId == null || e.HabitId == query.HabitId).Where(e => query.FromDate == null || e.Date >= query.FromDate).Where(e => query.ToDate == null || e.Date <= query.ToDate).Where(e => query.Source == null || e.Source == query.Source).Where(e => query.IsArchived == null || e.IsArchived == query.IsArchived);int totalCount = await entriesQuery.CountAsync();List<EntryDto> entries = await entriesQuery.ApplySort(query.Sort, sortMappings).Skip((query.Page - 1) * query.PageSize).Take(query.PageSize).Select(EntryQueries.ProjectToDto()).ToListAsync();var paginationResult = new PaginationResult<ExpandoObject>{Items = dataShapingService.ShapeCollectionData(entries,query.Fields,query.IncludeLinks ? e => CreateLinksForEntry(e.Id, query.Fields, e.IsArchived) : null),Page = query.Page,PageSize = query.PageSize,TotalCount = totalCount};if (query.IncludeLinks){paginationResult.Links = CreateLinksForEntries(query,paginationResult.HasNextPage,paginationResult.HasPreviousPage);}return Ok(paginationResult);}

二、游标分页

2.1 创建所需要的DTOs

1. 创建游标分页的请求参数

  • Curor主要是一个index用来记录上一页的位置
namespace DevHabit.Api.DTOs.Entries;
public sealed record EntriesCursorQueryParameters : AcceptHeaderDto
{public string? Cursor { get; init; }public string? Fields { get; init; }public string? HabitId { get; init; }public DateOnly? FromDate { get; init; }public DateOnly? ToDate { get; init; }public EntrySource? Source { get; init; }public bool? IsArchived { get; init; }public int Limit { get; init; } = 10;
}

2. 创建CollectionResponse

  • 该实体用来表示表示含有items和links的实体
namespace DevHabit.Api.DTOs.Common;
public sealed class CollectionResponse<T> : ICollectionResponse<T>, ILinksResponse
{public List<T> Items { get; init; }public List<LinkDto> Links { get; set; }
}
  • 数据结构为:

在这里插入图片描述

3. 添加游标编码和解码的DTO

  • 将最后一条数据的Id和时间进行base64的编码和解码,防止数据泄密
namespace DevHabit.Api.DTOs.Entries;
public sealed record EntryCursorDto(string Id, DateOnly Date)
{//将一个游标(ID 和时间)编码为字符串,前端分页请求时可用public static string Encode(string id, DateOnly date){var cursor = new EntryCursorDto(id, date); // 创建一个游标对象string json = JsonSerializer.Serialize(cursor); // 序列化为 JSON 字符串return Base64UrlEncoder.Encode(Encoding.UTF8.GetBytes(json)); // 转成 Base64,避免 JSON 暴露或格式错误}public static EntryCursorDto Decode(string? cursor){if (string.IsNullOrWhiteSpace(cursor)){return null;}try{string json = Base64UrlEncoder.Decode(cursor); //解码 Base64 字符串return JsonSerializer.Deserialize<EntryCursorDto>(json); // 反序列化回游标对象}catch{return null;}}
}

2.2 创建游标查询的Controller

流程梳理:

1. 传入带游标的query

在这里插入图片描述

2. 对query里的游标解码查询

  • 如果携带了游标,对游标进行解码,并且根据游标的信息查询数据
    在这里插入图片描述

3. 数据查询逻辑

  1. 获取比Limit多的数据11条
  2. 如果数据大于10条,说明还有下一页
  3. 将最后一条数据的id和Date,编码为下一个游标
  4. 去除掉多余的+1的数据,保证每次10条
  5. 将数据返回给前端
    在这里插入图片描述

2.3 测试

  • 当我们发起一个需要100条数据的请求,如果还有下一页的数据,将会得到有next-page的links
    在这里插入图片描述

三、Refit

3.1 安装需要的包

在这里插入图片描述

3.2 创建接口IGitHubApi

  • 用 Refit 自动帮你生成访问 GitHub API 的客户端

namespace DevHabit.Api.Services;//每次调用这个接口的时候,自动给 HTTP 请求带上这两个头
[Headers("User-Agent: DevHabit/1.0", "Accept: application/vnd.github+json")]
public interface IGitHubApi
{[Get("/user")] //GET 请求,访问的是 GitHub API 的 /user 路径Task<ApiResponse<GitHubUserProfileDto>> GetUserProfile([Authorize(scheme: "Bearer")] string accessToken, //自动添加jwt Token在请求头中CancellationToken cancellationToken = default);[Get("/users/{username}/events")] //GET 请求,访问的是 GitHub API 的 /users/{username}/events 路径Task<ApiResponse<IReadOnlyList<GitHubEventDto>>> GetUserEvents(string username,[Authorize(scheme: "Bearer")] string accessToken, // accessToken自动插到请求头里(带身份认证)int page = 1,[AliasAs("per_page")] int perPage = 100, //告诉RefitGitHub API 要求参数名是 per_page(不是 C# 里的驼峰 PerPageCancellationToken cancellationToken = default);
}
  • 注册该服务
    在这里插入图片描述

3.3 创建RefitGitHubService

using System.Net.Http.Headers;
using DevHabit.Api.DTOs.GitHub;
using Newtonsoft.Json;
using Refit;namespace DevHabit.Api.Services;public sealed class RefitGitHubService(IGitHubApi gitHubApi, ILogger<GitHubService> logger)
{public async Task<GitHubUserProfileDto?> GetUserProfileAsync(string accessToken,CancellationToken cancellationToken = default){ArgumentException.ThrowIfNullOrEmpty(accessToken);ApiResponse<GitHubUserProfileDto> response = await gitHubApi.GetUserProfile(accessToken, cancellationToken);if (!response.IsSuccessStatusCode){logger.LogWarning("Failed to get user profile from GitHub. Status code: {StatusCode}", response.StatusCode);return null;}return response.Content;}public async Task<IReadOnlyList<GitHubEventDto>?> GetUserEventsAsync(string username,string accessToken,int page = 1,int perPage = 100,CancellationToken cancellationToken = default){ArgumentException.ThrowIfNullOrEmpty(accessToken);ArgumentException.ThrowIfNullOrEmpty(username);ApiResponse<IReadOnlyList<GitHubEventDto>> response =await gitHubApi.GetUserEvents(username,accessToken,page,perPage,cancellationToken);if (!response.IsSuccessStatusCode){logger.LogWarning("Failed to get user events from GitHub. Status code: {StatusCode}", response.StatusCode);return null;}return response.Content;}
}
  • 注册该服务在DependencyInjection
//注册RefitGitHubService
builder.Services.AddTransient<RefitGitHubService>();

3.4 修改使用方法

  • 替换之前使用githubService方法的Controller
    在这里插入图片描述

四、Http resilience

4.1 安装所需要的包

在这里插入图片描述

4.2 创建resilience pipeline简单版

  • 直接给需要使用的地方添加,这里我们使用refit获取第三方github的api数据,所以在该服务后面添加
    在这里插入图片描述

4.3 创建全局的resilience处理

1. 创建清理全局ResilienceHandler

  • 如果我们配置了全局resilience,但是部分服务又想执行自己的熔断措施,就需要先清理当前全局的措施,在添加自己的
namespace DevHabit.Api.Extensions;
public static class ResilienceHttpClientBuilderExtensions
{public static IHttpClientBuilder InternalRemoveAllResilienceHandlers(this IHttpClientBuilder builder){builder.ConfigureAdditionalHttpMessageHandlers(static (handlers, _) =>{for (int i = handlers.Count - 1; i >= 0; i--){if (handlers[i] is ResilienceHandler){handlers.RemoveAt(i);}}});return builder;}
}
  • 使用: 在需要清除的服务,先清除
    在这里插入图片描述

2. 添加全局resilience

  • 直接在服务里使用微软的包即可
    在这里插入图片描述

3. 添加自定义的resilience策略

  • 如果上面的包里的方法不够使用,我们可以添加自己的策略;
  • 创建自己的测试策略:在 HttpClient 发送每一个请求前,强制延迟 10 秒再发送。
 namespace DevHabit.Api.Services;public sealed class DelayHandler : DelegatingHandler
{protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,CancellationToken cancellationToken){await Task.Delay(10000, cancellationToken);return await base.SendAsync(request, cancellationToken);}
}

4. 使用自定义策略

在这里插入图片描述

XXX

1.5 创建缓存属性过滤器Idempotent Request

  • 这个特性让你的某些接口(如创建订单、提交支付)变成“只处理一次”,防止用户刷新或网络重复请求时多次处理。

1. 创建户型过滤器

  1. 创建Services
namespace DevHabit.Api.Services;[AttributeUsage(AttributeTargets.Method)]
public sealed class IdempotentRequestAttribute : Attribute, IAsyncActionFilter
{private const string IdempotenceKeyHeader = "Idempotency-Key";private static readonly TimeSpan DefaultCacheDuration = TimeSpan.FromMinutes(60);public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next){if (!context.HttpContext.Request.Headers.TryGetValue(IdempotenceKeyHeader,out StringValues idempotenceKeyValue) ||!Guid.TryParse(idempotenceKeyValue, out Guid idempotenceKey)){ProblemDetailsFactory problemDetailsFactory = context.HttpContext.RequestServices.GetRequiredService<ProblemDetailsFactory>();ProblemDetails problemDetails = problemDetailsFactory.CreateProblemDetails(context.HttpContext,statusCode: StatusCodes.Status400BadRequest,title: "Bad Request",detail: $"Invalid or missing {IdempotenceKeyHeader} header");context.Result = new BadRequestObjectResult(problemDetails);return;}// In production code you would want to use some kind of distributed cache. This is just for proof of concept. IMemoryCache cache = context.HttpContext.RequestServices.GetRequiredService<IMemoryCache>();string cacheKey = $"IdempotentRequest:{idempotenceKey}";int? statusCode = cache.Get<int?>(cacheKey);if (statusCode is not null){var result = new StatusCodeResult(statusCode.Value);context.Result = result;return;}ActionExecutedContext executedContext = await next();if (executedContext.Result is ObjectResult objectResult){cache.Set(cacheKey, objectResult.StatusCode, DefaultCacheDuration);}}
}

2. 创建CreateEntry Controller

  • 在CreateEntry添加修饰器
    [HttpPost][IdempotentRequest]public async Task<ActionResult<EntryDto>> CreateEntry(CreateEntryDto createEntryDto,[FromHeader] AcceptHeaderDto acceptHeader,IValidator<CreateEntryDto> validator){string? userId = await userContext.GetUserIdAsync();if (string.IsNullOrWhiteSpace(userId)){return Unauthorized();}await validator.ValidateAndThrowAsync(createEntryDto);Habit? habit = await dbContext.Habits.FirstOrDefaultAsync(h => h.Id == createEntryDto.HabitId && h.UserId == userId);if (habit is null){return Problem(detail: $"Habit with ID '{createEntryDto.HabitId}' does not exist.",statusCode: StatusCodes.Status400BadRequest);}Entry entry = createEntryDto.ToEntity(userId, habit);dbContext.Entries.Add(entry);await dbContext.SaveChangesAsync();EntryDto entryDto = entry.ToDto();if (acceptHeader.IncludeLinks){entryDto.Links = CreateLinksForEntry(entry.Id, null, entry.IsArchived);}return CreatedAtAction(nameof(GetEntry), new { id = entryDto.Id }, entryDto);}
  • 前端请求时带上一个唯一的 Header,会根据Idempotency-Key进行判断,如果重复或者没有,报错
POST /orders
Idempotency-Key: d2b7e35c-5e23-4e1d-94cf-3acda6b9b5b3

1.6 批量提交Entry

1. 创建Dto

namespace DevHabit.Api.DTOs.Entries;
public sealed record CreateEntryBatchDto
{public required List<CreateEntryDto> Entries { get; init; }
}

2. 创建批量提交的Controller

    [HttpPost("batch")]public async Task<ActionResult<List<EntryDto>>> CreateEntryBatch(CreateEntryBatchDto createEntryBatchDto,[FromHeader] AcceptHeaderDto acceptHeader,IValidator<CreateEntryBatchDto> validator){string? userId = await userContext.GetUserIdAsync();if (string.IsNullOrWhiteSpace(userId)){return Unauthorized();}await validator.ValidateAndThrowAsync(createEntryBatchDto);//收集所有 entry 中使用到的 HabitId,防止重复var habitIds = createEntryBatchDto.Entries.Select(e => e.HabitId).ToHashSet();//从数据库中查询这些习惯是否属于当前用户List<Habit> existingHabits = await dbContext.Habits.Where(h => habitIds.Contains(h.Id) && h.UserId == userId).ToListAsync();//如果查询到的习惯数量和传入的习惯ID数量不一致,说明有无效的习惯IDif (existingHabits.Count != habitIds.Count){return Problem(detail: "One or more habit IDs is invalid",statusCode: StatusCodes.Status400BadRequest);}//将 DTO 转换为 Entity 对象,准备写入数据库var entries = createEntryBatchDto.Entries.Select(dto => dto.ToEntity(userId, existingHabits.First(h => h.Id == dto.HabitId))).ToList();dbContext.Entries.AddRange(entries);await dbContext.SaveChangesAsync();//把 Entity 转换为返回给前端的 DTOvar entryDtos = entries.Select(e => e.ToDto()).ToList();if (acceptHeader.IncludeLinks){foreach (EntryDto entryDto in entryDtos){entryDto.Links = CreateLinksForEntry(entryDto.Id, null, entryDto.IsArchived);}}return CreatedAtAction(nameof(GetEntries), entryDtos);}

1.7 Quartz后台任务调度

  • 安装Quartz
<PackageVersion Include="Quartz.Extensions.Hosting" Version="3.14.0" />
  • 使用教程以及持久化方案
https://www.bilibili.com/video/BV1c8AVerEQY/?spm_id_from=333.337.search-card.all.click&vd_source=791e6deaa9c8a56b1f845a0bc1431b71

1. 给Habit添加AutomationSource

  • 添加AutomationSource枚举类型字段,表示自动化数据来源
    在这里插入图片描述

2. 创建定时任务

  • 定期扫描数据库中启用了 GitHub 自动化的习惯(Habit)数据,为每个符合条件的 habit 创建并立即执行一个 GitHubHabitProcessorJob(处理任务)。
namespace DevHabit.Api.Jobs;[DisallowConcurrentExecution]
public sealed class GitHubAutomationSchedulerJob(ApplicationDbContext dbContext,ILogger<GitHubAutomationSchedulerJob> logger) : IJob
{public async Task Execute(IJobExecutionContext context){try{logger.LogInformation("Starting GitHub automation scheduler job");//找到所有是AutomationSource=1的集合List<Habit> habitsToProcess = await dbContext.Habits.Where(h => h.AutomationSource == AutomationSource.GitHub && !h.IsArchived).ToListAsync(context.CancellationToken);logger.LogInformation("Found {Count} habits with GitHub automation", habitsToProcess.Count);foreach (Habit habit in habitsToProcess){// Create a trigger for immediate executionITrigger trigger = TriggerBuilder.Create().WithIdentity($"github-habit-{habit.Id}", "github-habits").StartNow().Build();// Create the job with habit dataIJobDetail jobDetail = JobBuilder.Create<GitHubHabitProcessorJob>().WithIdentity($"github-habit-{habit.Id}", "github-habits").UsingJobData("habitId", habit.Id).Build();// Schedule the jobawait context.Scheduler.ScheduleJob(jobDetail, trigger, context.CancellationToken);logger.LogInformation("Scheduled processor job for habit {HabitId}", habit.Id);}logger.LogInformation("Completed GitHub automation scheduler job");}catch (Exception ex){logger.LogError(ex, "Error executing GitHub automation scheduler job");throw;}}
}

3. 创建GitHubHabitProcessorJob

using DevHabit.Api.Database;
using DevHabit.Api.DTOs.GitHub;
using DevHabit.Api.Entities;
using DevHabit.Api.Services;
using Microsoft.EntityFrameworkCore;
using Quartz;namespace DevHabit.Api.Jobs;public sealed class GitHubHabitProcessorJob(ApplicationDbContext dbContext,GitHubAccessTokenService gitHubAccessTokenService,RefitGitHubService gitHubService,ILogger<GitHubHabitProcessorJob> logger) : IJob
{private const string PushEventType = "PushEvent";public async Task Execute(IJobExecutionContext context){//取出当前任务对应的 HabitIdstring habitId = context.JobDetail.JobDataMap.GetString("habitId")?? throw new InvalidOperationException("HabitId not found in job data");try{logger.LogInformation("Processing GitHub events for habit {HabitId}", habitId);// Get the habit and ensure it still exists and is configured for GitHub automationHabit? habit = await dbContext.Habits.FirstOrDefaultAsync(h => h.Id == habitId &&h.AutomationSource == AutomationSource.GitHub &&!h.IsArchived, context.CancellationToken);if (habit == null){logger.LogWarning("Habit {HabitId} not found or no longer configured for GitHub automation", habitId);return;}// Get the user's GitHub access tokenstring? accessToken = await gitHubAccessTokenService.GetAsync(habit.UserId, context.CancellationToken);if (string.IsNullOrWhiteSpace(accessToken)){logger.LogWarning("No GitHub access token found for user {UserId}", habit.UserId);return;}// Get GitHub profileGitHubUserProfileDto? profile = await gitHubService.GetUserProfileAsync(accessToken, context.CancellationToken);if (profile == null){logger.LogWarning("Couldn't retrieve GitHub profile for user {UserId}", habit.UserId);return;}// Get GitHub eventsList<GitHubEventDto> gitHubEvents = [];const int perPage = 100;const int pagesToFetch = 10;//调用 Refit 封装的 GitHub API 拿到用户最近的 GitHub 活动(默认最多抓取 1000 条)for (int page = 1; page <= pagesToFetch; page++){IReadOnlyList<GitHubEventDto>? pageEvents = await gitHubService.GetUserEventsAsync(profile.Login,accessToken,page,perPage,context.CancellationToken);if (pageEvents is null || !pageEvents.Any()){break;}gitHubEvents.AddRange(pageEvents);}if (!gitHubEvents.Any()){logger.LogWarning("Couldn't retrieve GitHub events for user {UserId}", habit.UserId);return;}// Filter to push eventsvar pushEvents = gitHubEvents.Where(e => e.Type == PushEventType).ToList();logger.LogInformation("Found {Count} push events for habit {HabitId}", pushEvents.Count, habitId);foreach (GitHubEventDto gitHubEventDto in pushEvents){// Check if we already have an entry for this eventbool exists = await dbContext.Entries.AnyAsync(e => e.HabitId == habitId &&e.ExternalId == gitHubEventDto.Id,context.CancellationToken);if (exists){logger.LogDebug("Entry already exists for event {EventId}", gitHubEventDto.Id);continue;}// Create a new entryvar entry = new Entry{Id = $"e_{Guid.CreateVersion7()}",HabitId = habitId,UserId = habit.UserId,Value = 1, // Each push counts as 1Notes =$"""{gitHubEventDto.Actor.Login} pushed:{string.Join(Environment.NewLine,//从 GitHub 的 PushEvent 中提取 commit 信息gitHubEventDto.Payload.Commits?.Select(c => $"- {c.Message}") ?? [])}""",Date = DateOnly.FromDateTime(gitHubEventDto.CreatedAt),Source = EntrySource.Automation,ExternalId = gitHubEventDto.Id,CreatedAtUtc = DateTime.UtcNow};dbContext.Entries.Add(entry);logger.LogInformation("Created entry for event {EventId} on habit {HabitId}",gitHubEventDto.Id,habitId);}//保存到数据库await dbContext.SaveChangesAsync(context.CancellationToken);}catch (Exception ex){logger.LogError(ex, "Error processing GitHub events for habit {HabitId}", habitId);throw;}}
}

1.8 Hateoas Driven

  • 场景:
    1. 切换标签,显示该页面的links
    2. 根据当前页面的某些条件,例如tags是否大于5条,控制create 按钮是否显示

1. 前后端解耦

  • 在展示用户的习惯列表时,根据后端返回的 self 链接动态生成每个习惯详情页的路由跳转地址,让前端和后端解耦,前端不需要硬编码 URL
<Link key={habit.links.find(l=>l.rel === ‘self')?.href}to={new URL(habit.links.find(l=>l.rel==='self')?.href??’#').pathname}>
  • 实际生产的链接是
<Link key="https://api.example.com/habits/abc123" to="/habits/abc123">

2. 后端控制前端页面

  • 通过控制CreateLinksTags的返回内容,来控制
    在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/903239.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Node.js CSRF 保护指南:示例及启用方法

解释 CSRF 跨站请求伪造 (CSRF/XSRF) 是一种利用用户权限劫持会话的攻击。这种攻击策略允许攻击者通过诱骗用户以攻击者的名义提交恶意请求,从而绕过我们的安全措施。 CSRF 攻击之所以可能发生,是因为两个原因。首先,CSRF 攻击利用了用户无法辨别看似合法的 HTML 元素是否…

Flink介绍——实时计算核心论文之Dataflow论文总结

数据流处理的演变与 Dataflow 模型的革新 在大数据处理领域&#xff0c;流式数据处理系统的发展历程充满了创新与变革。从早期的 S4 到 Storm&#xff0c;再到 MillWheel&#xff0c;每一个系统都以其独特的方式推动了技术的进步。S4 以其无中心架构和 PE&#xff08;Processi…

Arduino 入门学习笔记(五):KEY实验

Arduino 入门学习笔记&#xff08;五&#xff09;&#xff1a;KEY实验 开发板&#xff1a;正点原子ESP32S3 例程源码在文章顶部可免费下载&#xff08;审核中…&#xff09; 1. GPIO 输入功能使用 1.1 GPIO 输入模式介绍 在上一文章中提及到 pinMode 函数&#xff0c; 要对…

Centos9安装docker

1. 卸载docker 查看是否安装了docker yum list | grep docker卸载老版本docker&#xff0c;拷贝自官网 sudo yum remove docker \docker-client \docker-client-latest \docker-common \docker-latest \docker-latest-logrotate \docker-logrotate \docker-engine卸载新版本…

Pgvector+R2R搭建RAG知识库

背景 R2R是一个采用Python编写的开源AI RAG框架项目&#xff0c;与PostgreSQL技术栈集成度高&#xff0c;运行需求资源少&#xff08;主要是本人的Macbook air m1内存只有8G&#xff09;的特点&#xff0c;对部署本地私有化化AI RAG应用友好。 Resource Recommendations Whe…

go中redis使用的简单介绍

目录 一、Redis 简介 二、Go中Redis的使用 1. 安装Go Redis包 2. 单机模式 连接示例 3. 哨兵模式 依赖 连接示例 三、Redis集群 1. 集群模式 集群部署 部署结构 使用redis-cli创建集群 连接示例 四、常用数据结构与操作 1. 字符串&#xff08;String&#xff0…

北京工业大学25计专上岸经验分享

1.个人情况介绍 本科就读于河北双非&#xff0c;专业为计算机科学与技术&#xff0c;四级三次498&#xff0c;六级两次460&#xff0c;拿过几次校级奖学金&#xff0c;竞赛经历有蓝桥杯国三、数学竞赛省二。本科成绩排名靠前&#xff0c;保研保7排8&#xff0c;遗憾选择考研继…

在 Ubuntu 24.04 系统上安装和管理 Nginx

1、安装Nginx 在Ubuntu 24.04系统上安装Nginx&#xff0c;可以按照下面的步骤进行&#xff1a; 1.1、 更新系统软件包列表 在安装新软件之前&#xff0c;需要先更新系统的软件包列表&#xff0c;确保获取到最新的软件包信息。打开终端&#xff0c;执行以下命令&#xff1a; …

HarmonyOS4+NEXT星河版入门与项目实战(26)-----版本控制与代码托管

引言 随着移动应用开发技术的不断进步,华为推出的鸿蒙操作系统(HarmonyOS)以及其配套的集成开发环境DevEco Studio逐渐成为开发者关注的焦点。对于新手开发者来说,掌握版本控制和代码托管不仅是提高工作效率的关键,也是团队协作的重要基础。本文将介绍如何在使用DevEco S…

利用Arcgis自己绘制shp文件

1.选择自己想要创建的shp文件的位置 我是直接创建在连接文件夹中 2.右键-新建-shp 3.设置名称、要素类型、空间参考 4、点击创建要素 5、右侧选择图层、创建面 6、开始绘制&#xff0c;双击任意位置结束绘制 之后可以改一下shp文件的名字

【C/C++】深入理解指针(六)

文章目录 深入理解指针(六)1.sizeof和strlen的对比1.1 sizeof1.2 strlen1.3 sizeof和strlen的对⽐ 2.数组和指针笔试题解析2.1 ⼀维数组2.2 字符数组代码1&#xff1a;代码2&#xff1a;代码3&#xff1a;代码4&#xff1a;代码5&#xff1a;代码6&#xff1a; 2.3 ⼆维数组 3.…

探索大语言模型(LLM):语言模型从海量文本中无师自通

文章目录 引言&#xff1a;当语言模型学会“自己教自己”一、自监督学习&#xff1a;从“无标签”中挖掘“有监督”信号二、语言模型的自监督训练范式&#xff1a;两大经典路径1. 掩码语言模型&#xff08;Masked Language Modeling, MLM&#xff09;——以BERT为例2. 自回归语…

2025.5.4机器学习笔记:PINN文献阅读

2025.5.4周报 文献阅读题目信息摘要创新点网络架构实验结论不足以及展望 文献阅读 题目信息 题目&#xff1a; Physics-Informed Neural Network Approach for Solving the One-Dimensional Unsteady Shallow-Water Equations in Riverine Systems期刊&#xff1a; Journal o…

Unity Post Processing 小记 【使用泛光实现灯光亮度效果】

一、前言 本篇适用于Unity 2018 - 2019及以上版本&#xff0c;以默认渲染管线为例。文章内容源于个人研究尝试与网络资料收集&#xff0c;可能存在不准确之处。初衷是因新版本制作时老的Bloom插件失效&#xff0c;经研究后分享开启Bloom效果的方法。若在项目中使用Post Proces…

牟乃夏《ArcGIS Engine地理信息系统开发教程》学习笔记3-地图基本操作与实战案例

目录 一、开发环境与框架搭建 二、地图数据加载与文档管理 1. 加载地图文档&#xff08;MXD&#xff09; 2. 动态添加数据源 三、地图浏览与交互操作 1. 基础导航功能 2. 书签管理 3. 量测功能 四、要素选择与属性查询 1. 属性查询 2. 空间查询 五、视图同步与鹰眼…

Qt指ModbusTcp协议的使用

Modbus 是一套通信“语言”&#xff08;协议&#xff09;&#xff0c;而 RS485 / RS232 / TCP 是通信“管道”&#xff08;物理接口&#xff09;。 编写modubusTcp程序&#xff0c;避免不了调试&#xff0c;首先用到的两个工具助手 poll是主机,slave是从机。主机也就是发送数据…

探索大语言模型(LLM):自监督学习——从数据内在规律中解锁AI的“自学”密码

文章目录 自监督学习&#xff1a;从数据内在规律中解锁AI的“自学”密码一、自监督学习的技术内核&#xff1a;用数据“自问自答”1. 语言建模&#xff1a;预测下一个单词2. 掩码语言模型&#xff08;MLM&#xff09;&#xff1a;填补文本空缺3. 句子顺序预测&#xff08;SOP&a…

CentOS7.9安装Python 3.10.11并包含OpenSSL1.1.1t

1. 安装编译 Python 所需的依赖包 yum -y install gcc make zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel libffi-devel wget2. 安装 OpenSSL 1.1.1 或更新版本 &#xff08;自定义路径安装的 OpenSSL 1.1.1 不会影响系统原有的…

qt事件过滤与传递机制

当点击 QLabel 时&#xff0c;正常情况下并不会直接触发 MyWidget 的 mousePressEvent 函数&#xff0c;原因在于事件的传递机制和事件过滤器的存在。下面详细分析这个过程&#xff1a; 事件传递机制 在 Qt 里&#xff0c;事件的传递是从子控件往父控件冒泡的。不过&#xff…

ubuntu 安装ollama后,如何让外网访问?

官网下载linux版本&#xff1a;https://ollama.com/download/linux 1、一键安装和运行 curl -fsSL https://ollama.com/install.sh | sh 2、下载和启动deepseek-r1大模型 ollama run deepseek-r1 这种方式的ollama是systemd形式的服务&#xff0c;会随即启动。默认开启了 …