Bläddra i källkod

Merge branch 'develop' into PL/develop-BI

Li 3 år sedan
förälder
incheckning
cbbb0a5206
34 ändrade filer med 1021 tillägg och 87 borttagningar
  1. 2 3
      TEAMModelOS.FunctionV4/ServiceBus/ActiveTaskTopic.cs
  2. 59 1
      TEAMModelOS.SDK/DI/AzureStorage/AzureStorageTableExtensions.cs
  3. 0 1
      TEAMModelOS.SDK/Models/Cosmos/School/Course.cs
  4. 1 0
      TEAMModelOS/ClientApp/public/lang/en-US.js
  5. 1 0
      TEAMModelOS/ClientApp/public/lang/zh-CN.js
  6. 1 0
      TEAMModelOS/ClientApp/public/lang/zh-TW.js
  7. 1 1
      TEAMModelOS/ClientApp/src/router/routes.js
  8. 3 3
      TEAMModelOS/ClientApp/src/view/ability/Review.vue
  9. 3 0
      TEAMModelOS/ClientApp/src/view/knowledge-point/index/operation/AddBlock.vue
  10. 2 12
      TEAMModelOS/ClientApp/src/view/knowledge-point/index/operation/ComposeBlock.vue
  11. 1 0
      TEAMModelOS/ClientApp/src/view/mgtPlatform/MgtPlatform.vue
  12. 4 2
      TEAMModelOS/ClientApp/src/view/mycourse/MyCourse.vue
  13. 3 0
      TEAMModelOS/ClientApp/src/view/mycourse/exam/Exam.vue
  14. 3 0
      TEAMModelOS/ClientApp/src/view/mycourse/homework/Homework.vue
  15. 5 0
      TEAMModelOS/ClientApp/src/view/mycourse/record/Record.less
  16. 155 0
      TEAMModelOS/ClientApp/src/view/mycourse/record/Record.vue
  17. 3 0
      TEAMModelOS/ClientApp/src/view/mycourse/survey/Survey.vue
  18. 4 1
      TEAMModelOS/ClientApp/src/view/mycourse/vote/Vote.vue
  19. 12 4
      TEAMModelOS/ClientApp/src/view/newcourse/CoursePlan.vue
  20. 0 1
      TEAMModelOS/ClientApp/src/view/newcourse/MyCourse.vue
  21. 2 7
      TEAMModelOS/ClientApp/src/view/syllabus/Syllabus.vue
  22. 3 0
      TEAMModelOS/ClientApp/src/view/teachermgmt/components/mgt/TeacherMgt.vue
  23. 0 10
      TEAMModelOS/Controllers/Teacher/InitController.cs
  24. 33 0
      TEAMModelOS/Controllers/Third/Xkw/Hmac/HmacConst.cs
  25. 19 0
      TEAMModelOS/Controllers/Third/Xkw/Hmac/HmacResult.cs
  26. 288 0
      TEAMModelOS/Controllers/Third/Xkw/Hmac/HmacUtils.cs
  27. 16 9
      TEAMModelOS/Controllers/Third/Xkw/OpenAuthClient.cs
  28. 120 0
      TEAMModelOS/Controllers/Third/Xkw/Sdk/XkwAPIHttpService.cs
  29. 25 0
      TEAMModelOS/Controllers/Third/Xkw/Sdk/XopException.cs
  30. 151 0
      TEAMModelOS/Controllers/Third/Xkw/Sdk/XopHttpClient.cs
  31. 10 32
      TEAMModelOS/Controllers/Third/Xkw/XkwOAuth2Controller.cs
  32. 88 0
      TEAMModelOS/Controllers/Third/Xkw/XkwServiceController.cs
  33. 2 0
      TEAMModelOS/Startup.cs
  34. 1 0
      TEAMModelOS/TEAMModelOS.csproj

+ 2 - 3
TEAMModelOS.FunctionV4/ServiceBus/ActiveTaskTopic.cs

@@ -545,10 +545,9 @@ namespace TEAMModelOS.FunctionV4.ServiceBus
 
         }
         [Function("ItemCond")]
-        public async Task ItemCondFunc([ServiceBusTrigger("%Azure:ServiceBus:ItemCondQueue%", Connection = "Azure:ServiceBus:ConnectionString")] string message)
+        public async Task ItemCondFunc([ServiceBusTrigger("%Azure:ServiceBus:ItemCondQueue%", Connection = "Azure:ServiceBus:ConnectionString")] string msg)
         {
-            string msg = "";
-            await _dingDing.SendBotMsg($"{message}",GroupNames.成都开发測試群組);
+            
             try
             {
                 var client = _azureCosmos.GetCosmosClient();

+ 59 - 1
TEAMModelOS.SDK/DI/AzureStorage/AzureStorageTableExtensions.cs

@@ -402,7 +402,51 @@ namespace TEAMModelOS.SDK.DI
             }
             return entitys;
         }
+        public static   List<T>  FindListByDictSync<T>(this CloudTable table, Dictionary<string, object> dict) where T : TableEntity, new()
+        {
+            var exQuery = new TableQuery<T>();
+            StringBuilder builder = new();
+            if (null != dict && dict.Count > 0)
+            {
+                var keys = dict.Keys;
+                int index = 1;
+                foreach (string key in keys)
+                {
+                    if (dict[key] != null && !string.IsNullOrEmpty(dict[key].ToString()))
+                    {
+                        string typeStr = SwitchType<T>(dict[key], key);
+                        if (string.IsNullOrEmpty(typeStr))
+                        {
+                            continue;
+                        }
+                        if (index == 1)
+                        {
+                            //builder.Append(TableQuery.GenerateFilterCondition(key, QueryComparisons.Equal, dict[key].ToString()));
+                            builder.Append(typeStr);
+
+                        }
+                        else
+                        {
+                            //builder.Append("  " + TableOperators.And + "  " + TableQuery.GenerateFilterCondition(key, QueryComparisons.Equal, dict[key].ToString()));
+                            builder.Append("  " + TableOperators.And + "  " + typeStr);
+
+                        }
+                        index++;
+                    }
+                    else
+                    {
+                        throw new Exception("The parameter must have value!");
+                    }
+                }
 
+                exQuery.Where(builder.ToString());
+                return   QueryListSync<T>(exQuery, table);
+            }
+            else
+            {
+                return null;
+            }
+        }
         public static async Task<List<T>> FindListByDict<T>(this CloudTable table, Dictionary<string, object> dict) where T : TableEntity, new()
         {            
             var exQuery = new TableQuery<T>();
@@ -449,7 +493,21 @@ namespace TEAMModelOS.SDK.DI
             }
         }
 
-
+        private static List<T> QueryListSync<T>(TableQuery<T> exQuery, CloudTable TableName) where T : TableEntity, new()
+        {
+            TableContinuationToken continuationToken = null;
+            List<T> entitys = new();
+            do
+            {
+                var result =   TableName.ExecuteQuerySegmented(exQuery, continuationToken);
+                if (result.Results.Count > 0)
+                {
+                    entitys.AddRange(result.ToList());
+                }
+                continuationToken = result.ContinuationToken;
+            } while (continuationToken != null);
+            return entitys;
+        }
 
         private static async Task<List<T>> QueryList<T>(TableQuery<T> exQuery, CloudTable TableName  ) where T : TableEntity, new()
         {

+ 0 - 1
TEAMModelOS.SDK/Models/Cosmos/School/Course.cs

@@ -53,7 +53,6 @@ namespace TEAMModelOS.SDK.Models
         /// <summary>
         /// 创建者的id 
         /// </summary>
-        [Required(ErrorMessage = "creatorId 必须设置")]
         public string creatorId { get; set; }
         /// <summary>
         /// 学校编码或教师tmdid

+ 1 - 0
TEAMModelOS/ClientApp/public/lang/en-US.js

@@ -1027,6 +1027,7 @@ const LANG_EN_US = {
         cusTable: 'Class Schedule',
         importLabel: 'Import Class Schedule',
         cusMode: 'Course Mode',
+        rtnRoom:'返回教室管理',
         roomLabel: 'Classroom',
         roomType: 'Type:',
 

+ 1 - 0
TEAMModelOS/ClientApp/public/lang/zh-CN.js

@@ -1027,6 +1027,7 @@ const LANG_ZH_CN = {
         cusTable: '课程表',
         importLabel: '导入课表',
         cusMode: '课程模式',
+        rtnRoom:'返回教室管理',
         roomLabel: '教室',
         roomType: '教室类型:',
 

+ 1 - 0
TEAMModelOS/ClientApp/public/lang/zh-TW.js

@@ -1027,6 +1027,7 @@ const LANG_ZH_TW = {
         cusTable: '課程表',
         importLabel: '匯入課表',
         cusMode: '課程模式',
+        rtnRoom:'返回教室管理',
         roomLabel: '教室',
         roomType: '教室類型:',
 

+ 1 - 1
TEAMModelOS/ClientApp/src/router/routes.js

@@ -595,7 +595,7 @@ export const routes = [{
 		name: 'CoursePlan',
 		component: resolve => require(['@/view/newcourse/CoursePlan.vue'], resolve),
 		meta: {
-			activeName: 'NewCusMgt'
+			activeName: ''
 		}
 	},
 	// 新课纲管理

+ 3 - 3
TEAMModelOS/ClientApp/src/view/ability/Review.vue

@@ -307,12 +307,12 @@ export default {
       let suffix = this.getSuffix(file.name)
       console.log(file)
       console.log(fullFilePath)
-      let isImg = ['jpg', 'png'].includes(suffix)
+      let isImg = ['jpg', 'png', 'gif'].includes(suffix)
       let isDoc = ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'].includes(suffix)
       let isPdf = suffix === 'pdf'
       let isVideo = suffix === 'mp4' || suffix === 'webm'
       let isAudio = ['mp3', 'wav'].includes(suffix)
-      let type = isImg ? 'image' : isVideo ? 'video' : isAudio ? 'audio' : 'doc'
+      let type = isImg ? 'image' : isVideo ? 'video' : isAudio ? 'audio' : (isDoc ? 'doc' : '')
       switch (type) {
         case 'doc':
           if (isPdf) {
@@ -493,7 +493,7 @@ export default {
     /* 根据类型换取ICON图标 */
     getTypeIcon(file) {
       let suffix = file.name.split('.')[file.name.split('.').length - 1].toLowerCase()
-      if (['jpg', 'png'].includes(suffix)) {
+      if (['jpg', 'png', 'gif','webp'].includes(suffix)) {
         return require('../../assets/icon/pic50.png')
       }
       if (['doc', 'docx'].includes(suffix)) {

+ 3 - 0
TEAMModelOS/ClientApp/src/view/knowledge-point/index/operation/AddBlock.vue

@@ -55,8 +55,11 @@
             // 提交添加
             handleSubmit() {
                 let newName = this.formTop.name
+                let existBlockList = this.$parent.$parent.blockList.map(i => i.name)
                 if (!newName) {
                     this.$Message.warning(this.$t('knowledge.blockWarning'))
+                }else if(existBlockList.includes(newName)){
+                    this.$Message.warning(this.$t('knowledge.blockWarning2'))
                 } else {
                     this.isLoading = true
                     let params = {

+ 2 - 12
TEAMModelOS/ClientApp/src/view/knowledge-point/index/operation/ComposeBlock.vue

@@ -84,7 +84,7 @@
 
             // 提交新增知识块
             handleSubmit() {
-                if (this.existBlockList.length) {
+                if (this.existBlockList.length && this.selectBlock) {
                     this.isLoading = true
 					let selectBlockItem = this.existBlockList.filter(item => item.name === this.selectBlock)[0]
                     let pointList = selectBlockItem.points
@@ -129,7 +129,7 @@
 						console.log(newValue);
                         this.newBlockName = ''
                         this.existBlockList = newValue
-                        this.selectBlock = newValue.length ? newValue[0].id : ''
+                        this.selectBlock = newValue.length ? newValue[0].name : ''
                     }
                 }
             }
@@ -162,21 +162,11 @@
         padding-bottom:20px;
     }
 
-    .compose-container .ivu-tabs-nav .ivu-tabs-tab-active,
-    .compose-container .ivu-tabs-nav .ivu-tabs-tab:hover {
-        /* color: #0b9775 !important; */
-    }
-
     .compose-container .ivu-tabs-bar {
         border-bottom:none;
     }
 
-    .compose-container .ivu-tabs-nav-container:focus .ivu-tabs-tab-focused {
-        /* border-color:rgb(11, 151, 117) !important; */
-    }
-
     .compose-container .ivu-tabs-ink-bar {
-        /* background:rgb(11, 151, 117) !important; */
         height:4px;
         bottom:0;
     }

+ 1 - 0
TEAMModelOS/ClientApp/src/view/mgtPlatform/MgtPlatform.vue

@@ -380,6 +380,7 @@ export default {
 .platform-list-wrap {
     display: flex;
     width: 100%;
+    flex-wrap: wrap;
 }
 .add-platform-icon {
     font-size: 50px;

+ 4 - 2
TEAMModelOS/ClientApp/src/view/mycourse/MyCourse.vue

@@ -327,6 +327,7 @@ export default {
     created() {
         this.initCusType()
         this.getAllStuList()
+        if (!this.filterPeriod) this.getCourseList()
     },
     activated() {
         let tn = this.tabName
@@ -634,7 +635,6 @@ export default {
                             code: this.$store.state.userInfo.TEAMModelId,
                             schedule: this.addCusInfo.schedule || [],
                             scope: 'private',
-                            creatorId: this.$store.state.userInfo.schoolCode
                         }
                     }
                     this.btnLoading = true
@@ -655,8 +655,8 @@ export default {
                                         ci.no = this.addCusInfo.no
                                         ci.desc = this.addCusInfo.desc
                                     }
-                                    this.getCusInfo()
                                 }
+                                this.getCusInfo()
                                 this.initCusInfo()
                                 this.addCusStatus = false
                                 this.$refs['addCusInfo'].resetFields()
@@ -704,6 +704,8 @@ export default {
                     value: 'school'
                 })
                 this.listType = 'school'
+            } else {
+                this.listType = 'private'
             }
         },
         // 获取课程列表

+ 3 - 0
TEAMModelOS/ClientApp/src/view/mycourse/exam/Exam.vue

@@ -305,6 +305,9 @@ export default {
                             item.scoreText = statusInfo.scoreText
                             item.scoreColor = statusInfo.scoreColor
                         })
+                        res.datas.sort((a, b) => {
+                            return a.startTime - b.startTime > 0 ? -1 : 1
+                        })
                         this.examList = res.datas
                     }
                 },

+ 3 - 0
TEAMModelOS/ClientApp/src/view/mycourse/homework/Homework.vue

@@ -118,6 +118,9 @@ export default {
                             item.progText = statusInfo.progText
                             item.progColor = statusInfo.progColor
                         })
+                        res.datas.sort((a, b) => {
+                            return a.startTime - b.startTime > 0 ? -1 : 1
+                        })
                         this.hwList = res.datas
                     }
                 },

+ 5 - 0
TEAMModelOS/ClientApp/src/view/mycourse/record/Record.less

@@ -66,4 +66,9 @@
     float:right;
     margin-right:20px;
     position: relative;    
+}
+.exam-action-wrap{
+    position: absolute;
+    right: 15px;
+    top: 10px;
 }

+ 155 - 0
TEAMModelOS/ClientApp/src/view/mycourse/record/Record.vue

@@ -1,5 +1,14 @@
 <template>
     <div class="record-container">
+        <div class="exam-action-wrap">
+            <span>
+                <Tooltip :max-width="180" :content="$t('cusMgt.autoShareTips')">
+                    <Icon type="ios-information-circle-outline" />
+                </Tooltip>
+                {{$t('cusMgt.autoShare')}}
+            </span>
+            <i-switch :loading="sLoading" v-model="isAuto" size="small" @on-change="setAutoPublish" />
+        </div>
         <vuescroll>
             <Alert v-show="rcdParams.scope == 'private'" show-icon type="warning" closable>
                 {{$t('cusMgt.recordTips')}}
@@ -96,6 +105,7 @@
     </div>
 </template>
 <script>
+import { mapGetters } from 'vuex'
 import RcdPoster from "../../homepage/RcdPoster.vue"
 export default {
     components: {
@@ -117,13 +127,143 @@ export default {
     },
     data() {
         return {
+            isAuto: false,//是否自动发布课堂记录
+            sLoading: false,
             btnLoading: false,
             editName: '',
             recordList: [],
             editRdStatus: false,
         }
     },
+    computed: {
+        ...mapGetters({
+            lessonShow: 'user/getTeacherLessonShow',//是否自动发布课堂记录
+        }),
+    },
     methods: {
+        //切换是否自动发布课堂记录
+        setAutoPublish() {
+            this.sLoading = true
+            let show = this.lessonShow
+            if (this.isAuto) {
+                show.push('student')
+            } else {
+                let index = show.findIndex(item => item === 'student')
+                if (index > -1) show.splice(index, 1)
+            }
+            let params = {
+                "opt": "UpdateLessonShow",
+                "lessonShow": show
+            }
+            this.$api.schoolUser.setTeacherInfo(params).then(
+                res => {
+                    this.$Message.success(this.$t('teachermgmt.setOk'))
+                    this.$store.commit('user/setLessonShow', show)
+                },
+                err => {
+                    this.$Message.error(this.$t('teachermgmt.setErr'))
+                    this.isAuto = !this.isAuto
+                }
+            ).finally(() => {
+                this.sLoading = false
+            })
+        },
+        //分享、取消分享给学生
+        toggleShare(data) {
+            if (data && data.id) {
+                let show = []
+                if (!data.isShare) show = ['student']
+                this.$api.lessonRecord.updateLesson({
+                    "lesson_id": data.id,
+                    "tmdid": data.tmdid,
+                    "school": data.school,
+                    "scope": data.scope,
+                    "grant_types": [{
+                        "grant_type": "up-baseinfo",
+                        "data": {
+                            "show": show
+                        }
+                    }]
+                }).then(
+                    res => {
+                        if (!res.error) {
+                            if (data.isShare) {
+                                this.$Message.success(this.$t('cusMgt.unShareOk'))
+                            } else {
+                                this.$Message.success(this.$t('cusMgt.shareOk'))
+                            }
+                            let info = this.recordList.find(item => item.id == data.id)
+                            if (info) {
+                                info.show = show
+                                info.isShare = !info.isShare
+                            }
+                        } else {
+                            if (data.isShare) {
+                                this.$Message.error(this.$t('cusMgt.unShareErr'))
+                            } else {
+                                this.$Message.error(this.$t('cusMgt.shareErr'))
+                            }
+                        }
+                    },
+                    err => {
+                        if (data.isShare) {
+                            this.$Message.error(this.$t('cusMgt.unShareErr'))
+                        } else {
+                            this.$Message.error(this.$t('cusMgt.shareErr'))
+                        }
+                    }
+                )
+            }
+        },
+        //收藏 取消
+        toggleFavorite(data) {
+            if (data && data.id) {
+                if (this.isFavorite(data.id)) {
+                    this.delCollection(data) //取消收藏
+                } else {
+                    this.collection(data) //收藏
+                }
+            }
+        },
+        collection(data) {
+            let params = {
+                favorite: {
+                    "name": data.name,
+                    "id": data.id,
+                    "code": this.$store.state.userInfo.TEAMModelId,
+                    "fromId": data.id,
+                    "fromCode": data.code,
+                    "scope": data.scope,
+                    "owner": data.scope == 'school' ? this.$store.state.userInfo.schoolCode : this.$store.state.userInfo.TEAMModelId,
+                    "type": data.pk
+                }
+            }
+            this.$api.courseMgmt.FavoriteUpsert(params).then(
+                res => {
+                    this.$Message.success(this.$t('cusMgt.fvtOk'))
+                    this.fIds.push(data.id)
+                },
+                err => {
+                    this.$Message.error(this.$t('cusMgt.fvtOk'))
+                }
+            )
+        },
+        delCollection(data) {
+            let params = {
+                "id": data.id,
+                "code": this.$store.state.userInfo.TEAMModelId,
+            }
+            this.$api.courseMgmt.FavoriteDelete(params).then(
+                res => {
+                    this.$Message.success(this.$t('cusMgt.unfvtOk'))
+                    let index = this.fIds.findIndex(item => item == data.id)
+                    if (index > -1) this.fIds.splice(index, 1)
+                },
+                err => {
+                    this.$Message.error(this.$t('cusMgt.unfvtErr'))
+                }
+            )
+        },
         //根据时长换算时间
         handleDuration(duration) {
             if (!duration) return this.$t('cusMgt.noData')
@@ -226,6 +366,9 @@ export default {
                             item.video = `${sasInfo.url}/${sasInfo.name}/records/${item.id}/Record/CourseRecord.mp4${sasInfo.sas}`
                             item.poster = `${sasInfo.url}/${sasInfo.name}/records/${item.id}/Record/CoverImage.jpg${sasInfo.sas}`
                         })
+                        this.recordList.sort((a, b) => {
+                            return a.startTime - b.startTime > 0 ? -1 : 1
+                        })
                     }
                 },
                 err => {
@@ -249,6 +392,18 @@ export default {
                 }
 
             }
+        },
+        lessonShow: {
+            deep: true,
+            immediate: true,
+            handler(n, o) {
+                console.log(typeof n)
+                if (n && Array.isArray(n)) {
+                    this.isAuto = n.includes('student')
+                } else {
+                    this.isAuto = false
+                }
+            }
         }
     }
 }

+ 3 - 0
TEAMModelOS/ClientApp/src/view/mycourse/survey/Survey.vue

@@ -119,6 +119,9 @@ export default {
                             item.progText = statusInfo.progText
                             item.progColor = statusInfo.progColor
                         })
+                        res.datas.sort((a, b) => {
+                            return a.startTime - b.startTime > 0 ? -1 : 1
+                        })
                         this.surveyList = res.datas
                     }
                 },

+ 4 - 1
TEAMModelOS/ClientApp/src/view/mycourse/vote/Vote.vue

@@ -79,7 +79,7 @@ export default {
         },
         toAcDetail(index) {
             this.$router.push({
-                name: 'manageHomeWork',
+                name: 'personalVote',
                 params: {
                     ac: this.voteList[index]
                 }
@@ -118,6 +118,9 @@ export default {
                             item.progText = statusInfo.progText
                             item.progColor = statusInfo.progColor
                         })
+                        res.datas.sort((a, b) => {
+                            return a.startTime - b.startTime > 0 ? -1 : 1
+                        })
                         this.voteList = res.datas
                     }
                 },

+ 12 - 4
TEAMModelOS/ClientApp/src/view/newcourse/CoursePlan.vue

@@ -4,8 +4,9 @@
             <b class="title">{{$t('cusMgt.schdTable')}}</b>
             <div class="action-btn-wrap">
                 <span v-if="$access.can('admin.*|course-upd')" @click="toggleView()" class="action-btn" style="margin-right:40px">
-                    <Icon custom="iconfont icon-kecheng" size="16" />
-                    <span>{{$t('cusMgt.cusMode')}}</span>
+                    <Icon v-if="fromRouter == 'classroom'" type="md-arrow-back" />
+                    <Icon v-else custom="iconfont icon-kecheng" size="16" />
+                    <span>{{ fromRouter == 'classroom' ? $t('cusMgt.rtnRoom') : $t('cusMgt.cusMode')}}</span>
                 </span>
             </div>
         </div>
@@ -82,6 +83,7 @@ export default {
     inject: ['reload'],
     data() {
         return {
+            fromRouter: '',
             split1: 0.2,
             filterPeriod: '',
             confImpStatus: false,
@@ -149,7 +151,7 @@ export default {
         // 切换课表模式和课程模式
         toggleView() {
             this.$router.push({
-                name: 'NewCusMgt'
+                name: this.fromRouter || 'NewCusMgt'
             })
         },
         //关键字搜索班级
@@ -377,7 +379,7 @@ export default {
                             schdItem.time.splice(j, 1)
                             //如果已经删完了时间安排,则把对应关系也删了
                             if (!schdItem.time.length) {
-                                cus.schedule.splice(i,1)
+                                cus.schedule.splice(i, 1)
                             }
                             this.updCusInfo(cus, 'cancel')
                             break
@@ -472,6 +474,12 @@ export default {
             this.findClassCus()
         })
     },
+    beforeRouteEnter(to, from, next) {
+        next(vm => {
+            vm.fromRouter = from.name
+            console.log(arguments)
+        })
+    },
     computed: {
         //当前班级对应的课程数据
         roomCus() {

+ 0 - 1
TEAMModelOS/ClientApp/src/view/newcourse/MyCourse.vue

@@ -2113,7 +2113,6 @@ export default {
                             code: this.$store.state.userInfo.TEAMModelId,
                             schedule: this.addCusInfo.schedule || [],
                             scope: 'private',
-                            creatorId: this.$store.state.userInfo.schoolCode
                         }
                     }
                     this.btnLoading = true

+ 2 - 7
TEAMModelOS/ClientApp/src/view/syllabus/Syllabus.vue

@@ -271,7 +271,7 @@
 				<p class="node-title">{{ $t('syllabus.linkName') }}</p>
 				<Input v-special-char v-model="curLink.name" style="width: 100%" />
 				<p class="node-title">{{ $t('syllabus.linkUrl') }}</p>
-				<Input v-special-char v-model="curLink.url" placeholder="" style="width: 100%" />
+				<Input v-model="curLink.url" placeholder="" style="width: 100%" />
 			</div>
 			<Button @click="onAddLink" style="width: 88%;margin-left: 6%;margin-bottom: 20px;"
 				class="modal-btn">{{ $t('syllabus.confirm') }}</Button>
@@ -2297,7 +2297,7 @@
 					if ((this.isEditVolume && this.curVolume)) {
 						addVolumeParams.id = this.curVolume.id
 						addVolumeParams.order = this.curVolume.order
-						addVolumeParams.name = this.addVolumeForm.name
+						addVolumeParams.name = this.addVolumeForm.name || this.getDefaultVolumeName
 						addVolumeParams.syllabusIds = this.allChapterIds || []
 						addVolumeParams.creatorId = this.curVolume.creatorId
 						addVolumeParams.creatorName = this.curVolume.creatorName
@@ -2727,11 +2727,6 @@
 
 	.choose-content-modal {
 
-		.ivu-modal-footer,
-		.ivu-modal-body {
-			// background-color: #444444;
-		}
-
 		.ev-list-container,
 		.pl-container {
 			height: 600px !important;

+ 3 - 0
TEAMModelOS/ClientApp/src/view/teachermgmt/components/mgt/TeacherMgt.vue

@@ -1228,6 +1228,9 @@ export default {
     created() {
         this.getAuthList()
         this.getSpaceInfo()
+        let routerData = this.$route.query
+        console.log(routerData)
+        if(routerData && routerData.openSpace) this.openPanel('space')
     },
     watch: {
         teachers: {

+ 0 - 10
TEAMModelOS/Controllers/Teacher/InitController.cs

@@ -353,8 +353,6 @@ namespace TEAMModelOS.Controllers
                 string school_code = $"{_school_code}";
                 var jwt = new JwtSecurityToken(id_token.GetString());
                 var id = jwt.Payload.Sub;
-
-                (string ip, string region) = await LoginService.LoginIp(HttpContext, _searcher);
                 var client = _azureCosmos.GetCosmosClient();
 
                 //權限token
@@ -537,14 +535,6 @@ namespace TEAMModelOS.Controllers
                 //TODO JJ,更新Token时,在取得学校资讯时,没有传入schoolId
                 var auth_token = JwtAuthExtension.CreateAuthToken(_option.HostName, id, name?.ToString(), picture?.ToString(), _option.JwtSecretKey, Website: "IES", scope: Constant.ScopeTeacher, schoolID: school_code.ToString(), areaId: currAreaId, standard: school_base.standard, roles: roles.ToArray(), permissions: permissions.ToArray(), expire: 1);
 
-                //用户在线记录
-                try
-                {
-                    _ = _httpTrigger.RequestHttpTrigger(new { school = school_code.ToString(), scope = $"{Constant.ScopeTeacher}", id = $"{id}", ip = $"{ip}", expire = 1 }, _option.Location, "online-record");
-                }
-                catch { }
-
-
                 //取得班级
                 List<object> school_classes = new List<object>();
                 await foreach (var item in client.GetContainer(Constant.TEAMModelOS, "School").GetItemQueryStreamIterator

+ 33 - 0
TEAMModelOS/Controllers/Third/Xkw/Hmac/HmacConst.cs

@@ -0,0 +1,33 @@
+using System;
+
+namespace TEAMModelOS.Controllers.Third.Xkw
+{
+    public class HmacConst
+    {
+
+        /// <summary>
+        ///  分隔符
+        /// </summary>
+        public static char SEPARATOR = '/';
+
+        /// <summary>
+        /// 允许的最大时间间隔
+        /// </summary>
+        public static long MAX_TIMESTAMP_GAP = 5 * 60L;
+
+        public static String KEY_TIMESTAMP = "Xop-Timestamp";
+
+        public static String KEY_SIGN = "Xop-Sign";
+
+        public static String KEY_APP_ID = "Xop-App-Id";
+        /// <summary>
+        /// 每次请求的防止重放攻击的随机串
+        /// </summary>
+        public static String KEY_NONCE = "Xop-Nonce";
+   
+
+        public static String KEY_URL = "xop_url";
+
+        public static String REQUEST_BODY = "xop_body";
+    }
+}

+ 19 - 0
TEAMModelOS/Controllers/Third/Xkw/Hmac/HmacResult.cs

@@ -0,0 +1,19 @@
+using System;
+
+namespace TEAMModelOS.Controllers.Third.Xkw
+{
+    public class HmacResult
+    {
+        public long TimeStamp { get; set; }
+        public String Sign { get; set; }
+        public String Nonce { get; set; }
+
+        public HmacResult(long timeStamp, String sign, String nonce)
+        {
+            this.TimeStamp = timeStamp;
+            this.Sign = sign;
+            this.Nonce = nonce;
+        }
+
+    }
+}

+ 288 - 0
TEAMModelOS/Controllers/Third/Xkw/Hmac/HmacUtils.cs

@@ -0,0 +1,288 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace TEAMModelOS.Controllers.Third.Xkw
+{
+    public class HmacUtils
+    {
+        /// <summary>
+        /// Java的System.currentTimeMillis()转成C#的
+        /// </summary>
+        /// <returns></returns>
+        public static long GetCurrentTimeMillis()
+        {
+            return (DateTime.UtcNow.Ticks - 621355968000000000L) / 10000 / 1000;
+        }
+
+        public static HmacResult Sign(String appId, String secret, Dictionary<String, String> parameters, String requestBodyStr)
+        {
+            Dictionary<String, String> dic = CopyDictionary(parameters);
+            //
+            long timeStamp = GetCurrentTimeMillis();
+            dic.Add(HmacConst.KEY_TIMESTAMP, timeStamp.ToString());
+            dic.Add(HmacConst.KEY_APP_ID, appId);
+            //去掉传递过来的sign     
+            dic.Remove(HmacConst.KEY_SIGN);
+            //去掉传递过来的nonce,一律在此统一放入
+            dic.Remove(HmacConst.KEY_NONCE);
+            String nonce = Guid.NewGuid().ToString("").Replace("-", "");
+            dic.Add(HmacConst.KEY_NONCE, nonce);
+
+            String sha1Str = GetSignatureString(dic, secret, requestBodyStr);
+            dic.Add(HmacConst.KEY_SIGN, sha1Str);
+            return new HmacResult(timeStamp, sha1Str, nonce);
+        }
+
+
+        public static bool ValidateRequestTimestamp(String timestampStr, long maxTimestampGap)
+        {
+            // validate the timestamp
+            long timeStamp = long.Parse(timestampStr);
+            long timeStampNow = GetCurrentTimeMillis();
+            return Math.Abs(timeStamp - timeStampNow) < maxTimestampGap;
+        }
+
+        public static bool ValidateRequest(Dictionary<string, string> parameters, string secret, string requestBodyStr)
+        {
+            Dictionary<string, string> dic = CopyDictionary(parameters);
+
+            // get AccessTokenId, sign
+            string sign = null;
+            if (dic.ContainsKey(HmacConst.KEY_SIGN))
+            {
+                sign = dic[HmacConst.KEY_SIGN];
+                dic.Remove(HmacConst.KEY_SIGN);
+            }
+            else
+            {
+                return false;
+            }
+
+            string md5Str = GetSignatureString(dic, secret, requestBodyStr);
+            return md5Str.Equals(sign, StringComparison.OrdinalIgnoreCase);
+        }
+
+        public static string GetSignatureString(Dictionary<string, string> dic, string secret, string requestBodyStr)
+        {
+            Dictionary<string, string> tempDic = CopyDictionary(dic);
+
+            if (!String.IsNullOrEmpty(requestBodyStr))
+            {
+                tempDic.Add(HmacConst.REQUEST_BODY, requestBodyStr);
+            }
+
+            String sortParamStr = GenerateQueryString(tempDic);
+
+            //下面的bapi的签名,xop没使用url编码了
+            //StringBuilder sb = new StringBuilder();
+            //foreach (string key in keyList)
+            //{
+            //    string value = dic[key];
+            //    if (value == null)
+            //    {
+            //        continue;
+            //    }
+            //    sb.Append(key);
+            //    sb.Append("=");
+            //    sb.Append(FormatString(value));
+            //    sb.Append("&");
+            //}
+            //sb.Append("secret=");
+            //sb.Append(secret);
+            //string keyListStr = sb.ToString();
+
+
+            String keyListStr = sortParamStr + "&secret=" + secret;
+            byte[] inputBytes = Encoding.UTF8.GetBytes(keyListStr);
+            string base64Str = Convert.ToBase64String(inputBytes);
+            byte[] bytes = SHA1.Create().ComputeHash(Encoding.UTF8.GetBytes(base64Str));
+            return BitConverter.ToString(bytes).Replace("-", "").ToLower();
+        }
+
+        public static String GenerateQueryString(Dictionary<String, String> dic)
+        {
+            List<string> sortedKeyList = dic.Keys.OrderBy(i => i, StringComparer.Ordinal).ToList();
+            List<String> tempParams = new List<string>(dic.Count);
+            foreach (var key in sortedKeyList)
+            {
+                String str = key + "=" + dic[key];
+                tempParams.Add(str);
+            }
+            return String.Join("&", tempParams);
+        }
+
+        public static Dictionary<string, string> CopyDictionary(Dictionary<string, string> dic)
+        {
+            Dictionary<string, string> copyDic = new Dictionary<string, string>();
+            if (dic == null || dic.Count == 0)
+            {
+                return copyDic;
+            }
+            foreach (var item in dic)
+            {
+                copyDic.Add(item.Key, item.Value);
+            }
+            return copyDic;
+        }
+
+
+    }
+
+
+
+    class UrlEncode
+    {
+        const int caseDiff = ('a' - 'A');
+        private static bool ValidateUrlEncodingParameters(byte[] bytes, int offset, int count)
+        {
+            if (bytes == null && count == 0)
+                return false;
+            if (bytes == null)
+            {
+                throw new ArgumentNullException("bytes");
+            }
+            if (offset < 0 || offset > bytes.Length)
+            {
+                throw new ArgumentOutOfRangeException("offset");
+            }
+            if (count < 0 || offset + count > bytes.Length)
+            {
+                throw new ArgumentOutOfRangeException("count");
+            }
+
+            return true;
+        }
+        public static string Encode(string str, Encoding e)
+        {
+            if (str == null)
+                return null;
+
+            byte[] bytes = e.GetBytes(str);
+            int offset = 0;
+            int count = bytes.Length;
+
+            if (!ValidateUrlEncodingParameters(bytes, offset, count))
+            {
+                return null;
+            }
+
+            int cSpaces = 0;
+            int cUnsafe = 0;
+
+            // count them first
+            for (int i = 0; i < count; i++)
+            {
+                char ch = (char)bytes[offset + i];
+
+                if (ch == ' ')
+                    cSpaces++;
+                else if (!HttpEncoderUtility.IsUrlSafeChar(ch))
+                    cUnsafe++;
+            }
+
+            // nothing to expand?
+            if (cSpaces == 0 && cUnsafe == 0)
+            {
+                // DevDiv 912606: respect "offset" and "count"
+                if (0 == offset && bytes.Length == count)
+                {
+                    return Encoding.ASCII.GetString(bytes);
+                }
+                else
+                {
+                    var subarray = new byte[count];
+                    Buffer.BlockCopy(bytes, offset, subarray, 0, count);
+                    return Encoding.ASCII.GetString(subarray);
+                }
+            }
+
+            // expand not 'safe' characters into %XX, spaces to +s
+            byte[] expandedBytes = new byte[count + cUnsafe * 2];
+            int pos = 0;
+
+            for (int i = 0; i < count; i++)
+            {
+                byte b = bytes[offset + i];
+                char ch = (char)b;
+
+                if (HttpEncoderUtility.IsUrlSafeChar(ch))
+                {
+                    expandedBytes[pos++] = b;
+                }
+                else if (ch == ' ')
+                {
+                    expandedBytes[pos++] = (byte)'+';
+                }
+                else
+                {
+                    expandedBytes[pos++] = (byte)'%';
+                    ch = (char)HttpEncoderUtility.IntToHex((b >> 4) & 0xF);
+                    if (char.IsLetter(ch))
+                    {
+                        ch = (char)((int)ch - caseDiff);
+                    }
+
+                    expandedBytes[pos++] = (byte)ch;
+
+                    ch = HttpEncoderUtility.IntToHex(b & 0x0F);
+                    if (char.IsLetter(ch))
+                    {
+                        ch = (char)((int)ch - caseDiff);
+                    }
+
+                    expandedBytes[pos++] = (byte)ch;
+                }
+            }
+            return Encoding.ASCII.GetString(expandedBytes);
+        }
+    }
+    class HttpEncoderUtility
+    {
+        public static char IntToHex(int n)
+        {
+            Debug.Assert(n < 0x10);
+
+            if (n <= 9)
+                return (char)(n + (int)'0');
+            else
+                return (char)(n - 10 + (int)'a');
+        }
+
+        // Set of safe chars, from RFC 1738.4 minus '+'
+        public static bool IsUrlSafeChar(char ch)
+        {
+            if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9'))
+                return true;
+
+            switch (ch)
+            {
+                case '-':
+                case '_':
+                case '.':
+                case '!':
+                case '*':
+                    //JAVA t appears that both Netscape and Internet Explorer escape
+                    //*all special characters from this list with the exception
+                    //*of "-", "_", ".", "*".
+                    //所以这两括号需要转义
+                    ////case '(':
+                    ////case ')':
+                    return true;
+            }
+
+            return false;
+        }
+
+        //  Helper to encode spaces only
+        internal static String UrlEncodeSpaces(string str)
+        {
+            if (str != null && str.IndexOf(' ') >= 0)
+                str = str.Replace(" ", "%20");
+            return str;
+        }
+    }
+}

+ 16 - 9
TEAMModelOS/Controllers/Third/Xkw/OpenAuthClient.cs

@@ -84,7 +84,7 @@ namespace TEAMModelOS.Controllers.Third.Xkw
         public override string GetAuthorizationUrl()
         {
 
-            string redirect_uri = "";
+            string redirect_uri ;
             if (Domain.Equals("kong.sso.com"))
             {
                 redirect_uri = $"https://{Domain}:5001/authorized/xkw?{HttpUtility.UrlEncode(Param)}";
@@ -98,16 +98,23 @@ namespace TEAMModelOS.Controllers.Third.Xkw
                 openSecret = CryptoUtils.EncryptAES(OpenId, AppSecret);
             }
             string timespan = CryptoUtils.EncryptAES(GetTimeStamp(), AppSecret);
-            string url = string.Format(AUTH_URL + "?client_id={0}&open_id={1}&service={2}&redirect_uri={3}&timespan={4}",
-                  AppKey, openSecret, SERVICE_URL, redirect_uri, timespan);
-            if (!string.IsNullOrEmpty(Extra))
-            {
-                url = string.Format("{0}&extra={1}", url, Extra);
-            }
+            // string url = string.Format(AUTH_URL + "?client_id={0}&open_id={1}&service={2}&redirect_uri={3}&timespan={4}",   AppKey, openSecret, SERVICE_URL, redirect_uri, timespan);
+
+            string url = string.Format(AUTH_URL + "?client_id={0}&open_id={1}&service={2}&redirect_uri={3}&timespan={4}&extra={5}",AppKey, openSecret, SERVICE_URL, RedirectUrl, timespan, Extra);
+            //if (!string.IsNullOrEmpty(Extra))
+            //{
+            //    url = string.Format("{0}&extra={1}", url, Extra);
+            //}
             //string retUrl = url + "&signature=" + SignatureHelper.GenerateSignature(url, AppSecret);
+            string signature = HttpUtility.UrlEncode(CryptoUtils.EncryptMD5(AppKey + Extra + openSecret + redirect_uri + SERVICE_URL + timespan + AppSecret));
+
+            //string URLEncoder = string.Format(AUTH_URL + "?client_id={0}&open_id={1}&service={2}&redirect_uri={3}&timespan={4}&extra={5}&signature={6}",
+            //    AppKey ,  openSecret , HttpUtility.UrlEncode(SERVICE_URL), HttpUtility.UrlEncode(redirect_uri),
+            //   HttpUtility.UrlEncode(timespan), HttpUtility.UrlEncode(Extra), HttpUtility.UrlEncode(SignatureHelper.GenerateSignature(signature, AppSecret)));
+
             string URLEncoder = string.Format(AUTH_URL + "?client_id={0}&open_id={1}&service={2}&redirect_uri={3}&timespan={4}&extra={5}&signature={6}",
-                AppKey ,  openSecret , HttpUtility.UrlEncode(SERVICE_URL), HttpUtility.UrlEncode(redirect_uri),
-               HttpUtility.UrlEncode(timespan), HttpUtility.UrlEncode(Extra), HttpUtility.UrlEncode(SignatureHelper.GenerateSignature(url, AppSecret)));
+                  HttpUtility.UrlEncode(AppKey), HttpUtility.UrlEncode(openSecret), HttpUtility.UrlEncode(SERVICE_URL), 
+                  HttpUtility.UrlEncode(redirect_uri), HttpUtility.UrlEncode(timespan), HttpUtility.UrlEncode(Extra), HttpUtility.UrlEncode(signature));
             return URLEncoder.Replace("+", "%2B");
         }
 

+ 120 - 0
TEAMModelOS/Controllers/Third/Xkw/Sdk/XkwAPIHttpService.cs

@@ -0,0 +1,120 @@
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Text;
+using System.Text.Json;
+using System.Threading;
+using System.Web;
+using TEAMModelOS.SDK.DI;
+using TEAMModelOS.SDK.Extension;
+using TEAMModelOS.SDK.Models;
+
+namespace TEAMModelOS.Controllers.Third.Xkw.Sdk
+{
+
+    public static class XkwAPIHttpServiceExtensions
+    {
+        public static IServiceCollection AddXkwAPIHttpService(this IServiceCollection services, IConfiguration _configuration, string name = "Default")
+        {
+            if (services == null) throw new ArgumentNullException(nameof(services));
+            string location = _configuration.GetValue<string>("Option:Location");
+            services.AddHttpClient<XkwAPIHttpService>();
+            services.Configure<XkwAPIHttpServiceOptions>(name, o => { o.location = location; });
+            return services;
+        }
+    }
+
+    public class XkwAPIHttpServiceOptions
+    {
+        public string location { get; set; }
+    }
+    public class XkwAPIHttpService
+    {
+        private static int DEFAULT_TIMEOUT = 5;
+
+        private String baseUrl;
+        private String appId;
+        private String secret;
+        public int TimeoutInSeconds { get; set; } = DEFAULT_TIMEOUT;
+        private readonly HttpClient _httpClient;
+        public readonly IOptionsMonitor<XkwAPIHttpServiceOptions> options;
+        public XkwAPIHttpService(HttpClient httpClient, IOptionsMonitor<XkwAPIHttpServiceOptions> optionsMonitor, AzureStorageFactory _azureStorageFactory)
+        {
+
+            _httpClient = httpClient;
+            options = optionsMonitor;
+            var table = _azureStorageFactory.GetCloudTableClient().GetTableReference("IESOAuth");
+
+            string domain = "w";
+
+          //  List<OAuthComConfig> configs = await table.FindListByDict<OAuthComConfig>(new Dictionary<string, object>() { { "PartitionKey", "OAuthComConfig" }, { "RowKey", RowKey } });
+            this.baseUrl = baseUrl;
+            this.appId = appId;
+            this.secret = secret;
+        }
+
+
+        public T Get<T>(String uri, Dictionary<String, String> parameters)
+        {
+            return SendRequest<T>(HttpMethod.Get, uri, parameters, null, null, this.TimeoutInSeconds);
+        }
+
+        public T PostAsJson<T>(String uri, Dictionary<String, String> parameters, JsonElement data)
+        {
+
+            string json = data.ToJsonString();
+            return SendRequest<T>(HttpMethod.Post, uri, parameters, json, "application/json", this.TimeoutInSeconds);
+        }
+        public T SendRequest<T>(HttpMethod method, String path, Dictionary<String, String> parameters, String requestBodyStr, String requestContentType, int timeoutInSeconds)
+        {
+            Dictionary<String, String> dic = new Dictionary<string, string>();
+            if (parameters != null)
+            {
+                foreach (var item in parameters)
+                {
+                    dic.Add(item.Key, item.Value);
+                }
+            }
+            // 不对body验签,添加url验签
+            //UriIdentifier uriIdentifier = HmacCommonUtils.SplitUriPrefix(path);
+            dic.Add(HmacConst.KEY_URL, path);
+            HmacResult signRlt = HmacUtils.Sign(appId, secret, dic, requestBodyStr);
+
+            String pathWithQuery = path;
+            if (parameters?.Count > 0)
+            {
+                pathWithQuery = path + "?" + String.Join("&", parameters.Select(i => $"{i.Key}={HttpUtility.UrlEncode(i.Value)}"));
+            }
+
+            HttpRequestMessage request = new HttpRequestMessage(method, baseUrl + pathWithQuery);
+            if (requestBodyStr != null)
+            {
+                StringContent requestContent = new StringContent(requestBodyStr, Encoding.UTF8, requestContentType);
+                request.Content = requestContent;
+            }
+            request.Headers.Add(HmacConst.KEY_TIMESTAMP, signRlt.TimeStamp.ToString());
+            request.Headers.Add(HmacConst.KEY_SIGN, signRlt.Sign);
+            request.Headers.Add(HmacConst.KEY_NONCE, signRlt.Nonce);
+
+            HttpResponseMessage response = null;
+            using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutInSeconds)))
+            {
+                response = _httpClient.SendAsync(request, cts.Token).Result;
+            }
+            String responseText = response.Content.ReadAsStringAsync().Result;
+            if (response.StatusCode != HttpStatusCode.OK)
+            {
+                String message = $"服务错误: 状态码: {(int)response.StatusCode}, resposneText: {responseText}";
+                XopException bapiException = new XopException(message, response);
+                throw bapiException;
+            }
+
+            return responseText.ToObject<T>();
+        }
+    }
+}

+ 25 - 0
TEAMModelOS/Controllers/Third/Xkw/Sdk/XopException.cs

@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Text;
+
+namespace TEAMModelOS.Controllers.Third.Xkw
+{
+    public class XopException : Exception
+    {
+        public HttpResponseMessage Response { get; set; }
+
+        public XopException()
+        {
+        }
+
+        public XopException(String message) : base(message)
+        {
+        }
+
+        public XopException(String message, HttpResponseMessage response) : base(message)
+        {
+            this.Response = response;
+        }
+    }
+}

+ 151 - 0
TEAMModelOS/Controllers/Third/Xkw/Sdk/XopHttpClient.cs

@@ -0,0 +1,151 @@
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Text;
+using System.Threading;
+using System.Web;
+
+namespace TEAMModelOS.Controllers.Third.Xkw
+{
+    public class XopHttpClient : IDisposable
+    {
+        private static int DEFAULT_TIMEOUT = 5;
+
+        private String baseUrl;
+        private String appId;
+        private String secret;
+
+        public int TimeoutInSeconds { get; set; } = DEFAULT_TIMEOUT;
+
+        private HttpClient httpClient;
+
+
+        public void Dispose()
+        {
+            this.httpClient?.Dispose();
+        }
+
+        public XopHttpClient(String baseUrl, String appId, String secret) : this(baseUrl, appId, secret, null)
+        {
+        }
+
+        public XopHttpClient(String baseUrl, String appId, String secret, WebProxy proxy) : this(baseUrl, appId, secret, DEFAULT_TIMEOUT, proxy)
+        {
+        }
+
+        public XopHttpClient(String baseUrl, String appId, String secret, int timeout) : this(baseUrl, appId, secret, timeout, null)
+        {
+        }
+
+        public XopHttpClient(String baseUrl, String appId, String secret, int timeout, WebProxy proxy)
+        {
+            this.baseUrl = baseUrl;
+            this.appId = appId;
+            this.secret = secret;
+            if (proxy != null)
+            {
+                var httpClientHandler = new HttpClientHandler
+                {
+                    Proxy = proxy
+                };
+                this.httpClient = new HttpClient(httpClientHandler, true);
+            }
+            else
+            {
+                this.httpClient = new HttpClient();
+            }
+            this.httpClient.DefaultRequestHeaders.Add(HmacConst.KEY_APP_ID, appId);
+        }
+
+        public T Get<T>(String uri)
+        {
+            return Get<T>(uri, null);
+        }
+
+        public T Get<T>(String uri, Dictionary<String, String> parameters)
+        {
+            return SendRequest<T>(HttpMethod.Get, uri, parameters, null, null, this.TimeoutInSeconds);
+        }
+
+        public T Post<T>(String uri, Dictionary<String, String> parameters, String text, String contentType)
+        {
+            return SendRequest<T>(HttpMethod.Post, uri, parameters, text, "application/json", this.TimeoutInSeconds);
+        }
+
+        public T PostAsText<T>(String uri, Dictionary<String, String> parameters, String text)
+        {
+            return SendRequest<T>(HttpMethod.Post, uri, parameters, text, "text/plain", this.TimeoutInSeconds);
+        }
+
+        public T PostAsJson<T>(String uri, Dictionary<String, String> parameters, Object data)
+        {
+            
+           string json = JsonConvert.SerializeObject(data);
+            return SendRequest<T>(HttpMethod.Post, uri, parameters, json, "application/json", this.TimeoutInSeconds);
+        }
+
+        //public T PostAsForm<T>(String uri, Dictionary<String, String> parameters, Dictionary<String, String> formData)
+        //{
+        //    if (formData == null)
+        //    {
+        //        formData = new Dictionary<string, string>();
+        //    }
+        //    using (HttpContent requestContent = new FormUrlEncodedContent(formData.ToList()))
+        //    {
+        //        return SendRequest<T>(HttpMethod.Post, uri, parameters, requestContent, this.TimeoutInSeconds);
+        //    }
+        //}
+
+        public T SendRequest<T>(HttpMethod method, String path, Dictionary<String, String> parameters, String requestBodyStr, String requestContentType, int timeoutInSeconds)
+        {
+            Dictionary<String, String> dic = new Dictionary<string, string>();
+            if (parameters != null)
+            {
+                foreach (var item in parameters)
+                {
+                    dic.Add(item.Key, item.Value);
+                }
+            }
+            // 不对body验签,添加url验签
+            //UriIdentifier uriIdentifier = HmacCommonUtils.SplitUriPrefix(path);
+            dic.Add(HmacConst.KEY_URL, path);
+            HmacResult signRlt = HmacUtils.Sign(appId, secret, dic, requestBodyStr);
+
+            String pathWithQuery = path;
+            if (parameters?.Count > 0)
+            {
+                pathWithQuery = path + "?" + String.Join("&", parameters.Select(i => $"{i.Key}={HttpUtility.UrlEncode(i.Value)}"));
+            }
+
+            HttpRequestMessage request = new HttpRequestMessage(method, baseUrl + pathWithQuery);
+            if (requestBodyStr != null)
+            {
+                StringContent requestContent = new StringContent(requestBodyStr, Encoding.UTF8, requestContentType);
+                request.Content = requestContent;
+            }
+            request.Headers.Add(HmacConst.KEY_TIMESTAMP, signRlt.TimeStamp.ToString());
+            request.Headers.Add(HmacConst.KEY_SIGN, signRlt.Sign);
+            request.Headers.Add(HmacConst.KEY_NONCE, signRlt.Nonce);
+
+            HttpResponseMessage response = null;
+            using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutInSeconds)))
+            {
+                response = this.httpClient.SendAsync(request, cts.Token).Result;
+            }
+            String responseText = response.Content.ReadAsStringAsync().Result;
+            if (response.StatusCode != HttpStatusCode.OK)
+            {
+                String message = $"服务错误: 状态码: {(int)response.StatusCode}, resposneText: {responseText}";
+                XopException bapiException = new XopException(message, response);
+                throw bapiException;
+            }
+
+            return JsonConvert.DeserializeObject<T>(responseText);
+        }
+
+    }
+
+}

+ 10 - 32
TEAMModelOS/Controllers/Third/Xkw/XkwOAuth2Controller.cs

@@ -59,22 +59,7 @@ namespace TEAMModelOS.Controllers
         private readonly ThirdApisService _scsApisService;
         private readonly HttpTrigger _httpTrigger;
         private readonly IWebHostEnvironment _environment;
-        /// <summary>
-        /// 机构安全码
-        /// </summary>
-        public string _sc_passKey;
-        /// <summary>
-        /// 机构ID
-        /// </summary>
-        public string _sc_trainComID;
-        /// <summary>
-        /// 机构 AES 密钥
-        /// </summary>
-        public string _sc_privateKey;
-        /// <summary>
-        /// 访问地址
-        /// </summary>
-        public string _sc_url;
+ 
         public IConfiguration _configuration { get; set; }
         public XkwOAuth2Controller(IWebHostEnvironment environment, AzureCosmosFactory azureCosmos, SnowflakeId snowflakeId, DingDing dingDing, IOptionsSnapshot<Option> option, AzureStorageFactory azureStorage,
           AzureRedisFactory azureRedis, AzureServiceBusFactory serviceBus, IConfiguration configuration, CoreAPIHttpService coreAPIHttpService, ThirdApisService scsApisService, HttpTrigger httpTrigger)
@@ -112,6 +97,7 @@ namespace TEAMModelOS.Controllers
             {
                 domain = _option.HostName;
             }
+           // domain = "test.teammodel.cn";
             var req = HttpContext?.Request;
             //https://ssoserviceurl/oauth2/authorize?client_id=APPKEY&openid=OPENID=&service=SERVICE
             var (tmdid, _, _, school) = HttpContext.GetAuthTokenInfo();
@@ -143,13 +129,7 @@ namespace TEAMModelOS.Controllers
             string url = client.GetAuthorizationUrl();
             return Ok(new { redirect = url });
         }
-        [HttpGet("paper-notice")]
-        //[Authorize(Roles = "IES")]
-        //[AuthToken(Roles = "teacher,admin,area,student")]
-        public async Task<IActionResult> PaperNotice([FromQuery] OAuthCode authCode) {
-
-          return   Ok();
-        }
+       
         [HttpGet("authorize")]
         //[Authorize(Roles = "IES")]
         //[AuthToken(Roles = "teacher,admin,area,student")]
@@ -175,6 +155,7 @@ namespace TEAMModelOS.Controllers
             if (_option.Location.Equals("China")) {
                 domain = _option.HostName;
             }
+           
             var client =await GetOpenAuthClient(authCode.tmdid, authCode.module, accessToken, domain);
             string schoolId = "teammodel.cn";
             //学科网测试
@@ -293,7 +274,7 @@ namespace TEAMModelOS.Controllers
                                 // var settings = ConfigurationManager.AppSettings;
                                 // var client = new XkwOAuthClient(settings["OAuth_Xkw_AppKey"], settings["OAuth_Xkw_AppSecret"], settings["OAuth_Xkw_RedirectUrl"], settings["OAuth_Xkw_OAuthHost"], accessToken, openId, userId);
 
-            List<OAuthUser> authUsers = await table.FindListByDict<OAuthUser>(new Dictionary<string, object>() { { "PartitionKey", "OAuthUser-xkw" }, { "RowKey", tmdid } });
+            List<OAuthUser> authUsers = await table.FindListByDict<OAuthUser>(new Dictionary<string, object>() { { "PartitionKey", $"OAuthUser-xkw-{domain}" }, { "RowKey", tmdid } });
             if (authUsers.Any()) {
                 openId = authUsers[0].OpenId;
             }
@@ -318,14 +299,9 @@ namespace TEAMModelOS.Controllers
                             {
                                 domain_port = "kong.sso.com:5001";
                             }
-                            else
-                            {
-                                domain_port = "kong.sso.com:5001";
-                            }
-
-                            OAuth_Xkw_ServiceUrl = OAuth_Xkw_ServiceUrl.Replace("{{iframe}}", $"{HttpUtility.UrlEncode("")}")
-                                .Replace("{{notice}}","")
-                                .Replace("{{openId}}", openId);
+                            OAuth_Xkw_ServiceUrl = OAuth_Xkw_ServiceUrl.Replace("{{iframe}}", HttpUtility.UrlEncode($"https://{domain_port}/home/newSchoolPaper"))
+                                .Replace("{{notice}}", HttpUtility.UrlEncode($"https://{domain_port}/xkw/paper-notice"))
+                                .Replace("{{openId}}", openId).Replace("{{target}}", "");
                         }
                     }
                     else {
@@ -348,5 +324,7 @@ namespace TEAMModelOS.Controllers
             }
 
         }
+
+        
     }
 }

+ 88 - 0
TEAMModelOS/Controllers/Third/Xkw/XkwServiceController.cs

@@ -0,0 +1,88 @@
+using Microsoft.AspNetCore.Mvc;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using TEAMModelOS.Models;
+using TEAMModelOS.SDK.DI;
+using System.Text.Json;
+using TEAMModelOS.SDK.Models;
+using Microsoft.AspNetCore.Http;
+using TEAMModelOS.SDK.Extension;
+using Azure.Cosmos;
+using System.Text;
+using TEAMModelOS.SDK.DI.AzureCosmos.Inner;
+using Microsoft.Extensions.Options;
+using Azure.Messaging.ServiceBus;
+using Microsoft.Extensions.Configuration;
+using HTEXLib.COMM.Helpers;
+using TEAMModelOS.SDK;
+using System.IdentityModel.Tokens.Jwt;
+using TEAMModelOS.Services;
+using TEAMModelOS.SDK.Models.Service;
+using System.IO;
+using System.Dynamic;
+using Microsoft.AspNetCore.Authorization;
+using Azure.Storage.Blobs.Models;
+using static TEAMModelOS.SDK.Models.Teacher;
+using System.Web;
+using static TEAMModelOS.Controllers.FixDataController;
+using static TEAMModelOS.SDK.SchoolService;
+using Microsoft.AspNetCore.Hosting;
+using TEAMModelOS.Filter;
+using TEAMModelOS.Controllers.Third.Xkw;
+using Microsoft.Extensions.Primitives;
+using System.Net.Http;
+namespace TEAMModelOS.Controllers.Third.Xkw
+{ 
+    // <summary>
+    ///  标准OAuth2
+    /// </summary>
+    ///  
+    [ProducesResponseType(StatusCodes.Status200OK)]
+    [ProducesResponseType(StatusCodes.Status400BadRequest)]
+    //
+    //[Route("")]
+    [Route("xkw")]
+    [ApiController]
+    public class XkwServiceController: ControllerBase
+    {
+        private readonly SnowflakeId _snowflakeId;
+        private readonly AzureCosmosFactory _azureCosmos;
+        private readonly DingDing _dingDing;
+        private readonly Option _option;
+        private readonly AzureStorageFactory _azureStorage;
+        private readonly AzureServiceBusFactory _serviceBus;
+        private readonly AzureRedisFactory _azureRedis;
+        private readonly CoreAPIHttpService _coreAPIHttpService;
+        private readonly ThirdApisService _scsApisService;
+        private readonly HttpTrigger _httpTrigger;
+        private readonly IWebHostEnvironment _environment;
+        public IConfiguration _configuration { get; set; }
+        public XkwServiceController(IWebHostEnvironment environment, AzureCosmosFactory azureCosmos, SnowflakeId snowflakeId, DingDing dingDing, IOptionsSnapshot<Option> option, AzureStorageFactory azureStorage,
+            AzureRedisFactory azureRedis, AzureServiceBusFactory serviceBus, IConfiguration configuration, CoreAPIHttpService coreAPIHttpService, ThirdApisService scsApisService, HttpTrigger httpTrigger)
+        {
+            _azureCosmos = azureCosmos;
+            _snowflakeId = snowflakeId;
+            _dingDing = dingDing;
+            _option = option?.Value;
+            _azureStorage = azureStorage;
+            _serviceBus = serviceBus;
+            _configuration = configuration;
+            _azureRedis = azureRedis;
+            _coreAPIHttpService = coreAPIHttpService;
+            _scsApisService = scsApisService;
+            _httpTrigger = httpTrigger;
+            _environment = environment;
+        }
+
+        [HttpGet("paper-notice")]
+        //[Authorize(Roles = "IES")]
+        //[AuthToken(Roles = "teacher,admin,area,student")]
+        public async Task<IActionResult> PaperNotice([FromQuery] OAuthCode authCode)
+        {
+            await _dingDing.SendBotMsg($"学科网推送消息:{authCode.ToJsonString()}", GroupNames.成都开发測試群組);
+            return Ok();
+        }
+    }
+}

+ 2 - 0
TEAMModelOS/Startup.cs

@@ -27,6 +27,7 @@ using Microsoft.Extensions.Hosting;
 using Microsoft.Extensions.Primitives;
 using Microsoft.IdentityModel.Tokens;
 using TEAMModelOS.Controllers;
+using TEAMModelOS.Controllers.Third.Xkw.Sdk;
 using TEAMModelOS.Filter;
 using TEAMModelOS.Models;
 using TEAMModelOS.SDK;
@@ -154,6 +155,7 @@ namespace TEAMModelOS
             //等保安全性验证。
             //  services.AddScoped<SecurityHeadersAttribute>();
             services.AddSingleton(typeof(IConverter), new SynchronizedConverter(new PdfTools()));
+            services.AddXkwAPIHttpService(Configuration);
         }
 
         // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.

+ 1 - 0
TEAMModelOS/TEAMModelOS.csproj

@@ -13,6 +13,7 @@
     <None Remove="libwkhtmltox.dll" />
   </ItemGroup>
   <ItemGroup>
+    <Folder Include="Controllers\Third\Xkw\Hmac\" />
     <Folder Include="logfile\" />
     <Folder Include="Lib\" />
     <Folder Include="wwwroot\" />