ExamController.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. using Azure.Cosmos;
  2. using Microsoft.AspNetCore.Http;
  3. using Microsoft.AspNetCore.Mvc;
  4. using Microsoft.CodeAnalysis.Options;
  5. using Microsoft.Extensions.Options;
  6. using System;
  7. using System.Collections.Generic;
  8. using System.IdentityModel.Tokens.Jwt;
  9. using System.Linq;
  10. using System.Text.Json;
  11. using System.Threading.Tasks;
  12. using TEAMModelOS.Models;
  13. using TEAMModelOS.Models.CommonInfo;
  14. using TEAMModelOS.Models.SchoolInfo;
  15. using TEAMModelOS.Models.StudentInfo;
  16. using TEAMModelOS.SDK;
  17. using TEAMModelOS.SDK.Context.Constant.Common;
  18. using TEAMModelOS.SDK.DI;
  19. using TEAMModelOS.SDK.Extension;
  20. using TEAMModelOS.SDK.Helper.Common.CollectionHelper;
  21. using TEAMModelOS.SDK.Helper.Common.StringHelper;
  22. namespace TEAMModelOS.Controllers
  23. {
  24. [ProducesResponseType(StatusCodes.Status200OK)]
  25. [ProducesResponseType(StatusCodes.Status400BadRequest)]
  26. //[Authorize(Roles = "IES5")
  27. [Route("school/exam")]
  28. [ApiController]
  29. public class ExamController : ControllerBase
  30. {
  31. private readonly AzureCosmosFactory _azureCosmos;
  32. private readonly SnowflakeId _snowflakeId;
  33. private readonly AzureServiceBusFactory _serviceBus;
  34. private readonly DingDing _dingDing;
  35. private readonly Option _option;
  36. public ExamController(AzureCosmosFactory azureCosmos, AzureServiceBusFactory serviceBus, SnowflakeId snowflakeId,
  37. DingDing dingDing,
  38. IOptionsSnapshot<Option> option)
  39. {
  40. _azureCosmos = azureCosmos;
  41. _serviceBus = serviceBus;
  42. _snowflakeId = snowflakeId;
  43. _dingDing = dingDing;
  44. _option = option?.Value;
  45. }
  46. /// <summary>
  47. /// 保存考试信息
  48. /// </summary>
  49. /// <param name="request"></param>
  50. /// <returns></returns>
  51. [ProducesDefaultResponseType]
  52. //[AuthToken(Roles = "Teacher")]
  53. [HttpPost("save")]
  54. public async Task<BaseResponse> Save(ExamInfo request)
  55. {
  56. ResponseBuilder builder = ResponseBuilder.custom();
  57. if (string.IsNullOrEmpty(request.id))
  58. {
  59. request.id = _snowflakeId.NextId()+"";
  60. request.status = 100;
  61. // await _azureCosmos.SaveOrUpdate(request.@params);
  62. }
  63. if (request.publish.Equals("0"))
  64. {
  65. request.startTime = new DateTimeOffset(DateTime.UtcNow).ToUnixTimeMilliseconds();
  66. request.status = 200;
  67. }
  68. else if (request.publish.Equals("1"))
  69. {
  70. //设定开始时间
  71. string msgId = _snowflakeId.NextId() + "";
  72. long SequenceNumber = await _serviceBus.GetServiceBusClient().SendLeamMessage<ExamInfo>(Constants.TopicName, request.id, request.code, request.startTime, 200, msgId);
  73. request.sequenceNumber = SequenceNumber;
  74. }
  75. if (request.status == 0)
  76. {
  77. if (request.startTime < new DateTimeOffset(DateTime.UtcNow).ToUnixTimeMilliseconds()) request.status = 200;
  78. else request.status = 100;
  79. }
  80. await _azureCosmos.SaveOrUpdate(request);
  81. //设定结束时间
  82. string msgEndId = _snowflakeId.NextId() + "";
  83. await _serviceBus.GetServiceBusClient().SendLeamMessage<ExamInfo>(Constants.TopicName, request.id, request.code, request.endTime, 300, msgEndId);
  84. return builder.Data(request).build();
  85. }
  86. /// <summary>
  87. /// 删除
  88. /// </summary>
  89. /// <param name="request"></param>
  90. /// <returns></returns>
  91. [ProducesDefaultResponseType]
  92. //[AuthToken(Roles = "Teacher")]
  93. [HttpPost("delete")]
  94. public async Task<BaseResponse> Delete(IdPk request)
  95. {
  96. ResponseBuilder builder = ResponseBuilder.custom();
  97. IdPk items = await _azureCosmos.DeleteAsync<ExamInfo>(request.id, request.pk);
  98. await _azureCosmos.DeleteAll<Paper>(new Dictionary<string, object>() { { "code", request.id } });
  99. return builder.Data(items).build();
  100. }
  101. /// <summary>
  102. /// 查询考试信息
  103. /// </summary>
  104. /// <param name="request"></param>
  105. /// <returns></returns>
  106. [ProducesDefaultResponseType]
  107. //[AuthToken(Roles = "Teacher")]
  108. [HttpPost("find")]
  109. public async Task<IActionResult> Find(JsonElement requert)
  110. {
  111. //ResponseBuilder builder = ResponseBuilder.custom();
  112. //if (!requert.TryGetProperty("id_token", out JsonElement id_token)) return BadRequest();
  113. if (!requert.TryGetProperty("school_code", out JsonElement school_code)) return BadRequest();
  114. //var jwt = new JwtSecurityToken(id_token.GetString());
  115. //if (!jwt.Payload.Iss.Equals("account.teammodel", StringComparison.Ordinal)) return BadRequest();
  116. //var id = jwt.Payload.Sub;
  117. var client = _azureCosmos.GetCosmosClient();
  118. List<object> examInfo = new List<object>();
  119. var query = $"select * from c ";
  120. await foreach (var item in client.GetContainer("TEAMModelOSTemp", "School").GetItemQueryStreamIterator(queryText: query, requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"ExamInfo-{school_code}") }))
  121. {
  122. using var json = await JsonDocument.ParseAsync(item.ContentStream);
  123. if (json.RootElement.TryGetProperty("_count", out JsonElement count) && count.GetUInt16() > 0)
  124. {
  125. foreach (var obj in json.RootElement.GetProperty("Documents").EnumerateArray())
  126. {
  127. examInfo.Add(obj.ToObject<object>());
  128. }
  129. }
  130. }
  131. return Ok(new { examInfo });
  132. /*if (StringHelper.getKeyCount(request) > 0)
  133. {
  134. return builder.Data(await _azureCosmos.FindByDict<ExamInfo>(request)).build();
  135. }
  136. else {
  137. return builder.build();
  138. }*/
  139. }
  140. /// <summary>
  141. /// 教师阅卷
  142. /// </summary>
  143. /// <param name="request"></param>
  144. /// <returns></returns>
  145. [ProducesDefaultResponseType]
  146. //[AuthToken(Roles = "Teacher")]
  147. [HttpPost("marking")]
  148. public async Task<BaseResponse> Marking(ExamRecord request)
  149. {
  150. ResponseBuilder builder = ResponseBuilder.custom();
  151. //判断是否每一个题目都有分数
  152. List<ExamInfo> exams = await _azureCosmos.FindByDict<ExamInfo>(new Dictionary<string, object> { { "id", request.examCode } });
  153. if (exams.IsNotEmpty())
  154. {
  155. ExamInfo examInfo = exams[0];
  156. //提交答案时间必须是状态已发布,且时间在起止时间内
  157. if ( examInfo.status == 300 &&
  158. examInfo.endTime <= new DateTimeOffset(DateTime.UtcNow).ToUnixTimeMilliseconds())
  159. {
  160. return builder.Data(await _azureCosmos.SaveOrUpdate(request)).build();
  161. }
  162. else
  163. {
  164. return builder.Error(ResponseCode.FAILED, "请在作答时间段内提交答案!").build();
  165. }
  166. }
  167. else
  168. {
  169. return builder.Error(ResponseCode.DATA_EXIST, "考试不存在!").build();
  170. }
  171. }
  172. /// <summary>
  173. /// 学生回答问题
  174. /// </summary>
  175. /// <param name="request"></param>
  176. /// <returns></returns>
  177. [ProducesDefaultResponseType]
  178. //[AuthToken(Roles = "Teacher")]
  179. [HttpPost("upsert-all-record")]
  180. public async Task<BaseResponse> upsertRecord(List<ExamRecord> request)
  181. {
  182. ResponseBuilder builder = ResponseBuilder.custom();
  183. if (request.IsNotEmpty())
  184. {
  185. //要先处理状态,判断卷子是否存在,并判断卷子归属的考试是否允许再次提交
  186. List<ExamInfo> exams = await _azureCosmos.FindByDict<ExamInfo>(new Dictionary<string, object> { { "id", request[0].examCode } });
  187. if (exams.IsNotEmpty())
  188. {
  189. return builder.Data(await _azureCosmos.SaveOrUpdateAll(request)).build();
  190. }
  191. else
  192. {
  193. return builder.Error(ResponseCode.DATA_EXIST, "考试不存在!").build();
  194. }
  195. }
  196. else {
  197. return builder.Error(ResponseCode.PARAMS_ERROR, "作答数据为空!").build();
  198. }
  199. }
  200. /// <summary>
  201. /// 学生回答问题
  202. /// </summary>
  203. /// <param name="request"></param>
  204. /// <returns></returns>
  205. [ProducesDefaultResponseType]
  206. //[AuthToken(Roles = "Teacher")]
  207. [HttpPost("upsert-record")]
  208. public async Task<BaseResponse> upsertRecord(ExamRecord request)
  209. {
  210. ResponseBuilder builder = ResponseBuilder.custom();
  211. //要先处理状态,判断卷子是否存在,并判断卷子归属的考试是否允许再次提交
  212. List<ExamInfo> exams= await _azureCosmos.FindByDict<ExamInfo>(new Dictionary<string, object> { { "id", request.examCode } });
  213. if (exams.IsNotEmpty())
  214. {
  215. ExamInfo examInfo = exams[0];
  216. //提交答案时间必须是状态已发布,且时间在起止时间内
  217. if (examInfo.startTime <= new DateTimeOffset(DateTime.UtcNow).ToUnixTimeMilliseconds() && examInfo.status == 200 &&
  218. examInfo.endTime >= new DateTimeOffset(DateTime.UtcNow).ToUnixTimeMilliseconds())
  219. {
  220. return builder.Data(await _azureCosmos.SaveOrUpdate(request)).build();
  221. }
  222. else
  223. {
  224. return builder.Error(ResponseCode.FAILED, "请在作答时间段内提交答案!").build();
  225. }
  226. }
  227. else {
  228. return builder.Error(ResponseCode.DATA_EXIST, "考试不存在!").build();
  229. }
  230. }
  231. /// <summary>
  232. /// 查询作答摘要信息
  233. /// </summary>
  234. /// <param name="request"></param>
  235. /// <returns></returns>
  236. [ProducesDefaultResponseType]
  237. //[AuthToken(Roles = "Teacher")]
  238. [HttpPost("find-summary-record")]
  239. public async Task<IActionResult> findSummaryRecord(JsonElement requert)
  240. {
  241. //ResponseBuilder builder = ResponseBuilder.custom();
  242. //var (id, school) = HttpContext.GetAuthTokenInfo();
  243. try {
  244. if (!requert.TryGetProperty("id_token", out JsonElement id_token)) return BadRequest();
  245. if (!requert.TryGetProperty("school_code", out JsonElement school_code)) return BadRequest();
  246. var jwt = new JwtSecurityToken(id_token.GetString());
  247. if (!jwt.Payload.Iss.Equals("account.teammodel", StringComparison.Ordinal)) return BadRequest();
  248. var id = jwt.Payload.Sub;
  249. var client = _azureCosmos.GetCosmosClient();
  250. // 如果只有学生id则返回学生参加过的考试 只返回相关摘要信息
  251. if (StringHelper.getKeyCount(requert) == 1 && requert.TryGetProperty("code", out JsonElement code))
  252. {
  253. //string pk = typeof(ExamRecord).Name +"-"+ code.ToString();
  254. //List<string> props = new List<string> { "id", "code", "examCode", "status", "mark", "score" };
  255. //List<ExamRecord> examRecords = await _azureCosmos.FindByDict<ExamRecord>(request, props);
  256. //return builder.Data(examRecords).Extend(new Dictionary<string, object> { { "props", props } }).build();
  257. List<object> props = new List<object>();
  258. await foreach (var item in client.GetContainer("TEAMModelOSTemp", "Student").GetItemQueryStreamIterator(queryText: $"select c.id, c.code, c.examCode, c.status ,c.mark, c.score from c where c.id = {id}", requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"ExamRecord-{school_code}") }))
  259. {
  260. using var json = await JsonDocument.ParseAsync(item.ContentStream);
  261. if (json.RootElement.TryGetProperty("_count", out JsonElement count) && count.GetUInt16() > 0)
  262. {
  263. foreach (var obj in json.RootElement.GetProperty("Documents").EnumerateArray())
  264. {
  265. props.Add(obj.ToObject<object>());
  266. }
  267. }
  268. }
  269. return Ok(new { props });
  270. }
  271. else
  272. {
  273. if (requert.TryGetProperty("examCode", out JsonElement _))
  274. {
  275. //List<string> props = new List<string> { "id", "code", "examCode", "status", "mark", "score" };
  276. //List<ExamRecord> examRecords = await _azureCosmos.FindByDict<ExamRecord>(request, props);
  277. //return builder.Data(examRecords).Extend(new Dictionary<string, object> { { "props", props } }).build();
  278. List<object> props = new List<object>();
  279. await foreach (var item in client.GetContainer("TEAMModelOSTemp", "Student").GetItemQueryStreamIterator(queryText: $"select c.id, c.code, c.examCode, c.status ,c.mark, c.score from c ", requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"ExamRecord-{school_code}") }))
  280. {
  281. using var json = await JsonDocument.ParseAsync(item.ContentStream);
  282. if (json.RootElement.TryGetProperty("_count", out JsonElement count) && count.GetUInt16() > 0)
  283. {
  284. foreach (var obj in json.RootElement.GetProperty("Documents").EnumerateArray())
  285. {
  286. props.Add(obj.ToObject<object>());
  287. }
  288. }
  289. }
  290. return Ok(new { props });
  291. }
  292. }
  293. return Ok();
  294. } catch (Exception ex) {
  295. await _dingDing.SendBotMsg($"CoreAPI2,{_option.Location},exam/findSummaryRecord()\n{ex.Message}", GroupNames.醍摩豆服務運維群組);
  296. return BadRequest();
  297. }
  298. }
  299. /// <summary>
  300. /// 查询单个作答信息 试卷id ,
  301. /// </summary>
  302. /// <param name="request"></param>
  303. /// <returns></returns>
  304. [ProducesDefaultResponseType]
  305. //[AuthToken(Roles = "Teacher")]
  306. [HttpPost("find-by-idPk")]
  307. public async Task<BaseResponse> findByIdPk(IdPk request)
  308. {
  309. ResponseBuilder builder = ResponseBuilder.custom();
  310. ExamRecord record = await _azureCosmos.FindByIdPk<ExamRecord>(request.id,request.pk);
  311. if (record != null) {
  312. //处理客观题自动阅卷
  313. Paper paper = await _azureCosmos.FindByIdPk<Paper>(record.id, record.examCode);
  314. //允许自动对客观题阅卷及,及阅卷状态大于2 已完成.
  315. if (paper.markConfig != null && paper.markConfig.auto && record.mark<2) {
  316. // TODO 需要处理已经打分的作答,以防止手动改分后被冲掉。
  317. if (paper.answers.IsNotEmpty() && record.answers.IsNotEmpty()) {
  318. autoMark(paper.item, paper.answers, record.answers, paper.markConfig);
  319. }
  320. }
  321. }
  322. return builder.Data(record).build();
  323. }
  324. #region private
  325. private List<Answer> autoMark(List<ItemInfo> items ,List<Answer> stdAnswers, List<Answer> stuAnswers ,MarkConfig markConfig) {
  326. int size = stuAnswers.Count;
  327. for (int i = 0; i < size; i++) {
  328. //如果当前题目已经打分则直接跳过。
  329. if (stuAnswers[i].score > 0) {
  330. stuAnswers[i].mark = 1;
  331. continue;
  332. }
  333. //客观题
  334. if (stuAnswers[i].type.Equals("single") || stuAnswers[i].type.Equals("multiple") || stuAnswers[i].type.Equals("judge")) {
  335. stuAnswers[i].mark = 1;
  336. //多选题单独处理
  337. if (stuAnswers[i].type.Equals("multiple"))
  338. {
  339. List<string> stuAns = stuAnswers[i].ans;
  340. Answer stdAnswer = stdAnswers.Where(x =>x.num== stuAnswers[i].num).FirstOrDefault();
  341. if (stdAnswer != null) {
  342. //处理多选答案是否有选错的 选错的则直接0分 ,如果少选则处理部分分数
  343. bool right = true;
  344. List<string> rightStr = new List<string>();
  345. if (stuAns.IsNotEmpty())
  346. {
  347. foreach (string stuAn in stuAns)
  348. {
  349. if (!stdAnswer.ans.Contains(stuAn))
  350. {
  351. right = false;
  352. break;
  353. }
  354. else {
  355. rightStr.Add(stuAn);
  356. }
  357. }
  358. }
  359. else {
  360. right = false;
  361. }
  362. if (right && rightStr.IsNotEmpty() && rightStr.Count != stdAnswer.ans.Count)
  363. {
  364. if (markConfig.type == 1)
  365. {
  366. //1多选漏选不得分
  367. stuAnswers[i].score = 0;
  368. }
  369. else if(markConfig.type==3){
  370. stuAnswers[i].score=Math.Floor((double)(1.0 * rightStr.Count / stdAnswer.ans.Count * stdAnswer.score));
  371. if (stuAnswers[i].score == 0) {
  372. stuAnswers[i].score = 1;
  373. }
  374. }
  375. else if (markConfig.type == 4)
  376. {
  377. stuAnswers[i].score = markConfig.score;
  378. }
  379. else
  380. { //2多选漏选得一半的分数(默认)
  381. stuAnswers[i].score = stdAnswer.score/2;
  382. }
  383. }
  384. else {
  385. //选错不得分
  386. stuAnswers[i].score = 0;
  387. }
  388. }
  389. }
  390. else {
  391. List<string> stuAns = stuAnswers[i].ans;
  392. Answer stdAnswer = stdAnswers.Where(x => x.num == stuAnswers[i].num).FirstOrDefault();
  393. if (stdAnswer != null&&stdAnswer.ans.IsNotEmpty()&& stuAns.IsNotEmpty()) {
  394. if (stuAns[0].Equals(stdAnswer.ans[0]))
  395. {
  396. stuAnswers[i].score = stdAnswer.score;
  397. }
  398. else {
  399. stuAnswers[i].score = 0;
  400. }
  401. }
  402. }
  403. }
  404. }
  405. return stuAnswers;
  406. }
  407. #endregion
  408. }
  409. }