diff --git a/backend/community/admin.py b/backend/community/admin.py index 2d8df09..c709641 100644 --- a/backend/community/admin.py +++ b/backend/community/admin.py @@ -120,14 +120,15 @@ class OrderableAdminMixin: @admin.register(Activity) class ActivityAdmin(ModelAdmin): - list_display = ('title', 'banner_display', 'start_time', 'location', 'signup_count', 'is_visible', 'is_active', 'auto_confirm', 'created_at') + list_display = ('title', 'author', 'banner_display', 'start_time', 'location', 'signup_count', 'is_visible', 'is_active', 'auto_confirm', 'created_at') list_filter = ('is_visible', 'is_active', 'auto_confirm', 'start_time') search_fields = ('title', 'location') + autocomplete_fields = ['author'] inlines = [ActivitySignupInline] fieldsets = ( ('基本信息', { - 'fields': ('title', 'description', 'banner', 'banner_url', 'is_visible', 'is_active', 'auto_confirm') + 'fields': ('title', 'author', 'description', 'banner', 'banner_url', 'is_visible', 'is_active', 'auto_confirm') }), ('费用与时间', { 'fields': ('is_paid', 'price', 'start_time', 'end_time', 'location'), diff --git a/backend/community/migrations/0001_initial.py b/backend/community/migrations/0001_initial.py index 74d848e..86d3205 100644 --- a/backend/community/migrations/0001_initial.py +++ b/backend/community/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.1 on 2026-02-11 06:43 +# Generated by Django 6.0.1 on 2026-03-04 04:57 import django.db.models.deletion from django.db import migrations, models @@ -9,22 +9,55 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('shop', '0025_vccourse_alter_courseenrollment_course_and_more'), + ('shop', '__first__'), ] operations = [ + migrations.CreateModel( + name='Announcement', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=100, verbose_name='公告标题')), + ('content', models.TextField(verbose_name='公告内容')), + ('image', models.ImageField(blank=True, null=True, upload_to='announcements/', verbose_name='公告图片')), + ('image_url', models.URLField(blank=True, help_text='优先使用上传的图片', null=True, verbose_name='图片链接')), + ('link_url', models.URLField(blank=True, null=True, verbose_name='跳转链接')), + ('is_active', models.BooleanField(default=True, verbose_name='是否启用')), + ('is_pinned', models.BooleanField(default=False, verbose_name='是否置顶')), + ('priority', models.IntegerField(default=0, help_text='数字越大越靠前', verbose_name='排序权重')), + ('start_time', models.DateTimeField(blank=True, null=True, verbose_name='开始展示时间')), + ('end_time', models.DateTimeField(blank=True, null=True, 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': '社区公告管理', + 'ordering': ['-is_pinned', '-priority', '-created_at'], + }, + ), migrations.CreateModel( name='Activity', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('title', models.CharField(max_length=100, verbose_name='活动标题')), ('description', models.TextField(verbose_name='活动详情')), - ('banner', models.ImageField(upload_to='activities/banners/', verbose_name='活动Banner图')), + ('banner', models.ImageField(blank=True, null=True, upload_to='activities/banners/', verbose_name='活动Banner图')), + ('banner_url', models.URLField(blank=True, help_text='可直接填写图片链接,若同时上传图片,将优先显示上传的图片', null=True, verbose_name='活动Banner链接')), ('start_time', models.DateTimeField(verbose_name='开始时间')), ('end_time', models.DateTimeField(verbose_name='结束时间')), ('location', models.CharField(max_length=100, verbose_name='活动地点')), ('max_participants', models.IntegerField(default=50, verbose_name='最大报名人数')), + ('is_paid', models.BooleanField(default=False, verbose_name='是否收费')), + ('price', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='报名费用')), ('is_active', models.BooleanField(default=True, verbose_name='是否启用')), + ('is_visible', models.BooleanField(default=True, help_text='关闭后将不在前端列表页显示', verbose_name='是否显示')), + ('auto_confirm', models.BooleanField(default=False, help_text='开启后,付费活动支付成功或免费活动报名后直接显示报名成功,不需要审核', verbose_name='无需审核')), + ('ask_name', models.BooleanField(default=False, verbose_name='收集姓名')), + ('ask_phone', models.BooleanField(default=False, verbose_name='收集手机号')), + ('ask_wechat', models.BooleanField(default=False, verbose_name='收集微信号')), + ('ask_company', models.BooleanField(default=False, verbose_name='收集公司/机构')), + ('signup_form_config', models.JSONField(blank=True, default=list, help_text='JSON格式的高级配置,若填写则优先于上方开关。例如:[{"name": "job", "label": "职位", "type": "text", "required": true}]', verbose_name='自定义报名配置')), ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), ], options={ @@ -37,34 +70,58 @@ class Migration(migrations.Migration): fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('title', models.CharField(max_length=200, verbose_name='标题')), - ('content', models.TextField(verbose_name='内容')), + ('category', models.CharField(choices=[('discussion', '技术讨论'), ('help', '求助问答'), ('share', '经验分享'), ('notice', '官方公告')], default='discussion', max_length=20, verbose_name='分类')), + ('status', models.CharField(choices=[('pending', '待审核'), ('published', '已发布'), ('rejected', '已拒绝')], default='published', max_length=20, verbose_name='状态')), + ('content', models.TextField(help_text='支持Markdown格式,支持插入图片', verbose_name='内容')), ('view_count', models.IntegerField(default=0, verbose_name='浏览量')), ('is_pinned', 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='更新时间')), + ('order', models.IntegerField(default=0, verbose_name='排序')), ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='topics', to='shop.wechatuser', verbose_name='作者')), - ('related_product', models.ForeignKey(blank=True, help_text='如果是技术求助,请选择关联的硬件', null=True, on_delete=django.db.models.deletion.SET_NULL, to='shop.esp32config', verbose_name='关联硬件')), + ('likes', models.ManyToManyField(blank=True, related_name='liked_topics', to='shop.wechatuser', verbose_name='点赞用户')), + ('related_course', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='shop.vccourse', verbose_name='关联课程')), + ('related_product', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='shop.esp32config', verbose_name='关联硬件')), + ('related_service', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='shop.service', verbose_name='关联服务')), ], options={ 'verbose_name': '论坛帖子', 'verbose_name_plural': '论坛帖子管理', - 'ordering': ['-is_pinned', '-created_at'], + 'ordering': ['order', '-is_pinned', '-created_at'], }, ), migrations.CreateModel( name='Reply', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('content', models.TextField(verbose_name='回复内容')), + ('content', models.TextField(help_text='支持Markdown格式', verbose_name='回复内容')), + ('is_pinned', models.BooleanField(default=False, verbose_name='置顶')), ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='回复时间')), ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='replies', to='shop.wechatuser', verbose_name='回复者')), + ('likes', models.ManyToManyField(blank=True, related_name='liked_replies', to='shop.wechatuser', verbose_name='点赞用户')), ('reply_to', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='community.reply', verbose_name='回复楼层')), ('topic', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='replies', to='community.topic', verbose_name='所属帖子')), ], options={ 'verbose_name': '帖子回复', 'verbose_name_plural': '帖子回复管理', - 'ordering': ['created_at'], + 'ordering': ['-is_pinned', '-created_at'], + }, + ), + migrations.CreateModel( + name='TopicMedia', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('file', models.FileField(blank=True, null=True, upload_to='community/media/', verbose_name='文件')), + ('file_url', models.URLField(blank=True, max_length=500, null=True, verbose_name='文件链接')), + ('media_type', models.CharField(choices=[('image', '图片'), ('video', '视频'), ('file', '文件')], default='image', max_length=10, verbose_name='媒体类型')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='上传时间')), + ('reply', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='media', to='community.reply', verbose_name='所属回复')), + ('topic', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='media', to='community.topic', verbose_name='所属帖子')), + ], + options={ + 'verbose_name': '论坛媒体资源', + 'verbose_name_plural': '论坛媒体资源管理', }, ), migrations.CreateModel( @@ -72,8 +129,10 @@ class Migration(migrations.Migration): fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('signup_time', models.DateTimeField(auto_now_add=True, verbose_name='报名时间')), - ('status', models.CharField(choices=[('pending', '审核中'), ('confirmed', '报名成功'), ('cancelled', '已取消')], default='confirmed', max_length=20, verbose_name='状态')), + ('signup_info', models.JSONField(blank=True, default=dict, verbose_name='报名信息')), + ('status', models.CharField(choices=[('unpaid', '待支付'), ('pending', '审核中'), ('confirmed', '报名成功'), ('cancelled', '已取消')], default='confirmed', max_length=20, verbose_name='状态')), ('activity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='signups', to='community.activity', verbose_name='活动')), + ('order', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='activity_signups', to='shop.order', verbose_name='关联订单')), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='activity_signups', to='shop.wechatuser', verbose_name='报名用户')), ], options={ diff --git a/backend/community/migrations/0002_activity_author.py b/backend/community/migrations/0002_activity_author.py new file mode 100644 index 0000000..4990fdb --- /dev/null +++ b/backend/community/migrations/0002_activity_author.py @@ -0,0 +1,20 @@ +# Generated by Django 6.0.1 on 2026-03-04 04:58 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('community', '0001_initial'), + ('shop', '__first__'), + ] + + operations = [ + migrations.AddField( + model_name='activity', + name='author', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='activities', to='shop.wechatuser', verbose_name='发布者'), + ), + ] diff --git a/backend/community/migrations/0002_topic_related_course_topic_related_service_and_more.py b/backend/community/migrations/0002_topic_related_course_topic_related_service_and_more.py deleted file mode 100644 index a8265a7..0000000 --- a/backend/community/migrations/0002_topic_related_course_topic_related_service_and_more.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 6.0.1 on 2026-02-11 06:48 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('community', '0001_initial'), - ('shop', '0025_vccourse_alter_courseenrollment_course_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='topic', - name='related_course', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='shop.vccourse', verbose_name='关联课程'), - ), - migrations.AddField( - model_name='topic', - name='related_service', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='shop.service', verbose_name='关联服务'), - ), - migrations.AlterField( - model_name='topic', - name='related_product', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='shop.esp32config', verbose_name='关联硬件'), - ), - ] diff --git a/backend/community/migrations/0003_alter_reply_content_alter_topic_content_topicmedia.py b/backend/community/migrations/0003_alter_reply_content_alter_topic_content_topicmedia.py deleted file mode 100644 index 99ef0d3..0000000 --- a/backend/community/migrations/0003_alter_reply_content_alter_topic_content_topicmedia.py +++ /dev/null @@ -1,39 +0,0 @@ -# Generated by Django 6.0.1 on 2026-02-11 06:57 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('community', '0002_topic_related_course_topic_related_service_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='reply', - name='content', - field=models.TextField(help_text='支持Markdown格式', verbose_name='回复内容'), - ), - migrations.AlterField( - model_name='topic', - name='content', - field=models.TextField(help_text='支持Markdown格式,支持插入图片', verbose_name='内容'), - ), - migrations.CreateModel( - name='TopicMedia', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('file', models.FileField(upload_to='community/media/', verbose_name='文件')), - ('media_type', models.CharField(choices=[('image', '图片'), ('video', '视频'), ('file', '文件')], default='image', max_length=10, verbose_name='媒体类型')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='上传时间')), - ('reply', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='media', to='community.reply', verbose_name='所属回复')), - ('topic', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='media', to='community.topic', verbose_name='所属帖子')), - ], - options={ - 'verbose_name': '论坛媒体资源', - 'verbose_name_plural': '论坛媒体资源管理', - }, - ), - ] diff --git a/backend/community/migrations/0004_activity_banner_url_alter_activity_banner.py b/backend/community/migrations/0004_activity_banner_url_alter_activity_banner.py deleted file mode 100644 index 0871a9d..0000000 --- a/backend/community/migrations/0004_activity_banner_url_alter_activity_banner.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 6.0.1 on 2026-02-11 07:00 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('community', '0003_alter_reply_content_alter_topic_content_topicmedia'), - ] - - operations = [ - migrations.AddField( - model_name='activity', - name='banner_url', - field=models.URLField(blank=True, help_text='可直接填写图片链接,若同时上传图片,将优先显示上传的图片', null=True, verbose_name='活动Banner链接'), - ), - migrations.AlterField( - model_name='activity', - name='banner', - field=models.ImageField(blank=True, null=True, upload_to='activities/banners/', verbose_name='活动Banner图'), - ), - ] diff --git a/backend/community/migrations/0005_topic_category.py b/backend/community/migrations/0005_topic_category.py deleted file mode 100644 index f101bac..0000000 --- a/backend/community/migrations/0005_topic_category.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 6.0.1 on 2026-02-12 06:19 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('community', '0004_activity_banner_url_alter_activity_banner'), - ] - - operations = [ - migrations.AddField( - model_name='topic', - name='category', - field=models.CharField(choices=[('discussion', '技术讨论'), ('help', '求助问答'), ('share', '经验分享'), ('notice', '官方公告')], default='discussion', max_length=20, verbose_name='分类'), - ), - ] diff --git a/backend/community/migrations/0006_topicmedia_file_url_alter_topicmedia_file.py b/backend/community/migrations/0006_topicmedia_file_url_alter_topicmedia_file.py deleted file mode 100644 index 606c557..0000000 --- a/backend/community/migrations/0006_topicmedia_file_url_alter_topicmedia_file.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 6.0.1 on 2026-02-12 06:52 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('community', '0005_topic_category'), - ] - - operations = [ - migrations.AddField( - model_name='topicmedia', - name='file_url', - field=models.URLField(blank=True, max_length=500, null=True, verbose_name='文件链接'), - ), - migrations.AlterField( - model_name='topicmedia', - name='file', - field=models.FileField(blank=True, null=True, upload_to='community/media/', verbose_name='文件'), - ), - ] diff --git a/backend/community/migrations/0007_announcement.py b/backend/community/migrations/0007_announcement.py deleted file mode 100644 index 5b97855..0000000 --- a/backend/community/migrations/0007_announcement.py +++ /dev/null @@ -1,36 +0,0 @@ -# Generated by Django 6.0.1 on 2026-02-12 06:55 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('community', '0006_topicmedia_file_url_alter_topicmedia_file'), - ] - - operations = [ - migrations.CreateModel( - name='Announcement', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=100, verbose_name='公告标题')), - ('content', models.TextField(verbose_name='公告内容')), - ('image', models.ImageField(blank=True, null=True, upload_to='announcements/', verbose_name='公告图片')), - ('image_url', models.URLField(blank=True, help_text='优先使用上传的图片', null=True, verbose_name='图片链接')), - ('link_url', models.URLField(blank=True, null=True, verbose_name='跳转链接')), - ('is_active', models.BooleanField(default=True, verbose_name='是否启用')), - ('is_pinned', models.BooleanField(default=False, verbose_name='是否置顶')), - ('priority', models.IntegerField(default=0, help_text='数字越大越靠前', verbose_name='排序权重')), - ('start_time', models.DateTimeField(blank=True, null=True, verbose_name='开始展示时间')), - ('end_time', models.DateTimeField(blank=True, null=True, 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': '社区公告管理', - 'ordering': ['-is_pinned', '-priority', '-created_at'], - }, - ), - ] diff --git a/backend/community/migrations/0008_activity_signup_form_config_and_more.py b/backend/community/migrations/0008_activity_signup_form_config_and_more.py deleted file mode 100644 index 0ac2f0d..0000000 --- a/backend/community/migrations/0008_activity_signup_form_config_and_more.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 6.0.1 on 2026-02-12 12:48 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('community', '0007_announcement'), - ] - - operations = [ - migrations.AddField( - model_name='activity', - name='signup_form_config', - field=models.JSONField(blank=True, default=list, help_text='配置报名时需要收集的信息,JSON格式,例如:[{"name": "phone", "label": "手机号", "type": "text", "required": true}]', verbose_name='报名表单配置'), - ), - migrations.AddField( - model_name='activitysignup', - name='signup_info', - field=models.JSONField(blank=True, default=dict, verbose_name='报名信息'), - ), - ] diff --git a/backend/community/migrations/0009_activity_ask_company_activity_ask_name_and_more.py b/backend/community/migrations/0009_activity_ask_company_activity_ask_name_and_more.py deleted file mode 100644 index 9bd1c87..0000000 --- a/backend/community/migrations/0009_activity_ask_company_activity_ask_name_and_more.py +++ /dev/null @@ -1,38 +0,0 @@ -# Generated by Django 6.0.1 on 2026-02-12 12:52 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('community', '0008_activity_signup_form_config_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='activity', - name='ask_company', - field=models.BooleanField(default=False, verbose_name='收集公司/机构'), - ), - migrations.AddField( - model_name='activity', - name='ask_name', - field=models.BooleanField(default=False, verbose_name='收集姓名'), - ), - migrations.AddField( - model_name='activity', - name='ask_phone', - field=models.BooleanField(default=False, verbose_name='收集手机号'), - ), - migrations.AddField( - model_name='activity', - name='ask_wechat', - field=models.BooleanField(default=False, verbose_name='收集微信号'), - ), - migrations.AlterField( - model_name='activity', - name='signup_form_config', - field=models.JSONField(blank=True, default=list, help_text='JSON格式的高级配置,若填写则优先于上方开关。例如:[{"name": "job", "label": "职位", "type": "text", "required": true}]', verbose_name='自定义报名配置'), - ), - ] diff --git a/backend/community/migrations/0010_activity_is_paid_activity_price_activitysignup_order.py b/backend/community/migrations/0010_activity_is_paid_activity_price_activitysignup_order.py deleted file mode 100644 index ec6b871..0000000 --- a/backend/community/migrations/0010_activity_is_paid_activity_price_activitysignup_order.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 6.0.1 on 2026-02-23 07:04 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('community', '0009_activity_ask_company_activity_ask_name_and_more'), - ('shop', '0031_adminphonenumber'), - ] - - operations = [ - migrations.AddField( - model_name='activity', - name='is_paid', - field=models.BooleanField(default=False, verbose_name='是否收费'), - ), - migrations.AddField( - model_name='activity', - name='price', - field=models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='报名费用'), - ), - migrations.AddField( - model_name='activitysignup', - name='order', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='activity_signups', to='shop.order', verbose_name='关联订单'), - ), - ] diff --git a/backend/community/migrations/0011_activity_auto_confirm_alter_activitysignup_status.py b/backend/community/migrations/0011_activity_auto_confirm_alter_activitysignup_status.py deleted file mode 100644 index 2e48ca4..0000000 --- a/backend/community/migrations/0011_activity_auto_confirm_alter_activitysignup_status.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 6.0.1 on 2026-02-23 08:31 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('community', '0010_activity_is_paid_activity_price_activitysignup_order'), - ] - - operations = [ - migrations.AddField( - model_name='activity', - name='auto_confirm', - field=models.BooleanField(default=False, help_text='开启后,付费活动支付成功或免费活动报名后直接显示报名成功,不需要审核', verbose_name='无需审核'), - ), - migrations.AlterField( - model_name='activitysignup', - name='status', - field=models.CharField(choices=[('unpaid', '待支付'), ('pending', '审核中'), ('confirmed', '报名成功'), ('cancelled', '已取消')], default='confirmed', max_length=20, verbose_name='状态'), - ), - ] diff --git a/backend/community/migrations/0012_activity_is_visible.py b/backend/community/migrations/0012_activity_is_visible.py deleted file mode 100644 index 3ee4e9c..0000000 --- a/backend/community/migrations/0012_activity_is_visible.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 6.0.1 on 2026-02-23 09:33 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('community', '0011_activity_auto_confirm_alter_activitysignup_status'), - ] - - operations = [ - migrations.AddField( - model_name='activity', - name='is_visible', - field=models.BooleanField(default=True, help_text='关闭后将不在前端列表页显示', verbose_name='是否显示'), - ), - ] diff --git a/backend/community/migrations/0013_alter_reply_options_reply_is_pinned.py b/backend/community/migrations/0013_alter_reply_options_reply_is_pinned.py deleted file mode 100644 index 8876b8f..0000000 --- a/backend/community/migrations/0013_alter_reply_options_reply_is_pinned.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 6.0.1 on 2026-02-24 16:30 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('community', '0012_activity_is_visible'), - ] - - operations = [ - migrations.AlterModelOptions( - name='reply', - options={'ordering': ['-is_pinned', '-created_at'], 'verbose_name': '帖子回复', 'verbose_name_plural': '帖子回复管理'}, - ), - migrations.AddField( - model_name='reply', - name='is_pinned', - field=models.BooleanField(default=False, verbose_name='置顶'), - ), - ] diff --git a/backend/community/migrations/0014_alter_topic_options_topic_order.py b/backend/community/migrations/0014_alter_topic_options_topic_order.py deleted file mode 100644 index 9b05992..0000000 --- a/backend/community/migrations/0014_alter_topic_options_topic_order.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 6.0.1 on 2026-02-24 16:44 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('community', '0013_alter_reply_options_reply_is_pinned'), - ] - - operations = [ - migrations.AlterModelOptions( - name='topic', - options={'ordering': ['order', '-is_pinned', '-created_at'], 'verbose_name': '论坛帖子', 'verbose_name_plural': '论坛帖子管理'}, - ), - migrations.AddField( - model_name='topic', - name='order', - field=models.IntegerField(default=0, verbose_name='排序'), - ), - ] diff --git a/backend/community/migrations/0015_topic_status.py b/backend/community/migrations/0015_topic_status.py deleted file mode 100644 index 569f734..0000000 --- a/backend/community/migrations/0015_topic_status.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 6.0.1 on 2026-02-27 06:43 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('community', '0014_alter_topic_options_topic_order'), - ] - - operations = [ - migrations.AddField( - model_name='topic', - name='status', - field=models.CharField(choices=[('pending', '待审核'), ('published', '已发布'), ('rejected', '已拒绝')], default='published', max_length=20, verbose_name='状态'), - ), - ] diff --git a/backend/community/migrations/0016_reply_likes_topic_likes.py b/backend/community/migrations/0016_reply_likes_topic_likes.py deleted file mode 100644 index ee850c4..0000000 --- a/backend/community/migrations/0016_reply_likes_topic_likes.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 6.0.1 on 2026-03-02 12:25 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('community', '0015_topic_status'), - ('shop', '0039_vccourse_video_embed_code'), - ] - - operations = [ - migrations.AddField( - model_name='reply', - name='likes', - field=models.ManyToManyField(blank=True, related_name='liked_replies', to='shop.wechatuser', verbose_name='点赞用户'), - ), - migrations.AddField( - model_name='topic', - name='likes', - field=models.ManyToManyField(blank=True, related_name='liked_topics', to='shop.wechatuser', verbose_name='点赞用户'), - ), - ] diff --git a/backend/community/models.py b/backend/community/models.py index 6d30769..0201a62 100644 --- a/backend/community/models.py +++ b/backend/community/models.py @@ -18,6 +18,8 @@ class Activity(models.Model): is_paid = models.BooleanField(default=False, verbose_name="是否收费") price = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name="报名费用") + author = models.ForeignKey(WeChatUser, on_delete=models.SET_NULL, related_name='activities', verbose_name="发布者", null=True, blank=True) + is_active = models.BooleanField(default=True, verbose_name="是否启用") is_visible = models.BooleanField(default=True, verbose_name="是否显示", help_text="关闭后将不在前端列表页显示") auto_confirm = models.BooleanField(default=False, verbose_name="无需审核", help_text="开启后,付费活动支付成功或免费活动报名后直接显示报名成功,不需要审核") diff --git a/backend/community/urls.py b/backend/community/urls.py index 13e12cd..9f9f180 100644 --- a/backend/community/urls.py +++ b/backend/community/urls.py @@ -1,6 +1,6 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter -from .views import ActivityViewSet, TopicViewSet, ReplyViewSet, TopicMediaViewSet, AnnouncementViewSet +from .views import ActivityViewSet, TopicViewSet, ReplyViewSet, TopicMediaViewSet, AnnouncementViewSet, AdminPublishViewSet router = DefaultRouter() router.register(r'activities', ActivityViewSet) @@ -8,6 +8,7 @@ router.register(r'topics', TopicViewSet) router.register(r'replies', ReplyViewSet) router.register(r'media', TopicMediaViewSet, basename='media') router.register(r'announcements', AnnouncementViewSet) +router.register(r'admin-publish', AdminPublishViewSet, basename='admin-publish') urlpatterns = [ path('', include(router.urls)), diff --git a/backend/community/views.py b/backend/community/views.py index 2d6ad09..c0f5ff1 100644 --- a/backend/community/views.py +++ b/backend/community/views.py @@ -411,3 +411,66 @@ class AnnouncementViewSet(viewsets.ReadOnlyModelViewSet): # Filter by end_time (if set, must be >= now) qs = qs.filter(models.Q(end_time__isnull=True) | models.Q(end_time__gte=now)) return qs.order_by('-is_pinned', '-priority', '-created_at') + +class AdminPublishViewSet(viewsets.ViewSet): + """ + 管理员/API发布接口 + """ + permission_classes = [] + authentication_classes = [] + + def check_api_key(self, request): + key = request.headers.get('X-API-KEY') or request.query_params.get('apikey') + if key != '123quant-speed': + return False + return True + + def get_admin_user_by_phone(self, phone): + if not phone: + return None + # Find WeChatUser by phone + user = WeChatUser.objects.filter(phone_number=phone).first() + if not user: + return None + + # Check if linked to a system user and has admin privileges (is_staff) + if user.user and user.user.is_staff: + return user + + return None + + @extend_schema(summary="API发布活动") + @action(detail=False, methods=['post']) + def publish_activity(self, request): + if not self.check_api_key(request): + return Response({'error': 'Invalid API Key'}, status=403) + + phone = request.data.get('phone_number') + user = self.get_admin_user_by_phone(phone) + if not user: + return Response({'error': 'Admin user not found with this phone number'}, status=404) + + data = request.data.copy() + serializer = ActivitySerializer(data=data) + if serializer.is_valid(): + activity = serializer.save(author=user) + return Response(serializer.data, status=201) + return Response(serializer.errors, status=400) + + @extend_schema(summary="API发布帖子") + @action(detail=False, methods=['post']) + def publish_topic(self, request): + if not self.check_api_key(request): + return Response({'error': 'Invalid API Key'}, status=403) + + phone = request.data.get('phone_number') + user = self.get_admin_user_by_phone(phone) + if not user: + return Response({'error': 'Admin user not found with this phone number'}, status=404) + + data = request.data.copy() + serializer = TopicSerializer(data=data) + if serializer.is_valid(): + topic = serializer.save(author=user, status='published') + return Response(serializer.data, status=201) + return Response(serializer.errors, status=400) diff --git a/frontend/src/context/AuthContext.jsx b/frontend/src/context/AuthContext.jsx index f968336..7180a6a 100644 --- a/frontend/src/context/AuthContext.jsx +++ b/frontend/src/context/AuthContext.jsx @@ -7,7 +7,6 @@ const AuthContext = createContext(null); export const AuthProvider = ({ children }) => { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); - const [loginModalVisible, setLoginModalVisible] = useState(false); useEffect(() => { const initAuth = async () => { @@ -72,11 +71,8 @@ export const AuthProvider = ({ children }) => { localStorage.setItem('user', JSON.stringify(newUser)); }; - const showLoginModal = () => setLoginModalVisible(true); - const hideLoginModal = () => setLoginModalVisible(false); - return ( - + {children} ); diff --git a/frontend/src/pages/ForumList.jsx b/frontend/src/pages/ForumList.jsx index 4d77789..ee882ed 100644 --- a/frontend/src/pages/ForumList.jsx +++ b/frontend/src/pages/ForumList.jsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react'; import { Typography, Input, Button, List, Tag, Avatar, Card, Space, Spin, message, Badge, Tooltip, Tabs, Row, Col, Grid, Carousel, Modal } from 'antd'; -import { SearchOutlined, PlusOutlined, UserOutlined, MessageOutlined, EyeOutlined, CheckCircleFilled, FireOutlined, StarFilled, QuestionCircleOutlined, ShareAltOutlined, SoundOutlined, RightOutlined, CloseOutlined, LikeOutlined } from '@ant-design/icons'; +import { SearchOutlined, PlusOutlined, UserOutlined, MessageOutlined, EyeOutlined, CheckCircleFilled, FireOutlined, StarFilled, QuestionCircleOutlined, ShareAltOutlined, SoundOutlined, RightOutlined, CloseOutlined } from '@ant-design/icons'; import { useNavigate } from 'react-router-dom'; import { motion } from 'framer-motion'; import { getTopics, getStarUsers, getAnnouncements } from '../api'; @@ -296,10 +296,6 @@ const ForumList = () => {
{item.view_count || 0}
-
- -
{item.like_count || 0}
-