Skip to content
1 change: 1 addition & 0 deletions app/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ def get_normal_fields(self, request, obj: NaturalPerson = None):
f(_m.identity), f(_m.status),
f(_m.wechat_receive_level),
f(_m.accept_promote), f(_m.active_score),
f(_m.course_priority)
])
return fields

Expand Down
64 changes: 49 additions & 15 deletions app/course_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
process_time: 把datetime对象转换成人类可读的时间表示
check_course_time_conflict: 检查当前选择的课是否与已选的课上课时间冲突
"""
from collections import Counter, defaultdict
from typing import Callable

from app.utils_dependency import *
from app.models import (
User,
Expand Down Expand Up @@ -46,7 +49,7 @@

import openpyxl
import openpyxl.worksheet.worksheet
from random import sample
from numpy.random import choice
from urllib.parse import quote
from collections import Counter
from datetime import datetime, timedelta
Expand Down Expand Up @@ -608,16 +611,13 @@ def registration_status_change(course_id: int, user: NaturalPerson,
and course_status != Course.Status.STAGE2):
return wrong("在非选课阶段不能选课!")

need_to_create = False

if action == "select":
if CourseParticipant.objects.filter(course_id=course_id,
person=user).exists():
participant_info = CourseParticipant.objects.get(
course_id=course_id, person=user)
cur_status = participant_info.status
else:
need_to_create = True
cur_status = CourseParticipant.Status.UNSELECT

if course_status == Course.Status.STAGE1:
Expand Down Expand Up @@ -681,15 +681,11 @@ def registration_status_change(course_id: int, user: NaturalPerson,
course.current_participants += 1
course.save()

# 由于不同用户之间的状态不共享,这个更新应该可以不加锁
if need_to_create:
CourseParticipant.objects.create(course_id=course_id,
person=user,
status=to_status)
else:
CourseParticipant.objects.filter(
course_id=course_id,
person=user).update(status=to_status)
CourseParticipant.objects.update_or_create(
course_id = course_id,
person = user,
defaults = {"status": to_status}
)
succeed("选课成功!", context)
except:
return context
Expand Down Expand Up @@ -816,7 +812,11 @@ def draw_lots():
if participants_num <= 0:
continue

participants_id = list(participants.values_list("id", flat=True))
participants_info = participants.values_list("id", "person__course_priority")
participants_id, priority = map(list, zip(*participants_info))
# Turn priority into a probability distribution
sum_priority = sum(priority)
priority = [i / sum_priority for i in priority]
capacity = course.capacity

if participants_num <= capacity:
Expand All @@ -828,7 +828,7 @@ def draw_lots():
current_participants=participants_num)
else:
# 抽签;可能实现得有一些麻烦
lucky_ones = sample(participants_id, capacity)
lucky_ones = choice(participants_id, capacity, replace=False, p=priority)
unlucky_ones = list(
set(participants_id).difference(set(lucky_ones)))
# 不确定是否要加悲观锁
Expand Down Expand Up @@ -1489,3 +1489,37 @@ def download_select_info(single_course: Course | None = None):
file_name = f'{year}{semester}选课名单汇总-{ctime}'
# 保存并返回
return _excel_response(wb, file_name)


def update_course_priority(year: int, semester: str, priority_func: Callable[[int], float]):
"""
根据指定学期的学时表,计算新的书院课选课权重。

:param year: 用于查找学时表 table, 一个可能值示例为2024
:param semester: 用于查找学时表 table。semester的值应为'Fall'或'Spring'
:param priority_func: 一个函数,它接受一个int,表示过去学期学时无效的书院课门数,返回新学期选课优先级(0~1)
:return: None
"""
if semester not in ('Fall', 'Spring'):
raise ValueError('semester must have value of "Fall" or "Spring"!')
with transaction.atomic():
# First, all set to 1.0
NaturalPerson.objects.update(course_priority=1.0)
# Invalid records in the given semester
invalid_records = CourseRecord.objects.filter(
year = year, invalid = True, semester = semester
).values_list(
'person__id', flat = True
)
# Counts the number of occurrences of a person's ID in invalid list
invalid_counter: Counter[int] = Counter(invalid_records)
# a[k] is the list of id of people who have k invalid records
a = defaultdict(list)
# Length of the current continuous segment
for person_id in invalid_counter:
cnt = invalid_counter[person_id]
a[cnt].append(person_id)
for i in a:
# There are len(a[i]) people with i invalid records
p = priority_func(i)
NaturalPerson.objects.filter(id__in = a[i]).update(course_priority = p)
21 changes: 21 additions & 0 deletions app/management/commands/update_course_priority.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from django.core.management.base import BaseCommand

from app.course_utils import update_course_priority

class Command(BaseCommand):
# This command should be run before new semester starts
help = "Updates course_priority for all NaturalPerson"

def add_arguments(self, parser):
parser.add_argument('year', type=int, help='year of the course records to check (required)')
parser.add_argument('semester', type=str, help='semester of the course records to check. Valid values are: Fall, Spring (required)')

def handle(self, *args, **options):
try:
# 修改这里的这个lambda,可以实现不同的权重策略
update_course_priority(options['year'], options['semester'],
lambda x: 1 - 0.05 * x)
except Exception as e:
print('Error:', e.args)
else:
print('Success!')
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.0.9 on 2025-01-17 20:52

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("app", "0008_pool_activity_alter_poolitem_exchange_attributes_and_more"),
]

operations = [
migrations.AddConstraint(
model_name="courseparticipant",
constraint=models.UniqueConstraint(
fields=("course", "person"), name="Unique course selection record"
),
),
]
17 changes: 17 additions & 0 deletions app/migrations/0010_naturalperson_course_priority.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 5.0.9 on 2025-01-17 21:27

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("app", "0009_courseparticipant_unique_course_selection_record"),
]

operations = [
migrations.AddField(
model_name="naturalperson",
name="course_priority",
field=models.FloatField(default=1.0, verbose_name="书院课选课权重"),
),
]
4 changes: 4 additions & 0 deletions app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@ class ReceiveLevel(models.IntegerChoices):

accept_promote = models.BooleanField(default=True) # 是否接受推广消息
active_score = models.FloatField("活跃度", default=0) # 用户活跃度
course_priority = models.FloatField("书院课选课权重", default=1.0) # 书院课预选抽签权重

def __str__(self):
return str(self.name)
Expand Down Expand Up @@ -1580,6 +1581,9 @@ class CourseParticipant(models.Model):
class Meta:
verbose_name = "4.课程报名情况"
verbose_name_plural = verbose_name
constraints = [
models.UniqueConstraint(fields = ['course', 'person'], name='Unique course selection record')
]

course = models.ForeignKey(Course, on_delete=models.CASCADE,
related_name="participant_set")
Expand Down
Loading