Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions AutoReturn.gs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/**
* 「帙雲」02 - 自動發還課業
*
* 功用:將老師批改完畢、放在「03_老師回饋區」的檔案,自動歸類至「04_已發還課業」。
* 方法:提取檔案名稱中的【班別】、【姓名】及課業【關鍵詞】,並配對至對應的學生文件夾。
*
* 觸發器:distributeHomework,每 15 分鐘觸發一次。
* 執行 createReturnTrigger() 可自動建立觸發器。
*
* 注意:ROOT_FOLDER_ID、getConfig() 及 getOrCreateFolder() 定義於 Shared.gs。
*/

// ─────────────────────────────────────────────────────────────────────────────
// 主函數
// ─────────────────────────────────────────────────────────────────────────────

/**
* 將「03_老師回饋區」中的批改課業,自動歸類至「04_已發還課業」對應的學生文件夾。
*
* 「04_已發還課業」的文件夾結構:
* 04_已發還課業/
* 【1C】/
* 【陳大文】/
* 寫作(長文)/
* 《藏在泥土的【寶物】》/
* 閱讀/
* 寫作(實用文)/
*/
function distributeHomework() {
const config = getConfig();
const uploadFolderId = config.TEACHER_RETURN_FOLDER_ID; // 03_老師回饋區
const returnFolderId = config.RETURNED_FOLDER_ID; // 04_已發還課業

const uploadFolder = DriveApp.getFolderById(uploadFolderId);
const returnFolder = DriveApp.getFolderById(returnFolderId);

// 步驟 1:獲取班別資料夾並建立映射(鍵:1C, 4A, ...)
const classFolderIter = returnFolder.getFolders();
const classMap = {};
while (classFolderIter.hasNext()) {
const classFolder = classFolderIter.next();
const className = classFolder.getName(); // 例如 "【1C】"
const classKey = className.replace(/【|】/g, ''); // 提取 "1C"
classMap[classKey] = classFolder;
}

// 步驟 2:獲取學生資料夾並建立映射(鍵:classKey_studentKey)
const studentMap = {};
for (const classKey in classMap) {
const classFolder = classMap[classKey];
const studentFolderIter = classFolder.getFolders();
while (studentFolderIter.hasNext()) {
const studentFolder = studentFolderIter.next();
const studentName = studentFolder.getName(); // 例如 "【陳大文】"
const studentKey = studentName.replace(/【|】/g, ''); // 提取 "陳大文"
if (!studentMap[classKey]) studentMap[classKey] = {};
studentMap[classKey][studentKey] = studentFolder;
}
}

// 步驟 3:在每位學生的「寫作(長文)」子文件夾下,提取課業關鍵詞
const assignmentMap = {};
for (const classKey in studentMap) {
for (const studentKey in studentMap[classKey]) {
const studentFolder = studentMap[classKey][studentKey];
const writingFolderIter = studentFolder.getFoldersByName('寫作(長文)');
if (writingFolderIter.hasNext()) {
const writingFolder = writingFolderIter.next();
const assignmentFolderIter = writingFolder.getFolders();
while (assignmentFolderIter.hasNext()) {
const assignmentFolder = assignmentFolderIter.next();
const match = assignmentFolder.getName().match(/【(.*?)】/);
if (match) {
const keyword = match[1];
assignmentMap[classKey + '_' + studentKey + '_' + keyword] = assignmentFolder;
}
}
}
}
}

// 步驟 4:處理上傳資料夾中的所有檔案
const files = uploadFolder.getFiles();
while (files.hasNext()) {
const file = files.next();
const fileName = file.getName();

// 提取班別
let fileClassKey = null;
for (const ck in classMap) {
if (fileName.indexOf(ck) !== -1) {
fileClassKey = ck;
break;
}
}

// 提取姓名
let fileStudentKey = null;
if (fileClassKey && studentMap[fileClassKey]) {
for (const sk in studentMap[fileClassKey]) {
if (fileName.indexOf(sk) !== -1) {
fileStudentKey = sk;
break;
}
}
}

// 提取課業關鍵詞
let assignmentKeyword = null;
if (fileClassKey && fileStudentKey) {
for (const key in assignmentMap) {
const parts = key.split('_');
if (parts[0] === fileClassKey && parts[1] === fileStudentKey) {
const keyword = parts[2];
if (fileName.indexOf(keyword) !== -1) {
assignmentKeyword = keyword;
break;
}
}
}
}

// 步驟 5:根據匹配情況移動檔案
if (fileClassKey && fileStudentKey && assignmentKeyword) {
// 完整匹配:移動到課業資料夾
const targetFolder = assignmentMap[fileClassKey + '_' + fileStudentKey + '_' + assignmentKeyword];
file.moveTo(targetFolder);
} else if (fileClassKey && fileStudentKey) {
// 缺少課業名稱:移動到學生資料夾
file.moveTo(studentMap[fileClassKey][fileStudentKey]);
} else if (fileClassKey) {
// 只有班別:移動到班別資料夾
file.moveTo(classMap[fileClassKey]);
}
// 完全無法匹配,留在原地
}
}

// ─────────────────────────────────────────────────────────────────────────────
// 輔助函數
// ─────────────────────────────────────────────────────────────────────────────

// ─────────────────────────────────────────────────────────────────────────────
// 觸發器設置
// ─────────────────────────────────────────────────────────────────────────────

/**
* 建立每 15 分鐘觸發一次 distributeHomework 的時間觸發器。
*/
function createReturnTrigger() {
ScriptApp.getProjectTriggers().forEach(function(trigger) {
if (trigger.getHandlerFunction() === 'distributeHomework') {
ScriptApp.deleteTrigger(trigger);
}
});
ScriptApp.newTrigger('distributeHomework')
.timeBased()
.everyMinutes(15)
.create();
Logger.log('✅ 已建立觸發器:distributeHomework,每 15 分鐘觸發一次。');
}
116 changes: 116 additions & 0 deletions AutoShare.gs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/**
* 「帙雲」03 - 自動共用、收集位址
*
* 功用:將「04_已發還課業」中每位學生的專屬文件夾共用給學生,
* 並將文件夾位址記錄至「自動共用、收集位址」試算表的 C 欄,
* 方便批量分發給學生。
*
* 方法:在試算表中手動輸入學號(A 欄)及學生姓名(B 欄),
* 然後手動執行 shareAllClasses() 或針對特定班別執行 shareFoldersForClass()。
*
* 觸發器:不設觸發器,手動執行。
*
* 試算表格式(每個分頁對應一個班別):
* A1=學號, B1=姓名, C1=文件夾位址
* A2 起:實際學號, B2 起:實際姓名, C2 起(自動填入):文件夾 URL
*
* 注意:ROOT_FOLDER_ID、SCHOOL_EMAIL_DOMAIN、getConfig() 及 getOrCreateFolder()
* 定義於 Shared.gs,此處直接使用。
*/

// ─────────────────────────────────────────────────────────────────────────────
// 主函數
// ─────────────────────────────────────────────────────────────────────────────

/**
* 針對試算表中所有班別,共用學生專屬文件夾並收集位址。
* 試算表中每個分頁(Sheet)對應一個班別,分頁名稱即班別名稱(如 1C)。
*/
function shareAllClasses() {
const config = getConfig();
const spreadsheet = SpreadsheetApp.openById(config.SHARE_SHEET_ID);
const returnedFolder = DriveApp.getFolderById(config.RETURNED_FOLDER_ID);

// 取得「04_已發還課業」下所有班別文件夾
const classFolderIter = returnedFolder.getFolders();
while (classFolderIter.hasNext()) {
const classFolder = classFolderIter.next();
const classKey = classFolder.getName().replace(/【|】/g, ''); // 去除【】
Logger.log('處理班別:' + classKey);
shareFoldersForClass(classKey, classFolder, spreadsheet);
}
}

/**
* 針對特定班別,共用學生專屬文件夾並收集位址。
*
* @param {string} className 班別名稱,如 "1C"
* @param {GoogleAppsScript.Drive.Folder} [classFolderOverride] 可選,直接傳入班別文件夾
* @param {GoogleAppsScript.Spreadsheet.Spreadsheet} [spreadsheetOverride] 可選,直接傳入試算表
*/
function shareFoldersForClass(className, classFolderOverride, spreadsheetOverride) {
const config = getConfig();
const spreadsheet = spreadsheetOverride || SpreadsheetApp.openById(config.SHARE_SHEET_ID);
const returnedFolder = DriveApp.getFolderById(config.RETURNED_FOLDER_ID);

// 找到對應的班別文件夾(名稱格式為「【1C】」)
let classFolder = classFolderOverride;
if (!classFolder) {
const iter = returnedFolder.getFoldersByName('【' + className + '】');
if (!iter.hasNext()) {
throw new Error('找不到班別文件夾:【' + className + '】');
}
classFolder = iter.next();
}

// 取得或建立試算表分頁
let sheet = spreadsheet.getSheetByName(className);
if (!sheet) {
sheet = spreadsheet.insertSheet(className);
sheet.getRange('A1:C1').setValues([['學號', '姓名', '文件夾位址']]);
sheet.getRange('A1:C1').setFontWeight('bold');
}

// 獲取學號及姓名(A2:B 往下)
const lastRow = sheet.getLastRow();
if (lastRow < 2) {
Logger.log('班別 ' + className + ' 的試算表中尚未輸入學生資料,跳過。');
return;
}
const values = sheet.getRange('A2:B' + lastRow).getValues();
const students = values.filter(function(row) { return row[0] && row[1]; });

// 建立姓名→文件夾映射
const folderMap = {};
const studentFolderIter = classFolder.getFolders();
while (studentFolderIter.hasNext()) {
const folder = studentFolderIter.next();
const match = folder.getName().match(/【(.*?)】/);
if (match) folderMap[match[1]] = folder;
}

// 共用並填入 URL
students.forEach(function(student, index) {
const studentId = student[0];
const studentName = student[1];
const email = studentId + '@' + SCHOOL_EMAIL_DOMAIN;
const folder = folderMap[studentName];

if (folder) {
try {
folder.addEditor(email);
sheet.getRange(index + 2, 3).setValue(folder.getUrl());
Logger.log('已共用文件夾給 ' + studentName + ' (' + email + ')');
} catch (e) {
Logger.log('共用失敗:' + studentName + ' - ' + e.message);
}
} else {
Logger.log('找不到學生文件夾:' + studentName);
}
});
}

// ─────────────────────────────────────────────────────────────────────────────
// 輔助函數
// ─────────────────────────────────────────────────────────────────────────────

Loading
Loading