12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502 |
- using System;
- using System.Collections;
- using System.Collections.Generic;
- using System.Dynamic;
- using System.IdentityModel.Tokens.Jwt;
- using System.IO;
- using System.Linq;
- using System.Net;
- using System.Net.Http;
- using System.Net.Http.Headers;
- using System.Net.Http.Json;
- using System.Text;
- using System.Text.Json;
- using System.Threading.Tasks;
- using Azure;
- using Azure.Cosmos;
- using Azure.Messaging.ServiceBus;
- using Azure.Storage.Sas;
- using HTEXLib.COMM.Helpers;
- using Microsoft.AspNetCore.Authorization;
- using Microsoft.AspNetCore.Cryptography.KeyDerivation;
- using Microsoft.AspNetCore.Hosting;
- using Microsoft.AspNetCore.Http;
- using Microsoft.AspNetCore.Mvc;
- using Microsoft.Azure.Amqp.Framing;
- using Microsoft.Extensions.Configuration;
- using Microsoft.Extensions.Hosting;
- using Microsoft.Extensions.Options;
- using Microsoft.Identity.Client;
- using Org.BouncyCastle.Asn1.Tsp;
- using StackExchange.Redis;
- using TEAMModelOS.Filter;
- using TEAMModelOS.Models;
- using TEAMModelOS.SDK;
- using TEAMModelOS.SDK.DI;
- using TEAMModelOS.SDK.Extension;
- using TEAMModelOS.SDK.Models;
- using TEAMModelOS.SDK.Models.Cosmos;
- using TEAMModelOS.SDK.Models.Cosmos.Common;
- using TEAMModelOS.SDK.Models.Service;
- using static TEAMModelOS.SDK.Services.BlobService;
- using static TEAMModelOS.SDK.StudentService;
- namespace TEAMModelOS.Controllers
- {
- [Route("student")]
- [ApiController]
- public class StudentController : Controller
- {
- private readonly AzureCosmosFactory _azureCosmos;
- private readonly AzureStorageFactory _azureStorage;
- private readonly AzureRedisFactory _azureRedis;
- private readonly DingDing _dingDing;
- private readonly Option _option;
- private readonly IPSearcher _searcher;
- private readonly HttpTrigger _httpTrigger;
- private readonly CoreAPIHttpService _coreAPIHttpService;
- private readonly IWebHostEnvironment _environment;
- public IConfiguration _configuration { get; set; }
- private readonly AzureServiceBusFactory _serviceBus;
- private readonly IHttpClientFactory _httpClient;
- public StudentController(IWebHostEnvironment environment, CoreAPIHttpService coreAPIHttpService, AzureCosmosFactory azureCosmos, AzureStorageFactory azureStorage, AzureRedisFactory azureRedis, DingDing dingDing, IPSearcher searcher, IOptionsSnapshot<Option> option,IConfiguration configuration, AzureServiceBusFactory serviceBus, HttpTrigger httpTrigger, IHttpClientFactory httpClient
- )
- {
- _searcher = searcher;
- _azureCosmos = azureCosmos;
- _azureStorage = azureStorage;
- _azureRedis = azureRedis;
- _dingDing = dingDing;
- _option = option?.Value;
- _configuration = configuration;
- _serviceBus = serviceBus;
- _httpTrigger = httpTrigger;
- _coreAPIHttpService = coreAPIHttpService;
- _environment = environment;
- _httpClient = httpClient;
- }
- /// <summary>
- /// 學生帳號管理
- /// </summary>
- /// <param name="request"></param>
- /// <returns></returns>
- [HttpPost("student-manage")]
- [Authorize(Roles = "IES")]
- [AuthToken(Roles = "admin,student,teacher")]
- public async Task<IActionResult> StudentManage(JsonElement request)
- {
- try
- {
- //TODO : 權限檢查、學校檢查。
- if (!request.TryGetProperty("grant_type", out JsonElement grant_type) || !request.TryGetProperty("schoolId", out JsonElement schoolId)) return BadRequest();
- List<Student> preStudents = null;
- List<Student> webStudents = null;
- var (stuid, _, _, school) = HttpContext.GetAuthTokenInfo();
- switch (grant_type.GetString())
- {
- case "create":
- //單人創建 創建學生->將學生加入教室 檢查學生ID是否重複,欲加入的班級存不存在,座號是否重複。
- //id pw name classId no year
- //retrun 如果有重複則回{ existNo } , 成功則{ id, name, year, classId, no }
- var importStuds = request.GetProperty("students").EnumerateArray();
- Dictionary<string, GroupChange> dictChange = new Dictionary<string, GroupChange>();
- while (importStuds.MoveNext())
- {
- JsonElement currStud = importStuds.Current;
- string id = null, name = null, pw = null, no = null, classId = null, periodId = null, imei = null , gender= null;
- List<StudentGuardian> guardians = null;
- int year = 0;
- //讀取輸入的資料
- if (!currStud.TryGetProperty("id", out var tmpId) || !currStud.TryGetProperty("name", out var tmpName)) continue;
- id = tmpId.GetString();
- name = tmpName.GetString();
- if (currStud.TryGetProperty("pw", out var tmpPw)) pw = tmpPw.GetString();
- if (currStud.TryGetProperty("year", out var tmpYear)) year = tmpYear.GetInt32();
- if (currStud.TryGetProperty("no", out var tmpNo)) no = tmpNo.GetString();
- if (currStud.TryGetProperty("classId", out var tmpClassId)) classId = tmpClassId.GetString();
- if (currStud.TryGetProperty("imei", out var tmpImei)) imei = $"{tmpImei}";
- if (currStud.TryGetProperty("gender", out var tmpGender)) gender = $"{tmpGender}";
- if (currStud.TryGetProperty("guardians", out var tmpGuardians)) {
- guardians = tmpGuardians.Deserialize<List<StudentGuardian>>();
- }
- //要檢查座號使否已被使用
- var existNo = await StudentService.checkStudNo(_azureCosmos, _dingDing, _option, schoolId.GetString(), classId, new List<string>() { id });
- if (existNo.Count != 0) return this.Ok(new { code = $"Base-{schoolId.GetString()}", existNo = existNo.Select(o => o.id).ToList() });
- if (currStud.TryGetProperty("periodId", out var tempPeriodId)) periodId = tempPeriodId.GetString();
- //建立學生
- studCreateInfo studCreateInfo = new studCreateInfo(id, name, gender, year, pw, classId, no, periodId,imei,guardians);
- var isCreateSuc = await createStudent(_azureCosmos, _dingDing, _option, schoolId.GetString(), studCreateInfo);
- if (isCreateSuc)
- {
- if (dictChange.ContainsKey(classId))
- {
- dictChange[classId].stujoin.Add(
- new Member
- {
- id = id,
- code = $"{schoolId.GetString()}",
- type = 2,
- });
- }
- else
- {
- GroupChange change = new GroupChange
- {
- scope = "school",
- school = schoolId.GetString(),
- type = "student",
- originCode = schoolId.GetString(),
- listid = classId,
- stujoin = new List<Member>
- {
- new Member
- {
- id= id,
- code=schoolId.GetString(),
- type=2,
- }
- }
- };
- dictChange.Add(classId, change);
- }
- foreach (var changed in dictChange.Keys)
- {
- var change = dictChange[changed];
- if (change.stujoin.Count != 0 || change.stuleave.Count != 0)
- {
- var messageChange = new ServiceBusMessage(change.ToJsonString());
- messageChange.ApplicationProperties.Add("name", "GroupChange");
- var ActiveTask = _configuration.GetValue<string>("Azure:ServiceBus:ActiveTask");
- await _serviceBus.GetServiceBusClient().SendMessageAsync(ActiveTask, messageChange);
- }
- }
- return this.Ok(new { code = $"Base-{schoolId.GetString()}", id, name, year, classId, no, periodId });
- }
- else return this.Ok(new { code = $"Base-{schoolId.GetString()}", errorId = id });
- }
- break;
- case "import":
- //只有ClassNo可以比對
- webStudents = request.GetProperty("students").ToObject<List<Student>>();
- preStudents = await StudentService.GeStudentData(_azureCosmos, schoolId.GetString(), webStudents?.Select(x => x.id));
- var retUpsert = await StudentService.upsertStudents(_azureCosmos, _dingDing, _option, schoolId.GetString(), request.GetProperty("students").EnumerateArray());
- await CheckStudent(_serviceBus, _configuration, _azureCosmos, schoolId.GetString(), webStudents, preStudents);
- School school_base = await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, "School").ReadItemAsync<School>($"{schoolId}", new PartitionKey("Base"));
- //实时处理导入毕业的班级。
- await SchoolService.DoGraduateClasses(_httpTrigger, _azureCosmos, null, school_base, _option,_dingDing);
- return this.Ok(new { code = $"Base-{schoolId.GetString()}", students = retUpsert.studs, retUpsert.classDuplNos, retUpsert.errorIds });
- case "read":
- //增加毕业查询。当毕业查询字段1时,则需要传入入学年份。
- int graduate = 0;
- int inyear = 0;
- //讀取該間學校所有的學生資訊
- if (request.TryGetProperty("graduate", out JsonElement _graduate) && $"{_graduate}".Equals("1")) {
- if (request.TryGetProperty("year", out JsonElement _year) && _year.ValueKind.Equals(JsonValueKind.Number))
- {
- if (int.TryParse($"{_year}", out inyear) && inyear > 0)
- {
- graduate = 1;
- }
- else {
- return BadRequest("入学年份大于0");
- }
- }
- else {
- return BadRequest("请输入毕业学生的入学年份");
- }
- }
-
- var students = await StudentService.getAllStudent(_azureCosmos, _dingDing, _option, schoolId.GetString(), inyear, graduate);
- return this.Ok(new { code = $"Base-{schoolId.GetString()}", students });
- case "update":
- //更新學生資料,批量密碼重置,基本資訊更新(姓名、教室ID、性別、學年及座號)
- webStudents = request.GetProperty("students").ToObject<List<Student>>();
- //var cleanImei = false;
- // request.TryGetProperty("cleanImei",out JsonElement _cleanImei);
- //if (_cleanImei.ValueKind.Equals(JsonValueKind.True))
- //{
- // cleanImei = true;
- //}
-
- preStudents = await StudentService.GeStudentData(_azureCosmos, schoolId.GetString(), webStudents?.Select(x => x.id));
- var retUpdate = await StudentService.updateStudents(_azureCosmos, _dingDing, _option, schoolId.GetString(), request.GetProperty("students").EnumerateArray());
- await StudentService.CheckStudent(_serviceBus, _configuration, _azureCosmos, schoolId.GetString(), webStudents, preStudents);
- return this.Ok(new { code = $"Base-{schoolId.GetString()}", students = retUpdate.studs, retUpdate.classDuplNos, retUpdate.nonexistentIds, retUpdate.errorNos, retUpdate.errorClassId });
- case "delete":
- //刪除學生資料及從教室學生名單內移除該學生
- webStudents = request.GetProperty("students").ToObject<List<Student>>();
- preStudents = await StudentService.GeStudentData(_azureCosmos, schoolId.GetString(), webStudents?.Select(x => x.id));
- var sucDelIds = await StudentService.deleteStudents(_azureCosmos, _dingDing, _option, schoolId.GetString(), request.GetProperty("students").EnumerateArray());
- await StudentService.CheckStudent(_serviceBus, _configuration, _azureCosmos, schoolId.GetString(), webStudents, preStudents);
- return this.Ok(new { code = $"Base-{schoolId.GetString()}", ids = sucDelIds });
- case "remove":
- //將學生基本資料內的classId、no、groupId及groupName寫入null
- List<string> stus = request.GetProperty("students").ToObject<List<string>>();
- webStudents = new List<Student>();
- foreach (string idstu in stus)
- {
- webStudents.Add(new Student { id = idstu, code = $"Base-{schoolId}" });
- }
- preStudents = await StudentService.GeStudentData(_azureCosmos, schoolId.GetString(), webStudents?.Select(x => x.id));
- (List<string> studs, List<string> nonexistentIds, List<string> errorIds) retRemove = await StudentService.removeStudentClassInfo(
- _azureCosmos, _dingDing, _option,
- schoolId.GetString(), request.GetProperty("students").EnumerateArray());
- await StudentService.CheckStudent(_serviceBus, _configuration, _azureCosmos, schoolId.GetString(), webStudents, preStudents);
- return Ok(new { code = $"Base-{schoolId.GetString()}", ids = retRemove.studs, retRemove.nonexistentIds, retRemove.errorIds });
- case "avatar":
- if (request.TryGetProperty("avatar", out JsonElement _avatar) && _avatar.ValueKind.Equals(JsonValueKind.Array))
- {
- List<StudentInfo> avatars = _avatar.ToObject<List<StudentInfo>>();
- if (avatars.IsNotEmpty())
- {
- List<Student> studentsp = new List<Student>();
- string insql = string.Join(',', avatars.Select(x => $"'{x.studentId}'"));
- string sql = $"select value(c) from c where c.id in ({insql}) ";
- await foreach (var item in _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, "Student")
- .GetItemQueryIterator<Student>(sql, requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey($"Base-{schoolId}") }))
- {
- studentsp.Add(item);
- }
- (string url, string sas) = _azureStorage.GetBlobContainerSAS99Year($"{schoolId}", BlobContainerSasPermissions.Read);
- foreach (Student student in studentsp)
- {
- StudentInfo avatar = avatars.Find(x => x.studentId.Equals(student.id));
- student.picture = avatar != null ? $"{avatar.picture}?{sas}" : null;
- await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, "Student").ReplaceItemAsync<Student>(student, student.id, new PartitionKey(student.code));
- }
- return Ok(new { students = studentsp.Select(x => new { x.id, x.picture, x.code, x.name }) });
- }
- else
- {
- return BadRequest();
- }
- }
- else
- {
- return BadRequest();
- }
- case "update-self-info":
- if (request.TryGetProperty("studentInfo", out JsonElement _studentInfo))
- {
- var studentInfo = _studentInfo.ToObject<StudentInfo>();
- if (studentInfo.studentId.Equals(stuid) && school.Equals($"{schoolId}"))
- {
- Student student = await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, "Student")
- .ReadItemAsync<Student>(studentInfo.studentId, new PartitionKey($"Base-{schoolId}"));
- student.mail = string.IsNullOrEmpty(studentInfo.mail) ? student.mail : studentInfo.mail;
- student.mobile = string.IsNullOrEmpty(studentInfo.mobile) ? student.mobile : studentInfo.mobile;
- student.name = string.IsNullOrEmpty(studentInfo.name) ? student.name : studentInfo.name;
- (string url, string sas) = _azureStorage.GetBlobContainerSAS99Year($"{schoolId}", BlobContainerSasPermissions.Read);
- student.picture = string.IsNullOrEmpty(studentInfo.picture) ? student.picture : $"{studentInfo.picture}?{sas}";
- student.gender = string.IsNullOrEmpty(studentInfo.gender) ? student.gender : studentInfo.gender;
- await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, "Student")
- .ReplaceItemAsync<Student>(student, studentInfo.studentId, new PartitionKey($"Base-{schoolId}"));
- return Ok(new { studentInfo });
- }
- else
- {
- return Ok(new { error = false, status = false, msg = "修改的不是自己的信息" });
- }
- }
- else
- {
- return BadRequest();
- }
- case "update-self-password":
- if (request.TryGetProperty("newpwd", out JsonElement _newpwd) &&
- request.TryGetProperty("oldpwd", out JsonElement _oldpwd) &&
- request.TryGetProperty("studentId", out JsonElement _studentId))
- {
- if ($"{_studentId}".Equals(stuid) && school.Equals($"{schoolId}"))
- {
- try
- {
- Student student = await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, "Student")
- .ReadItemAsync<Student>($"{_studentId}", new PartitionKey($"Base-{schoolId}"));
- var HashedPW = Utils.HashedPassword($"{_oldpwd}", student.salt);
- if (HashedPW.Equals(student.pw))
- {
- student.pw = Utils.HashedPassword($"{_newpwd}", student.salt);
- await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, "Student")
- .ReplaceItemAsync<Student>(student, student.id, new PartitionKey($"Base-{schoolId}"));
- return Ok(new { status = true });
- }
- else
- {
- return Ok(new { error = false, status = false, msg = "密码不一致" });
- }
- }
- catch (Exception ex)
- {
- await _dingDing.SendBotMsg($"IES5,{_option.Location},StudentController/StudentManage()\n{ex.Message}\n{ex.StackTrace}\n{request.ToJsonString()}", GroupNames.醍摩豆服務運維群組);
- return Ok(new { error = false, status = false, msg = "账号不存在" });
- }
- }
- else
- {
- return Ok(new { error = false, status = false, msg = "修改的不是自己的密码" });
- }
- }
- else
- {
- return BadRequest();
- }
- case "read-self-info":
- if (request.TryGetProperty("studentId", out JsonElement __studentId))
- {
- try
- {
- Student student = await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, "Student")
- .ReadItemAsync<Student>($"{__studentId}", new PartitionKey($"Base-{schoolId}"));
- return Ok(new { student.mail, student.mobile, student.name, student.picture, student.gender, student.id, student.schoolId, student.year, student.no, student.classId, student.periodId, irs = student.irs });
- }
- catch (Exception ex)
- {
- await _dingDing.SendBotMsg($"IES5,{_option.Location},StudentController/StudentManage()\n{ex.Message}\n{ex.StackTrace}\n{request.ToJsonString()}", GroupNames.醍摩豆服務運維群組);
- }
- return Ok();
- }
- else
- {
- return BadRequest();
- }
- default:
- return BadRequest();
- }
- }
- catch (Exception ex)
- {
- await _dingDing.SendBotMsg($"IES5,{_option.Location},StudentController/StudentManage()\n{ex.Message}\n{ex.StackTrace}\n{request.ToJsonString()}", GroupNames.醍摩豆服務運維群組);
- }
- return BadRequest();
- }
- [HttpPost("get-student-info")]
-
- #if !DEBUG
- [Authorize(Roles = "IES")]
- #endif
- public async Task<IActionResult> GetStudentInfo(JsonElement request)
- {
- var client = _azureCosmos.GetCosmosClient();
- var schoolClient = client.GetContainer(Constant.TEAMModelOS, "School");
- var teacherClient = client.GetContainer(Constant.TEAMModelOS, "Teacher");
- var studentClient = client.GetContainer(Constant.TEAMModelOS, "Student");
- if (!request.TryGetProperty("id_token", out JsonElement id_token)) return BadRequest();
- var jwt = new JwtSecurityToken(id_token.GetString());
- var id = jwt.Payload.Sub;
- string school_code = jwt.Payload.Azp;
- //權限token
- jwt.Payload.TryGetValue("name", out object _name);
- jwt.Payload.TryGetValue("picture", out object _picture);
- jwt.Payload.TryGetValue("lang", out object _lang);
- (string ip, string region) = await LoginService.LoginIp(HttpContext, _searcher);
- School school = await schoolClient.ReadItemAsync<School>($"{school_code}", new PartitionKey("Base"));
- var response = await studentClient.ReadItemStreamAsync(id, new PartitionKey($"Base-{school_code.ToLower()}"));
- if (response.Status == 200)
- {
- var rjson = await JsonDocument.ParseAsync(response.ContentStream);
- Student student = rjson.ToObject<Student>();
- rjson.RootElement.TryGetProperty("salt", out JsonElement salt);
- rjson.RootElement.TryGetProperty("pw", out JsonElement dbpw);
- rjson.RootElement.TryGetProperty("name", out JsonElement name);
- rjson.RootElement.TryGetProperty("picture", out JsonElement picture);
- rjson.RootElement.TryGetProperty("classId", out JsonElement classId);
- rjson.RootElement.TryGetProperty("no", out JsonElement no);
- rjson.RootElement.TryGetProperty("groupId", out JsonElement groupId);
- rjson.RootElement.TryGetProperty("groupName", out JsonElement groupName);
- (string auth_token, string blob_uri, string blob_sas, object classinfo, List<object> courses, AuthenticationResult token) = await StudentCheck(school,$"{id}", $"{classId}", $"{school_code}", $"{picture}", $"{name}", schoolClient, teacherClient, school.areaId, ip, client, student);
- //授权规模数量
- DateTimeOffset dateTime = DateTimeOffset.UtcNow;
- var dateDay = dateTime.ToString("yyyyMMdd"); //获取当天的日期
- string key = $"Login:School:{school_code}:student-day:{dateDay}";
- SortedSetEntry[] countStudent = _azureRedis.GetRedisClient(8).SortedSetRangeByScoreWithScores(key);
- int countAuthorized = 0;
- if (countStudent != null && countStudent.Length > 0)
- {
- countAuthorized = countStudent.Length;
- }
- int scale = school.scale;
- if (scale<=0)
- {
- string sql = $" SELECT value s FROM c join s in c.service where c.id='{school_code}' and s.prodCode='3CLYJ6NP' ";
- var result = await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.School).GetList<SchoolProductSumDataService>(sql, "ProductSum");
- if (result.list.IsNotEmpty())
- {
- SchoolProductSumDataService service = result.list[0];
- if (service.avaliable>0)
- {
- scale= service.avaliable;
- }
- }
- }
-
- return Ok(new { scale, countAuthorized, location = _option.Location, error = 0, auth_token, blob_uri, blob_sas, classinfo, courses, token = new { access_token = token.AccessToken, expires_in = token.ExpiresOn, id_token = auth_token, token_type = token.TokenType } });
- }
- else
- {
- return Ok(new { error = 2, message = "無此帳號存在" });
- }
- }
- /// <summary>
- /// 學生登入
- /// </summary>
- /// <param name = "request" ></ param >
- [AllowAnonymous]
- [HttpPost("login")]
- public async Task<IActionResult> Login(JsonElement request)
- {
- try
- {
- var client = _azureCosmos.GetCosmosClient();
- var schoolClient = client.GetContainer(Constant.TEAMModelOS, "School");
- var teacherClient = client.GetContainer(Constant.TEAMModelOS, "Teacher");
- var studentClient = client.GetContainer(Constant.TEAMModelOS, "Student");
- //參數取得
- if (!request.TryGetProperty("school_code", out JsonElement school_code)) return BadRequest();
- if (!request.TryGetProperty("id", out JsonElement id)) return BadRequest();
- if (!request.TryGetProperty("pw", out JsonElement pw)) return BadRequest();
- (string ip, string region) = await LoginService.LoginIp(HttpContext, _searcher);
- School school = await schoolClient.ReadItemAsync<School>($"{school_code}", new PartitionKey("Base"));
- var response = await studentClient.ReadItemStreamAsync(id.GetString(), new PartitionKey($"Base-{school_code.GetString().ToLower()}"));
- if (response.Status == 200)
- {
- var rjson = await JsonDocument.ParseAsync(response.ContentStream);
- Student student = rjson.ToObject<Student>();
- rjson.RootElement.TryGetProperty("salt", out JsonElement salt);
- rjson.RootElement.TryGetProperty("pw", out JsonElement dbpw);
- rjson.RootElement.TryGetProperty("name", out JsonElement name);
- rjson.RootElement.TryGetProperty("picture", out JsonElement picture);
- rjson.RootElement.TryGetProperty("classId", out JsonElement classId);
- rjson.RootElement.TryGetProperty("no", out JsonElement no);
- rjson.RootElement.TryGetProperty("groupId", out JsonElement groupId);
- rjson.RootElement.TryGetProperty("groupName", out JsonElement groupName);
- rjson.RootElement.TryGetProperty("graduate", out JsonElement graduate);
- if (graduate.ValueKind.Equals(JsonValueKind.Number) && $"{graduate}".Equals("1")) {
- return Ok(new { error = 3, message = "你已毕业,暂不能登录!" });
- }
- var HashedPW = Utils.HashedPassword(pw.ToString(), salt.ToString());
- if (HashedPW.Equals(dbpw.GetString()))
- {
- (string auth_token, string blob_uri, string blob_sas, object classinfo, List<object> courses, AuthenticationResult token) = await StudentCheck(school,$"{id}", $"{classId}", $"{school_code}", $"{picture}", $"{name}", schoolClient, teacherClient, school.areaId,ip, client, student);
- int countAuthorized = await GetStudentAuthNumByScale($"{school_code}", school);
- return Ok(new {school.scale, countAuthorized, location = _option.Location, error = 0, auth_token, blob_uri, blob_sas, classinfo, courses, token = new { access_token = token.AccessToken, expires_in = token.ExpiresOn, id_token = auth_token, token_type = token.TokenType } });
- }
- else
- {
- return Ok(new { error = 1, message = "账号或密码错误" });
- }
- }
- else
- {
- return Ok(new { error = 2, message = "無此帳號存在" });
- }
- }
- catch (Exception ex)
- {
- await _dingDing.SendBotMsg($"IES5,{_option.Location},StudentController/login()\n{ex.Message}\n{ex.StackTrace}", GroupNames.醍摩豆服務運維群組);
- return BadRequest();
- }
- }
- private async Task<(string auth_token, string blob_uri, string blob_sas, object classinfo, List<object> courses, AuthenticationResult token)> StudentCheck(School schoolBase,string id, string classId, string school_code, string picture, string name, CosmosContainer schoolClient, CosmosContainer teacherClient, string areaId,string ip,CosmosClient cosmosClient,Student student)
- {
- //班級課程
- object classinfo = null;
- List<object> courses = new List<object>();
- ////校本
- //取得所屬預設班級信息
- if (!string.IsNullOrWhiteSpace(classId))
- {
- var query = $"SELECT c.code, c.id, c.name, c.periodId, c.gradeId FROM c WHERE c.id = '{classId}'";
- await foreach (var item in schoolClient.GetItemQueryStreamIterator(queryText: query, requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"Class-{school_code}") }))
- {
- using var json = await JsonDocument.ParseAsync(item.ContentStream);
- if (json.RootElement.TryGetProperty("_count", out JsonElement count) && count.GetUInt16() > 0)
- {
- foreach (var obj in json.RootElement.GetProperty("Documents").EnumerateArray())
- {
- classinfo = obj.ToObject<object>();
- }
- }
- }
- }
- //取得該學生跑班課名單ID
- List<string> stulistidsSch = new List<string>();
- var querysl = $"SELECT c.id FROM c JOIN members IN c.members WHERE members.id = '{id}' AND members.code = '{school_code}'";
- await foreach (var item in schoolClient.GetItemQueryStreamIterator(queryText: querysl, requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"GroupList-{school_code}") }))
- {
- using var json = await JsonDocument.ParseAsync(item.ContentStream);
- if (json.RootElement.TryGetProperty("_count", out JsonElement count) && count.GetUInt16() > 0)
- {
- foreach (var obj in json.RootElement.GetProperty("Documents").EnumerateArray())
- {
- stulistidsSch.Add(obj.GetProperty("id").ToString());
- }
- }
- }
- //取得該學生的學校課程名單
- var queryc = $"SELECT DISTINCT c.id, c.name, schedule.class, schedule.time, schedule.notice, c.scope FROM c JOIN schedule IN c.schedule WHERE (schedule.class.id = '{classId}' AND schedule.stulist = null) OR (ARRAY_CONTAINS({JsonSerializer.Serialize(stulistidsSch)}, schedule.stulist, true))";
- await foreach (var item in schoolClient.GetItemQueryStreamIterator(queryText: queryc, requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"Course-{school_code}") }))
- {
- using var json = await JsonDocument.ParseAsync(item.ContentStream);
- if (json.RootElement.TryGetProperty("_count", out JsonElement count) && count.GetUInt16() > 0)
- {
- foreach (var obj in json.RootElement.GetProperty("Documents").EnumerateArray())
- {
- courses.Add(obj.ToObject<object>());
- }
- }
- }
- ////個人
- //取得該學生跑班課名單ID
- Dictionary<string, Dictionary<string, string>> stulistidsTea = new Dictionary<string, Dictionary<string, string>>();
- var queryslt = $"SELECT c.id, c.course.id as courseId, c.course.code as courseCode FROM c JOIN members IN c.members WHERE members.id = '{id}' AND members.code = '{school_code}'";
- await foreach (var item in teacherClient.GetItemQueryStreamIterator(queryText: queryslt, requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey("GroupList") }))
- {
- using var json = await JsonDocument.ParseAsync(item.ContentStream);
- var js = json.RootElement.ToJsonString();
- if (json.RootElement.TryGetProperty("_count", out JsonElement count) && count.GetUInt16() > 0)
- {
- foreach (var obj in json.RootElement.GetProperty("Documents").EnumerateArray())
- {
- string courseCode = "";
- if (obj.TryGetProperty("courseCode", out var code))
- {
- courseCode = code.GetString();
- }
- string courseId = "";
- if (obj.TryGetProperty("courseId", out var cosid))
- {
- courseId = cosid.GetString();
- }
- string stulistId = "";
- if (obj.TryGetProperty("id", out var listId))
- {
- stulistId = listId.GetString();
- }
- if (!string.IsNullOrEmpty(courseCode))
- {
- if (!stulistidsTea.ContainsKey(courseCode))
- {
- Dictionary<string, string> pCourseIdDic = new Dictionary<string, string>();
- pCourseIdDic.Add(courseId, stulistId);
- stulistidsTea.Add(courseCode, pCourseIdDic);
- }
- else
- {
- if (!stulistidsTea[courseCode].ContainsKey(courseId))
- {
- stulistidsTea[courseCode].Add(courseId, stulistId);
- }
- }
- }
- }
- }
- }
- //取得該學生的老師個人課程名單
- foreach (KeyValuePair<string, Dictionary<string, string>> item in stulistidsTea)
- {
- string courseCode = item.Key;
- Dictionary<string, string> courseIdDic = item.Value;
- string stucourseWhere = string.Empty;
- foreach (KeyValuePair<string, string> itemDic in courseIdDic)
- {
- string courseId = itemDic.Key;
- string stuListId = itemDic.Value;
- if (!string.IsNullOrWhiteSpace(stucourseWhere))
- {
- stucourseWhere += " OR ";
- }
- stucourseWhere += $"( c.id = '{courseId}' AND schedule.stulist = '{stuListId}' )";
- }
- var querycst = $"SELECT DISTINCT c.id, c.name, schedule.class, schedule.time, schedule.notice, c.scope FROM c JOIN schedule IN c.schedule WHERE {stucourseWhere}";
- await foreach (var itemcs in teacherClient.GetItemQueryStreamIterator(queryText: querycst, requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"{courseCode}") }))
- {
- using var json = await JsonDocument.ParseAsync(itemcs.ContentStream);
- if (json.RootElement.TryGetProperty("_count", out JsonElement count) && count.GetUInt16() > 0)
- {
- foreach (var obj in json.RootElement.GetProperty("Documents").EnumerateArray())
- {
- courses.Add(obj.ToObject<object>());
- }
- }
- }
- }
- // BLOB(學校,唯讀)
- var (blob_uri, blob_sas) = _azureStorage.GetBlobContainerSAS(school_code.ToLower(), BlobContainerSasPermissions.Read);
- //換取AuthToken,提供給前端
- var auth_token = JwtAuthExtension.CreateAuthToken(_option.HostName, id, name, picture, _option.JwtSecretKey, scope: Constant.ScopeStudent, Website: "IES", areaId: areaId, schoolID: school_code, roles: new[] { "student" }, expire: 1,year: student.year);
- //用户在线记录
- try
- {
- _ = _httpTrigger.RequestHttpTrigger(new { school = school_code, scope = $"{Constant.ScopeStudent}", id = $"{id}", ip = $"{ip}", expire = 1 }, _option.Location, "online-record");
- }
- catch {}
- await cosmosClient.GetContainer("TEAMModelOS", "Student").ReplaceItemAsync<Student>(student, id, new PartitionKey($"Base-{school_code}"));
-
- var clientID = _configuration.GetValue<string>("HaBookAuth:CoreService:clientID");
- var clientSecret = _configuration.GetValue<string>("HaBookAuth:CoreService:clientSecret");
- var token = await CoreTokenExtensions.CreateAccessToken(clientID, clientSecret, _option.Location.Replace("-Dep", "").Replace("-Test", ""));
- return (auth_token, blob_uri, blob_sas, classinfo, courses, token);
- }
- /// <summary>
- /// 學生教育雲登入
- /// </summary>
- /// <param name = "request" ></ param >
- [AllowAnonymous]
- [HttpPost("login-open")]
- public async Task<IActionResult> OpenIDLogin(JsonElement request)
- {
- try
- {
- var client = _azureCosmos.GetCosmosClient();
- var schoolClient = client.GetContainer(Constant.TEAMModelOS, "School");
- var teacherClient = client.GetContainer(Constant.TEAMModelOS, "Teacher");
- var studentClient = client.GetContainer(Constant.TEAMModelOS, "Student");
- //參數取得
- string location = _option.Location;
- if (!request.TryGetProperty("open_code", out JsonElement _open_code)) return BadRequest();
- if (!location.Contains("Global")) return BadRequest();
- string grant_type = "educloudtw";
- string client_id = _configuration.GetValue<string>("HaBookAuth:CoreService:clientID");
- string redirect_uri = _configuration.GetValue<string>("HaBookAuth:CoreAccountAPI");
- string nonce = RandomString(16);
- string lang = "zh-tw";
- string open_code = _open_code.GetString();
- if(!open_code.Contains("EduCloudTWL")) return BadRequest();
- bool is_extrnal_id = true;
- //向CS取得OpenData
- stuOpenData openData = new stuOpenData();
- string csv2Domain = _configuration.GetValue<string>("HaBookAuth:CoreAPI");
- string csv2Url = $"{csv2Domain}/oauth2/Login";
- Dictionary<string, object> dict = new() {
- { "grant_type", grant_type },
- { "client_id", client_id },
- { "redirect_uri", $"{redirect_uri}/" },
- { "nonce", nonce },
- { "lang", lang },
- { "open_code", open_code },
- { "is_extrnal_id", is_extrnal_id }
- };
- var httpClient = _httpClient.CreateClient();
- HttpContent content = new StringContent(dict.ToJsonString(), Encoding.UTF8, "application/json");
- HttpResponseMessage httpResponse = await httpClient.PostAsync(csv2Url, content);
- if (httpResponse.StatusCode == HttpStatusCode.OK)
- {
- string responseContent = await httpResponse.Content.ReadAsStringAsync();
- openData = responseContent.ToObject<stuOpenData>();
- if (string.IsNullOrWhiteSpace(openData.open_id) || string.IsNullOrWhiteSpace(openData.schoolCode))
- {
- return Ok(new { error = 1, message = "Can not get opendata from CS." });
- }
- }
- else
- {
- return Ok(new { error = 1, message = "Can not get opendata from CS." });
- }
- //用OpenData取得學生資訊
- Student stuinfo = new Student();
- var queryLogin = $"SELECT * FROM c WHERE c.pk = 'Base' AND IS_DEFINED(c.openId) AND c.openId = '{openData.open_id}'";
- await foreach (var item in studentClient.GetItemQueryStreamIterator(queryText: queryLogin, requestOptions: null))
- {
- using var json = await JsonDocument.ParseAsync(item.ContentStream);
- if (json.RootElement.TryGetProperty("_count", out JsonElement count) && count.GetUInt16().Equals(1))
- {
- foreach (var obj in json.RootElement.GetProperty("Documents").EnumerateArray())
- {
- stuinfo = obj.ToObject<Student>();
- }
- }
- }
- (string ip, string region) = await LoginService.LoginIp(HttpContext, _searcher);
- string schShortCode = string.Empty;
- School school = new School();
- //分歧1 有此學生 => Login流程
- if (!string.IsNullOrWhiteSpace(stuinfo.id))
- {
- if (stuinfo.graduate.Equals(1))
- {
- return Ok(new { error = 3, message = "Graduate already!" });
- }
- //取得學校資訊
- schShortCode = stuinfo.schoolId;
- try
- {
- school = await schoolClient.ReadItemAsync<School>($"{schShortCode}", new PartitionKey("Base"));
- }
- catch (CosmosException ex)
- {
- return Ok(new { error = 2, message = "Can not find school data." });
- }
- //Login流程回傳值
- (string auth_token, string blob_uri, string blob_sas, object classinfo, List<object> courses, AuthenticationResult token) = await StudentCheck(school, $"{stuinfo.id}", $"{stuinfo.classId}", $"{schShortCode}", $"{stuinfo.picture}", $"{stuinfo.name}", schoolClient, teacherClient, school.areaId, ip, client, stuinfo);
- int countAuthorized = await GetStudentAuthNumByScale($"{school.id}", school);
- return Ok(new { school.scale, countAuthorized, location = _option.Location, error = 0, auth_token, blob_uri, blob_sas, classinfo, courses, token = new { access_token = token.AccessToken, expires_in = token.ExpiresOn, id_token = auth_token, token_type = token.TokenType } });
- }
- //分歧2 無此學生 => 取得該校同名學生資訊
- else
- {
- //把OpenData的schoolCode換成學校簡碼
- string AccessToken = await getCoreAccessToken();
- httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", AccessToken);
- string csv2SchoolUrl = $"{csv2Domain}/service/SchoolData";
- Dictionary<string, object> csv2SchDict = new() {
- { "code", openData.schoolCode }
- };
- HttpContent csv2SchContent = new StringContent(csv2SchDict.ToJsonString(), Encoding.UTF8, "application/json");
- HttpResponseMessage csv2SchHttpResponse = await httpClient.PostAsync(csv2SchoolUrl, csv2SchContent);
- if (csv2SchHttpResponse.StatusCode == HttpStatusCode.OK)
- {
- string responseContent = await csv2SchHttpResponse.Content.ReadAsStringAsync();
- if (!string.IsNullOrWhiteSpace(responseContent))
- {
- List<csSchApiResponse> csSchResult = responseContent.ToObject<List<csSchApiResponse>>();
- if (!csSchResult.Count.Equals(1))
- {
- return Ok(new { error = 2, message = "Can not find school data." });
- }
- foreach (csSchApiResponse csv2Sch in csSchResult)
- {
- schShortCode = csv2Sch.code;
- }
- }
- }
- else
- {
- return Ok(new { error = 2, message = "Can not find school data." });
- }
- if (string.IsNullOrWhiteSpace(schShortCode))
- {
- return Ok(new { error = 2, message = "Can not find school data." });
- }
- //用OpenData取得學校資訊
- try
- {
- school = await schoolClient.ReadItemAsync<School>($"{schShortCode}", new PartitionKey("Base"));
- }
- catch (CosmosException ex)
- {
- return Ok(new { error = 2, message = "Can not find school data." });
- }
- //學段
- Dictionary<string, string> periodDic = new Dictionary<string, string>();
- foreach(Period stuSchoolPeriod in school.period)
- {
- periodDic.Add(stuSchoolPeriod.id, stuSchoolPeriod.name);
- }
- //取得同名學生
- HashSet<string> classIds = new HashSet<string>();
- List<Student> stuList = new List<Student>();
- var queryStuSame = $"SELECT * FROM c WHERE c.name = '{openData.open_name}'";
- await foreach (var item in studentClient.GetItemQueryStreamIterator(queryText: queryStuSame, requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"Base-{schShortCode}") }))
- {
- using var json = await JsonDocument.ParseAsync(item.ContentStream);
- if (json.RootElement.TryGetProperty("_count", out JsonElement count) && count.GetUInt16() > 0)
- {
- foreach (var obj in json.RootElement.GetProperty("Documents").EnumerateArray())
- {
- Student stuRow = obj.ToObject<Student>();
- stuList.Add(stuRow);
- if(!string.IsNullOrWhiteSpace(stuRow.classId))
- {
- classIds.Add(stuRow.classId);
- }
- }
- }
- }
- if(stuList.Count.Equals(0))
- {
- return Ok(new { error = 4, message = "Can not find any student." });
- }
- if (classIds.Count.Equals(0))
- {
- return Ok(new { error = 6, message = "Can not find any class." });
- }
- //取得學校班級
- Dictionary<string, string> classDic = new Dictionary<string, string>();
- string classIdJsonStr = JsonSerializer.Serialize(classIds);
- var query = $"SELECT c.code, c.id, c.name, c.periodId, c.gradeId FROM c WHERE ARRAY_CONTAINS({classIdJsonStr}, c.id)";
- await foreach (var item in schoolClient.GetItemQueryStreamIterator(queryText: query, requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"Class-{schShortCode}") }))
- {
- using var json = await JsonDocument.ParseAsync(item.ContentStream);
- if (json.RootElement.TryGetProperty("_count", out JsonElement count) && count.GetUInt16() > 0)
- {
- foreach (var obj in json.RootElement.GetProperty("Documents").EnumerateArray())
- {
- Class classinfo = obj.ToObject<Class>();
- classDic.Add(classinfo.id, classinfo.name);
- }
- }
- }
- if (classDic.Count.Equals(0))
- {
- return Ok(new { error = 6, message = "Can not find any class." });
- }
- //OpenID AES加密
- string aeskey = _configuration.GetValue<string>("HaBookAuth:CoreService:clientID");
- string aesiv = _configuration.GetValue<string>("HaBookAuth:CoreService:clientSecret");
- openToken openToken = new openToken() { openId = openData.open_id, shortCode = schShortCode };
- string encOpenToken = LoginService.AesEncrypt(JsonSerializer.Serialize(openToken), aeskey, aesiv);
- //回傳值
- var schResult = new { schoolId = school.id, schoolName = school.name };
- List<stuOpenDataOrientation> stuResult = new List<stuOpenDataOrientation>();
- foreach (Student studata in stuList)
- {
- stuOpenDataOrientation stuResultRow = new stuOpenDataOrientation();
- stuResultRow.id = studata.id;
- stuResultRow.name = studata.name;
- stuResultRow.classId = studata.classId;
- stuResultRow.className = (!string.IsNullOrWhiteSpace(studata.classId) && classDic.ContainsKey(studata.classId)) ? classDic[studata.classId] : string.Empty;
- stuResultRow.periodId = studata.periodId;
- stuResultRow.periodName = (!string.IsNullOrWhiteSpace(studata.periodId) && periodDic.ContainsKey(studata.periodId)) ? periodDic[studata.periodId] : string.Empty;
- stuResultRow.year = studata.year;
- stuResultRow.no = studata.no;
- Period curPeriod = new Period();
- if (!string.IsNullOrWhiteSpace(studata.periodId))
- {
- curPeriod = school.period.Where(p => p.id.Equals(studata.periodId)).FirstOrDefault();
- }
- ExamSimple gradeInfo = new ExamSimple();
- if (!string.IsNullOrWhiteSpace(curPeriod.id))
- {
- gradeInfo = getGradeInfoByYear(studata.year, curPeriod);
- }
- stuResultRow.gradeIndex = (!string.IsNullOrWhiteSpace(gradeInfo.id)) ? gradeInfo.id : string.Empty;
- stuResultRow.gradeName = (!string.IsNullOrWhiteSpace(gradeInfo.name)) ? gradeInfo.name : string.Empty;
- stuResult.Add(stuResultRow);
- }
- //回傳值
- return Ok(new { openToken = encOpenToken, school = schResult, students = stuResult } );
- }
- }
- catch (Exception ex)
- {
- //await _dingDing.SendBotMsg($"OS,{_option.Location},student/openlogin()\n{ex.Message}\n{ex.StackTrace}\n", GroupNames.醍摩豆服務運維群組);
- return BadRequest();
- }
- }
- /// <summary>
- /// 學生追加教育雲ID
- /// </summary>
- /// <param name = "request" ></ param >
- [AllowAnonymous]
- [HttpPost("add-open-stu")]
- public async Task<IActionResult> addOpenidToStudent(JsonElement request)
- {
- try
- {
- //參數取得
- string location = _option.Location;
- string openToken = (request.TryGetProperty("open_token", out JsonElement _open_token)) ? _open_token.GetString() : string.Empty;
- if(string.IsNullOrWhiteSpace(openToken)) return BadRequest();
- if (!location.Contains("Global")) return BadRequest();
- //string schoolCode = (request.TryGetProperty("school_code", out JsonElement _school_code)) ? _school_code.GetString() : string.Empty;
- //if (string.IsNullOrWhiteSpace(schoolCode)) return BadRequest();
- string stuid = (request.TryGetProperty("stuid", out JsonElement _stuid)) ? _stuid.GetString() : string.Empty;
- if (string.IsNullOrWhiteSpace(stuid)) return BadRequest();
- //OpenID AES解密
- string aeskey = _configuration.GetValue<string>("HaBookAuth:CoreService:clientID");
- string aesiv = _configuration.GetValue<string>("HaBookAuth:CoreService:clientSecret");
- string openTokenString = LoginService.AesDecrypt(openToken, aeskey, aesiv);
- if(string.IsNullOrWhiteSpace(openTokenString)) return BadRequest();
- openToken decOpenToken = openTokenString.ToObject<openToken>();
- if(string.IsNullOrWhiteSpace(decOpenToken.openId) || string.IsNullOrWhiteSpace(decOpenToken.shortCode)) return BadRequest();
- string schoolCode = decOpenToken.shortCode;
- string openId = decOpenToken.openId;
- var client = _azureCosmos.GetCosmosClient();
- var schoolClient = client.GetContainer(Constant.TEAMModelOS, "School");
- var teacherClient = client.GetContainer(Constant.TEAMModelOS, "Teacher");
- var studentClient = client.GetContainer(Constant.TEAMModelOS, "Student");
- //取得學校基本資料
- School school = new School();
- try
- {
- school = await schoolClient.ReadItemAsync<School>($"{schoolCode}", new PartitionKey("Base"));
- }
- catch (CosmosException ex)
- {
- return Ok(new { error = 1, message = "Can not find school data." });
- }
- //取得學生基本資料
- Student student = new Student();
- try
- {
- student = await studentClient.ReadItemAsync<Student>($"{stuid}", new PartitionKey($"Base-{schoolCode}"));
- }
- catch (CosmosException ex)
- {
- return Ok(new { error = 2, message = "Can not find student data." });
- }
- if (student.graduate.Equals(1))
- {
- return Ok(new { error = 3, message = "Graduate already!" });
- }
- //1.向CS詢問是否已綁定
- stuOpenData openData = new stuOpenData();
- var httpClient = _httpClient.CreateClient();
- string AccessToken = await getCoreAccessToken();
- httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", AccessToken);
- string grant_type = "bind";
- string csv2Domain = _configuration.GetValue<string>("HaBookAuth:CoreAPI");
- string csv2Url = $"{csv2Domain}/oauth2/EduCloudTWBingManage";
- Dictionary<string, object> dict = new() {
- { "grant_type", grant_type },
- { "open_id", openId },
- { "id", $"Base-{school.id},{student.id}" }
- };
- HttpContent content = new StringContent(dict.ToJsonString(), Encoding.UTF8, "application/json");
- HttpResponseMessage httpResponse = await httpClient.PostAsync(csv2Url, content);
- if (httpResponse.StatusCode == HttpStatusCode.OK)
- {
- string responseContent = await httpResponse.Content.ReadAsStringAsync();
- if(!string.IsNullOrWhiteSpace(responseContent))
- {
- csApiResponse csResult = responseContent.ToObject<csApiResponse>();
- if(csResult.error.Equals(2))
- {
- return Ok(new { error = 5, message = "This id already exist." });
- }
- }
- }
- else
- {
- return Ok(new { error = 1, message = "Can not get opendata from CS." });
- }
- //2.將OpenID放入學生Base
- student.openId = openId;
- //await studentClient.ReplaceItemAsync(student, student.id);
- //3.學生登入流程(包含學生資料更新)
- (string ip, string region) = await LoginService.LoginIp(HttpContext, _searcher);
- (string auth_token, string blob_uri, string blob_sas, object classinfo, List<object> courses, AuthenticationResult token) = await StudentCheck(school, $"{student.id}", $"{student.classId}", $"{schoolCode}", $"{student.picture}", $"{student.name}", schoolClient, teacherClient, school.areaId, ip, client, student);
- int countAuthorized = await GetStudentAuthNumByScale($"{schoolCode}", school);
- return Ok(new { school.scale, countAuthorized, location = _option.Location, error = 0, auth_token, blob_uri, blob_sas, classinfo, courses, token = new { access_token = token.AccessToken, expires_in = token.ExpiresOn, id_token = auth_token, token_type = token.TokenType } });
- }
- catch (Exception ex)
- {
- //await _dingDing.SendBotMsg($"OS,{_option.Location},student/addOpenidToStudent()\n{ex.Message}\n{ex.StackTrace}\n", GroupNames.醍摩豆服務運維群組);
- return BadRequest();
- }
- }
- /// <summary>
- /// 學生解綁教育雲ID
- /// </summary>
- /// <param name = "request" ></ param >
- [AllowAnonymous]
- [HttpPost("rmv-open-stu")]
- public async Task<IActionResult> rmvOpenidToStudent(JsonElement request)
- {
- if (!request.TryGetProperty("school_code", out JsonElement _school_code)) return BadRequest();
- string schoolCode = _school_code.GetString();
- if(string.IsNullOrWhiteSpace(schoolCode)) return BadRequest();
- if (!request.TryGetProperty("stuid", out JsonElement _stuid)) return BadRequest();
- string stuId = _stuid.GetString();
- if(string.IsNullOrWhiteSpace(stuId)) return BadRequest();
- string openType = (request.TryGetProperty("type", out JsonElement _type)) ? _type.GetString().ToLower() : "educloudtwl"; //educloudtwl: 教育雲
- var client = _azureCosmos.GetCosmosClient();
- var studentClient = client.GetContainer(Constant.TEAMModelOS, "Student");
- //取得學生基本資料
- Student student = new Student();
- try
- {
- student = await studentClient.ReadItemAsync<Student>($"{stuId}", new PartitionKey($"Base-{schoolCode}"));
- }
- catch (CosmosException ex)
- {
- return Ok(new { error = 2, message = "Can not find student data." });
- }
- //CSV解綁
- bool rmvSuccess = false;
- if (openType.Equals("educloudtwl")) //教育雲
- {
- string openId = student.openId;
- if(string.IsNullOrWhiteSpace(openId)) //學生無OpenID,視為解綁成功
- {
- return Ok(new { error = 0, message = string.Empty });
- }
- stuOpenData openData = new stuOpenData();
- var httpClient = _httpClient.CreateClient();
- string AccessToken = await getCoreAccessToken();
- httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", AccessToken);
- string csv2Domain = _configuration.GetValue<string>("HaBookAuth:CoreAPI");
- string csv2Url = $"{csv2Domain}/oauth2/EduCloudTWBingManage";
- Dictionary<string, object> dict = new() {
- { "grant_type", "unbind" },
- { "open_id", openId },
- { "id", $"Base-{schoolCode},{student.id}" }
- };
- HttpContent content = new StringContent(dict.ToJsonString(), Encoding.UTF8, "application/json");
- HttpResponseMessage httpResponse = await httpClient.PostAsync(csv2Url, content);
- if (httpResponse.StatusCode == HttpStatusCode.OK)
- {
- string responseContent = await httpResponse.Content.ReadAsStringAsync();
- if (string.IsNullOrWhiteSpace(responseContent))
- {
- rmvSuccess = true;
- }
- else
- {
- csApiResponse csResult = responseContent.ToObject<csApiResponse>();
- if (!string.IsNullOrWhiteSpace(csResult.message))
- {
- return Ok(new { error = csResult.error, message = csResult.message });
- }
- }
- }
- else
- {
- return Ok(new { error = 1, message = "Can not get opendata from CS." });
- }
- }
- //學生資料變更
- if(rmvSuccess)
- {
- if (openType.Equals("educloudtwl")) //教育雲
- {
- student.openId = null;
- }
- //DB更新
- await studentClient.ReplaceItemAsync(student, student.id);
- return Ok(new { error = 0, message = string.Empty });
- }
- return Ok(new { error = 9, message = "Can not unbind student." });
- }
- //查询学生名单详情
- [ProducesDefaultResponseType]
- //[AuthToken(Roles = "teacher")]
- [HttpPost("get-summary-student")]
- [AuthToken(Roles = "teacher,admin,student")]
- #if !DEBUG
- [Authorize(Roles = "IES")]
- #endif
- public async Task<IActionResult> getSummary(JsonElement request)
- {
- try
- {
- request.TryGetProperty("students", out JsonElement students);
- request.TryGetProperty("tmdIds", out JsonElement tmdIds);
- List<TmdInfo> tmdinfos = new List<TmdInfo>();
- List<object> stus = new List<object>();
- var client = _azureCosmos.GetCosmosClient();
- if (students.ValueKind.Equals(JsonValueKind.Array))
- {
- List<Students> stuList = students.ToObject<List<Students>>();
- if (stuList.IsNotEmpty())
- {
- foreach (Students stu in stuList)
- {
- var query = $"select c.id,c.name,c.picture,c.classId,c.code,c.groupId,c.groupName,c.no from c where c.id = '{stu.id}'";
- await foreach (var item in client.GetContainer(Constant.TEAMModelOS, "Student").GetItemQueryStreamIterator(queryText: query, requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"{stu.code}") }))
- {
- using var json = await JsonDocument.ParseAsync(item.ContentStream);
- if (json.RootElement.TryGetProperty("_count", out JsonElement count) && count.GetUInt16() > 0)
- {
- foreach (var obj in json.RootElement.GetProperty("Documents").EnumerateArray())
- {
- stus.Add(obj.ToObject<object>());
- }
- }
- }
- }
- }
- }
- if (tmdIds.ValueKind.Equals(JsonValueKind.Array))
- {
- List<string> tmdids = tmdIds.ToObject<List<string>>();
- if (tmdids.IsNotEmpty())
- {
- List<string> inids = new List<string>();
- tmdids.ForEach(x => { inids.Add($"'{x}'"); });
- var insql = string.Join(",", inids);
- var queryslt = $"SELECT c.id,c.name,c.picture FROM c where c.id in ({insql})";
- await foreach (var item in client.GetContainer(Constant.TEAMModelOS, "Student").GetItemQueryIterator<TmdInfo>(queryText: queryslt, requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"Base") }))
- {
- tmdinfos.Add(item);
- }
- }
- }
- return Ok(new { stus, tmdinfos });
- }
- catch (Exception ex)
- {
- await _dingDing.SendBotMsg($"OS,{_option.Location},student/get-summary-student()\n{ex.Message}\n{ex.StackTrace}\n", GroupNames.醍摩豆服務運維群組);
- return BadRequest();
- }
- }
- /// <summary>
- /// 學生簡易登入
- /// </summary>
- /// <param name = "request" ></ param >
- [AllowAnonymous]
- [HttpPost("login-simple")]
- public async Task<IActionResult> LoginSimple(JsonElement request)
- {
- try
- {
- var client = _azureCosmos.GetCosmosClient();
- var schoolClient = client.GetContainer(Constant.TEAMModelOS, "School");
- var studentClient = client.GetContainer(Constant.TEAMModelOS, "Student");
- //參數取得
- if (!request.TryGetProperty("school_code", out JsonElement school_code)) return BadRequest();
- if (!request.TryGetProperty("id", out JsonElement id)) return BadRequest();
- if (!request.TryGetProperty("pw", out JsonElement pw)) return BadRequest();
- (string ip, string region) = await LoginService.LoginIp(HttpContext, _searcher);
- var response = await studentClient.ReadItemStreamAsync(id.GetString(), new PartitionKey($"Base-{school_code.GetString().ToLower()}"));
- if (response.Status == 200)
- {
- var rjson = await JsonDocument.ParseAsync(response.ContentStream);
- Student student = rjson.ToObject<Student>();
- rjson.RootElement.TryGetProperty("salt", out JsonElement salt);
- rjson.RootElement.TryGetProperty("pw", out JsonElement dbpw);
- rjson.RootElement.TryGetProperty("name", out JsonElement name);
- rjson.RootElement.TryGetProperty("picture", out JsonElement picture);
- rjson.RootElement.TryGetProperty("classId", out JsonElement classId);
- rjson.RootElement.TryGetProperty("no", out JsonElement no);
- rjson.RootElement.TryGetProperty("groupId", out JsonElement groupId);
- rjson.RootElement.TryGetProperty("groupName", out JsonElement groupName);
- dynamic user = new ExpandoObject();
- user.no = no;
- user.groupId = groupId;
- user.groupName = groupName;
- var HashedPW = Utils.HashedPassword(pw.ToString(), salt.ToString());
- if (HashedPW.Equals(dbpw.GetString()))
- {
- School schoolInfo = await schoolClient.ReadItemAsync<School>($"{school_code}", new PartitionKey("Base"));
- //取得所屬預設班級信息
- object classinfo = null;
- if (!classId.ValueKind.Equals(JsonValueKind.Null) && classId.ValueKind.Equals(JsonValueKind.String))
- {
- var query = $"SELECT c.id, c.no, c.name FROM c WHERE c.id = '{classId.GetString()}'";
- await foreach (var item in schoolClient.GetItemQueryStreamIterator(queryText: query, requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"Class-{school_code}") }))
- {
- using var json = await JsonDocument.ParseAsync(item.ContentStream);
- if (json.RootElement.TryGetProperty("_count", out JsonElement count) && count.GetUInt16() > 0)
- {
- foreach (var obj in json.RootElement.GetProperty("Documents").EnumerateArray())
- {
- classinfo = obj.ToObject<object>();
- }
- }
- }
- }
- //換取AuthToken,提供給前端
- var auth_token = JwtAuthExtension.CreateAuthToken(_option.HostName, id.GetString(), name.GetString(), picture.GetString(), _option.JwtSecretKey, Website: "IES", areaId: schoolInfo.areaId, scope: Constant.ScopeStudent, schoolID: school_code.GetString(), roles: new[] { "student" }, expire: 1);
-
- //用户在线记录
- try
- {
- _ = _httpTrigger.RequestHttpTrigger(new { school = school_code.GetString(), scope = $"{Constant.ScopeStudent}", id = $"{id}", ip = $"{ip}", expire = 1 }, _option.Location, "online-record");
- }
- catch { }
- //保存学生登录信息
- await client.GetContainer("TEAMModelOS", "Student").ReplaceItemAsync<Student>(student, student.id, new PartitionKey($"{student.code}"));
- //其他訊息
- dynamic school = new ExpandoObject();
- //回傳
- return Ok(new { error = 0, auth_token, classinfo, user });
- }
- else
- {
- return Ok(new { error = 1, message = "Invalid account or password" });
- }
- }
- else
- {
- return Ok(new { error = 2, message = "Invalid account" });
- }
- }
- catch (Exception ex)
- {
- await _dingDing.SendBotMsg($"IES5,{_option.Location},StudentController/login-simple()\n{ex.Message}\n{ex.StackTrace}\n", GroupNames.醍摩豆服務運維群組);
- return BadRequest();
- }
- }
- //TODO 此API需處理對應前端返回的相關數據
- [ProducesDefaultResponseType]
- [AuthToken(Roles = "student,teacher")]
- [HttpPost("get-school-info")]
- public async Task<IActionResult> GetSchoolInfo(JsonElement request)
- {
- try
- {
- var (id, _, _, school) = HttpContext.GetAuthTokenInfo();
- var client = _azureCosmos.GetCosmosClient();
- /// tmdid, schoolid
- var userType = "schoolid";
- if (request.TryGetProperty("userType", out JsonElement usertype))
- {
- if (!usertype.ValueKind.Equals(JsonValueKind.Undefined) && !usertype.ValueKind.Equals(JsonValueKind.Null) && usertype.ValueKind.Equals(JsonValueKind.String))
- {
- userType = usertype.GetString();
- }
- }
- if (string.IsNullOrEmpty(school))
- {
- if (userType.Equals("tmdid"))
- {
- Teacher teacher = await client.GetContainer(Constant.TEAMModelOS, "School").ReadItemAsync<Teacher>(id, new PartitionKey("Base"));
- if (teacher.schools.IsNotEmpty())
- {
- var tech = teacher.schools.Find(x => x.status.Equals("join"));
- if (tech == null)
- {
- school = teacher.schools[0].schoolId;
- }
- else
- {
- school = tech.schoolId;
- }
- }
- }
- }
- if (!string.IsNullOrEmpty(school))
- {
- object school_base = null;
- var response = await client.GetContainer(Constant.TEAMModelOS, "School").ReadItemStreamAsync(school, new PartitionKey("Base"));
- if (response.Status == 200)
- {
- using var json = await JsonDocument.ParseAsync(response.ContentStream);
- school_base = json.RootElement.ToObject<object>();
- }
- //取得班级
- List<object> school_classes = new List<object>();
- await foreach (var item in client.GetContainer(Constant.TEAMModelOS, "School").GetItemQueryStreamIterator(queryText: $"SELECT c.id,c.x,c.y,c.name,c.year,c.teacher,c.periodId,c.gradeId,c.room,c.sn,c.no,c.style,c.status,c.openType,c.scope, ARRAY_LENGTH(c.students) AS studCount FROM c", requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"Class-{school}") }))
- {
- var jsonc = await JsonDocument.ParseAsync(item.ContentStream);
- foreach (var classeinfo in jsonc.RootElement.GetProperty("Documents").EnumerateArray())
- {
- school_classes.Add(classeinfo.ToObject<object>());
- }
- }
- //取得教室
- List<Room> school_rooms = new List<Room>();
- await foreach (var item in client.GetContainer(Constant.TEAMModelOS, "School").GetItemQueryIterator<Room>(queryText: $"select value(c) from c ",
- requestOptions: new QueryRequestOptions() { PartitionKey = new Azure.Cosmos.PartitionKey($"Room-{school}") }))
- {
- school_rooms.Add(item);
- }
- return Ok(new { school_base, school_classes, school_rooms, status = 200 });
- }
- else
- {
- return Ok(new { status = 404 }); ;
- }
- }
- catch (CosmosException ex)
- {
- return Ok(new { status = ex.Status }); ;
- }
- catch (Exception ex)
- {
- await _dingDing.SendBotMsg($"IES5,{_option.Location},Student/get-school-info()\n{ex.Message}\n{ex.StackTrace}", GroupNames.醍摩豆服務運維群組);
- return BadRequest();
- }
- }
- /**
- * 根据学年获取年级信息
- * @param year 学年
- * @param Period 学段資料
- */
- private ExamSimple getGradeInfoByYear(int year, Period curPeriod)
- {
- ExamSimple result = new ExamSimple();
- if (year > 0)
- {
- DateTime date = DateTime.UtcNow;
- int curYear = date.Year;
- int month = date.Month;
- Semester semesterStart = curPeriod.semesters.Where((Semester x) => x.start.Equals(1)).FirstOrDefault();
- if (semesterStart != null)
- {
- if (month < semesterStart.month)
- {
- curYear--;
- }
- int gradeIndex = curYear - year;
- result.id = gradeIndex.ToString();
- result.name = (gradeIndex >= curPeriod.grades.Count) ? "graduated" : (gradeIndex >= 0) ? curPeriod.grades[gradeIndex] : "not-enrollment";
- }
- }
- return result;
- }
- //學生登入後根據學校規模取得授權數
- private async Task<int> GetStudentAuthNumByScale(string school_code, School school)
- {
- //授权规模数量
- DateTimeOffset dateTime = DateTimeOffset.UtcNow;
- var dateDay = dateTime.ToString("yyyyMMdd"); //获取当天的日期
- string key = $"Login:School:{school_code}:student-day:{dateDay}";
- SortedSetEntry[] countStudent = _azureRedis.GetRedisClient(8).SortedSetRangeByScoreWithScores(key);
- int countAuthorized = 0;
- if (countStudent != null && countStudent.Length > 0)
- {
- bool notify = false;
- countAuthorized = countStudent.Length;
- if (school.scale > 0 && school.scale - countAuthorized <= 0)
- {
- //登录人数已达授权规模数上限
- if (!string.IsNullOrWhiteSpace(school.areaId))
- {
- AreaSetting areaSetting = await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.Normal).ReadItemAsync<AreaSetting>(school.areaId, new PartitionKey("AreaSetting"));
- if (areaSetting.ignoreScaleExpire > dateTime.ToUnixTimeMilliseconds())
- {
- //将人数控制在最大规模数以下。
- countAuthorized = school.scale - 1;
- }
- else
- {
- notify = true;
- }
- }
- else
- {
- notify = true;
- }
- if (notify)
- {
- //通知key 一天只通知一次
- string scaleNotifykey = $"Login:School:{school.id}:student-scale-notify:{dateDay}";
- bool Exists = await _azureRedis.GetRedisClient(8).KeyExistsAsync(scaleNotifykey);
- if (!Exists)
- {
- //获取学校管理员
- List<IdNameCode> ids = new List<IdNameCode>();
- string sql = $"select value c from c where c.code='Teacher-{school.id}' and c.status='join' and array_contains(c.roles,'admin') ";
- List<SchoolTeacher> adminTeachers = new List<SchoolTeacher>();
- await foreach (var item in _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.School)
- .GetItemQueryIterator<SchoolTeacher>(queryText: sql, requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey($"Teacher-{school.id}") }))
- {
- adminTeachers.Add(item);
- }
- if (adminTeachers.IsNotEmpty())
- {
- string sqlAdmin = $"select c.id,c.lang as code ,c.name from c where c.id in ({string.Join(",", adminTeachers.Select(z => $"'{z.id}'"))}) ";
- await foreach (var item in _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.Teacher)
- .GetItemQueryIterator<IdNameCode>(queryText: sqlAdmin, requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey($"Base") }))
- {
- ids.Add(item);
- }
- foreach (var uds in ids)
- {
- _coreAPIHttpService.PushNotify(new List<IdNameCode> { uds }, $"school-scale-notify", Constant.NotifyType_IES5_Management, new Dictionary<string, object> { { "tmdname", uds.name }, { "countAuthorized", $"{countAuthorized}" }, { "scale", $"{school.scale}" }, { "schoolName", school.name }, { "schoolId", $"{school.id}" }, }, _option.Location, _configuration, _dingDing, _environment.ContentRootPath);
- }
- await _azureRedis.GetRedisClient(8).StringSetAsync(scaleNotifykey, scaleNotifykey, new TimeSpan(hours: 24, minutes: 0, seconds: 0));
- }
- }
- }
- }
- }
- return countAuthorized;
- }
- private async Task<string> getCoreAccessToken()
- {
- string AccessToken = "";
- try
- {
- string Url = _configuration.GetValue<string>("HaBookAuth:CoreAPI") + "/oauth2/token";
- string GrantType = "device";
- string ClientID = _configuration.GetValue<string>("HaBookAuth:CoreService:clientID");
- string Secret = _configuration.GetValue<string>("HaBookAuth:CoreService:clientSecret");
- var content = new { grant_type = GrantType, client_id = ClientID, client_secret = Secret };
- var response = await _httpClient.CreateClient().PostAsJsonAsync($"{Url}", content);
- if (response.IsSuccessStatusCode)
- {
- string responseBody = response.Content.ReadAsStringAsync().Result;
- using (JsonDocument document = JsonDocument.Parse(responseBody.ToString()))
- {
- if (document.RootElement.TryGetProperty("access_token", out JsonElement AccessTokenObj))
- {
- AccessToken = AccessTokenObj.ToString();
- }
- }
- }
- return AccessToken;
- }
- catch (Exception ex)
- {
- return AccessToken;
- }
- }
- public static string RandomString(int length)
- {
- Random random = new Random();
- const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
- return new string(Enumerable.Repeat(chars, length)
- .Select(s => s[random.Next(s.Length)]).ToArray());
- }
- //取得學生OpenData
- private class stuOpenData
- {
- public string open_id { get; set; }
- public string open_name { get; set; }
- public string open_mail { get; set; }
- public string schoolCode { get; set; }
- }
- //CS API 返回架構
- private class csApiResponse
- {
- public int error { get; set; }
- public string message { get; set; }
- }
- //CSV2 學校資料庫 API 返回架構
- private class csSchApiResponse
- {
- public string code { get; set; }
- public string name { get; set; }
- public string type { get; set; }
- }
- //把openId打包成openToken架構
- private class openToken
- {
- public string openId { get; set; }
- public string shortCode { get; set; }
- }
- //無法取得OpenID搜尋學生姓名回傳的學生資料
- private class stuOpenDataOrientation
- {
- public string id { get; set; }
- public string name { get; set; }
- public string classId { get; set; }
- public string className { get; set; }
- public string periodId { get; set; }
- public string periodName { get; set; }
- public int year { get; set; }
- public string gradeIndex { get; set; }
- public string gradeName { get; set; }
- public string no { get; set; }
- }
- }
- }
|