活动发布
All checks were successful
Deploy to Server / deploy (push) Successful in 1m44s

This commit is contained in:
jeremygan2021
2026-03-04 13:07:54 +08:00
parent 96ebd781b9
commit d3dbaaa090
23 changed files with 160 additions and 409 deletions

View File

@@ -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'),

View File

@@ -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={

View File

@@ -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='发布者'),
),
]

View File

@@ -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='关联硬件'),
),
]

View File

@@ -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': '论坛媒体资源管理',
},
),
]

View File

@@ -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图'),
),
]

View File

@@ -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='分类'),
),
]

View File

@@ -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='文件'),
),
]

View File

@@ -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'],
},
),
]

View File

@@ -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='报名信息'),
),
]

View File

@@ -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='自定义报名配置'),
),
]

View File

@@ -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='关联订单'),
),
]

View File

@@ -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='状态'),
),
]

View File

@@ -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='是否显示'),
),
]

View File

@@ -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='置顶'),
),
]

View File

@@ -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='排序'),
),
]

View File

@@ -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='状态'),
),
]

View File

@@ -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='点赞用户'),
),
]

View File

@@ -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="开启后,付费活动支付成功或免费活动报名后直接显示报名成功,不需要审核")

View File

@@ -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)),

View File

@@ -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)

View File

@@ -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 (
<AuthContext.Provider value={{ user, login, logout, updateUser, loading, loginModalVisible, showLoginModal, hideLoginModal }}>
<AuthContext.Provider value={{ user, login, logout, updateUser, loading }}>
{children}
</AuthContext.Provider>
);

View File

@@ -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 = () => {
<EyeOutlined style={{ fontSize: isMobile ? 14 : 16, color: '#666' }} />
<div style={{ color: '#888', fontSize: isMobile ? 10 : 12 }}>{item.view_count || 0}</div>
</div>
<div style={{ textAlign: 'center', marginTop: isMobile ? 2 : 5 }}>
<LikeOutlined style={{ fontSize: isMobile ? 14 : 16, color: '#666' }} />
<div style={{ color: '#888', fontSize: isMobile ? 10 : 12 }}>{item.like_count || 0}</div>
</div>
</div>
</div>
</Card>