using Dolphin.ExamPictureCut.Constants; using Dolphin.ExamPictureCut.Domains; using Dolphin.ExamPictureCut.Domains.Basic; using Dolphin.ExamPictureCut.Domains.Biz; using Dolphin.ExamPictureCut.Domains.Quest; using Dolphin.ExamPictureCut.Exams.Dto; using Dolphin.ExamPictureCut.Extensions; using Dolphin.ExamPictureCut.Options; using Flurl.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Newtonsoft.Json; using NoFurion; using NoFurion.SqlSugar; using SkiaSharp; using SqlSugar; using Volo.Abp.BlobStoring; using Volo.Abp.Domain.Services; using Volo.Abp.Uow; using Yitter.IdGenerator; namespace Dolphin.ExamPictureCut.Exams; public class ExamManager : DomainService, IExamManager { private readonly ISqlSugarClient Db; private readonly ISqlSugarClient DbPenOffline; private readonly IBlobContainer _blobContainer; private readonly AliyunOption _aliyunOption; private readonly SKPaint skPaint = new SKPaint { Color = SKColors.Black, IsAntialias = true, // 抗锯齿 Style = SKPaintStyle.Stroke, StrokeWidth = 1, StrokeCap = SKStrokeCap.Round, }; public ExamManager(ISqlSugarClient db, IBlobContainer blobContainer, IOptions aliyunOption) { Db = db; DbPenOffline = (db as SqlSugarScope).GetConnection(DbConsts.penoffline); _blobContainer = blobContainer; _aliyunOption = aliyunOption.Value; } [UnitOfWork(false)] public async Task ExamStudentGather(ExamStudentGatherEto eto) { var penSerial = eto.StudentExamNum; Logger.LogInformation($"开始收集... 参数:{JsonConvert.SerializeObject(eto)}"); var guid = GuidGenerator.Create().ToString("N"); var templates = await Db.Queryable().Where(w => w.BookId == eto.BookId) .Select(s => new { s.Id, s.PaperId, s.PartId, s.PageIndex, s.ImgUrl, s.QueData, s.Order }).ToListAsync(); var paperInfo = templates.Select(s => { var data = JsonConvert.DeserializeObject(s.QueData); return new PaperQueData { TemplateId = s.Id, PaperId = s.PaperId, PartId = s.PartId, PageIndex = s.PageIndex, QueData = data.queData, ImgUrl = s.ImgUrl, Sort = s.Order, }; }).OrderBy(s => s.PartId).ThenBy(s => s.PageIndex).ToList(); var paperIds = templates.Select(s => s.PaperId).ToList(); // 获取点阵数据 var timespan = (long)(eto.LastCollectTime - TimeZoneInfo.ConvertTimeFromUtc(new DateTime(1970, 1, 1), TimeZoneInfo.Local)).TotalMilliseconds; var lattices = await DbPenOffline.Queryable() .Where(w => w.PenSerial == penSerial && paperIds.Contains(w.PageSerial) && w.logType == LogType.作业 && w.Time <= timespan) .Select(s => new PenOfflineData { PageSerial = s.PageSerial, CX = s.CX, CY = s.CY, Time = s.Time, strokeIndex = s.strokeIndex, }).ToListAsync(); var DbBiz = await GetTenantDb(eto.SchoolId); if (lattices.Count == 0) { await DbBiz.Updateable().SetColumns(s => s.CollectStatus == 2).Where(w => w.ExamSubjectSchoolId == eto.ExamSubjectSchoolId && w.StudentExamNum == penSerial).ExecuteCommandAsync(); Logger.LogInformation($"{eto.ExamSubjectId} {penSerial} {string.Join(',', paperIds)} {timespan} 无点阵数据"); return; } var kgtDtls = await DbBiz.Queryable() .Where(w => w.ExamSubjectId == eto.ExamSubjectId) .Select(s => new MkExamResult { StudentNo = penSerial, ExamId = eto.ExamSubjectSchoolId, ExamSubjectId = eto.ExamSubjectId, QuestionNumber = s.QuestionNum, IsObjectiveQuestion = true, QuestionValue = string.Empty, GroupNo = guid, }).ToListAsync(); var gotoCount = 0; dotPenOriginalImg: var zgtSettingDtls = await DbBiz.Queryable().Where(w => w.ExamSubjectId == eto.ExamSubjectId).ToListAsync(); // 割原题 if (!zgtSettingDtls.Any(s => s.DotPenOriginalImg.IsNotNullOrEmpty())) { if (gotoCount >= 1) { Logger.LogError("割原题超出次数限制,收集终止"); return; } var redisLockKey = "GatherLockKey:" + eto.ExamSubjectId; var redisLock = await RedisHelper.GetAsync(redisLockKey); if (string.IsNullOrEmpty(redisLock)) { await RedisHelper.SetAsync(redisLockKey, "1"); try { var dotPenOriginalImgs = new List(); foreach (var paper in paperInfo) { var imgBytes = await paper.ImgUrl.GetBytesAsync(); var bitmap = SKBitmap.Decode(imgBytes); var nextPaper = paperInfo.FirstOrDefault(w => w.PartId == paper.PartId && w.PageIndex == paper.PageIndex + 1); foreach (var que in paper.QueData) { if (que.type != "2") continue; if (que.options == null || que.options.Count == 0) continue; var dotPenOriginalImg = $"que/{eto.ExamSubjectId}/{que.no}.jpg"; if (dotPenOriginalImgs.Contains(dotPenOriginalImg)) continue; var area = que.options[0].AnswerArea; var areaTop = area.pxTop; var areaHeight = area.pxHeight; var sourceRect = new SKRect(0, areaTop, bitmap.Width, areaTop + areaHeight); var height = areaHeight; SKBitmap nextImgBitmap = null; SKRect nextSourceRect = new(); var queOnNextPaper = nextPaper?.QueData.FirstOrDefault(w => w.no == que.no); if (queOnNextPaper?.options?.Count >= 1) { var nextImgBytes = await nextPaper.ImgUrl.GetBytesAsync(); nextImgBitmap = SKBitmap.Decode(nextImgBytes); var nextArea = queOnNextPaper.options[0].AnswerArea; var nextAreaTop = nextArea.pxTop; var nextAreaHeight = nextArea.pxHeight; height += nextAreaHeight; nextSourceRect = new SKRect(0, nextAreaTop, bitmap.Width, nextAreaTop + nextAreaHeight); } using (var newBitmap = new SKBitmap(bitmap.Width, height.SSWR(), SKColorType.Rgba8888, SKAlphaType.Premul)) { using (var canvas = new SKCanvas(newBitmap)) { canvas.DrawBitmap(bitmap, sourceRect, new SKRect(0, 0, bitmap.Width, areaHeight)); if (nextImgBitmap != null) canvas.DrawBitmap(nextImgBitmap, nextSourceRect, new SKRect(0, areaHeight, bitmap.Width, height)); } var dtl = zgtSettingDtls.FirstOrDefault(w => w.QuestionNum == que.no); dtl.DotPenOriginalImg = dotPenOriginalImg; await _blobContainer.SaveAsync(dtl.DotPenOriginalImg, newBitmap.Encode(SKEncodedImageFormat.Jpeg, 100).ToArray(), true); dotPenOriginalImgs.Add(dotPenOriginalImg); } } } await DbBiz.Updateable(zgtSettingDtls).UpdateColumns(s => s.DotPenOriginalImg).ExecuteCommandAsync(); await RedisHelper.DelAsync(redisLockKey); } catch (Exception ex) { Logger.LogError("{ExamSubjectId} {penSerial} 收集失败! 原题切割异常: {error}", eto.ExamSubjectId, penSerial, ex); await RedisHelper.DelAsync(redisLockKey); throw; } } else { while (await RedisHelper.GetAsync(redisLockKey) == "1") { Thread.Sleep(1000); } gotoCount++; goto dotPenOriginalImg; } } // 锁处理 var studentRedisLockKey = "LockKey:" + eto.ExamSubjectId + penSerial; var studentRedisLock = await RedisHelper.GetAsync(studentRedisLockKey); if (studentRedisLock == "1") { Logger.LogInformation("{ExamSubjectId} {penSerial} 正在收集中,此次收集跳过", eto.ExamSubjectId, penSerial); return; } await RedisHelper.SetAsync(studentRedisLockKey, "1"); try { var zgtDtls = zgtSettingDtls.Select(s => new SubjectiveMarkingResult { Id = YitIdHelper.NextId(), ExamSubjectId = eto.ExamSubjectId, ExamSubjectSchoolId = eto.ExamSubjectSchoolId, StudentExamNum = penSerial, QuestionNum = s.QuestionNum, TotalScore = s.Score, SubQuestionCount = s.SubQuestionCount, SubQuestionDetail = s.SubQuestionDetail, StudentAnswer = $"[\"{_aliyunOption.Host}/{s.DotPenOriginalImg}\"]", GroupNo = guid, BigQuestionNum = s.BigQuestionNum, IsExcess = s.IsExcess, }).ToList(); var paperAnswers = new List(); var kgt = new List>(); var zgt = new List>(); // 纸张Id, 题号, 是否跨页 var pageSerials = new List(); // 需要计算的页 foreach (var paper in paperInfo) { var paperLatts = lattices.Where(w => w.PageSerial == paper.PaperId).ToList(); // 原卷生成 var imgBytes = await paper.ImgUrl.GetBytesAsync(); var bitmap = SKBitmap.Decode(imgBytes); var pxLatts = paperLatts.Select(s => new SubjectiveLatt() { Stroke = s.strokeIndex, X = s.CX.AUToPX(), Y = s.CY.AUToPX(), Time = s.Time, }).ToList(); using (var canvas = new SKCanvas(bitmap)) { // 一笔一笔的画上去 var strokeIndexs = pxLatts.GroupBy(g => g.Stroke).Select(s => s.Key).ToList(); foreach (var stroke in strokeIndexs) { var points = pxLatts.Where(w => w.Stroke == stroke).OrderBy(s => s.Time).Select(s => new SKPoint(s.X, s.Y)).ToArray(); var skPointMode = SKPointMode.Polygon; if (points.Length == 1) skPointMode = SKPointMode.Points; else if (points.Length == 2) skPointMode = SKPointMode.Lines; canvas.DrawPoints(skPointMode, points, skPaint); } var paperAnswer = $"origin-paper/{eto.ExamSubjectId}/{penSerial}/{paper.PageIndex}.jpg"; await _blobContainer.SaveAsync(paperAnswer, bitmap.Encode(SKEncodedImageFormat.Jpeg, 100).ToArray(), true); paperAnswers.Add($"\"{_aliyunOption.Host}/{paperAnswer}\""); } foreach (var que in paper.QueData) { if (que.type == "1") // 客观题 { if (que.options.Any() && que.options.Any(opt => paperLatts.Any(s => RectExt.IsRectContainsLattice(opt.AnswerArea, s)))) { pageSerials.Add(paper.PaperId); kgt.Add(new(paper.PaperId, que.no)); } } else if (que.type == "2") // 主观题 { if (que.options.Any() && paperLatts.Any(s => RectExt.IsRectContainsLattice(que.options[0].AnswerArea, s))) { pageSerials.Add(paper.PaperId); var ky = false; var quePaperId = paper.PaperId; var queOthPaper = paperInfo.FirstOrDefault(w => w.PartId == paper.PartId && w.PageIndex != paper.PageIndex && w.QueData.Any(s => s.no == que.no)); if (queOthPaper != null) { ky = true; if (paper.PageIndex > queOthPaper.PageIndex) { pageSerials.Add(queOthPaper.PaperId); quePaperId = queOthPaper.PaperId; } } if (!zgt.Any(s => s.Item1 == quePaperId && s.Item2 == que.no)) zgt.Add(new(quePaperId, que.no, ky)); } } } } var kgtPapers = kgt.GroupBy(s => s.Item1).Select(s => s.Key).ToList(); // 客观题处理 foreach (var paperId in kgtPapers) { var paper = paperInfo.FirstOrDefault(w => w.PaperId == paperId); var paperLatts = lattices.Where(w => w.PageSerial == paperId).ToList(); var queNos = kgt.Where(w => w.Item1 == paperId).Select(s => s.Item2).ToList(); foreach (var queNo in queNos) { var queInfo = paper.QueData.FirstOrDefault(w => w.no == queNo); var queLatts = paperLatts.Where(w => queInfo.options.Any(s => RectExt.IsRectContainsLattice(s.AnswerArea, w)) || queInfo.resetPoint.Any(s => RectExt.IsRectContainsLattice(s, w))).ToList(); long? resetTime = null; if (queInfo.resetPoint.Any()) { var resetLatts = queLatts.Where(w => RectExt.IsRectContainsLattice(queInfo.resetPoint[0], w)).ToList(); resetTime = resetLatts.Any() ? resetLatts.Max(w => w.Time) : null; } var stuAnswer = ""; // 遍历每个选项 foreach (var option in queInfo.options) { if (!option.point.Any()) continue; // 该选项无坐标数据 var choose = queLatts.WhereIF(resetTime.HasValue, w => w.Time > resetTime).Where(a => RectExt.IsRectContainsLattice(option.point[0], a)); if (!choose.Any()) continue; stuAnswer += option.option; } var dtl = kgtDtls.FirstOrDefault(w => w.QuestionNumber == queNo); if (dtl != null) { dtl.QuestionValue = stuAnswer; } } } var zgtPapers = zgt.GroupBy(s => s.Item1).Select(s => s.Key).ToList(); // 主观题处理 foreach (var paperId in zgtPapers) { var paper = paperInfo.FirstOrDefault(w => w.PaperId == paperId); var paperLatts = lattices.Where(w => w.PageSerial == paperId).ToList(); var ques = zgt.Where(w => w.Item1 == paperId).ToList(); foreach (var que in ques) { var queNo = que.Item2; var queInfo = paper.QueData.FirstOrDefault(w => w.no == queNo); var answerArea = queInfo.options[0].AnswerArea; var queLatts = paperLatts.Where(w => RectExt.IsRectContainsLattice(answerArea, w)) .Select(s => new SubjectiveLatt() { Stroke = s.strokeIndex, X = s.CX.AUToPX(), Y = s.CY.AUToPX() - answerArea.pxTop, Time = s.Time, }).ToList(); if (que.Item3) // 跨页 { var queOnNextPaper = paperInfo.FirstOrDefault(w => w.PartId == paper.PartId && w.PageIndex == paper.PageIndex + 1 && w.QueData.Any(s => s.no == queNo)); var queInfoOnNextPaper = queOnNextPaper.QueData.FirstOrDefault(w => w.no == queNo); var answerAreaOnNextPaper = queInfoOnNextPaper.options[0].AnswerArea; var queLattsOnNextPaper = lattices.Where(w => w.PageSerial == queOnNextPaper.PaperId && RectExt.IsRectContainsLattice(answerAreaOnNextPaper, w)) .Select(s => new SubjectiveLatt() { Stroke = s.strokeIndex, X = s.CX.AUToPX(), Y = s.CY.AUToPX() + answerArea.pxHeight - answerAreaOnNextPaper.pxTop, Time = s.Time, }).ToList(); queLatts.AddRange(queLattsOnNextPaper); } var stuAnswer = ""; var queImgUrl = zgtSettingDtls.FirstOrDefault(w => w.QuestionNum == queNo).DotPenOriginalImg; var imgStream = await _blobContainer.GetAsync(queImgUrl); var queBitmap = SKBitmap.Decode(imgStream); using (var canvas = new SKCanvas(queBitmap)) { // 一笔一笔的画上去 var strokeIndexs = queLatts.GroupBy(g => g.Stroke).Select(s => s.Key).ToList(); foreach (var stroke in strokeIndexs) { var points = queLatts.Where(w => w.Stroke == stroke).OrderBy(s => s.Time).Select(s => new SKPoint(s.X, s.Y)).ToArray(); var skPointMode = SKPointMode.Polygon; if (points.Length == 1) skPointMode = SKPointMode.Points; else if (points.Length == 2) skPointMode = SKPointMode.Lines; canvas.DrawPoints(skPointMode, points, skPaint); } stuAnswer = $"stu-answer/{eto.ExamSubjectId}/{penSerial}/{queNo}.jpg"; await _blobContainer.SaveAsync(stuAnswer, queBitmap.Encode(SKEncodedImageFormat.Jpeg, 100).ToArray(), true); } var dtl = zgtDtls.FirstOrDefault(w => w.QuestionNum == queNo); if (dtl != null) { dtl.StudentAnswer = $"[\"{_aliyunOption.Host}/{stuAnswer}\"]"; } } } try { await DbBiz.BeginTranAsync(); // 删除 var delObjectiveIds = await DbBiz.Queryable() .Where(w => w.ExamSubjectId == eto.ExamSubjectId && w.StudentNo == penSerial) .Select(s => s.Id).ToListAsync(); await DbBiz.Deleteable().Where(w => delObjectiveIds.Contains(w.Id)).ExecuteCommandAsync(); var delSubjectiveIds = await DbBiz.Queryable() .Where(w => w.ExamSubjectId == eto.ExamSubjectId && w.StudentExamNum == penSerial && w.IsDeleted == false) .Select(s => s.Id).ToListAsync(); await DbBiz.Updateable() .SetColumns(s => new SubjectiveMarkingResult { IsDeleted = true, UpdateDate = Clock.Now }) .Where(w => delSubjectiveIds.Contains(w.Id)) .ExecuteCommandAsync(); // 新增 await DbBiz.Insertable(kgtDtls).ExecuteCommandAsync(); await DbBiz.Insertable(zgtDtls).ExecuteCommandAsync(); var paperAnswersStr = $"[{string.Join(",", paperAnswers)}]"; await DbBiz.Updateable().SetColumns(s => new ExamSubjectSchoolStudent { CollectStatus = 2, DotPenOriginalImg = paperAnswersStr }).Where(w => w.ExamSubjectSchoolId == eto.ExamSubjectSchoolId && w.StudentExamNum == penSerial).ExecuteCommandAsync(); await DbBiz.CommitTranAsync(); Logger.LogInformation("{ExamSubjectId} {penSerial} 收集成功", eto.ExamSubjectId, penSerial); } catch (Exception saveEx) { await DbBiz.RollbackTranAsync(); Logger.LogError("{ExamSubjectId} {penSerial} 收集失败! 数据存储异常: {error}", eto.ExamSubjectId, penSerial, saveEx); } } catch (Exception ex) { Logger.LogError("{ExamSubjectId} {penSerial} 收集失败! {error}", eto.ExamSubjectId, penSerial, ex); await NotifyExt.DingTalk($"{eto.ExamSubjectId} {penSerial} 收集失败! 异常:{ex.Message}"); } finally { await RedisHelper.DelAsync(studentRedisLockKey); } } [AutoTran] public async Task ExamAnnotate(ExamAnnotateEto eto) { } public async Task GetTenantDb(long tenantCode) { var tenant = await Db.Queryable().Where(w => w.TenantCode == tenantCode).FirstAsync(); ExceptionExt.ThrowIf(tenant == null, $"{nameof(tenant)} is null with ${tenantCode}"); return new SqlSugarClient(new ConnectionConfig() { DbType = DbType.MySql, ConnectionString = tenant.ConnectionString, IsAutoCloseConnection = true, ConfigureExternalServices = MySqlConfigureExternalServices.MySqlExtService, }); } }