فهرست منبع

Merge branch 'develop' of http://163.228.141.122:3000/TEAMMODEL/TEAMModelOS into develop

Eden 2 ماه پیش
والد
کامیت
c461a4caa4
21فایلهای تغییر یافته به همراه774 افزوده شده و 333 حذف شده
  1. 56 26
      TEAMModelBI/ClientApp/src/view/systemConfig/NewMsg/active/test.vue
  2. 17 3
      TEAMModelBI/ClientApp/src/view/systemConfig/NewMsg/geos_info_id.vue
  3. 13 2
      TEAMModelBI/ClientApp/src/view/systemConfig/NewMsg/geos_info_id_cn.vue
  4. 13 3
      TEAMModelBI/ClientApp/src/view/systemConfig/NewMsg/geos_info_school.vue
  5. 8 2
      TEAMModelBI/ClientApp/src/view/systemConfig/NewMsg/geos_info_school_cn.vue
  6. 3 40
      TEAMModelBI/ClientApp/src/view/systemConfig/NewMsg/index.vue
  7. 0 10
      TEAMModelBI/ClientApp/src/view/systemConfig/NewMsg/new_msg_BatchSelect.vue
  8. 132 21
      TEAMModelBI/ClientApp/src/view/systemConfig/NewMsg/new_msg_EditEmail.vue
  9. 66 26
      TEAMModelBI/ClientApp/src/view/systemConfig/NewMsg/new_msg_EditMsg.vue
  10. 5 5
      TEAMModelBI/ClientApp/src/view/systemConfig/NewMsg/new_msg_Type.vue
  11. 5 1
      TEAMModelBI/ClientApp/src/view/systemConfig/NewMsg/school_district.vue
  12. 14 3
      TEAMModelBI/ClientApp/src/view/systemConfig/NewMsg/units_info.vue
  13. 363 157
      TEAMModelBI/Controllers/BICommon/BINoticeController.cs
  14. 12 12
      TEAMModelBI/Controllers/BITest/TestController.cs
  15. 0 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/image/background.jpg
  16. 2 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/view/admin/ActivityManage.less
  17. 36 9
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/view/admin/ActivityManage.vue
  18. 4 4
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/view/login/Admin.vue
  19. 1 1
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/view/login/Student.vue
  20. 17 5
      TEAMModelOS.SDK/Models/Cosmos/BI/BICommon/Notice.cs
  21. 7 3
      TEAMModelOS/Controllers/XTest/TestController.cs

+ 56 - 26
TEAMModelBI/ClientApp/src/view/systemConfig/NewMsg/active/test.vue

@@ -1,36 +1,66 @@
 <template>
-    <div style="text-align:left;padding-left:20px;">
-        <el-radio-group v-model="radio1" size="large">
-            <el-radio-button label="模版一" value="New York" />
-            <el-radio-button label="模版二" value="Washington" />
-        </el-radio-group>
-    </div>
-
-    <div v-if="radio1=='模版一'">
-        模版一
-    </div>
-
-    <div v-if="radio1=='模版二'">
-        模版二
-    </div>
-   
+    <el-form ref="formEl" :model="formData" :rules="rules">
+        <el-form-item label="主旨" prop="sub">
+            <el-input v-model="formData.sub"></el-input>
+        </el-form-item>
+        <el-form-item label="標題" prop="title">
+            <el-input v-model="formData.title"></el-input>
+        </el-form-item>
+        <el-form-item label="內容" prop="body">
+            <el-input v-model="formData.body"></el-input>
+        </el-form-item>
+        <el-button @click="handleSubmit">提交</el-button>
+    </el-form>
 </template>
 
 <script setup>
+    import { ref } from 'vue';
+    import { ElMessage } from 'element-plus';
 
-    import { ref } from 'vue'
+    // 定義表單模型
+    const formData = ref({
+        sub: '',
+        title: '',
+        body: ''
+    });
 
-    const radio1 = ref('模版一')
-    
-</script>
+    // 定義驗證規則
+    const rules = {
+        sub: [{ required: true, message: '請填寫主旨', trigger: 'blur' }],
+        title: [{ required: true, message: '請填寫標題', trigger: 'blur' }],
+        body: [{ required: true, message: '請填寫內容', trigger: 'blur' }]
+    };
 
-<style scoped>
+    // 定義表單元素引用
+    const formEl = ref(null);
 
-    /* 自定義 el-radio-button 大小*/
-    :deep(.el-radio-button--large .el-radio-button__inner) {
-        padding: 10px 20px;
-        font-size: 14px;
-        width: 200px;
+    // 表單驗證函式
+    function validateForm(formEl) {
+        return new Promise((resolve, reject) => {
+            if (!formEl) return reject("表單元素不存在");
+
+            formEl.validate((valid) => {
+                if (valid) {
+                    resolve(true);  // 驗證成功
+                } else {
+                    reject("表單驗證失敗,請檢查錯誤");
+                }
+            });
+        });
     }
 
-</style>
+    // 提交表單
+    async function handleSubmit() {
+        try {
+            // 等待表單驗證
+            await validateForm(formEl.value);
+
+            // 驗證成功後,進行其他操作(例如 API 請求)
+            ElMessage.success("表單提交成功!");
+            // 在這裡可以處理實際的提交操作
+        } catch (error) {
+            // 如果驗證失敗,顯示錯誤訊息
+            ElMessage.error(error);
+        }
+    }
+</script>

+ 17 - 3
TEAMModelBI/ClientApp/src/view/systemConfig/NewMsg/geos_info_id.vue

@@ -122,6 +122,14 @@
     // 从父组件注入的接收名单和更新方法
     const column3Items = inject("column3Items", ref([])); // 接收名單
 
+    // 取得站台資訊
+    let new_msg_station = inject('new_msg_station');   // 站台資訊: CN or ORG
+
+    // 通知類型
+    //"type": "notify", //發送類型 mail:郵件、notify:端外、sms:簡訊
+    const notifyType = inject("notifyType");
+
+
     // 取得服務 url
     const new_msg_host = inject('new_msg_host');
 
@@ -237,7 +245,7 @@
         column3Items.value.splice(index, 1);
     };
 
-    let new_msg_station = "";
+    //let new_msg_station = "";
     // 一開始加載時,获取地理资讯数据
     const fetchData = async () => {
 
@@ -258,7 +266,10 @@
 
         // 依站台別,決定 Resuest 的參數
 
-        const requestData = { showList: false, type: "tmid" };   //  地理資訊ID,要輸入參數 , "type":"tmid"
+        // 設定 hasMail 參數
+        let hasMail = notifyType.value == "mail";
+
+        const requestData = { showList: false, type: "tmid", hasMail: hasMail };   //  地理資訊ID,要輸入參數 , "type":"tmid"
         //const requestData = { showList: false };   // 國際站,不需要輸入特定參數,就可以取得國碼
         //const requestData = { showList: false, countryId: "CN" };   // 大陸站,不需要輸入特定參數,就可以取得省分
 
@@ -312,9 +323,12 @@
         //const requestData = { showList: false, provinceId: provinceId };
         //const requestData = { showList: false, countryId: "CN", provinceId };
 
+        // 設定 hasMail 參數
+        let hasMail = notifyType.value == "mail";
+
         // 地理資訊-ID "type":"tmid"
         // 設定 Request 參數,countryId  (國際站) 
-        const requestData = { showList: false, countryId: SelectParentId, type: "tmid" };
+        const requestData = { showList: false, countryId: SelectParentId, type: "tmid", hasMail: hasMail };
 
         //debugger;
         try {

+ 13 - 2
TEAMModelBI/ClientApp/src/view/systemConfig/NewMsg/geos_info_id_cn.vue

@@ -122,6 +122,11 @@
     // 从父组件注入的接收名单和更新方法
     const column3Items = inject("column3Items", ref([])); // 接收名單
 
+    // 通知類型
+    //"type": "notify", //發送類型 mail:郵件、notify:端外、sms:簡訊
+    const notifyType = inject("notifyType");
+
+
     // 取得服務 url
     const new_msg_host = inject('new_msg_host');
 
@@ -230,7 +235,10 @@
 
         // 先取得省的資訊
 
-        const requestData = { showList: false, type: "tmid" };   //  地理資訊ID,要輸入參數 , "type":"tmid"
+        // 設定 hasMail 參數
+        let hasMail = notifyType.value == "mail";
+
+        const requestData = { showList: false, type: "tmid", hasMail: hasMail };   //  地理資訊ID,要輸入參數 , "type":"tmid"
         //const requestData = { showList: false };   // 國際站,不需要輸入特定參數,就可以取得國碼
         //const requestData = { showList: false, countryId: "CN" };   // 大陸站,不需要輸入特定參數,就可以取得省分
 
@@ -286,9 +294,12 @@
         //const requestData = { showList: false, provinceId: provinceId };
         //const requestData = { showList: false, countryId: "CN", provinceId };
 
+        // 設定 hasMail 參數
+        let hasMail = notifyType.value == "mail";
+
         // 地理資訊-ID "type":"tmid"
         // 設定 Request 參數,provinceId 
-        const requestData = { showList: false, provinceId: SelectParentId, type: "tmid" };
+        const requestData = { showList: false, provinceId: SelectParentId, type: "tmid", hasMail: hasMail };
 
 
         try {

+ 13 - 3
TEAMModelBI/ClientApp/src/view/systemConfig/NewMsg/geos_info_school.vue

@@ -125,6 +125,10 @@
     // 从父组件注入的接收名单和更新方法
     const column3Items = inject("column3Items", ref([])); // 接收名單
 
+    // 通知類型
+    //"type": "notify", //發送類型 mail:郵件、notify:端外、sms:簡訊
+    const notifyType = inject("notifyType");
+
     // 取得服務 url
     const new_msg_host = inject('new_msg_host');
 
@@ -260,7 +264,10 @@
 
         // 依站台別,決定 Resuest 的參數
 
-        const requestData = { showSchool: false };   // 其實不需要輸入參數
+        // 設定 hasMail 參數
+        let hasMail = notifyType.value == "mail";
+
+        const requestData = { showSchool: false, hasMail: hasMail };   // 其實不需要輸入參數
         //const requestData = { showSchool: false };   // 國際站,不需要輸入特定參數,就可以取得國碼
         //const requestData = { showSchool: false, countryId: "CN" };   // 大陸站,不需要輸入特定參數,就可以取得省分
 
@@ -313,9 +320,12 @@
         // 設定 Request 參數,provinceId  (大陸站),先不考慮
         //const requestData = { showSchool: false, provinceId: provinceId };
         //const requestData = { showSchool: false, countryId: "CN", provinceId };
-
+               
+        // 設定 hasMail 參數
+        let hasMail = notifyType.value == "mail";
+        
         // 設定 Request 參數,countryId  (國際站)
-        const requestData = { showSchool: false, countryId: SelectParentId };
+        const requestData = { showSchool: false, countryId: SelectParentId, hasMail: hasMail };
 
         
         try {

+ 8 - 2
TEAMModelBI/ClientApp/src/view/systemConfig/NewMsg/geos_info_school_cn.vue

@@ -242,7 +242,10 @@
 
         // 依站台別,決定 Resuest 的參數
 
-        const requestData = { showSchool: false };   // 其實不需要輸入參數
+        // 設定 hasMail 參數
+        let hasMail = notifyType.value == "mail";
+
+        const requestData = { showSchool: false, hasMail: hasMail };   // 其實不需要輸入參數
         //const requestData = { showSchool: false };   // 國際站,不需要輸入特定參數,就可以取得國碼
         //const requestData = { showSchool: false, countryId: "CN" };   // 大陸站,不需要輸入特定參數,就可以取得省分
 
@@ -293,13 +296,16 @@
         // 載入中狀態
         showLoading();
 
+        // 設定 hasMail 參數
+        let hasMail = notifyType.value == "mail";
+
         // 設定 Request 參數,provinceId  (大陸站),先不考慮
         //const requestData = { showSchool: false, provinceId: provinceId };
         //const requestData = { showSchool: false, countryId: "CN", provinceId };
 
         // 最主要的輸入參教不同
         // 設定 Request 參數,provinceId  (大陸站)
-        const requestData = { showSchool: false, provinceId: SelectParentId };
+        const requestData = { showSchool: false, provinceId: SelectParentId, hasMail: hasMail };
 
         
         try {

+ 3 - 40
TEAMModelBI/ClientApp/src/view/systemConfig/NewMsg/index.vue

@@ -107,44 +107,7 @@
     // 提供給子組件
     provide('messageData', messageData);
 
-    const messageDemo = ref({
-        target: {
-            area: ["02944f32-f534-3397-ea56-e6f1fc6c3714"],
-            geo: [
-                {
-                    countryId: "TW",
-                    provinceId: null,
-                    cityId: "30",
-                    type: "school",
-                },
-            ],
-            unit: ["1", "2"],
-            school: ["habook", "hbgl"],
-            tmid: ["1522758684", "1595321354"],
-        },
-        type: "notify",
-        method: "multi",
-        subject: "主題",
-        title: "BI寄送訊息測試",
-        body: "這是系統寄送訊息的測試內容,旨在確認訊息傳遞是否正常運作。",
-        sender: "IES",
-        hubName: "hita5",
-        data: JSON.stringify({
-            img: ["https://account.teammodel.net/img/teammodel_title_en.c4d8a10b.png"],
-            action: [
-                {
-                    type: "click",
-                    label: "醍摩豆",
-                    url: "https://www.habook.com/?code=",
-                    quickLogin: true,                    
-                },
-            ],
-        }),
-        template: "d-f1c5abd8218736783",
-        send: 1736215236,
-    });
-
-
+    
     // 取得服務 url
     const new_msg_host = ref("");
     // 在组件挂载时获取 window.location.host
@@ -156,8 +119,8 @@
     // 定义 active 变量并设定初始值
     const active = ref(0);
 
-    // 批量或個別:  BatchType, SingleType
-    const selectionType = ref('BatchType');
+    //挑選方式 single:個別 multi:批次
+    const selectionType = ref('multi');
     provide("selectionType", selectionType);
 
     // 通知類型

+ 0 - 10
TEAMModelBI/ClientApp/src/view/systemConfig/NewMsg/new_msg_BatchSelect.vue

@@ -225,21 +225,11 @@
         //alert("no thing");
     };
 
-
-    //const items = ref([
-    //    '台北學區',
-    //    '臺北科技大學',
-    //    '文化大學',
-    //    '陽明大學',
-    //    '致理科大',
-    //]);
-
     function removeSchool(index) {
         column3Items.value.splice(index, 1); // 移除指定索引的学校
         console.log('移除学校:', index);
     }
 
-
     // 通过 provide 提供的方法
     provide('AddSchool', (school) => {
         addSchoolToList(school); // 调用添加学校方法

+ 132 - 21
TEAMModelBI/ClientApp/src/view/systemConfig/NewMsg/new_msg_EditEmail.vue

@@ -6,15 +6,19 @@
         <el-tag size="large" type="info" effect="light" style="margin:20px;">Email通知</el-tag>
 
         <div style="text-align: left; padding-left: 20px; display: flex; flex-direction: column; align-items: flex-start;">
-            <el-radio-group v-model="radio1" size="large">
-                <el-radio-button label="模版一" value="New York" />
-                <el-radio-button label="模版二" value="Washington" />
+            <el-radio-group v-model="template" size="large">
+                <el-radio-button label="d-f1c5abd8247f4be79ceaecdd327e9a68">
+                    模版一
+                </el-radio-button>
+                <el-radio-button label="template2" v-if="false">
+                    模版二
+                </el-radio-button>
             </el-radio-group>
         </div>
     </div>
 
     <!-- 模版一-->
-    <div style="padding-bottom:10px;background-color: #FFFFFF; /* 背景白色 */" v-if="radio1==='模版一'">
+    <div style="padding-bottom:10px;background-color: #FFFFFF; /* 背景白色 */">
 
         <el-form ref="ruleFormRef" :model="noticeForm" :rules="rules" label-width="120px" style="gap: 210px;">
 
@@ -128,14 +132,14 @@
     </div>
 
     <!-- 模版二-->
-    <div style="padding-bottom:10px;background-color: #FFFFFF; /* 背景白色 */" v-if="radio1==='模版二'">
+    <div style="padding-bottom:10px;background-color: #FFFFFF; /* 背景白色 */" v-if="template==='不顯示'">
         模版二
     </div>
 
     <!-- 上一步,發送,重置-->
     <el-button type="primary" @click="goPreviousStep">上一步</el-button>
     <!--el-button type="primary" @click="submitForm(ruleFormRef)" :loading="loading">發送</!--el-button> -->
-    <el-button type="primary" @click="sendMessage" :loading="loading">發送</el-button>
+    <el-button type="primary" @click="handleSubmit" :loading="loading">發送</el-button>
     <el-button @click="resetForm(ruleFormRef)" :loading="loading">重置</el-button>
 
 
@@ -144,25 +148,34 @@
 <script setup>
     import { reactive, ref, computed, getCurrentInstance, markRaw, inject, watch } from 'vue'
     import { Warning, SuccessFilled, Plus, Close } from '@element-plus/icons'
-    import { ElMessageBox } from 'element-plus'
+    import { ElMessageBox, ElMessage } from 'element-plus'
     import { onMounted } from '../../../../node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler'
     let { proxy } = getCurrentInstance()
 
-    const radio1 = ref('模版一')
+    // 模版 ID
+    const template = ref('d-f1c5abd8247f4be79ceaecdd327e9a68')
 
     //標題和內容
     const new_msg_title = inject('new_msg_title');
     const new_msg_body = inject('new_msg_body');
 
     // 从父组件注入的接收名单和更新方法
+    const selectionType = inject('selectionType'); 
+
     const column3Items = inject("column3Items", ref([])); // 接收名單
 
+    //"type": "notify", //發送類型 mail:郵件、notify:端外、sms:簡訊
+    const notifyType = inject('notifyType'); 
+
+    // 取得站台資訊
+    let new_msg_station = inject('new_msg_station');   // 站台資訊: CN or ORG
+
     // 定义触发事件的方式,emit 会用来触发更新 active 的事件
     const emit = defineEmits();
 
     onMounted(() => {
                 
-        UseFakeData();  // 使用 Fake Data
+        //UseFakeData();  // 使用 Fake Data
     });
 
     // 上一步的逻辑:将 active 设置为 1
@@ -211,6 +224,39 @@
         noticeForm.data.img = FakeData.value.image ? [FakeData.value.image]:[];
     };
 
+    // 表單驗證函式
+    function validateForm(formEl) {
+        return new Promise((resolve, reject) => {
+            if (!formEl) return reject("表單元素不存在");
+
+            formEl.validate((valid) => {
+                if (valid) {
+                    resolve(true);  // 驗證成功
+                } else {
+                    reject("表單驗證失敗,請檢查錯誤");
+                }
+            });
+        });
+    }
+
+    // 提交表單
+    async function handleSubmit() {
+        try {
+            // 等待表單驗證
+            await validateForm(ruleFormRef.value);
+
+            // 驗證成功後,進行其他操作(例如 API 請求)
+            //ElMessage.success("表單提交成功!");
+            // 在這裡可以處理實際的提交操作
+            
+            await sendMessage();
+
+
+        } catch (error) {
+            // 如果驗證失敗,顯示錯誤訊息
+            ElMessage.error(error);
+        }
+    }
 
 
     // 儲存 標題
@@ -500,18 +546,57 @@
         }
     }
 
+    const requestData = ref({
+        target: {
+            area: [],
+            geo: [],
+            unit: [],
+            school: [],
+            tmid: [],
+        },
+        type: "",        // 發送類型        
+        method: "",      //挑選方式 single:個別 multi:批次
+        subject:"",      // 主旨  
+        title: "",       // 標題
+        body: "",        // 內文
+        sender: "",      // 訊息發送來源
+        hubName: "",     // 訊息中樞
+        data: "",        // 序列化資料
+        //data: {
+        //    img: [
+        //        "https://account.teammodel.net/img/teammodel_title_en.c4d8a10b.png"
+        //    ],
+        //    action: [
+        //        {
+        //            type: "click",
+        //            label: "醍摩豆",
+        //            url: "https://www.habook.com/?code=",
+        //            quickLogin: true,
+        //            quicklogin: 1
+        //        }
+        //    ]
+        //},
+        //template: "",    // 模板ID : Email 用
+        send: 0,         // 發送時間,預設 0
+    });
+
     // 發送訊息 (新)
     const sendMessage = async () => {
-
-        //ElMessage("Hi, Buddy~!");
+    
 
         //填入資料
-
+              
         // // 發送類型  type : "notify", //發送類型 mail:郵件、notify:端外、sms:簡訊
-        requestData.value.type = notifyType;
+        requestData.value.type = notifyType.value;
 
         //挑選方式 single:個別 multi:批次
-        requestData.value.method = selectionType;
+        requestData.value.method = selectionType.value;
+
+        //模版
+        requestData.value.template = template.value;
+
+        // sub 主旨
+        requestData.value.subject = noticeForm.sub;
 
         // 標題
         requestData.value.title = new_msg_title;
@@ -519,8 +604,26 @@
         // 內文
         requestData.value.body = new_msg_body;
 
-        //附加按鈕 非必須
-        requestData.value.action = new_msg_action;
+        // 開始發送訊息
+        console.log('開始發送訊息...');
+        
+        //data 空值判斷  (圖片)
+
+        const { img, action } = noticeForm.data;
+        let result = {};
+        if (img?.[0] !== "") {
+            result.img = img;
+        }
+
+        // 都為空字串,則設空字串
+        if (img?.[0] === "") {
+            requestData.value.data = "";
+        } else {
+            requestData.value.data = JSON.stringify(result); // 序列化成 JSON 字串
+        }
+
+        // 發送者
+        requestData.value.sender = noticeForm.sender;
 
         // target
         // area, geo, unit, school, tmid
@@ -532,7 +635,7 @@
         const listSchool = [];
         const listUnit = [];
         const listTmid = [];
-        //debugger;
+
         // 取值
         for (const item of column3Items.value) {
 
@@ -579,8 +682,8 @@
                 listUnit.push(item.id);
             }
 
-            // 醍摩豆ID: tmid
             if (item.receiveType === 5) {
+            // 醍摩豆ID: tmid
 
                 // 實作中
                 //listTmid.push(item.id);
@@ -631,7 +734,7 @@
 
         //debugger;
 
-        if (0) {
+        if (1) {
 
             // 傳送訊訊
             proxy.$api.sendMessage(requestData.value).then((res) => {
@@ -655,9 +758,17 @@
 
         }
 
+        if (1) {
+            // 回到首頁
+            emit('update-active', 0); // 通知父组件更新 active 的值为 0
+
+            // 清空接收名單
+            column3Items.value = []
 
-        // 回到發送歷程
-        //goMsgHistory()
+
+            // 回到發送歷程
+            //goMsgHistory()
+        }
     };
 
 

+ 66 - 26
TEAMModelBI/ClientApp/src/view/systemConfig/NewMsg/new_msg_EditMsg.vue

@@ -125,9 +125,14 @@
     import { ElMessageBox, ElMessage } from 'element-plus'
     let { proxy } = getCurrentInstance()
 
+    const template = ref('')
+
     // 接收父組件提供的 messageData
     const messageData = inject('messageData');
 
+    // 取得站台資訊
+    let new_msg_station = inject('new_msg_station');   // 站台資訊: CN or ORG
+
     //發送時間
     const new_msg_send = inject('new_msg_send');
 
@@ -517,28 +522,61 @@
     // 定義發送請求的參數  (名單部分沒有變)
 
 
+    //const requestData = ref({
+    //    target: {
+    //        area: ["02944f32-f534-3397-ea56-e6f1fc6c3714"],
+    //        geo: [
+    //            {
+    //                countryId: "TW",
+    //                provinceId: null,
+    //                cityId: "30",
+    //                type: "school",
+    //            },
+    //        ],
+    //        unit: ["1", "2"],
+    //        school: ["habook", "hbgl"],
+    //        tmid: ["1522758684", "1595321354"],
+    //    },
+    //    type: "notify", // 發送類型
+    //    method: "multi", // 挑選方式
+    //    title: "BI寄送訊息測試", // 標題
+    //    body: "這是系統寄送訊息的測試內容,旨在確認訊息傳遞是否正常運作。", // 內文
+    //    sender: "IES", // 訊息發送來源
+    //    hubName: "hita5", // 訊息中樞
+    //    data: "",    // data 要序例化成 json 字串
+    //    //data: {
+    //    //    img: [
+    //    //        "https://account.teammodel.net/img/teammodel_title_en.c4d8a10b.png"
+    //    //    ],
+    //    //    action: [
+    //    //        {
+    //    //            type: "click",
+    //    //            label: "醍摩豆",
+    //    //            url: "https://www.habook.com/?code=",
+    //    //            quickLogin: true,
+    //    //            quicklogin: 1
+    //    //        }
+    //    //    ]
+    //    //},
+    //    template: "d-f1c5abd8218736783", // 模板ID
+    //    send: 0, // 發送時間,0 表示立即發送
+    //});
+
     const requestData = ref({
         target: {
-            area: ["02944f32-f534-3397-ea56-e6f1fc6c3714"],
-            geo: [
-                {
-                    countryId: "TW",
-                    provinceId: null,
-                    cityId: "30",
-                    type: "school",
-                },
-            ],
-            unit: ["1", "2"],
-            school: ["habook", "hbgl"],
-            tmid: ["1522758684", "1595321354"],
+            area: [],
+            geo: [],
+            unit: [],
+            school: [],
+            tmid: [],
         },
-        type: "notify", // 發送類型
-        method: "multi", // 挑選方式
-        title: "BI寄送訊息測試", // 標題
-        body: "這是系統寄送訊息的測試內容,旨在確認訊息傳遞是否正常運作。", // 內文
-        sender: "IES", // 訊息發送來源
-        hubName: "hita5", // 訊息中樞
-        data: "",    // data 要序例化成 json 字串
+        type: "",        // 發送類型
+        method: "",      // 挑選方式
+        title: "",       // 標題
+        body: "",        // 內文
+        sender: "",      // 訊息發送來源
+        hubName: "",     // 訊息中樞
+        data: "",        // 序列化資料
         //data: {
         //    img: [
         //        "https://account.teammodel.net/img/teammodel_title_en.c4d8a10b.png"
@@ -553,15 +591,13 @@
         //        }
         //    ]
         //},
-        template: "d-f1c5abd8218736783", // 模板ID
-        send: 0, // 發送時間,0 表示立即發送
+        //template: "",    // 模板ID : Email 用
+        send: 0,         // 發送時間,預設 0
     });
 
     // 發送訊息 (新)
     const sendMessage = async () => {
-
-        //ElMessage("Hi, Buddy~!");
-
+                
         //填入資料
 
         // // 發送類型  type : "notify", //發送類型 mail:郵件、notify:端外、sms:簡訊
@@ -576,7 +612,7 @@
         // 內文
         requestData.value.body = new_msg_body;
     
-        //data 空值判斷
+        //data 空值判斷  (圖片,按鈕)
 
         const { img, action } = noticeForm.data;
         let result = {};
@@ -734,7 +770,11 @@
 
         // 回到首頁
         emit('update-active', 0); // 通知父组件更新 active 的值为 0
-        
+
+        // 清空接收名單
+        column3Items.value =[]
+
+
         // 回到發送歷程
         //goMsgHistory()
     };

+ 5 - 5
TEAMModelBI/ClientApp/src/view/systemConfig/NewMsg/new_msg_Type.vue

@@ -9,7 +9,7 @@
 
             <el-radio-group v-model="notifyType" style="margin-top: 20px;">
                 <el-radio label="notify">端外通知</el-radio>
-                <el-radio label="mail" disabled >Email</el-radio>
+                <el-radio label="mail" >Email</el-radio>
                 <el-radio label="sms" disabled>簡訊</el-radio>
             </el-radio-group>
 
@@ -21,8 +21,8 @@
             </template>
 
             <el-radio-group v-model="selectionType" style="margin-top: 20px;">
-                <el-radio label="BatchType">批量挑選</el-radio>
-                <el-radio label="SingleType" disabled>個別挑選</el-radio>
+                <el-radio label="multi">批量挑選</el-radio>
+                <el-radio label="single" disabled>個別挑選</el-radio>
                 
             </el-radio-group>
 
@@ -68,9 +68,9 @@
     const goNextStep = () => {
         //emit('update-active', 1);  // 通知父组件更新 active 的值
 
-        if (selectionType.value === 'BatchType') {
+        if (selectionType.value === 'multi') {
             emit('update-active', 1);  // 通知父组件更新 active 的值
-        } else if (selectionType.value === 'SingleType') {
+        } else if (selectionType.value === 'single') {
             emit('update-active', 2);  // 通知父组件更新 active 的值
         }
 

+ 5 - 1
TEAMModelBI/ClientApp/src/view/systemConfig/NewMsg/school_district.vue

@@ -178,8 +178,12 @@ const removeFromColumn3 = (index) => {
 // 获取学区和学校数据
 const fetchData = async () => {
 
+    let hasMail = notifyType.value == "mail";
+
     //設定 show 學校列表
-    const requestData = { showList: true };  
+    const requestData = { showList: true, hasMail: hasMail };  
+
+   
 
     // 取得學區資訊
     try {

+ 14 - 3
TEAMModelBI/ClientApp/src/view/systemConfig/NewMsg/units_info.vue

@@ -109,6 +109,11 @@
     // 从父组件注入的接收名单和更新方法
     const column3Items = inject("column3Items", ref([])); // 接收名單
 
+    // 通知類型
+    //"type": "notify", //發送類型 mail:郵件、notify:端外、sms:簡訊
+    const notifyType = inject("notifyType");
+
+
     // 取得服務 url
     const new_msg_host = inject('new_msg_host');
 
@@ -201,8 +206,11 @@
     // 根据选中的機構ID获取學校資訊
     const fetchSchoolsDataByUnits = async (UnitsId) => {
 
+        // 設定 hasMail 參數
+        let hasMail = notifyType.value == "mail";
+
         // 設定 Request 參數,UnitsId
-        const requestData = { showList: true, type : UnitsId };
+        const requestData = { showList: true, type: UnitsId, hasMail: hasMail };
         
         try {
 
@@ -249,9 +257,12 @@
     };
     // 获取教育機構数据
     const fetchData = async () => {
-        
+
+        // 設定 hasMail 參數
+        let hasMail = notifyType.value == "mail";
+
         //設定 show 學校列表
-        const requestData = { type: "" };
+        const requestData = { type: "", hasMail: hasMail };
 
         try {
 

+ 363 - 157
TEAMModelBI/Controllers/BICommon/BINoticeController.cs

@@ -38,6 +38,11 @@ using System.Text.RegularExpressions;
 using MathNet.Numerics.Distributions;
 using System.Drawing.Drawing2D;
 using Microsoft.Azure.Amqp.Framing;
+using TEAMModelOS.SDK.DI.IPIP;
+using Azure;
+using Microsoft.AspNetCore.Http.HttpResults;
+using System.Net;
+using System.Dynamic;
 
 
 namespace TEAMModelBI.Controllers.BICommon
@@ -333,7 +338,7 @@ namespace TEAMModelBI.Controllers.BICommon
                 //取得學校ID列表
                 Dictionary<string, string> schAreaDic = new Dictionary<string, string>();
                 Dictionary<string, string> schNameDic = new Dictionary<string, string>();
-                List<string> teacherCodes = new List<string>();
+                //List<string> teacherCodes = new List<string>();
                 List<string> areaIds = areaInfos.Select(a => a.id).ToList();
                 string sqlSch = $"SELECT c.id, c.name, c.areaId FROM c WHERE ARRAY_CONTAINS({JsonSerializer.Serialize(areaIds)}, c.areaId)";
                 ///實體校
@@ -342,15 +347,8 @@ namespace TEAMModelBI.Controllers.BICommon
                     string schId = item.GetProperty("id").ToString();
                     string schName = item.GetProperty("name").ToString();
                     string areaId = item.GetProperty("areaId").ToString();
-                    if (!schAreaDic.ContainsKey(schId))
-                    {
-                        schAreaDic.Add(schId, areaId);
-                        schNameDic.Add(schId, schName);
-                    }
-                    if (!teacherCodes.Contains($"Teacher-{schId}"))
-                    {
-                        teacherCodes.Add($"Teacher-{schId}");
-                    }
+                    if (!schAreaDic.ContainsKey(schId)) schAreaDic.Add(schId, areaId);
+                    if (!schNameDic.ContainsKey(schId)) schNameDic.Add(schId, schName);
                 }
                 ///虛擬校
                 await foreach (var item in cosmosClient.GetContainer(Constant.TEAMModelOS, Constant.School).GetItemQueryIteratorSql<JsonElement>(queryText: sqlSch, requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey($"VirtualBase") }))
@@ -358,39 +356,28 @@ namespace TEAMModelBI.Controllers.BICommon
                     string schId = item.GetProperty("id").ToString();
                     string schName = item.GetProperty("name").ToString();
                     string areaId = item.GetProperty("areaId").ToString();
-                    if (!schAreaDic.ContainsKey(schId))
-                    {
-                        schAreaDic.Add(schId, areaId);
-                        if (!schNameDic.ContainsKey(schId)) schNameDic.Add(schId, schName);
-                    }
+                    if (!schAreaDic.ContainsKey(schId)) schAreaDic.Add(schId, areaId);
+                    if (!schNameDic.ContainsKey(schId)) schNameDic.Add(schId, schName);
                 }
-                //有無Email篩檢
-                List<string> tmidFilterList = new List<string>();
+                //取得歸戶學校教師數
+                Dictionary<string, int> schTeacherCntDic = new Dictionary<string, int>();
+                List<string> filterScid = schNameDic.Select(s => s.Key).ToList();
+                string sqlEx = $"SELECT c.id, c.schoolCode, c.schoolCodeW FROM c WHERE ((ARRAY_CONTAINS({JsonSerializer.Serialize(filterScid)}, c.schoolCode) OR ARRAY_CONTAINS({JsonSerializer.Serialize(filterScid)}, c.schoolCodeW)))";
                 if (hasMail)
                 {
-                    List<string> tmidListIes = new List<string>();
-                    string sqlTchHasMailIes = $"SELECT DISTINCT c.id FROM c WHERE c.pk = 'Teacher' AND c.status = 'join' AND ARRAY_CONTAINS({JsonSerializer.Serialize(teacherCodes)}, c.code)";
-                    await foreach (var item in cosmosClient.GetContainer(Constant.TEAMModelOS, Constant.School).GetItemQueryIteratorSql<JsonElement>(queryText: sqlTchHasMailIes, requestOptions: null))
-                    {
-                        string tmid = item.GetProperty("id").ToString();
-                        tmidListIes.Add(tmid);
-                    }
-                    string sqlEx = $"SELECT c.id FROM c WHERE IS_DEFINED(c.mail) AND NOT IS_NULL(c.mail) AND c.mail != '' AND ARRAY_CONTAINS({JsonSerializer.Serialize(tmidListIes)}, c.id) ";
-                    await foreach (var item in cosmosClientCsv2.GetContainer("Core", "ID2").GetItemQueryIteratorSql<JsonElement>(queryText: sqlEx, requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey($"base-ex") }))
-                    {
-                        string tmid = item.GetProperty("id").ToString();
-                        tmidFilterList.Add(tmid);
-                    }
+                    sqlEx += " AND (IS_DEFINED(c.mail) AND NOT IS_NULL(c.mail) AND c.mail != '')";
                 }
-                //取得學校教師數
-                Dictionary<string, int> schTeacherCntDic = new Dictionary<string, int>();
-                string sqlTmidFilter = (tmidFilterList.Count > 0) ? $" AND ARRAY_CONTAINS({JsonSerializer.Serialize(tmidFilterList)}, c.id) " : string.Empty;
-                string sqlTch = $"SELECT REPLACE(c.code, 'Teacher-', '')  as schoolId, COUNT(c.id) AS tchCnt FROM c WHERE c.pk = 'Teacher' AND c.status = 'join' AND ARRAY_CONTAINS({JsonSerializer.Serialize(teacherCodes)}, c.code) {sqlTmidFilter} GROUP BY c.code";
-                await foreach (var item in cosmosClient.GetContainer(Constant.TEAMModelOS, Constant.School).GetItemQueryIteratorSql<JsonElement>(queryText: sqlTch, requestOptions: null))
+                await foreach (var item in cosmosClientCsv2.GetContainer("Core", "ID2").GetItemQueryIteratorSql<JsonElement>(queryText: sqlEx, requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey($"base-ex") }))
                 {
-                    string schId = item.GetProperty("schoolId").ToString();
-                    int tchCnt = item.GetProperty("tchCnt").GetInt32();
-                    schTeacherCntDic.Add(schId, tchCnt);
+                    string tmid = item.GetProperty("id").ToString();
+                    string schoolCode = (item.TryGetProperty("schoolCode", out JsonElement _schoolCode)) ? _schoolCode.ToString() : string.Empty;
+                    string schoolCodeW = (item.TryGetProperty("schoolCodeW", out JsonElement _schoolCodeW)) ? _schoolCodeW.ToString() : string.Empty;
+                    string schId = (!string.IsNullOrWhiteSpace(schoolCode)) ? schoolCode : (!string.IsNullOrWhiteSpace(schoolCodeW)) ? schoolCodeW : string.Empty;
+                    if(filterScid.Contains(schId))
+                    {
+                        if (!schTeacherCntDic.ContainsKey(schId)) schTeacherCntDic.Add(schId, 0);
+                        schTeacherCntDic[schId]++;
+                    }
                 }
                 //資料整理1 有教師則記入
                 foreach (KeyValuePair<string, int> item in schTeacherCntDic)
@@ -469,14 +456,14 @@ namespace TEAMModelBI.Controllers.BICommon
             //取得ID帳號中所有的歸戶學校ID
             List<string> coreSchIds = new List<string>();
             Dictionary<string, List<string>> schTmidDic = new Dictionary<string, List<string>>(); //學校ID > TMID 字典
-            string sqlEx = "SELECT c.id, c.schoolCode, c.schoolCodeW FROM c WHERE IS_DEFINED(c.schoolCode) AND NOT IS_NULL(c.schoolCode) AND IS_DEFINED(c.schoolCodeW) AND NOT IS_NULL(c.schoolCodeW)";
-            if (hasMail) sqlEx += " AND IS_DEFINED(c.mail) AND NOT IS_NULL(c.mail) ";
+            string sqlEx = "SELECT c.id, c.schoolCode, c.schoolCodeW FROM c WHERE ((IS_DEFINED(c.schoolCode) AND NOT IS_NULL(c.schoolCode)) OR (IS_DEFINED(c.schoolCodeW) AND NOT IS_NULL(c.schoolCodeW)))";
+            if (hasMail) sqlEx += " AND (IS_DEFINED(c.mail) AND NOT IS_NULL(c.mail)) ";
             await foreach (var item in cosmosClientCsv2.GetContainer("Core", "ID2").GetItemQueryIteratorSql<JsonElement>(queryText: sqlEx, requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey($"base-ex") }))
             {
                 string tmid = item.GetProperty("id").ToString();
-                string shortCode = (item.TryGetProperty("shortCode", out JsonElement _shortCode)) ? _shortCode.ToString() : string.Empty;
+                string schoolCode = (item.TryGetProperty("schoolCode", out JsonElement _schoolCode)) ? _schoolCode.ToString() : string.Empty;
                 string schoolCodeW = (item.TryGetProperty("schoolCodeW", out JsonElement _schoolCodeW)) ? _schoolCodeW.ToString() : string.Empty;
-                string schId = (!string.IsNullOrWhiteSpace(shortCode)) ? shortCode : (!string.IsNullOrWhiteSpace(schoolCodeW)) ? schoolCodeW : string.Empty;
+                string schId = (!string.IsNullOrWhiteSpace(schoolCode)) ? schoolCode : (!string.IsNullOrWhiteSpace(schoolCodeW)) ? schoolCodeW : string.Empty;
                 if (!string.IsNullOrWhiteSpace(schId))
                 {
                     if(!coreSchIds.Contains(schId)) coreSchIds.Add(schId);
@@ -1237,17 +1224,27 @@ namespace TEAMModelBI.Controllers.BICommon
             string data = (jsonElement.TryGetProperty("data", out JsonElement _data)) ? _data.ToString() : string.Empty; //額外資料
             long send = (jsonElement.TryGetProperty("send", out JsonElement _send)) ? _send.GetInt64() : 0; //發送時間 0:立即發送
             string template = (jsonElement.TryGetProperty("template", out JsonElement _template)) ? _template.ToString() : string.Empty; //模板ID
-
-            if (target == null || string.IsNullOrWhiteSpace(type) || string.IsNullOrWhiteSpace(method) || string.IsNullOrWhiteSpace(title) || string.IsNullOrWhiteSpace(body) || string.IsNullOrWhiteSpace(sender)) return BadRequest();
+            //必須項檢驗
+            bool hasMail = false;
+            if (target == null || string.IsNullOrWhiteSpace(type) || string.IsNullOrWhiteSpace(method) || string.IsNullOrWhiteSpace(title) || string.IsNullOrWhiteSpace(body)) return BadRequest();
+            if(type.Equals("notify"))
+            {
+                if(string.IsNullOrWhiteSpace(sender)) return BadRequest();
+            }
+            else if(type.Equals("mail"))
+            {
+                if (string.IsNullOrWhiteSpace(subject)) return BadRequest();
+                hasMail = true;
+            }
             string eventKey = "bi-gen-notify";
             string eventId = $"{eventKey}_{_snowflakeId.NextId()}";
             string eventName = "BI send notification";
-            var result = await SendMessageCore(target, type, method, subject, title, body, data, sender, hubName, template, send, eventId, eventName);
+            var result = await SendMessageCore(target, type, method, subject, title, body, data, sender, hubName, template, send, eventId, eventName, hasMail);
 
             return Ok(new { state = RespondCode.Ok, result });
         }
         ///寄發訊息核心邏輯
-        private async Task<object> SendMessageCore(SendMessageParam target, string type, string method, string subject, string title, string body, string data, string sender, string hubName, string template, long send, string eventId = "", string eventName = "")
+        private async Task<object> SendMessageCore(SendMessageParam target, string type, string method, string subject, string title, string body, string data, string sender, string hubName, string template, long send, string eventId = "", string eventName = "", bool hasMail = false)
         {
             #region target 內容例
             //{
@@ -1279,12 +1276,14 @@ namespace TEAMModelBI.Controllers.BICommon
             //     ]
             //}
             #endregion
+            var cosmosClientIes = _azureCosmos.GetCosmosClient();
             var cosmosClientCsv2 = _azureCosmos.GetCosmosClient(name: "CoreServiceV2");
+            var (_tmdId, _tmdName, _, _, _, _) = HttpJwtAnalysis.JwtXAuthBI(HttpContext.GetXAuth("AuthToken"), _option);
 
-            List<string> tmids_area = await GetTmidByAreaId(target.area);
-            List<string> tmids_geo = await GetTmidByGeo(target.geo);
-            List<string> tmids_unit = await GetTmidByUnitId(target.unit);
-            List<string> tmids_school = await GetTmidBySchoolId(target.school);
+            List<string> tmids_area = await GetTmidByAreaId(target.area, hasMail);
+            List<string> tmids_geo = await GetTmidByGeo(target.geo, hasMail);
+            List<string> tmids_unit = await GetTmidByUnitId(target.unit, hasMail);
+            List<string> tmids_school = await GetTmidBySchoolId(target.school, hasMail);
             List<string> tmids_direct = target.tmid;
 
             List<string> tmids = new List<string>(); //聯集化
@@ -1294,6 +1293,10 @@ namespace TEAMModelBI.Controllers.BICommon
             tmids = tmids.Union(tmids_school).ToList();
             tmids = tmids.Union(tmids_direct).ToList();
             tmids = tmids.Distinct().ToList();  //唯一化
+//#if DEBUG //測試模式時限制TMID帳號,正式站佈署時不生效
+//            List<string> filterTmid = new List<string>() { "1522758684", "1595321354", "1629875867" }; //"1522758684", "1595321354"
+//            tmids = tmids.Intersect(filterTmid).ToList();
+//#endif
 
             //取得TMID資料
             List<IdInfo> tmidInfos = new List<IdInfo>();
@@ -1324,7 +1327,18 @@ namespace TEAMModelBI.Controllers.BICommon
                 List<string> tmIds = tmidInfos.Select(i => i.id).ToList();
                 if(send.Equals(0)) //立即寄送
                 {
+                    //訊息寄送
                     HttpResponseMessage response = CallPushNotifyApi(tmIds, title, body, sender, hubName, template, data, eventId, eventName);
+                    //寄送訊息DB記入
+                    if(response.IsSuccessStatusCode)
+                    {
+                        BINotice bINotice = TransMsgRequestToBINotice(type, method, subject, title, body, sender, hubName, target, tmIds, data);
+                        bINotice.id = Guid.NewGuid().ToString();
+                        bINotice.createId = _tmdId;
+                        bINotice.sendTime = (send.Equals(0)) ? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() : send; //先處理"及時寄送","定時寄送"待處理
+                        await cosmosClientIes.GetContainer(Constant.TEAMModelOS, "Common").CreateItemAsync<BINotice>(bINotice, new PartitionKey("BINotice"));
+                    }
+
                     var result = new { status = response.StatusCode, content = await response.Content.ReadAsStringAsync() };
                     return result;
                 }
@@ -1336,15 +1350,58 @@ namespace TEAMModelBI.Controllers.BICommon
             }
             else if(type.Equals("mail")) //Email
             {
+                string defaultTemplate = "d-f1c5abd8247f4be79ceaecdd327e9a68"; //國際站預設模板 ※CN站還沒有
+                List<string> mailList = tmidInfos.Where(i => !string.IsNullOrWhiteSpace(i.mail)).Select(i => i.mail).ToList();
                 List<string> tmIds = tmidInfos.Where(i => !string.IsNullOrWhiteSpace(i.mail)).Select(i => i.id).ToList();
                 //呼叫Email API [未]
                 ///模板設定
-                if(_option.Location.Contains("Global"))
+                if (_option.Location.Contains("Global"))
                 {
-                    if (string.IsNullOrWhiteSpace(template)) template = "d-f1c5abd8247f4be79ceaecdd327e9a68"; //若未指定模板ID,用預設模板
+                    if (string.IsNullOrWhiteSpace(template))
+                        template = defaultTemplate; //若未指定模板ID,用預設模板
+                }
+                if(string.IsNullOrWhiteSpace(template)) //無模板ID,就不執行
+                {
+                    var result = new { status = HttpStatusCode.BadRequest };
+                    return result;
+                }
+                //Email寄送
+                bool sendMail = false;
+                if (send.Equals(0)) //立即寄送
+                {
+                    List<string> imgArr = new List<string>();
+                    try
+                    {
+                        JsonElement dataJobj = JsonSerializer.Deserialize<JsonElement>(data);
+                        if(dataJobj.TryGetProperty("img", out JsonElement _img))
+                        {
+                            imgArr = _img.ToObject<List<string>>();
+                        }
+                    }
+                    catch (JsonException) { }
+                    string image = (imgArr.Count > 0) ? imgArr[0] : string.Empty;
+                    foreach(string email in mailList)
+                    {
+                        await CallSendMailApiAsync(email, subject, title, body, template, image);
+                        sendMail = true;
+                    }
+                    //寄送訊息DB記入
+                    if (sendMail)
+                    {
+                        BINotice bINotice = TransMsgRequestToBINotice(type, method, subject, title, body, sender, hubName, target, tmIds, data);
+                        bINotice.id = Guid.NewGuid().ToString();
+                        bINotice.createId = _tmdId;
+                        bINotice.sendTime = (send.Equals(0)) ? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() : send; //先處理"及時寄送","定時寄送"待處理
+                        await cosmosClientIes.GetContainer(Constant.TEAMModelOS, "Common").CreateItemAsync<BINotice>(bINotice, new PartitionKey("BINotice"));
+                    }
+
+                    var result = new { status = HttpStatusCode.OK, content = string.Empty };
+                    return result;
+                }
+                else //定時寄送 [待做]
+                {
+
                 }
-                var result = new { };
-                return result;
             }
             else if (type.Equals("sms")) //短訊
             {
@@ -1357,7 +1414,7 @@ namespace TEAMModelBI.Controllers.BICommon
             return new { };
         }
         //取得學區所屬學校教師
-        private async Task<List<string>> GetTmidByAreaId(List<string> areaIds)
+        private async Task<List<string>> GetTmidByAreaId(List<string> areaIds, bool hasMail)
         {
             var cosmosClientIes = _azureCosmos.GetCosmosClient();
             //取得學校ID列表
@@ -1369,26 +1426,36 @@ namespace TEAMModelBI.Controllers.BICommon
                 string schId = item.GetProperty("id").ToString();
                 schIds.Add(schId);
             }
-            List<string> teacherCodes = schIds.Select(s => $"Teacher-{s}").ToList();
-            //取得學校教師ID列表
-            List<string> tmids = new List<string>();
-            string sqlTch = $"SELECT DISTINCT c.id FROM c WHERE c.pk = 'Teacher' AND c.status = 'join' AND ARRAY_CONTAINS({JsonSerializer.Serialize(teacherCodes)}, c.code)";
-            await foreach (var item in cosmosClientIes.GetContainer(Constant.TEAMModelOS, Constant.School).GetItemQueryIteratorSql<JsonElement>(queryText: sqlTch, requestOptions: null))
+            ///虛擬校
+            await foreach (var item in cosmosClientIes.GetContainer(Constant.TEAMModelOS, Constant.School).GetItemQueryIteratorSql<JsonElement>(queryText: sqlSch, requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey($"VirtualBase") }))
             {
-                string tmid = item.GetProperty("id").ToString();
-                tmids.Add(tmid);
+                string schId = item.GetProperty("id").ToString();
+                if(!schIds.Contains(schId)) schIds.Add(schId);
             }
+            //結果輸出
+            List<string> tmids = new List<string>();
+            tmids = await GetTmidBySchoolId(schIds, hasMail);
+
+            //List<string> teacherCodes = schIds.Select(s => $"Teacher-{s}").ToList();
+            ////取得學校教師ID列表
+            //List<string> tmids = new List<string>();
+            //string sqlTch = $"SELECT DISTINCT c.id FROM c WHERE c.pk = 'Teacher' AND c.status = 'join' AND ARRAY_CONTAINS({JsonSerializer.Serialize(teacherCodes)}, c.code)";
+            //await foreach (var item in cosmosClientIes.GetContainer(Constant.TEAMModelOS, Constant.School).GetItemQueryIteratorSql<JsonElement>(queryText: sqlTch, requestOptions: null))
+            //{
+            //    string tmid = item.GetProperty("id").ToString();
+            //    tmids.Add(tmid);
+            //}
             return tmids;
         }
         //取得地理資訊所屬TMID
-        private async Task<List<string>> GetTmidByGeo(List<Geo> geos)
+        private async Task<List<string>> GetTmidByGeo(List<Geo> geos, bool hasMail)
         {
             List<string> tmids = new List<string>();
             foreach (Geo geo in geos)
             {
                 if(!string.IsNullOrWhiteSpace(geo.type) && geo.type.Equals("tmid"))
                 {
-                    var (geoInfos, _) = await GetDataByGeo("tmid", "tmid", true, geo.countryId, geo.provinceId, geo.cityId);
+                    var (geoInfos, _) = await GetDataByGeo("tmid", "tmid", true, geo.countryId, geo.provinceId, geo.cityId, hasMail);
                     foreach (var geoInfo in geoInfos)
                     {
                         if (!tmids.Contains(geoInfo.id)) tmids.Add(geoInfo.id);
@@ -1396,7 +1463,7 @@ namespace TEAMModelBI.Controllers.BICommon
                 }
                 else if(!string.IsNullOrWhiteSpace(geo.type) && geo.type.Equals("school"))
                 {
-                    var (geoInfos, _) = await GetDataByGeo("school", "tmid", true, geo.countryId, geo.provinceId, geo.cityId);
+                    var (geoInfos, _) = await GetDataByGeo("school", "tmid", true, geo.countryId, geo.provinceId, geo.cityId, hasMail);
                     foreach (var geoInfo in geoInfos)
                     {
                         if (!tmids.Contains(geoInfo.id)) tmids.Add(geoInfo.id);
@@ -1407,30 +1474,41 @@ namespace TEAMModelBI.Controllers.BICommon
         }
 
         //取得學校機構所屬學校教師
-        private async Task<List<string>> GetTmidByUnitId(List<string> units)
+        private async Task<List<string>> GetTmidByUnitId(List<string> units, bool hasMail = false)
         {
             if (units.Count.Equals(0)) return new List<string>();
 
-            var cosmosClientIes = _azureCosmos.GetCosmosClient();
+            //var cosmosClientIes = _azureCosmos.GetCosmosClient();
             var coreCosmosClientCn = _azureCosmos.GetCosmosClient(name: "CoreServiceV2CnRead");
-            //取得IES5學校ID
-            string iesSql = "SELECT c.id, c.type FROM c WHERE 1=1 ";
-            string iesWhere = string.Empty;
-            //IES學校ID取得、學校類型取得
-            List<string> iesSchIds = new List<string>();
-            Dictionary<string, string> schTypeDic = new Dictionary<string, string>(); //IES5學校ID與學校類型對照表
-            List<string> teacherCodes = new List<string>(); //用來取得老師資料的分區鍵
-            string sqlSch = $"{iesSql}{iesWhere}";
-            await foreach (var item in cosmosClientIes.GetContainer(Constant.TEAMModelOS, Constant.School).GetItemQueryIteratorSql<JsonElement>(queryText: sqlSch, requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey($"Base") }))
+            var cosmosClientCsv2 = _azureCosmos.GetCosmosClient(name: "CoreServiceV2");
+            List<string> tmids = new List<string>();
+            List<string> coreSchIds = new List<string>();
+            Dictionary<string, List<string>> schTmidDic = new Dictionary<string, List<string>>(); //學校ID > TMID 字典
+            //取得有歸戶的所有TMID
+            string sqlEx = "SELECT c.id, c.schoolCode, c.schoolCodeW FROM c WHERE ((IS_DEFINED(c.schoolCode) AND NOT IS_NULL(c.schoolCode)) OR (IS_DEFINED(c.schoolCodeW) AND NOT IS_NULL(c.schoolCodeW)))";
+            if (hasMail) sqlEx += " AND (IS_DEFINED(c.mail) AND NOT IS_NULL(c.mail)) ";
+            await foreach (var item in cosmosClientCsv2.GetContainer("Core", "ID2").GetItemQueryIteratorSql<JsonElement>(queryText: sqlEx, requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey($"base-ex") }))
             {
-                string schId = item.GetProperty("id").ToString();
-                int schType = item.GetProperty("type").GetInt32();
-                iesSchIds.Add(schId);
-                if (!schTypeDic.ContainsKey(schId)) schTypeDic.Add(schId, schType.ToString());
+                string tmid = item.GetProperty("id").ToString();
+                string schoolCode = (item.TryGetProperty("schoolCode", out JsonElement _schoolCode)) ? _schoolCode.ToString() : string.Empty;
+                string schoolCodeW = (item.TryGetProperty("schoolCodeW", out JsonElement _schoolCodeW)) ? _schoolCodeW.ToString() : string.Empty;
+                string schId = (!string.IsNullOrWhiteSpace(schoolCode)) ? schoolCode : (!string.IsNullOrWhiteSpace(schoolCodeW)) ? schoolCodeW : string.Empty;
+                if (!string.IsNullOrWhiteSpace(schId))
+                {
+                    if (!coreSchIds.Contains(schId)) coreSchIds.Add(schId);
+                    if (schTmidDic.ContainsKey(schId))
+                    {
+                        if (!schTmidDic[schId].Contains(tmid)) schTmidDic[schId].Add(tmid);
+                    }
+                    else
+                    {
+                        schTmidDic.Add(schId, new List<string>() { tmid });
+                    }
+                }
             }
             //取得Core學校ID及類型
             string coreSql = "SELECT c.shortCode, c.unitType FROM c";
-            string coreWhere = $" WHERE ARRAY_CONTAINS({JsonSerializer.Serialize(iesSchIds)}, c.shortCode) ";
+            string coreWhere = $" WHERE ARRAY_CONTAINS({JsonSerializer.Serialize(coreSchIds)}, c.shortCode) ";
             string coreWhereUnittype = string.Empty;
             if (units.Contains("1"))
             {
@@ -1461,85 +1539,79 @@ namespace TEAMModelBI.Controllers.BICommon
             await foreach (var item in coreCosmosClientCn.GetContainer("Core", "School").GetItemQueryIteratorSql<JsonElement>(queryText: sqlCore, requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey($"base") }))
             {
                 string shortCode = item.GetProperty("shortCode").ToString();
-                string unitType = item.GetProperty("unitType").ToString();
-                if (!teacherCodes.Contains($"Teacher-{shortCode}")) teacherCodes.Add($"Teacher-{shortCode}");
+                string unitType = (item.TryGetProperty("unitType", out JsonElement _unitType)) ? _unitType.ToString() : string.Empty;
                 if (!coreSchUnitTypeDic.ContainsKey(shortCode)) coreSchUnitTypeDic.Add(shortCode, unitType);
             }
             //學校類型轉換
-            Dictionary<string, string> finalSchUnitTypeDic = new Dictionary<string, string>();
-            foreach (KeyValuePair<string, string> item in coreSchUnitTypeDic)
-            {
-                string schId = item.Key;
-                string coreUnitType = (coreSchUnitTypeDic.ContainsKey(schId)) ? coreSchUnitTypeDic[schId] : string.Empty;
-                string iesUnitType = (schTypeDic.ContainsKey(schId)) ? schTypeDic[schId] : string.Empty;
-                string unitTypeF = string.Empty; //機構類型(最終判斷)
-                if (!string.IsNullOrWhiteSpace(coreUnitType))
-                {
-                    switch (coreUnitType)
-                    {
-                        case "1": //基礎教育機構
-                        case "8": //學前教育
-                            unitTypeF = "1"; // => 基礎教育機構(K-小學)
-                            break;
-                        case "2": //中等教育機構
-                            unitTypeF = "2"; // => 中等教育機構(國中、高中/職)
-                            break;
-
-                        case "3": //高等教育機構
-                            unitTypeF = "3"; // => 高等教育機構(大學、研究所)
-                            break;
-                        case "4": //政府單位機構
-                        case "5": //NGO機構
-                        case "6": //企業機構
-                        case "7": //其他
-                        case "9": //特殊教育
-                            unitTypeF = "4"; // => 其他
-                            break;
-                    }
-                }
-                else if (!string.IsNullOrWhiteSpace(iesUnitType))
-                {
-                    switch (iesUnitType)  //1 普教,2 高职教
-                    {
-                        case "1": //國教(K-12)
-                            unitTypeF = "1"; // => 基礎教育機構(K-小學)
-                            break;
-                        case "2": //大專院校
-                            unitTypeF = "3"; // => 高等教育機構(大學、研究所)
-                            break;
-                        default: //未設定
-                            unitTypeF = "4"; // => 其他
-                            break;
-                    }
-                }
-                finalSchUnitTypeDic.Add(schId, unitTypeF);
-            }
-            //取得學校所屬老師TMID
-            teacherCodes = finalSchUnitTypeDic.Select(s => $"Teacher-{s.Key}").ToList();
-            List<string> tmids = new List<string>();
-            string sqlTch = $"SELECT DISTINCT c.id FROM c WHERE c.pk = 'Teacher' AND c.status = 'join' AND ARRAY_CONTAINS({JsonSerializer.Serialize(teacherCodes)}, c.code)";
-            await foreach (var item in cosmosClientIes.GetContainer(Constant.TEAMModelOS, Constant.School).GetItemQueryIteratorSql<JsonElement>(queryText: sqlTch, requestOptions: null))
+            //Dictionary<string, string> finalSchUnitTypeDic = new Dictionary<string, string>();
+            //foreach (KeyValuePair<string, string> item in coreSchUnitTypeDic)
+            //{
+            //    string schId = item.Key;
+            //    string coreUnitType = (coreSchUnitTypeDic.ContainsKey(schId)) ? coreSchUnitTypeDic[schId] : string.Empty;
+            //    string unitTypeF = string.Empty; //機構類型(最終判斷)
+            //    if (!string.IsNullOrWhiteSpace(coreUnitType))
+            //    {
+            //        switch (coreUnitType)
+            //        {
+            //            case "1": //基礎教育機構
+            //            case "8": //學前教育
+            //                unitTypeF = "1"; // => 基礎教育機構(K-小學)
+            //                break;
+            //            case "2": //中等教育機構
+            //                unitTypeF = "2"; // => 中等教育機構(國中、高中/職)
+            //                break;
+
+            //            case "3": //高等教育機構
+            //                unitTypeF = "3"; // => 高等教育機構(大學、研究所)
+            //                break;
+            //            case "4": //政府單位機構
+            //            case "5": //NGO機構
+            //            case "6": //企業機構
+            //            case "7": //其他
+            //            case "9": //特殊教育
+            //                unitTypeF = "4"; // => 其他
+            //                break;
+            //        }
+            //    }
+                
+            //    finalSchUnitTypeDic.Add(schId, unitTypeF);
+            //}
+            //結果輸出
+            List<string> filterSchIds = coreSchUnitTypeDic.Keys.ToList();
+            var schTmidLists = schTmidDic.Where(s => filterSchIds.Contains(s.Key)).Select(s => s.Value).ToList();
+            foreach(List<string> schTmid in schTmidLists)
             {
-                string tmid = item.GetProperty("id").ToString();
-                tmids.Add(tmid);
+                tmids.AddRange(schTmid);
             }
-            return tmids;
+            tmids = tmids.Distinct().ToList();
 
+            return tmids;
         }
-        //取得學校所屬學校教師
-        private async Task<List<string>> GetTmidBySchoolId(List<string> schIds)
+        //取得歸戶學校的TMID
+        private async Task<List<string>> GetTmidBySchoolId(List<string> schIds, bool hasMail = false)
         {
-            var cosmosClientIes = _azureCosmos.GetCosmosClient();
-            //取得學校ID列表
-            List<string> teacherCodes = schIds.Select(s => $"Teacher-{s}").ToList();
-            //取得學校教師ID列表
+            var cosmosClientCsv2 = _azureCosmos.GetCosmosClient(name: "CoreServiceV2");
             List<string> tmids = new List<string>();
-            string sqlTch = $"SELECT DISTINCT c.id FROM c WHERE c.pk = 'Teacher' AND c.status = 'join' AND ARRAY_CONTAINS({JsonSerializer.Serialize(teacherCodes)}, c.code)";
-            await foreach (var item in cosmosClientIes.GetContainer(Constant.TEAMModelOS, Constant.School).GetItemQueryIteratorSql<JsonElement>(queryText: sqlTch, requestOptions: null))
+            string sqlEx = $"SELECT c.id, c.schoolCode, c.schoolCodeW FROM c WHERE ((ARRAY_CONTAINS({JsonSerializer.Serialize(schIds)}, c.schoolCode) OR ARRAY_CONTAINS({JsonSerializer.Serialize(schIds)}, c.schoolCodeW))) ";
+            if (hasMail) sqlEx += " AND (IS_DEFINED(c.mail) AND NOT IS_NULL(c.mail) AND c.mail != '')";
+            await foreach (var item in cosmosClientCsv2.GetContainer("Core", "ID2").GetItemQueryIteratorSql<JsonElement>(queryText: sqlEx, requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey($"base-ex") }))
             {
                 string tmid = item.GetProperty("id").ToString();
-                tmids.Add(tmid);
+                string schoolCode = (item.TryGetProperty("schoolCode", out JsonElement _schoolCode)) ? _schoolCode.ToString() : string.Empty;
+                string schoolCodeW = (item.TryGetProperty("schoolCodeW", out JsonElement _schoolCodeW)) ? _schoolCodeW.ToString() : string.Empty;
+                string schId = (!string.IsNullOrWhiteSpace(schoolCode)) ? schoolCode : (!string.IsNullOrWhiteSpace(schoolCodeW)) ? schoolCodeW : string.Empty;
+                if(schIds.Contains(schId)) tmids.Add(tmid);
             }
+            ////取得學校ID列表
+            //List<string> teacherCodes = schIds.Select(s => $"Teacher-{s}").ToList();
+            ////取得學校教師ID列表
+            //List<string> tmids = new List<string>();
+            //string sqlTch = $"SELECT DISTINCT c.id FROM c WHERE c.pk = 'Teacher' AND c.status = 'join' AND ARRAY_CONTAINS({JsonSerializer.Serialize(teacherCodes)}, c.code)";
+            //await foreach (var item in cosmosClientIes.GetContainer(Constant.TEAMModelOS, Constant.School).GetItemQueryIteratorSql<JsonElement>(queryText: sqlTch, requestOptions: null))
+            //{
+            //    string tmid = item.GetProperty("id").ToString();
+            //    tmids.Add(tmid);
+            //}
             return tmids;
         }
 
@@ -1558,10 +1630,6 @@ namespace TEAMModelBI.Controllers.BICommon
             NotifyData notify = new NotifyData();
             notify.hubName = hubName;
             notify.sender = sender;
-//#if DEBUG //測試模式時限制TMID帳號,正式站佈署時不生效
-//            List<string> filterTmid = new List<string>() { "1522758684", "1595321354" };
-//            tmIds = tmIds.Intersect(filterTmid).ToList();
-//#endif
             notify.tags = tmIds.Select(s => $"{s}_{sender}").ToList();
             notify.title = title;
             notify.body = body;
@@ -1583,15 +1651,153 @@ namespace TEAMModelBI.Controllers.BICommon
             return responseMessage;
         }
 
-        //BI訊息推送DB記入
-        private async Task<BINotice> CrtBiNoticeData(BINotice bINotice)
+        private async Task CallSendMailApiAsync(string email, string subject, string title, string body, string template, string image)
+        {
+            dynamic mailVars = new ExpandoObject();
+            mailVars.sub = subject;
+            mailVars.title = title;
+            mailVars.body = body;
+            if (!string.IsNullOrWhiteSpace(image))
+                mailVars.image = image;
+            await _coreAPIHttpService.SendMail(new Dictionary<string, object> { { "to", email }, { "tid", template }, { "vars", mailVars } }, _option.Location, _configuration);
+        }
+
+        /// <summary>
+        /// 取得訊息寄送紀錄
+        /// </summary>
+        /// <param name="msgType"></param>
+        /// <param name="selType"></param>
+        /// <param name="theme">標題(title)</param>
+        /// <param name="content">內文</param>
+        /// <param name="source">發送源(HiTeach、IES、Sokrates、Auth、Event)</param>
+        /// <returns></returns>
+        [HttpPost("get-notice-history")]
+#if !DEBUG
+        [AuthToken(Roles = "admin")]
+#endif
+        public async Task<List<BINotice>> GetBINoticeHistory(JsonElement jsonElement)
         {
-            var cosmosClientIes = _azureCosmos.GetCosmosClient(); //IES
-            bINotice.id = Guid.NewGuid().ToString();
-            BINotice result = await cosmosClientIes.GetContainer(Constant.TEAMModelOS, "Common").CreateItemAsync<BINotice>(bINotice, new PartitionKey("BINotice"));
+            string msgType = (jsonElement.TryGetProperty("msgType", out JsonElement _msgType)) ? _msgType.ToString() : string.Empty; //發送類型 mail:郵件、notify:端外、sms:簡訊
+            string selType = (jsonElement.TryGetProperty("selType", out JsonElement _selType)) ? _selType.ToString() : string.Empty; //挑選方式 single:個別 multi:批次
+            string theme = (jsonElement.TryGetProperty("theme", out JsonElement _theme)) ? _theme.ToString() : string.Empty; //標題(title)
+            string content = (jsonElement.TryGetProperty("content", out JsonElement _content)) ? _content.ToString() : string.Empty; //內文
+            string source = (jsonElement.TryGetProperty("source", out JsonElement _source)) ? _source.ToString() : string.Empty; //发送消息来源 HiTeach、IES、Sokrates、Auth、Event
+            string createId = (jsonElement.TryGetProperty("createId", out JsonElement _createId)) ? _createId.ToString() : string.Empty;
+            Geo geo = (jsonElement.TryGetProperty("geo", out JsonElement _geo)) ? _geo.ToObject<Geo>() : null;
+            List<string> unitType = (jsonElement.TryGetProperty("unitType", out JsonElement _unitType)) ? _unitType.ToObject<List<string>>() : null;
+            List<string> school = (jsonElement.TryGetProperty("school", out JsonElement _school)) ? _school.ToObject<List<string>>() : null;
+            List<string> crowdIds = (jsonElement.TryGetProperty("crowdIds", out JsonElement _crowdIds)) ? _crowdIds.ToObject<List<string>>() : null; //寄送對象(tmid列表)
+            long sendTimeFrom = (jsonElement.TryGetProperty("sendTimeFrom", out JsonElement _sendTimeFrom)) ?  _sendTimeFrom.GetInt64() : 0;
+            long sendTimeTo = (jsonElement.TryGetProperty("sendTimeTo", out JsonElement _sendTimeTo)) ? _sendTimeTo.GetInt64() : 0;
+
+            var cosmosClientIes = _azureCosmos.GetCosmosClient();
+            List<BINotice> result = new List<BINotice> ();
+            string sqlWhere = string.Empty;
+            if(!string.IsNullOrWhiteSpace(msgType)) 
+                sqlWhere += $" AND c.msgType = '{msgType}' ";
+            if (!string.IsNullOrWhiteSpace(selType)) 
+                sqlWhere += $" AND c.selType = '{selType}' ";
+            if (!string.IsNullOrWhiteSpace(theme))
+                sqlWhere += $" AND CONTAINS(c.theme, '{theme}') ";
+            if (!string.IsNullOrWhiteSpace(content))
+                sqlWhere += $" AND CONTAINS(c.content, '{content}') ";
+            if (!string.IsNullOrWhiteSpace(source))
+                sqlWhere += $" AND c.source = '{source}' ";
+            if (geo != null)
+            {
+                if(geo.type.Equals("tmid"))
+                    sqlWhere += $" AND cs.mode = 'tmidGeo' ";
+                else if(geo.type.Equals("school"))
+                    sqlWhere += $" AND cs.mode = 'schGeo' ";
+                if (!string.IsNullOrWhiteSpace(geo.countryId))
+                    sqlWhere += $" AND cs.countryId = '{geo.countryId}' ";
+                if (!string.IsNullOrWhiteSpace(geo.provinceId))
+                    sqlWhere += $" AND cs.provinceId = '{geo.provinceId}' ";
+                if (!string.IsNullOrWhiteSpace(geo.cityId))
+                    sqlWhere += $" AND cs.cityId = '{geo.cityId}' ";
+                if (!string.IsNullOrWhiteSpace(geo.distId))
+                    sqlWhere += $" AND cs.distId = '{geo.distId}' ";
+            }
+            if (school != null && school.Count > 0)
+                sqlWhere += $" AND ARRAY_CONTAINS({JsonSerializer.Serialize(school)}, cs.school) ";
+            if (crowdIds != null && crowdIds.Count > 0)
+                sqlWhere += $" AND EXISTS( SELECT VALUE 1 FROM crowdId IN c.crowdIds WHERE ARRAY_CONTAINS({JsonSerializer.Serialize(crowdIds)}, crowdId) ) ";
+            if (unitType != null && unitType.Count > 0)
+                sqlWhere += $" AND ARRAY_CONTAINS({JsonSerializer.Serialize(unitType)}, cs.unitType) ";
+            if (sendTimeFrom > 0)
+                sqlWhere += $" AND c.sendTime >= {sendTimeFrom} ";
+            if (sendTimeTo > 0)
+                sqlWhere += $" AND c.sendTime <= {sendTimeTo} ";
+            string sql = $"SELECT c.msgType, c.selType, c.hubName, c.subject, c.template, c.search, c.data, c.type, c.theme, c.content, c.crowd, c.crowdIds, c.createId, c.sendTime, c.createTime, c.source FROM c JOIN cs IN c.search WHERE 1=1 {sqlWhere}";
+            await foreach (var item in cosmosClientIes.GetContainer(Constant.TEAMModelOS, "Common").GetItemQueryIteratorSql<BINotice>(queryText: sql, requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"BINotice") }))
+            {
+                result.Add(item);
+            }
             return result;
         }
 
+        //將訊息寄送轉換為訊息寄送DB紀錄架構
+        private BINotice TransMsgRequestToBINotice(string msgType, string method, string subject, string title, string body, string sender, string hubName, SendMessageParam target, List<string> tmids, string data)
+        {
+            List<PickParam> searchParams = new List<PickParam>();
+            ///學區
+            if (target.area.Count > 0)
+            {
+                foreach (string areaId in target.area)
+                {
+                    searchParams.Add(new PickParam() { mode = "area", areaId = areaId });
+                }
+            }
+            ///地理資訊
+            if (target.geo.Count > 0)
+            {
+                foreach (Geo geo in target.geo)
+                {
+                    PickParam pickParam = new PickParam() { mode = (geo.type.Equals("tmid")) ? "tmidGeo" : "schGeo" };
+                    if (!string.IsNullOrWhiteSpace(geo.countryId)) pickParam.countryId = geo.countryId;
+                    if (!string.IsNullOrWhiteSpace(geo.provinceId)) pickParam.provinceId = geo.provinceId;
+                    if (!string.IsNullOrWhiteSpace(geo.cityId)) pickParam.cityId = geo.cityId;
+                    if (!string.IsNullOrWhiteSpace(geo.distId)) pickParam.distId = geo.distId;
+                }
+            }
+            ///教育機構
+            if (target.unit.Count > 0)
+            {
+                foreach (string unitType in target.unit)
+                {
+                    searchParams.Add(new PickParam() { mode = "unit", unitType = unitType });
+                }
+            }
+            ///學校
+            if (target.school.Count > 0)
+            {
+                foreach (string schId in target.school)
+                {
+                    searchParams.Add(new PickParam() { mode = "school", school = schId });
+                }
+            }
+            ///TMID
+            if (target.tmid.Count > 0)
+            {
+                searchParams.Add(new PickParam() { mode = "tmid", tmId = target.tmid });
+            }
+            BINotice bINotice = new BINotice()
+            {
+                msgType = msgType,
+                selType = method,
+                search = searchParams,
+                crowdIds = tmids,
+                theme = title,
+                content = body,
+                subject = subject,
+                createTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
+                source = sender,
+                hubName = hubName,
+                data = data
+            };
+            return bINotice;
+        }
+
         private string GetDictionaryKeyByValue(Dictionary<string, string> dic, string value)
         {
             string result = string.Empty;

+ 12 - 12
TEAMModelBI/Controllers/BITest/TestController.cs

@@ -1835,20 +1835,20 @@ namespace TEAMModelBI.Controllers.BITest
             bINoticeM.id = "6ec182ab-ad08-43f4-8cc2-6fdbd985f584";
             bINoticeM.msgType = "email";
             bINoticeM.selType = "mul";
-            bINoticeM.accept = new List<PickParam>();
+            bINoticeM.search = new List<PickParam>();
             ///學區
             PickParam area = new PickParam() {
                 mode = "area",
                 areaId = "02944f32-f534-3397-ea56-e6f1fc6c3714"
             };
-            bINoticeM.accept.Add(area);
+            bINoticeM.search.Add(area);
             ///地理 學校
             PickParam geoSch = new PickParam() {
                 mode = "schGeo",
                 countryId = "TW",
                 cityId = "30"
             };
-            bINoticeM.accept.Add(geoSch);
+            bINoticeM.search.Add(geoSch);
             ///地理 ID
             PickParam geoId = new PickParam()
             {
@@ -1856,21 +1856,21 @@ namespace TEAMModelBI.Controllers.BITest
                 countryId = "TW",
                 cityId = "30"
             };
-            bINoticeM.accept.Add(geoId);
+            bINoticeM.search.Add(geoId);
             ///機構
             PickParam inst = new PickParam()
             {
-                mode = "inst",
+                mode = "unit",
                 unitType = "1"
             };
-            bINoticeM.accept.Add(inst);
+            bINoticeM.search.Add(inst);
             ///學校ID
             PickParam sch = new PickParam()
             {
-                mode = "sch",
-                school = new NoticeSchool() { eduId = "hbgl", shortCode = "hbgl", name = "測試學校" }
+                mode = "school",
+                school = "hbgl"
             };
-            bINoticeM.accept.Add(sch);
+            bINoticeM.search.Add(sch);
             await client.GetContainer(Constant.TEAMModelOS, Constant.Common).UpsertItemAsync(bINoticeM, new PartitionKey($"BINotice"));
 
             //個別
@@ -1918,13 +1918,13 @@ namespace TEAMModelBI.Controllers.BITest
             PickParam schPre1 = new PickParam()
             {
                 mode = "sch",
-                school = new NoticeSchool() { eduId = "hbgl", shortCode = "hbgl", name = "測試學校" }
+                school = "hbgl"
             };
             bINoticeS.search.Add(schPre1);
             PickParam schPre2 = new PickParam()
             {
                 mode = "sch",
-                school = new NoticeSchool() { eduId = "habook", shortCode = "habook", name = "玉山學校" }
+                school = "habook"
             };
             bINoticeS.search.Add(schPre2);
 
@@ -1934,7 +1934,7 @@ namespace TEAMModelBI.Controllers.BITest
             };
             tmidList.tmId.Add("1595321354");
             tmidList.tmId.Add("1522758684");
-            bINoticeS.accept.Add(tmidList);
+            bINoticeS.search.Add(tmidList);
             await client.GetContainer(Constant.TEAMModelOS, Constant.Common).UpsertItemAsync(bINoticeS, new PartitionKey($"BINotice"));
 
             return Ok(new { state = 200 });

TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/view/login/background.jpg → TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/image/background.jpg


+ 2 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/view/admin/ActivityManage.less

@@ -1,6 +1,7 @@
 .el-container {
     height: 100%;
     background-color: #F7F5F4;
+    position: relative;
 
     .base-user-center {
         position: absolute;
@@ -262,6 +263,7 @@
         top: 70px;
         right: 20px;
         width: 20%;
+        height: calc(100% - 70px);
 
         .info-box {
             display: flex;

+ 36 - 9
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/view/admin/ActivityManage.vue

@@ -78,7 +78,10 @@
                                 <el-button type="warning" size="mini" v-show="needUpdate.status === 1" @click="updatePackage()">立即更新</el-button>
                                 <span v-show="needUpdate.status === 2">该活动在云端不存在或未联网</span>
                             </span>
+                            <el-button v-show="!needUpdate.status" size="mini" style="margin-left: 10px;" @click="updatePackage()">重新下载资源</el-button>
                             <el-button size="mini" style="margin-left: 10px;" @click="pushAnswer()">推送学生作答</el-button>
+                            <el-button size="mini" style="margin-left: 10px;" @click="uploadAnswer()">导出学生作答</el-button>
+                            <el-button size="mini" type="warning" plain style="margin-left: 10px;" @click="cleanPackage()">清理资源</el-button>
                         </span>
                     </div>
                     <el-tabs v-model="activeName" :before-leave="beforeTabLeave">
@@ -97,9 +100,15 @@
                                     提取码:
                                     <span style="color: #24b880; font-size: 20px; font-weight: bold;">{{ evaluationClient.shortCode }}</span>
                                 </div>
+                                <div class="info-tag">学生总数:
+                                    <span>{{ evaluationClient.studentCount }}人</span>
+                                </div>
+                                <div class="info-tag">资源大小:
+                                    <span>{{ evaluationClient.fileSize }}M</span>
+                                </div>
                                 <div class="paper-content">
                                     <div v-for="(item, index) in evaluationClient.subjects" :key="index">
-                                        <p class="subject-name">{{ item.subjectName }}</p>
+                                        <p class="subject-name">{{ item.subjectName }}({{ item.papers.length }}份)</p>
                                         <div>
                                             <el-tag type="warning" v-for="(papers, pIndex) in item.papers" :key="pIndex" @click="openPaper(index, pIndex)">{{ papers.paperName }}</el-tag>
                                         </div>
@@ -244,13 +253,15 @@
             </div>
         </el-dialog>
         <div class="open-evaluation" v-if="showErrorMsgs">
-            <div class="info-box error-info" v-for="(item, index) in openErrorMsgs" :key="index">
-                <p :title="item">
-                    <i class="el-icon-error"></i>
-                    {{ item }}
-                </p>
-                <span><i class="el-icon-close" @click="delInfo(index)"></i></span>
-            </div>
+            <vuescroll>
+                <div class="info-box error-info" v-for="(item, index) in openErrorMsgs" :key="index">
+                    <p :title="item">
+                        <i class="el-icon-error"></i>
+                        {{ item }}
+                    </p>
+                    <span><i class="el-icon-close" @click="delInfo(index)"></i></span>
+                </div>
+            </vuescroll>
             <!-- <div class="info-box success-info">
                 <p>
                     <i class="el-icon-success"></i>
@@ -269,6 +280,7 @@
 import {jwtDecode} from 'jwt-decode'
 import { Loading } from 'element-ui'
 import TestPaper from './TestPaper.vue'
+import vuescroll from 'vuescroll'
 
 export default {
     components: { TestPaper },
@@ -380,6 +392,7 @@ export default {
                             group.isUesed = false
                             return group
                         })
+                        item.fileSize = ((item.blobSize + item.dataSize) / 1024 / 1024).toFixed(2)
                         return item
                     })
                     if(this.examList.length) this.onSelectAct(0)
@@ -642,7 +655,7 @@ export default {
             this.openErrorMsgs = []
             this.isLoading = Loading.service({
                 lock: true,
-                text: '加载中',
+                text: '资源下载中...',
                 background: 'rgba(0, 0, 0, 0.7)'
             })
             let params = {
@@ -807,6 +820,20 @@ export default {
             this.isPushAnswer = true
             this.isInputOpen = true
         },
+        cleanPackage() {
+            // 等待后端接口
+            this.$message({
+                message: '暂未开发',
+                type: 'warning'
+            });
+        },
+        uploadAnswer() {
+            // 等待后端接口
+            this.$message({
+                message: '暂未开发',
+                type: 'warning'
+            });
+        },
         loginOut() {
             localStorage.removeItem('auth_token')
 			localStorage.removeItem('expires_in')

+ 4 - 4
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/view/login/Admin.vue

@@ -86,7 +86,7 @@
                     <span>{{ deviceInfo.ip }}</span>
                 </el-form-item>
                 <el-form-item label="内存大小:">
-                    <span>{{ deviceInfo.server.shwoRam }}GB</span>
+                    <span>{{ deviceInfo.server.showRam }}GB</span>
                 </el-form-item>
                 <el-form-item label="处理器:">
                     <span v-for="(item, index) in deviceInfo.server.cpuInfos" :key="index" style="display: block;">{{ item.name }} {{ item.showHZ }}Hz({{ deviceInfo.server.cpu }}核)</span>
@@ -184,7 +184,7 @@ export default {
                         res.data.server.host.push(url)
                     })
                 })
-                res.data.server.shwoRam = (res.data.server.ram / 1024 / 1024 / 1024).toFixed(1)
+                res.data.server.showRam = (res.data.server.ram / 1024 / 1024 / 1024).toFixed(1)
                 res.data.server.cpuInfos.forEach(item => {
                     item.showHZ = item.hz ? (item.hz / 1000) : 0
                 });
@@ -308,7 +308,7 @@ export default {
                             res.data.server.host.push(url)
                         })
                     })
-                    res.data.server.shwoRam = (res.data.server.ram / 1024 / 1024 / 1024).toFixed(1)
+                    res.data.server.showRam = (res.data.server.ram / 1024 / 1024 / 1024).toFixed(1)
                     res.data.server.cpuInfos.forEach(item => {
                         item.showHZ = item.hz ? (item.hz / 1000) : 0
                     });
@@ -381,7 +381,7 @@ export default {
     width: 100%;
     height: 100%;
     background: #F4F7FF;
-    background-image: url(./background.jpg);
+    background-image: url(@/assets/image/background.jpg);
     background-repeat: no-repeat;
     background-attachment: fixed;
     background-position: 50%;

+ 1 - 1
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/view/login/Student.vue

@@ -126,7 +126,7 @@ export default {
     height: 100%;
     background: #F4F7FF;
     position: relative;
-    background-image: url(./background.jpg);
+    background-image: url(@/assets/image/background.jpg);
     background-repeat: no-repeat;
     background-attachment: fixed;
     background-position: 50%;

+ 17 - 5
TEAMModelOS.SDK/Models/Cosmos/BI/BICommon/Notice.cs

@@ -25,13 +25,25 @@ namespace TEAMModelOS.SDK.Models.Cosmos.BI
         /// </summary>
         public string selType { get; set; }
         /// <summary>
+        /// 訊息中樞
+        /// </summary>
+        public string hubName { get; set; }
+        /// <summary>
+        /// 主旨 ※Email用
+        /// </summary>
+        public string subject { get; set; }
+        /// <summary>
+        /// 模板ID ※Email用
+        /// </summary>
+        public string template { get; set; }
+        /// <summary>
         /// 搜尋條件 ※複數
         /// </summary>
         public List<PickParam> search { get; set; } = new();
         /// <summary>
-        /// 接收名單 ※複數
+        /// 附加資料 ※JSON to string
         /// </summary>
-        public List<PickParam> accept { get; set; } = new();
+        public string data { get; set; }
         #endregion
 
         #region 原有舊通知類型
@@ -89,7 +101,7 @@ namespace TEAMModelOS.SDK.Models.Cosmos.BI
         /// </summary>
         public long createTime { get; set; }
         /// <summary>
-        /// 发送消息来源, 默认(BI)、IES5、HiTA
+        /// 发送消息来源, HiTeach、IES、Sokrates、Auth、Event
         /// </summary>
         public string source { get; set; } = "IES";
         #endregion
@@ -99,7 +111,7 @@ namespace TEAMModelOS.SDK.Models.Cosmos.BI
     /// </summary>
     public class PickParam
     {
-        public string mode { get; set; } //格式 area:學區 schGeo:學校地理 inst:機構 sch:學校     crtTime:帳號生成時間 tmidGeo:TMID地理 softUse:使用軟體 point:積分範圍 tmid:帳號列表(個別挑選用)
+        public string mode { get; set; } //格式 area:學區 schGeo:學校地理 tmidGeo:TMID地理 unit:機構 school:學校 crtTime:帳號生成時間 softUse:使用軟體 point:積分範圍 tmid:帳號列表(個別挑選用)
         public string areaId { get; set; } //學區ID ※area專有
         public string countryId { get; set; } //國ID ※schGeo、tmidGeo專有
         public string provinceId { get; set; } //省ID ※schGeo、tmidGeo專有
@@ -112,7 +124,7 @@ namespace TEAMModelOS.SDK.Models.Cosmos.BI
         public string softUseMode { get; set; } //軟體使用模式 and or ※softUse專有
         public int pointFrom { get; set; } //積分範圍 起始 ※point專有
         public int pointTo { get; set; } //積分範圍 終止 ※point專有
-        public NoticeSchool school { get; set; } //學校 ※sch專有
+        public string school { get; set; } //學校ID ※sch專有
         public List<string> tmId { get; set; } = new(); //TMID列表 ※tmid專有
     }
     /// <summary>

+ 7 - 3
TEAMModelOS/Controllers/XTest/TestController.cs

@@ -2572,11 +2572,15 @@ namespace TEAMModelOS.Controllers
         public async Task<IActionResult> TestSendMail(JsonElement json)
         {
             string mail = "jeff@habook.com.tw";
-                          
-            string tid = "d-833d40ac6397414b852b91e2fa45850a"; //活動報名成功通知模板
+            string tid = "d-f1c5abd8247f4be79ceaecdd327e9a68"; //"d-f1c5abd8247f4be79ceaecdd327e9a68":國際站預設模板  //"d-833d40ac6397414b852b91e2fa45850a":活動報名成功通知模板
             string name = "Jeff";
             string location = "Global";
-            await _coreAPIHttpService.SendMail(new Dictionary<string, object> { { "to", mail }, { "tid", tid }, { "vars", new { name = name } } }, location, _configuration);
+            string sub = "New subject";
+            string title = "HiTeach 6";
+            string body = "HiTeach 發布了!";
+            string image = "https://corestorageservice.blob.core.windows.net/public-marketing/S__43540491.png";
+            object data = new { sub = sub, title = title, body = body };
+            await _coreAPIHttpService.SendMail(new Dictionary<string, object> { { "to", mail }, { "tid", tid }, { "vars", data } }, location, _configuration);
 
             return Ok();
         }