This commit is contained in:
@@ -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'),
|
||||
|
||||
@@ -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={
|
||||
|
||||
20
backend/community/migrations/0002_activity_author.py
Normal file
20
backend/community/migrations/0002_activity_author.py
Normal 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='发布者'),
|
||||
),
|
||||
]
|
||||
@@ -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='关联硬件'),
|
||||
),
|
||||
]
|
||||
@@ -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': '论坛媒体资源管理',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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图'),
|
||||
),
|
||||
]
|
||||
@@ -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='分类'),
|
||||
),
|
||||
]
|
||||
@@ -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='文件'),
|
||||
),
|
||||
]
|
||||
@@ -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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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='报名信息'),
|
||||
),
|
||||
]
|
||||
@@ -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='自定义报名配置'),
|
||||
),
|
||||
]
|
||||
@@ -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='关联订单'),
|
||||
),
|
||||
]
|
||||
@@ -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='状态'),
|
||||
),
|
||||
]
|
||||
@@ -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='是否显示'),
|
||||
),
|
||||
]
|
||||
@@ -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='置顶'),
|
||||
),
|
||||
]
|
||||
@@ -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='排序'),
|
||||
),
|
||||
]
|
||||
@@ -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='状态'),
|
||||
),
|
||||
]
|
||||
@@ -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='点赞用户'),
|
||||
),
|
||||
]
|
||||
@@ -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="开启后,付费活动支付成功或免费活动报名后直接显示报名成功,不需要审核")
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user