diff --git a/.gitignore b/.gitignore
index 0e8c4e2..f5d62d5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -93,7 +93,7 @@ ehthumbs.db
*.3g2
*.asf
*.rm
-*.rmvb
+*.rmVB
*.vob
*.mpg
*.mpeg
diff --git a/README.md b/README.md
deleted file mode 100644
index 35048e2..0000000
--- a/README.md
+++ /dev/null
@@ -1,220 +0,0 @@
-# 量极AI硬件商城
-
-一个基于React和Django的AI硬件在线商城系统,提供硬件配置展示、订单管理和支付功能。
-
-## 🚀 项目概述
-
-量极AI硬件商城是一个全栈Web应用程序,专注于AI硬件产品的在线销售。系统采用前后端分离架构,前端使用React + Vite + Ant Design,后端使用Django REST Framework。
-
-## 📋 功能特性
-
-### 前端功能
-- 🛍️ 硬件配置展示和选择
-- 🛒 购物车功能
-- 📋 订单创建和管理
-- 💳 支付流程集成
-- 🔗 推广码支持
-- 📱 响应式设计
-
-### 后端功能
-- 🏪 产品配置管理
-- 📦 订单处理
-- 💰 支付接口
-- 👥 用户管理
-- 📊 数据统计
-
-## 🛠️ 技术栈
-
-### 前端技术
-- **React 19** - 现代化UI库
-- **Vite** - 快速构建工具
-- **Ant Design** - 企业级UI组件库
-- **React Router** - 路由管理
-- **Axios** - HTTP客户端
-
-### 后端技术
-- **Django 6.0** - Python Web框架
-- **Django REST Framework** - RESTful API
-- **PostgreSQL** - 数据库
-- **CORS Headers** - 跨域支持
-
-## 📁 项目结构
-
-```
-Quant-Speed_ai_hardware/
-├── frontend/ # React前端应用
-│ ├── src/
-│ │ ├── components/ # React组件
-│ │ │ └── HardwareShop.jsx
-│ │ ├── App.jsx # 主应用组件
-│ │ ├── api.js # API接口封装
-│ │ └── main.jsx # 应用入口
-│ ├── package.json # 前端依赖配置
-│ └── vite.config.js # Vite配置
-├── backend/ # Django后端应用
-│ ├── config/ # Django配置
-│ │ ├── settings.py # 主配置文件
-│ │ ├── urls.py # URL路由配置
-│ │ └── wsgi.py # WSGI配置
-│ ├── shop/ # 商城应用
-│ │ ├── models.py # 数据模型
-│ │ ├── views.py # 视图函数
-│ │ ├── serializers.py # 序列化器
-│ │ └── urls.py # 应用路由
-│ ├── manage.py # Django管理脚本
-│ └── populate_db.py # 数据库初始化脚本
-└── README.md
-```
-
-## 🚀 快速开始
-
-### 环境要求
-- Node.js 18+
-- Python 3.8+
-- PostgreSQL 12+
-
-### 前端安装
-
-```bash
-# 进入前端目录
-cd frontend
-
-# 安装依赖
-npm install
-
-# 启动开发服务器
-npm run dev
-
-# 构建生产版本
-npm run build
-```
-
-### 后端安装
-
-```bash
-# 进入后端目录
-cd backend
-
-# 创建虚拟环境
-python -m venv venv
-
-# 激活虚拟环境
-# Windows
-venv\Scripts\activate
-# macOS/Linux
-source venv/bin/activate
-
-# 安装依赖
-pip install django djangorestframework django-cors-headers psycopg2-binary
-
-# 数据库配置
-# 编辑 config/settings.py 中的数据库配置
-
-# 运行数据库迁移
-python manage.py migrate
-
-# 创建超级用户
-python manage.py createsuperuser
-
-# 启动开发服务器
-python manage.py runserver
-```
-
-### 数据库初始化
-
-```bash
-# 运行数据库填充脚本
-python populate_db.py
-```
-### admin账户:
-‘
-jeremygan2021
-qweasdzxc1
-’
-
-## 🔧 配置说明
-
-### 前端配置
-- **Vite配置**: `frontend/vite.config.js`
-- **环境变量**: 支持 `.env` 文件配置
-
-### 后端配置
-- **Django设置**: `backend/config/settings.py`
-- **数据库**: PostgreSQL配置
-- **CORS**: 跨域请求配置
-- **国际化**: 中文支持
-
-## 📡 API接口
-
-### 硬件配置接口
-- `GET /api/configs/` - 获取硬件配置列表
-- `GET /api/configs/{id}/` - 获取特定配置详情
-
-### 订单接口
-- `POST /api/orders/` - 创建订单
-- `GET /api/orders/{id}/` - 获取订单详情
-- `POST /api/orders/{id}/pay/` - 订单支付
-
-### 支付接口
-- `POST /api/payments/initiate/` - 发起支付
-- `POST /api/payments/confirm/` - 确认支付
-
-
-## 上传图片接口 不要乱传文件,造成oss存储费用增加
-### 上传硬件的3D文件(小智参数) zip压缩包,包含3文件和材质文件
-- `POST https://tangledup-ai-staging.oss-cn-shanghai.aliyuncs.com/market_page/hardware_xiaozhi/product_3D_image` - 上传3D文件
-
-### 上传硬件的图片(小智参数) 单张图片
-- `POST https://tangledup-ai-staging.oss-cn-shanghai.aliyuncs.com/market_page/hardware_xiaozhi/product_image` - 上传图片
-
-## 🎯 使用说明
-
-### 推广码功能
-系统支持URL推广码参数,格式:`?ref=推广码`
-
-### 支付流程
-1. 选择硬件配置
-2. 填写订单信息
-3. 发起支付请求
-4. 确认支付结果
-5. 订单完成
-
-## 🔒 安全说明
-
-- 生产环境请修改 `SECRET_KEY`
-- 配置HTTPS证书
-- 设置适当的CORS白名单
-- 定期备份数据库
-
-## 🐛 常见问题
-
-### 跨域问题
-确保后端CORS配置正确,开发环境可设置为允许所有来源。
-
-### 数据库连接失败
-检查PostgreSQL服务状态和连接配置。
-
-### 前端构建失败
-检查Node.js版本和依赖包完整性。
-
-## 📞 联系方式
-
-如有问题或建议,请通过以下方式联系:
-- 邮箱:support@Quant-Speed-ai.com
-- 电话:400-123-4567
-
-## 📄 许可证
-
-本项目采用 MIT 许可证 - 详见 [LICENSE](LICENSE) 文件
-
-## 🙏 致谢
-
-感谢以下开源项目的支持:
-- [React](https://reactjs.org/)
-- [Django](https://www.djangoproject.com/)
-- [Ant Design](https://ant.design/)
-- [Vite](https://vitejs.dev/)
-
----
-
-**⭐ 如果这个项目对您有帮助,请给我们一个星标!**
\ No newline at end of file
diff --git a/backend/check_urls.py b/backend/check_urls.py
index 6dd3a96..19ca943 100644
--- a/backend/check_urls.py
+++ b/backend/check_urls.py
@@ -12,7 +12,7 @@ links = [
"admin:shop_distributor_changelist",
"admin:shop_esp32config_changelist",
"admin:shop_service_changelist",
- "admin:shop_vbcourse_changelist",
+ "admin:shop_VBcourse_changelist",
"admin:shop_order_changelist",
"admin:shop_serviceorder_changelist",
"admin:shop_withdrawal_changelist",
diff --git a/backend/config/__pycache__/settings.cpython-312.pyc b/backend/config/__pycache__/settings.cpython-312.pyc
index c8aed22..7392c16 100644
Binary files a/backend/config/__pycache__/settings.cpython-312.pyc and b/backend/config/__pycache__/settings.cpython-312.pyc differ
diff --git a/backend/config/__pycache__/settings.cpython-313.pyc b/backend/config/__pycache__/settings.cpython-313.pyc
index b372ee5..f89d647 100644
Binary files a/backend/config/__pycache__/settings.cpython-313.pyc and b/backend/config/__pycache__/settings.cpython-313.pyc differ
diff --git a/backend/config/settings.py b/backend/config/settings.py
index 215dbd0..634dffd 100644
--- a/backend/config/settings.py
+++ b/backend/config/settings.py
@@ -232,9 +232,9 @@ UNFOLD = {
"link": reverse_lazy("admin:shop_service_changelist"),
},
{
- "title": "VB课程",
+ "title": "VC课程",
"icon": "school",
- "link": reverse_lazy("admin:shop_vbcourse_changelist"),
+ "link": reverse_lazy("admin:shop_vccourse_changelist"),
},
],
},
diff --git a/backend/db.sqlite3 b/backend/db.sqlite3
index 0cf1b4e..8be5396 100644
Binary files a/backend/db.sqlite3 and b/backend/db.sqlite3 differ
diff --git a/backend/shop/__pycache__/admin.cpython-312.pyc b/backend/shop/__pycache__/admin.cpython-312.pyc
index db1d0fe..8285aa9 100644
Binary files a/backend/shop/__pycache__/admin.cpython-312.pyc and b/backend/shop/__pycache__/admin.cpython-312.pyc differ
diff --git a/backend/shop/__pycache__/admin.cpython-313.pyc b/backend/shop/__pycache__/admin.cpython-313.pyc
index b314ad6..b311a91 100644
Binary files a/backend/shop/__pycache__/admin.cpython-313.pyc and b/backend/shop/__pycache__/admin.cpython-313.pyc differ
diff --git a/backend/shop/__pycache__/models.cpython-312.pyc b/backend/shop/__pycache__/models.cpython-312.pyc
index 1961d41..01bc3a1 100644
Binary files a/backend/shop/__pycache__/models.cpython-312.pyc and b/backend/shop/__pycache__/models.cpython-312.pyc differ
diff --git a/backend/shop/__pycache__/models.cpython-313.pyc b/backend/shop/__pycache__/models.cpython-313.pyc
index 4383773..0218948 100644
Binary files a/backend/shop/__pycache__/models.cpython-313.pyc and b/backend/shop/__pycache__/models.cpython-313.pyc differ
diff --git a/backend/shop/__pycache__/serializers.cpython-312.pyc b/backend/shop/__pycache__/serializers.cpython-312.pyc
index 8cf1e44..6140d80 100644
Binary files a/backend/shop/__pycache__/serializers.cpython-312.pyc and b/backend/shop/__pycache__/serializers.cpython-312.pyc differ
diff --git a/backend/shop/__pycache__/serializers.cpython-313.pyc b/backend/shop/__pycache__/serializers.cpython-313.pyc
index 0164209..76f8aff 100644
Binary files a/backend/shop/__pycache__/serializers.cpython-313.pyc and b/backend/shop/__pycache__/serializers.cpython-313.pyc differ
diff --git a/backend/shop/__pycache__/urls.cpython-312.pyc b/backend/shop/__pycache__/urls.cpython-312.pyc
index f690115..202d13c 100644
Binary files a/backend/shop/__pycache__/urls.cpython-312.pyc and b/backend/shop/__pycache__/urls.cpython-312.pyc differ
diff --git a/backend/shop/__pycache__/urls.cpython-313.pyc b/backend/shop/__pycache__/urls.cpython-313.pyc
index a251174..dc0bc45 100644
Binary files a/backend/shop/__pycache__/urls.cpython-313.pyc and b/backend/shop/__pycache__/urls.cpython-313.pyc differ
diff --git a/backend/shop/__pycache__/views.cpython-312.pyc b/backend/shop/__pycache__/views.cpython-312.pyc
index db1c7c7..bfb9b97 100644
Binary files a/backend/shop/__pycache__/views.cpython-312.pyc and b/backend/shop/__pycache__/views.cpython-312.pyc differ
diff --git a/backend/shop/__pycache__/views.cpython-313.pyc b/backend/shop/__pycache__/views.cpython-313.pyc
index 8cd5bca..4eee818 100644
Binary files a/backend/shop/__pycache__/views.cpython-313.pyc and b/backend/shop/__pycache__/views.cpython-313.pyc differ
diff --git a/backend/shop/admin.py b/backend/shop/admin.py
index 81f2527..9117603 100644
--- a/backend/shop/admin.py
+++ b/backend/shop/admin.py
@@ -4,7 +4,7 @@ from django.db.models import Sum
from django import forms
from unfold.admin import ModelAdmin, TabularInline
from unfold.decorators import display
-from .models import ESP32Config, Order, Salesperson, WeChatPayConfig, Service, VBCourse, ProductFeature, CommissionLog, WeChatUser, Distributor, Withdrawal, ServiceOrder
+from .models import ESP32Config, Order, Salesperson, WeChatPayConfig, Service, VCCourse, ProductFeature, CommissionLog, WeChatUser, Distributor, Withdrawal, ServiceOrder, CourseEnrollment
import qrcode
from io import BytesIO
import base64
@@ -83,7 +83,7 @@ class ESP32ConfigAdmin(ModelAdmin):
inlines = [ProductFeatureInline]
fieldsets = (
('基本信息', {
- 'fields': ('name', 'price', 'stock', 'description')
+ 'fields': ('name', 'price', 'stock', 'commission_rate', 'description')
}),
('硬件参数', {
'fields': ('chip_type', 'flash_size', 'ram_size', 'has_camera', 'has_microphone')
@@ -141,17 +141,21 @@ class ServiceOrderAdmin(ModelAdmin):
}),
)
-@admin.register(VBCourse)
-class VBCourseAdmin(ModelAdmin):
- list_display = ('title', 'course_type', 'tag', 'instructor', 'lesson_count', 'duration', 'created_at')
+@admin.register(VCCourse)
+class VCCourseAdmin(ModelAdmin):
+ list_display = ('title', 'course_type', 'price', 'tag', 'instructor', 'lesson_count', 'duration', 'created_at')
search_fields = ('title', 'description', 'instructor', 'tag')
list_filter = ('course_type', 'instructor', 'tag')
fieldsets = (
('基本信息', {
- 'fields': ('title', 'description', 'course_type', 'tag')
+ 'fields': ('title', 'description', 'course_type', 'tag', 'price')
+ }),
+ ('讲师信息', {
+ 'fields': ('instructor', 'instructor_title', 'instructor_desc', 'instructor_avatar', 'instructor_avatar_url'),
+ 'description': '讲师头像上传和URL二选一,优先使用URL'
}),
('课程详情', {
- 'fields': ('instructor', 'duration', 'lesson_count')
+ 'fields': ('duration', 'lesson_count', 'content')
}),
('封面', {
'fields': ('cover_image', 'cover_image_url'),
@@ -163,6 +167,24 @@ class VBCourseAdmin(ModelAdmin):
}),
)
+@admin.register(CourseEnrollment)
+class CourseEnrollmentAdmin(ModelAdmin):
+ list_display = ('customer_name', 'course', 'phone_number', 'status', 'created_at')
+ list_filter = ('status', 'course', 'created_at')
+ search_fields = ('customer_name', 'phone_number', 'wechat_id')
+
+ fieldsets = (
+ ('报名信息', {
+ 'fields': ('course', 'status', 'created_at')
+ }),
+ ('客户资料', {
+ 'fields': ('customer_name', 'phone_number', 'wechat_id', 'email', 'message')
+ }),
+ ('销售归属', {
+ 'fields': ('salesperson', 'distributor')
+ }),
+ )
+
@admin.register(Salesperson)
class SalespersonAdmin(ModelAdmin):
list_display = ('name', 'code', 'total_sales', 'view_promotion_url')
@@ -240,14 +262,14 @@ class SalespersonAdmin(ModelAdmin):
@admin.register(CommissionLog)
class CommissionLogAdmin(ModelAdmin):
- list_display = ('id', 'salesperson', 'amount', 'level', 'status', 'created_at')
- list_filter = ('status', 'level', 'salesperson', 'created_at')
- search_fields = ('salesperson__name', 'order__id')
+ list_display = ('id', 'salesperson', 'distributor', 'amount', 'level', 'status', 'created_at')
+ list_filter = ('status', 'level', 'salesperson', 'distributor', 'created_at')
+ search_fields = ('salesperson__name', 'distributor__user__nickname', 'order__id')
readonly_fields = ('amount', 'level', 'created_at')
fieldsets = (
('基本信息', {
- 'fields': ('salesperson', 'order', 'amount', 'level')
+ 'fields': ('salesperson', 'distributor', 'order', 'amount', 'level')
}),
('状态管理', {
'fields': ('status', 'created_at')
@@ -256,23 +278,31 @@ class CommissionLogAdmin(ModelAdmin):
@admin.register(Order)
class OrderAdmin(ModelAdmin):
- list_display = ('id', 'customer_name', 'config', 'total_price', 'status', 'courier_name', 'tracking_number', 'salesperson', 'created_at')
- list_filter = ('status', 'salesperson', 'created_at')
+ list_display = ('id', 'customer_name', 'get_item_name', 'total_price', 'status', 'salesperson', 'distributor', 'created_at')
+ list_filter = ('status', 'salesperson', 'distributor', 'created_at')
search_fields = ('id', 'customer_name', 'phone_number', 'wechat_trade_no')
readonly_fields = ('total_price', 'created_at', 'wechat_trade_no')
+ def get_item_name(self, obj):
+ if obj.config:
+ return f"[硬件] {obj.config.name}"
+ if obj.course:
+ return f"[课程] {obj.course.title}"
+ return "未知商品"
+ get_item_name.short_description = "购买商品"
+
fieldsets = (
('订单信息', {
- 'fields': ('config', 'quantity', 'total_price', 'status', 'created_at')
+ 'fields': ('config', 'course', 'quantity', 'total_price', 'status', 'created_at')
}),
('客户信息', {
- 'fields': ('customer_name', 'phone_number', 'shipping_address')
+ 'fields': ('customer_name', 'phone_number', 'shipping_address', 'wechat_user')
}),
('物流信息', {
'fields': ('courier_name', 'tracking_number')
}),
('销售归属', {
- 'fields': ('salesperson',)
+ 'fields': ('salesperson', 'distributor')
}),
('支付信息', {
'fields': ('wechat_trade_no',)
diff --git a/backend/shop/migrations/0021_commissionlog_distributor_order_distributor_and_more.py b/backend/shop/migrations/0021_commissionlog_distributor_order_distributor_and_more.py
new file mode 100644
index 0000000..c452b7a
--- /dev/null
+++ b/backend/shop/migrations/0021_commissionlog_distributor_order_distributor_and_more.py
@@ -0,0 +1,29 @@
+# Generated by Django 6.0.1 on 2026-02-10 19:08
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('shop', '0020_alter_vbcourse_course_type'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='commissionlog',
+ name='distributor',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='commissions', to='shop.distributor', verbose_name='获佣分销员'),
+ ),
+ migrations.AddField(
+ model_name='order',
+ name='distributor',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to='shop.distributor', verbose_name='所属分销员'),
+ ),
+ migrations.AlterField(
+ model_name='commissionlog',
+ name='salesperson',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='commissions', to='shop.salesperson', verbose_name='获佣销售员'),
+ ),
+ ]
diff --git a/backend/shop/migrations/0022_vbcourse_content_vbcourse_price_courseenrollment.py b/backend/shop/migrations/0022_vbcourse_content_vbcourse_price_courseenrollment.py
new file mode 100644
index 0000000..0c3faab
--- /dev/null
+++ b/backend/shop/migrations/0022_vbcourse_content_vbcourse_price_courseenrollment.py
@@ -0,0 +1,45 @@
+# Generated by Django 6.0.1 on 2026-02-10 19:20
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('shop', '0021_commissionlog_distributor_order_distributor_and_more'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='vbcourse',
+ name='content',
+ field=models.TextField(blank=True, help_text='支持Markdown或HTML', verbose_name='详细内容'),
+ ),
+ migrations.AddField(
+ model_name='vbcourse',
+ name='price',
+ field=models.DecimalField(decimal_places=2, default=0, help_text='0表示免费', max_digits=10, verbose_name='价格'),
+ ),
+ migrations.CreateModel(
+ name='CourseEnrollment',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('customer_name', models.CharField(max_length=100, verbose_name='姓名')),
+ ('phone_number', models.CharField(max_length=20, verbose_name='联系电话')),
+ ('email', models.EmailField(blank=True, max_length=254, verbose_name='电子邮箱')),
+ ('wechat_id', models.CharField(blank=True, max_length=50, verbose_name='微信号')),
+ ('message', models.TextField(blank=True, verbose_name='留言/备注')),
+ ('status', models.CharField(choices=[('pending', '待联系'), ('contacted', '已联系'), ('completed', '已完成'), ('cancelled', '已取消')], default='pending', max_length=20, verbose_name='状态')),
+ ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='提交时间')),
+ ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
+ ('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='enrollments', to='shop.vbcourse', verbose_name='咨询课程')),
+ ('distributor', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='shop.distributor', verbose_name='所属分销员')),
+ ('salesperson', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='shop.salesperson', verbose_name='所属销售员')),
+ ],
+ options={
+ 'verbose_name': '课程报名',
+ 'verbose_name_plural': '课程报名管理',
+ },
+ ),
+ ]
diff --git a/backend/shop/migrations/0023_order_course_alter_order_config.py b/backend/shop/migrations/0023_order_course_alter_order_config.py
new file mode 100644
index 0000000..48ff8a3
--- /dev/null
+++ b/backend/shop/migrations/0023_order_course_alter_order_config.py
@@ -0,0 +1,24 @@
+# Generated by Django 6.0.1 on 2026-02-10 19:23
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('shop', '0022_vbcourse_content_vbcourse_price_courseenrollment'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='order',
+ name='course',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to='shop.vbcourse', verbose_name='所选课程'),
+ ),
+ migrations.AlterField(
+ model_name='order',
+ name='config',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='shop.esp32config', verbose_name='所选配置'),
+ ),
+ ]
diff --git a/backend/shop/migrations/0024_vbcourse_instructor_avatar_and_more.py b/backend/shop/migrations/0024_vbcourse_instructor_avatar_and_more.py
new file mode 100644
index 0000000..1be9c01
--- /dev/null
+++ b/backend/shop/migrations/0024_vbcourse_instructor_avatar_and_more.py
@@ -0,0 +1,33 @@
+# Generated by Django 6.0.1 on 2026-02-10 19:28
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('shop', '0023_order_course_alter_order_config'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='vbcourse',
+ name='instructor_avatar',
+ field=models.ImageField(blank=True, null=True, upload_to='instructors/avatars/', verbose_name='讲师头像 (上传)'),
+ ),
+ migrations.AddField(
+ model_name='vbcourse',
+ name='instructor_avatar_url',
+ field=models.URLField(blank=True, null=True, verbose_name='讲师头像 (URL)'),
+ ),
+ migrations.AddField(
+ model_name='vbcourse',
+ name='instructor_desc',
+ field=models.TextField(blank=True, default='拥有多年开发经验,擅长...', verbose_name='讲师简介'),
+ ),
+ migrations.AddField(
+ model_name='vbcourse',
+ name='instructor_title',
+ field=models.CharField(default='资深讲师', max_length=50, verbose_name='讲师头衔'),
+ ),
+ ]
diff --git a/backend/shop/migrations/0025_vccourse_alter_courseenrollment_course_and_more.py b/backend/shop/migrations/0025_vccourse_alter_courseenrollment_course_and_more.py
new file mode 100644
index 0000000..9188f37
--- /dev/null
+++ b/backend/shop/migrations/0025_vccourse_alter_courseenrollment_course_and_more.py
@@ -0,0 +1,55 @@
+# Generated by Django 6.0.1 on 2026-02-10 19:44
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('shop', '0024_vbcourse_instructor_avatar_and_more'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='VCCourse',
+ 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='课程简介')),
+ ('course_type', models.CharField(choices=[('software', '软件课程'), ('hardware', '硬件课程'), ('incubation', '产品商业孵化')], default='software', max_length=20, verbose_name='课程类型')),
+ ('duration', models.CharField(default='30分钟', help_text='例如: 30分钟', max_length=50, verbose_name='课程时长')),
+ ('lesson_count', models.IntegerField(default=1, verbose_name='课时数量')),
+ ('instructor', models.CharField(default='VC讲师', max_length=50, verbose_name='讲师')),
+ ('instructor_title', models.CharField(default='资深讲师', max_length=50, verbose_name='讲师头衔')),
+ ('instructor_avatar', models.ImageField(blank=True, null=True, upload_to='instructors/avatars/', verbose_name='讲师头像 (上传)')),
+ ('instructor_avatar_url', models.URLField(blank=True, null=True, verbose_name='讲师头像 (URL)')),
+ ('instructor_desc', models.TextField(blank=True, default='拥有多年开发经验,擅长...', verbose_name='讲师简介')),
+ ('tag', models.CharField(blank=True, help_text='例如: 热门, 推荐, 进阶', max_length=20, verbose_name='标签')),
+ ('price', models.DecimalField(decimal_places=2, default=0, help_text='0表示免费', max_digits=10, verbose_name='价格')),
+ ('content', models.TextField(blank=True, help_text='支持Markdown或HTML', verbose_name='详细内容')),
+ ('cover_image', models.ImageField(blank=True, null=True, upload_to='courses/covers/', verbose_name='封面图 (上传)')),
+ ('cover_image_url', models.URLField(blank=True, null=True, verbose_name='封面图 (URL)')),
+ ('detail_image', models.ImageField(blank=True, null=True, upload_to='courses/details/', verbose_name='详情页长图 (上传)')),
+ ('detail_image_url', models.URLField(blank=True, help_text='如果填写了URL,将优先使用URL', null=True, verbose_name='详情页长图 (URL)')),
+ ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
+ ],
+ options={
+ 'verbose_name': 'VC课程',
+ 'verbose_name_plural': 'VC课程管理',
+ },
+ ),
+ migrations.AlterField(
+ model_name='courseenrollment',
+ name='course',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='enrollments', to='shop.vccourse', verbose_name='咨询课程'),
+ ),
+ migrations.AlterField(
+ model_name='order',
+ name='course',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to='shop.vccourse', verbose_name='所选课程'),
+ ),
+ migrations.DeleteModel(
+ name='VBCourse',
+ ),
+ ]
diff --git a/backend/shop/models.py b/backend/shop/models.py
index 2623222..fa31b82 100644
--- a/backend/shop/models.py
+++ b/backend/shop/models.py
@@ -165,7 +165,8 @@ class CommissionLog(models.Model):
)
order = models.ForeignKey('Order', on_delete=models.CASCADE, verbose_name="关联订单", related_name='commissions')
- salesperson = models.ForeignKey(Salesperson, on_delete=models.CASCADE, verbose_name="获佣销售员", related_name='commissions')
+ salesperson = models.ForeignKey(Salesperson, on_delete=models.CASCADE, verbose_name="获佣销售员", related_name='commissions', null=True, blank=True)
+ distributor = models.ForeignKey(Distributor, on_delete=models.CASCADE, verbose_name="获佣分销员", related_name='commissions', null=True, blank=True)
amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="佣金金额")
level = models.IntegerField(default=1, verbose_name="分销层级", help_text="1: 直接销售, 2: 二级分销")
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending', verbose_name="状态")
@@ -219,13 +220,15 @@ class Order(models.Model):
('cancelled', '已取消'),
)
- config = models.ForeignKey(ESP32Config, on_delete=models.CASCADE, verbose_name="所选配置")
+ config = models.ForeignKey(ESP32Config, on_delete=models.CASCADE, verbose_name="所选配置", null=True, blank=True)
+ course = models.ForeignKey('VCCourse', on_delete=models.SET_NULL, null=True, blank=True, verbose_name="所选课程", related_name='orders')
quantity = models.IntegerField(default=1, verbose_name="数量")
total_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="总价")
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending', verbose_name="订单状态")
# 销售归属
salesperson = models.ForeignKey(Salesperson, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="所属销售员", related_name='orders')
+ distributor = models.ForeignKey(Distributor, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="所属分销员", related_name='orders')
# 关联微信用户
wechat_user = models.ForeignKey(WeChatUser, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="下单微信用户", related_name='orders')
@@ -312,9 +315,9 @@ class ServiceOrder(models.Model):
verbose_name_plural = "服务订单列表"
-class VBCourse(models.Model):
+class VCCourse(models.Model):
"""
- VB Coding 课程模型
+ VC (VB Coding) 课程模型
"""
COURSE_TYPE_CHOICES = (
('software', '软件课程'),
@@ -327,10 +330,17 @@ class VBCourse(models.Model):
course_type = models.CharField(max_length=20, choices=COURSE_TYPE_CHOICES, default='software', verbose_name="课程类型")
duration = models.CharField(max_length=50, verbose_name="课程时长", help_text="例如: 30分钟", default="30分钟")
lesson_count = models.IntegerField(default=1, verbose_name="课时数量")
- instructor = models.CharField(max_length=50, verbose_name="讲师", default="VB讲师")
+ instructor = models.CharField(max_length=50, verbose_name="讲师", default="VC讲师")
+ instructor_title = models.CharField(max_length=50, verbose_name="讲师头衔", default="资深讲师")
+ instructor_avatar = models.ImageField(upload_to='instructors/avatars/', blank=True, null=True, verbose_name="讲师头像 (上传)")
+ instructor_avatar_url = models.URLField(blank=True, null=True, verbose_name="讲师头像 (URL)")
+ instructor_desc = models.TextField(blank=True, verbose_name="讲师简介", default="拥有多年开发经验,擅长...")
tag = models.CharField(max_length=20, blank=True, verbose_name="标签", help_text="例如: 热门, 推荐, 进阶")
+ price = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name="价格", help_text="0表示免费")
+ content = models.TextField(blank=True, verbose_name="详细内容", help_text="支持Markdown或HTML")
+
cover_image = models.ImageField(upload_to='courses/covers/', blank=True, null=True, verbose_name="封面图 (上传)")
cover_image_url = models.URLField(blank=True, null=True, verbose_name="封面图 (URL)")
@@ -343,5 +353,40 @@ class VBCourse(models.Model):
return self.title
class Meta:
- verbose_name = "VB课程"
- verbose_name_plural = "VB课程管理"
+ verbose_name = "VC课程"
+ verbose_name_plural = "VC课程管理"
+
+
+class CourseEnrollment(models.Model):
+ """
+ 课程报名/咨询记录
+ """
+ STATUS_CHOICES = (
+ ('pending', '待联系'),
+ ('contacted', '已联系'),
+ ('completed', '已完成'),
+ ('cancelled', '已取消'),
+ )
+
+ course = models.ForeignKey(VCCourse, on_delete=models.CASCADE, verbose_name="咨询课程", related_name='enrollments')
+ customer_name = models.CharField(max_length=100, verbose_name="姓名")
+ phone_number = models.CharField(max_length=20, verbose_name="联系电话")
+ email = models.EmailField(blank=True, verbose_name="电子邮箱")
+ wechat_id = models.CharField(max_length=50, blank=True, verbose_name="微信号")
+ message = models.TextField(blank=True, verbose_name="留言/备注")
+
+ status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending', verbose_name="状态")
+
+ # 销售归属
+ salesperson = models.ForeignKey(Salesperson, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="所属销售员")
+ distributor = models.ForeignKey(Distributor, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="所属分销员")
+
+ created_at = models.DateTimeField(auto_now_add=True, verbose_name="提交时间")
+ updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
+
+ def __str__(self):
+ return f"{self.customer_name} - {self.course.title}"
+
+ class Meta:
+ verbose_name = "课程报名"
+ verbose_name_plural = "课程报名管理"
diff --git a/backend/shop/serializers.py b/backend/shop/serializers.py
index 8c4d7e8..3e42a00 100644
--- a/backend/shop/serializers.py
+++ b/backend/shop/serializers.py
@@ -1,5 +1,23 @@
from rest_framework import serializers
-from .models import ESP32Config, Order, Salesperson, Service, VBCourse, ProductFeature, ServiceOrder, WeChatUser, Distributor, Withdrawal
+from .models import ESP32Config, Order, Salesperson, Service, VCCourse, ProductFeature, ServiceOrder, WeChatUser, Distributor, Withdrawal, CommissionLog, CourseEnrollment
+
+class CommissionLogSerializer(serializers.ModelSerializer):
+ """
+ 佣金记录序列化器
+ """
+ order_info = serializers.SerializerMethodField()
+
+ class Meta:
+ model = CommissionLog
+ fields = ['id', 'amount', 'level', 'status', 'created_at', 'order_info']
+ read_only_fields = ['id', 'amount', 'level', 'status', 'created_at', 'order_info']
+
+ def get_order_info(self, obj):
+ return {
+ 'order_id': obj.order.id,
+ 'total_price': obj.order.total_price,
+ 'customer_name': obj.order.customer_name
+ }
class WeChatUserSerializer(serializers.ModelSerializer):
class Meta:
@@ -69,6 +87,37 @@ class ServiceSerializer(serializers.ModelSerializer):
return obj.detail_image.url
return None
+class CourseEnrollmentSerializer(serializers.ModelSerializer):
+ """
+ 课程报名序列化器
+ """
+ course_title = serializers.CharField(source='course.title', read_only=True)
+ ref_code = serializers.CharField(write_only=True, required=False, allow_blank=True)
+
+ class Meta:
+ model = CourseEnrollment
+ fields = ['id', 'course', 'course_title', 'customer_name', 'phone_number', 'email', 'wechat_id', 'message', 'status', 'created_at', 'ref_code']
+ read_only_fields = ['status', 'created_at']
+
+ def create(self, validated_data):
+ ref_code = validated_data.pop('ref_code', None)
+
+ # 尝试关联销售员或分销员
+ if ref_code:
+ try:
+ salesperson = Salesperson.objects.get(code=ref_code)
+ validated_data['salesperson'] = salesperson
+ except Salesperson.DoesNotExist:
+ pass
+
+ try:
+ distributor = Distributor.objects.get(invite_code=ref_code)
+ validated_data['distributor'] = distributor
+ except Distributor.DoesNotExist:
+ pass
+
+ return super().create(validated_data)
+
class ServiceOrderSerializer(serializers.ModelSerializer):
"""
AI服务订单序列化器
@@ -101,16 +150,16 @@ class ServiceOrderSerializer(serializers.ModelSerializer):
return super().create(validated_data)
-class VBCourseSerializer(serializers.ModelSerializer):
+class VCCourseSerializer(serializers.ModelSerializer):
"""
- VB课程序列化器
+ VC课程序列化器
"""
display_cover_image = serializers.SerializerMethodField()
display_detail_image = serializers.SerializerMethodField()
course_type_display = serializers.CharField(source='get_course_type_display', read_only=True)
class Meta:
- model = VBCourse
+ model = VCCourse
fields = '__all__'
def get_display_cover_image(self, obj):
@@ -151,6 +200,7 @@ class OrderSerializer(serializers.ModelSerializer):
订单序列化器
"""
config_name = serializers.CharField(source='config.name', read_only=True)
+ course_title = serializers.CharField(source='course.title', read_only=True)
config_image = serializers.SerializerMethodField()
salesperson_name = serializers.CharField(source='salesperson.name', read_only=True)
salesperson_code = serializers.CharField(source='salesperson.code', read_only=True)
@@ -159,41 +209,76 @@ class OrderSerializer(serializers.ModelSerializer):
class Meta:
model = Order
- fields = ['id', 'config', 'config_name', 'config_image', 'quantity', 'total_price', 'status', 'created_at', 'updated_at', 'wechat_trade_no',
+ fields = ['id', 'config', 'config_name', 'config_image', 'course', 'course_title', 'quantity', 'total_price', 'status', 'created_at', 'updated_at', 'wechat_trade_no',
'customer_name', 'phone_number', 'shipping_address', 'ref_code', 'salesperson_name', 'salesperson_code', 'courier_name', 'tracking_number']
read_only_fields = ['total_price', 'status', 'wechat_trade_no', 'created_at', 'updated_at']
extra_kwargs = {
'customer_name': {'required': True},
'phone_number': {'required': True},
- 'shipping_address': {'required': True},
}
+ def validate(self, data):
+ # 如果是部分更新 (PATCH),可能不需要校验所有字段,但这里主要用于创建
+ if self.instance:
+ return data
+
+ config = data.get('config')
+ course = data.get('course')
+
+ if not config and not course:
+ raise serializers.ValidationError("必须选择一种商品(硬件配置或课程)")
+
+ if config and course:
+ raise serializers.ValidationError("一次只能购买一种类型的商品")
+
+ if config and not data.get('shipping_address'):
+ raise serializers.ValidationError({"shipping_address": "购买硬件产品需要填写收货地址"})
+
+ return data
+
def get_config_image(self, obj):
- if obj.config.static_image_url:
- return obj.config.static_image_url
- if obj.config.detail_image_url:
- return obj.config.detail_image_url
- if obj.config.detail_image:
- return obj.config.detail_image.url
+ if obj.config:
+ if obj.config.static_image_url:
+ return obj.config.static_image_url
+ if obj.config.detail_image_url:
+ return obj.config.detail_image_url
+ if obj.config.detail_image:
+ return obj.config.detail_image.url
+ elif obj.course:
+ if obj.course.cover_image_url:
+ return obj.course.cover_image_url
+ if obj.course.cover_image:
+ return obj.course.cover_image.url
return None
def create(self, validated_data):
"""
- 重写创建方法,自动计算总价并关联销售员
+ 重写创建方法,自动计算总价并关联销售员/分销员
"""
config = validated_data.get('config')
+ course = validated_data.get('course')
quantity = validated_data.get('quantity', 1)
ref_code = validated_data.pop('ref_code', None)
- validated_data['total_price'] = config.price * quantity
-
- # 尝试关联销售员
+ if config:
+ validated_data['total_price'] = config.price * quantity
+ elif course:
+ validated_data['total_price'] = course.price * quantity
+
+ # 尝试关联销售员或分销员
if ref_code:
+ # 1. 尝试查找旧版销售员
try:
salesperson = Salesperson.objects.get(code=ref_code)
validated_data['salesperson'] = salesperson
except Salesperson.DoesNotExist:
- # 如果找不到对应的销售员,忽略该推广码,仍继续创建订单(算作自然流量)
+ pass
+
+ # 2. 尝试查找新版分销员
+ try:
+ distributor = Distributor.objects.get(invite_code=ref_code)
+ validated_data['distributor'] = distributor
+ except Distributor.DoesNotExist:
pass
return super().create(validated_data)
diff --git a/backend/shop/urls.py b/backend/shop/urls.py
index 3464a6d..bc81dae 100644
--- a/backend/shop/urls.py
+++ b/backend/shop/urls.py
@@ -2,15 +2,17 @@ from django.urls import path, include, re_path
from rest_framework.routers import DefaultRouter
from .views import (
ESP32ConfigViewSet, OrderViewSet, order_check_view,
- ServiceViewSet, VBCourseViewSet, ServiceOrderViewSet,
- payment_finish, pay, send_sms_code, wechat_login, update_user_info, DistributorViewSet
+ ServiceViewSet, VCCourseViewSet, ServiceOrderViewSet,
+ payment_finish, pay, send_sms_code, wechat_login, update_user_info, DistributorViewSet,
+ CourseEnrollmentViewSet
)
router = DefaultRouter()
router.register(r'configs', ESP32ConfigViewSet)
router.register(r'orders', OrderViewSet)
router.register(r'services', ServiceViewSet)
-router.register(r'courses', VBCourseViewSet)
+router.register(r'courses', VCCourseViewSet)
+router.register(r'course-enrollments', CourseEnrollmentViewSet)
router.register(r'service-orders', ServiceOrderViewSet)
router.register(r'distributor', DistributorViewSet, basename='distributor')
diff --git a/backend/shop/views.py b/backend/shop/views.py
index 3982f2e..b9d6dd8 100644
--- a/backend/shop/views.py
+++ b/backend/shop/views.py
@@ -5,8 +5,8 @@ from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponse
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter, OpenApiExample
-from .models import ESP32Config, Order, WeChatPayConfig, Service, VBCourse, ServiceOrder, Salesperson, CommissionLog, WeChatUser, Distributor, Withdrawal
-from .serializers import ESP32ConfigSerializer, OrderSerializer, ServiceSerializer, VBCourseSerializer, ServiceOrderSerializer, WeChatUserSerializer, DistributorSerializer, WithdrawalSerializer
+from .models import ESP32Config, Order, WeChatPayConfig, Service, VCCourse, ServiceOrder, Salesperson, CommissionLog, WeChatUser, Distributor, Withdrawal, CourseEnrollment
+from .serializers import ESP32ConfigSerializer, OrderSerializer, ServiceSerializer, VCCourseSerializer, ServiceOrderSerializer, WeChatUserSerializer, DistributorSerializer, WithdrawalSerializer, CommissionLogSerializer, CourseEnrollmentSerializer
from django.core.signing import TimestampSigner, BadSignature, SignatureExpired
from django.contrib.auth.models import User
from wechatpayv3 import WeChatPay, WeChatPayType
@@ -215,6 +215,7 @@ def pay(request):
# 1. 获取并验证请求参数
good_id = request.data.get('goodid')
+ order_type = request.data.get('type', 'config') # 默认为 config
quantity = int(request.data.get('quantity', 1))
customer_name = request.data.get('customer_name')
phone_number = request.data.get('phone_number')
@@ -237,15 +238,23 @@ def pay(request):
return Response({'error': f'支付配置错误: {error_msg}'}, status=status.HTTP_400_BAD_REQUEST)
# 3. 查找商品和销售员,创建订单
- try:
- product = ESP32Config.objects.get(id=good_id)
- except ESP32Config.DoesNotExist:
- print(f"商品不存在: {good_id}")
- return Response({'error': f'找不到 ID 为 {good_id} 的商品'}, status=status.HTTP_404_NOT_FOUND)
+ product = None
+ if order_type == 'course':
+ try:
+ product = VBCourse.objects.get(id=good_id)
+ except VBCourse.DoesNotExist:
+ print(f"课程不存在: {good_id}")
+ return Response({'error': f'找不到 ID 为 {good_id} 的课程'}, status=status.HTTP_404_NOT_FOUND)
+ else:
+ try:
+ product = ESP32Config.objects.get(id=good_id)
+ except ESP32Config.DoesNotExist:
+ print(f"商品不存在: {good_id}")
+ return Response({'error': f'找不到 ID 为 {good_id} 的商品'}, status=status.HTTP_404_NOT_FOUND)
- # 检查库存
- if product.stock < quantity:
- return Response({'error': f'库存不足,仅剩 {product.stock} 件'}, status=status.HTTP_400_BAD_REQUEST)
+ # 检查库存 (仅针对硬件)
+ if product.stock < quantity:
+ return Response({'error': f'库存不足,仅剩 {product.stock} 件'}, status=status.HTTP_400_BAD_REQUEST)
salesperson = None
if ref_code:
@@ -255,24 +264,34 @@ def pay(request):
total_price = product.price * quantity
amount_in_cents = int(total_price * 100)
- order = Order.objects.create(
- config=product,
- quantity=quantity,
- total_price=total_price,
- customer_name=customer_name,
- phone_number=phone_number,
- shipping_address=shipping_address,
- salesperson=salesperson,
- status='pending'
- )
+ order_kwargs = {
+ 'quantity': quantity,
+ 'total_price': total_price,
+ 'customer_name': customer_name,
+ 'phone_number': phone_number,
+ 'shipping_address': shipping_address,
+ 'salesperson': salesperson,
+ 'status': 'pending'
+ }
+
+ if order_type == 'course':
+ order_kwargs['course'] = product
+ else:
+ order_kwargs['config'] = product
- # 扣减库存
- product.stock -= quantity
- product.save()
+ order = Order.objects.create(**order_kwargs)
+
+ # 扣减库存 (仅针对硬件)
+ if order_type != 'course':
+ product.stock -= quantity
+ product.save()
# 4. 调用微信支付接口
out_trade_no = f"PAY{order.id}T{int(time.time())}"
- description = f"购买 {product.name} x {quantity}"
+ if order_type == 'course':
+ description = f"报名 {product.title}"
+ else:
+ description = f"购买 {product.name} x {quantity}"
# 保存商户订单号到数据库,方便后续查询
order.out_trade_no = out_trade_no
@@ -459,13 +478,19 @@ def payment_finish(request):
order.save()
print(f"订单 {order.id} 状态已更新")
- # 计算佣金
+ # 计算佣金 (旧版销售员系统)
try:
salesperson = order.salesperson
if salesperson:
# 1. 计算直接佣金 (一级)
# 优先级: 产品独立分润比例 > 销售员个人分润比例
- rate_1 = order.config.commission_rate if order.config.commission_rate > 0 else salesperson.commission_rate
+ rate_1 = 0
+ if order.config:
+ rate_1 = order.config.commission_rate if order.config.commission_rate > 0 else salesperson.commission_rate
+ elif order.course:
+ # 课程暂时使用销售员默认比例
+ rate_1 = salesperson.commission_rate
+
amount_1 = order.total_price * rate_1
if amount_1 > 0:
@@ -476,7 +501,7 @@ def payment_finish(request):
level=1,
status='pending'
)
- print(f"生成一级佣金: {salesperson.name} - {amount_1}")
+ print(f"生成一级佣金(Salesperson): {salesperson.name} - {amount_1}")
# 2. 计算上级佣金 (二级)
parent = salesperson.parent
@@ -492,9 +517,61 @@ def payment_finish(request):
level=2,
status='pending'
)
- print(f"生成二级佣金: {parent.name} - {amount_2}")
+ print(f"生成二级佣金(Salesperson): {parent.name} - {amount_2}")
+
+ # 计算佣金 (新版分销员系统)
+ distributor = order.distributor
+ if distributor:
+ # 1. 计算直接佣金 (一级)
+ # 优先级: 产品独立分润比例 > 分销员个人分润比例
+ rate_1 = 0
+ if order.config:
+ rate_1 = order.config.commission_rate if order.config.commission_rate > 0 else distributor.commission_rate
+ elif order.course:
+ # 课程暂时使用分销员默认比例
+ rate_1 = distributor.commission_rate
+
+ amount_1 = order.total_price * rate_1
+
+ if amount_1 > 0:
+ CommissionLog.objects.create(
+ order=order,
+ distributor=distributor,
+ amount=amount_1,
+ level=1,
+ status='settled' # 简化流程,直接结算到余额
+ )
+ # 更新余额
+ distributor.total_earnings += amount_1
+ distributor.withdrawable_balance += amount_1
+ distributor.save()
+ print(f"生成一级佣金(Distributor): {distributor.user.nickname} - {amount_1}")
+
+ # 2. 计算上级佣金 (二级)
+ parent = distributor.parent
+ if parent:
+ # 二级固定比例 2% (0.02)
+ rate_2 = 0.02
+ amount_2 = order.total_price * models.DecimalField(max_digits=5, decimal_places=4).to_python(rate_2)
+
+ if amount_2 > 0:
+ CommissionLog.objects.create(
+ order=order,
+ distributor=parent,
+ amount=amount_2,
+ level=2,
+ status='settled'
+ )
+ # 更新余额
+ parent.total_earnings += amount_2
+ parent.withdrawable_balance += amount_2
+ parent.save()
+ print(f"生成二级佣金(Distributor): {parent.user.nickname} - {amount_2}")
+
except Exception as e:
print(f"佣金计算失败: {str(e)}")
+ import traceback
+ traceback.print_exc()
except Exception as e:
print(f"订单更新失败: {str(e)}")
@@ -508,15 +585,22 @@ def payment_finish(request):
return HttpResponse(str(e), status=500)
@extend_schema_view(
- list=extend_schema(summary="获取VB课程列表", description="获取所有可用的VB课程"),
- retrieve=extend_schema(summary="获取VB课程详情", description="获取指定VB课程的详细信息")
+ list=extend_schema(summary="获取VC课程列表", description="获取所有可用的VC课程"),
+ retrieve=extend_schema(summary="获取VC课程详情", description="获取指定VC课程的详细信息")
)
-class VBCourseViewSet(viewsets.ReadOnlyModelViewSet):
+class VCCourseViewSet(viewsets.ReadOnlyModelViewSet):
"""
- VB课程列表和详情
+ VC课程列表和详情
"""
- queryset = VBCourse.objects.all().order_by('-created_at')
- serializer_class = VBCourseSerializer
+ queryset = VCCourse.objects.all().order_by('-created_at')
+ serializer_class = VCCourseSerializer
+
+class CourseEnrollmentViewSet(viewsets.ModelViewSet):
+ """
+ 课程报名管理
+ """
+ queryset = CourseEnrollment.objects.all().order_by('-created_at')
+ serializer_class = CourseEnrollmentSerializer
def order_check_view(request):
"""
@@ -989,3 +1073,64 @@ class DistributorViewSet(viewsets.GenericViewSet):
return Response({'message': 'Withdrawal request submitted'})
+ @action(detail=False, methods=['get'])
+ def earnings(self, request):
+ """查看个人分销金额及明细"""
+ user = get_current_wechat_user(request)
+ if not user or not hasattr(user, 'distributor'):
+ return Response({'error': 'Unauthorized'}, status=401)
+
+ distributor = user.distributor
+ logs = CommissionLog.objects.filter(distributor=distributor).order_by('-created_at')
+
+ page = self.paginate_queryset(logs)
+ if page is not None:
+ serializer = CommissionLogSerializer(page, many=True)
+ return self.get_paginated_response(serializer.data)
+
+ serializer = CommissionLogSerializer(logs, many=True)
+ return Response(serializer.data)
+
+ @action(detail=False, methods=['get'])
+ def team(self, request):
+ """查看团队(二级分销情况)"""
+ user = get_current_wechat_user(request)
+ if not user or not hasattr(user, 'distributor'):
+ return Response({'error': 'Unauthorized'}, status=401)
+
+ distributor = user.distributor
+
+ # 直推下级
+ children = Distributor.objects.filter(parent=distributor)
+ children_data = DistributorSerializer(children, many=True).data
+
+ # 二级分销收益统计
+ second_level_earnings = CommissionLog.objects.filter(distributor=distributor, level=2).aggregate(total=models.Sum('amount'))['total'] or 0
+
+ return Response({
+ 'children_count': children.count(),
+ 'children': children_data,
+ 'second_level_earnings': second_level_earnings
+ })
+
+ @action(detail=False, methods=['get'])
+ def orders(self, request):
+ """查看分销订单"""
+ user = get_current_wechat_user(request)
+ if not user or not hasattr(user, 'distributor'):
+ return Response({'error': 'Unauthorized'}, status=401)
+
+ distributor = user.distributor
+ # 查找我赚了钱的订单
+ commission_logs = CommissionLog.objects.filter(distributor=distributor).select_related('order')
+ order_ids = commission_logs.values_list('order_id', flat=True)
+ orders = Order.objects.filter(id__in=order_ids).order_by('-created_at')
+
+ page = self.paginate_queryset(orders)
+ if page is not None:
+ serializer = OrderSerializer(page, many=True)
+ return self.get_paginated_response(serializer.data)
+
+ serializer = OrderSerializer(orders, many=True)
+ return Response(serializer.data)
+
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index f44e2cf..e8bb771 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -6,7 +6,8 @@ import ProductDetail from './pages/ProductDetail';
import Payment from './pages/Payment';
import AIServices from './pages/AIServices';
import ServiceDetail from './pages/ServiceDetail';
-import VBCourses from './pages/VBCourses';
+import VCCourses from './pages/VCCourses';
+import VCCourseDetail from './pages/VCCourseDetail';
import MyOrders from './pages/MyOrders';
import 'antd/dist/reset.css';
import './App.css';
@@ -19,7 +20,8 @@ function App() {
+ * 提交后我们的顾问将尽快与您联系确认 +
+请填写您的联系方式,我们将为您安排课程顾问。
+