VoteController.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. using Azure.Cosmos;
  2. using Microsoft.AspNetCore.Http;
  3. using Microsoft.AspNetCore.Mvc;
  4. using System;
  5. using System.Collections.Generic;
  6. using System.IdentityModel.Tokens.Jwt;
  7. using System.Linq;
  8. using System.Text;
  9. using System.Text.Json;
  10. using System.Threading.Tasks;
  11. using TEAMModelOS.Models.Dto;
  12. using TEAMModelOS.SDK.Models;
  13. using TEAMModelOS.SDK;
  14. using TEAMModelOS.SDK.Context.Constant.Common;
  15. using TEAMModelOS.SDK.DI;
  16. using TEAMModelOS.SDK.DI.AzureCosmos.Inner;
  17. using TEAMModelOS.SDK.Extension;
  18. using TEAMModelOS.SDK.Helper.Common.CollectionHelper;
  19. using TEAMModelOS.SDK.Helper.Common.StringHelper;
  20. using TEAMModelOS.Models;
  21. using Microsoft.Extensions.Options;
  22. using TEAMModelOS.SDK.Models.Cosmos;
  23. using Microsoft.AspNetCore.Authorization;
  24. using TEAMModelOS.Filter;
  25. using StackExchange.Redis;
  26. using TEAMModelOS.SDK.Models.Cosmos.Common.Inner;
  27. using TEAMModelOS.Services.Common;
  28. namespace TEAMModelOS.Controllers.Learn
  29. {
  30. /// <summary>
  31. /// 投票活动
  32. /// </summary>
  33. [ProducesResponseType(StatusCodes.Status200OK)]
  34. [ProducesResponseType(StatusCodes.Status400BadRequest)]
  35. //[Authorize(Roles = "IES5")]
  36. [Route("common/vote")]
  37. [ApiController]
  38. public class VoteController : ControllerBase
  39. {
  40. private readonly AzureRedisFactory _azureRedis;
  41. private readonly AzureCosmosFactory _azureCosmos;
  42. private readonly SnowflakeId _snowflakeId;
  43. private readonly AzureServiceBusFactory _serviceBus;
  44. private readonly DingDing _dingDing;
  45. private readonly Option _option;
  46. public VoteController(AzureCosmosFactory azureCosmos, AzureServiceBusFactory serviceBus, SnowflakeId snowflakeId, DingDing dingDing, IOptionsSnapshot<Option> option,
  47. AzureRedisFactory azureRedis)
  48. {
  49. _azureCosmos = azureCosmos;
  50. _serviceBus = serviceBus;
  51. _snowflakeId = snowflakeId;
  52. _dingDing = dingDing;
  53. _option = option?.Value;
  54. _azureRedis = azureRedis;
  55. }
  56. /// <summary>
  57. /// 新增 或 修改投票活动
  58. /// </summary>
  59. /// <param name="request"></param>
  60. /// <returns></returns>
  61. [ProducesDefaultResponseType]
  62. [HttpPost("upsert")]
  63. public async Task<IActionResult> Upsert(Vote request)
  64. {
  65. try
  66. {
  67. //新增Vote
  68. var client = _azureCosmos.GetCosmosClient();
  69. request.code = request.pk + "-" + request.code;
  70. long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
  71. request.createTime = now;
  72. if (string.IsNullOrEmpty(request.id))
  73. {
  74. request.id = Guid.NewGuid().ToString();
  75. if (request.startTime > now)
  76. {
  77. request.progress = "pending";
  78. }
  79. else {
  80. request.progress = "going";
  81. }
  82. request = await client.GetContainer("TEAMModelOS", "Common").CreateItemAsync(request, new PartitionKey($"{request.code}"));
  83. }
  84. else
  85. {
  86. var response = await client.GetContainer("TEAMModelOS", "Common").ReadItemStreamAsync(request.id, new PartitionKey($"{request.code}"));
  87. if (response.Status == 200)
  88. {
  89. using var json = await JsonDocument.ParseAsync(response.ContentStream);
  90. var info = json.ToObject<Vote>();
  91. if (info.progress.Equals("going"))
  92. {
  93. return Ok(new { v = "活动正在进行中" });
  94. }
  95. if (request.startTime > now)
  96. {
  97. request.progress = "pending";
  98. }
  99. else
  100. {
  101. request.progress = "going";
  102. }
  103. request.progress = info.progress;
  104. request = await client.GetContainer("TEAMModelOS", "Common").ReplaceItemAsync(request, info.id, new PartitionKey($"{info.code}"));
  105. }
  106. else
  107. {
  108. if (request.startTime > now)
  109. {
  110. request.progress = "pending";
  111. }
  112. else
  113. {
  114. request.progress = "going";
  115. }
  116. request = await client.GetContainer("TEAMModelOS", "Common").CreateItemAsync(request, new PartitionKey($"{request.code}"));
  117. }
  118. }
  119. return Ok(new { vote = request });
  120. }
  121. catch (Exception e)
  122. {
  123. await _dingDing.SendBotMsg($"OS,{_option.Location},common/vote/save()\n{e.Message}", GroupNames.醍摩豆服務運維群組);
  124. return BadRequest(e.StackTrace);
  125. }
  126. }
  127. /// <summary>
  128. /// 查询投票活动,用于列表,编辑,查看
  129. /// </summary>
  130. /// <data>
  131. ///Vote-学校/教师编码 活动分区 !"code":"hbcn"/1606285227
  132. ///时间筛选范围开始时间 默认30天之前 ?"stime":1608274766154
  133. ///时间筛选范围结束时间 默认当前时间 ?"etime":1608274766666
  134. ///每页大小 ?"count":10/null/Undefined
  135. ///分页Token ?"continuationToken":Undefined/null/"[{\"token\":\"+RID:~omxMAP3ipcSEEwAAAAAAAA==#RT:2#TRC:20#ISV:2#IEO:65551#QCF:1#FPC:AYQTAAAAAAAAiRMAAAAAAAA=\",\"range\":{\"min\":\"\",\"max\":\"FF\"}}]"
  136. /// 当前状态 ?"progress":Undefined/null/"" 表示两种状态都要查询/ "going"/"finish" 表示查询进行中/ 或者已完成 学生端只能查询正在进行或已经结束 going 已发布|finish 已结束
  137. /// </data>
  138. /// <param name="request"></param>
  139. /// <returns></returns>
  140. [ProducesDefaultResponseType]
  141. [HttpPost("find")]
  142. public async Task<IActionResult> Find(JsonElement requert)
  143. {
  144. try
  145. {
  146. //必须有学校或者教师编码
  147. if (!requert.TryGetProperty("code", out JsonElement code)) return BadRequest();
  148. //开始时间,默认最近三十天
  149. var stimestamp = DateTimeOffset.UtcNow.AddDays(-30).ToUnixTimeMilliseconds();
  150. if (requert.TryGetProperty("stime", out JsonElement stime)) {
  151. if (!stime.ValueKind.Equals(JsonValueKind.Undefined)&&stime.TryGetInt64(out long data))
  152. {
  153. stimestamp = data;
  154. };
  155. };
  156. //默认当前时间
  157. var etimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
  158. if (requert.TryGetProperty("etime", out JsonElement etime))
  159. {
  160. if (!etime.ValueKind.Equals(JsonValueKind.Undefined)&&etime.TryGetInt64(out long data))
  161. {
  162. etimestamp = data;
  163. };
  164. };
  165. var progresssql = "";
  166. if (!requert.TryGetProperty("progress", out JsonElement progress))
  167. {
  168. if (!progress.ValueKind.Equals(JsonValueKind.Undefined) && !progress.ValueKind.Equals(JsonValueKind.Null) && progress.ValueKind.Equals(JsonValueKind.String))
  169. {
  170. progresssql = $" and c.progress='{progresssql}' ";
  171. }
  172. }
  173. string continuationToken = null;
  174. //默认不指定返回大小
  175. int? topcout=null;
  176. if (requert.TryGetProperty("count", out JsonElement jcount)) {
  177. if(!jcount.ValueKind.Equals(JsonValueKind.Undefined) && jcount.TryGetInt32(out int data))
  178. {
  179. topcout = data;
  180. }
  181. };
  182. //是否需要进行分页查询,默认不分页
  183. bool iscontinuation = false;
  184. //如果指定了返回大小
  185. if (requert.TryGetProperty("continuationToken", out JsonElement continuation))
  186. {
  187. //指定了cancellationToken 表示需要进行分页
  188. if (!continuation.ValueKind.Equals(JsonValueKind.Null) && !continuation.ValueKind.Equals(JsonValueKind.Undefined))
  189. {
  190. continuationToken = continuation.GetString();
  191. iscontinuation = true;
  192. }
  193. };
  194. List<object> votes = new List<object>();
  195. var client = _azureCosmos.GetCosmosClient();
  196. var query =$"select c.id,c.name,c.code,c.startTime,c.endTime,c.progress from c where c.createTime >= {stimestamp} and c.createTime <= {etimestamp} {progresssql } ";
  197. await foreach (var item in client.GetContainer("TEAMModelOS", "Common").GetItemQueryStreamIterator(queryText: query,
  198. requestOptions: new QueryRequestOptions() {MaxItemCount = topcout, PartitionKey = new PartitionKey($"Vote-{code}") }))
  199. {
  200. using var json = await JsonDocument.ParseAsync(item.ContentStream);
  201. if (json.RootElement.TryGetProperty("_count", out JsonElement count) && count.GetUInt16() > 0)
  202. {
  203. foreach (var obj in json.RootElement.GetProperty("Documents").EnumerateArray())
  204. {
  205. votes.Add(obj.ToObject<JsonElement>());
  206. }
  207. //如果需要分页则跳出
  208. if (iscontinuation) {
  209. continuationToken = item.GetContinuationToken();
  210. break;
  211. }
  212. }
  213. }
  214. return Ok(new { votes, continuationToken });
  215. }
  216. catch (Exception ex)
  217. {
  218. await _dingDing.SendBotMsg($"OS,{_option.Location},common/vote/find()\n{ex.StackTrace}", GroupNames.醍摩豆服務運維群組);
  219. return BadRequest(ex.StackTrace);
  220. }
  221. }
  222. ///<summary>
  223. /// 查询投票活动,用于创建者列表,编辑,查看,投票人员查看
  224. /// </summary>
  225. /// <data>
  226. /// ! "id":"3c075347-75ef-4bcb-ae03-68678d02d5ef",
  227. /// ! "code":"Vote-hbcn"/"code":"Vote-1606285227"
  228. /// </data>
  229. /// <param name="request"></param>
  230. /// <returns></returns>
  231. [ProducesDefaultResponseType]
  232. [HttpPost("find-id")]
  233. public async Task<IActionResult> FindById(JsonElement requert)
  234. {
  235. try
  236. {
  237. var client = _azureCosmos.GetCosmosClient();
  238. //活动id
  239. if (!requert.TryGetProperty("id", out JsonElement id)) return BadRequest();
  240. //活动分区
  241. if (!requert.TryGetProperty("code", out JsonElement code)) return BadRequest();
  242. Vote vote = await client.GetContainer("TEAMModelOS", "Common").ReadItemAsync<Vote>(id.GetString(), new PartitionKey($"{code}"));
  243. if (vote != null)
  244. {
  245. return Ok(new { vote });
  246. }
  247. else
  248. {
  249. return BadRequest("id,code不存在!");
  250. }
  251. }
  252. catch (Exception ex)
  253. {
  254. await _dingDing.SendBotMsg($"OS,{_option.Location},common/vote/find-id()\n{ex.Message}", GroupNames.醍摩豆服務運維群組);
  255. return BadRequest(ex.StackTrace);
  256. }
  257. }
  258. /// <summary>
  259. /// 删除投票活动 TODO 使用ttl删除,并处理相关事务逻辑
  260. /// </summary>
  261. /// <param name="request"></param>
  262. /// <returns></returns>
  263. [ProducesDefaultResponseType]
  264. [HttpPost("delete")]
  265. [AuthToken(Roles = "admin,teacher")]
  266. public async Task<IActionResult> Delete(JsonElement request)
  267. {
  268. try
  269. {
  270. var (userid, _, _,school) = HttpContext.GetAuthTokenInfo();
  271. if (!request.TryGetProperty("id", out JsonElement id)) return BadRequest();
  272. if (!request.TryGetProperty("code", out JsonElement code)) return BadRequest();
  273. var client = _azureCosmos.GetCosmosClient();
  274. Vote vote =await client.GetContainer("TEAMModelOS", "Common").ReadItemAsync<Vote>(id.GetString(), new PartitionKey($"{code}") );
  275. bool flag = false;
  276. //必须是本人或者这个学校的管理者才能删除
  277. if (vote.creatorId == userid)
  278. {
  279. flag = true;
  280. }
  281. else {
  282. if (vote.scope == "school"&& vote.owner.Equals(school)) {
  283. flag = true;
  284. }
  285. }
  286. if (flag)
  287. {
  288. //使用ttl删除,并处理相关事务逻辑
  289. vote.ttl = 1;
  290. vote = await client.GetContainer("TEAMModelOS", "Common").ReplaceItemAsync(vote, vote.id, new PartitionKey($"{vote.code}"));
  291. return Ok(new { flag });
  292. }
  293. else {
  294. return Ok(new { flag });
  295. }
  296. //var response = await client.GetContainer("TEAMModelOS", "Common").DeleteItemStreamAsync(id.ToString(), new PartitionKey($"{code}"));
  297. // return Ok(new { code = response.Status });
  298. }
  299. catch (Exception e)
  300. {
  301. return BadRequest(e.StackTrace);
  302. }
  303. }
  304. /// <summary>
  305. /// 投票记录 当活动没结算且没有BlobUrl时则调用此接口
  306. /// </summary>
  307. /// <redis>
  308. /// 投票活动选项计数器 使用SortedSet(有序集合)ZSET 数据集合 使用命令 ZINCRBY key:"Vote:Count:AAA",value:"A",score:1
  309. /// 投票活动 投票记录 使用Hash(哈希表)使用命令 key:"Vote:Record:AAA",feild:15283771540-20210105,value:"{"opt":["A","C","A"],"time":1608274766154}"
  310. /// </redis>
  311. /// <param name="request">
  312. /// !"id":"aaaa"
  313. /// !"code":"Vote-hbcn"/"code":"Vote-1606285227"
  314. /// </param>
  315. /// <returns>
  316. /// </returns>
  317. [ProducesDefaultResponseType]
  318. [HttpPost("record")]
  319. //[AuthToken(Roles = "teacher,student")]
  320. public async Task<IActionResult> Record(JsonElement request)
  321. {
  322. if (!request.TryGetProperty("id", out JsonElement id))
  323. {
  324. return BadRequest();
  325. }
  326. //活动分区
  327. if (!request.TryGetProperty("code", out JsonElement code))
  328. {
  329. return BadRequest();
  330. }
  331. //获取投票活动的所有投票记录
  332. var records = await _azureRedis.GetRedisClient(8).HashGetAllAsync($"Vote:Record:{id}");
  333. //获取投票活动的选项及投票数
  334. var counts = _azureRedis.GetRedisClient(8).SortedSetRangeByScoreWithScores($"Vote:Count:{id}");
  335. List<OptionVote> options = new List<OptionVote>();
  336. if (counts != null && counts.Length > 0)
  337. {
  338. foreach (var count in counts)
  339. {
  340. options.Add(new OptionVote { code = count.Element.ToString(), count = (int)count.Score });
  341. }
  342. }
  343. List<JsonElement> res = new List<JsonElement>();
  344. foreach (var rcd in records)
  345. {
  346. var value = rcd.Value.ToString().ToObject<JsonElement>();
  347. res.Add(value);
  348. }
  349. return Ok(new { options, records= res});
  350. }
  351. /// <summary>
  352. /// 个人已投票记录查询
  353. /// </summary>
  354. /// <param name="request"></param>
  355. /// <returns></returns>
  356. [ProducesDefaultResponseType]
  357. [HttpPost("decided")]
  358. [AuthToken(Roles = "teacher,student")]
  359. public async Task<IActionResult> Decided(JsonElement request)
  360. {
  361. var (userid, _, _, _) = HttpContext.GetAuthTokenInfo();
  362. if (!request.TryGetProperty("id", out JsonElement id))
  363. {
  364. return BadRequest();
  365. }
  366. //活动分区
  367. if (!request.TryGetProperty("code", out JsonElement code))
  368. {
  369. return BadRequest();
  370. }
  371. //获取投票活动的所有投票记录
  372. var records = await _azureRedis.GetRedisClient(8).HashGetAllAsync($"Vote:Record:{id}:{userid}");
  373. List<JsonElement> res = new List<JsonElement>();
  374. foreach (var rcd in records)
  375. {
  376. var value = rcd.Value.ToString().ToObject<JsonElement>();
  377. res.Add(value);
  378. }
  379. return Ok(new { records= res });
  380. }
  381. /// <summary>
  382. /// 投票
  383. /// </summary>
  384. /// <redis>
  385. /// 投票活动选项计数器 使用SortedSet(有序集合)ZSET 数据集合 使用命令 ZINCRBY key:"Vote:Count:AAA",value:"A",score:1
  386. /// 投票活动 投票记录 使用Hash(哈希表)使用命令 key:"Vote:Record:AAA",feild:15283771540-20210105,value:"{"opt":["A","C","A"],"time":1608274766154}"
  387. /// </redis>
  388. /// <param name="request">
  389. /// !"id":"aaaa"
  390. /// !"code":"Vote-hbcn"/"code":"Vote-1606285227"
  391. /// !"option":{"A":5,"B":6}/{"1":5,"2":8}
  392. /// </param>
  393. /// <returns>
  394. /// msgid=0投票失败,1投票成功,2不在时间范围内,3不在发布范围内,4投票周期内重复投票,5周期内的可投票数不足
  395. ///
  396. /// </returns>
  397. [ProducesDefaultResponseType]
  398. [HttpPost("decide")]
  399. [AuthToken(Roles = "teacher,student")]
  400. public async Task<IActionResult> Decide(JsonElement request)
  401. {
  402. var (userid, _, _, _) = HttpContext.GetAuthTokenInfo();
  403. int msgid = await ActivityStudentService.Decide(request, _azureCosmos, _azureRedis, userid);
  404. return Ok(new { msgid });
  405. }
  406. /// <summary>
  407. /// 投票
  408. /// </summary>
  409. /// <redis>
  410. /// 投票活动选项计数器 使用SortedSet(有序集合)ZSET 数据集合 使用命令 ZINCRBY key:"Vote:Count:AAA",value:"A",score:1
  411. /// 投票活动 投票记录 使用Hash(哈希表)使用命令 key:"Vote:Record:AAA",feild:15283771540-20210105,value:"{"opt":["A","C","A"],"time":1608274766154}"
  412. /// </redis>
  413. /// <param name="request">
  414. /// !"id":"aaaa"
  415. /// !"code":"Vote-hbcn"/"code":"Vote-1606285227"
  416. /// !"option":{"A":5,"B":6}/{"1":5,"2":8}
  417. /// !"userid":"15283771540"
  418. /// </param>
  419. /// <returns>
  420. /// msgid=0投票失败,1投票成功,2不在时间范围内,3不在发布范围内,4投票周期内重复投票,5周期内的可投票数不足,6未设置投票项
  421. /// </returns>
  422. [ProducesDefaultResponseType]
  423. [HttpPost("decide-mock")]
  424. public async Task<IActionResult> DecideMock(JsonElement request)
  425. {
  426. //var (userid, _, _, _) = HttpContext.GetAuthTokenInfo();
  427. //活动id
  428. if (request.TryGetProperty("userid", out JsonElement userid))
  429. {
  430. int msgid = await ActivityStudentService.Decide(request, _azureCosmos, _azureRedis, $"{userid}");
  431. return Ok(new { msgid });
  432. }
  433. else { return Ok(new { msgid = 0 }); }
  434. }
  435. }
  436. }