diff --git a/Appointment/admin.py b/Appointment/admin.py
index 708fdbfe5..128d78738 100644
--- a/Appointment/admin.py
+++ b/Appointment/admin.py
@@ -31,21 +31,22 @@ class College_AnnouncementAdmin(admin.ModelAdmin):
class ParticipantAdmin(admin.ModelAdmin):
actions_on_top = True
actions_on_bottom = True
- search_fields = ('Sid__username', 'Sid__name', 'Sid__pinyin', 'Sid__acronym')
+ search_fields = ('Sid__username', 'Sid__name',
+ 'Sid__pinyin', 'Sid__acronym')
list_display = ('Sid_id', 'name', 'credit', 'longterm', 'hidden')
list_display_links = ('Sid_id', 'name')
class AgreeFilter(admin.SimpleListFilter):
title = '签署状态'
parameter_name = 'Agree'
-
+
def lookups(self, request, model_admin):
'''针对字段值设置过滤器的显示效果'''
return (
('true', "已签署"),
('false', "未签署"),
)
-
+
def queryset(self, request, queryset):
'''定义过滤器的过滤动作'''
if self.value() == 'true':
@@ -71,11 +72,11 @@ class AppointInline(admin.TabularInline):
]
show_change_link = True
# 可申诉的范围只有一周,筛选两周内范围的即可
+
def get_queryset(self, request):
return super().get_queryset(request).filter(
Astart__gte=datetime.now().date() - timedelta(days=14))
-
inlines = [AppointInline]
actions = []
@@ -116,7 +117,8 @@ class RoomAdmin(admin.ModelAdmin):
'RneedAgree',
)
list_display_links = ('Rid', )
- list_editable = ('Rtitle', 'Rmin', 'Rmax', 'Rstart', 'Rfinish', 'RneedAgree')
+ list_editable = ('Rtitle', 'Rmin', 'Rmax',
+ 'Rstart', 'Rfinish', 'RneedAgree')
search_fields = ('Rid', 'Rtitle')
list_filter = ('Rstatus', 'RIsAllNight', 'RneedAgree')
@@ -140,7 +142,7 @@ class AppointAdmin(admin.ModelAdmin):
LETTERS = set(string.digits + string.ascii_letters + string.punctuation)
search_fields = ('Room__Rtitle', 'Room__Rid',
'major_student__name', "students__name",
- 'major_student__pinyin', # 仅发起者缩写,方便搜索者区分发起者和参与者
+ 'major_student__pinyin', # 仅发起者缩写,方便搜索者区分发起者和参与者
)
list_display = (
'Aid',
@@ -169,16 +171,16 @@ class AppointAdmin(admin.ModelAdmin):
readonly_fields = ('Atime', )
class ActivateFilter(admin.SimpleListFilter):
- title = '有效状态' # 过滤标题显示为"以 有效状态"
- parameter_name = 'Activate' # 过滤器使用的过滤字段
-
+ title = '有效状态' # 过滤标题显示为"以 有效状态"
+ parameter_name = 'Activate' # 过滤器使用的过滤字段
+
def lookups(self, request, model_admin):
'''针对字段值设置过滤器的显示效果'''
return (
('true', "有效"),
('false', "无效"),
)
-
+
def queryset(self, request, queryset):
'''定义过滤器的过滤动作'''
if self.value() == 'true':
@@ -200,7 +202,8 @@ def get_search_results(self, request, queryset, search_term: str):
if ' ' not in search_term:
# 判断时需要增加exists,否则会报错,似乎是QuerySet的缓存问题?
if str.isascii(search_term) and str.isalpha(search_term):
- pinyin_result = queryset.filter(major_student__pinyin__icontains=search_term)
+ pinyin_result = queryset.filter(
+ major_student__pinyin__icontains=search_term)
if pinyin_result.exists():
return pinyin_result, False
elif str.isascii(search_term) and str.isalnum(search_term):
@@ -208,7 +211,8 @@ def get_search_results(self, request, queryset, search_term: str):
if room_result.exists():
return room_result, False
else:
- room_result = queryset.filter(Room__Rtitle__icontains=search_term)
+ room_result = queryset.filter(
+ Room__Rtitle__icontains=search_term)
if room_result.exists():
return room_result, False
return super().get_search_results(request, queryset, search_term)
@@ -232,7 +236,8 @@ def usage_display(self, obj):
usage = obj.Ausage
else:
usage = obj.Ausage[:half_len] + '...' + obj.Ausage[3-half_len:]
- usage = '
'.join([usage[i:i+batch] for i in range(0, len(usage), batch)])
+ usage = '
'.join([usage[i:i+batch]
+ for i in range(0, len(usage), batch)])
return mark_safe(usage)
@as_display('通过率')
@@ -275,7 +280,6 @@ def _waiting2confirm(self, appoint: Appoint):
students_id=[appoint.get_major_id()], admin=True)
logger.info(f"{appoint.Aid}号预约被管理员通过,发起人:{_appointor(appoint)}")
-
def _violated2judged(self, appoint: Appoint):
appoint.Astatus = Appoint.Status.JUDGED
appoint.save()
@@ -284,7 +288,6 @@ def _violated2judged(self, appoint: Appoint):
students_id=[appoint.get_major_id()], admin=True)
logger.info(f"{appoint.Aid}号预约被管理员通过,发起人:{_appointor(appoint)}")
-
@as_action('所选条目 通过', actions, 'change', update=True)
def confirm(self, request, queryset: QuerySet[Appoint]): # 确认通过
invalid = []
@@ -303,18 +306,18 @@ def confirm(self, request, queryset: QuerySet[Appoint]): # 确认通过
message = f'部分成功!但{invalid}状态不为等待、违约,不允许更改!'
return self.message_user(request, message, messages.WARNING)
-
@as_action('所选条目 违约', actions, 'change', update=True)
def violate(self, request, queryset: QuerySet[Appoint]): # 确认违约
for appoint in queryset:
if (appoint.Astatus == Appoint.Status.VIOLATED
- and appoint.Areason == Appoint.Reason.R_ELSE):
+ and appoint.Areason == Appoint.Reason.R_ELSE):
return self.message_user(
request, '操作失败!只允许对未审核的条目操作!', messages.WARNING)
ori_status = appoint.get_status()
if appoint.Astatus != Appoint.Status.VIOLATED:
appoint.Astatus = Appoint.Status.VIOLATED
- User.objects.modify_credit(appoint.get_major_id(), -1, '地下室:后台')
+ User.objects.modify_credit(
+ appoint.get_major_id(), -1, '地下室:后台')
appoint.Areason = Appoint.Reason.R_ELSE
appoint.save()
@@ -326,7 +329,6 @@ def violate(self, request, queryset: QuerySet[Appoint]): # 确认违约
return self.message_user(request, "设为违约成功!")
-
@as_action('更新定时任务', actions, ['add', 'change'])
def refresh_scheduler(self, request, queryset):
'''
@@ -339,8 +341,8 @@ def refresh_scheduler(self, request, queryset):
start = appoint.Astart
finish = appoint.Afinish
if start > finish:
- return self.message_user(request,
- f'操作失败,预约{aid}开始和结束时间冲突!请勿篡改数据!', messages.WARNING)
+ return self.message_user(request,
+ f'操作失败,预约{aid}开始和结束时间冲突!请勿篡改数据!', messages.WARNING)
cancel_scheduler(aid) # 注销原有定时任务 无异常
set_scheduler(appoint) # 开始时进入进行中 结束后判定
set_appoint_reminder(appoint)
@@ -349,7 +351,6 @@ def refresh_scheduler(self, request, queryset):
return self.message_user(request, str(e), messages.WARNING)
return self.message_user(request, '定时任务更新成功!')
-
def longterm_wk(self, request, queryset, times, interval_week=1):
new_appoints = {}
for appoint in queryset:
@@ -365,7 +366,8 @@ def longterm_wk(self, request, queryset, times, interval_week=1):
longterm_info = jobs.get_longterm_display(times, interval_week)
notify_appoint(appoint, MessageType.LONGTERM_CREATED,
f'新增了{longterm_info}同时段预约', admin=True)
- new_appoints[appoint.pk] = list(appoints.values_list('pk', flat=True))
+ new_appoints[appoint.pk] = list(
+ appoints.values_list('pk', flat=True))
except Exception as e:
return self.message_user(request, f'长线化失败!', messages.WARNING)
new_infos = []
@@ -377,8 +379,8 @@ def longterm_wk(self, request, queryset, times, interval_week=1):
new_infos.append(f'{appoint}->{new_appoint_ids}')
return self.message_user(request, f'长线化成功!生成预约{";".join(new_appoints)}')
-
# @as_action('增加一周本预约', actions, 'add', single=True)
+
def longterm1(self, request, queryset):
return self.longterm_wk(request, queryset, 1)
@@ -418,7 +420,7 @@ class CardCheckInfoAdmin(admin.ModelAdmin):
'Cardtime', 'CardStatus',
('Cardroom', admin.EmptyFieldListFilter),
]
-
+
@as_display('刷卡者', except_value='-')
def student_display(self, obj):
return obj.Cardstudent.name
@@ -433,3 +435,29 @@ class LongTermAppointAdmin(admin.ModelAdmin):
def view_on_site(self, obj: LongTermAppoint):
return f'/underground/review?Lid={obj.pk}'
+
+
+@admin.register(AI_Inspection_Info)
+class AI_Inspection_InfoAdmin(admin.ModelAdmin):
+ list_display = ['id', 'Iperson', 'Iroom', 'Ireason_truncated',
+ 'Iresult', 'Itimestamp', 'Idetail_truncated']
+ list_select_related = ['Iperson', 'Iroom']
+ search_fields = ['Iperson__name', 'Iperson__Sid__username',
+ 'Iroom__Rtitle', 'Ireason', 'Idetail']
+ list_filter = ['Iresult', 'Itimestamp']
+ readonly_fields = ['Iperson', 'Iroom',
+ 'Ireason', 'Iresult', 'Itimestamp', 'Idetail']
+
+ @as_display('事由')
+ def Ireason_truncated(self, obj):
+ if len(obj.Ireason) > 30:
+ return obj.Ireason[:27] + '...'
+ return obj.Ireason
+
+ @as_display('详情')
+ def Idetail_truncated(self, obj):
+ if not obj.Idetail:
+ return '-'
+ if len(obj.Idetail) > 30:
+ return obj.Idetail[:27] + '...'
+ return obj.Idetail
diff --git a/Appointment/config.py b/Appointment/config.py
index d5c2026eb..4c150286b 100644
--- a/Appointment/config.py
+++ b/Appointment/config.py
@@ -49,4 +49,18 @@ class AppointmentConfig(Config):
# 是否允许单人同一时段预约两个房间
allow_overlap = False
+ # AI 审核功能开启与否,使用的 API 地址和密钥
+ AI_Inspection_Enabled = LazySetting('AI_Inspection/enabled', type=bool)
+ AI_Inspection_Method = LazySetting(
+ 'AI_Inspection/method', type=str, default="Ollama") # "Ollama" or "GLM"
+
+ # Ollama 参数
+ Ollama_ADDR = LazySetting('AI_Inspection/Ollama_ADDR', type=str)
+
+ # GLM 参数
+ GLM_API_KEY = LazySetting('AI_Inspection/GLM_API_Key', type=str)
+ GLM_Timeout = LazySetting(
+ 'AI_Inspection/GLM_Timeout', type=int, default=30)
+
+
appointment_config = AppointmentConfig(ROOT_CONFIG, 'underground')
diff --git a/Appointment/migrations/0004_ai_inspection_info.py b/Appointment/migrations/0004_ai_inspection_info.py
new file mode 100644
index 000000000..4d7cf8a38
--- /dev/null
+++ b/Appointment/migrations/0004_ai_inspection_info.py
@@ -0,0 +1,69 @@
+# Generated by Django 5.0.14 on 2025-09-16 19:51
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("Appointment", "0003_alter_appoint_options_alter_appoint_areason"),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="AI_Inspection_Info",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("Ireason", models.CharField(max_length=256, verbose_name="事由")),
+ (
+ "Iresult",
+ models.SmallIntegerField(
+ choices=[(1, "合规"), (0, "不合规"), (-1, "错误")],
+ default=-1,
+ verbose_name="审核结果",
+ ),
+ ),
+ (
+ "Idetail",
+ models.CharField(
+ blank=True, max_length=512, verbose_name="审核详情"
+ ),
+ ),
+ (
+ "Itimestamp",
+ models.DateTimeField(auto_now_add=True, verbose_name="审核时间"),
+ ),
+ (
+ "Iperson",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to=settings.AUTH_USER_MODEL,
+ verbose_name="预约人",
+ ),
+ ),
+ (
+ "Iroom",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="Appointment.room",
+ verbose_name="房间号",
+ ),
+ ),
+ ],
+ options={
+ "verbose_name": "AI审核信息",
+ "verbose_name_plural": "AI审核信息",
+ },
+ ),
+ ]
diff --git a/Appointment/models.py b/Appointment/models.py
index 5f04c30fe..ab7b1f462 100644
--- a/Appointment/models.py
+++ b/Appointment/models.py
@@ -22,10 +22,10 @@
'Appoint',
'LongTermAppoint',
'CardCheckInfo',
+ 'AI_Inspection_Info',
]
-
class College_Announcement(models.Model):
class Meta:
verbose_name = "全院公告"
@@ -172,7 +172,8 @@ class Status(models.IntegerChoices):
UNLIMITED = 1, '无需预约' # 允许使用
FORBIDDEN = 2, '禁止使用' # 禁止使用
- Rstatus = models.SmallIntegerField('房间状态', choices=Status.choices, default=0)
+ Rstatus = models.SmallIntegerField(
+ '房间状态', choices=Status.choices, default=0)
# 标记当前房间是否可以通宵使用,可由管理员修改(主要针对自习室)
RIsAllNight = models.BooleanField('可通宵使用', default=False)
@@ -249,7 +250,7 @@ class CheckStatus(models.IntegerChoices):
PASSED = 1 # 预约在特定分钟内的检查是通过的
UNSAVED = 2 # 预约在此分钟内尚未记录检测状态
Acheck_status = models.SmallIntegerField('检测状态',
- choices=CheckStatus.choices, default=2)
+ choices=CheckStatus.choices, default=2)
# 这里Room使用外键的话只能设置DO_NOTHING,否则删除房间就会丢失预约信息
# 所以房间信息不能删除,只能逻辑删除
@@ -309,7 +310,7 @@ class Type(models.IntegerChoices):
INTERVIEW = choice(4, '面试预约')
Atype = models.SmallIntegerField('预约类型',
- choices=Type.choices, default=Type.NORMAL)
+ choices=Type.choices, default=Type.NORMAL)
get_Atype_display: CustomizedDisplay
objects: AppointManager = AppointManager()
@@ -519,6 +520,31 @@ def get_applicant_id(self) -> str:
return self.applicant.get_id()
+class AI_Inspection_Info(models.Model):
+ """
+ 记录AI审核的结果和理由
+ """
+ class Meta:
+ verbose_name = 'AI审核信息'
+ verbose_name_plural = verbose_name
+
+ Iperson = models.ForeignKey(
+ User, on_delete=models.CASCADE, verbose_name='预约人')
+ Iroom = models.ForeignKey(
+ Room, on_delete=models.CASCADE, verbose_name='房间号')
+ Ireason = models.CharField('事由', max_length=256)
+
+ class Result(models.IntegerChoices):
+ APPROVED = (1, '合规')
+ NOT_APPROVED = (0, '不合规')
+ ERROR = (-1, '错误')
+
+ Iresult = models.SmallIntegerField(
+ '审核结果', choices=Result.choices, default=Result.ERROR)
+ Idetail = models.CharField('审核详情', max_length=512, blank=True)
+ Itimestamp = models.DateTimeField('审核时间', auto_now_add=True)
+
+
@receiver(pre_delete, sender=Appoint)
def before_delete_Appoint(sender, instance, **kwargs):
from Appointment.appoint.jobs import cancel_scheduler
diff --git a/Appointment/utils/AI_Inspection.py b/Appointment/utils/AI_Inspection.py
new file mode 100644
index 000000000..03dbd0433
--- /dev/null
+++ b/Appointment/utils/AI_Inspection.py
@@ -0,0 +1,206 @@
+from Appointment.config import appointment_config as CONFIG
+from Appointment.models import AI_Inspection_Info, Room
+from Appointment.utils.log import logger
+import os
+import json
+import requests
+from django.utils import timezone
+
+# 模型人设:只返回“合规/不合规”,JSON格式
+SYSTEM_PROMPT = (
+ "你是一个审查学生预约功能房间信息的老师,请审核以下预约信息,审核要求是预约事由和房间用途匹配,且不能包含政治敏感信息和色情内容。"
+ "你的输出必须严格为JSON,字段为:"
+ '{"decision": 0 或 1, "reason":"一句话理由"}'
+ "其中 0 代表不允许预约,1 代表允许预约"
+)
+
+
+def _call_glm_decision(text: str, timeout: int = 30) -> tuple[str, str]:
+ """
+ 调用智谱 HTTP 接口,返回 0 或 1,超时/网络错误报出异常。
+ """
+ ENDPOINT = "https://open.bigmodel.cn/api/paas/v4/chat/completions"
+ MODEL = "glm-4.5-flash"
+ api_key = CONFIG.GLM_API_KEY
+ if not api_key:
+ raise RuntimeError("缺少参数 API_KEY")
+
+ headers = {
+ "Authorization": f"Bearer {api_key}",
+ "Content-Type": "application/json",
+ }
+ payload = {
+ "model": MODEL,
+ "temperature": 0.2, # 可调
+ "top_p": 0.7, # 可调
+ "response_format": {"type": "json_object"},
+ "messages": [
+ {"role": "system", "content": SYSTEM_PROMPT},
+ {"role": "user", "content": f"待审核文本:\n{text}"},
+ ],
+ }
+
+ resp = requests.post(ENDPOINT, headers=headers,
+ json=payload, timeout=timeout)
+ if resp.status_code != 200:
+ # 把上游错误透出一小段,便于排查
+ if resp.status_code == 400:
+ return False, "请勿在预约理由中包含敏感词汇!"
+ raise RuntimeError(f"Upstream {resp.status_code}: {resp.text[:300]}")
+
+ data = resp.json()
+ try:
+ content_str = data["choices"][0]["message"]["content"]
+ obj = json.loads(content_str)
+ except Exception as e:
+ raise RuntimeError(f"Bad JSON content from provider: {e}; raw={data}")
+
+ decision = obj.get("decision")
+ reason = obj.get("reason", "")
+ if decision not in (0, 1):
+ raise ValueError(f"unexpected decision: {decision};obj={obj}")
+ if not isinstance(reason, str):
+ reason = str(reason)
+ if decision == 1:
+ return True, reason
+ else:
+ return False, reason
+
+
+def Ollama_Inspection(room_name, reason, user) -> tuple[bool, str]:
+ # Ollama 审核功能接口,将预约房间(名称)和事由发送至 API,然后接收判断结果(合格/不合格)
+ # 完整的处理办法(需要在API调用失败时抛出错误)
+ try:
+ response = requests.post(
+ CONFIG.Ollama_ADDR + "/api/generate",
+ json={
+ "model": "gpt-oss:20b",
+ "prompt": f"你是一个审查学生预约功能房间信息的老师,请审核以下预约信息,审核要求是预约事由和房间用途匹配,且不能包含政治敏感信息和色情内容。房间名称:{room_name}。事由:{reason}。如果审核通过,只输出 1,不通过,则输出 0,然后接一个空格,在空格后输出不通过的原因,原因尽量简要;如无法判断,默认不通过",
+ "stream": False,
+ "think": False
+ }
+ ) # GPT-OSS:20b 实测效果最好
+ response.raise_for_status() # 如果响应状态码不是 200,将抛出异常
+ result = response.json()
+ # 根据上述格式解析 response 部分,给出返回值
+ output = result.get("response", "").strip()
+ if output.startswith("1"):
+ AI_Inspection_Info.objects.create(
+ Iperson=user,
+ Iroom=Room.objects.get(Rtitle=room_name),
+ Ireason=reason,
+ Iresult=AI_Inspection_Info.Result.APPROVED
+ )
+ return True, "Approved"
+ elif output.startswith("0"):
+ why = output[1:].strip() # 获取不通过的原因
+ AI_Inspection_Info.objects.create(
+ Iperson=user,
+ Iroom=Room.objects.get(Rtitle=room_name),
+ Ireason=reason,
+ Iresult=AI_Inspection_Info.Result.NOT_APPROVED,
+ Idetail=why
+ )
+ return False, "房间用途不合规:" + why if why else "Not approved"
+ else:
+ logger.error(
+ f"AI Inspection failed: Unexpected response format {output}")
+ return False, "Unexpected response format"
+
+ except requests.RequestException as e:
+ # 处理请求异常
+ # Idetail 最多记录 500 字符
+ AI_Inspection_Info.objects.create(
+ Iperson=user,
+ Iroom=Room.objects.get(Rtitle=room_name),
+ Ireason=reason,
+ Iresult=AI_Inspection_Info.Result.NOT_APPROVED,
+ Idetail=f"Request failed: {str(e)[:480]}"
+ )
+
+ # 错误信息需记入日志
+ logger.error(f"AI Inspection failed: {str(e)}")
+
+ return False, f"发生错误,请稍后再试或联系管理员!"
+
+
+def GLM_Inspection(room_name, reason, user) -> tuple[bool, str]:
+ # GLM 审核
+
+ # 组装要审核的文本
+ content = f"房间:{room_name}\n事由:{reason}"
+ # 超时时间可从配置读取,默认30秒
+ timeout = CONFIG.GLM_Timeout
+ try:
+ decision, why = _call_glm_decision(content, timeout=timeout)
+ passed = (decision == 1)
+ # 返回是否通过和理由 True通过,False不通过
+ if passed:
+ AI_Inspection_Info.objects.create(
+ Iperson=user,
+ Iroom=Room.objects.get(Rtitle=room_name),
+ Ireason=reason,
+ Iresult=AI_Inspection_Info.Result.APPROVED
+ )
+ return True, "合规"
+ else:
+ AI_Inspection_Info.objects.create(
+ Iperson=user,
+ Iroom=Room.objects.get(Rtitle=room_name),
+ Ireason=reason,
+ Iresult=AI_Inspection_Info.Result.NOT_APPROVED,
+ Idetail=why
+ )
+ return False, f"房间用途不合规:{why or '无具体理由'}"
+
+ except Exception as e:
+ # API 调用失败:不通过并附带错误信息
+ # Idetail 最多记录 500 字符
+ AI_Inspection_Info.objects.create(
+ Iperson=user,
+ Iroom=Room.objects.get(Rtitle=room_name),
+ Ireason=reason,
+ Iresult=AI_Inspection_Info.Result.NOT_APPROVED,
+ Idetail=f"Request failed: {str(e)[:480]}"
+ )
+
+ # 错误信息需记入日志
+ logger.error(f"AI Inspection failed: {str(e)}")
+
+ return False, f"发生错误,请稍后再试或联系管理员!"
+
+
+def AI_Inspection(room_name, reason, user) -> tuple[bool, str]:
+ # AI 审核功能接口,将预约房间(名称)和事由发送至 API,然后接收判断结果(合格/不合格)
+ # Additional:最好能在不合格时附带理由
+
+ if not CONFIG.AI_Inspection_Enabled:
+ # 默认通过
+ return True, "AI Inspection is disabled"
+
+ # reason 长度限制在 250 字符以内
+ if len(reason) > 250:
+ AI_Inspection_Info.objects.create(
+ Iperson=user,
+ Iroom=Room.objects.get(Rtitle=room_name),
+ Ireason=reason[:250],
+ Iresult=AI_Inspection_Info.Result.NOT_APPROVED,
+ Idetail="事由过长,超过250字符"
+ )
+ return False, "事由过长,请限制在250字符以内!"
+
+ # 获取该用户上一次提交审核记录的时间,若距离现在不足 1 分钟,则拒绝本次审核
+ last_record = AI_Inspection_Info.objects.filter(
+ Iperson=user).order_by('-Itimestamp').first()
+ if last_record:
+ if (timezone.now() - last_record.Itimestamp).total_seconds() < 60:
+ return False, "请勿频繁提交审核请求,请稍后再试!"
+
+ # 在此处实现 API 的调用,并处理 API 调用失败的情况
+ if CONFIG.AI_Inspection_Method == "Ollama":
+ return Ollama_Inspection(room_name, reason, user)
+
+ if CONFIG.AI_Inspection_Method == "GLM":
+ return GLM_Inspection(room_name, reason, user)
+
+ return False, "AI Inspection method is not recognized"
diff --git a/Appointment/views.py b/Appointment/views.py
index c02aefedb..27df92f52 100644
--- a/Appointment/views.py
+++ b/Appointment/views.py
@@ -23,9 +23,9 @@
)
from Appointment.extern.wechat import MessageType, notify_appoint
from Appointment.utils.utils import (
- get_conflict_appoints,
- to_feedback_url,
- get_total_appoint_time,
+ get_conflict_appoints,
+ to_feedback_url,
+ get_total_appoint_time,
get_overlap_appoints
)
from Appointment.utils.log import logger, get_user_logger
@@ -41,6 +41,7 @@
)
from Appointment import jobs
from Appointment.config import appointment_config as CONFIG
+from Appointment.utils.AI_Inspection import AI_Inspection
# 一些固定值
@@ -148,7 +149,6 @@ def renewLongtermAppoint(request):
return redirect(message_url(context, reverse("Appointment:account")))
-
@identity_check(redirect_field_name='origin')
def account(request: HttpRequest):
"""
@@ -420,15 +420,16 @@ def arrange_time(request: HttpRequest):
has_longterm_permission = get_participant(request.user).longterm
# 用于前端使用
- allow_overlap = CONFIG.allow_overlap
- max_appoint_time = int(CONFIG.max_appoint_time.total_seconds() // 3600) # 以小时计算
+ allow_overlap = CONFIG.allow_overlap
+ max_appoint_time = int(
+ CONFIG.max_appoint_time.total_seconds() // 3600) # 以小时计算
is_person = request.user.is_person()
- # 处理从URL参数传递过来的错误信息
+ # 处理从URL参数传递过来的错误信息
html_display = {}
html_display["warn_code"], html_display["warn_message"] = my_messages.get_request_message(
request)
-
+
# 获取房间编号
Rid = request.GET.get('Rid')
try:
@@ -465,7 +466,8 @@ def arrange_time(request: HttpRequest):
for day in [start_day + timedelta(days=i) for i in range(7)]:
used_time = get_total_appoint_time(get_participant(request.user), day)
available_hour = CONFIG.max_appoint_time - used_time
- available_hours[day.strftime('%a')] = int(available_hour.total_seconds() // 3600) # 以小时计算
+ available_hours[day.strftime('%a')] = int(
+ available_hour.total_seconds() // 3600) # 以小时计算
# 获取预约时间的最大时间块id
max_stamp_id = web_func.get_time_id(room, room.Rfinish, mode="leftopen")
@@ -684,6 +686,7 @@ def _get_content_room(contents: dict) -> Room:
assert room is not None, f'房间{room_id}不存在!'
return room
+
def _get_content_students(contents: dict):
students_id = contents.get('students')
# TODO: 目前调用时一定存在,后续看情况是处理后调用本函数与否,修改检查方式
@@ -692,9 +695,10 @@ def _get_content_students(contents: dict):
assert len(students) == len(students_id), '预约人信息有误,请检查后重新发起预约!'
return students
+
def _add_appoint(contents: dict, start: datetime, finish: datetime, non_yp_num: int,
- type: Appoint.Type = Appoint.Type.NORMAL,
- notify_create: bool = True) -> tuple[Appoint | None, str]:
+ type: Appoint.Type = Appoint.Type.NORMAL,
+ notify_create: bool = True) -> tuple[Appoint | None, str]:
'''
创建一个预约,检查各种条件,屎山函数
@@ -881,7 +885,8 @@ def checkout_appoint(request: UserRequest):
appoint_params=appoint_params,
has_longterm_permission=has_longterm_permission,
has_interview_permission=has_interview_permission,
- interview_max_count=CONFIG.interview_max_num)
+ interview_max_count=CONFIG.interview_max_num,
+ AI_Inspection_Enabled=CONFIG.AI_Inspection_Enabled)
# 提供搜索功能的数据
search_users = User.objects.filter_type(User.Type.PERSON)
@@ -915,16 +920,21 @@ def checkout_appoint(request: UserRequest):
# 检查是否未填写房间用途
if not contents['Ausage']:
wrong("请输入房间用途!", render_context)
+ # 自动化审核房间用途是否合规
+ is_valid, reason = AI_Inspection(
+ room.Rtitle, contents['Ausage'], request.user)
+ if not is_valid:
+ wrong(f"预约失败! {reason}", render_context)
# 处理单人预约
if "students" not in contents.keys():
contents['students'] = [contents['Sid']]
else:
contents['students'].append(contents['Sid'])
-
+
# 不允许用非活跃用户凑数
# 虽然在生成搜索列表时已经排除了非活跃用户,但这里再检查一次,以防万一
contents['students'] = list(filter(
- lambda sid: User.objects.get(username = sid).active,
+ lambda sid: User.objects.get(username=sid).active,
contents['students']
))
# 处理长期预约的times和interval参数 - 移到这里
@@ -941,7 +951,8 @@ def checkout_appoint(request: UserRequest):
selected_ids = [w['id']
for w in stu_list if w['id'] in selected_ids]
json_context.update(selected_ids=selected_ids)
- render_context.update(contents=contents, show_clause=True, json_context=json_context)
+ render_context.update(
+ contents=contents, show_clause=True, json_context=json_context)
return render(request, 'Appointment/checkout.html', render_context)
# 检查长期预约周数是否填写
if is_longterm and not times:
@@ -976,26 +987,26 @@ def checkout_appoint(request: UserRequest):
# TODO: 隔周预约的处理可优化,根据start_week调整实际预约时间
start_time += timedelta(weeks=start_week)
end_time += timedelta(weeks=start_week)
-
+
# 预约时间检查
if (
applicant.Sid.is_person()
- and not is_longterm
+ and not is_longterm
and not is_interview
and get_total_appoint_time(applicant, start_time.date()) + (end_time - start_time) > CONFIG.max_appoint_time
):
wrong('您预约的时长已超过每日最大预约时长', render_context)
-
+
# 检查预约者是否有同时段的预约
if (
applicant.Sid.is_person()
- and not CONFIG.allow_overlap
- and not is_longterm
- and not is_interview
+ and not CONFIG.allow_overlap
+ and not is_longterm
+ and not is_interview
and get_overlap_appoints(applicant, start_time, end_time).exists()
):
wrong(f'您在该时间段已经有预约', render_context)
-
+
if my_messages.get_warning(render_context)[0] is None:
# 参数检查全部通过,下面开始创建预约
appoint_type = Appoint.Type.NORMAL
@@ -1054,7 +1065,8 @@ def checkout_appoint(request: UserRequest):
selected_ids = set(contents.pop('students'))
selected_ids = [w['id'] for w in stu_list if w['id'] in selected_ids]
json_context.update(selected_ids=selected_ids)
- render_context.update(contents=contents, show_clause=True, json_context=json_context)
+ render_context.update(
+ contents=contents, show_clause=True, json_context=json_context)
return render(request, 'Appointment/checkout.html', render_context)
diff --git a/config_template.json b/config_template.json
index 549f0c1a8..ca9f9062b 100644
--- a/config_template.json
+++ b/config_template.json
@@ -56,6 +56,13 @@
},
"semester_data": {
"semester_start": "2023-02-20"
+ },
+ "AI_Inspection": {
+ "enabled": false,
+ "method": "Ollama",
+ "Ollama_ADDR": "",
+ "GLM_API_Key": "",
+ "GLM_Timeout": 30
}
},
"course": {
diff --git a/templates/Appointment/checkout.html b/templates/Appointment/checkout.html
index 535397eca..bd394ff12 100644
--- a/templates/Appointment/checkout.html
+++ b/templates/Appointment/checkout.html
@@ -39,6 +39,14 @@