new
All checks were successful
Deploy to Server / deploy (push) Successful in 17s

This commit is contained in:
jeremygan2021
2026-03-20 13:27:54 +08:00
parent 0d7ba5d87c
commit c62c5b98ea
5 changed files with 238 additions and 7 deletions

View File

@@ -1,7 +1,7 @@
from django.contrib import admin from django.contrib import admin
from unfold.admin import ModelAdmin from unfold.admin import ModelAdmin
from unfold.decorators import display from unfold.decorators import display
from .models import Competition, CompetitionEnrollment, ScoreDimension, Project, ProjectFile, Score, Comment from .models import Competition, CompetitionEnrollment, ScoreDimension, Project, ProjectFile, Score, Comment, ScoreFormula
class ScoreDimensionInline(admin.TabularInline): class ScoreDimensionInline(admin.TabularInline):
model = ScoreDimension model = ScoreDimension
@@ -143,3 +143,60 @@ class CommentAdmin(ModelAdmin):
def content_preview(self, obj): def content_preview(self, obj):
return obj.content[:50] + '...' if len(obj.content) > 50 else obj.content return obj.content[:50] + '...' if len(obj.content) > 50 else obj.content
content_preview.short_description = "评语内容" content_preview.short_description = "评语内容"
class ScoreFormulaAdmin(ModelAdmin):
"""
评分公式管理
提供可视化公式编辑功能
"""
list_display = ['name', 'competition', 'formula_preview_display', 'is_active', 'is_default', 'created_at']
list_filter = ['competition', 'is_active', 'is_default']
search_fields = ['name', 'description', 'formula', 'competition__title']
autocomplete_fields = ['competition']
fieldsets = (
('基本信息', {
'fields': ('competition', 'name', 'description')
}),
('公式配置', {
'fields': ('formula',),
'description': '使用维度名称作为变量,例如: 创新性 * 0.3 + 实用性 * 0.5 + 演示效果 * 0.2'
}),
('公式设置', {
'fields': ('is_active', 'is_default')
}),
)
@display(description="公式预览")
def formula_preview_display(self, obj):
preview = obj.get_formula_preview()
return preview[:100] + '...' if len(preview) > 100 else preview if preview else '-'
def get_form_kwargs(self, request, *args, **kwargs):
kwargs = super().get_form_kwargs(request, *args, **kwargs)
return kwargs
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
extra_context = extra_context or {}
if request.method == 'GET' and not object_id:
competition_id = request.GET.get('competition')
if competition_id:
try:
from .models import ScoreDimension
dimensions = ScoreDimension.objects.filter(competition_id=competition_id)
extra_context['dimensions'] = dimensions
except:
pass
return super().changeform_view(request, object_id, form_url, extra_context)
class Media:
css = {
'all': ('competition/admin/css/formula-editor.css',)
}
js = ('competition/admin/js/formula-editor.js',)
admin.site.register(ScoreFormula, ScoreFormulaAdmin)

View File

@@ -0,0 +1,33 @@
# Generated by Django 6.0.1 on 2026-03-20 05:27
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('competition', '0007_competition_custom_score_formula_and_more'),
]
operations = [
migrations.CreateModel(
name='ScoreFormula',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='用于标识这个公式,方便管理', max_length=100, verbose_name='公式名称')),
('description', models.TextField(blank=True, verbose_name='公式说明')),
('formula', models.TextField(help_text='使用维度名称作为变量,支持四则运算和函数', verbose_name='计算公式')),
('is_active', models.BooleanField(default=True, verbose_name='是否启用')),
('is_default', models.BooleanField(default=False, verbose_name='是否设为默认公式')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('competition', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='score_formulas', to='competition.competition', verbose_name='所属比赛')),
],
options={
'verbose_name': '评分公式',
'verbose_name_plural': '评分公式配置',
'ordering': ['-is_default', '-created_at'],
},
),
]

View File

@@ -177,13 +177,14 @@ class Project(models.Model):
def calculate_score(self): def calculate_score(self):
""" """
计算项目得分 计算项目得分
支持种模式: 支持种模式:
1. 默认加权平均:每个评委的得分 = sum(维度分数 × 维度权重),然后所有评委取平均 1. 默认加权平均:每个评委的得分 = sum(维度分数 × 维度权重),然后所有评委取平均
2. 自定义算式:使用比赛级别的 custom_score_formula 计算最终得分 2. 自定义算式(比赛级别):使用比赛级别的 custom_score_formula 计算最终得分
3. 公式配置(公式级别):使用 ScoreFormula 模型中的公式配置
自定义算式变量格式: 自定义算式变量格式:
- dimension_X: 第X个维度的平均分所有评委对该维度的平均分 - dimension_X: 第X个维度的平均分所有评委对该维度的平均分
- 也可以在算式中直接使用维度ID - 也可以使用维度名称作为变量
""" """
scores = self.scores.all() scores = self.scores.all()
if not scores.exists(): if not scores.exists():
@@ -193,6 +194,15 @@ class Project(models.Model):
competition = self.competition competition = self.competition
active_formula = ScoreFormula.objects.filter(
competition=competition,
is_active=True,
is_default=True
).first()
if active_formula:
return self._calculate_formula_score(scores, active_formula)
if competition.score_calculation_type == 'custom' and competition.custom_score_formula: if competition.score_calculation_type == 'custom' and competition.custom_score_formula:
return self._calculate_custom_score(scores, competition.custom_score_formula) return self._calculate_custom_score(scores, competition.custom_score_formula)
@@ -227,7 +237,7 @@ class Project(models.Model):
def _calculate_custom_score(self, scores, formula): def _calculate_custom_score(self, scores, formula):
""" """
自定义算式模式 自定义算式模式(比赛级别)
使用比赛配置的自定义算式计算得分 使用比赛配置的自定义算式计算得分
""" """
dimension_scores = {} dimension_scores = {}
@@ -238,6 +248,7 @@ class Project(models.Model):
if dim_scores.exists(): if dim_scores.exists():
avg = sum(float(s.score) for s in dim_scores) / dim_scores.count() avg = sum(float(s.score) for s in dim_scores) / dim_scores.count()
dimension_scores[f'dimension_{dimension.id}'] = avg dimension_scores[f'dimension_{dimension.id}'] = avg
dimension_scores[dimension.name] = avg
if not dimension_scores: if not dimension_scores:
self.final_score = 0 self.final_score = 0
@@ -254,6 +265,37 @@ class Project(models.Model):
print(f"算式计算错误: {e}, formula: {formula}, values: {dimension_scores}") print(f"算式计算错误: {e}, formula: {formula}, values: {dimension_scores}")
return self._calculate_default_score(scores) return self._calculate_default_score(scores)
def _calculate_formula_score(self, scores, formula_obj):
"""
公式配置模式(使用 ScoreFormula 模型)
使用公式配置中的公式计算得分
"""
dimension_scores = {}
dimensions = self.competition.score_dimensions.all()
for dimension in dimensions:
dim_scores = scores.filter(dimension=dimension)
if dim_scores.exists():
avg = sum(float(s.score) for s in dim_scores) / dim_scores.count()
dimension_scores[dimension.name] = avg
if not dimension_scores:
self.final_score = 0
self.save()
return 0
formula = formula_obj.formula
try:
result = eval(formula, {"__builtins__": {}}, dimension_scores)
final_score = float(result)
self.final_score = round(final_score, 2)
self.save()
return self.final_score
except Exception as e:
print(f"公式计算错误: {e}, formula: {formula}, values: {dimension_scores}")
return self._calculate_default_score(scores)
def calculate_judge_score(self, judge): def calculate_judge_score(self, judge):
""" """
计算指定评委对该项目的得分 计算指定评委对该项目的得分
@@ -346,3 +388,70 @@ class Comment(models.Model):
def __str__(self): def __str__(self):
return f"{self.judge.user.nickname} -> {self.project.title}" return f"{self.judge.user.nickname} -> {self.project.title}"
class ScoreFormula(models.Model):
"""
评分公式配置
用于可视化配置得分计算公式
"""
competition = models.ForeignKey(Competition, on_delete=models.CASCADE, related_name='score_formulas', verbose_name="所属比赛")
name = models.CharField(max_length=100, verbose_name="公式名称", help_text="用于标识这个公式,方便管理")
description = models.TextField(verbose_name="公式说明", blank=True)
formula = models.TextField(verbose_name="计算公式", help_text="使用维度名称作为变量,支持四则运算和函数")
is_active = models.BooleanField(default=True, verbose_name="是否启用")
is_default = models.BooleanField(default=False, verbose_name="是否设为默认公式")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
class Meta:
verbose_name = "评分公式"
verbose_name_plural = "评分公式配置"
ordering = ['-is_default', '-created_at']
def __str__(self):
return f"{self.competition.title} - {self.name}"
def get_formula_preview(self):
"""
获取公式预览,将维度变量替换为维度名称
"""
if not self.formula:
return ""
dimension_map = {f'd["{d.name}"]': f'[{d.name}]' for d in self.competition.score_dimensions.all()}
dimension_map.update({f"d['{d.name}']": f'[{d.name}]' for d in self.competition.score_dimensions.all()})
result = self.formula
for old, new in dimension_map.items():
result = result.replace(old, new)
return result
def generate_python_code(self):
"""
生成可执行的 Python 代码
"""
if not self.formula:
return ""
dimension_names = [d.name for d in self.competition.score_dimensions.all()]
code_lines = [
"def calculate_score(d):",
" '''",
f" 计算公式: {self.name}",
" 参数 d: 字典,键为维度名称,值为该维度的平均分",
" '''",
]
for name in dimension_names:
code_lines.append(f" {name} = d.get('{name}', 0)")
code_lines.append("")
code_lines.append(f" return {self.formula}")
return "\n".join(code_lines)

View File

@@ -2,7 +2,7 @@ from django.urls import path, include
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from .views import ( from .views import (
CompetitionViewSet, ProjectViewSet, ProjectFileViewSet, CompetitionViewSet, ProjectViewSet, ProjectFileViewSet,
ScoreViewSet, CommentViewSet ScoreViewSet, CommentViewSet, CompetitionDimensionsAPIView
) )
from . import judge_views from . import judge_views
@@ -18,6 +18,9 @@ urlpatterns = [
path('admin/', judge_views.admin_entry, name='judge_admin_entry'), path('admin/', judge_views.admin_entry, name='judge_admin_entry'),
path('judge/', include('competition.judge_urls')), # Sub-routes under /judge/ path('judge/', include('competition.judge_urls')), # Sub-routes under /judge/
# API Routes
path('competition/<int:competition_id>/dimensions/', CompetitionDimensionsAPIView.as_view(), name='competition-dimensions'),
# Existing API Routes # Existing API Routes
path('', include(router.urls)), path('', include(router.urls)),
] ]

View File

@@ -1,6 +1,7 @@
from rest_framework import viewsets, permissions, status, filters, serializers from rest_framework import viewsets, permissions, status, filters, serializers
from rest_framework.decorators import action from rest_framework.decorators import action, api_view, permission_classes
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView
from django.db.models import Q from django.db.models import Q
from shop.utils import get_current_wechat_user from shop.utils import get_current_wechat_user
from .models import Competition, CompetitionEnrollment, Project, ProjectFile, Score, Comment, ScoreDimension from .models import Competition, CompetitionEnrollment, Project, ProjectFile, Score, Comment, ScoreDimension
@@ -280,3 +281,31 @@ class CommentViewSet(viewsets.ModelViewSet):
raise serializers.ValidationError("您不是该比赛的评委") raise serializers.ValidationError("您不是该比赛的评委")
serializer.save(judge=enrollment) serializer.save(judge=enrollment)
class CompetitionDimensionsAPIView(APIView):
"""
获取比赛评分维度的API
"""
permission_classes = [permissions.AllowAny]
def get(self, request, competition_id):
try:
competition = Competition.objects.get(id=competition_id)
dimensions = ScoreDimension.objects.filter(competition=competition).order_by('order')
data = {
'dimensions': [
{
'id': d.id,
'name': d.name,
'weight': float(d.weight),
'max_score': d.max_score,
'description': d.description
}
for d in dimensions
]
}
return Response(data)
except Competition.DoesNotExist:
return Response({'error': '比赛不存在'}, status=404)