新增 评鉴结果优化功能

This commit is contained in:
小肥羊 2026-01-15 18:14:15 +08:00
parent 1adeba007c
commit b4b02cfbd3
5 changed files with 337 additions and 39 deletions

View File

@ -32,6 +32,12 @@ namespace VideoAnalysisCore.AICore.GPT.Dto
public virtual string? Content { get; set; }
}
public class VideoKnowPointDto
{
public string KnowPoint { get; set; }
public string KnowPointId { get; set; }
public float KnowSourceTime { get; set; }
}
public class VideoKnowRes
{
/// <summary>
@ -50,6 +56,10 @@ namespace VideoAnalysisCore.AICore.GPT.Dto
public virtual long? StageId { get; set; }
public virtual VideoQuestionShowDto[]? QuestionArr { get; set; }
/// <summary>
/// 知识点列表
/// </summary>
public virtual VideoKnowPointDto[]? KnowPoints { get; set; }
/// <summary>
/// 知识点
/// </summary>
public virtual string? KnowPoint { get; set; }
@ -65,6 +75,11 @@ namespace VideoAnalysisCore.AICore.GPT.Dto
/// 内容总结
/// </summary>
public virtual string? Content { get; set; }
/// <summary>
/// 教材来源
/// <para> 课本/试卷/挹青苑 ...</para>
/// </summary>
public virtual string? TextbookSource { get; set; }
}
public class FileNameInfo

View File

@ -37,6 +37,7 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
private readonly RedisManager redisManager;
private readonly Repository<VideoTask> videoTaskDB;
private readonly Repository<VideoKonwPoint> videoKonwPointDB;
private readonly Repository<VideoTaskStage> videoTaskStageDB;
private readonly Repository<VideoQuestion> videoQuestionDB;
private readonly Repository<VideoQuestionKonw> videoQuestionKonwDB;
private readonly Repository<KnowledgeInfo> knowledgeInfoDB;
@ -49,7 +50,7 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
/// <param name="logger"></param>
public GTP_Analysis_1(DeepSeekGPTClient moonshotClient, Repository<CourseGradingCriteria> criteria, Repository<VideoTask> videoTaskDB,
Repository<KnowledgeInfo> knowledgeInfoDB, Repository<VideoKonwPoint> videoKonwPointDB, SimpLetexClient simpLetexClient,
Repository<VideoQuestion> videoQuestionDB, OssClient ossClient, Repository<VideoQuestionKonw> videoQuestionKonwDB, RedisManager redisManager, BestAIClient chatGPTClient, GeminiGPTClient geminiClient)
Repository<VideoQuestion> videoQuestionDB, OssClient ossClient, Repository<VideoQuestionKonw> videoQuestionKonwDB, RedisManager redisManager, BestAIClient chatGPTClient, GeminiGPTClient geminiClient, Repository<VideoTaskStage> videoTaskStageDB)
{
deepSeekClient = moonshotClient;
criteriaDB = criteria;
@ -63,12 +64,13 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
this.redisManager = redisManager;
this.chatGPTClient = chatGPTClient;
this.geminiClient = geminiClient;
this.videoTaskStageDB = videoTaskStageDB;
}
/// <summary>
/// 获取分段内容对应的章节知识点
/// </summary>
/// <returns></returns>
private async Task<List<VideoKonwPoint>> GetVideoKnow(VideoKnowRes[] questionRes, VideoTask taskInfo,
private async Task<List<VideoKonwPoint>> GetVideoKnow(List<VideoKnowRes> questionRes, VideoTask taskInfo,
string sections, List<KnowledgeInfo> knowledgeInfos)
{
var knows = string.Join(',', knowledgeInfos.Select(s => s.Id + "|" + s.Name));
@ -77,16 +79,18 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
.GroupBy(s => s.Name)
.ToDictionary(s => s.First().Name, s => s.First().Id);
questionRes = questionRes.Where(s => s != null)
.OrderBy(s => s.StartTime).ToArray();
.OrderBy(s => s.StartTime).ToList();
var thems = questionRes.Adapt<VideoKnowQueryDto[]>().ToJson();
var checkResFormat1 = """[{"StartTime":开始秒(number),"KnowPoint":知识点名称(string),"KnowPointId":知识点Id(string)}]""";
var checkResFormat1 = """[{"StartTime":开始秒(number),"TextbookSource":教材来源(string),"KnowPoints":[{"KnowSourceTime":"(number)","KnowPoint":知识点名称(string),"KnowPointId":知识点Id(string)}]}]""";
var knowMessages =
$"我针对{taskInfo.Subject}课堂授课视频分析出了视频的授课阶段片段。\n" +
$"现在需要你通过每个片段的内容总结来分配正确的知识点(单个片段允许多个知识点用逗号','分割)。\n" +
$"现在需要你通过每个片段的内容总结来分配正确的知识点(单个片段允许多个知识点)。\n" +
$"KnowSourceTime字段 需要提供出知识点被匹配的字幕内容最开始的时间来源。\n" +
$"TextbookSource字段 需要你分析出单个片段内讲述内容所属的教材范围 例如 (范围限定在 课本/试卷/挹青苑/其他)。\n" +
$"这是我的分段 {thems}。\n" +
$"课堂内容与{sections}章节相关\n" +
$"最后请确保分配的知识点是用户提供的,并且一定正确合理!\n" +
$"返回的片段数量与传入片段数量一致(硬性条件)!\n" +
$"返回的片段数量与传入片段数量一致!(硬性条件)\n" +
$"输出内容只返回json格式({checkResFormat1})\n" +
$" 格式 (方法点Id|方法点名称) \n" +
$"提供的`知识点名称({knows})。\n";
@ -98,9 +102,12 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
{
konwRes = await chatClentArr[i].ChatAsync<VideoKnowRes[]>(taskInfo.Id.ToString(), knowMessages, "知识点");
// 分析结果的片段数量与预期不匹配
if (questionRes.Length != konwRes.Length) continue;
if (questionRes.Count() != konwRes.Length) continue;
for (int xi = 0; xi < konwRes.Count(); xi++)
questionRes[xi].KnowPoint = konwRes[xi].KnowPoint;
{
questionRes[xi].KnowPoints = konwRes[xi].KnowPoints;
questionRes[xi].TextbookSource = konwRes[xi].TextbookSource;
}
knowOK = true;
break;
}
@ -111,13 +118,12 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
}
return questionRes
.Where(s => !string.IsNullOrEmpty(s.KnowPoint))
.Where(s => s.KnowPoints != null && s.KnowPoints.Length > 0)
.SelectMany(
s =>
{
var ks = s.KnowPoint.Split(",").Distinct();
var StageId = Yitter.IdGenerator.YitIdHelper.NextId();
return ks.Where(x => knowDic.ContainsKey(x))
return s.KnowPoints.Where(x => knowDic.ContainsKey(x.KnowPoint))
.Select(x => new VideoKonwPoint()
{
Content = s.Content,
@ -125,8 +131,9 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
StartTime = s.StartTime,
EndTime = s.EndTime,
StageId = StageId,
KnowPoint = x,
KnowPointId = knowDic[x].ToString(),
KnowPoint = x.KnowPoint,
KnowSourceTime = x.KnowSourceTime,
KnowPointId = knowDic[x.KnowPoint].ToString(),
TagId = taskInfo.TagId,
VideoTaskId = taskInfo.Id,
CloudSchoolId = taskInfo.CloudSchoolId,
@ -183,26 +190,24 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
/// 检查AI切片结果质量
/// </summary>
/// <returns></returns>
private async Task<CheckMessageDto> VerifySpanQuality(VideoKnowRes[] questionRes, VideoTask taskInfo, TotalCaptionsDto captions, string sections, long course_Id)
private async Task<CheckMessageDto> VerifySpanQuality(List<VideoKnowRes> questionRes, VideoTask taskInfo, TotalCaptionsDto captions, string sections, long course_Id)
{
//校验结果质量
var thems = questionRes.Adapt<VideoKnowQueryDto[]>().ToJson();
var pptFormat = taskInfo.VideoType == AttachmentsInfoType.
? "这堂课是习题课,所讲解内容几乎都是试题。"
: string.Empty;
var checkResFormat = """{"Score":打分(number),"MinusScore":简洁的扣分原因(string)",Suggestion":改进建议(string)""";//,"Data":优化后的分段(array)}""";
var checkResFormat = """{"Score":85.5,"MinusScore":"","Suggestion":""}""";
var checkMessage =
$"""
40
{pptFormat}
{sections}
Content是否与对应时间段内的字幕文本内容匹配(,)
Theme/Conten匹配,()
Conten有关联()
()
0-10070
@ -214,6 +219,50 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
""";
return await chatGPTClient.ChatAsync<CheckMessageDto>(taskInfo.Id.ToString(), checkMessage, "结果检查");
}
/// <summary>
/// 采用改进意见
/// </summary>
/// <param name="questionRes"></param>
/// <param name="taskInfo"></param>
/// <param name="captions"></param>
/// <param name="sections"></param>
/// <param name="suggestion"></param>
/// <returns></returns>
private async Task<List<VideoKnowRes>?> ImproveSpanBySuggestion(List<VideoKnowRes> questionRes, VideoTask taskInfo, TotalCaptionsDto captions, string sections, string suggestion)
{
if (string.IsNullOrWhiteSpace(suggestion))
return null;
var thems = questionRes.ToJson();
var pptFormat = taskInfo.VideoType == AttachmentsInfoType.
? "这堂课是习题课,所讲解内容几乎都是试题。"
: string.Empty;
var resFormat = """[{"StartTime":0.0,"EndTime":12.3,"Theme":"","Content":"","KnowPoint":"()"}]""";
var message =
$"""
使
{pptFormat}
{sections}
1)
2) StartTime
3) Content
4)
5) JSON
{thems}
{suggestion}
:::|{captions.Captions}
JSON{resFormat}
""";
var improved = await geminiClient.ChatAsync<VideoKnowRes[]>(taskInfo.Id.ToString(), message, "分段优化");
if (improved is null || improved.Length != questionRes.Count())
return null;
return improved.OrderBy(s => s.StartTime ?? 0).ToList();
}
/// <summary>
/// 优化字幕
@ -293,7 +342,7 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
/// 视频AI分析字幕
/// </summary>
/// <returns></returns>
private async Task<VideoKnowRes[]> Analytics(VideoTask taskInfo,
private async Task<List<VideoKnowRes>> Analytics(VideoTask taskInfo,
TotalCaptionsDto captions, string sections)
{
var tryCount = 10;
@ -306,32 +355,31 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
? $"请分析授课中字幕描述的知识内容,然后基于视频整体知识点讲解提炼出不同的阶段以便对老师上课内容切片提取为知识库,所以请确保阶段的内容准确性"
: $"授课中老师的PPT在这些时间段内进行了切换{taskInfo.PPTKeyFrame},理应这些时间段内的讲述内容也发生了变化,请你基于PPT变化时间点结合字幕描述的知识内容提炼出不同的切片。" +
$"每个阶段的起始和结束应接近这些时间点(例如,以时间点为中心,扩展至内容自然过渡处)。";
var resFormat = """[{"StartTime":开始秒(number),"EndTime":结束秒(number),"Stage":阶段(string),"Theme":主题(string),"Content":内容总结(string)}]""";
var resFormat = """[{"StartTime":开始秒(number),"EndTime":结束秒(number),"Stage":阶段(string),"Theme":阶段主题(string),"Content":内容总结(string)}]""";
var reviewStr = taskInfo?.VideoType == AttachmentsInfoType.
? $"但本堂课是习题课,所以大部分阶段是不同的例题讲解内容。\n"
: string.Empty;
var postMessages = string.Empty;
postMessages =
$"请通过视频字幕内容分析出视频中课堂的授课知识点切片\n" +
$"阶段的细分程度到某个知识点的讲解/认识/例题/总结\n" +
$"课堂内容与{taskInfo.Subject}学科下的{sections}章节相关。\n" +
$"完整的课堂标准流程包含以下5个阶段课程引入/新知讲解/例题精讲/课堂练习/知识总结。\n" +
reviewStr +
$"讲解知识内容的阶段的细分程度到某个知识点的讲解/认识/例题/总结\n" +
$"初步划分阶段:{keyFrameStr}\n" +
$"\n" +
$"内容分析:对每个时间段,提取主要讲解内容:识别关键词(如“例题”“证明”“练习”“总结”)和内容结构。\n" +
$"判断阶段类型:如果内容以解题为主,归类为“例题精讲”;如果涉及新知识讲解,归类为“新知讲解”;以此类推。\n" +
$"内容总结:简述该阶段的核心讲解内容70~200字,确保内容与阶段时间内授课内容符合。\n" +
$"内容总结:简述该阶段的核心讲解内容40~150字, 必须完全基于字幕文本可推断的信息,禁止捏造不存在的内容(硬性条件)。\n" +
$"阶段主题:基于内容总结,提炼一个恰当的主题(例如,“柯西不等式的基本应用”)。\n" +
$"输出要求:确保阶段划分合理、无` 重叠,且时长符合要求,并且每个阶段的时长需要超过60秒如果时长不够去考虑合并到相邻的阶段\n" +
$"输出要求:确保阶段划分合理、无重叠\n" +
$"作业布置阶段一般出现在末尾如果有" +
$"输出格式要求内容只返回json格式({resFormat})\n" +
$"字幕格式(开始秒:内容|下一段字幕).以下是包含时间的视频字幕文本。\n" +
$"字幕列表 {captions.Captions} 字幕结束!";
await redisManager.AddTaskLog(taskInfo.Id, $"开始分析视频内容 {tryCount}");
//return await chatGPTClient.ChatAsync<VideoKnowRes[]>(taskInfo.Id.ToString(), postMessages, "分析字幕");
var res = await geminiClient.ChatAsync<VideoKnowRes[]>(taskInfo.Id.ToString(), postMessages, "分析字幕");
//var r2 = await chatClient.ChatAsync<VideoKnowRes[]>(taskInfo.Id.ToString(), postMessages, "分析字幕");
var res = await geminiClient.ChatAsync<List<VideoKnowRes>>(taskInfo.Id.ToString(), postMessages, "分析字幕");
return res;
}
catch (Exception ex)
@ -342,6 +390,122 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
return null;
}
private async Task<VideoKnowRes?> DetectHomeworkAssignment(VideoTask taskInfo, TotalCaptionsDto captions, string sections)
{
if (captions is null || string.IsNullOrWhiteSpace(captions.Captions))
return null;
var parts = captions.Captions
.Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (parts.Length == 0)
return null;
var tail = string.Join('|', parts.Skip(Math.Max(0, parts.Length - 80)));
var resFormat = """{"StartTime":123.4,"EndTime":456.7,"Stage":"|","Theme":"","Content":"()"}""";
var pptFormat = taskInfo.VideoType == AttachmentsInfoType.
? "这堂课是习题课,作业可能是布置练习/订正/讲义整理。"
: string.Empty;
var message =
$"""
80 VideoKnowRes
{pptFormat}
{sections}
////////////
1)
2) Stage Content
3) Stage Theme
4) StartTime/EndTime
4) JSON
50 :::|
{tail}
JSON{resFormat}
""";
var res = await deepSeekClient.ChatAsync<VideoKnowRes>(taskInfo.Id.ToString(), message, "作业布置识别");
if (res is null)
return null;
if (!string.Equals(res.Stage, "作业布置", StringComparison.OrdinalIgnoreCase))
return null;
if (string.IsNullOrWhiteSpace(res.Content))
return null;
return res;
}
private VideoKnowRes[] MergeHomeworkStage(VideoKnowRes[] segments, VideoKnowRes homeworkStage, float maxVideoTime)
{
if (homeworkStage is null)
return segments;
if (segments is null)
segments = [];
var ordered = segments
.Where(s => s != null)
.OrderBy(s => s.StartTime ?? 0)
.ToList();
if (ordered.Any(s =>
(!string.IsNullOrWhiteSpace(s.Stage) && s.Stage.Contains("作业")) ||
(!string.IsNullOrWhiteSpace(s.Theme) && s.Theme.Contains("作业"))))
return ordered.ToArray();
var end = homeworkStage.EndTime ?? maxVideoTime;
if (end <= 0)
return ordered.ToArray();
var start = homeworkStage.StartTime ?? Math.Max(0, end - 120);
if (end - start < 1)
{
start = Math.Max(0, end - 30);
if (end - start < 1)
end = start + 30;
}
if (maxVideoTime > 0 && end > maxVideoTime)
end = maxVideoTime;
if (ordered.Count > 0)
{
var last = ordered[^1];
var lastStart = last.StartTime ?? 0;
var lastEnd = last.EndTime ?? lastStart;
if (start - lastEnd > 40)
start = lastEnd;
if (start <= lastStart)
start = lastStart + 0.01f;
if (start >= end)
{
end = start + 30;
if (maxVideoTime > 0 && end > maxVideoTime)
end = maxVideoTime;
}
if (last.EndTime is null || last.EndTime > start)
last.EndTime = start;
}
var homeworkContent = homeworkStage.Content;
var homeworkTheme = string.IsNullOrWhiteSpace(homeworkStage.Theme) ? "课后作业布置" : homeworkStage.Theme;
ordered.Add(new VideoKnowRes()
{
StartTime = start,
EndTime = end,
Stage = "作业布置",
Theme = homeworkTheme,
Content = homeworkContent
});
return ordered.ToArray();
}
@ -524,25 +688,66 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
captionsArr = await OptimizeSubtitles(taskInfo, captionsArr, sections);
//合并字幕
var captions = ExpandFunction.GetSpeakerCaptions(captionsArr);
var homework = await DetectHomeworkAssignment(taskInfo, captions, sections);
if (homework != null)
{
await redisManager.AddTaskLog(taskInfo.Id, $"==>识别到作业布置 {homework.Content}");
await redisManager.Redis.HMSetAsync(RedisExpandKey.Task(task), "Homework", homework);
}
var maxVideoTime = captions?.TimeBase?.LastOrDefault()?.End ?? 0;
VideoKnowRes[]? questionRes = null;
List<VideoKnowRes>? questionRes = null;
var tryCount = 20;
while (tryCount-- > 0)
{
//视频字幕分析
questionRes = await Analytics(taskInfo, captions, sections);
if (questionRes is null) continue;
//处理分段 知识点
var insertData = await GetVideoKnow(questionRes, taskInfo, sections, knowledgeInfos);
List<VideoKonwPoint> insertData = await GetVideoKnow(questionRes, taskInfo, sections, knowledgeInfos);
if (homework != null)
questionRes.Add(homework);
//校验结果质量
var checkRes = await VerifySpanQuality(questionRes, taskInfo, captions, sections, Course_Id);
await redisManager.AddTaskLog(taskInfo.Id, $"==>课堂内容AI分析结果 得分=>{checkRes.Score}");
await redisManager.AddTaskLog(taskInfo.Id, $"==>改进意见 {checkRes.Suggestion}");
await redisManager.AddTaskLog(taskInfo.Id, $"==>扣分原因 {checkRes.MinusScore}");
// 质量复检
if (checkRes != null)
{
var improved = await ImproveSpanBySuggestion(questionRes, taskInfo, captions, sections, checkRes.Suggestion);
if (improved != null)
{
var improvedCheck = await VerifySpanQuality(improved, taskInfo, captions, sections, Course_Id);
await redisManager.AddTaskLog(taskInfo.Id, $"==>优化后复检得分=>{improvedCheck.Score}");
await redisManager.AddTaskLog(taskInfo.Id, $"==>优化后扣分原因 {improvedCheck.MinusScore}");
if (checkRes != null && checkRes.Score >= 85)
if (improvedCheck != null && improvedCheck.Score >= 90)
{
questionRes = improved;
insertData = await GetVideoKnow(questionRes, taskInfo, sections, knowledgeInfos);
await videoKonwPointDB.DeleteAsync(s => s.VideoTaskId == taskInfo.Id);
await videoTaskStageDB.DeleteAsync(s => s.VideoTaskId == taskInfo.Id);
var tStage = insertData.GroupBy(s => s.StageId).Select(s => new VideoTaskStage
{
Id=s.Key,
TagId = s.First().TagId,
CloudSchoolId =s.First().CloudSchoolId,
StartTime = s.Last().StartTime,
EndTime =s.Last().EndTime,
KnowSourceTime=s.Last().KnowSourceTime,
Stage=s.First().Stage,
Theme=s.First().Theme,
VideoTaskId=taskInfo.Id,
}).ToArray();
await videoTaskStageDB.InsertRangeAsync(tStage);
await videoKonwPointDB.InsertRangeAsync(insertData);
break;
}
}
}
if (checkRes != null && checkRes.Score >= 90)
{
//写入知识点
await videoKonwPointDB.DeleteAsync(s => s.VideoTaskId == taskInfo.Id);

View File

@ -42,7 +42,7 @@ namespace VideoAnalysisCore.AICore.GPT.ChatGPT
/// <returns></returns>
/// <exception cref="Exception"></exception>
public override async Task<T> ChatAsync<T>(string task, string postMessages, string title,
string model = null, int max_tokens = 16000)
string model = null, int max_tokens = 32_000)
{
Message[] messageArr = [
new Message(postMessages,"user"),
@ -57,7 +57,8 @@ namespace VideoAnalysisCore.AICore.GPT.ChatGPT
max_tokens = max_tokens,
stream = true,
temperature = 0.2f,
messages = messageArr
messages = messageArr,
max_completion_tokens=16000,
};
chatReq.modalities = null;

View File

@ -65,9 +65,14 @@ namespace VideoAnalysisCore.Model
/// </summary>
public string? KnowPointId { get; set; }
/// <summary>
/// 内容总结
/// 知识点来源 视频秒
/// </summary>
public string? Content { get; set; }
public float KnowSourceTime { get; set; }
/// <summary>
/// 内容总结[不写入数据库]
/// </summary>
[SugarColumn(IsIgnore = true)]
public virtual string? Content { get; set; }
/// <summary>
/// 课程阶段
/// </summary>

View File

@ -0,0 +1,72 @@
using SqlSugar;
using System.ComponentModel.DataAnnotations;
using System.Net;
using System.Text.Json;
using UserCenter.Model.Enum;
using VideoAnalysisCore.AICore.GPT.Dto;
using VideoAnalysisCore.AICore.SherpaOnnx;
using VideoAnalysisCore.Model.Enum;
using VideoAnalysisCore.Model.Interface;
using Whisper.net;
namespace VideoAnalysisCore.Model
{
/// <summary>
/// 视频片段
/// </summary>
[SugarTable("videotaskstage")]
public class VideoTaskStage : IDB
{
/// <summary>
/// id
/// </summary>
[SugarColumn(IsPrimaryKey = true)]
public long Id { get; set; }
/// <summary>
/// 视频任务id
/// <see cref="VideoTask.Id"/>
/// </summary>
public long VideoTaskId { get; set; }
/// <summary>
/// 自定义Id [任务视频自定义id]
/// <see cref="VideoTask.TagId"/>
/// </summary>
[SugarColumn(Length = 500, IsNullable = true)]
public string? TagId { get; set; }
/// <summary>
/// 开始时间
/// </summary>
[SugarColumn( IsNullable = true)]
public float? StartTime { get; set; }
/// <summary>
/// 结束时间
/// </summary>
[SugarColumn(IsNullable = true)]
public float? EndTime { get; set; }
/// <summary>
/// 持续时间
/// </summary>
[SugarColumn(IsIgnore = true)]
public float? KeepTime => (EndTime ?? 0) - StartTime ?? 0;
/// <summary>
/// 主题
/// </summary>
public string? Theme { get; set; }
/// <summary>
/// 知识点来源 视频秒
/// </summary>
public float KnowSourceTime { get; set; }
/// <summary>
/// 课程阶段
/// </summary>
[SugarColumn(IsIgnore = true)]
public virtual StageEnum? Stage { get; set; }
/// <summary>
/// 视频所属云校ID
/// <para><see cref="UserCenter.Model.CloudSchool"/> 用户中心的云校id</para>
/// </summary>
[SugarColumn(IsNullable = true)]
public long? CloudSchoolId { get; set; }
}
}