Initial commit

This commit is contained in:
Nvex
2025-10-24 12:53:22 +08:00
parent fb98a84ea5
commit fe802b9f34
7 changed files with 158 additions and 66 deletions

View File

@@ -237,6 +237,4 @@ A: 确保 Excel 文件包含 "学生ID" 和 "课程班ID" 列,且数据格式
- 📧 提交 Issue: [项目 Issues](https://git.vcck.cn/nvex/toolbox/issues)
- 💬 项目讨论: [项目讨论区](https://git.vcck.cn/nvex/toolbox/discussions)
---
*最后更新: 2024年12月*
---

View File

@@ -10,31 +10,22 @@ load_dotenv()
app = Flask(__name__)
CORS(app)
# --- 从 .env 文件加载配置 ---
BASE_URL = os.getenv("BASE_URL")
AUTHORIZATION = os.getenv("AUTHORIZATION")
SEMESTER_ID = os.getenv("SEMESTER_ID")
X_ROLE = os.getenv("X_ROLE")
X_SCHOOL_ID = os.getenv("X_SCHOOL_ID")
X_REFLECTION_ID = os.getenv("X_REFLECTION_ID")
# Use absolute path for the template directory
TEMPLATE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'toolbox-app', 'public'))
TEMPLATE_FILENAME = '退课列表.xlsx'
def get_student_info_from_api(student_id):
def get_student_info_from_api(base_url, authorization, x_role, x_school_id, x_reflection_id, student_id):
"""通过 API 查询学生姓名和班级"""
if not all([BASE_URL, AUTHORIZATION, X_ROLE, X_SCHOOL_ID, X_REFLECTION_ID]):
return "配置缺失", None, "Error: Missing API configuration"
if not all([base_url, authorization, x_role, x_school_id, x_reflection_id]):
return None, None, f"缺少一个或多个 API 配置参数"
url = f"{base_url}/chalk/role/reflection/students"
headers = {
"Authorization": AUTHORIZATION,
"X-Role": X_ROLE,
"X-School-ID": X_SCHOOL_ID,
"X-Reflection-ID": X_REFLECTION_ID
'Authorization': authorization,
'X-Role': x_role,
'X-School-ID': x_school_id,
'X-Reflection-ID': x_reflection_id
}
url = f"{BASE_URL}/chalk/role/reflection/students"
params = {
"id_in": student_id,
"policy": "admin"
@@ -57,20 +48,19 @@ def get_student_info_from_api(student_id):
app.logger.error(f"API request failed for student_id {student_id}: {e}")
return "查询失败", None, f"Error fetching info for student {student_id}"
def get_class_info_from_api(student_id, class_ids):
def get_class_info_from_api(base_url, authorization, x_role, x_school_id, x_reflection_id, semester_id, student_id):
"""通过 API 查询课程班信息返回ID和名称的字典"""
if not all([BASE_URL, AUTHORIZATION, SEMESTER_ID, X_ROLE, X_SCHOOL_ID, X_REFLECTION_ID]):
return None, "错误: 缺少 API 配置"
if not all([base_url, authorization, x_role, x_school_id, x_reflection_id, semester_id]):
return None, f"缺少一个或多个 API 配置参数"
url = f"{base_url}/scms/class/students/{student_id}/classes"
headers = {
"Authorization": AUTHORIZATION,
"X-Role": X_ROLE,
"X-School-ID": X_SCHOOL_ID,
"X-Reflection-ID": X_REFLECTION_ID
'Authorization': authorization,
'X-Role': x_role,
'X-School-ID': x_school_id,
'X-Reflection-ID': x_reflection_id
}
url = f"{BASE_URL}/scms/class/students/{student_id}/classes"
params = {"semester_id": SEMESTER_ID, "paginated": "1", "expand": "course"}
params = {"semester_id": semester_id, "paginated": "1", "expand": "course"}
try:
resp = requests.get(url, headers=headers, params=params)
@@ -92,18 +82,18 @@ def get_class_info_from_api(student_id, class_ids):
app.logger.error(f"API request failed for student_id {student_id}: {e}")
return None, f"为学生 {student_id} 获取名称时出错"
def get_school_name_from_api():
def get_school_name_from_api(base_url, authorization, x_role, x_school_id):
"""通过 API 查询学校名称"""
if not all([BASE_URL, AUTHORIZATION, X_ROLE, X_SCHOOL_ID]):
if not all([base_url, authorization, x_role, x_school_id]):
return "配置缺失", "Error: Missing API configuration"
headers = {
"Authorization": AUTHORIZATION,
"X-Role": X_ROLE,
"X-School-ID": X_SCHOOL_ID
"Authorization": authorization,
"X-Role": x_role,
"X-School-ID": x_school_id
}
url = f"{BASE_URL}/chalk/system/schools/{X_SCHOOL_ID}?expand=custom_constraints"
url = f"{base_url}/chalk/system/schools/{x_school_id}?expand=custom_constraints"
try:
resp = requests.get(url, headers=headers)
@@ -113,13 +103,27 @@ def get_school_name_from_api():
return school_name, None
except requests.exceptions.RequestException as e:
app.logger.error(f"API request failed for school_id {X_SCHOOL_ID}: {e}")
return "查询失败", f"Error fetching info for school {X_SCHOOL_ID}"
app.logger.error(f"API request failed for school_id {x_school_id}: {e}")
return "查询失败", f"Error fetching info for school {x_school_id}"
@app.route('/api/school-name', methods=['GET'])
def get_school_name():
"""Provides the school name."""
school_name, error = get_school_name_from_api()
"""Provides the school name based on headers."""
auth_header = request.headers.get('Authorization')
school_id_header = request.headers.get('X-School-ID')
base_url_header = request.headers.get('X-Base-URL')
role_header = request.headers.get('X-Role')
if not all([auth_header, school_id_header, base_url_header, role_header]):
return jsonify({'error': 'Missing required headers: Authorization, X-School-ID, X-Base-URL, X-Role'}), 400
school_name, error = get_school_name_from_api(
base_url=base_url_header,
authorization=auth_header,
x_role=role_header,
x_school_id=school_id_header
)
if error:
return jsonify({'error': error}), 500
return jsonify({'school_name': school_name})
@@ -158,6 +162,16 @@ def preview_file():
if file.filename == '':
return jsonify({'error': 'No selected file'}), 400
auth_header = request.headers.get('Authorization')
school_id_header = request.headers.get('X-School-ID')
base_url_header = request.headers.get('X-Base-URL')
role_header = request.headers.get('X-Role')
reflection_id_header = request.headers.get('X-Reflection-ID')
semester_id_header = request.headers.get('X-Semester-ID')
if not all([auth_header, school_id_header, base_url_header, role_header, reflection_id_header, semester_id_header]):
return jsonify({'error': 'Missing required headers: Authorization, X-School-ID, X-Base-URL, X-Role, X-Reflection-ID, X-Semester-ID'}), 400
try:
df = pd.read_excel(file)
df = df.where(pd.notnull(df), None)
@@ -170,8 +184,24 @@ def preview_file():
student_id = row['学生ID']
class_ids_str = row['课程班ID']
student_name, admin_classes, info_error = get_student_info_from_api(student_id)
class_details, class_error = get_class_info_from_api(student_id, class_ids_str)
student_name, admin_classes, info_error = get_student_info_from_api(
base_url=base_url_header,
authorization=auth_header,
x_role=role_header,
x_school_id=school_id_header,
x_reflection_id=reflection_id_header,
student_id=student_id
)
class_details, class_error = get_class_info_from_api(
base_url=base_url_header,
authorization=auth_header,
x_role=role_header,
x_school_id=school_id_header,
x_reflection_id=reflection_id_header,
semester_id=semester_id_header,
student_id=student_id,
class_ids=class_ids_str
)
if class_error:
class_ids = [id.strip() for id in str(class_ids_str).split(',')]
@@ -218,11 +248,20 @@ def withdraw_courses():
if not withdraw_list:
return jsonify({'error': 'Withdrawal list is required.'}), 400
auth_header = request.headers.get('Authorization')
school_id_header = request.headers.get('X-School-ID')
base_url_header = request.headers.get('X-Base-URL')
role_header = request.headers.get('X-Role')
reflection_id_header = request.headers.get('X-Reflection-ID')
if not all([auth_header, school_id_header, base_url_header, role_header, reflection_id_header]):
return jsonify({'error': 'Missing required headers: Authorization, X-School-ID, X-Base-URL, X-Role, X-Reflection-ID'}), 400
headers = {
"Authorization": AUTHORIZATION,
"X-Role": X_ROLE,
"X-School-ID": X_SCHOOL_ID,
"X-Reflection-ID": X_REFLECTION_ID
"Authorization": auth_header,
"X-Role": role_header,
"X-School-ID": school_id_header,
"X-Reflection-ID": reflection_id_header
}
student_withdrawals = {}
@@ -247,17 +286,17 @@ def withdraw_courses():
payload = {
"reflection_ids": [student_id],
"class_ids": [int(cid) for cid in class_ids]
"class_ids": [int(cid) for cid in class_ids] if class_ids else []
}
try:
resp = requests.delete(f"{BASE_URL}/scms/class/class-selections", headers=headers, json=payload)
if resp.status_code == 204:
success_count += 1
details.append(f"✅ 学生 {student_id} 已成功退课")
else:
failure_count += 1
details.append(f"❌ 学生 {student_id} 退课失败:{resp.status_code} - {resp.text}")
resp = requests.delete(f"{base_url_header}/scms/class/class-selections", headers=headers, json=payload)
resp.raise_for_status()
success_count += 1
details.append(f"✅ 学生 {student_id} 已成功退课")
except requests.exceptions.HTTPError as e:
failure_count += 1
details.append(f"❌ 学生 {student_id} 退课失败:{e.response.status_code} - {e.response.text}")
except requests.exceptions.RequestException as e:
failure_count += 1
details.append(f"❌ 学生 {student_id} 退课失败: {e}")

34
toolbox-app/src/api.js Normal file
View File

@@ -0,0 +1,34 @@
import axios from 'axios';
import { useSettingsStore } from './store/settings';
const api = axios.create({
baseURL: 'https://api.seiue.com',
timeout: 60000,
});
api.interceptors.request.use(
(config) => {
const settingsStore = useSettingsStore();
if (settingsStore.authorization) {
config.headers['Authorization'] = settingsStore.authorization;
}
if (settingsStore.xSchoolId) {
config.headers['X-School-ID'] = settingsStore.xSchoolId;
}
if (settingsStore.xRole) {
config.headers['X-Role'] = settingsStore.xRole;
}
if (settingsStore.xReflectionId) {
config.headers['X-Reflection-ID'] = settingsStore.xReflectionId;
}
if (settingsStore.semesterId) {
config.headers['X-Semester-ID'] = settingsStore.semesterId;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
export default api;

View File

@@ -1,6 +1,6 @@
<script setup>
import { ref, computed, onMounted } from 'vue';
import axios from 'axios';
import { useSettingsStore } from '../../store/settings';
const file = ref(null);
const previewData = ref([]);
@@ -8,6 +8,7 @@ const isParsing = ref(false);
const isWithdrawing = ref(false);
const fileInput = ref(null);
const schoolName = ref('');
const schoolNameError = ref('');
const showFailedOnly = ref(false);
const withdrawalResults = ref(null);
@@ -119,12 +120,19 @@ function clearPreview() {
}
}
const settingsStore = useSettingsStore();
onMounted(async () => {
try {
const response = await axios.get('http://127.0.0.1:5001/api/school-name');
schoolName.value = response.data.school_name;
} catch (error) {
console.error('Error fetching school name:', error);
if (settingsStore.authorization) {
schoolNameError.value = '令牌失效,请检查您的 Authorization';
} else {
schoolNameError.value = '请先在设置页面填写配置信息';
}
}
});
function getResultItemClass(studentId) {
@@ -146,6 +154,9 @@ function getResultIcon(studentId) {
<div v-if="schoolName" class="school-info-tag">
正在给 {{ schoolName }} 学校退课
</div>
<div v-if="schoolNameError" class="school-info-tag school-info-error">
{{ schoolNameError }}
</div>
<div class="step-section">
<div class="step-header">
@@ -391,6 +402,12 @@ function getResultIcon(studentId) {
display: inline-block;
}
.school-info-error {
background-color: #fff1f0;
border-color: #ffa39e;
color: #cf1322;
}
.admin-classes-grid {
grid-template-columns: repeat(3, 1fr);
gap: 4px;

View File

@@ -2,10 +2,14 @@ import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import router from './router';
import api from './api';
const app = createApp(App);
app.use(createPinia());
app.use(router);
app.config.globalProperties.$api = api;
app.provide('api', api);
app.mount('#app');

View File

@@ -2,11 +2,11 @@ import { defineStore } from 'pinia';
export const useSettingsStore = defineStore('settings', {
state: () => ({
authorization: 'Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIzIiwiZXhwIjoxNzYxMTk0MjQzLCJqdGkiOiJmZjdkYTlkMS05ZGEzLTRhZDctYWZhZS04Njc3YWEzMzI0ZGMiLCJpYXQiOjE3NjExMDc4NDMsImlzcyI6Imh0dHBzOi8vbWRwLnNlaXVlLmNvbS9zZWl1ZSIsInN1YiI6IjEwMjg1MjYiLCJjdHgiOiJsb2dpbl9yaWQ9OTY5MjY5XHUwMDI2cmVhZG9ubHk9MFx1MDAyNnN0YWZmX2lkPTI0MCIsInNpZCI6MTAzNDkyOTJ9.bDuRFltrbFkV5NPxMAABFhTxv9zDQDBq4S9ZCRmQwohANrUD7HriP7vmospkrA_5skxJtyi3iK5OtINyIVGFP9zK7jcrfGJlhTrQimfgnHH-39mYsk08fOH-MhDvr2Q87ng-2gACIpNvyvzP-VCxo6IGOk9ZZWg2aT6xua-cV-8',
semesterId: '61626',
xRole: 'shadow',
xSchoolId: '665',
xReflectionId: '969269',
authorization: '',
semesterId: '',
xRole: '',
xSchoolId: '',
xReflectionId: '',
}),
actions: {
setSettings(newSettings) {

View File

@@ -4,23 +4,23 @@
<form @submit.prevent="saveSettings">
<div class="form-group">
<label for="authorization">Authorization</label>
<input type="text" id="authorization" v-model="settings.authorization">
<input type="text" id="authorization" v-model="settings.authorization" placeholder="请输入您的 Authorization">
</div>
<div class="form-group">
<label for="semesterId">Semester ID</label>
<input type="text" id="semesterId" v-model="settings.semesterId">
<input type="text" id="semesterId" v-model="settings.semesterId" placeholder="请输入学期 ID">
</div>
<div class="form-group">
<label for="xRole">X-Role</label>
<input type="text" id="xRole" v-model="settings.xRole">
<input type="text" id="xRole" v-model="settings.xRole" placeholder="请输入您的角色">
</div>
<div class="form-group">
<label for="xSchoolId">X-School-ID</label>
<input type="text" id="xSchoolId" v-model="settings.xSchoolId">
<input type="text" id="xSchoolId" v-model="settings.xSchoolId" placeholder="请输入学校 ID">
</div>
<div class="form-group">
<label for="xReflectionId">X-Reflection-ID</label>
<input type="text" id="xReflectionId" v-model="settings.xReflectionId">
<input type="text" id="xReflectionId" v-model="settings.xReflectionId" placeholder="请输入 Reflection ID">
</div>
<button type="submit">Save</button>
</form>