修复 API服务的部分流程问题

新增 API服务的日志请求写入
This commit is contained in:
小肥羊 2026-02-12 10:30:34 +08:00
parent dac6eee091
commit 9d7edad80a
12 changed files with 550 additions and 31 deletions

View File

@ -0,0 +1,279 @@
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using System;
using System.Linq;
using Microsoft.AspNetCore.Http;
using SqlSugar;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
using System.Diagnostics;
using System.Text.Json;
using System.Collections.Generic;
using System.Data;
using Microsoft.Extensions.Hosting;
using System.IO;
using Microsoft.AspNetCore.Hosting;
using SqlSugar.IOC;
using Microsoft.AspNetCore.Authorization;
using VideoAnalysisCore.Common.Dto;
using VideoAnalysisCore.Common;
using VideoAnalysisCore.Model;
namespace Learn.VideoAnalysis.API.Expand
{
/// <summary>
/// 使用该属性,接口对结果原样输出,不做包装
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)]
public class ResultIgnore : Attribute { }
/// <summary>
/// http接口日志启用
/// </summary>
public class HttpLogEnable : Attribute { }
/// <summary>
/// Http请求过滤器
/// </summary>
public class HttpLogAttribute : ActionFilterAttribute, IAsyncExceptionFilter
{
readonly Stopwatch _stopwatch;//统计程序耗时
public HttpLogAttribute()
{
_stopwatch = Stopwatch.StartNew();
}
/// <summary>
/// 执行接口前文件做缓存处理
/// </summary>
/// <param name="context"></param>
/// <exception cref="CustomException"></exception>
public void ExecutingFileCached(ActionExecutingContext context)
{
//特殊处理ResultIgnore不进行返回结果包装原样输出
var endpoint = context.HttpContext.GetEndpoint();
// 直接返回原始结果,不封装
if (endpoint?.Metadata.GetMetadata<HttpLogEnable>() == null) return;
if (context.HttpContext.Request.HasFormContentType &&
context.HttpContext.Request.Form.Files != null &&
context.HttpContext.Request.Form.Files.Count() > 0)
{
context.HttpContext.Items["FileCached"]=
context.HttpContext.Request.Form.Files.Select(s =>
{
var stream = new MemoryStream();
s.CopyTo(stream);
stream.Position = 0;
return (s, stream);
}).ToArray();
}
}
/// <summary>
/// 执行接口前400 处理
/// </summary>
/// <param name="context"></param>
/// <exception cref="CustomException"></exception>
public void Executing400(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
var errMsg = string.Join(',', context.ModelState.Values.SelectMany(s => s.Errors.Select(e => e.ErrorMessage)));
Oh.ModelError(errMsg);
}
}
private bool HasAttribute<T>(ActionExecutedContext context) where T : Attribute
{
if (context.ActionDescriptor is ControllerActionDescriptor descriptor)
{
// 检查方法上是否有 SkipApiResultAttribute
if (descriptor.MethodInfo.GetCustomAttributes(typeof(T), false).Any())
return true;
// 检查控制器上是否有 SkipApiResultAttribute
if (descriptor.ControllerTypeInfo.GetCustomAttributes(typeof(T), false).Any())
return true;
}
return false;
}
/// <summary>
/// 接口结果格式化
/// </summary>
/// <param name="context"></param>
public BaseReturn<object>? ApiResultFormatting(ActionExecutedContext context)
{
//特殊处理ResultIgnore不进行返回结果包装原样输出
if (HasAttribute<ResultIgnore>(context))
{
base.OnActionExecuted(context);
return null;
}
// 返回结果为JsonResult的请求进行Result包装
if (context.Exception != null)
throw context.Exception;
if (context.Result != null)
{
object? resData = null;
if (context.Result is ObjectResult objectResult)
resData = objectResult.Value;
else if (context.Result is ContentResult contentRes)
resData = contentRes.Content;
else if (context.Result is JsonResult resJ)
resData = resJ.Value;
else if (context.Result is FileResult)
return null;
var code = (context?.Result as IStatusCodeActionResult)?.StatusCode ?? 200;
var res = new BaseReturn<object>()
{
Code = code,
Data = resData,
Message = "SUCCESS"
};
//不对返回结果结果做修改
//context.Result = new JsonResult(res);
return res;
}
return null;
}
/// <summary>
/// 添加http日志信息
/// </summary>
/// <param name="context"></param>
/// <param name="result"></param>
/// <param name="e"></param>
/// <returns></returns>
public async Task AddHttpLogAsync(HttpContext context, BaseReturn<object>? result = null, Exception? e = null)
{
//特殊处理ResultIgnore不进行返回结果包装原样输出
var endpoint = context.GetEndpoint();
// 所有请求都记录
//if (endpoint?.Metadata.GetMetadata<HttpLogEnable>() == null&& e is null) return;
string request = null;
var logId = Yitter.IdGenerator.YitIdHelper.NextId();
if (!context.Request.Method.Equals("GET", StringComparison.InvariantCultureIgnoreCase))
{
//记录请求参数
if (context.Request.Body.CanSeek)
{
try
{
var fileArr = context.Items.ContainsKey("FileCached") ? context.Items["FileCached"] as (IFormFile file, MemoryStream stream)[] : null;
if (context.Request.HasFormContentType && fileArr != null)
{
// 设置保存目录例如项目根目录下的Uploads文件夹
string uploadsFolder = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "UploadLogs", logId.ToString());
// 创建目录(如果不存在)
if (!Directory.Exists(uploadsFolder))
Directory.CreateDirectory(uploadsFolder);
foreach (var fileInfo in fileArr)
{
// 生成安全文件名(防止路径遍历攻击)
string uniqueFileName = Guid.NewGuid().ToString().Substring(0, 5) + "_" + Path.GetFileName(fileInfo.file.FileName);
// 保存文件
using var stream = new FileStream(Path.Combine(uploadsFolder, uniqueFileName), FileMode.Create, FileAccess.Write);
fileInfo.stream.Position = 0;
await fileInfo.stream.CopyToAsync(stream);
fileInfo.stream.Dispose();
}
request = $"请求体包含{context.Request.Form.Files.Count()}个文件 目录 {uploadsFolder}";
}
else
{
context.Request.Body.Position = 0;
using var sr = new System.IO.StreamReader(context.Request.Body);
request = await sr.ReadToEndAsync();
}
}
catch (Exception ex)
{
request = "处理请求入参时发生了错误 \r\n" + ex.ToString() + "\r\n 原有请求数据 " + request;
}
}
}
//写入队列
await DbScoped.Sugar.CopyNew()
.Insertable<HttpLog>(new HttpLog
{
Id = logId,
Url = context.Request.Path + context.Request.QueryString,
Method = context.Request.Method,
Request = request,
IP = $"{context.Connection?.RemoteIpAddress?.ToString()}",
ResponseCode = result?.Code ?? -1,
Response = (result != null ? JsonSerializer.Serialize(result) : null) ,
Authorization = context.Request.Headers.ContainsKey("Authorization")
? context.Request.Headers["Authorization"].ToString()
: string.Empty,
Exception = e?.ToString(),
ExceptionMessage = e?.Message,
AdminId = 0,
TotalMilliseconds = (double)_stopwatch.Elapsed.TotalMilliseconds
}).ExecuteCommandAsync();
}
public override async void OnActionExecuting(ActionExecutingContext context)
{
// 过期的
//if (context.HttpContext.GetEndpoint()?
// .Metadata.GetMetadata<IAllowAnonymous>() is null)
//{
// context.Result = new UnauthorizedResult();
// return;
//}
Executing400(context);
ExecutingFileCached(context);
base.OnActionExecuting(context);
}
/// <summary>
/// 在Controller的Action执行后执行
/// </summary>
/// <param name="context"></param>
public override async void OnActionExecuted(ActionExecutedContext context)
{
try
{
BaseReturn<object>? res = ApiResultFormatting(context);
await AddHttpLogAsync(context.HttpContext, res);
}
catch (Exception ex)
{
}
base.OnActionExecuted(context);
}
/// <summary>
/// 执行错误时
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public async Task OnExceptionAsync(ExceptionContext context)
{
var code = -1;
var msg = context.Exception.Message;
if (context.Exception is OhException exception)
code = exception.Code;
var result = new BaseReturn()
{
Code = code,
Message = context.Exception.Message
};
context.Result = new JsonResult(result);
await AddHttpLogAsync(context.HttpContext, null, context.Exception);
if (code == 401 || code == 403)
context.HttpContext.Response.StatusCode = code;
context.ExceptionHandled = true;
}
}
}

View File

@ -2,6 +2,9 @@
using Learn.VideoAnalysis.API.Expand;
using Mapster;
using Microsoft.OpenApi.Models;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Unicode;
using VideoAnalysisCore.AICore.FFMPGE;
using VideoAnalysisCore.AICore.GPT.DeepSeek;
using VideoAnalysisCore.AICore.SherpaOnnx;
@ -18,8 +21,17 @@ namespace Learn.VideoAnalysis.API
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddControllers(options =>
{
// 全局模型赋值默认值 和 统一返回格式处理
options.Filters.Add<HttpLogAttribute>();
}).AddJsonOptions(options =>
{
options.JsonSerializerOptions.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All);//中文转换时不使用Unicode
//options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;// 默认小驼峰 null 大驼峰
});
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
@ -42,10 +54,8 @@ namespace Learn.VideoAnalysis.API
builder.Services.AddCoravel();
builder.Services.AddCorsExpand();
builder.Services.AddDownloadFileExpand();
builder.Services.AddFFMPGEExpand();
builder.Services.AddSenseVoiceExpand();
builder.Services.AddHttpContextAccessor();
builder.Services.AddControllersWithViews(options =>
{
options.Filters.Add(typeof(ExceptionFilter));
@ -62,7 +72,6 @@ namespace Learn.VideoAnalysis.API
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

View File

@ -11,6 +11,7 @@ using Microsoft.Extensions.DependencyInjection;
using VideoAnalysisCore.AICore.GPT.ChatGPT;
using VideoAnalysisCore.AICore.GPT.DeepSeek;
using System.Text.Json.Serialization;
using VideoAnalysisCore.AICore.GPT.Gemini;
namespace VideoAnalysisCore.AICore.GPT
{

View File

@ -7,12 +7,10 @@ using System.Net.Http;
using Newtonsoft.Json;
using System.Net.Http.Json;
using System.Net;
using VideoAnalysisCore.AICore.GPT.DeepSeek;
using VideoAnalysisCore.AICore.GPT;
using System.Text.Json;
namespace VideoAnalysisCore.AICore.GPT.ChatGPT
namespace VideoAnalysisCore.AICore.GPT.DeepSeek
{
public class DeepSeekGPTClient : GPTClient
@ -24,7 +22,7 @@ namespace VideoAnalysisCore.AICore.GPT.ChatGPT
private readonly RedisManager redisManager;
public DeepSeekGPTClient(IHttpClientFactory httpClientFactory, RedisManager redisManager)
:base(httpClientFactory, redisManager)
: base(httpClientFactory, redisManager)
{
_httpClientFactory = httpClientFactory;
this.redisManager = redisManager;
@ -48,7 +46,7 @@ namespace VideoAnalysisCore.AICore.GPT.ChatGPT
new Message(postMessages,"user"),
];
messageArr = messageArr.Where(s => s != null).ToArray();
if (max_tokens > 8000 &&(model is null || model == ChatGPTType.Deepseek_Chat))
if (max_tokens > 8000 && (model is null || model == ChatGPTType.Deepseek_Chat))
max_tokens = 8000;
var chatReq = new ChatRequest
{
@ -59,7 +57,7 @@ namespace VideoAnalysisCore.AICore.GPT.ChatGPT
stream = true,
messages = messageArr
};
return await base.ChatAsync<T>(chatReq);
return await ChatAsync<T>(chatReq);
}
}

View File

@ -25,13 +25,15 @@ using System.Text.RegularExpressions;
using System.Diagnostics;
using Dm.util;
using static System.Net.Mime.MediaTypeNames;
using VideoAnalysisCore.AICore.GPT.DeepSeek;
using VideoAnalysisCore.AICore.GPT.Gemini;
namespace VideoAnalysisCore.AICore.GPT.DeepSeek
namespace VideoAnalysisCore.AICore.GPT
{
/// <summary>
/// 视频分析工作流1
/// </summary>
public class GTP_Analysis_1 : IBserGPTWorkflow
public class GTP_Analysis_1 : IBserGPTWorkflow
{
private readonly GeminiGPTClient geminiClient;
private readonly DeepSeekGPTClient deepSeekClient;
@ -134,7 +136,7 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
.SelectMany(
s =>
{
var StageId = Yitter.IdGenerator.YitIdHelper.NextId();
var StageId = YitIdHelper.NextId();
return s.KnowPoints.Where(x => knowDic.ContainsKey(x.KnowPoint))
.Select(x => new VideoKonwPoint()
{
@ -173,7 +175,7 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
|| s.Depth == 2))
.Select(s => s.Name).ToArrayAsync();
var captionsArr = JsonSerializer.Deserialize<SenseVoiceRes[]>(taskInfo.Captions);
var fileNameResFormat = "{授课章节: string|null}";
var rCaptionArr = string.Join(',', captionsArr
.Where((s, i) => i % 3 == 0)
@ -300,11 +302,11 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
Func<string, Task<List<SenseVoiceInput>>>[] chatClentArr =
[
async (string m)=>await deepSeekClient
async (m)=>await deepSeekClient
.ChatAsync<List<SenseVoiceInput>>(taskInfo.Id.ToString(), m, "优化字幕",ChatGPTType.Deepseek_Chat,8_000),
async (string m)=>await chatGPTClient
async (m)=>await chatGPTClient
.ChatAsync<List<SenseVoiceInput>>(taskInfo.Id.ToString(), m, "优化字幕",ChatGPTType.GPT5,16_000),
async (string m)=>await geminiClient
async (m)=>await geminiClient
.ChatAsync<List<SenseVoiceInput>>(taskInfo.Id.ToString(), m, "优化字幕",ChatGPTType.Gemini_3_Chat,16_000), ];
await Parallel.ForAsync(0, totalCount,
new ParallelOptions()
@ -499,8 +501,8 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
.ToList();
if (ordered.Any(s =>
(!string.IsNullOrWhiteSpace(s.Stage) && s.Stage.Contains("作业")) ||
(!string.IsNullOrWhiteSpace(s.Theme) && s.Theme.Contains("作业"))))
!string.IsNullOrWhiteSpace(s.Stage) && s.Stage.Contains("作业") ||
!string.IsNullOrWhiteSpace(s.Theme) && s.Theme.Contains("作业")))
return ordered.ToArray();
var end = homeworkStage.EndTime ?? maxVideoTime;
@ -804,11 +806,11 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
VideoTaskId = taskInfo.Id,
CourseLevel = taskInfo.CourseLevel,
TextBookVersionId = taskInfo.TextBookVersionId,
GradeSemester= taskInfo.GradeSemester,
GradeSemester = taskInfo.GradeSemester,
GradeId = taskInfo.GradeId,
}).ToList();
//尝试追加 作业布置分段
if (homework != null && (!questionRes.Any(s => s.Stage == StageEnum..ToString())))
if (homework != null && !questionRes.Any(s => s.Stage == StageEnum..ToString()))
tStage.Add(homework.Adapt<VideoTaskStage>());
await videoTaskStageDB.InsertRangeAsync(tStage);
await videoKonwPointDB.InsertRangeAsync(insertData);

View File

@ -8,11 +8,10 @@ using Newtonsoft.Json;
using System.Net.Http.Json;
using System.Net;
using VideoAnalysisCore.AICore.GPT.DeepSeek;
using VideoAnalysisCore.AICore.GPT;
using System.Text.Json;
namespace VideoAnalysisCore.AICore.GPT.ChatGPT
namespace VideoAnalysisCore.AICore.GPT.Gemini
{
public class GeminiGPTClient : GPTClient
@ -52,19 +51,19 @@ namespace VideoAnalysisCore.AICore.GPT.ChatGPT
var chatReq = new ChatRequest
{
taskId = task,
title=title,
title = title,
model = model,
max_tokens = max_tokens,
stream = true,
messages = messageArr,
max_completion_tokens= 12288,
max_completion_tokens = 12288,
};
chatReq.modalities = null;
chatReq.max_tokens = null;
chatReq.top_p = null;
return await base.ChatAsync<T>(chatReq);
return await ChatAsync<T>(chatReq);
}
}

View File

@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace VideoAnalysisCore.Common.Dto
{
public class BaseReturn
{
/// <summary>
/// 消息码
/// </summary>
public int Code { get; set; }
/// <summary>
/// 消息
/// </summary>
public string? Message { get; set; }
}
public class BaseReturn<T>
{
public T? Data { get; set; }
/// <summary>
/// 消息码
/// </summary>
public int Code { get; set; }
/// <summary>
/// 消息
/// </summary>
public string? Message { get; set; }
}
}

View File

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace VideoAnalysisCore.Common.Dto
{
/// <summary>
/// 公共返回实体
/// </summary>
public class ComboModel
{
public ComboModel(string t, object v)
{
Text = t;
Value = v;
}
public ComboModel()
{
}
public object Value { get; set; }
public string Text { get; set; }
}
}

View File

@ -0,0 +1,16 @@
using System.Collections.Generic;
namespace VideoAnalysisCore.Common.Dto
{
public class PageResult<T>
{
/// <summary>
/// 数据
/// </summary>
public List<T> Data { get; set; }
/// <summary>
/// 总条数
/// </summary>
public int Total { get; set; }
}
}

View File

@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace VideoAnalysisCore.Common
{
/// <summary>
/// 异常抛出的拓展类
/// </summary>
public class Oh
{
/// <summary>
/// 抛出 异常
/// </summary>
/// <param name="message"></param>
/// <param name="code"></param>
/// <exception cref="OhException"></exception>
public static void Error(string message, int code = 500)
{
throw new OhException(message, code);
}
/// <summary>
/// 抛出 异常
/// </summary>
/// <param name="message"></param>
/// <param name="code"></param>
/// <exception cref="OhException"></exception>
public static T Error<T>(string message, int code = 500)
{
throw new OhException(message, code);
}
/// <summary>
/// 抛出 模型校验异常
/// </summary>
/// <param name="message"></param>
/// <param name="code"></param>
/// <exception cref="OhException"></exception>
public static void ModelError(string message, int code = 400)
{
throw new OhException(message, code);
}
/// <summary>
/// 抛出 模型校验异常
/// </summary>
/// <param name="message"></param>
/// <param name="code"></param>
/// <exception cref="OhException"></exception>
public static void ToeknError(string message, int code = 401)
{
throw new OhException(message, code);
}
}
public class OhException : Exception
{
/// <summary>
/// 错误码
/// </summary>
public virtual int Code { get; }
public OhException(string message, int code = -1) : base(message)
{
Code = code;
}
}
}

View File

@ -67,7 +67,7 @@ namespace VideoAnalysisCore.Controllers
[HttpPost(Name = "NodePackage")]
public async Task<IActionResult> NodePackage(NodePackageReq[] reqArr)
{
Console.WriteLine($"{DateTime.Now} 文件包订阅请求 req=" + reqArr.ToJson());
Console.WriteLine($"{DateTime.Now} 文件包订阅请求数量 req=" + reqArr.Count());
if (reqArr is null || reqArr.Count() == 0)
return BadRequest("无效视频列表数据");
var videos = new List<VideoTask>(reqArr.Count());
@ -77,7 +77,16 @@ namespace VideoAnalysisCore.Controllers
//系统可接收任务的学科
var subjectArr = new List<SubjectEnum?>
{
SubjectEnum.
SubjectEnum.,
SubjectEnum.,
SubjectEnum.,
SubjectEnum.,
SubjectEnum.,
SubjectEnum.,
SubjectEnum.,
SubjectEnum.,
SubjectEnum.,
};
var courseTypeArr = new List<AttachmentsInfoType?>
{

View File

@ -0,0 +1,79 @@
using SqlSugar;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Metadata.Ecma335;
using System.Text;
using System.Threading.Tasks;
using UserCenter.Model;
using VideoAnalysisCore.Model.Interface;
namespace VideoAnalysisCore.Model
{
///<summary>
/// 请求日志表
///</summary>
[SugarTable("httplog")]
public partial class HttpLog : EntityBaseId, IDB
{
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreateTime { get; set; } = DateTime.Now;
/// <summary>
/// 路由
/// </summary>
[SugarColumn(Length = 500)]
public string Url { get; set; }
/// <summary>
/// 请求方法类型
/// </summary>
[SugarColumn(Length = 10)]
public string Method { get; set; }
/// <summary>
/// 请求来自哪个ip
/// </summary>
[SugarColumn(IsNullable = true, Length = 30)]
public string IP { get; set; }
/// <summary>
/// 请求参数
/// </summary>
[SugarColumn(IsNullable = true, ColumnDataType = "longtext")]
public string? Request { get; set; }
/// <summary>
/// 请求返回参数
/// </summary>
[SugarColumn(IsNullable = true, ColumnDataType = "longtext")]
public string? Response { get; set; }
/// <summary>
/// 响应状态码
/// </summary>
public int ResponseCode { get; set; }
/// <summary>
/// 授权信息
/// </summary>
[SugarColumn(IsNullable = true, Length = 500)]
public string? Authorization { get; set; }
/// <summary>
/// 异常完整信息
/// </summary>
[SugarColumn(IsNullable = true, ColumnDataType = "text")]
public string Exception { get; set; }
/// <summary>
/// 异常信息
/// </summary>
[SugarColumn(IsNullable = true, ColumnDataType = "text")]
public string? ExceptionMessage { get; set; }
/// <summary>
/// 管理员ID
/// </summary>
public long AdminId { get; set; }
/// <summary>
/// 总耗时(秒)
/// </summary>
public double TotalMilliseconds { get; set; }
}
}