Parcourir la source

調整DingDing發送群組的設定&新增歸戶以及通知功能

osbert il y a 1 an
Parent
commit
ee4234cab8

+ 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",

+ 6 - 0
TEAMModelBI/ClientApp/src/api/index.js

@@ -588,5 +588,11 @@ export default {
     // 歸戶
     consolidationCoupon(data) {
         return post('/coupon/consolidation-coupon',data)
+    },
+
+    /*簡版通知*/
+    // 發送
+    pushNotify(data) {
+        return post('/coupon/push-notify',data)
     }
 }

+ 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>

+ 333 - 74
TEAMModelBI/ClientApp/src/view/issueCoupons/crteadCoupon.vue

@@ -1,21 +1,40 @@
 <template>
-    <div>
+    <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="couponType" >
+            <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="crtCouponForm.couponType != 'General'" label="票券名稱" prop="couponName" >
+            <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 label="活動名稱" prop="eventName">
+            <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" :default-time="defaultTime"/>
+                <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;"/>
@@ -24,7 +43,7 @@
             <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 label="規則" prop="rule">
+            <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;"/>
@@ -45,7 +64,7 @@
                                 <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" type="number" placeholder="數值" style="width: 100px;" :disabled="(item.how == 'is null' || item.how == 'isnot null')"/>
+                                <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;">
@@ -72,8 +91,8 @@
                             </div>
                         </template>
                         <div style="text-align: left;">
-                            票券標題:<el-input v-model="info.n"/>
-                            連結:<el-input v-model="info.u" />
+                            票券標題:<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;">
@@ -107,15 +126,18 @@
                     </el-card>
                 </div>
             </el-form-item>
-            <el-form-item v-if="crtCouponForm.couponType == 'Event'" label="票券限制" prop="targets" >
-                <el-input style="width: 300px;" v-model="crtCouponForm.targets" :rows="6" type="textarea" placeholder="請填入教師ID, 並用換行分隔" />
+            <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-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)">重置</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;"/>
@@ -127,15 +149,151 @@
 
 <script setup>
 import { reactive, ref, computed, getCurrentInstance, markRaw } from 'vue'
-import { Ticket, Plus, CloseBold, Warning, CopyDocument } from '@element-plus/icons'
+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().getTime() + (60*60*1000) // '12:00:00
+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: "&&"}
@@ -169,7 +327,7 @@ const howList = [
     { label: "是", val: "is"},
     { label: "不是", val: "isnot"},
     { label: "是空的", val: "is null"},
-    { label: "不是空的", val: "isnot null"}
+    { label: "不是空的", val: "isnot null"}
 ]
 
 const ruleBList = [
@@ -181,10 +339,13 @@ const ruleBList = [
     { label: "HiTeachCC-6任務數(展延)", val: "903001"},
     { label: "HiTeachCC-10任務數(展延)", val: "903002"},
     { label: "HiTeachCC-100連線數(展延)", val: "903003"},
-    { label: "HiTeachCC-200連線數(展延)", val: "903004"}
+    { label: "HiTeachCC-200連線數(展延)", val: "903004"},
+    { label: "小組協作(一年)", val: "901006"},
+    { label: "AI GPT(一年)", val: "901007"}
 ]
 
 const crtCouponForm = reactive({
+    srvAdr: '',
     couponType: "",
     couponName: "",
     expire: "",
@@ -200,7 +361,7 @@ const crtCouponForm = reactive({
                     type: '',
                     how: '',
                     val:''
-                }
+                },
             ],
             "b": "", // 給的權益
             "p": "0", // 給的積分
@@ -381,7 +542,7 @@ const checkRule = (rule, value, callback) => {
                 if(q.how == ""){
                     errC++
                 }
-                if(q.val == ""){
+                if(q.how != 'is null' && q.how != 'isnot null' && q.val == ""){
                     errC++
                 }
             })
@@ -410,14 +571,68 @@ const checkMaxTaker = (rule, value, 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) {
-        callback(new Error("請填入要歸戶的老師"))
+        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 => {
@@ -431,76 +646,87 @@ const checkCupoInfo = (rule, value, 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, 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"}]
+    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) {
-        loading.value = true
         console.log("submit!")
-        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 + ' '
+
+        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.rule[i].q = qStr
-        })
-        data.maxTaker = crtCouponForm.maxTaker == 0 ? -1 : crtCouponForm.maxTaker
-        if(crtCouponForm.targets == ''){
+            data.maxTaker = crtCouponForm.maxTaker == 0 ? -1 : crtCouponForm.maxTaker
             data.targets = []
-        } else {
-            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
+            if(crtCouponForm.targets != ''){
+                data.targets = crtCouponForm.targets.split('\n')
             }
-            success = true
-        }).catch(e=>{
-            ElMessageBox.alert('請確認優惠券設定是否合乎格式', '建立優惠券',
-                {
-                    type: 'warning',
-                    icon: markRaw(Warning),
+            
+            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
                 }
-            )
-        }).finally(() => {
-            loading.value = false
-        })
-
-        if(consolidationFlag.value == true && success) {
-            loading.value = true
-            let consolidationData = {
-                ids: crtCouponForm.targets.split('\n'),
-                coupon: couponResult.value
-            }
-            await proxy.$api.consolidationCoupon(consolidationData).then((res) => {
-                consolidationFlag.value = false
-                console.log(res, '歸戶成功')
+                success = true
             }).catch(e=>{
-                ElMessageBox.alert('歸戶失敗', '歸戶',
+                ElMessageBox.alert('請確認優惠券設定是否合乎格式', '建立優惠券',
                     {
                         type: 'warning',
                         icon: markRaw(Warning),
@@ -509,11 +735,39 @@ const submitForm = formEl => {
             }).finally(() => {
                 loading.value = false
             })
-        }
 
-        if(success){
-            resetForm(formEl) // 欄位清除
-        }
+            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
@@ -556,6 +810,11 @@ const resetForm = formEl => {
             "p": "0"
         }
     ]
+
+    crtCouponForm.targets = ''
+    eventType.value = ''
+    consolidationFlag.value= false
+    crtCouponForm.couponType = ''
 }
 
 const copyDocument = (data) => {

+ 7 - 5
TEAMModelBI/ClientApp/src/view/issueCoupons/index.vue

@@ -1,19 +1,21 @@
 <template>
     <div >
       <el-tabs type="border-card">
-        <el-tab-pane label="建立優惠券">
-          <CrteadCoupon />
-      </el-tab-pane>
-      <el-tab-pane label="優惠券歸戶">優惠券歸戶</el-tab-pane>
-      <el-tab-pane label="通知">通知</el-tab-pane>
+        <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 () {
 

+ 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>

+ 50 - 4
TEAMModelBI/Controllers/BICommon/BICouponController.cs

@@ -17,6 +17,7 @@ using System.ComponentModel.DataAnnotations;
 using System.Net;
 using System.Net.Http.Headers;
 using Newtonsoft.Json;
+using System.Text.Encodings.Web;
 
 namespace TEAMModelBI.Controllers.BICommon
 {
@@ -56,7 +57,10 @@ namespace TEAMModelBI.Controllers.BICommon
         {
             try
             {
-                string url = _configuration.GetValue<string>("HaBookAuth:CoreAPI");
+                //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);
@@ -64,6 +68,12 @@ namespace TEAMModelBI.Controllers.BICommon
                 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 {
@@ -86,7 +96,9 @@ namespace TEAMModelBI.Controllers.BICommon
         {
             try
             {
-                string url = _configuration.GetValue<string>("HaBookAuth:CoreAPI");
+                //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);
@@ -106,6 +118,39 @@ namespace TEAMModelBI.Controllers.BICommon
             }
         }
 
+        /// <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 = "";
@@ -154,6 +199,7 @@ namespace TEAMModelBI.Controllers.BICommon
         public int p { get; set; } = 0;
     }
 
-    public record GenerateCouponRequest([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 coupon, [Required] List<string> ids);
+    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")]
+        高飛
     }
 }