You've already forked opc-backend
开发了多角色登录与鉴权接口:实现了普通用户、企业和管理员的登录分流,并支持Token验证。
开发了权限控制接口:实现了通过数据库分配菜单权限节点,控制接口访问安全。 开发了实名认证中心:实现了个人身份证信息与企业营业执照的提交与审核接口。 开发了任务与协作大厅核心业务:实现了任务的发布、接单、状态流转以及专家邀约接口。 配置了全局环境变量与数据库引擎:集成了 PostgreSQL 数据库、Redis 缓存与 MinIO 对象存储。
This commit is contained in:
0
system/__init__.py
Normal file
0
system/__init__.py
Normal file
3
system/admin.py
Normal file
3
system/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
6
system/apps.py
Normal file
6
system/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class SystemConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'system'
|
||||
108
system/migrations/0001_initial.py
Normal file
108
system/migrations/0001_initial.py
Normal file
@@ -0,0 +1,108 @@
|
||||
# Generated by Django 4.2.30 on 2026-04-25 09:46
|
||||
|
||||
from django.db import migrations, models
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Announcement',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('title', models.CharField(max_length=256)),
|
||||
('content', models.TextField()),
|
||||
('cover_url', models.CharField(blank=True, max_length=512, null=True)),
|
||||
('is_published', models.BooleanField(default=False)),
|
||||
('published_at', models.DateTimeField(blank=True, null=True)),
|
||||
('is_deleted', models.BooleanField(default=False)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'db_table': 'announcements',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AuditLog',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('action', models.CharField(max_length=128)),
|
||||
('resource', models.CharField(blank=True, max_length=128, null=True)),
|
||||
('resource_id', models.CharField(blank=True, max_length=128, null=True)),
|
||||
('detail', models.JSONField(default=dict)),
|
||||
('ip', models.GenericIPAddressField(blank=True, null=True)),
|
||||
('user_agent', models.TextField(blank=True, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
options={
|
||||
'db_table': 'audit_logs',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ModelToken',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('model_id', models.CharField(max_length=128)),
|
||||
('model_name', models.CharField(blank=True, max_length=256, null=True)),
|
||||
('token_value', models.TextField()),
|
||||
('expires_at', models.DateTimeField(blank=True, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
options={
|
||||
'db_table': 'model_tokens',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Notification',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('type', models.CharField(choices=[('SYSTEM', '系统通知'), ('CERTIFICATION', '认证通知'), ('TASK', '任务通知'), ('RESERVATION', '预约通知')], max_length=32)),
|
||||
('title', models.CharField(max_length=256)),
|
||||
('content', models.TextField()),
|
||||
('related_id', models.CharField(blank=True, max_length=128, null=True)),
|
||||
('is_read', models.BooleanField(default=False)),
|
||||
('is_deleted', models.BooleanField(default=False)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
options={
|
||||
'db_table': 'notifications',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SmsVerification',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('phone', models.CharField(max_length=20)),
|
||||
('scene', models.CharField(choices=[('REGISTER', '注册'), ('LOGIN', '登录'), ('RESET_PASSWORD', '重置密码')], max_length=32)),
|
||||
('code', models.CharField(max_length=8)),
|
||||
('is_used', models.BooleanField(default=False)),
|
||||
('expired_at', models.DateTimeField()),
|
||||
('used_at', models.DateTimeField(blank=True, null=True)),
|
||||
('ip', models.GenericIPAddressField(blank=True, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
options={
|
||||
'db_table': 'sms_verifications',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SystemConfig',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('config_key', models.CharField(max_length=128, unique=True)),
|
||||
('config_value', models.JSONField()),
|
||||
('description', models.TextField(blank=True, null=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'db_table': 'system_configs',
|
||||
},
|
||||
),
|
||||
]
|
||||
43
system/migrations/0002_initial.py
Normal file
43
system/migrations/0002_initial.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# Generated by Django 4.2.30 on 2026-04-25 09:46
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('system', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='systemconfig',
|
||||
name='updated_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_configs', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='notification',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='modeltoken',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='model_tokens', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='auditlog',
|
||||
name='user',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='audit_logs', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='announcement',
|
||||
name='publisher',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='published_announcements', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
||||
30
system/migrations/0003_aimodel.py
Normal file
30
system/migrations/0003_aimodel.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# Generated by Django 4.2.30 on 2026-04-25 13:10
|
||||
|
||||
from django.db import migrations, models
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('system', '0002_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AiModel',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=128)),
|
||||
('provider', models.CharField(max_length=128)),
|
||||
('description', models.TextField(blank=True, null=True)),
|
||||
('icon', models.CharField(blank=True, max_length=64, null=True)),
|
||||
('price_per_token', models.DecimalField(decimal_places=6, default=0, max_digits=10)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
options={
|
||||
'db_table': 'ai_models',
|
||||
},
|
||||
),
|
||||
]
|
||||
30
system/migrations/0004_skill.py
Normal file
30
system/migrations/0004_skill.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# Generated by Django 4.2.30 on 2026-04-27 16:36
|
||||
|
||||
from django.db import migrations, models
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('system', '0003_aimodel'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Skill',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=64, unique=True)),
|
||||
('category', models.CharField(blank=True, help_text='分类: 技术/设计/内容/数据 等', max_length=64, null=True)),
|
||||
('sort_order', models.IntegerField(default=0, help_text='排序权重, 越小越靠前')),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'db_table': 'skills',
|
||||
'ordering': ['sort_order', 'name'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,27 @@
|
||||
# Generated by Django 4.2.30 on 2026-04-27 17:30
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('system', '0004_skill'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='announcement',
|
||||
options={'ordering': ['-is_recommended', '-recommend_priority', '-created_at']},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='announcement',
|
||||
name='is_recommended',
|
||||
field=models.BooleanField(default=False, help_text='推荐/置顶'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='announcement',
|
||||
name='recommend_priority',
|
||||
field=models.IntegerField(default=0, help_text='推荐优先级, 越大越靠前'),
|
||||
),
|
||||
]
|
||||
18
system/migrations/0006_announcement_target_audience.py
Normal file
18
system/migrations/0006_announcement_target_audience.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.30 on 2026-04-27 17:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('system', '0005_alter_announcement_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='announcement',
|
||||
name='target_audience',
|
||||
field=models.CharField(choices=[('ALL', '全平台'), ('USER', '用户端'), ('ENTERPRISE', '企业端')], default='ALL', help_text='公告受众', max_length=16),
|
||||
),
|
||||
]
|
||||
0
system/migrations/__init__.py
Normal file
0
system/migrations/__init__.py
Normal file
78
system/minio_utils.py
Normal file
78
system/minio_utils.py
Normal file
@@ -0,0 +1,78 @@
|
||||
from minio import Minio
|
||||
from django.conf import settings
|
||||
import uuid
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class MinioClient:
|
||||
def __init__(self):
|
||||
self._client = None
|
||||
self.bucket_name = getattr(settings, 'MINIO_BUCKET_NAME', 'opc-assets')
|
||||
self._initialized = False
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
if self._client is None:
|
||||
self._client = Minio(
|
||||
endpoint=getattr(settings, 'MINIO_ENDPOINT', 'localhost:9000'),
|
||||
access_key=getattr(settings, 'MINIO_ACCESS_KEY', 'minioadmin'),
|
||||
secret_key=getattr(settings, 'MINIO_SECRET_KEY', 'minioadmin'),
|
||||
secure=getattr(settings, 'MINIO_SECURE', False)
|
||||
)
|
||||
if not self._initialized:
|
||||
try:
|
||||
self._ensure_bucket()
|
||||
self._initialized = True
|
||||
except Exception as e:
|
||||
logger.warning(f'MinIO bucket init failed (will retry on next call): {e}')
|
||||
return self._client
|
||||
|
||||
def _ensure_bucket(self):
|
||||
if not self._client.bucket_exists(self.bucket_name):
|
||||
self._client.make_bucket(self.bucket_name)
|
||||
|
||||
# Always update policy to ensure public access
|
||||
policy = {
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Principal": {"AWS": ["*"]},
|
||||
"Action": ["s3:GetBucketLocation", "s3:ListBucket"],
|
||||
"Resource": [f"arn:aws:s3:::{self.bucket_name}"],
|
||||
},
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Principal": {"AWS": ["*"]},
|
||||
"Action": ["s3:GetObject"],
|
||||
"Resource": [f"arn:aws:s3:::{self.bucket_name}/*"],
|
||||
},
|
||||
],
|
||||
}
|
||||
self._client.set_bucket_policy(self.bucket_name, json.dumps(policy))
|
||||
|
||||
def upload_file(self, file_obj, folder='general'):
|
||||
# Generate a unique name
|
||||
ext = os.path.splitext(file_obj.name)[1]
|
||||
file_name = f"{folder}/{uuid.uuid4()}{ext}"
|
||||
|
||||
# Upload (this will trigger lazy init of the client)
|
||||
self.client.put_object(
|
||||
self.bucket_name,
|
||||
file_name,
|
||||
file_obj,
|
||||
length=file_obj.size,
|
||||
content_type=file_obj.content_type
|
||||
)
|
||||
|
||||
# Return the public URL
|
||||
base_url = getattr(settings, 'MINIO_PUBLIC_URL', 'http://127.0.0.1:9000')
|
||||
return f"{base_url}/{self.bucket_name}/{file_name}"
|
||||
|
||||
|
||||
# Lazy singleton — does NOT connect at import time
|
||||
minio_client = MinioClient()
|
||||
|
||||
132
system/models.py
Normal file
132
system/models.py
Normal file
@@ -0,0 +1,132 @@
|
||||
import uuid
|
||||
from django.db import models
|
||||
|
||||
class NotificationType(models.TextChoices):
|
||||
SYSTEM = 'SYSTEM', '系统通知'
|
||||
CERTIFICATION = 'CERTIFICATION', '认证通知'
|
||||
TASK = 'TASK', '任务通知'
|
||||
RESERVATION = 'RESERVATION', '预约通知'
|
||||
|
||||
class SmsScene(models.TextChoices):
|
||||
REGISTER = 'REGISTER', '注册'
|
||||
LOGIN = 'LOGIN', '登录'
|
||||
RESET_PASSWORD = 'RESET_PASSWORD', '重置密码'
|
||||
|
||||
class AudienceChoices(models.TextChoices):
|
||||
ALL = 'ALL', '全平台'
|
||||
USER = 'USER', '用户端'
|
||||
ENTERPRISE = 'ENTERPRISE', '企业端'
|
||||
|
||||
class Announcement(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
title = models.CharField(max_length=256)
|
||||
content = models.TextField()
|
||||
cover_url = models.CharField(max_length=512, null=True, blank=True)
|
||||
publisher = models.ForeignKey('users.User', on_delete=models.CASCADE, related_name='published_announcements')
|
||||
target_audience = models.CharField(max_length=16, choices=AudienceChoices.choices, default=AudienceChoices.ALL, help_text='公告受众')
|
||||
is_published = models.BooleanField(default=False)
|
||||
published_at = models.DateTimeField(null=True, blank=True)
|
||||
is_deleted = models.BooleanField(default=False)
|
||||
is_recommended = models.BooleanField(default=False, help_text='推荐/置顶')
|
||||
recommend_priority = models.IntegerField(default=0, help_text='推荐优先级, 越大越靠前')
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'announcements'
|
||||
ordering = ['-is_recommended', '-recommend_priority', '-created_at']
|
||||
|
||||
class ModelToken(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
user = models.ForeignKey('users.User', on_delete=models.CASCADE, related_name='model_tokens')
|
||||
model_id = models.CharField(max_length=128)
|
||||
model_name = models.CharField(max_length=256, null=True, blank=True)
|
||||
token_value = models.TextField()
|
||||
expires_at = models.DateTimeField(null=True, blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'model_tokens'
|
||||
|
||||
class SmsVerification(models.Model):
|
||||
phone = models.CharField(max_length=20)
|
||||
scene = models.CharField(max_length=32, choices=SmsScene.choices)
|
||||
code = models.CharField(max_length=8)
|
||||
is_used = models.BooleanField(default=False)
|
||||
expired_at = models.DateTimeField()
|
||||
used_at = models.DateTimeField(null=True, blank=True)
|
||||
ip = models.GenericIPAddressField(null=True, blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'sms_verifications'
|
||||
|
||||
class AuditLog(models.Model):
|
||||
user = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, blank=True, related_name='audit_logs')
|
||||
action = models.CharField(max_length=128)
|
||||
resource = models.CharField(max_length=128, null=True, blank=True)
|
||||
resource_id = models.CharField(max_length=128, null=True, blank=True)
|
||||
detail = models.JSONField(default=dict)
|
||||
ip = models.GenericIPAddressField(null=True, blank=True)
|
||||
user_agent = models.TextField(null=True, blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'audit_logs'
|
||||
|
||||
class Notification(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
user = models.ForeignKey('users.User', on_delete=models.CASCADE, related_name='notifications')
|
||||
type = models.CharField(max_length=32, choices=NotificationType.choices)
|
||||
title = models.CharField(max_length=256)
|
||||
content = models.TextField()
|
||||
related_id = models.CharField(max_length=128, null=True, blank=True)
|
||||
is_read = models.BooleanField(default=False)
|
||||
is_deleted = models.BooleanField(default=False)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'notifications'
|
||||
|
||||
class SystemConfig(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
config_key = models.CharField(max_length=128, unique=True)
|
||||
config_value = models.JSONField()
|
||||
description = models.TextField(null=True, blank=True)
|
||||
updated_by = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, blank=True, related_name='updated_configs')
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'system_configs'
|
||||
|
||||
class AiModel(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
name = models.CharField(max_length=128)
|
||||
provider = models.CharField(max_length=128)
|
||||
description = models.TextField(null=True, blank=True)
|
||||
icon = models.CharField(max_length=64, null=True, blank=True)
|
||||
price_per_token = models.DecimalField(max_digits=10, decimal_places=6, default=0)
|
||||
is_active = models.BooleanField(default=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'ai_models'
|
||||
|
||||
|
||||
class Skill(models.Model):
|
||||
"""Admin-managed skill/expertise domain tags used across the platform."""
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
name = models.CharField(max_length=64, unique=True)
|
||||
category = models.CharField(max_length=64, null=True, blank=True, help_text='分类: 技术/设计/内容/数据 等')
|
||||
sort_order = models.IntegerField(default=0, help_text='排序权重, 越小越靠前')
|
||||
is_active = models.BooleanField(default=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'skills'
|
||||
ordering = ['sort_order', 'name']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
36
system/serializers.py
Normal file
36
system/serializers.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from rest_framework import serializers
|
||||
from .models import Announcement, Notification, SystemConfig, AiModel, ModelToken, Skill
|
||||
|
||||
class AnnouncementSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Announcement
|
||||
fields = '__all__'
|
||||
|
||||
class NotificationSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Notification
|
||||
fields = '__all__'
|
||||
read_only_fields = ['id', 'user', 'created_at']
|
||||
|
||||
class SystemConfigSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = SystemConfig
|
||||
fields = '__all__'
|
||||
|
||||
class AiModelSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = AiModel
|
||||
fields = '__all__'
|
||||
|
||||
class ModelTokenSerializer(serializers.ModelSerializer):
|
||||
model_name = serializers.ReadOnlyField(source='model.name')
|
||||
class Meta:
|
||||
model = ModelToken
|
||||
fields = '__all__'
|
||||
read_only_fields = ['id', 'user', 'token_value', 'created_at']
|
||||
|
||||
class SkillSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Skill
|
||||
fields = '__all__'
|
||||
read_only_fields = ['id', 'created_at', 'updated_at']
|
||||
3
system/tests.py
Normal file
3
system/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
17
system/urls.py
Normal file
17
system/urls.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import AnnouncementViewSet, NotificationViewSet, SystemConfigViewSet, AiModelViewSet, ModelTokenViewSet, FileUploadView, SkillViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register('announcements', AnnouncementViewSet, basename='announcement')
|
||||
router.register('notifications', NotificationViewSet, basename='notification')
|
||||
router.register('configs', SystemConfigViewSet, basename='config')
|
||||
router.register('models', AiModelViewSet, basename='model')
|
||||
router.register('tokens', ModelTokenViewSet, basename='model-token')
|
||||
router.register('skills', SkillViewSet, basename='skill')
|
||||
|
||||
urlpatterns = [
|
||||
path('upload/', FileUploadView.as_view(), name='file_upload'),
|
||||
path('', include(router.urls)),
|
||||
]
|
||||
|
||||
152
system/views.py
Normal file
152
system/views.py
Normal file
@@ -0,0 +1,152 @@
|
||||
from rest_framework import viewsets, generics, permissions, status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.decorators import action
|
||||
from .models import Announcement, Notification, SystemConfig, AiModel, ModelToken, Skill
|
||||
from .serializers import AnnouncementSerializer, NotificationSerializer, SystemConfigSerializer, AiModelSerializer, ModelTokenSerializer, SkillSerializer
|
||||
|
||||
class AnnouncementViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
@author: xujl
|
||||
Api说明: 系统公告接口视图。管理员发布全局或定向通知公告,前端获取并展示给对应受众。
|
||||
"""
|
||||
queryset = Announcement.objects.filter(is_deleted=False)
|
||||
serializer_class = AnnouncementSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
qs = Announcement.objects.filter(is_deleted=False).order_by('-is_recommended', '-recommend_priority', '-created_at')
|
||||
audience = self.request.query_params.get('audience')
|
||||
if audience:
|
||||
qs = qs.filter(target_audience__in=[audience, 'ALL'])
|
||||
if self.action == 'list' and not self.request.user.is_staff:
|
||||
qs = qs.filter(is_published=True)
|
||||
return qs
|
||||
|
||||
def get_permissions(self):
|
||||
if self.action in ['create', 'update', 'partial_update', 'destroy', 'toggle_recommend']:
|
||||
return [permissions.IsAdminUser()]
|
||||
return [permissions.AllowAny()]
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def toggle_recommend(self, request, pk=None):
|
||||
ann = self.get_object()
|
||||
ann.is_recommended = request.data.get('is_recommended', not ann.is_recommended)
|
||||
ann.recommend_priority = request.data.get('recommend_priority', ann.recommend_priority)
|
||||
ann.save(update_fields=['is_recommended', 'recommend_priority'])
|
||||
return Response({'is_recommended': ann.is_recommended, 'recommend_priority': ann.recommend_priority})
|
||||
|
||||
class NotificationViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
@author: xujl
|
||||
Api说明: 站内消息通知接口视图。用于系统内部产生的各类站内信、状态变更通知的拉取和未读/已读状态更新。
|
||||
"""
|
||||
serializer_class = NotificationSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
return Notification.objects.filter(user=self.request.user, is_deleted=False)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def read(self, request, pk=None):
|
||||
notification = self.get_object()
|
||||
notification.is_read = True
|
||||
notification.save()
|
||||
return Response({'status': '已标记为已读'})
|
||||
|
||||
@action(detail=False, methods=['post'])
|
||||
def read_all(self, request):
|
||||
self.get_queryset().update(is_read=True)
|
||||
return Response({'status': '全部标记为已读'})
|
||||
|
||||
class SystemConfigViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
@author: xujl
|
||||
Api说明: 系统全局动态配置接口视图。用于保存字典数据、开关配置等,避免硬编码参数。
|
||||
"""
|
||||
queryset = SystemConfig.objects.all()
|
||||
serializer_class = SystemConfigSerializer
|
||||
lookup_field = 'config_key'
|
||||
|
||||
def get_permissions(self):
|
||||
if self.action in ['create', 'update', 'partial_update', 'destroy']:
|
||||
return [permissions.IsAdminUser()]
|
||||
return [permissions.AllowAny()]
|
||||
|
||||
class AiModelViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
@author: xujl
|
||||
Api说明: AI模型配置接口视图。统一管理平台对接的各大AI平台(如OpenAI、文心一言等)的模型参数及端点。
|
||||
"""
|
||||
queryset = AiModel.objects.all()
|
||||
serializer_class = AiModelSerializer
|
||||
|
||||
def get_permissions(self):
|
||||
if self.action in ['create', 'update', 'partial_update', 'destroy']:
|
||||
return [permissions.IsAdminUser()]
|
||||
return [permissions.IsAuthenticatedOrReadOnly()]
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset()
|
||||
if self.action == 'list' and not self.request.user.is_staff:
|
||||
return qs.filter(is_active=True)
|
||||
return qs
|
||||
|
||||
class ModelTokenViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
@author: xujl
|
||||
Api说明: 大模型Token/密钥接口视图。安全管理模型请求时使用的Token或API Key认证信息。
|
||||
"""
|
||||
serializer_class = ModelTokenSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
return ModelToken.objects.filter(user=self.request.user)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
# In a real app, this would call MoRouter or another provider
|
||||
# For now, we generate a mock token
|
||||
import uuid
|
||||
mock_token = f"sk-opc-{uuid.uuid4().hex}"
|
||||
serializer.save(user=self.request.user, token_value=mock_token)
|
||||
|
||||
|
||||
class SkillViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
@author: xujl
|
||||
Api说明: 技能字典/用户标签接口视图。用于系统内的技能标签池管理,以供任务画像和用户画像进行匹配。
|
||||
"""
|
||||
"""Admin-managed skill/expertise tags. Read-only for non-admins."""
|
||||
queryset = Skill.objects.all()
|
||||
serializer_class = SkillSerializer
|
||||
|
||||
def get_permissions(self):
|
||||
if self.action in ['create', 'update', 'partial_update', 'destroy']:
|
||||
return [permissions.IsAdminUser()]
|
||||
return [permissions.AllowAny()]
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset()
|
||||
if self.action == 'list' and not (self.request.user.is_authenticated and self.request.user.is_staff):
|
||||
return qs.filter(is_active=True)
|
||||
return qs
|
||||
|
||||
from .minio_utils import minio_client
|
||||
|
||||
class FileUploadView(generics.GenericAPIView):
|
||||
"""
|
||||
@author: xujl
|
||||
Api说明: 系统通用文件上传接口。对接MinIO或S3等对象存储,处理前端传入的文件并返回URL外链。
|
||||
"""
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
def post(self, request):
|
||||
|
||||
file_obj = request.FILES.get('file')
|
||||
if not file_obj:
|
||||
return Response({'detail': '没有上传文件'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
folder = request.data.get('folder', 'general')
|
||||
try:
|
||||
file_url = minio_client.upload_file(file_obj, folder=folder)
|
||||
return Response({'url': file_url}, status=status.HTTP_201_CREATED)
|
||||
except Exception as e:
|
||||
return Response({'detail': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
Reference in New Issue
Block a user