From 65c5e6572e48bddb5d34c2fc27906ae53c8c9923 Mon Sep 17 00:00:00 2001 From: XingQue Date: Fri, 26 Jun 2026 05:48:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=89=93=E6=89=8B=E6=8E=A5=E5=8D=95?= =?UTF-8?q?=E8=80=83=E8=AF=95=EF=BC=88=E6=8C=89=E4=BF=B1=E4=B9=90=E9=83=A8?= =?UTF-8?q?=E9=A2=98=E5=BA=93=E3=80=81=E9=85=8D=E7=BD=AE=E4=B8=8E=E9=80=9A?= =?UTF-8?q?=E8=BF=87=E7=8A=B6=E6=80=81=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jituan/management/commands/seed_kefu_menu.py | 2 + jituan/migrations/0008_dashou_exam.py | 83 +++++ jituan/models.py | 67 ++++ jituan/services/club_bootstrap.py | 2 + jituan/services/dashou_exam.py | 366 +++++++++++++++++++ jituan/urls.py | 16 + jituan/views_dashou_exam.py | 159 ++++++++ 7 files changed, 695 insertions(+) create mode 100644 jituan/migrations/0008_dashou_exam.py create mode 100644 jituan/services/dashou_exam.py create mode 100644 jituan/views_dashou_exam.py diff --git a/jituan/management/commands/seed_kefu_menu.py b/jituan/management/commands/seed_kefu_menu.py index cb0bf74..eeee4b2 100644 --- a/jituan/management/commands/seed_kefu_menu.py +++ b/jituan/management/commands/seed_kefu_menu.py @@ -102,6 +102,8 @@ MENU_ROWS = [ 'perm_codes': ['8080a']}, {'page_id': 'miniapp.kaohe-tags', 'name': '抢单考核标签', 'path': '/miniapp/kaohe-tags', 'parent_id': 'miniapp', 'sort_order': 116, 'perm_codes': ['8080a']}, + {'page_id': 'miniapp.dashou-exam', 'name': '打手接单考试', 'path': '/miniapp/dashou-exam', 'parent_id': 'miniapp', 'sort_order': 117, + 'perm_codes': ['8080a']}, # 店铺 {'page_id': 'shop', 'name': '店铺管理', 'path': '', 'parent_id': '', 'sort_order': 120}, {'page_id': 'shop.list', 'name': '店铺列表', 'path': '/shop/list', 'parent_id': 'shop', 'sort_order': 121, diff --git a/jituan/migrations/0008_dashou_exam.py b/jituan/migrations/0008_dashou_exam.py new file mode 100644 index 0000000..501d79d --- /dev/null +++ b/jituan/migrations/0008_dashou_exam.py @@ -0,0 +1,83 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('jituan', '0007_club_catalog_config'), + ] + + operations = [ + migrations.CreateModel( + name='ClubDashouExamConfig', + fields=[ + ('club_id', models.CharField(max_length=16, primary_key=True, serialize=False)), + ('is_enabled', models.BooleanField(default=False, verbose_name='是否开启考试')), + ('draw_count', models.PositiveIntegerField(default=10, verbose_name='每次抽题数量')), + ('pass_count', models.PositiveIntegerField(default=10, verbose_name='至少答对题数')), + ('require_member', models.BooleanField(default=True, verbose_name='须开通会员方可考试')), + ('CreateTime', models.DateTimeField(auto_now_add=True)), + ('UpdateTime', models.DateTimeField(auto_now=True)), + ], + options={'db_table': 'club_dashou_exam_config'}, + ), + migrations.CreateModel( + name='ClubDashouExamQuestion', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('club_id', models.CharField(db_index=True, max_length=16)), + ('stem', models.TextField(verbose_name='题干文字')), + ('question_type', models.IntegerField(default=1, verbose_name='1单选2多选')), + ('option_1', models.CharField(blank=True, default='', max_length=500)), + ('option_2', models.CharField(blank=True, default='', max_length=500)), + ('option_3', models.CharField(blank=True, default='', max_length=500)), + ('option_4', models.CharField(blank=True, default='', max_length=500)), + ('option_5', models.CharField(blank=True, default='', max_length=500)), + ('option_6', models.CharField(blank=True, default='', max_length=500)), + ('correct_1', models.BooleanField(default=False)), + ('correct_2', models.BooleanField(default=False)), + ('correct_3', models.BooleanField(default=False)), + ('correct_4', models.BooleanField(default=False)), + ('correct_5', models.BooleanField(default=False)), + ('correct_6', models.BooleanField(default=False)), + ('explanation', models.TextField(blank=True, default='', verbose_name='错题解析')), + ('is_enabled', models.BooleanField(default=True, verbose_name='是否上架')), + ('sort_order', models.IntegerField(default=0)), + ('CreateTime', models.DateTimeField(auto_now_add=True)), + ('UpdateTime', models.DateTimeField(auto_now=True)), + ], + options={ + 'db_table': 'club_dashou_exam_question', + 'indexes': [models.Index(fields=['club_id', 'is_enabled'], name='club_dashou_exam_q_club_en_idx')], + }, + ), + migrations.CreateModel( + name='ClubDashouExamQuestionImage', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('club_id', models.CharField(db_index=True, max_length=16)), + ('question_id', models.IntegerField(db_index=True)), + ('image_url', models.CharField(max_length=500)), + ('sort_order', models.IntegerField(default=0)), + ('CreateTime', models.DateTimeField(auto_now_add=True)), + ], + options={ + 'db_table': 'club_dashou_exam_question_image', + 'indexes': [models.Index(fields=['club_id', 'question_id'], name='club_dashou_exam_img_q_idx')], + }, + ), + migrations.CreateModel( + name='ClubDashouExamPass', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('club_id', models.CharField(db_index=True, max_length=16)), + ('yonghuid', models.CharField(db_index=True, max_length=7)), + ('passed_at', models.DateTimeField(auto_now_add=True)), + ('CreateTime', models.DateTimeField(auto_now_add=True)), + ], + options={ + 'db_table': 'club_dashou_exam_pass', + 'unique_together': {('club_id', 'yonghuid')}, + }, + ), + ] diff --git a/jituan/models.py b/jituan/models.py index 67f998d..637b654 100644 --- a/jituan/models.py +++ b/jituan/models.py @@ -230,6 +230,73 @@ class IdentityTagBind(QModel): ] +class ClubDashouExamConfig(QModel): + """各俱乐部打手抢单前考试配置(互不影响)。""" + club_id = models.CharField(max_length=16, primary_key=True) + is_enabled = models.BooleanField(default=False, verbose_name='是否开启考试') + draw_count = models.PositiveIntegerField(default=10, verbose_name='每次抽题数量') + pass_count = models.PositiveIntegerField(default=10, verbose_name='至少答对题数') + require_member = models.BooleanField(default=True, verbose_name='须开通会员方可考试') + CreateTime = models.DateTimeField(auto_now_add=True) + UpdateTime = models.DateTimeField(auto_now=True) + + class Meta: + db_table = 'club_dashou_exam_config' + + +class ClubDashouExamQuestion(QModel): + """俱乐部考试题库(题干+最多6选项)。""" + club_id = models.CharField(max_length=16, db_index=True) + stem = models.TextField(verbose_name='题干文字') + question_type = models.IntegerField(default=1, verbose_name='1单选2多选') + option_1 = models.CharField(max_length=500, blank=True, default='') + option_2 = models.CharField(max_length=500, blank=True, default='') + option_3 = models.CharField(max_length=500, blank=True, default='') + option_4 = models.CharField(max_length=500, blank=True, default='') + option_5 = models.CharField(max_length=500, blank=True, default='') + option_6 = models.CharField(max_length=500, blank=True, default='') + correct_1 = models.BooleanField(default=False) + correct_2 = models.BooleanField(default=False) + correct_3 = models.BooleanField(default=False) + correct_4 = models.BooleanField(default=False) + correct_5 = models.BooleanField(default=False) + correct_6 = models.BooleanField(default=False) + explanation = models.TextField(blank=True, default='', verbose_name='错题解析') + is_enabled = models.BooleanField(default=True, verbose_name='是否上架') + sort_order = models.IntegerField(default=0) + CreateTime = models.DateTimeField(auto_now_add=True) + UpdateTime = models.DateTimeField(auto_now=True) + + class Meta: + db_table = 'club_dashou_exam_question' + indexes = [models.Index(fields=['club_id', 'is_enabled'])] + + +class ClubDashouExamQuestionImage(QModel): + """题目配图(题干下方,可多张)。""" + club_id = models.CharField(max_length=16, db_index=True) + question_id = models.IntegerField(db_index=True) + image_url = models.CharField(max_length=500) + sort_order = models.IntegerField(default=0) + CreateTime = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = 'club_dashou_exam_question_image' + indexes = [models.Index(fields=['club_id', 'question_id'])] + + +class ClubDashouExamPass(QModel): + """打手在本俱乐部是否已通过考试。""" + club_id = models.CharField(max_length=16, db_index=True) + yonghuid = models.CharField(max_length=7, db_index=True) + passed_at = models.DateTimeField(auto_now_add=True) + CreateTime = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = 'club_dashou_exam_pass' + unique_together = [['club_id', 'yonghuid']] + + class KefuMenuPage(QModel): """客服后台左侧菜单页(固定 page_id,与前端路由对应)。""" page_id = models.CharField(max_length=64, primary_key=True, verbose_name='页面ID') diff --git a/jituan/services/club_bootstrap.py b/jituan/services/club_bootstrap.py index 681ac41..982a057 100644 --- a/jituan/services/club_bootstrap.py +++ b/jituan/services/club_bootstrap.py @@ -104,5 +104,7 @@ def bootstrap_club_from_template( from jituan.services.club_kaohe_chenghao import copy_club_kaohe_chenghao_config copy_club_shangpin_leixing_config(template_id, new_id) copy_club_kaohe_chenghao_config(template_id, new_id) + from jituan.services.dashou_exam import copy_exam_config + copy_exam_config(template_id, new_id) return club diff --git a/jituan/services/dashou_exam.py b/jituan/services/dashou_exam.py new file mode 100644 index 0000000..2868657 --- /dev/null +++ b/jituan/services/dashou_exam.py @@ -0,0 +1,366 @@ +"""打手抢单前考试:按俱乐部独立题库与配置。""" +import logging +import os +import random +import uuid + +from django.utils import timezone + +from jituan.constants import DATA_SCOPE_ALL +from jituan.models import ( + ClubDashouExamConfig, + ClubDashouExamPass, + ClubDashouExamQuestion, + ClubDashouExamQuestionImage, +) +from jituan.services.club_context import ( + club_id_for_write, + resolve_club_id_from_request, + resolve_club_scope, + resolve_effective_club_id, +) +from products.models import Huiyuangoumai +from utils.oss_utils import delete_from_oss, upload_to_oss, validate_image + +logger = logging.getLogger(__name__) + +SCOPE_MSG = '请切换到具体小程序/俱乐部后再操作' +OPTION_FIELDS = [f'option_{i}' for i in range(1, 7)] +CORRECT_FIELDS = [f'correct_{i}' for i in range(1, 7)] + + +def _scope_blocked(request): + return resolve_club_scope(request) == DATA_SCOPE_ALL + + +def get_or_create_config(club_id): + row, _ = ClubDashouExamConfig.query.get_or_create( + club_id=club_id, + defaults={ + 'is_enabled': False, + 'draw_count': 10, + 'pass_count': 10, + 'require_member': True, + }, + ) + return row + + +def config_to_dict(row): + return { + 'club_id': row.club_id, + 'is_enabled': bool(row.is_enabled), + 'draw_count': int(row.draw_count or 10), + 'pass_count': int(row.pass_count or 10), + 'require_member': bool(row.require_member), + 'max_wrong': max(0, int(row.draw_count or 0) - int(row.pass_count or 0)), + } + + +def user_has_active_member(yonghuid, club_id): + for r in Huiyuangoumai.query.filter(yonghu_id=yonghuid, club_id=club_id): + if r.huiyuan_zhuangtai == 1 and not r.jiance_shifou_daoqi(): + return True + return False + + +def user_exam_passed(club_id, yonghuid): + return ClubDashouExamPass.query.filter(club_id=club_id, yonghuid=yonghuid).exists() + + +def validate_question_payload(data, for_update=False): + stem = (data.get('stem') or '').strip() + if not stem: + return '题干不能为空' + qtype = int(data.get('question_type') or 1) + if qtype not in (1, 2): + return '题目类型无效' + options = [] + correct_slots = [] + for i in range(1, 7): + text = (data.get(f'option_{i}') or '').strip() + is_corr = bool(data.get(f'correct_{i}')) + if text: + options.append(i) + if is_corr: + correct_slots.append(i) + if len(options) < 2: + return '至少需要两个有效选项' + if not correct_slots: + return '至少标记一个正确答案' + if qtype == 1 and len(correct_slots) != 1: + return '单选题只能有一个正确答案' + return None + + +def question_to_admin_dict(q, images=None): + item = { + 'id': q.id, + 'stem': q.stem, + 'question_type': q.question_type, + 'explanation': q.explanation or '', + 'is_enabled': bool(q.is_enabled), + 'sort_order': q.sort_order, + 'images': images or [], + } + for i in range(1, 7): + item[f'option_{i}'] = getattr(q, f'option_{i}', '') or '' + item[f'correct_{i}'] = bool(getattr(q, f'correct_{i}', False)) + return item + + +def build_question_bundle_item(q, image_urls): + options = [] + correct_slots = [] + for i in range(1, 7): + text = (getattr(q, f'option_{i}', '') or '').strip() + if not text: + continue + options.append({'slot': i, 'text': text}) + if getattr(q, f'correct_{i}', False): + correct_slots.append(i) + return { + 'id': q.id, + 'stem': q.stem, + 'question_type': q.question_type, + 'images': image_urls, + 'options': options, + 'correct_slots': correct_slots, + 'explanation': q.explanation or '', + } + + +def get_exam_status(request, user): + club_id = resolve_effective_club_id(request, user) + cfg = get_or_create_config(club_id) + yonghuid = user.UserUID + has_member = user_has_active_member(yonghuid, club_id) + passed = user_exam_passed(club_id, yonghuid) + enabled = bool(cfg.is_enabled) + pool_count = ClubDashouExamQuestion.query.filter(club_id=club_id, is_enabled=True).count() + return { + 'club_id': club_id, + 'exam_enabled': enabled, + 'exam_passed': passed, + 'has_member': has_member, + 'can_take_exam': has_member if cfg.require_member else True, + 'exam_required': enabled and not passed, + 'pool_count': pool_count, + 'config': config_to_dict(cfg), + } + + +def build_exam_bundle(request, user): + club_id = resolve_effective_club_id(request, user) + cfg = get_or_create_config(club_id) + yonghuid = user.UserUID + has_member = user_has_active_member(yonghuid, club_id) + if cfg.require_member and not has_member: + return None, '须先开通会员才能参加考试' + + qs = ClubDashouExamQuestion.query.filter(club_id=club_id, is_enabled=True) + all_ids = list(qs.values_list('id', flat=True)) + draw = min(int(cfg.draw_count or 10), len(all_ids)) + if draw < 1: + return None, '题库暂无可用题目,请联系管理员' + + selected_ids = random.sample(all_ids, draw) + questions = [] + for qid in selected_ids: + q = ClubDashouExamQuestion.query.get(id=qid) + imgs = [ + r.image_url for r in ClubDashouExamQuestionImage.query.filter( + club_id=club_id, question_id=qid, + ).order_by('sort_order', 'id') + ] + questions.append(build_question_bundle_item(q, imgs)) + + return { + 'club_id': club_id, + 'config': config_to_dict(cfg), + 'has_member': has_member, + 'already_passed': user_exam_passed(club_id, yonghuid), + 'questions': questions, + }, None + + +def mark_exam_passed(request, user): + club_id = resolve_effective_club_id(request, user) + cfg = get_or_create_config(club_id) + if not cfg.is_enabled: + return None, '本俱乐部未开启考试' + yonghuid = user.UserUID + if cfg.require_member and not user_has_active_member(yonghuid, club_id): + return None, '须先开通会员' + ClubDashouExamPass.query.update_or_create( + club_id=club_id, + yonghuid=yonghuid, + defaults={'passed_at': timezone.now()}, + ) + return {'club_id': club_id, 'passed': True}, None + + +def build_admin_config_payload(request): + if _scope_blocked(request): + return {'scope_error': SCOPE_MSG} + club_id = resolve_club_id_from_request(request) + cfg = get_or_create_config(club_id) + pool = ClubDashouExamQuestion.query.filter(club_id=club_id).count() + enabled_pool = ClubDashouExamQuestion.query.filter(club_id=club_id, is_enabled=True).count() + return { + 'club_id': club_id, + 'config': config_to_dict(cfg), + 'pool_count': pool, + 'enabled_pool_count': enabled_pool, + } + + +def save_admin_config(request, data): + if _scope_blocked(request): + return None, SCOPE_MSG + club_id = club_id_for_write(request) + draw = int(data.get('draw_count') or 10) + pass_cnt = int(data.get('pass_count') or draw) + if draw < 1: + return None, '抽题数量至少为1' + if pass_cnt < 1 or pass_cnt > draw: + return None, '及格题数须在 1 到抽题数量之间' + cfg = get_or_create_config(club_id) + cfg.is_enabled = bool(data.get('is_enabled')) + cfg.draw_count = draw + cfg.pass_count = pass_cnt + cfg.require_member = bool(data.get('require_member', True)) + cfg.save(update_fields=['is_enabled', 'draw_count', 'pass_count', 'require_member', 'UpdateTime']) + return config_to_dict(cfg), None + + +def list_admin_questions(request): + if _scope_blocked(request): + return {'list': [], 'scope_error': SCOPE_MSG} + club_id = resolve_club_id_from_request(request) + rows = ClubDashouExamQuestion.query.filter(club_id=club_id).order_by('-sort_order', 'id') + out = [] + for q in rows: + imgs = [ + {'id': img.id, 'image_url': img.image_url, 'sort_order': img.sort_order} + for img in ClubDashouExamQuestionImage.query.filter( + club_id=club_id, question_id=q.id, + ).order_by('sort_order', 'id') + ] + out.append(question_to_admin_dict(q, imgs)) + return {'club_id': club_id, 'list': out} + + +def create_question(request, data): + if _scope_blocked(request): + return None, SCOPE_MSG + err = validate_question_payload(data) + if err: + return None, err + club_id = club_id_for_write(request) + q = ClubDashouExamQuestion.query.create( + club_id=club_id, + stem=data.get('stem', '').strip(), + question_type=int(data.get('question_type') or 1), + explanation=(data.get('explanation') or '').strip(), + is_enabled=bool(data.get('is_enabled', True)), + sort_order=int(data.get('sort_order') or 0), + **{f'option_{i}': (data.get(f'option_{i}') or '').strip() for i in range(1, 7)}, + **{f'correct_{i}': bool(data.get(f'correct_{i}')) for i in range(1, 7)}, + ) + return {'id': q.id}, None + + +def update_question(request, question_id, data): + if _scope_blocked(request): + return None, SCOPE_MSG + club_id = club_id_for_write(request) + try: + q = ClubDashouExamQuestion.query.get(id=question_id, club_id=club_id) + except ClubDashouExamQuestion.DoesNotExist: + return None, '题目不存在' + err = validate_question_payload(data, for_update=True) + if err: + return None, err + q.stem = data.get('stem', q.stem).strip() + q.question_type = int(data.get('question_type') or q.question_type) + q.explanation = (data.get('explanation') or '').strip() + if 'is_enabled' in data: + q.is_enabled = bool(data.get('is_enabled')) + if 'sort_order' in data: + q.sort_order = int(data.get('sort_order') or 0) + for i in range(1, 7): + if f'option_{i}' in data: + q.__setattr__(f'option_{i}', (data.get(f'option_{i}') or '').strip()) + if f'correct_{i}' in data: + q.__setattr__(f'correct_{i}', bool(data.get(f'correct_{i}'))) + q.save() + return {'id': q.id}, None + + +def delete_question(request, question_id): + if _scope_blocked(request): + return None, SCOPE_MSG + club_id = club_id_for_write(request) + try: + q = ClubDashouExamQuestion.query.get(id=question_id, club_id=club_id) + except ClubDashouExamQuestion.DoesNotExist: + return None, '题目不存在' + for img in ClubDashouExamQuestionImage.query.filter(club_id=club_id, question_id=q.id): + if img.image_url: + delete_from_oss(img.image_url) + ClubDashouExamQuestionImage.query.filter(club_id=club_id, question_id=q.id).delete() + q.delete() + return {'id': question_id}, None + + +def upload_question_image(request, question_id, file_obj): + if _scope_blocked(request): + return None, SCOPE_MSG + club_id = club_id_for_write(request) + if not ClubDashouExamQuestion.query.filter(id=question_id, club_id=club_id).exists(): + return None, '题目不存在' + ok, msg = validate_image(file_obj) + if not ok: + return None, msg + ext = os.path.splitext(file_obj.name)[1].lower() or '.png' + if ext not in ('.png', '.jpg', '.jpeg', '.webp'): + ext = '.png' + oss_path = f'club/{club_id}/exam/{question_id}_{uuid.uuid4().hex}{ext}' + if not upload_to_oss(file_obj, oss_path): + return None, '图片上传失败' + row = ClubDashouExamQuestionImage.query.create( + club_id=club_id, + question_id=question_id, + image_url=oss_path, + sort_order=int(request.data.get('sort_order') or 0), + ) + return {'id': row.id, 'image_url': oss_path}, None + + +def delete_question_image(request, image_id): + if _scope_blocked(request): + return None, SCOPE_MSG + club_id = club_id_for_write(request) + try: + img = ClubDashouExamQuestionImage.query.get(id=image_id, club_id=club_id) + except ClubDashouExamQuestionImage.DoesNotExist: + return None, '图片不存在' + if img.image_url: + delete_from_oss(img.image_url) + img.delete() + return {'id': image_id}, None + + +def copy_exam_config(template_club_id, new_club_id): + tpl = ClubDashouExamConfig.query.filter(club_id=template_club_id).first() + if tpl: + ClubDashouExamConfig.query.update_or_create( + club_id=new_club_id, + defaults={ + 'is_enabled': tpl.is_enabled, + 'draw_count': tpl.draw_count, + 'pass_count': tpl.pass_count, + 'require_member': tpl.require_member, + }, + ) diff --git a/jituan/urls.py b/jituan/urls.py index bdd3541..62062ad 100644 --- a/jituan/urls.py +++ b/jituan/urls.py @@ -51,6 +51,15 @@ from jituan.views import ( ClubWechatLoginView, ) +from jituan.views_dashou_exam import ( + DashouExamBundleView, + DashouExamConfigAdminView, + DashouExamImageManageView, + DashouExamMarkPassedView, + DashouExamQuestionListView, + DashouExamQuestionManageView, + DashouExamStatusView, +) from jituan.views_club_catalog import ( ClubKaoheChenghaoCatalogView, ClubKaoheChenghaoDisableView, @@ -130,5 +139,12 @@ urlpatterns = [ path('houtai/club-kaohe-catalog', ClubKaoheChenghaoCatalogView.as_view(), name='jituan_club_kaohe_catalog'), path('houtai/club-kaohe-enable', ClubKaoheChenghaoEnableView.as_view(), name='jituan_club_kaohe_enable'), path('houtai/club-kaohe-disable', ClubKaoheChenghaoDisableView.as_view(), name='jituan_club_kaohe_disable'), + path('dashou-exam/status', DashouExamStatusView.as_view(), name='jituan_dashou_exam_status'), + path('dashou-exam/bundle', DashouExamBundleView.as_view(), name='jituan_dashou_exam_bundle'), + path('dashou-exam/mark-passed', DashouExamMarkPassedView.as_view(), name='jituan_dashou_exam_mark_passed'), + path('houtai/dashou-exam-config', DashouExamConfigAdminView.as_view(), name='jituan_dashou_exam_config'), + path('houtai/dashou-exam-question-list', DashouExamQuestionListView.as_view(), name='jituan_dashou_exam_q_list'), + path('houtai/dashou-exam-question-manage', DashouExamQuestionManageView.as_view(), name='jituan_dashou_exam_q_manage'), + path('houtai/dashou-exam-image-manage', DashouExamImageManageView.as_view(), name='jituan_dashou_exam_img'), path('club/list', ClubListView.as_view(), name='jituan_club_list'), ] diff --git a/jituan/views_dashou_exam.py b/jituan/views_dashou_exam.py new file mode 100644 index 0000000..9d566d9 --- /dev/null +++ b/jituan/views_dashou_exam.py @@ -0,0 +1,159 @@ +"""打手考试:后台管理 + 小程序接口。""" +from rest_framework.parsers import FormParser, JSONParser, MultiPartParser +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +from backend.utils import verify_kefu_permission + +EXAM_PERM = '8080a' + + +class DashouExamStatusView(APIView): + """小程序:考试状态(抢单前检查) POST /jituan/dashou-exam/status""" + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + from jituan.services.dashou_exam import get_exam_status + data = get_exam_status(request, request.user) + return Response({'code': 200, 'msg': '成功', 'data': data}) + + +class DashouExamBundleView(APIView): + """小程序:拉取整套考题(含答案与解析,前端本地判分) POST /jituan/dashou-exam/bundle""" + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + from jituan.services.dashou_exam import build_exam_bundle + data, err = build_exam_bundle(request, request.user) + if err: + return Response({'code': 400, 'msg': err}) + return Response({'code': 200, 'msg': '成功', 'data': data}) + + +class DashouExamMarkPassedView(APIView): + """小程序:前端判分通过后登记 POST /jituan/dashou-exam/mark-passed""" + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + from jituan.services.dashou_exam import mark_exam_passed + data, err = mark_exam_passed(request, request.user) + if err: + return Response({'code': 400, 'msg': err}) + return Response({'code': 200, 'msg': '考试通过', 'data': data}) + + +class DashouExamConfigAdminView(APIView): + """后台:考试配置 GET/SAVE POST /jituan/houtai/dashou-exam-config""" + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + phone = (request.data.get('phone') or request.data.get('username') or '').strip() + if not phone: + return Response({'code': 401, 'msg': '缺少客服账号'}) + kefu, permissions = verify_kefu_permission(request, phone) + if kefu is None: + return permissions + if EXAM_PERM not in permissions: + return Response({'code': 403, 'msg': '无权限'}) + action = (request.data.get('action') or 'get').strip() + from jituan.services.dashou_exam import build_admin_config_payload, save_admin_config + if action == 'save': + data, err = save_admin_config(request, request.data) + if err: + return Response({'code': 400, 'msg': err}) + return Response({'code': 0, 'msg': '保存成功', 'data': {'config': data}}) + payload = build_admin_config_payload(request) + return Response({'code': 0, 'data': payload}) + + +class DashouExamQuestionListView(APIView): + """后台:题库列表 POST /jituan/houtai/dashou-exam-question-list""" + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + phone = (request.data.get('phone') or request.data.get('username') or '').strip() + if not phone: + return Response({'code': 401, 'msg': '缺少客服账号'}) + kefu, permissions = verify_kefu_permission(request, phone) + if kefu is None: + return permissions + if EXAM_PERM not in permissions: + return Response({'code': 403, 'msg': '无权限'}) + from jituan.services.dashou_exam import list_admin_questions + return Response({'code': 0, 'data': list_admin_questions(request)}) + + +class DashouExamQuestionManageView(APIView): + """后台:题目增删改 POST /jituan/houtai/dashou-exam-question-manage""" + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + phone = (request.data.get('phone') or request.data.get('username') or '').strip() + if not phone: + return Response({'code': 401, 'msg': '缺少客服账号'}) + kefu, permissions = verify_kefu_permission(request, phone) + if kefu is None: + return permissions + if EXAM_PERM not in permissions: + return Response({'code': 403, 'msg': '无权限'}) + action = (request.data.get('action') or '').strip() + qid = request.data.get('question_id') or request.data.get('id') + from jituan.services.dashou_exam import ( + create_question, + delete_question, + update_question, + ) + if action == 'create': + data, err = create_question(request, request.data) + elif action == 'update': + if not qid: + return Response({'code': 400, 'msg': '缺少题目ID'}) + data, err = update_question(request, int(qid), request.data) + elif action == 'delete': + if not qid: + return Response({'code': 400, 'msg': '缺少题目ID'}) + data, err = delete_question(request, int(qid)) + else: + return Response({'code': 400, 'msg': '无效 action'}) + if err: + return Response({'code': 400, 'msg': err}) + return Response({'code': 0, 'msg': '成功', 'data': data}) + + +class DashouExamImageManageView(APIView): + """后台:题目图片 POST /jituan/houtai/dashou-exam-image-manage""" + permission_classes = [IsAuthenticated] + parser_classes = [MultiPartParser, FormParser, JSONParser] + + def post(self, request): + phone = (request.data.get('phone') or request.data.get('username') or '').strip() + if not phone: + return Response({'code': 401, 'msg': '缺少客服账号'}) + kefu, permissions = verify_kefu_permission(request, phone) + if kefu is None: + return permissions + if EXAM_PERM not in permissions: + return Response({'code': 403, 'msg': '无权限'}) + action = (request.data.get('action') or 'upload').strip() + from jituan.services.dashou_exam import delete_question_image, upload_question_image + if action == 'delete': + image_id = request.data.get('image_id') + if not image_id: + return Response({'code': 400, 'msg': '缺少 image_id'}) + data, err = delete_question_image(request, int(image_id)) + else: + qid = request.data.get('question_id') + file_obj = request.FILES.get('file') + if not qid or not file_obj: + return Response({'code': 400, 'msg': '缺少题目或图片'}) + data, err = upload_question_image(request, int(qid), file_obj) + if err: + return Response({'code': 400, 'msg': err}) + return Response({'code': 0, 'msg': '成功', 'data': data})