Compare commits

...

13 Commits

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="FacetManager">
<facet type="django" name="Django">
<configuration>
<option name="rootFolder" value="$MODULE_DIR$" />
<option name="settingsModule" value="learning_platform/settings.py" />
<option name="manageScript" value="$MODULE_DIR$/manage.py" />
<option name="environment" value="&lt;map/&gt;" />
<option name="doNotUseTestRunner" value="false" />
<option name="trackFilePattern" value="migrations" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.8" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
<component name="TemplatesService">
<option name="TEMPLATE_CONFIGURATION" value="Django" />
<option name="TEMPLATE_FOLDERS">
<list>
<option value="$MODULE_DIR$/templates" />
</list>
</option>
</component>
</module>

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="Django default" uuid="d1281534-9e1d-4d3f-b11f-84fe6706a605">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>
<remarks>$PROJECT_DIR$/learning_platform/settings.py</remarks>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:C:\Users\杨新硕\Desktop\Knowledge_Atlas\db.sqlite3</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.8" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.8" project-jdk-type="Python SDK" />
</project>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/Knowledge_Atlas.iml" filepath="$PROJECT_DIR$/.idea/Knowledge_Atlas.iml" />
</modules>
</component>
</project>

@ -0,0 +1,44 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from .models import User, UserFollow
@admin.register(User)
class UserAdmin(BaseUserAdmin):
"""用户管理"""
list_display = ['username', 'email', 'phone', 'school', 'major', 'upload_count', 'following_count', 'followers_count', 'is_active', 'date_joined']
list_filter = ['is_active', 'is_staff', 'school', 'major', 'grade']
search_fields = ['username', 'email', 'phone', 'school']
ordering = ['-date_joined']
fieldsets = BaseUserAdmin.fieldsets + (
('扩展信息', {
'fields': ('phone', 'avatar', 'bio', 'birth_date', 'location', 'school', 'major', 'grade')
}),
('统计信息', {
'fields': ('upload_count', 'download_count'),
'classes': ('collapse',)
}),
)
def following_count(self, obj):
return obj.following_count
following_count.short_description = '关注数'
def followers_count(self, obj):
return obj.followers_count
followers_count.short_description = '粉丝数'
@admin.register(UserFollow)
class UserFollowAdmin(admin.ModelAdmin):
"""用户关注关系管理"""
list_display = ['follower', 'following', 'created_at']
list_filter = ['created_at']
search_fields = ['follower__username', 'following__username']
ordering = ['-created_at']
def get_readonly_fields(self, request, obj=None):
if obj: # 编辑时只读
return ['follower', 'following', 'created_at']
return ['created_at']

@ -0,0 +1,7 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'accounts'
verbose_name = '用户管理'

@ -0,0 +1,98 @@
from django import forms
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
from django.contrib.auth import get_user_model
from .models import College, EnrollmentYear
User = get_user_model()
class CustomUserCreationForm(UserCreationForm):
"""自定义用户注册表单"""
email = forms.EmailField(
required=True,
widget=forms.EmailInput(attrs={
'class': 'form-control',
'placeholder': '请输入邮箱地址'
})
)
username = forms.CharField(
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '请输入用户名'
})
)
password1 = forms.CharField(
widget=forms.PasswordInput(attrs={
'class': 'form-control',
'placeholder': '请输入密码'
})
)
password2 = forms.CharField(
widget=forms.PasswordInput(attrs={
'class': 'form-control',
'placeholder': '请确认密码'
})
)
class Meta:
model = User
fields = ('username', 'email', 'password1', 'password2')
def save(self, commit=True):
user = super().save(commit=False)
user.email = self.cleaned_data['email']
if commit:
user.save()
return user
class CustomAuthenticationForm(AuthenticationForm):
"""自定义用户登录表单"""
username = forms.CharField(
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '用户名或邮箱'
})
)
password = forms.CharField(
widget=forms.PasswordInput(attrs={
'class': 'form-control',
'placeholder': '密码'
})
)
class UserProfileForm(forms.ModelForm):
"""用户资料编辑表单"""
enrollment_year = forms.ChoiceField(
choices=[],
required=False,
widget=forms.Select(attrs={'class': 'form-control'}),
label='入学年份'
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 从数据库读取启用的入学年份选项
year_choices = [('', '请选择入学年份')]
enrollment_years = EnrollmentYear.objects.filter(is_active=True).order_by('-display_order', '-year')
for enrollment_year in enrollment_years:
year_choices.append((enrollment_year.year, f'{enrollment_year.year}'))
self.fields['enrollment_year'].choices = year_choices
# 如果用户已有入学年份,设置初始值
if self.instance and self.instance.enrollment_year:
self.fields['enrollment_year'].initial = self.instance.enrollment_year
class Meta:
model = User
fields = ['username', 'email', 'phone', 'bio', 'school', 'college', 'major', 'enrollment_year']
widgets = {
'username': forms.TextInput(attrs={'class': 'form-control'}),
'email': forms.EmailInput(attrs={'class': 'form-control'}),
'phone': forms.TextInput(attrs={'class': 'form-control'}),
'bio': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
'school': forms.TextInput(attrs={'class': 'form-control'}),
'college': forms.Select(attrs={'class': 'form-control'}),
'major': forms.TextInput(attrs={'class': 'form-control'}),
}

@ -0,0 +1,20 @@
from django.urls import path
from . import html_views
urlpatterns = [
path('register/', html_views.register_view, name='register'),
path('login/', html_views.login_view, name='login'),
path('logout/', html_views.logout_view, name='logout'),
path('profile/', html_views.profile_view, name='profile'),
path('dashboard/', html_views.dashboard_view, name='dashboard'),
# 关注功能
path('following/', html_views.following_list_view, name='following_list'),
path('followers/', html_views.followers_list_view, name='followers_list'),
path('discover-users/', html_views.discover_users_view, name='discover_users'),
path('follow/<int:user_id>/', html_views.follow_user_view, name='follow_user'),
path('unfollow/<int:user_id>/', html_views.unfollow_user_view, name='unfollow_user'),
# 资料管理
path('delete-material/<int:material_id>/', html_views.delete_material_view, name='delete_material'),
]

@ -0,0 +1,318 @@
from django.shortcuts import render, redirect
from django.contrib.auth import login, authenticate, logout
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.urls import reverse
from .forms import CustomUserCreationForm, CustomAuthenticationForm, UserProfileForm
def register_view(request):
"""用户注册页面"""
if request.user.is_authenticated:
return redirect('home')
if request.method == 'POST':
form = CustomUserCreationForm(request.POST)
if form.is_valid():
user = form.save()
username = form.cleaned_data.get('username')
messages.success(request, f'账户创建成功!欢迎 {username}')
login(request, user)
return redirect('home')
else:
messages.error(request, '注册失败,请检查输入信息。')
else:
form = CustomUserCreationForm()
return render(request, 'accounts/register.html', {'form': form})
def login_view(request):
"""用户登录页面"""
if request.user.is_authenticated:
return redirect('home')
if request.method == 'POST':
form = CustomAuthenticationForm(request, data=request.POST)
if form.is_valid():
username = form.cleaned_data.get('username')
password = form.cleaned_data.get('password')
user = authenticate(username=username, password=password)
if user is not None:
login(request, user)
messages.success(request, f'欢迎回来,{user.username}')
next_url = request.GET.get('next', 'home')
return redirect(next_url)
else:
messages.error(request, '登录失败,请检查用户名和密码。')
else:
form = CustomAuthenticationForm()
return render(request, 'accounts/login.html', {'form': form})
def logout_view(request):
"""用户登出"""
logout(request)
messages.success(request, '您已成功登出。')
return redirect('home')
@login_required
def profile_view(request):
"""用户资料页面"""
if request.method == 'POST':
form = UserProfileForm(request.POST, request.FILES, instance=request.user)
if form.is_valid():
form.save()
messages.success(request, '资料更新成功!')
return redirect('profile')
else:
messages.error(request, '更新失败,请检查输入信息。')
else:
form = UserProfileForm(instance=request.user)
return render(request, 'accounts/profile.html', {
'form': form,
'user': request.user
})
@login_required
def dashboard_view(request):
"""用户仪表板"""
from community.models import PostLike, PostFavorite, UserActivity
from materials.models import Material, MaterialLike, MaterialFavorite
# 获取用户统计数据
user_stats = {
'upload_count': request.user.upload_count,
'download_count': request.user.download_count,
'join_date': request.user.date_joined,
'following_count': request.user.following_count,
'followers_count': request.user.followers_count,
'coins': request.user.coins, # 金币余额
}
# 获取社区统计
community_stats = {
'posts_count': request.user.posts.filter(is_active=True).count(),
'replies_count': request.user.replies.filter(is_active=True).count(),
'likes_given': PostLike.objects.filter(user=request.user).count() + MaterialLike.objects.filter(user=request.user).count(),
'favorites_count': PostFavorite.objects.filter(user=request.user).count() + MaterialFavorite.objects.filter(user=request.user).count(),
}
# 最近的点赞和收藏(社区帖子)
recent_likes = PostLike.objects.filter(user=request.user).select_related(
'post__author', 'post__category'
)[:5]
recent_favorites = PostFavorite.objects.filter(user=request.user).select_related(
'post__author', 'post__category'
)[:5]
# 最近点赞的资料
recent_material_likes = MaterialLike.objects.filter(user=request.user).select_related(
'material__uploader', 'material__category'
).order_by('-created_at')[:5]
# 最近收藏的资料
recent_material_favorites = MaterialFavorite.objects.filter(user=request.user).select_related(
'material__uploader', 'material__category'
).order_by('-created_at')[:5]
# 最近活动
recent_activities = UserActivity.objects.filter(user=request.user).select_related(
'post', 'reply'
)[:10]
# 关注的用户列表最近关注的5个
recent_following = request.user.following.filter(is_active=True)[:5]
# 最近的粉丝最近关注我的5个用户
recent_followers = request.user.followers.filter(is_active=True)[:5]
# 获取用户上传的资料
user_materials = Material.objects.filter(uploader=request.user).select_related(
'category', 'classification__predicted_category'
).order_by('-created_at')[:10]
return render(request, 'accounts/dashboard.html', {
'user_stats': user_stats,
'community_stats': community_stats,
'recent_likes': recent_likes,
'recent_favorites': recent_favorites,
'recent_material_likes': recent_material_likes,
'recent_material_favorites': recent_material_favorites,
'recent_activities': recent_activities,
'recent_following': recent_following,
'recent_followers': recent_followers,
'user_materials': user_materials,
})
@login_required
def delete_material_view(request, material_id):
"""删除用户上传的资料"""
from django.http import JsonResponse
from materials.models import Material
if request.method != 'POST':
return JsonResponse({'error': '请求方法错误'}, status=405)
try:
material = Material.objects.get(id=material_id, uploader=request.user)
material_title = material.title
material.delete()
# 更新用户上传统计
request.user.upload_count = max(0, request.user.upload_count - 1)
request.user.save(update_fields=['upload_count'])
return JsonResponse({
'success': True,
'message': f'资料《{material_title}》已成功删除'
})
except Material.DoesNotExist:
return JsonResponse({'error': '资料不存在或无权限删除'}, status=404)
except Exception as e:
return JsonResponse({'error': f'删除失败: {str(e)}'}, status=500)
@login_required
def following_list_view(request):
"""我关注的用户列表页面"""
# 使用正确的related_name: following_relations
following_users = request.user.following.filter(is_active=True)
# 通过UserFollow模型按时间排序
from .models import UserFollow
follow_relations = UserFollow.objects.filter(
follower=request.user
).select_related('following').order_by('-created_at')
following_users = [relation.following for relation in follow_relations if relation.following.is_active]
return render(request, 'accounts/following_list.html', {
'following_users': following_users,
'following_count': len(following_users),
})
@login_required
def followers_list_view(request):
"""我的粉丝列表页面"""
# 使用正确的related_name: follower_relations
from .models import UserFollow
follower_relations = UserFollow.objects.filter(
following=request.user
).select_related('follower').order_by('-created_at')
followers_users = [relation.follower for relation in follower_relations if relation.follower.is_active]
return render(request, 'accounts/followers_list.html', {
'followers_users': followers_users,
'followers_count': len(followers_users),
})
@login_required
def follow_user_view(request, user_id):
"""关注用户AJAX请求"""
from django.http import JsonResponse
from django.contrib.auth import get_user_model
if request.method != 'POST':
return JsonResponse({'error': '请求方法错误'}, status=405)
User = get_user_model()
try:
user_to_follow = User.objects.get(id=user_id, is_active=True)
except User.DoesNotExist:
return JsonResponse({'error': '用户不存在'}, status=404)
if user_to_follow == request.user:
return JsonResponse({'error': '不能关注自己'}, status=400)
if request.user.follow(user_to_follow):
return JsonResponse({
'success': True,
'message': f'成功关注 {user_to_follow.username}',
'is_following': True,
'followers_count': user_to_follow.followers_count
})
else:
return JsonResponse({
'success': False,
'message': '已经关注过了',
'is_following': True,
'followers_count': user_to_follow.followers_count
})
@login_required
def unfollow_user_view(request, user_id):
"""取消关注用户AJAX请求"""
from django.http import JsonResponse
from django.contrib.auth import get_user_model
if request.method != 'POST':
return JsonResponse({'error': '请求方法错误'}, status=405)
User = get_user_model()
try:
user_to_unfollow = User.objects.get(id=user_id, is_active=True)
except User.DoesNotExist:
return JsonResponse({'error': '用户不存在'}, status=404)
if request.user.unfollow(user_to_unfollow):
return JsonResponse({
'success': True,
'message': f'已取消关注 {user_to_unfollow.username}',
'is_following': False,
'followers_count': user_to_unfollow.followers_count
})
else:
return JsonResponse({
'success': False,
'message': '未关注该用户',
'is_following': False,
'followers_count': user_to_unfollow.followers_count
})
@login_required
def discover_users_view(request):
"""发现用户页面"""
from django.db.models import Count
from django.contrib.auth import get_user_model
User = get_user_model()
# 获取排序参数
sort_by = request.GET.get('sort', 'active')
# 排除当前用户,获取其他活跃用户
users_queryset = User.objects.filter(is_active=True).exclude(id=request.user.id)
# 根据排序条件排序
if sort_by == 'newest':
users_queryset = users_queryset.order_by('-date_joined')
elif sort_by == 'followers':
users_queryset = users_queryset.annotate(
follower_count=Count('followers')
).order_by('-follower_count')
elif sort_by == 'materials':
users_queryset = users_queryset.order_by('-upload_count')
else: # active - 最活跃(按上传资料数量和关注数综合)
users_queryset = users_queryset.annotate(
activity_score=Count('material') + Count('followers')
).order_by('-activity_score', '-date_joined')
# 限制数量,避免一次加载太多
users = users_queryset[:50]
return render(request, 'accounts/discover_users.html', {
'users': users,
'sort_by': sort_by,
})

@ -0,0 +1,53 @@
from django.core.management.base import BaseCommand
from accounts.models import College
class Command(BaseCommand):
help = '初始化学院数据'
def handle(self, *args, **options):
colleges_data = [
{'name': '计算机科学与技术学院', 'code': 'CS', 'description': '计算机科学与技术相关专业'},
{'name': '软件工程学院', 'code': 'SE', 'description': '软件工程相关专业'},
{'name': '信息工程学院', 'code': 'IE', 'description': '信息工程相关专业'},
{'name': '电子工程学院', 'code': 'EE', 'description': '电子工程相关专业'},
{'name': '机械工程学院', 'code': 'ME', 'description': '机械工程相关专业'},
{'name': '土木工程学院', 'code': 'CE', 'description': '土木工程相关专业'},
{'name': '化学工程学院', 'code': 'CHE', 'description': '化学工程相关专业'},
{'name': '材料科学与工程学院', 'code': 'MSE', 'description': '材料科学与工程相关专业'},
{'name': '数学与统计学院', 'code': 'MATH', 'description': '数学与统计学相关专业'},
{'name': '物理学院', 'code': 'PHY', 'description': '物理学相关专业'},
{'name': '化学学院', 'code': 'CHEM', 'description': '化学相关专业'},
{'name': '生命科学学院', 'code': 'BIO', 'description': '生命科学相关专业'},
{'name': '经济管理学院', 'code': 'EM', 'description': '经济管理相关专业'},
{'name': '外国语学院', 'code': 'FL', 'description': '外国语言文学相关专业'},
{'name': '文学院', 'code': 'LIT', 'description': '中国语言文学相关专业'},
{'name': '法学院', 'code': 'LAW', 'description': '法学相关专业'},
{'name': '艺术学院', 'code': 'ART', 'description': '艺术相关专业'},
{'name': '体育学院', 'code': 'PE', 'description': '体育相关专业'},
{'name': '医学院', 'code': 'MED', 'description': '医学相关专业'},
{'name': '教育学院', 'code': 'EDU', 'description': '教育学相关专业'},
]
created_count = 0
for college_data in colleges_data:
college, created = College.objects.get_or_create(
code=college_data['code'],
defaults={
'name': college_data['name'],
'description': college_data['description']
}
)
if created:
created_count += 1
self.stdout.write(
self.style.SUCCESS(f'创建学院: {college.name}')
)
else:
self.stdout.write(
self.style.WARNING(f'学院已存在: {college.name}')
)
self.stdout.write(
self.style.SUCCESS(f'成功创建 {created_count} 个学院')
)

@ -0,0 +1,50 @@
"""
将现有用户设置为管理员账号
使用方法: python manage.py make_admin <username>
"""
from django.core.management.base import BaseCommand, CommandError
from django.contrib.auth import get_user_model
User = get_user_model()
class Command(BaseCommand):
help = '将现有用户设置为管理员账号'
def add_arguments(self, parser):
parser.add_argument('username', type=str, help='要设置为管理员的用户名')
parser.add_argument(
'--email',
type=str,
help='管理员邮箱(可选)',
default=None
)
def handle(self, *args, **options):
username = options['username']
email = options['email']
try:
user = User.objects.get(username=username)
# 设置为管理员
user.is_staff = True
user.is_superuser = True
if email:
user.email = email
user.save()
self.stdout.write(
self.style.SUCCESS(
f'成功将用户 "{username}" 设置为管理员!\n'
f'用户名: {user.username}\n'
f'邮箱: {user.email}\n'
f'是否管理员: {user.is_staff}\n'
f'是否超级用户: {user.is_superuser}'
)
)
except User.DoesNotExist:
raise CommandError(f'用户 "{username}" 不存在!')
except Exception as e:
raise CommandError(f'设置管理员失败: {str(e)}')

@ -0,0 +1,10 @@
# Generated by Django 5.0.7 on 2025-09-17 12:30
from django.db import migrations
class Migration(migrations.Migration):
dependencies = []
operations = []

@ -0,0 +1,185 @@
# Generated by Django 5.0.7 on 2025-09-17 13:05
import django.contrib.auth.models
import django.contrib.auth.validators
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("accounts", "0001_initial"),
("auth", "0012_alter_user_first_name_max_length"),
]
operations = [
migrations.CreateModel(
name="User",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("password", models.CharField(max_length=128, verbose_name="password")),
(
"last_login",
models.DateTimeField(
blank=True, null=True, verbose_name="last login"
),
),
(
"is_superuser",
models.BooleanField(
default=False,
help_text="Designates that this user has all permissions without explicitly assigning them.",
verbose_name="superuser status",
),
),
(
"username",
models.CharField(
error_messages={
"unique": "A user with that username already exists."
},
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
max_length=150,
unique=True,
validators=[
django.contrib.auth.validators.UnicodeUsernameValidator()
],
verbose_name="username",
),
),
(
"first_name",
models.CharField(
blank=True, max_length=150, verbose_name="first name"
),
),
(
"last_name",
models.CharField(
blank=True, max_length=150, verbose_name="last name"
),
),
(
"is_staff",
models.BooleanField(
default=False,
help_text="Designates whether the user can log into this admin site.",
verbose_name="staff status",
),
),
(
"is_active",
models.BooleanField(
default=True,
help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
verbose_name="active",
),
),
(
"date_joined",
models.DateTimeField(
default=django.utils.timezone.now, verbose_name="date joined"
),
),
(
"email",
models.EmailField(max_length=254, unique=True, verbose_name="邮箱"),
),
(
"phone",
models.CharField(
blank=True, max_length=11, null=True, verbose_name="手机号"
),
),
(
"avatar",
models.ImageField(
blank=True, null=True, upload_to="avatars/", verbose_name="头像"
),
),
(
"bio",
models.TextField(
blank=True, max_length=500, verbose_name="个人简介"
),
),
(
"birth_date",
models.DateField(blank=True, null=True, verbose_name="出生日期"),
),
(
"location",
models.CharField(blank=True, max_length=100, verbose_name="所在地"),
),
(
"school",
models.CharField(blank=True, max_length=100, verbose_name="学校"),
),
(
"major",
models.CharField(blank=True, max_length=100, verbose_name="专业"),
),
(
"grade",
models.CharField(blank=True, max_length=20, verbose_name="年级"),
),
(
"upload_count",
models.PositiveIntegerField(default=0, verbose_name="上传资料数"),
),
(
"download_count",
models.PositiveIntegerField(default=0, verbose_name="下载次数"),
),
(
"created_at",
models.DateTimeField(auto_now_add=True, verbose_name="创建时间"),
),
(
"updated_at",
models.DateTimeField(auto_now=True, verbose_name="更新时间"),
),
(
"groups",
models.ManyToManyField(
blank=True,
help_text="The groups this user belongs to.",
related_name="custom_user_set",
related_query_name="custom_user",
to="auth.group",
verbose_name="groups",
),
),
(
"user_permissions",
models.ManyToManyField(
blank=True,
help_text="Specific permissions for this user.",
related_name="custom_user_set",
related_query_name="custom_user",
to="auth.permission",
verbose_name="user permissions",
),
),
],
options={
"verbose_name": "用户",
"verbose_name_plural": "用户",
"db_table": "accounts_user",
},
managers=[
("objects", django.contrib.auth.models.UserManager()),
],
),
]

@ -0,0 +1,36 @@
# Generated by Django 5.0.7 on 2025-09-20 00:03
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0002_initial'),
]
operations = [
migrations.CreateModel(
name='UserFollow',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='关注时间')),
('follower', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='following_relations', to=settings.AUTH_USER_MODEL, verbose_name='关注者')),
('following', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='follower_relations', to=settings.AUTH_USER_MODEL, verbose_name='被关注者')),
],
options={
'verbose_name': '用户关注',
'verbose_name_plural': '用户关注',
'db_table': 'accounts_user_follow',
'ordering': ['-created_at'],
'unique_together': {('follower', 'following')},
},
),
migrations.AddField(
model_name='user',
name='following',
field=models.ManyToManyField(blank=True, related_name='followers', through='accounts.UserFollow', to=settings.AUTH_USER_MODEL, verbose_name='关注的用户'),
),
]

@ -0,0 +1,13 @@
# Generated by Django 5.0.7 on 2025-09-20 00:03
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('accounts', '0003_userfollow_user_following'),
]
operations = [
]

@ -0,0 +1,62 @@
# Generated by Django 4.2.7 on 2025-10-24 08:30
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("accounts", "0004_auto_20250920_0803"),
]
operations = [
migrations.CreateModel(
name="College",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"name",
models.CharField(
max_length=100, unique=True, verbose_name="学院名称"
),
),
(
"code",
models.CharField(
max_length=20, unique=True, verbose_name="学院代码"
),
),
("description", models.TextField(blank=True, verbose_name="学院描述")),
(
"created_at",
models.DateTimeField(auto_now_add=True, verbose_name="创建时间"),
),
],
options={
"verbose_name": "学院",
"verbose_name_plural": "学院",
"db_table": "accounts_college",
"ordering": ["name"],
},
),
migrations.AddField(
model_name="user",
name="college",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="accounts.college",
verbose_name="学院",
),
),
]

@ -0,0 +1,20 @@
# Generated by Django 4.2.25 on 2025-11-14 07:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0005_college_user_college"),
]
operations = [
migrations.AddField(
model_name="user",
name="enrollment_year",
field=models.PositiveIntegerField(
blank=True, help_text="例如2023", null=True, verbose_name="入学年份"
),
),
]

@ -0,0 +1,20 @@
# Generated by Django 4.2.25 on 2025-11-19 05:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0006_user_enrollment_year"),
]
operations = [
migrations.AddField(
model_name="user",
name="coins",
field=models.PositiveIntegerField(
default=0, help_text="用户金币余额", verbose_name="金币"
),
),
]

@ -0,0 +1,57 @@
# Generated by Django 4.2.25 on 2025-11-19 09:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0007_user_coins"),
]
operations = [
migrations.CreateModel(
name="EnrollmentYear",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"year",
models.PositiveIntegerField(
help_text="例如2023", unique=True, verbose_name="年份"
),
),
(
"is_active",
models.BooleanField(default=True, verbose_name="是否启用"),
),
(
"display_order",
models.IntegerField(
default=0, help_text="数字越大越靠前", verbose_name="显示顺序"
),
),
(
"created_at",
models.DateTimeField(auto_now_add=True, verbose_name="创建时间"),
),
(
"updated_at",
models.DateTimeField(auto_now=True, verbose_name="更新时间"),
),
],
options={
"verbose_name": "入学年份",
"verbose_name_plural": "入学年份",
"db_table": "accounts_enrollment_year",
"ordering": ["-display_order", "-year"],
},
),
]

@ -0,0 +1,229 @@
from django.contrib.auth.models import AbstractUser
from django.db import models
class College(models.Model):
"""学院模型"""
name = models.CharField('学院名称', max_length=100, unique=True)
code = models.CharField('学院代码', max_length=20, unique=True)
description = models.TextField('学院描述', blank=True)
created_at = models.DateTimeField('创建时间', auto_now_add=True)
class Meta:
db_table = 'accounts_college'
verbose_name = '学院'
verbose_name_plural = '学院'
ordering = ['name']
def __str__(self):
return self.name
class EnrollmentYear(models.Model):
"""入学年份模型"""
year = models.PositiveIntegerField('年份', unique=True, help_text='例如2023')
is_active = models.BooleanField('是否启用', default=True)
display_order = models.IntegerField('显示顺序', default=0, help_text='数字越大越靠前')
created_at = models.DateTimeField('创建时间', auto_now_add=True)
updated_at = models.DateTimeField('更新时间', auto_now=True)
class Meta:
db_table = 'accounts_enrollment_year'
verbose_name = '入学年份'
verbose_name_plural = '入学年份'
ordering = ['-display_order', '-year']
def __str__(self):
return f'{self.year}'
class User(AbstractUser):
"""扩展用户模型"""
email = models.EmailField('邮箱', unique=True)
phone = models.CharField('手机号', max_length=11, blank=True, null=True)
avatar = models.ImageField('头像', upload_to='avatars/', blank=True, null=True)
bio = models.TextField('个人简介', max_length=500, blank=True)
birth_date = models.DateField('出生日期', blank=True, null=True)
location = models.CharField('所在地', max_length=100, blank=True)
school = models.CharField('学校', max_length=100, blank=True)
college = models.ForeignKey(College, on_delete=models.SET_NULL, null=True, blank=True, verbose_name='学院')
major = models.CharField('专业', max_length=100, blank=True)
grade = models.CharField('年级', max_length=20, blank=True) # 保留用于兼容
enrollment_year = models.PositiveIntegerField('入学年份', null=True, blank=True, help_text='例如2023')
# 统计字段
upload_count = models.PositiveIntegerField('上传资料数', default=0)
download_count = models.PositiveIntegerField('下载次数', default=0)
# 金币字段
coins = models.PositiveIntegerField('金币', default=0, help_text='用户金币余额')
# 关注关系
following = models.ManyToManyField(
'self',
through='UserFollow',
related_name='followers',
symmetrical=False,
blank=True,
verbose_name='关注的用户'
)
created_at = models.DateTimeField('创建时间', auto_now_add=True)
updated_at = models.DateTimeField('更新时间', auto_now=True)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username']
# 解决与默认User模型的冲突
groups = models.ManyToManyField(
'auth.Group',
verbose_name='groups',
blank=True,
help_text='The groups this user belongs to.',
related_name='custom_user_set',
related_query_name='custom_user',
)
user_permissions = models.ManyToManyField(
'auth.Permission',
verbose_name='user permissions',
blank=True,
help_text='Specific permissions for this user.',
related_name='custom_user_set',
related_query_name='custom_user',
)
class Meta:
db_table = 'accounts_user'
verbose_name = '用户'
verbose_name_plural = '用户'
def __str__(self):
return self.username or self.email
@property
def current_grade(self):
"""根据入学年份自动计算当前年级"""
if not self.enrollment_year:
return self.grade if self.grade else '未设置'
from datetime import datetime
now = datetime.now()
current_year = now.year
current_month = now.month
# 如果当前月份>=99月开学当前学年=当前年份
# 如果当前月份<9当前学年=当前年份-1
if current_month >= 9:
current_academic_year = current_year
else:
current_academic_year = current_year - 1
# 计算年级 = 当前学年 - 入学年份 + 1
grade_number = current_academic_year - self.enrollment_year + 1
# 映射到大一、大二、大三、大四
grade_mapping = {
1: '大一',
2: '大二',
3: '大三',
4: '大四',
}
if grade_number in grade_mapping:
return grade_mapping[grade_number]
elif grade_number < 1:
return '未入学'
elif grade_number > 4:
return '已毕业'
else:
return f'{grade_number}年级'
@property
def current_grade_en(self):
"""返回英文年级代码(用于推荐系统)"""
if not self.enrollment_year:
# 如果没有入学年份尝试从旧的grade字段映射
grade_mapping = {
'大一': 'freshman',
'大二': 'sophomore',
'大三': 'junior',
'大四': 'senior',
}
return grade_mapping.get(self.grade, None)
from datetime import datetime
now = datetime.now()
current_year = now.year
current_month = now.month
if current_month >= 9:
current_academic_year = current_year
else:
current_academic_year = current_year - 1
grade_number = current_academic_year - self.enrollment_year + 1
grade_en_mapping = {
1: 'freshman',
2: 'sophomore',
3: 'junior',
4: 'senior',
}
return grade_en_mapping.get(grade_number, None)
@property
def following_count(self):
"""关注数量"""
return self.following.count()
@property
def followers_count(self):
"""粉丝数量"""
return self.followers.count()
def is_following(self, user):
"""检查是否关注了某用户"""
return self.following.filter(id=user.id).exists()
def follow(self, user):
"""关注用户"""
if not self.is_following(user) and user != self:
UserFollow.objects.create(follower=self, following=user)
return True
return False
def unfollow(self, user):
"""取消关注用户"""
follow_relation = UserFollow.objects.filter(follower=self, following=user).first()
if follow_relation:
follow_relation.delete()
return True
return False
class UserFollow(models.Model):
"""用户关注关系"""
follower = models.ForeignKey(
'User',
on_delete=models.CASCADE,
related_name='following_relations',
verbose_name='关注者'
)
following = models.ForeignKey(
'User',
on_delete=models.CASCADE,
related_name='follower_relations',
verbose_name='被关注者'
)
created_at = models.DateTimeField('关注时间', auto_now_add=True)
class Meta:
db_table = 'accounts_user_follow'
verbose_name = '用户关注'
verbose_name_plural = '用户关注'
unique_together = ['follower', 'following']
ordering = ['-created_at']
def __str__(self):
return f'{self.follower.username} 关注 {self.following.username}'

@ -0,0 +1,142 @@
from rest_framework import serializers
from django.contrib.auth import authenticate
from django.contrib.auth.password_validation import validate_password
from .models import User, UserFollow, College
class CollegeSerializer(serializers.ModelSerializer):
"""学院序列化器"""
class Meta:
model = College
fields = ['id', 'name', 'code', 'description']
class UserRegistrationSerializer(serializers.ModelSerializer):
"""用户注册序列化器"""
password = serializers.CharField(write_only=True, validators=[validate_password])
password_confirm = serializers.CharField(write_only=True)
class Meta:
model = User
fields = ['username', 'email', 'password', 'password_confirm', 'phone', 'college', 'major']
def validate(self, attrs):
if attrs['password'] != attrs['password_confirm']:
raise serializers.ValidationError("两次输入的密码不一致")
return attrs
def create(self, validated_data):
validated_data.pop('password_confirm')
user = User.objects.create_user(**validated_data)
return user
class UserLoginSerializer(serializers.Serializer):
"""用户登录序列化器"""
email = serializers.EmailField()
password = serializers.CharField()
def validate(self, attrs):
email = attrs.get('email')
password = attrs.get('password')
if email and password:
user = authenticate(username=email, password=password)
if not user:
raise serializers.ValidationError('邮箱或密码错误')
if not user.is_active:
raise serializers.ValidationError('账户已被禁用')
attrs['user'] = user
else:
raise serializers.ValidationError('必须提供邮箱和密码')
return attrs
class UserProfileSerializer(serializers.ModelSerializer):
"""用户资料序列化器"""
college = CollegeSerializer(read_only=True)
following_count = serializers.ReadOnlyField()
followers_count = serializers.ReadOnlyField()
is_following = serializers.SerializerMethodField()
class Meta:
model = User
fields = [
'id', 'username', 'email', 'phone', 'avatar', 'bio',
'birth_date', 'location', 'school', 'college', 'major', 'grade',
'upload_count', 'download_count', 'following_count', 'followers_count',
'is_following', 'date_joined', 'last_login'
]
read_only_fields = [
'id', 'upload_count', 'download_count', 'following_count', 'followers_count',
'is_following', 'date_joined', 'last_login'
]
def get_is_following(self, obj):
"""获取当前用户是否关注了这个用户"""
request = self.context.get('request')
if request and request.user.is_authenticated and request.user != obj:
return request.user.is_following(obj)
return False
class UserUpdateSerializer(serializers.ModelSerializer):
"""用户信息更新序列化器"""
class Meta:
model = User
fields = [
'username', 'phone', 'avatar', 'bio', 'birth_date',
'location', 'school', 'college', 'major', 'grade'
]
class PasswordChangeSerializer(serializers.Serializer):
"""密码修改序列化器"""
old_password = serializers.CharField()
new_password = serializers.CharField(validators=[validate_password])
new_password_confirm = serializers.CharField()
def validate(self, attrs):
if attrs['new_password'] != attrs['new_password_confirm']:
raise serializers.ValidationError("两次输入的新密码不一致")
return attrs
def validate_old_password(self, value):
user = self.context['request'].user
if not user.check_password(value):
raise serializers.ValidationError("原密码错误")
return value
class UserFollowSerializer(serializers.ModelSerializer):
"""用户关注关系序列化器"""
follower = UserProfileSerializer(read_only=True)
following = UserProfileSerializer(read_only=True)
class Meta:
model = UserFollow
fields = ['id', 'follower', 'following', 'created_at']
read_only_fields = ['id', 'created_at']
class UserSimpleSerializer(serializers.ModelSerializer):
"""简化的用户信息序列化器(用于关注列表)"""
following_count = serializers.ReadOnlyField()
followers_count = serializers.ReadOnlyField()
is_following = serializers.SerializerMethodField()
class Meta:
model = User
fields = [
'id', 'username', 'avatar', 'bio', 'school', 'major',
'following_count', 'followers_count', 'is_following', 'date_joined'
]
read_only_fields = ['id', 'following_count', 'followers_count', 'is_following', 'date_joined']
def get_is_following(self, obj):
"""获取当前用户是否关注了这个用户"""
request = self.context.get('request')
if request and request.user.is_authenticated and request.user != obj:
return request.user.is_following(obj)
return False

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save