Ver código fonte

Merge branch 'develop' of http://52.130.252.100:10000/TEAMMODEL/TEAMModelOS into develop

OnePsycho 1 ano atrás
pai
commit
4336ea6de8

+ 1 - 1
TEAMModelBI/ClientApp/package.json

@@ -27,7 +27,7 @@
         "less": "^4.1.2",
         "qs": "^6.10.1",
         "splitpanes": "^3.1.1",
-        "vue": "^3.2.0",
+        "vue": "^3.2.26",
         "vue-clipboard3": "^2.0.0",
         "vue-i18n": "^9.2.2",
         "vue-loader-v16": "npm:vue-loader@^16.0.0-alpha.3",

+ 1 - 1
TEAMModelBI/ClientApp/public/index.html

@@ -12,7 +12,7 @@
     </title>
 </head>
 <script src="https://g.alicdn.com/dingding/dinglogin/0.0.5/ddLogin.js"></script>
-<script src="https://at.alicdn.com/t/c/font_2934132_dz617c1kcup.js"></script>
+<script src="https://at.alicdn.com/t/c/font_2934132_mzapgo0jijg.js"></script>
 <script src="../src/access/iconfont.js"></script>
 
 <body>

+ 17 - 1
TEAMModelBI/ClientApp/src/api/index.js

@@ -578,5 +578,21 @@ export default {
     /*ID查询 相关*/
     getUserdatas(data) {
         return post('/tmid/get-tmidstics',data)
-     }
+    },
+
+    /*優惠券相關*/
+    // 建立優惠券
+    crtCoupon(data) {
+        return post('/coupon/create-coupon',data)
+    },
+    // 歸戶
+    consolidationCoupon(data) {
+        return post('/coupon/consolidation-coupon',data)
+    },
+
+    /*簡版通知*/
+    // 發送
+    pushNotify(data) {
+        return post('/coupon/push-notify',data)
+    }
 }

+ 10 - 1
TEAMModelBI/ClientApp/src/router/index.js

@@ -231,7 +231,16 @@ const routes = [{
                 roles: ['admin'],
                 isShow: true,
                 component: () => require.ensure([], (require) => require(`@/view/systemConfig/server/index.vue`))
-            }
+            },
+            //優惠券發行
+            {
+                name: 'issuecoupons',
+                path: 'issuecoupons',
+                permission: "",
+                roles: ['admin'],
+                isShow: true,
+                component: () => require.ensure([], (require) => require(`@/view/issueCoupons/index.vue`))
+            },
         ]
     },
     //消息通知跳转页面

+ 8 - 0
TEAMModelBI/ClientApp/src/view/common/aside.vue

@@ -149,6 +149,14 @@ export default {
           //   isShow: true,
           //   sort: 19,
           // },
+          {
+            name: '發優惠券',
+            router: '/home/issuecoupons',
+            icon: '#icon-Ticket',
+            permission: [],
+            isShow: true,
+            sort: 6,
+          },
         ],
       },
       {

+ 156 - 0
TEAMModelBI/ClientApp/src/view/issueCoupons/consolidationCoupon.vue

@@ -0,0 +1,156 @@
+<template>
+    <div>
+        <el-form ref="ruleFormRef" :model="consolidationForm" :rules="rules" label-width="120px">
+            <el-form-item label="站別" prop="srvAdr">
+                <el-radio-group v-model="consolidationForm.srvAdr">
+                    <el-radio label="Global" size="large" border>國際站</el-radio>
+                    <el-radio label="China" size="large" border>大陸站</el-radio>
+                </el-radio-group>
+            </el-form-item>
+            <el-form-item label="券號" prop="coupon">
+                <el-input style="width: 500px;" v-model="consolidationForm.coupon" placeholder="請輸入券號" />
+            </el-form-item>
+            <el-form-item label="醍摩豆ID" prop="targets">
+                <div style="display: flex;align-items: flex-end;">
+                    <el-input style="width: 300px;" v-model="consolidationForm.targets" :rows="6" type="textarea" placeholder="請填入教師ID, 並用換行分隔" />
+                    <div style="margin-left: 5px;">總共 {{ targetsCount }} 位</div>
+                </div>
+            </el-form-item>
+            <el-form-item>
+                <el-button type="primary" @click="submitForm(ruleFormRef)" :loading="loading">歸戶</el-button>
+                <el-button @click="resetForm(ruleFormRef)" :loading="loading">重置</el-button>
+            </el-form-item>
+        </el-form>
+    </div>
+</template>
+<script setup>
+import { reactive, ref, computed, getCurrentInstance, markRaw } from 'vue'
+import { Warning, SuccessFilled } from '@element-plus/icons'
+import { ElMessageBox } from 'element-plus'
+let { proxy } = getCurrentInstance()
+const targetsCount = ref(0)
+const ruleFormRef = ref()
+const loading = ref(false)
+const consolidationForm = reactive({
+    srvAdr: '',
+    coupon: '',
+    targets: ""
+})
+
+const checkTargets = (rule, value, callback) => {
+    let isErr = false
+    function isArrayRep(array){
+        var result = new Set();
+        var repeat = new Set();
+        array.forEach(item => {
+            result.has(item) ? repeat.add(item) : result.add(item);
+        })
+        // console.log(result); // {1, 2, "a", 3, "b"}
+        // console.log(repeat); // {1, "a"}
+        // console.log(repeat.size)
+        if(repeat.size != 0){
+            return true
+        } else {
+            return false
+        }
+    }
+    if(value && !/^[0-9\n]+$/.test(value)){
+        isErr = true
+        callback(new Error("只能輸入醍摩豆ID與換行"))
+    } else if(value && isArrayRep(value.split('\n'))){
+        isErr = true
+        callback(new Error("醍摩豆ID有重複"))
+    } else if(value){
+        let errIds = []
+        let tmp = value.split('\n')
+        tmp.forEach(e=> {
+            if(e.length != 10) {
+                errIds.push(e)
+            } else {
+                let now = Math.floor(new Date().getTime() / 1000)
+                let orgId = parseInt(e)
+                if(parseInt(e.substring(0, 1)) >= 6){
+                    orgId -= 5000000000
+                }
+
+                if(orgId > now){
+                    errIds.push(e)
+                }
+            }
+        })
+
+        if(errIds.length > 0){
+            // console.log(errIds, 'errIds')
+            isErr = true
+            callback(new Error("請檢查醍摩豆ID是否符合格式或有空格"))
+        }
+    }
+
+    if(!isErr && value != ''){
+        let tArray = value.split('\n')
+        targetsCount.value = tArray.length
+    } else {
+        targetsCount.value = 0
+    }
+
+    callback()
+}
+
+const rules = reactive({
+    srvAdr: [{required: true, trigger: "blur", message: '請選擇一個站別' }],
+    coupon: [{required: true, trigger: "blur", message: '請選擇一個發券類型' }],
+    targets: [{required: true, validator: checkTargets, trigger: "blur"}],
+})
+
+const submitForm = formEl => {
+  if (!formEl) return
+  formEl.validate(async (valid, ddd) => {
+    if (valid) {
+
+        ElMessageBox.confirm(
+            '開始歸戶?',
+            '',
+            {
+                type: 'info',
+                confirmButtonText: '歸戶'
+            }
+        ).then(async ()=>{
+            loading.value = true
+            let consolidationData = {
+                srvAdr: consolidationForm.srvAdr,
+                ids: consolidationForm.targets.split('\n'),
+                coupon: consolidationForm.coupon
+            }
+            await proxy.$api.consolidationCoupon(consolidationData).then((res) => {
+                console.log(res, '歸戶成功')
+                ElMessageBox.alert('成功', '歸戶',
+                    {
+                        type: 'info',
+                        icon: markRaw(SuccessFilled),
+                    }
+                )
+                resetForm(formEl) // 欄位清除
+            }).catch(e=>{
+                ElMessageBox.alert('歸戶失敗', '歸戶',
+                    {
+                        type: 'warning',
+                        icon: markRaw(Warning),
+                    }
+                )
+            }).finally(() => {
+                loading.value = false
+            })
+        })
+    } else {
+        console.log("error submit!")
+        return false
+    }
+  })
+}
+
+const resetForm = formEl => {
+    if (!formEl) return
+    formEl.resetFields()
+}
+
+</script>

+ 929 - 0
TEAMModelBI/ClientApp/src/view/issueCoupons/crteadCoupon.vue

@@ -0,0 +1,929 @@
+<template>
+    <div style="position: relative;">
+        <div style="width: 100px; height:100px;position: absolute;right: 0;z-index: 10;" @click="showClick()"></div>
+        <el-form ref="ruleFormRef" :model="crtCouponForm" :rules="rules" label-width="120px">
+            <el-form-item label="發券位置" prop="srvAdr">
+                <el-radio-group v-model="crtCouponForm.srvAdr">
+                    <el-radio label="Global" size="large" border>國際站</el-radio>
+                    <el-radio label="China" size="large" border>大陸站</el-radio>
+                </el-radio-group>
+            </el-form-item>
+            <el-form-item label="活動票券" prop="eventType">
+                <el-radio-group v-model="eventType" @change="setEventType">
+                    <el-radio label="hiteach50-3" size="large" border>Hiteach 50 第三階段</el-radio>
+                    <el-radio label="hiteach333-1" size="large" border>HiTeach 333 第一階段</el-radio>
+                    <el-radio label="hiteach333-3" size="large" border>HiTeach 333 第三階段</el-radio>
+                </el-radio-group>
+            </el-form-item>
+            <el-form-item v-if="detailSettingFlag" label="票券類型" prop="couponType" >
+                <el-select v-model="crtCouponForm.couponType" placeholder="請選擇" clearable style="width: 500px;">
+                    <el-option label="公開型" value="Public" />
+                    <el-option label="事件型" value="Event" />
+                    <el-option label="大量生成" value="General" />
+                </el-select>
+            </el-form-item>
+            <el-form-item v-if="detailSettingFlag && crtCouponForm.couponType != 'General'" label="票券名稱" prop="couponName" >
+                <el-input v-model="crtCouponForm.couponName" placeholder="範例: TEAM10T2G(不填會自動生成)" style="width: 500px;"/>
+            </el-form-item>
+            <el-form-item v-if="detailSettingFlag" label="活動名稱" prop="eventName">
+                <el-input v-model="crtCouponForm.eventName" placeholder="範例: hiteach333-1" style="width: 500px;"/>
+            </el-form-item>
+            <el-form-item label="到期時間" prop="expire">
+                <el-date-picker 
+                v-model="crtCouponForm.expire" 
+                type="datetime" 
+                style="width: 500px;" 
+                :disabled-date="disabledDate" 
+                :shortcuts="shortcuts"/>
+            </el-form-item>
+            <el-form-item v-if="crtCouponForm.couponType == 'Public'" label="兌換上限" prop="maxTaker">
+                <el-input-number v-model="crtCouponForm.maxTaker" :min="0"  :disabled="!maxTakerFlag" style="margin-right: 6px;"/>
+                <el-checkbox v-model="maxTakerFlag" label="啟用" size="large" />
+            </el-form-item>
+            <el-form-item v-if="crtCouponForm.couponType == 'General'" label="生成券數" prop="quantity">
+                <el-input-number v-model="crtCouponForm.quantity" :min="0"  />
+            </el-form-item>
+            <el-form-item v-if="detailSettingFlag" label="規則" prop="rule">
+                <div style="max-height: 330px;overflow: scroll;">
+                    <div v-for="(rule, rIndex) in crtCouponForm.rule" style="display: flex;flex-direction: row;align-items: center;margin-bottom: 5px;">
+                        <el-button :icon="Plus" circle @click="addRule(rIndex)" style="margin-right: 5px;"/>
+                        <div style="text-align: left;border: 1px solid #dcdfe6;padding: 6px 11px;border-radius: 5px;margin-right: 5px;">
+                            條件:
+                            <div style="min-height: 36px;max-width: 550px;border: 1px solid #e4e7ed;padding: 1px 11px;border-radius: 5px;background-color: #f5f7fa;">{{ translateRule(rule.q) }}</div>
+                            <div v-for="(item, index) in rule.q" style="text-align:left;">
+                                <el-button-group style="margin-right: 5px;">
+                                    <el-button :icon="Plus" @click="addCondition(rIndex, index)" />
+                                    <el-button :icon="CloseBold" @click="delCondition(rIndex, index)" :disabled="index == 0" />
+                                </el-button-group>
+                                <el-select v-model="item.operator" :disabled="index == 0" clearable :placeholder="index == 0 ? ' ' : '請選擇'" style="width: 100px;">
+                                    <el-option v-for="(o, i) in operatorList" :label="o.label" :value="o.val" :key="i" />
+                                </el-select>
+                                <el-select v-model="item.type" placeholder="檢核屬性" clearable style="width: 250px;">
+                                    <el-option  v-for="(t, i) in typeList" :label="t.label" :value="t.val" :key="i"/>
+                                </el-select>
+                                <el-select v-model="item.how" placeholder="判斷方式" style="width: 120px;" clearable :change="(item.how == 'is null' || item.how == 'isnot null') ? item.val = '' : item.val = item.val">
+                                    <el-option  v-for="(h, i) in howList" :label="h.label" :value="h.val" :key="i"/>
+                                </el-select>
+                                <el-input  v-model="item.val" placeholder="數值" style="width: 100px;" :disabled="(item.how == 'is null' || item.how == 'isnot null')"/>
+                            </div>
+                            獲得增益:
+                            <div style="text-align:left;">
+                                <el-select v-model="rule.b" placeholder="請選擇" clearable style="width: 500px;">
+                                    <el-option  v-for="(b, i) in ruleBList" :label="b.label" :value="b.val" :key="i"/>
+                                </el-select>
+                            </div>
+                            獲得積分: 
+                            <div style="text-align:left;">
+                                <el-input-number v-model="rule.p" :min="0"  />
+                            </div>
+                        </div>
+                        <el-button v-if="rIndex != 0" :icon="CloseBold" circle @click="delRule(rIndex)" style="margin-right: 5px;"/>
+                    </div>
+
+                </div>
+            </el-form-item>
+            <el-form-item label="票券資訊" prop="info">
+                <div style="display: flex;">
+                    <el-card v-for="info in crtCouponForm.info" style="width: 400px;margin-right:7px;">
+                        <template #header>
+                            <div style="text-align: left;font-weight: bold;font-size: 17px;">
+                                {{langToName(info.l)}}
+                            </div>
+                        </template>
+                        <div style="text-align: left;">
+                            票券標題:<el-input v-model="info.n" :readonly="!detailSettingFlag"/>
+                            連結:<el-input v-model="info.u" :readonly="!detailSettingFlag"/>
+                        </div>
+                        <template #footer>
+                            <div style="display: flex;justify-content: center;">
+                                <div class="coupons">
+                                    <div class="coupons-cont">
+                                        <div class="hole"/>
+                                        <div class="coupons-cont-box">
+                                            <div class="coupons-cont-box-title">{{ info.n }}</div>
+                                            <div class="coupons-cont-box-exp" >
+                                                {{ '有效期限: ' + convertDate(crtCouponForm.expire) }}
+                                            </div>
+                                            <div class="coupons-cont-box-link">
+                                                <el-button round size="small" @click="linkToPage(info.u)">了解更多</el-button>
+                                            </div>
+                                        </div>
+                                    </div>
+                                    <div class="coupons-btn">
+                                        <div class="coupons-btn-hole-positon">
+                                            <div class="hole"/> 
+                                        </div>
+                                        <div class="icon-box">
+                                            <el-icon :size="23" style="transform:rotate(-50deg);">
+                                            <Ticket />
+                                            </el-icon>
+                                        </div>
+                                        {{ '兌換' }}
+                                    </div>
+                                </div>
+                            </div>
+                        </template>
+                    </el-card>
+                </div>
+            </el-form-item>
+            <el-form-item v-if="crtCouponForm.couponType == 'Event'" label="指定醍摩豆ID" prop="targets">
+                <div style="display: flex;align-items: flex-end;">
+                    <el-input style="width: 300px;" v-model="crtCouponForm.targets" :rows="6" type="textarea" placeholder="請填入教師ID, 並用換行分隔" />
+                    <div style="margin-left: 5px;">總共 {{ targetsCount }} 位</div>
+                </div>
+            </el-form-item>
+            <el-form-item v-if="crtCouponForm.couponType == 'Event'">
+                <el-checkbox v-model="consolidationFlag" label="直接歸戶給以上老師" size="large" />
+            </el-form-item>
+            <el-form-item>
+                <el-button type="primary" @click="submitForm(ruleFormRef)" :loading="loading">建立優惠券</el-button>
+                <el-button @click="resetForm(ruleFormRef)" :loading="loading">重置</el-button>
+            </el-form-item>
+            <el-form-item label="優惠券"  style="margin-top: 50px;max-width: 600px;">
+                <el-input :rows="6" type="textarea" v-model="couponResult" :readonly="true" style="margin-bottom: 5px;"/>
+                <el-button type="primary" :icon="CopyDocument" @click="copyDocument(couponResult)">複製優惠券</el-button>
+            </el-form-item>
+        </el-form>
+    </div>
+</template>
+
+<script setup>
+import { reactive, ref, computed, getCurrentInstance, markRaw } from 'vue'
+import { Ticket, Plus, CloseBold, Warning, CopyDocument, SuccessFilled } from '@element-plus/icons'
+import { ElMessageBox } from 'element-plus'
+let { proxy } = getCurrentInstance()
+
+const clickCount = ref(0)
+const detailSettingFlag = ref(false)
+
+const showClick = ()=>{
+    clickCount.value++
+    if(clickCount.value == 10){
+        detailSettingFlag.value = !detailSettingFlag.value
+        clickCount.value = 0
+    }
+}
+
+const targetsCount = ref(0)
+
+const maxTakerFlag = ref(false)
+const consolidationFlag = ref(false)
+const loading = ref(false)
+const couponResult = ref('')
+const defaultTime = new Date().setTime(new Date().getTime() + (60*60*1000))
+const shortcuts = [
+    {
+        text: '一個月後',
+        value: () => {
+            const date = new Date()
+            date.setTime(date.getTime() + 30 * 3600 * 1000 * 24)
+            return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59)
+        },
+    },
+    {
+        text: '二個月後',
+        value: () => {
+            const date = new Date()
+            date.setTime(date.getTime() + 60 * 3600 * 1000 * 24)
+            return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59)
+        },
+    },
+    {
+        text: '三個月後',
+        value: () => {
+            const date = new Date()
+            date.setTime(date.getTime() + 90 * 3600 * 1000 * 24)
+            return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59)
+        },
+    },
+]
+const eventType= ref('')
+
+const setEventType = (type)=>{
+    switch(type){
+        case 'hiteach50-3':
+            crtCouponForm.couponType =  'Event'
+            crtCouponForm.eventName = 'hiteach50-3'
+            crtCouponForm.rule[0].q = [
+                {
+                    operator: '',
+                    type: 'GetWebIRS50EventType',
+                    how: '==',
+                    val: 'hiteach50'
+                },
+                {
+                    operator: '&&',
+                    type: 'GetWebIRS50TimeStep2',
+                    how: '!=',
+                    val:'-1'
+                },
+                {
+                    operator: '&&',
+                    type: 'GetWebIRS50TimeStep2',
+                    how: 'isnot null',
+                    val:''
+                }
+            ]
+            crtCouponForm.rule[0].b = '901003'
+            crtCouponForm.info[0].l = 'zh-tw' 
+            crtCouponForm.info[0].n = '教師自主增能三'
+            crtCouponForm.info[0].u = 'https://www.habook.com/zh-tw/event.php?act=view&id=170'
+            crtCouponForm.info[1].l = 'zh-cn'
+            crtCouponForm.info[1].n = '教师自主增能阶段三'
+            crtCouponForm.info[1].u = 'https://www.habook.com.cn/event.php?act=view&id=50'
+            crtCouponForm.info[2].l = 'en-us'
+            crtCouponForm.info[2].n = 'Self-enhancement Plan Stage 3'
+            crtCouponForm.info[2].u = 'https://www.habook.com/en/event.php?act=view&id=170'
+        break
+        case 'hiteach333-1':
+            crtCouponForm.couponType =  'Event'
+            crtCouponForm.eventName = 'hiteach333-1'
+            crtCouponForm.rule[0].q = [
+                {
+                    operator: '',
+                    type: 'GetWebIRS50TimeStep1',
+                    how: 'isnot null',
+                    val:''
+                }
+            ]
+            crtCouponForm.rule[0].b = '901003'
+            crtCouponForm.info[0].l = 'zh-tw' 
+            crtCouponForm.info[0].n = 'HiTeach 333 階段一'
+            crtCouponForm.info[0].u = 'https://www.habook.com/zh-tw/event.php?act=view&id=162'
+            crtCouponForm.info[1].l = 'zh-cn'
+            crtCouponForm.info[1].n = 'HiTeach 333 阶段一'
+            crtCouponForm.info[1].u = 'https://www.habook.com.cn/event.php?act=view&id=47'
+            crtCouponForm.info[2].l = 'en-us'
+            crtCouponForm.info[2].n = 'HiTeach 333 Stage 1'
+            crtCouponForm.info[2].u = 'https://www.habook.com/en/event.php?act=view&id=162'
+        break
+        case 'hiteach333-3':
+        crtCouponForm.couponType =  'Event'
+            crtCouponForm.eventName = 'hiteach333-3'
+            crtCouponForm.rule[0].q = [
+                {
+                    operator: '',
+                    type: 'GetWebIRS50EventType',
+                    how: '==',
+                    val: 'hiteach333'
+                },
+                {
+                    operator: '&&',
+                    type: 'GetWebIRS50TimeStep2',
+                    how: '!=',
+                    val:'-1'
+                },
+                {
+                    operator: '&&',
+                    type: 'GetWebIRS50TimeStep2',
+                    how: 'isnot null',
+                    val:''
+                }
+            ]
+            crtCouponForm.rule[0].b = '901003'
+            crtCouponForm.info[0].l = 'zh-tw' 
+            crtCouponForm.info[0].n = 'HiTeach 333 階段三'
+            crtCouponForm.info[0].u = 'https://www.habook.com/zh-tw/event.php?act=view&id=162'
+            crtCouponForm.info[1].l = 'zh-cn'
+            crtCouponForm.info[1].n = 'HiTeach 333 阶段三'
+            crtCouponForm.info[1].u = 'https://www.habook.com.cn/event.php?act=view&id=47'
+            crtCouponForm.info[2].l = 'en-us'
+            crtCouponForm.info[2].n = 'HiTeach 333 Stage 3'
+            crtCouponForm.info[2].u = 'https://www.habook.com/en/event.php?act=view&id=162'
+        break
+    }
+}
+
+const operatorList = [
+    { label: "或", val: "||" },
+    { label: "而且", val: "&&"}
+]
+const typeList = [
+    { label: "年資", val: "TenureID"},
+    { label: "T綠燈(Total)", val: "TGreen"},
+    { label: "T數據(Total)", val: "TData"},
+    { label: "T綠燈(月)", val: "TGreenMonth"},
+    { label: "T數據(月)", val: "TDataMonth"},
+    { label: "綁定手機號", val: "Mobile"},
+    { label: "綁定Mail", val: "Mail"},
+    { label: "取得WebIRS50的時間", val: "TimeForWebIRS50Get"},
+    { label: "取得智慧評分模組的時間", val: "TimeForSmartRatingGet"},
+    { label: "取得AI文字分析的時間", val: "TimeForAitextGet"},
+    { label: "取得蘇格拉底小數據的時間", val: "TimeForSokliteGet"},
+    { label: "333和50 第一階段取得的時間", val: "GetWebIRS50TimeStep1"},
+    { label: "333和50 第二階段取得的時間", val: "GetWebIRS50TimeStep2"},
+    { label: "333和50 第三階段取得的時間", val: "GetWebIRS50TimeStep3"},
+    { label: "取得參加333還是50", val: "GetWebIRS50EventType"},
+    { label: "是否完善進階資訊", val: "FillBaseEx" }
+]
+
+const howList = [
+    { label: "大於", val: ">"},
+    { label: "小於", val: "<"},
+    { label: "等於", val: "=="},
+    { label: "不等於", val: "!="},
+    { label: "大於等於", val: ">="},
+    { label: "小於等於", val: "<="},
+    { label: "是", val: "is"},
+    { label: "不是", val: "isnot"},
+    { label: "是空的", val: "is null"},
+    { label: "不是空的", val: "isnot null"}
+]
+
+const ruleBList = [
+    { label: "AI文字分析模組(展延)", val: "901001"},
+    { label: "AI蘇格拉底小數據(展延)", val: "901002"},
+    { label: "Web IRS 50人 (展延3個月)", val: "901003"},
+    { label: "Web IRS 50人 (延長1個月)", val: "901004"},
+    { label: "智慧評分 (一年)", val: "901005"},
+    { label: "HiTeachCC-6任務數(展延)", val: "903001"},
+    { label: "HiTeachCC-10任務數(展延)", val: "903002"},
+    { label: "HiTeachCC-100連線數(展延)", val: "903003"},
+    { label: "HiTeachCC-200連線數(展延)", val: "903004"},
+    { label: "小組協作(一年)", val: "901006"},
+    { label: "AI GPT(一年)", val: "901007"}
+]
+
+const crtCouponForm = reactive({
+    srvAdr: '',
+    couponType: "",
+    couponName: "",
+    expire: "",
+    eventName: "",
+    maxTaker: -1,
+    quantity: 0,
+    rule:[
+        {
+            // 條件
+            "q": [
+                {
+                    operator: '',
+                    type: '',
+                    how: '',
+                    val:''
+                },
+            ],
+            "b": "", // 給的權益
+            "p": "0", // 給的積分
+        }
+    ],
+    info: [
+        {
+            "l": "zh-tw",
+            "n": "",
+            "u": ""
+        },
+        {
+            "l": "zh-cn",
+            "n": "",
+            "u": ""
+        },
+        {
+            "l": "en-us",
+            "n": "",
+            "u": ""
+        }
+    ],
+    targets: ""
+})
+
+// 翻譯條件字串
+const translateRule = (q)=>{
+    let str = ''
+    q.forEach(e => {
+        
+        let o = operatorList.find(i=> i.val == e.operator)
+        if(o) str += o.label +' '
+
+        let t = typeList.find(i=> i.val == e.type)
+        if(t) str += t.label +' '
+
+        let h = howList.find(i=> i.val == e.how)
+        if(h) str += h.label +' '
+
+        if(e.how != 'is null' && e.how != 'isnot null') str += e.val +' '
+    });
+    return str
+}
+
+// 增加規則
+const addRule = (rIndex) => {
+    crtCouponForm.rule.splice(rIndex+1, 0,  {
+        "q": [
+            {
+                operator: '',
+                type: '',
+                how: '',
+                val:''
+            }
+        ],
+        "b": "",
+        "p": "0"
+    })
+}
+
+// 刪除規則
+const delRule = (rIndex) => {
+    crtCouponForm.rule.splice(rIndex, 1)
+}
+
+// 增加條件
+const addCondition = (rIndex, index) => {
+    crtCouponForm.rule[rIndex].q.splice(index+1, 0, {
+        operator: '',
+        type: '',
+        how: '',
+        val:''
+    })
+}
+
+// 刪除條件
+const delCondition = (rIndex, index) => {
+    crtCouponForm.rule[rIndex].q.splice(index, 1)
+}
+
+// 語系轉字串
+const langToName = (lang) => {
+    switch(lang){
+        case 'zh-tw':
+            return '繁體中文'
+        case 'zh-cn':
+            return '簡體中文'
+        case 'en-us':
+            return '英文'
+    }
+}
+
+// 跳轉url測試
+const linkToPage = (url) => {
+    try {        
+        if(url != ''){
+            var temp = new URL(url);
+            window.open(url, '_blank')
+        } else {
+            ElMessageBox.alert('請確認網址格式', '了解更多',
+                {
+                    type: 'warning',
+                    icon: markRaw(Warning),
+                }
+            )
+        }
+    } catch (err) {
+        ElMessageBox.alert('請確認網址格式', '了解更多',
+            {
+                type: 'warning',
+                icon: markRaw(Warning),
+            }
+        )
+    }
+}
+
+// 日曆元件限制設定
+const disabledDate = time => {
+    if(time.getTime() > (Date.now() + 7776000000)){ // 不能超過3個月
+        return true
+    } else if(time.getTime() < Date.now()- 3600 * 1000 * 24){
+        return true
+    }
+}
+
+// 時間轉型
+const convertDate = computed(()=>{
+    return function(val){
+        let date = new Date(val).getTime()
+        if(date.toString().substring(0,1) !== '9'){
+            if(date.toString().length < 13) date = date * 1000
+            return date ? new Date(parseInt(date)).toLocaleDateString() : ''
+        } else {
+            return null
+        }
+    }
+})
+
+const ruleFormRef = ref()
+
+const checkRule = (rule, value, callback) => {
+    let isCheck = false
+    value.forEach(e=>{
+        if(e.b == ""){
+            isCheck = true
+        }
+
+        e.q.forEach((q) => {
+            if(q.operator != ""){
+                isCheck = true
+            }
+            if(q.type != ""){
+                isCheck = true
+            }
+            if(q.how != ""){
+                isCheck = true
+            }
+            if(q.val != ""){
+                isCheck = true
+            }
+        })
+    })
+
+    if(isCheck){
+        let errC = 0
+        value.forEach(e=>{
+            if(e.b == ""){
+                errC++
+            }
+
+            e.q.forEach((q, i) => {
+                if(i != 0 && q.operator == ""){
+                    errC++
+                }
+                if(q.type == ""){
+                    errC++
+                }
+                if(q.how == ""){
+                    errC++
+                }
+                if(q.how != 'is null' && q.how != 'isnot null' && q.val == ""){
+                    errC++
+                }
+            })
+        })
+        if(errC > 0){
+            callback(new Error("請完成票券規則設定, 至少需要給予權益"))
+        }
+    }
+    callback()
+}
+
+const checkQuantity = (rule, value, callback) => {
+    if(!value && value < 1) {
+        callback(new Error("生成券數最少為1"))
+    }
+    callback()
+}
+
+const checkMaxTaker = (rule, value, callback) => {
+    if(maxTakerFlag.value){
+        if(value < 1){
+            callback(new Error("兌換上限不能小於1"))
+        }
+    }
+    callback()
+}
+
+const checkTargets = (rule, value, callback) => {
+    let isErr = false
+    if(value && !/^[0-9\n]+$/.test(value)){
+        isErr = true
+        callback(new Error("只能輸入醍摩豆ID與換行"))
+    } else if(consolidationFlag.value && !value) {
+        isErr = true
+        callback(new Error("請填入要歸戶的醍摩豆ID"))
+    } else if(value && isArrayRep(value.split('\n'))){
+        isErr = true
+        callback(new Error("醍摩豆ID有重複"))
+    } else if(value){
+        let errIds = []
+        let tmp = value.split('\n')
+        tmp.forEach(e=> {
+            if(e.length != 10) {
+                errIds.push(e)
+            } else {
+                let now = Math.floor(new Date().getTime() / 1000)
+                let orgId = parseInt(e)
+                if(parseInt(e.substring(0, 1)) >= 6){
+                    orgId -= 5000000000
+                }
+
+                if(orgId > now){
+                    errIds.push(e)
+                }
+            }
+        })
+
+        if(errIds.length > 0){
+            // console.log(errIds, 'errIds')
+            isErr = true
+            callback(new Error("請檢查醍摩豆ID是否符合格式或有空格"))
+        }
+    }
+
+    if(!isErr && value != ''){
+        let tArray = value.split('\n')
+        targetsCount.value = tArray.length
+    } else {
+        targetsCount.value = 0
+    }
+
+    callback()
+}
+
+function isArrayRep(array){
+    var result = new Set();
+    var repeat = new Set();
+    array.forEach(item => {
+        result.has(item) ? repeat.add(item) : result.add(item);
+    })
+    // console.log(result); // {1, 2, "a", 3, "b"}
+    // console.log(repeat); // {1, "a"}
+    // console.log(repeat.size)
+    if(repeat.size != 0){
+        return true
+    } else {
+        return false
+    }
+}
+
+const checkCupoInfo = (rule, value, callback) => {
+    let errC = 0
+    value.forEach(e => {
+        if(!e.n) errC ++
+        if(!e.u) errC ++
+    });
+    if(errC > 0){
+        callback(new Error("請完成票券資訊設定"))
+    } else {
+        callback()
+    }
+}
+
+const checkEventType = (rule, value, callback) => {    
+    if(eventType.value == '') callback(new Error("請選擇一個優惠券活動"))
+    callback()
+}
+
+const checkExpire = (rule, value, callback) => {
+    if(!value){
+        callback(new Error("請輸入到期時間"))
+    } else {
+        let timestemp = new Date(value).getTime()
+        console.log(timestemp)
+        console.log(Date.now())
+        console.log(timestemp - Date.now())
+        if(Math.floor((timestemp - Date.now())/1000) < 60 *60){
+            callback(new Error("限制時間至少要大於一小時"))
+        }
+    }
+    callback()
+}
+
+const rules = reactive({
+    couponType: [{required: true, trigger: "blur", message: '請選擇一個發券類型' }],
+    eventName: [{required: true, trigger: "blur", message: '請輸入活動名稱'}],
+    expire: [{required: true, validator: checkExpire, trigger: "blur"}],
+    maxTaker: [{validator: checkMaxTaker, trigger: "blur"}],
+    quantity: [{required: true,validator: checkQuantity, trigger: "blur"}],
+    rule: [{required: true, validator: checkRule, trigger: "blur"}],
+    info: [{required: true, validator: checkCupoInfo, trigger: "blur"}],
+    targets: [{required: true, validator: checkTargets, trigger: "blur"}],
+    srvAdr: [{required: true, trigger: "blur", message: '請選擇一個站別' }],
+    eventType: [{required: true, trigger: "blur", validator: checkEventType}],
+})
+
+const submitForm = formEl => {
+  if (!formEl) return
+  formEl.validate(async (valid, ddd) => {
+    if (valid) {
+        console.log("submit!")
+
+        ElMessageBox.confirm(
+            '確認建立此優惠券嗎?',
+            '',
+            {
+                type: 'info',
+                confirmButtonText: '我確定'
+            }
+        ).then(async ()=>{
+            loading.value = true
+        
+            let success = false
+            let data = JSON.parse(JSON.stringify(crtCouponForm))
+            data.expire = Math.floor( new Date(crtCouponForm.expire).getTime() / 1000)
+            crtCouponForm.rule.forEach((e, i)=>{
+                let qStr = ""
+                e.q.forEach(qe => {
+                    let values = Object.values(qe)
+                    values.forEach(v => {
+                        if(v != '') qStr += v + ' '
+                    })
+                })
+                data.rule[i].q = qStr
+            })
+            data.maxTaker = crtCouponForm.maxTaker == 0 ? -1 : crtCouponForm.maxTaker
+            data.targets = []
+            if(crtCouponForm.targets != ''){
+                data.targets = crtCouponForm.targets.split('\n')
+            }
+            
+            await proxy.$api.crtCoupon(data).then((res) => {
+                console.log(res)
+                couponResult.value = '' // 先清空
+                if(res.coupons){
+                    res.coupons.forEach(e=>{
+                        couponResult.value += (e+'\n')
+                    })
+                } else if(res.coupon){
+                    couponResult.value = res.coupon
+                }
+                success = true
+            }).catch(e=>{
+                ElMessageBox.alert('請確認優惠券設定是否合乎格式', '建立優惠券',
+                    {
+                        type: 'warning',
+                        icon: markRaw(Warning),
+                    }
+                )
+            }).finally(() => {
+                loading.value = false
+            })
+
+            if(consolidationFlag.value == true && success) {
+                loading.value = true
+                let consolidationData = {
+                    srvAdr: crtCouponForm.srvAdr,
+                    ids: crtCouponForm.targets.split('\n'),
+                    coupon: couponResult.value
+                }
+                await proxy.$api.consolidationCoupon(consolidationData).then((res) => {
+                    consolidationFlag.value = false
+                    console.log(res, '歸戶成功')
+                }).catch(e=>{
+                    ElMessageBox.alert('歸戶失敗', '歸戶',
+                        {
+                            type: 'warning',
+                            icon: markRaw(Warning),
+                        }
+                    )
+                }).finally(() => {
+                    loading.value = false
+                })
+            }
+
+            if(success){
+                ElMessageBox.alert('成功', '建立優惠券',
+                    {
+                        type: 'info',
+                        icon: markRaw(SuccessFilled),
+                    }
+                )
+                resetForm(formEl) // 欄位清除
+            }
+        })
+    } else {
+        console.log("error submit!")
+        return false
+    }
+  })
+}
+
+const resetForm = formEl => {
+    if (!formEl) return
+    formEl.resetFields()
+    crtCouponForm.info = [
+        {
+            "l": "zh-tw",
+            "n": "",
+            "u": ""
+        },
+        {
+            "l": "zh-cn",
+            "n": "",
+            "u": ""
+        },
+        {
+            "l": "en-us",
+            "n": "",
+            "u": ""
+        }
+    ]
+
+    crtCouponForm.rule = [
+        {
+            "q": [
+                {
+                    operator: '',
+                    type: '',
+                    how: '',
+                    val:''
+                }
+            ],
+            "b": "",
+            "p": "0"
+        }
+    ]
+
+    crtCouponForm.targets = ''
+    eventType.value = ''
+    consolidationFlag.value= false
+    crtCouponForm.couponType = ''
+}
+
+const copyDocument = (data) => {
+    navigator.clipboard.writeText(data)
+    .then(() => {
+        ElMessageBox.alert('複製成功', '複製優惠券',
+        {
+            type: 'info',
+            icon: markRaw(CopyDocument),
+        }
+    )
+    });
+}
+
+</script>
+
+<style scoped>
+.coupons {
+    box-shadow: 0px 3px 2px 2px #E0E0E0;
+    border-radius: 10px;
+    background-color: #ffffff;
+    display: flex;
+    width: 100%;
+    margin-bottom: 10px;
+    align-items: stretch;
+    font-family: "Noto Sans CJK TC", "serif";
+    max-width: 330px;
+    min-height: 100px;
+}
+.coupons-cont {
+    flex: 2;
+    padding: 8px 16px;
+    position: relative;
+    display: flex;
+    align-items: center;
+}
+.coupons-cont .hole {
+    position: absolute;
+    width: 20px;
+    height: 20px;
+    border-radius: 50%;
+    background-color: #ffffff;
+    left: -9px;
+    box-shadow: #e0e0e0 3px 0px 2px 0px;
+}
+.coupons-cont-box {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    justify-content: space-around;
+}
+.coupons-cont-box-title {
+    font-size: 16px;
+    font-weight: bold;
+    white-space: pre-line;
+    text-align: left;
+    line-height: 1.3;
+}
+.coupons-cont-box-link {
+    text-align: left;
+	font-size: 13px;
+}
+.coupons-cont-box-exp {
+    display: flex;
+    font-size: 12px;
+}
+.coupons-cont-box-exp.isExpiringSoon {
+    color: #f57c00;
+    font-weight: bold;
+}
+.coupons-btn {
+    position: relative;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    padding: 15px;
+    border-left: 1px dashed #9e9e9e;
+    background-color: #03a9f4;
+    color: #f3f3f3;
+    border-top-right-radius: 10px;
+    border-bottom-right-radius: 10px;
+    flex-direction: column;
+    flex: 1;
+}
+.coupons-btn.exchange {
+    background-color: #9e9e9e;
+}
+.coupons-btn-hole-positon {
+    position: absolute;
+    height: 100%;
+    width: 100%;
+    display: flex;
+    justify-content: flex-end;
+    align-items: center;
+}
+.coupons-btn-hole-positon .hole {
+    width: 20px;
+    height: 20px;
+    border-radius: 50%;
+    position: absolute;
+    background-color: #ffffff;
+    right: -9px;
+}
+.coupons-btn .icon-box {
+    border: 1px solid #fff;
+    display: flex;
+    border-radius: 50%;
+    padding: 4px;
+}
+ 
+</style>

+ 24 - 0
TEAMModelBI/ClientApp/src/view/issueCoupons/index.vue

@@ -0,0 +1,24 @@
+<template>
+    <div >
+      <el-tabs type="border-card">
+        <el-tab-pane label="建立優惠券"><CrteadCoupon /></el-tab-pane>
+      <el-tab-pane label="優惠券歸戶"><ConsolidationCoupon /></el-tab-pane>
+      <el-tab-pane label="通知"><Notice /></el-tab-pane>
+    </el-tabs>
+  </div>
+</template>
+<script>
+import CrteadCoupon from './crteadCoupon.vue'
+import ConsolidationCoupon from './consolidationCoupon.vue'
+import Notice from './notice.vue'
+export default {
+  components: {
+    CrteadCoupon,
+    ConsolidationCoupon,
+    Notice
+  },
+  setup () {
+
+  }
+}
+</script>

+ 181 - 0
TEAMModelBI/ClientApp/src/view/issueCoupons/notice.vue

@@ -0,0 +1,181 @@
+<template>
+    <div>
+        <el-form ref="ruleFormRef" :model="noticeForm" :rules="rules" label-width="120px">
+            <el-form-item label="站別" prop="srvAdr">
+                <el-radio-group v-model="noticeForm.srvAdr">
+                    <el-radio label="Global" size="large" border>國際站</el-radio>
+                    <el-radio label="China" size="large" border>大陸站</el-radio>
+                </el-radio-group>
+            </el-form-item>
+            <el-form-item label="標題" prop="title">
+                <el-input style="width: 500px;" v-model="noticeForm.title" placeholder="請輸入標題" />
+            </el-form-item>
+            <el-form-item label="內容" prop="body">
+                <el-input style="width: 500px;" :rows="6" type="textarea" v-model="noticeForm.body" placeholder="請輸入通知內容" />
+            </el-form-item>
+            <el-form-item label="醍摩豆ID" prop="targets">
+                <div style="display: flex;align-items: flex-end;">
+                    <el-input style="width: 300px;" v-model="noticeForm.targets" :rows="6" type="textarea" placeholder="請填入教師ID, 並用換行分隔" />
+                    <div style="margin-left: 5px;">總共 {{ targetsCount }} 位</div>
+                </div>
+            </el-form-item>
+            <el-form-item label="發送者" prop="sender">
+                <el-radio-group v-model="noticeForm.sender">
+                    <el-radio label="HiTeach" size="large" border>HiTeach</el-radio>
+                    <el-radio label="IES" size="large" border>IES</el-radio>
+                    <el-radio label="Sokrates" size="large" border>Sokrates</el-radio>
+                    <!-- <el-radio label="HiTA" size="large" border>HiTA</el-radio> -->
+                </el-radio-group>
+            </el-form-item>
+            <el-form-item>
+                <el-button type="primary" @click="submitForm(ruleFormRef)" :loading="loading">發送</el-button>
+                <el-button @click="resetForm(ruleFormRef)" :loading="loading">重置</el-button>
+            </el-form-item>
+        </el-form>
+    </div>
+</template>
+<script setup>
+import { reactive, ref, computed, getCurrentInstance, markRaw } from 'vue'
+import { Warning, SuccessFilled } from '@element-plus/icons'
+import { ElMessageBox } from 'element-plus'
+let { proxy } = getCurrentInstance()
+const targetsCount = ref(0)
+const ruleFormRef = ref()
+const loading = ref(false)
+const noticeForm = reactive({
+    srvAdr: '',
+    title: '',
+    body: '',
+    targets: "",
+    sender: '',
+    hubName: 'hita5' // 暫時固定
+})
+
+const checkTargets = (rule, value, callback) => {
+    let isErr = false
+    function isArrayRep(array){
+        var result = new Set();
+        var repeat = new Set();
+        array.forEach(item => {
+            result.has(item) ? repeat.add(item) : result.add(item);
+        })
+        // console.log(result); // {1, 2, "a", 3, "b"}
+        // console.log(repeat); // {1, "a"}
+        // console.log(repeat.size)
+        if(repeat.size != 0){
+            return true
+        } else {
+            return false
+        }
+    }
+    if(value && !/^[0-9\n]+$/.test(value)){
+        isErr = true
+        callback(new Error("只能輸入醍摩豆ID與換行"))
+    } else if(value && isArrayRep(value.split('\n'))){
+        isErr = true
+        callback(new Error("醍摩豆ID有重複"))
+    } else if(value){
+        let errIds = []
+        let tmp = value.split('\n')
+        tmp.forEach(e=> {
+            if(e.length != 10) {
+                errIds.push(e)
+            } else {
+                let now = Math.floor(new Date().getTime() / 1000)
+                let orgId = parseInt(e)
+                if(parseInt(e.substring(0, 1)) >= 6){
+                    orgId -= 5000000000
+                }
+
+                if(orgId > now){
+                    errIds.push(e)
+                }
+            }
+        })
+
+        if(errIds.length > 0){
+            // console.log(errIds, 'errIds')
+            isErr = true
+            callback(new Error("請檢查醍摩豆ID是否符合格式或有空格"))
+        }
+    }
+
+    if(!isErr && value != ''){
+        let tArray = value.split('\n')
+        targetsCount.value = tArray.length
+    } else {
+        targetsCount.value = 0
+    }
+
+    callback()
+}
+
+const rules = reactive({
+    srvAdr: [{required: true, trigger: "blur", message: '請選擇一個站別' }],
+    title: [{required: true, trigger: "blur", message: '請填寫標題' }],
+    body: [{required: true, trigger: "blur", message: '請填寫通知內容' }],
+    targets: [{required: true, validator: checkTargets, trigger: "blur"}],
+    sender: [{required: true, trigger: "blur", message: '請選擇一個產品接收'}],
+})
+
+const submitForm = formEl => {
+  if (!formEl) return
+  formEl.validate(async (valid, ddd) => {
+    if (valid) {
+
+        ElMessageBox.confirm(
+            '發送此通知?',
+            '',
+            {
+                type: 'info',
+                confirmButtonText: '發送'
+            }
+        ).then(async ()=>{
+            let request = {
+                srvAdr: noticeForm.srvAdr,
+                title: noticeForm.title,
+                body: noticeForm.body,
+                tags: [],
+                sender: noticeForm.sender,
+                hubName: noticeForm.hubName,
+            }
+            
+            noticeForm.targets.split('\n').forEach(e =>{
+                request.tags.push(e + '_' +request.sender)
+            })
+            console.log(request)
+
+            loading.value = true
+            await proxy.$api.pushNotify(request).then((res) => {
+                console.log(res, '發送成功')
+                ElMessageBox.alert('成功', '發送通知',
+                    {
+                        type: 'info',
+                        icon: markRaw(SuccessFilled),
+                    }
+                )
+                resetForm(formEl) // 欄位清除
+            }).catch(e=>{
+                ElMessageBox.alert('失敗', '發送通知',
+                    {
+                        type: 'warning',
+                        icon: markRaw(Warning),
+                    }
+                )
+            }).finally(() => {
+                loading.value = false
+            })
+        })
+    } else {
+        console.log("error submit!")
+        return false
+    }
+  })
+}
+
+const resetForm = formEl => {
+    if (!formEl) return
+    formEl.resetFields()
+}
+
+</script>

+ 205 - 0
TEAMModelBI/Controllers/BICommon/BICouponController.cs

@@ -0,0 +1,205 @@
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Options;
+using System.Net.Http;
+using TEAMModelOS.SDK.DI;
+using TEAMModelOS.SDK;
+using TEAMModelOS.Models;
+using System.Text.Json;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+using System.Text;
+using TEAMModelOS.SDK.Extension;
+using System;
+using System.Net.Http.Json;
+using System.ComponentModel.DataAnnotations;
+using System.Net;
+using System.Net.Http.Headers;
+using Newtonsoft.Json;
+using System.Text.Encodings.Web;
+
+namespace TEAMModelBI.Controllers.BICommon
+{
+    [Route("coupon")]
+    [ApiController]
+    public class BICouponController : ControllerBase
+    {
+        private readonly DingDing _dingDing;
+        private readonly Option _option;
+        private readonly IConfiguration _configuration;
+        private readonly AzureServiceBusFactory _serviceBus;
+        private readonly IHttpClientFactory _http;
+        private readonly CoreAPIHttpService _coreAPIHttpService;
+        private readonly IWebHostEnvironment _environment; //读取文件
+        private readonly HttpClient _httpClient;
+
+        public BICouponController(DingDing dingDing, IOptionsSnapshot<Option> option, IConfiguration configuration, AzureServiceBusFactory serviceBus, IHttpClientFactory http, CoreAPIHttpService coreAPIHttpService, IWebHostEnvironment hostingEnvironment, HttpClient httpClient)
+        {
+            _dingDing = dingDing;
+            _option = option?.Value;
+            _configuration = configuration;
+            _serviceBus = serviceBus;
+            _http = http;
+            _coreAPIHttpService = coreAPIHttpService;
+            _environment = hostingEnvironment;
+            _httpClient = httpClient;
+        }
+
+        /// <summary>
+        /// 建立優惠券
+        /// </summary>
+        /// <param name="GenerateCouponRequest"></param>
+        /// <returns></returns>
+        //[AuthToken(Roles = "admin,rdc,assist,sales")]
+        [HttpPost("create-coupon")]
+        public async Task<IActionResult> CreateCoupon(GenerateCouponRequest request)
+        {
+            try
+            {
+                //string url = _configuration.GetValue<string>("HaBookAuth:CoreAPI");
+                string url = "https://api2.teammodel.net";
+                if (request.srvAdr == "China") url = "https://api2.teammodel.cn";
+                
+                string AccessToken = await getCoreAccessToken();
+                var client = _http.CreateClient();
+                client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", AccessToken);
+                HttpResponseMessage response = await client.PostAsJsonAsync($"{url}/Service/GenerateCoupon", request);
+                if (response.StatusCode == HttpStatusCode.OK)
+                {
+                    string jsonStr = await response.Content.ReadAsStringAsync();
+                    var options1 = new JsonSerializerOptions
+                    {
+                        Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
+                    };
+
+                    await _dingDing.SendBotMsg(System.Text.Json.JsonSerializer.Serialize(request, options1), GroupNames.高飛);
+                    return Ok(jsonStr.ToObject<JsonElement>());
+                }
+                else {
+                    return BadRequest();
+                }
+            }
+            catch (Exception ex) {
+                return BadRequest();
+            }
+        }
+
+        /// <summary>
+        /// 歸戶
+        /// </summary>
+        /// <param name="GenerateCouponRequest"></param>
+        /// <returns></returns>
+        //[AuthToken(Roles = "admin,rdc,assist,sales")]
+        [HttpPost("consolidation-coupon")]
+        public async Task<IActionResult> ConsolidationCoupon(ConsolidationCouponRequest request)
+        {
+            try
+            {
+                //string url = _configuration.GetValue<string>("HaBookAuth:CoreAPI");
+                string url = "https://api2.teammodel.net";
+                if (request.srvAdr == "China") url = "https://api2.teammodel.cn";
+                string AccessToken = await getCoreAccessToken();
+                var client = _http.CreateClient();
+                client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", AccessToken);
+                HttpResponseMessage response = await client.PostAsJsonAsync($"{url}/Service/CouponConsolidation", request);
+                if (response.StatusCode == HttpStatusCode.OK)
+                {
+                    return Ok(new { state = 0, msg = "歸戶成功" });
+                }
+                else
+                {
+                    return BadRequest();
+                }
+            }
+            catch (Exception ex)
+            {
+                return BadRequest();
+            }
+        }
+
+        /// <summary>
+        /// 通知
+        /// </summary>
+        /// <param name="GenerateCouponRequest"></param>
+        /// <returns></returns>
+        //[AuthToken(Roles = "admin,rdc,assist,sales")]
+        [HttpPost("push-notify")]
+        public async Task<IActionResult> PushNotify(PushNotifyRequest request)
+        {
+            try
+            {
+                //string url = _configuration.GetValue<string>("HaBookAuth:CoreAPI");
+                string url = "https://api2.teammodel.net";
+                if (request.srvAdr == "China") url = "https://api2.teammodel.cn";
+                string AccessToken = await getCoreAccessToken();
+                var client = _http.CreateClient();
+                client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", AccessToken);
+                HttpResponseMessage response = await client.PostAsJsonAsync($"{url}/Service/PushNotify", request);
+                if (response.StatusCode == HttpStatusCode.OK)
+                {
+                    return Ok(new { state = 0, msg = "發送成功" });
+                }
+                else
+                {
+                    return BadRequest();
+                }
+            }
+            catch (Exception ex)
+            {
+                return BadRequest();
+            }
+        }
+
+        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 _http.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 class info
+    {
+        public string l { get; set; }
+        public string n { get; set; }
+        public string u { get; set; }
+    }
+    public class CouponRule
+    {
+        public string q { get; set; }
+        //可獲得的權益
+        public string b { get; set; }
+        //若已有權益 則給積分
+        public int p { get; set; } = 0;
+    }
+
+    public record GenerateCouponRequest([Required] string srvAdr, [Required] string CouponType, List<string> targets, string couponName, int quantity, [Required] long expire, [Required] List<CouponRule> rule, string eventName, List<info> info, int maxTaker = -1);
+    public record ConsolidationCouponRequest([Required] string srvAdr, [Required] string coupon, [Required] List<string> ids);
+    public record PushNotifyRequest([Required] string srvAdr, [Required] string title, [Required] string body, [Required] List<string> tags, [Required] string sender, [Required] string hubName);
+}

+ 4 - 13
TEAMModelOS.SDK/DI/DingDing/DingDing.cs

@@ -74,19 +74,8 @@ namespace TEAMModelOS.SDK.DI
                 atMobiles=mobiles;
             }
             var content = new { msgtype = "text", text = new { content = msg }, at=new { atMobiles  } };
-#if DEBUG
-            string[] keys = null; 
-            if (groupkey.Equals(GroupNames.醍摩豆小财神))
-            {
-                  keys = GroupNames.醍摩豆小财神.GetDescriptionText().Split(',');
-            }
-            else
-            {
-                  keys = GroupNames.成都开发測試群組.GetDescriptionText().Split(',');
-            }
-#else
+
             var keys = groupkey.GetDescriptionText().Split(',');
-#endif
             if (keys.Length == 1) await _httpClient.PostAsJsonAsync($"{url}{keys[0]}", content);
             else
             {
@@ -156,6 +145,8 @@ namespace TEAMModelOS.SDK.DI
         [Description("b1293e05c6aaeece746a2e46d69164a4373bab071bfafb5d7b5141f947e493cb,SEC658c7c70204f18976fa5e02f554d4fc72e9892c9bb82c5c6b98ecfd3c4eb0531")]
         大陸客戶聯繫通知群, 
         [Description("feac70431e5b3cf68c621c5397ca62c573499275557335f0d140e05c3e2437fa,SECb365526c7d7a3fd230f8f1c8ee82b869db9a3fa81d035233b5350028d19c527b")]
-        醍摩豆小财神
+        醍摩豆小财神,
+        [Description("8e3c5efdf8ad02eb44584dfd08ee66cfa9d31860af51670df69fc9d7bb55f3bb,SECb1267571406d89d2ac451a11495be417d3cbfe2b04ed7f1742d28e1a1d098c9d")]
+        高飛
     }
 }

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

@@ -7849,5 +7849,21 @@ const LANG_ZH_CN = {
         addNewTable:'新增"新"成绩表',
         InheritTable:'沿用成绩表',
         addTableNotice:'注意:新增的成绩表成绩以当下系统有的成绩为准,成绩表成绩没有跟其他功能连动',          
-    }
+    },
+    activity: {
+        scoreWord: {
+            only: '按评审分数',
+            avg: '按平均分',
+            top: '按最高分',
+            rmLowAvg: '去掉最低分的平均分',
+            rmTopAvg: '去掉最高分的平均分',
+            rmLowTopAvg: '去掉最高分和最低分的平均分',
+        },
+        distributeWord: {
+            none: '不需要匹配',
+            period: '只匹配学段',
+            subject: '只匹配学科',
+            periodAndSubject: '同时匹配学科和学段',
+        },
+    },
 }

+ 4 - 4
TEAMModelOS/ClientApp/src/view/dashboardCenter/DashboardCenter.vue

@@ -56,13 +56,13 @@
 	export default {
 		methods: {
 			goDash(val) {
+				let isGlobalSite = this.$store.state.config.srvAdr !== "China";
 				switch (val) {
 					case "school":
 						this.$router.push("schoolDash");
 						break;
 					case "teacher":
 						// 判断是否为全局站点,以及是否有教师权限
-						let isGlobalSite = this.$store.state.config.srvAdr !== "China";
 						let hasTeacherAuth = localStorage.edition === 'pro'
 						// 如果有教师权限或者为全局站点,则跳转到研究仪表盘页面
 						if(hasTeacherAuth || isGlobalSite){
@@ -84,10 +84,10 @@
 						}
 						break;
 					case "schooliot":
-						// 判断是否有物联网权限
+						// 判断是否为全局站点,以及是否有物联网权限
 						let hasIotAuth = localStorage.edition === 'standard' || localStorage.edition === 'pro'
-						// 如果有物联网权限,则跳转到物联网平台页面
-						if(hasIotAuth){
+						// 如果有物联网权限或者为全局站点,则跳转到物联网平台页面
+						if (hasIotAuth || isGlobalSite){
 							this.$router.push("schooliot");
 						// 否则弹出警告信息
 						}else{

+ 2 - 1
TEAMModelOS/ClientApp/src/view/signupActivity/createActivity.vue

@@ -295,7 +295,7 @@
             </vuescroll>
         </div>
         <Drawer title="评审规则" :width="50" :mask-closable="false" v-model="ruleDrawer" class="light-iview-form" @on-close="closeRule()">
-            <RuleDrawer :ruleDrawerAdd="ruleDrawerAdd" :ruleInfoParent="ruleInfo" @saveRule="saveRule" :needEdit="ruleDrawerAdd === 1" />
+            <RuleDrawer :ruleDrawerAdd="ruleDrawerAdd" :ruleInfoParent="ruleInfo" @saveRule="saveRule" :needEdit="ruleDrawerAdd === 1 ? 2 : 0" />
         </Drawer>
         <Modal v-model="modalType" title="自定义填报信息" class="light-iview-form" @on-ok="addInform()">
             <Form :label-width="100" :model="addInfoForm">
@@ -855,6 +855,7 @@ export default {
                             this.areaSchools = res.schools
                             this.showSchools = this.areaSchools
                         }
+                        this.showSchools = this.$route.params.id ? (this.createData.scope === 'area' ? this.areaSchools : this.allSchools) : (this.isTMD ? this.allSchools : this.areaSchools)
                     }
                 })
             }

+ 7 - 7
TEAMModelOS/ClientApp/src/view/signupActivity/infoComponent/ruleDrawer.vue

@@ -125,9 +125,9 @@ export default {
                 return undefined
             }
         },
-        needEdit: {
-            type: Boolean,
-            default: false,
+        needEdit: { //0:不编辑 1:活动详情中修改 2:创建/编辑中修改
+            type: Number,
+            default: 0,
         },
     },
     data () {
@@ -351,7 +351,7 @@ export default {
         },
         // 编辑完成后保存
         saveEdit() {
-            if(this.needEdit) {
+            if(this.needEdit === 2) {
                 if(this.ruleInfo.haveSame) {
                     this.$Modal.confirm({
                         title: '是否同时修改模板内容',
@@ -367,8 +367,8 @@ export default {
                 } else {
                     this.$emit('saveRule', {info: this.ruleInfo, isEdit: true})
                 }
-                // this.$emit('saveRule', {info: this.ruleInfo, isEdit: true})
-                /* let params = {
+            } else if(this.needEdit === 1) {
+                let params = {
                     grant_type: 'rule-update',
                     reviewConfig: this.ruleInfo
                 }
@@ -382,7 +382,7 @@ export default {
                     } else {
                         this.$Message.warning('编辑失败')
                     }
-                }) */
+                })
             }
         },
         cancelEdit() {

+ 361 - 155
TEAMModelOS/ClientApp/src/view/signupActivity/infoComponent/skContent.vue

@@ -1,134 +1,173 @@
 <template>
     <div class="sk-content">
-        <div class="data-box">
-            <div class="module-title">报名数据</div>
-            <div class="module-data">
-                <div class="tab-header join-data">
-                    <!-- <div>
-                        <p>报名方式</p>
-                        <p>报名制</p>
-                    </div> -->
-                    <div>
-                        <p>参赛方式</p>
-                        <p>{{ contestInfo.sign.type ? '团队赛' : '个人赛' }}</p>
-                    </div>
-                    <div>
-                        <p>已报名</p>
-                        <p style="color: #14b53b;">
-                            {{ '-' }}
-                            <span style="font-size: 16px; color: #737373;">/{{ contestInfo.sign.limit || '不限' }}</span>
-                        </p>
-                    </div>
-                    <div>
-                        <p>已上传</p>
-                        <p style="color: #4b93ff;">{{ '-' }}</p>
+        <div class="sk-info">
+            <div class="data-box" id="sign-position">
+                <div class="module-title">报名数据</div>
+                <div class="module-data">
+                    <div class="tab-header join-data">
+                        <!-- <div>
+                            <p>报名方式</p>
+                            <p>报名制</p>
+                        </div> -->
+                        <div>
+                            <p>参赛方式</p>
+                            <p>{{ contestInfo.sign.type ? '团队赛' : '个人赛' }}</p>
+                        </div>
+                        <div>
+                            <p>已报名</p>
+                            <p style="color: #14b53b;">
+                                {{ '-' }}
+                                <span style="font-size: 16px; color: #737373;">/{{ contestInfo.sign.limit || '不限' }}</span>
+                            </p>
+                        </div>
+                        <div>
+                            <p>已上传</p>
+                            <p style="color: #4b93ff;">{{ '-' }}</p>
+                        </div>
+                        <div>
+                            <p>已评审</p>
+                            <p style="color: #ff9900;">{{ '-' }}</p>
+                        </div>
+                        <!-- <div>
+                            <p>成绩发布</p>
+                            <p style="color: #5f5f5f;">未发布</p>
+                        </div> -->
                     </div>
-                    <div>
-                        <p>已评审</p>
-                        <p style="color: #ff9900;">{{ '-' }}</p>
+                    <div style="margin-bottom: 10px;">
+                        <Input placeholder="搜索醍摩豆帐号" style="width: 300px; margin-right: 10px;" />
+                        <Button type="success" @click="getTeacherList()" v-show="actInfo.joinMode === 'invite' && !isArea">邀请老师</Button>
                     </div>
-                    <!-- <div>
-                        <p>成绩发布</p>
-                        <p style="color: #5f5f5f;">未发布</p>
-                    </div> -->
-                </div>
-                <div style="margin-bottom: 10px;">
-                    <Input placeholder="搜索醍摩豆帐号" style="width: 300px; margin-right: 10px;" />
-                    <Button type="success" @click="getTeacherList()" v-show="actInfo.joinMode === 'invite' && !isArea">邀请老师</Button>
-                </div>
-                <Table :columns="applicationColumns" :data="applicationList" stripe row-key="id">
-                    <template #head="{}">
-                        <span> </span>
-                    </template>
-                    <template #poster="{row}">
-                        <PersonalPhoto :name="row.name || row.iname" :picture="row.picture" />
-                    </template>
-                    <template #joinStatus="{row}">
-                        <Tag color="success" v-if="row.inviteStatus && row.signContestStatus === 1">已报名</Tag>
-                        <template v-else-if="!row.inviteStatus">
-                            <Tag color="green">已邀请</Tag>
-                            <Tag color="orange" v-show="!row.signContestStatus">未报名</Tag>
-                            <Tag color="default" v-show="row.signContestStatus === -2">未到报名时间</Tag>
+                    <Table :columns="applicationColumns" :data="applicationList" stripe row-key="id" height="300">
+                        <template #head="{}">
+                            <span> </span>
                         </template>
-                        <span v-else-if="row.inviteStatus != -2">-</span>
-                    </template>
-                    <template #uploadContestType="{row}">
-                        <span>{{ row.uploadContestType === 'file' ? '文件' : (row.uploadContestType ? '苏格拉底' : '-') }}</span>
-                    </template>
-                    <template #uploadContestScore="{row}">
-                        <span>{{ row.uploadContestScore === -1 ? '-' : row.uploadContestScore }}</span>
-                    </template>
-                    <template #action="{row}">
-                        <Button type="success" size="small" style="margin-right: 10px;" @click="getTeaInfo(row)" v-show="row.inviteStatus != -2">查看</Button>
-                        <!-- <Button type="error" size="small" @click="deleteApplica(row, index, 'join')">删除</Button> -->
-                    </template>
-                </Table>
-            </div>
-        </div>
-        <template v-if="actInfo.scope != 'school' && isArea || actInfo.scope === 'school'">
-            <div class="data-box">
-                <div class="module-title">评审管理</div>
-                <div class="module-data">
-                    <div class="tab-header">
-                        <Button @click="processShow = true">添加评审专家</Button>
-                        <Button style="margin-left: 20px;" @click="workPro = true">自动分配评审作品</Button>
-                        <Button style="float: right;" @click="ruleDrawer = true">评审规则</Button>
-                    </div>
-                    <Table :columns="processColumns" :data="processList">
-                        <template #subjects="{row}">
-                            <span v-for="(item, index) in row.subjects" :key="index">
-                                {{ item.period }} - {{ item.subject }}
-                            </span>
+                        <template #poster="{row}">
+                            <PersonalPhoto :name="row.name || row.iname" :picture="row.picture" />
+                        </template>
+                        <template #joinStatus="{row}">
+                            <Tag color="blue" v-if="row.inviteStatus && row.signContestStatus === 1">已报名</Tag>
+                            <template v-else-if="!row.inviteStatus">
+                                <Tag color="green">已邀请</Tag>
+                                <Tag color="orange" v-show="!row.signContestStatus">未报名</Tag>
+                                <Tag color="default" v-show="row.signContestStatus === -2">未到报名时间</Tag>
+                            </template>
+                            <span v-else-if="row.inviteStatus != -2">-</span>
+                            <Tag color="orange" v-show="!row.reviewContestAssignCount">未分配</Tag>
+                        </template>
+                        <template #uploadContestType="{row}">
+                            <span>{{ row.uploadContestType === 'file' ? '文件' : (row.uploadContestType ? '苏格拉底' : '-') }}</span>
                         </template>
-                        <template #process="{}">
-                            <Progress :percent="25" :stroke-width="10" />
+                        <template #uploadContestScore="{row}">
+                            <span>{{ row.uploadContestScore === -1 ? '-' : row.uploadContestScore }}</span>
                         </template>
-                        <template #actions="{row, index}">
-                            <Button type="error" size="small" @click="delProInfo(row, index)">删除</Button>
+                        <template #action="{row}">
+                            <Button type="success" size="small" style="margin-right: 10px;" @click="getTeaInfo(row)" v-show="row.inviteStatus != -2">查看</Button>
+                            <!-- <Button type="error" size="small" @click="deleteApplica(row, index, 'join')">删除</Button> -->
                         </template>
                     </Table>
                 </div>
             </div>
-            <div class="data-box">
-                <div class="module-title">成绩统计</div>
-                <div class="module-data">
-                    <div class="tab-header">
-                        <div v-show="!awardsing">
-                            <Button @click="setAwards()">设置奖项</Button>
-                            <Button @click="openAch()" style="margin-left: 20px;">公示成绩</Button>
+            <template v-if="actInfo.scope != 'school' && isArea || actInfo.scope === 'school'">
+                <div class="data-box" id="review-position">
+                    <div class="module-title">评审管理</div>
+                    <div class="module-data">
+                        <div class="tab-header join-data">
+                            <div>
+                                <p>评审次数</p>
+                                <p>{{ ruleInfo.taskCount }}次</p>
+                            </div>
+                            <div>
+                                <p>作品分配</p>
+                                <p>{{ $t(`activity.distributeWord[${ruleInfo.distribute}]`) }}</p>
+                            </div>
+                            <div>
+                                <p>评审方式</p>
+                                <p>{{ ruleInfo.scoreDetail ? '对细项评分' : '只打总分' }}</p>
+                            </div>
+                            <div>
+                                <p>统分规则</p>
+                                <p>{{ $t(`activity.scoreWord[${ruleInfo.scoreRule}]`) }}</p>
+                            </div>
                         </div>
-                        <div v-show="awardsing">
-                            <Button @click="awardsShow = true">批量设置</Button>
-                            <Button @click="awardTypes()" style="margin-left: 20px;">取消</Button>
+                        <div class="tab-header">
+                            <Button style="margin-right: 20px;" @click="processShow = true">添加评审专家</Button>
+                            <Button @click="allocationWork()">自动分配评审作品</Button>
+                            <!-- 根据review reviewStatus为1表示专家可以评审 -->
+                            <Button style="margin-left: 20px;">开始评审</Button>
+                            <!-- 专家列表中taskCount判断是否已经分配作品 -->
+                            <Button style="float: right;" @click="ruleDrawer = true">评审规则</Button>
                         </div>
+                        <Table :columns="processColumns" :data="processList">
+                            <template #name="{row}">
+                                <div>
+                                    <span>{{ row.iname }}({{ row.name }})</span>
+                                    <Tooltip content="该账号无效" v-show="row.tmdid && !row.id" transfer>
+                                        <Icon type="ios-alert" color="orange" />
+                                    </Tooltip>
+                                    <Tooltip content="该账号未注册" v-show="(row.email || row.mobile) && !row.tmdid && !row.id">
+                                        <Icon type="ios-alert" color="orange" />
+                                    </Tooltip>
+                                </div>
+                            </template>
+                            <template #subjects="{row}">
+                                <span v-for="(item, index) in row.subjects" :key="index">
+                                    {{ item.period }} - {{ item.subject }}
+                                </span>
+                            </template>
+                            <template #taskCount="{row}">
+                                <span>{{ row.taskCount === -1 ? '-' : row.taskCount }}</span>
+                            </template>
+                            <template #process="{row}">
+                                <div v-if="row.progress === -1">-</div>
+                                <Progress :percent="row.progress" :stroke-width="10" v-else />
+                            </template>
+                            <template #actions="{row, index}">
+                                <Button type="error" size="small" @click="delProInfo(row, index)">删除</Button>
+                            </template>
+                        </Table>
                     </div>
-                    <Table :columns="scoreColumns" :data="scoreList" height="600">
-                        <template #score1="{row}">
-                            <div v-show="!row.edit">
-                                <span>{{ row.score1 }}</span>
-                                <Icon type="md-nutrition" @click="row.edit = true" />
+                </div>
+                <div class="data-box" id="score-position">
+                    <div class="module-title">成绩统计</div>
+                    <div class="module-data">
+                        <div class="tab-header">
+                            <div v-show="!awardsing">
+                                <Button @click="setAwards()">设置奖项</Button>
+                                <Button @click="openAch()" style="margin-left: 20px;">公示成绩</Button>
                             </div>
-                            <div v-show="row.edit">
-                                <InputNumber :min="0" v-model="row.score1" />
-                                <Icon type="md-checkmark-circle" @click="row.edit = false" />
-                                <Icon type="md-close-circle" @click="row.edit = false" />
+                            <div v-show="awardsing">
+                                <Button @click="awardsShow = true">批量设置</Button>
+                                <Button @click="awardTypes()" style="margin-left: 20px;">取消</Button>
                             </div>
-                        </template>
-                        <template #awards="{row}">
-                            <span v-if="!awardsing">{{ row.awards ? row.awards : '-' }}</span>
-                            <div v-else>
-                                <Select v-model="row.awards" style="width:200px" :transfer="true">
-                                    <Option v-for="item in awardsList" :value="item.value" :key="item.value">{{ item.label }}</Option>
-                                </Select>
-                                <Icon type="md-add-circle" @click="addAward = true" />
-                            </div>
-                        </template>
-                    </Table>
+                        </div>
+                        <Table :columns="scoreColumns" :data="scoreList" height="600">
+                            <template #score1="{row}">
+                                <div v-show="!row.edit">
+                                    <span>{{ row.score1 }}</span>
+                                    <Icon type="md-nutrition" @click="row.edit = true" />
+                                </div>
+                                <div v-show="row.edit">
+                                    <InputNumber :min="0" v-model="row.score1" />
+                                    <Icon type="md-checkmark-circle" @click="row.edit = false" />
+                                    <Icon type="md-close-circle" @click="row.edit = false" />
+                                </div>
+                            </template>
+                            <template #awards="{row}">
+                                <span v-if="!awardsing">{{ row.awards ? row.awards : '-' }}</span>
+                                <div v-else>
+                                    <Select v-model="row.awards" style="width:200px" :transfer="true">
+                                        <Option v-for="item in awardsList" :value="item.value" :key="item.value">{{ item.label }}</Option>
+                                    </Select>
+                                    <Icon type="md-add-circle" @click="addAward = true" />
+                                </div>
+                            </template>
+                        </Table>
+                    </div>
                 </div>
-            </div>
-        </template>
+            </template>
+        </div>
         <Drawer title="评审规则" :width="50" v-model="ruleDrawer">
-            <RuleDrawer :ruleDrawerAdd="0" :ruleInfoParent="ruleInfo" @saveRule="saveRule" :needEdit="true" />
+            <RuleDrawer :ruleDrawerAdd="0" :ruleInfoParent="ruleInfo" @saveRule="saveRule" :needEdit="!isAllocation ? 1 : 0" />
         </Drawer>
         <Drawer title="教师报名信息" :width="25" v-model="teaDrawer">
             <Form :model="teaSignInfo" :label-width="80">
@@ -174,12 +213,24 @@
                 <Option v-for="item in awardsList" :value="item.value" :key="item.value">{{ item.label }}</Option>
             </Select>
         </Modal>
-        <Modal v-model="workPro" title="自动分配评审作品">
-            <p>
-                每个作品被评审次数
-                <InputNumber v-model="workProNum" controls-outside />
-            </p>
-            <p>系统将根据参赛作品数量和评审专家数量,随机将作品分配给评审专家。</p>
+        <Modal v-model="isAllocation" title="自动分配评审作品" width="1000" :mask-closable="false" :footer-hide="true">
+            <Alert type="warning" show-icon>
+                已为专家预分配作品,若无调整,请保存分配结果,作品才会真正分配给专家
+            </Alert>
+            <Table :columns="alloWorkCloumns" :data="alloWorkList" height="450">
+                <template #name="{row}">
+                    {{ row.name }}
+                    <Tooltip content="未上传作品" v-show="row.errInfo" transfer>
+                        <Icon type="ios-alert" color="orange" />
+                    </Tooltip>
+                </template>
+                <template #expert="{row, column}">
+                    <span v-show="row[column.expertId] === column.expertId">
+                        <Icon type="md-checkbox" color="green" size="20" />
+                    </span>
+                </template>
+            </Table>
+            <Button type="success" long @click="saveAllocationResults()" style="margin-top: 20px;">保存</Button>
         </Modal>
         <Modal v-model="addAward" title="新增奖项" @on-ok="addAwardOK()">
             <Input v-model="addAwardWord" placeholder="Enter something..." style="width: 300px" />
@@ -193,7 +244,7 @@ import RuleDrawer from './ruleDrawer.vue'
 export default {
     components: {
         expertImport,
-        RuleDrawer
+        RuleDrawer,
     },
     props: {
         actInfo: {
@@ -212,6 +263,14 @@ export default {
             type: Object,
             default: {},
         },
+        changeRule: {
+            type: Function,
+            default: () => () => {}
+        },
+        changeLoad: {
+            type: Function,
+            default: () => () => {}
+        },
     },
     data () {
         return {
@@ -292,36 +351,42 @@ export default {
             ],
             applicationList: [],
             processShow: false,
-            workPro: false,
             ruleDrawer: false,
             processColumns: [
                 {
                     title: '姓名',
-                    key: 'name'
+                    slot: 'name',
+                    align: 'center',
                 },
                 {
                     title: '职称',
-                    key: 'title'
+                    key: 'title',
+                    align: 'center',
                 },
                 {
                     title: '手机号码',
-                    key: 'mobile'
+                    key: 'mobile',
+                    align: 'center',
                 },
                 {
                     title: '醍摩豆ID',
-                    key: 'tmdid'
+                    key: 'tmdid',
+                    align: 'center',
                 },
                 {
                     title: '电子邮箱',
-                    key: 'email'
+                    key: 'email',
+                    align: 'center',
                 },
                 {
                     title: '评审科目',
-                    slot: 'subjects'
+                    slot: 'subjects',
+                    align: 'center',
                 },
                 {
                     title: '评审数量',
-                    key: 'num'
+                    slot: 'taskCount',
+                    align: 'center',
                 },
                 {
                     title: '评审进度',
@@ -476,6 +541,24 @@ export default {
                 period: '学段',
                 subject: '学科',
             },
+            isAllocation: false,
+            alloWorkCloumns: [],
+            alloWorkList: [],
+            taskKey: null,
+            scoreWord: {
+                only: '按评审分数',
+                avg: '按平均分',
+                top: '按最高分',
+                rmLowAvg: '去掉最低分的平均分',
+                rmTopAvg: '去掉最高分的平均分',
+                rmLowTopAvg: '去掉最高分和最低分的平均分',
+            },
+            distributeWord: {
+                none: '不需要匹配',
+                period: '只匹配学段',
+                subject: '只匹配学科',
+                periodAndSubject: '同时匹配学科和学段',
+            },
         }
     },
     computed: {
@@ -487,6 +570,7 @@ export default {
         },
     },
     created () {
+        console.log('1BTHE5G', this.$parent.ruleInfo);
         // this.applicationColumns[2].filters = this.actInfo.invitedSchools
         this.getProList()
         this.actTeaList()
@@ -576,19 +660,127 @@ export default {
             this.$api.areaActivity.manageAct(params).then(res => {
                 console.log(res);
                 if(res.code === 200) {
-                    this.processList = res?.activityExpert?.experts.map(item => {
-                        item.num = 0
+                    this.processList = res?.expertTasks.map(item => {
+                        item.progress = item.taskCount === -1 ? -1 : ((item.completeCount === -1 ? 0 : item.completeCount) / item.taskCount * 100)
                         return item
                     })
+                    this.processList = this.processList || []
                 }
             })
         },
         setProList(list) {
             this.processList = list.map(item => {
-                item.num = 0
+                item.completeCount = -1
+                item.taskCount = -1
+                item.teacherCount = -1
+                item.progress = item.taskCount === -1 ? -1 : ((item.completeCount === -1 ? 0 : item.completeCount) / item.taskCount * 100)
                 return item
             })
         },
+        allocationWork() {
+            if(!this.processList.length) {
+                this.$Message.warning('未添加专家,无法分配作品')
+                return
+            }
+            this.changeLoad(true)
+            let params = {
+                grant_type: 'allocation-task-auto-assign',
+                activityId: this.actInfo.id
+            }
+            this.$api.areaActivity.manageAct(params).then(res => {
+                switch (res.code) {
+                    case 4:
+                        this.$Message.warning('评审专家人数少于作品分配次数')
+                        break
+                    case 6:
+                        this.$Message.warning('学段匹配的专家数量不足')
+                        break
+                    case 8:
+                        this.$Message.warning('学科匹配的专家数量不足')
+                        break
+                    case 12:
+                        this.$Message.warning('未到评审时间')
+                        break
+                    case 13:
+                        this.$Message.warning('学段和学科匹配的专家数量不足')
+                        break
+                    case 200:
+                        if(res.tasksAdd.length) {
+                            this.taskKey = res.taskKey
+                            res.tasksAdd.map(item => {
+                                let workIndex = this.alloWorkList.findIndex(work => {
+                                    return work.tmdid === item.tmdid
+                                })
+                                if(workIndex != -1) {
+                                    this.alloWorkList[workIndex][item.expertId] = item.expertId
+                                } else {
+                                    let info = {
+                                        name: item.name,
+                                        tmdid: item.tmdid,
+                                        expertName: item.expertName,
+                                    }
+                                    info[item.expertId] = item.expertId
+                                    this.alloWorkList.push(info)
+                                }
+                            })
+                            this.alloWorkCloumns = this.processList.map((item, index) => {
+                                return {
+                                    title: item.iname,
+                                    slot: 'expert',
+                                    align: 'center',
+                                    expertId: item.id,
+                                    eIndex: index
+                                }
+                            })
+                            this.alloWorkCloumns.unshift({
+                                title: '作品',
+                                slot: 'name',
+                                align: 'center'
+                            })
+                        }
+                        res.invalid.forEach(item => {
+                            let workIndex = this.alloWorkList.findIndex(work => {
+                                return work.tmdid === item.tmdid
+                            })
+                            if(workIndex != -1) {
+                                this.alloWorkList[workIndex].errInfo = item.available
+                            } else {
+                                let info = {
+                                    name: item.name,
+                                    tmdid: item.tmdid,
+                                    expertName: item.expertName,
+                                    errInfo: item.available
+                                }
+                                this.alloWorkList.unshift(info)
+                            }
+                        })
+                        
+                        this.isAllocation = true
+                        this.$Message.success('自动分配完成,检查无误后请保存')
+                        break
+                    default:
+                        this.$Message.warning('分配失败')
+                        break
+                }
+            }).finally(() => {
+                this.changeLoad(false)
+            })
+        },
+        saveAllocationResults() {
+            let params = {
+                grant_type: 'allocation-task-auto-save',
+                activityId: this.actInfo.id,
+                taskKey: this.taskKey
+            }
+            this.$api.areaActivity.manageAct(params).then(res => {
+                if(res.code === 200) {
+                    this.$Message.success('成功保存分配结果')
+                } else {
+                    this.$Message.warning('保存失败,请重新分配作品')
+                }
+                this.isAllocation = false
+            })
+        },
         delProInfo(info, index) {
             this.$Modal.confirm({
                 title: '删除专家',
@@ -601,7 +793,7 @@ export default {
                         remove_experts: [],
                     }
                     params.remove_experts.push({
-                        tmdids: info.tmdid,
+                        tmdid: info.tmdid,
                         mobile: info.mobile,
                         email: info.email
                     })
@@ -669,12 +861,12 @@ export default {
         saveRule(data) {
             let {info, isEdit} = data
             if(isEdit) {
-                this.$parent.ruleInfo = info
+                this.changeRule(info)
             }
             this.ruleDrawer = false
         },
         getTeaInfo(data) {
-            this.$parent.isLoading = true
+            this.changeLoad(true)
             let params = {
                 grant_type: 'get-teacher-enroll',
                 teacherId: data.id,
@@ -691,7 +883,7 @@ export default {
                     this.teaDrawer = true
                 }
             }).finally(() => {
-                this.$parent.isLoading = false
+                this.changeLoad(false)
             })
         },
 
@@ -749,8 +941,10 @@ export default {
         },
         openAch() {
             this.$Modal.confirm({
-                content: '确认公示本次活动的成绩吗?',
-                okText: '公示',
+                title: '公示成绩',
+                content: '是否公示评审规则的细项分数?',
+                okText: '公示总分和细项分数',
+                cancelText: '公示总分',
                 onOk: () => {
 
                 }
@@ -772,6 +966,8 @@ export default {
 
 <style lang="less" scoped>
 .sk-content {
+    height: 100%;
+    position: relative;
     .tab-header{
         margin-bottom: 10px;
     }
@@ -800,25 +996,35 @@ export default {
             }
         }
     }
-    .data-box {
-        // border-top: 1px dashed #d0d0d0;
-        padding-top: 10px;
-        margin-bottom: 30px;
+    .sk-info {
+        height: 100vh;
+        overflow-y: scroll;
+        .data-box {
+            // border-top: 1px dashed #d0d0d0;
+            padding-top: 10px;
+            margin-bottom: 30px;
 
-        /* .ivu-input-wrapper {
-            margin-bottom: 10px;
-        } */
-        .module-title {
-            background: linear-gradient(45deg, #7ab6f5, #f3fbff);
-            color: #fff;
-            font-size: 18px;
-            padding: 10px 15px;
-            // border-top-left-radius: 10px;
-            border-radius: 5px;
-            margin-bottom: 10px;
-        }
-        .module-data  {
-            margin: 0 20px;
+            /* .ivu-input-wrapper {
+                margin-bottom: 10px;
+            } */
+            .module-title {
+                background: linear-gradient(45deg, #7ab6f5, #f3fbff);
+                color: #fff;
+                font-size: 18px;
+                padding: 10px 15px;
+                // border-top-left-radius: 10px;
+                border-radius: 5px;
+                margin-bottom: 10px;
+            }
+            .module-data  {
+                margin: 0 20px;
+
+                .review-info {
+                    font-size: 16px;
+                    font-weight: bold;
+                    margin: 10px 0;
+                }
+            }
         }
     }
 }

+ 15 - 2
TEAMModelOS/ClientApp/src/view/signupActivity/infoGoing.vue

@@ -44,7 +44,11 @@
                             </p>
                             <p v-if="actInfo.scope != 'school' && isArea">学校:
                                 <span v-if="actInfo.invitedSchools.length">
-                                    <span v-for="item in actInfo.invitedSchools" :key="item.id" style="margin-right: 10px;">{{ item.name }}</span>
+                                    <span v-for="item in actInfo.invitedSchools" :key="item.id" style="margin-right: 10px;">
+                                        {{ item.name }}
+                                        <Icon type="md-checkmark-circle" color="#5aa05a" v-show="item.confirmed" />
+                                        <!-- <Icon type="md-radio-button-off" v-show="!item.confirmed" /> -->
+                                    </span>
                                 </span>
                                 <span v-else>
                                     所有学校
@@ -102,7 +106,7 @@
                         </Steps>
                     </div> -->
                     <vuescroll>
-                        <skContent :actInfo="actInfo" :contestInfo="contestInfo" :inviteTeachers="inviteTeachers" :ruleInfo="ruleInfo" @inviTea="inviteTeaChange" />
+                        <skContent :actInfo="actInfo" :contestInfo="contestInfo" :inviteTeachers="inviteTeachers" :ruleInfo="ruleInfo" :changeRule="changeRule" :changeLoad="changeLoad" @inviTea="inviteTeaChange" />
                     </vuescroll>
                 </TabPane>
             </Tabs>
@@ -255,6 +259,9 @@ export default {
                             attach.urlShow = `${host}/${this.actInfo.owner}${attach.url}?${this.actInfo.sas}`
                         }
                     })
+                    this.actInfo.invitedSchools.forEach(item => {
+                        item.confirmed = this.actInfo.confirmedSchools.find(com => com.id === item.id) ? true: false
+                    })
                     res.contest.modules.forEach(item => {
                         res.contest[item].startTime = this.$tools.formatTime(res.contest[item].stime, 'yyyy-MM-dd hh:mm:ss')
                         res.contest[item].endTime = this.$tools.formatTime(res.contest[item].etime, 'yyyy-MM-dd hh:mm:ss')
@@ -415,6 +422,12 @@ export default {
                 }
             })
         },
+        changeRule(info) {
+            this.ruleInfo = info
+        },
+        changeLoad(type) {
+            this.isLoading = type
+        },
     }
 }
 </script>

+ 13 - 9
TEAMModelOS/ClientApp/src/view/signupActivity/processActivity.vue

@@ -107,16 +107,20 @@ export default {
             }
             this.$api.areaActivity.manageAct(params).then(res => {
                 if(res.code === 200)  {
-                    const currentYear = new Date(Number(localStorage.getItem('serverTime'))).getFullYear()
-                    res.countYear = res.countYear.sort((a, b) => b.year - a.year)
-                    console.log(res.countYear);
-                    this.yearList = res.countYear
-                    if(res.countYear[0].year != currentYear) {
-                        this.yearList.unshift({year: currentYear, count: 0})
+                    if(res.countYear.length) {
+                        const currentYear = new Date(Number(localStorage.getItem('serverTime'))).getFullYear()
+                        res.countYear = res.countYear.sort((a, b) => b.year - a.year)
+                        console.log(res.countYear);
+                        this.yearList = res.countYear
+                        if(res.countYear[0].year != currentYear) {
+                            this.yearList.unshift({year: currentYear, count: 0})
+                        }
+                        this.nowYear = this.yearList[0].year
+                        
+                        this.getActivityList(this.nowYear)
+                    } else {
+                        this.isLoading = false
                     }
-                    this.nowYear = this.yearList[0].year
-                    
-                    this.getActivityList(this.nowYear)
                 }
             })
         },

+ 5 - 5
TEAMModelOS/Controllers/Analysis/ClassAnalysisController.cs

@@ -1038,7 +1038,7 @@ namespace TEAMModelOS.Controllers.Analysis
             var tyear = DateTimeOffset.UtcNow.Year;
             var tday = DateTimeOffset.UtcNow.DayOfYear;
             //求今年多少天
-            int tdays = DateTimeHelper.getDays(tyear);
+            //int tdays = DateTimeHelper.getDays(tyear);
             //如果跨年 求前年多少天
             int pydays = DateTimeHelper.getDays(syear);
             (List<LessonCount> scount, List<LessonCount> tcount) = await getCount(client, records, syear, tyear, code, tId, periodId);
@@ -1085,12 +1085,12 @@ namespace TEAMModelOS.Controllers.Analysis
                                 tsum = dense.SubMatrix(tday, tday, 0, dense.ColumnCount).ColumnSums().Sum();
                             }
                             //前一年余下的值
-                            var pysum = matrix.SubMatrix(pydays - (sday - tday) - 1, sday - tday + 1, 0, matrix.ColumnCount).ColumnSums().Sum();
+                            var pysum = matrix.SubMatrix(pydays - (sday - tday) , sday - tday , 0, matrix.ColumnCount).ColumnSums().Sum();
                             counts.Add(("week", tsum + pysum));
                         }
                         else
                         {
-                            var subDay = dense.SubMatrix(tday - sday - 1, sday + 1, 0, dense.ColumnCount).ColumnSums().Sum();
+                            var subDay = dense.SubMatrix(tday - sday, sday, 0, dense.ColumnCount).ColumnSums().Sum();
                             counts.Add(("week", subDay));
                         }
                         var subMonth = dense.SubMatrix(tday - tmonth, tmonth, 0, dense.ColumnCount).ColumnSums().Sum();
@@ -1117,7 +1117,7 @@ namespace TEAMModelOS.Controllers.Analysis
                     //今日
                     double tcounts = matrix.Row(tday - 1).Sum();
                     counts.Add(("today", tcounts));
-                    var subDay = matrix.SubMatrix(tday - sday - 1, sday + 1, 0, matrix.ColumnCount).ColumnSums().Sum();
+                    var subDay = matrix.SubMatrix(tday - sday, sday, 0, matrix.ColumnCount).ColumnSums().Sum();
                     counts.Add(("week", subDay));
                     var subMonth = matrix.SubMatrix(tday - tmonth, tmonth, 0, matrix.ColumnCount).ColumnSums().Sum();
                     counts.Add(("month", subMonth));
@@ -1133,7 +1133,7 @@ namespace TEAMModelOS.Controllers.Analysis
                  var subMonth = matrix.SubMatrix(tmonth - 1, tmonth + 1, 0, matrix.ColumnCount).ColumnSums().Sum();
                  counts.Add(("month", subMonth));*/
                 //求今年
-                var subYear = matrix.SubMatrix(0, tdays-1, 0, matrix.ColumnCount).ColumnSums().Sum();
+                var subYear = matrix.SubMatrix(0, tday, 0, matrix.ColumnCount).ColumnSums().Sum();
 
 
             }

+ 42 - 3
TEAMModelOS/Controllers/Client/HiTAControlller.cs

@@ -73,6 +73,7 @@ namespace TEAMModelOS.Controllers.Client
 
         public class HiTAJoinSchool
         {
+            public string type { get; set; }
             //[Required(ErrorMessage = "{0} 必须填写")]
             public string id { get; set; }
             //[Required(ErrorMessage = "{0} 必须填写")]
@@ -82,6 +83,7 @@ namespace TEAMModelOS.Controllers.Client
             public string school { get; set; }
             public long ts { get; set; }
             public string id_token { get; set; }
+            public string code { get; set; }
         }
         /// <summary>
         /// 
@@ -127,8 +129,27 @@ namespace TEAMModelOS.Controllers.Client
             }
         }
 
-
-
+        [ProducesDefaultResponseType]
+        [HttpGet("check-login")]
+        public async Task<IActionResult> CheckLogin([FromQuery] HiTAJoinSchool join)
+        {
+            var data = await _azureRedis.GetRedisClient(8).StringGetAsync($"HiTA:Login:{join.code}");
+            if (data.HasValue)
+            {
+                var id = data.ToString();
+                var location = _option.Location;
+                var clientID = _configuration.GetValue<string>("HaBookAuth:CoreService:clientID");
+                TmdidImplicit implicit_token = await _coreAPIHttpService.Implicit(new Dictionary<string, string>()
+                        {
+                        { "grant_type", "implicit" },
+                        { "client_id",clientID },
+                        { "account",id },
+                        { "nonce",Guid.NewGuid().ToString()}
+                        }, location, _configuration);
+                return Ok(new { token = implicit_token });
+            }
+            return Ok();
+        }
         /// <summary>
         /// 扫码加入学校 //已被school-join 替代
         /// </summary>
@@ -139,7 +160,25 @@ namespace TEAMModelOS.Controllers.Client
         //[Authorize(Roles = "HiTA")]
         public async Task<IActionResult> ScanCodeJoinSchool([FromQuery] HiTAJoinSchool join)
         {
-            
+            Dictionary<string, object> dict = new Dictionary<string, object>();
+            Dictionary<string, object> qure = new Dictionary<string, object>();
+            foreach (var a in HttpContext.Request.Query)
+            {
+                qure.Add(a.Key, a.Value);
+            }
+            foreach (var a in HttpContext.Request.Headers)
+            {
+                dict.Add(a.Key, a.Value);
+            }
+            if (string.IsNullOrWhiteSpace(join.id)) {
+                return Ok("请使用HiTA扫码!");
+            }
+            if (!string.IsNullOrWhiteSpace(join.type)&& !string.IsNullOrWhiteSpace(join.id) &&  join.type.Equals("login") && !string.IsNullOrWhiteSpace(join.code)) 
+            {
+                await _azureRedis.GetRedisClient(8).StringSetAsync($"HiTA:Login:{join.code}", join.id,expiry: new  TimeSpan(0,0,10));
+                return Ok(new { name= ",关闭弹窗以获取登录信息" ,school="tmd"});
+            }
+            //await _dingDing.SendBotMsg(join.ToJsonString()+ dict.ToJsonString(), GroupNames.成都开发測試群組);
             string school = join.school;
             string id = join.id;
             string name = join.name;

+ 4 - 1
TEAMModelOS/Controllers/Student/StudentController.cs

@@ -715,7 +715,7 @@ namespace TEAMModelOS.Controllers
             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);
+            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: 2,year: student.year);
 
             //用户在线记录
             try
@@ -1497,6 +1497,9 @@ namespace TEAMModelOS.Controllers
         //學生登入後根據學校規模取得授權數
         private async Task<int> GetStudentAuthNumByScale(string school_code, School school)
         {
+            if ((school.areaId.Equals("7a51072f-b329-4e74-99e0-ba0407ba8926") || school.areaId.Equals("69e3d413-50a1-4f5e-844a-e0f7c9622ea3"))  &&  DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()<1706630400000) {
+                return school.scale-5;
+            }
             //授权规模数量
             DateTimeOffset dateTime = DateTimeOffset.UtcNow;
             var dateDay = dateTime.ToString("yyyyMMdd"); //获取当天的日期

+ 5 - 11
TEAMModelOS/Controllers/Teacher/InitController.cs

@@ -1018,17 +1018,11 @@ namespace TEAMModelOS.Controllers
         /// <returns></returns>
         [ProducesDefaultResponseType]
         //[AuthToken(Roles = "teacher")]
-        [HttpPost("get-school-by-code")]
-        public async Task<IActionResult> GetSchoolByCode(JsonElement request) {
-            if (request.TryGetProperty("id", out JsonElement _code))
-            {
-                Azure.Response response =  await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.School).ReadItemStreamAsync(_code.GetString(), new PartitionKey("Base"));
-                School school= JsonDocument.Parse(response.Content).RootElement.ToObject<School>();
-                return Ok(new { id= school.id,school.city,school.name,school.region,school.picture });
-            }
-            else {
-                return Ok(new { id = $"{_code}", city=string.Empty,name =string.Empty, region=string.Empty, picture=string.Empty });
-            }
+        [HttpPost("get-schools")]
+        public async Task<IActionResult> GetSchools(JsonElement request) {
+            string sql = $"select c.id,c.name,c.picture,c.region,c.province,c.city from c where c.code='Base'";
+            var data  = await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS,Constant.School).GetList<JsonElement>(sql,"Base");
+            return Ok(new { schools =data.list});
         }
         /// <summary>
         /// 取得學校所有列表