466 lines
22 KiB
C#
466 lines
22 KiB
C#
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> 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("{ExamSubjectId} {penSerial} 开始收集...", eto.ExamSubjectId, penSerial);
|
|
var guid = GuidGenerator.Create().ToString("N");
|
|
|
|
var templates = await Db.Queryable<GroupBookPaperTemplate>().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<TemplateJsonModel_DataArr>(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 - new DateTime(1970, 1, 1, 0, 0, 0)).TotalMilliseconds;
|
|
var lattices = await DbPenOffline.Queryable<PenOfflineData>()
|
|
.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<ExamSubjectSchoolStudent>().SetColumns(s => s.CollectStatus == 2).Where(w => w.ExamSubjectSchoolId == eto.ExamSubjectSchoolId && w.StudentExamNum == penSerial).ExecuteCommandAsync();
|
|
Logger.LogInformation("{ExamSubjectId} {penSerial} 无点阵数据", eto.ExamSubjectId, penSerial);
|
|
return;
|
|
}
|
|
|
|
var kgtDtls = await DbBiz.Queryable<MarkingSettingObjective>()
|
|
.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<MarkingSettingSubjective>().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<string>();
|
|
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<string>();
|
|
var kgt = new List<Tuple<string, string>>();
|
|
var zgt = new List<Tuple<string, string, bool>>(); // 纸张Id, 题号, 是否跨页
|
|
var pageSerials = new List<string>(); // 需要计算的页
|
|
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();
|
|
|
|
// 删除
|
|
await DbBiz.Deleteable<MkExamResult>().Where(w => w.ExamSubjectId == eto.ExamSubjectId && w.StudentNo == penSerial).ExecuteCommandAsync();
|
|
await DbBiz.Updateable<SubjectiveMarkingResult>()
|
|
.SetColumns(s => new SubjectiveMarkingResult { IsDeleted = true, UpdateDate = Clock.Now })
|
|
.Where(w => w.ExamSubjectId == eto.ExamSubjectId && w.StudentExamNum == penSerial && w.IsDeleted == false)
|
|
.ExecuteCommandAsync();
|
|
// 新增
|
|
await DbBiz.Insertable(kgtDtls).ExecuteCommandAsync();
|
|
await DbBiz.Insertable(zgtDtls).ExecuteCommandAsync();
|
|
|
|
var paperAnswersStr = $"[{string.Join(",", paperAnswers)}]";
|
|
await DbBiz.Updateable<ExamSubjectSchoolStudent>().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);
|
|
}
|
|
finally
|
|
{
|
|
await RedisHelper.DelAsync(studentRedisLockKey);
|
|
}
|
|
}
|
|
|
|
[AutoTran]
|
|
public async Task ExamAnnotate(ExamAnnotateEto eto)
|
|
{
|
|
}
|
|
|
|
public async Task<SqlSugarClient> GetTenantDb(long tenantCode)
|
|
{
|
|
var tenant = await Db.Queryable<Tenant>().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,
|
|
});
|
|
}
|
|
}
|