From 1100143a6ebc73dc773c9f3a7ee72199a135bb37 Mon Sep 17 00:00:00 2001 From: jeremygan2021 Date: Wed, 11 Feb 2026 04:06:51 +0800 Subject: [PATCH] finish --- .gitignore | 2 +- README.md | 220 -------------- backend/check_urls.py | 2 +- .../__pycache__/settings.cpython-312.pyc | Bin 5768 -> 5768 bytes .../__pycache__/settings.cpython-313.pyc | Bin 5506 -> 5772 bytes backend/config/settings.py | 4 +- backend/db.sqlite3 | Bin 262144 -> 303104 bytes .../shop/__pycache__/admin.cpython-312.pyc | Bin 15328 -> 16615 bytes .../shop/__pycache__/admin.cpython-313.pyc | Bin 15427 -> 16823 bytes .../shop/__pycache__/models.cpython-312.pyc | Bin 24075 -> 27221 bytes .../shop/__pycache__/models.cpython-313.pyc | Bin 22716 -> 26639 bytes .../__pycache__/serializers.cpython-312.pyc | Bin 11028 -> 15085 bytes .../__pycache__/serializers.cpython-313.pyc | Bin 11162 -> 15858 bytes backend/shop/__pycache__/urls.cpython-312.pyc | Bin 1566 -> 1672 bytes backend/shop/__pycache__/urls.cpython-313.pyc | Bin 1521 -> 1630 bytes .../shop/__pycache__/views.cpython-312.pyc | Bin 45696 -> 51895 bytes .../shop/__pycache__/views.cpython-313.pyc | Bin 45764 -> 52011 bytes backend/shop/admin.py | 62 +++- ..._distributor_order_distributor_and_more.py | 29 ++ ...content_vbcourse_price_courseenrollment.py | 45 +++ .../0023_order_course_alter_order_config.py | 24 ++ ...024_vbcourse_instructor_avatar_and_more.py | 33 ++ ..._alter_courseenrollment_course_and_more.py | 55 ++++ backend/shop/models.py | 59 +++- backend/shop/serializers.py | 119 ++++++-- backend/shop/urls.py | 8 +- backend/shop/views.py | 213 ++++++++++--- frontend/src/App.jsx | 6 +- frontend/src/api.js | 4 +- frontend/src/components/Layout.jsx | 2 +- frontend/src/pages/VCCourseDetail.jsx | 285 +++++++++++++++++ .../pages/{VBCourses.jsx => VCCourses.jsx} | 18 +- miniprogram/src/pages/courses/detail.scss | 286 ++++++++++++++---- miniprogram/src/pages/courses/detail.tsx | 110 +++++-- miniprogram/src/pages/courses/index.tsx | 2 +- miniprogram/src/pages/order/checkout.tsx | 36 ++- 36 files changed, 1223 insertions(+), 401 deletions(-) delete mode 100644 README.md create mode 100644 backend/shop/migrations/0021_commissionlog_distributor_order_distributor_and_more.py create mode 100644 backend/shop/migrations/0022_vbcourse_content_vbcourse_price_courseenrollment.py create mode 100644 backend/shop/migrations/0023_order_course_alter_order_config.py create mode 100644 backend/shop/migrations/0024_vbcourse_instructor_avatar_and_more.py create mode 100644 backend/shop/migrations/0025_vccourse_alter_courseenrollment_course_and_more.py create mode 100644 frontend/src/pages/VCCourseDetail.jsx rename frontend/src/pages/{VBCourses.jsx => VCCourses.jsx} (90%) 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 c8aed22fd73879393f2c5fdd077a2c70726b1001..7392c167fa6c84f5264e9a14628ac8c8364aeb3a 100644 GIT binary patch delta 32 mcmeCs?a<{q&CAQh00i7U8@Yn{7@a4_@yReIZ?5L!jx_U delta 32 mcmeCs?a<{q&CAQh00eb)8@Yn{7@a1^@yReIZLa3y%Wo1v90%}S9=4&7w6!%&j4)yhr7R2BQrlXsR;vO<9^baUut9_&R^(|C7wuwTd=TJ=hya2J;T$G0g)pXZUK|MW zuwn+Ybe_Y7jML3wKFoI-;-YBAB`h$#+@n{p$n>gF17=(k%?&KkX4i3pCEPS7Ac82< zSdT6v&h(a1JI#=wy^=_=c-j~Tu)=h;N7vA}&G=3a-^D$q_l+8`==Vj29`OL{ES@#S z@enzt8%7P7^eS4Lr?myqD!X8n35xU;H&H@)EXdPG-3;)kU`sUNk!X=mmrUMjGh=$* zKj`J#cDeYhC@aNmHCKPp{B9FWd`RUT9|ir3*CLBc`=7r2{IJ_TrsT_#RIQU-;c;bq zPAN-IvU)f#)nui)=Xh(f$7w~|B1_Va*WKJ1tn)(yg{|CnNfK*vRgv8^to|B$X{*tN z{;Nv5SsQ-J_x&B?__GK7>i+RWc#jC*{0WUD|MXc#PaN1e_hgGqF->YD)z#@18Skq4 zZsg?bLW@jvrJEz*AM&ex+kE->PXtgWGa-tfEb#sC(A}~#9ByH3gragD0xgn-$ ejX?c9;&QRwibhtAm7V)VG!j+G=(e_~3-# zM6fIqNLHUEI0fEF-~4BRLfNi6lDbBV%E(@!i9`X zA7m!467g5Qq2Tn9okdja0}q3e@eKu&8w$>FzS#{04IqEAo~S%f9YS2^hJyAD1$($U zARmYvf$F5q4Q?pt-%zlH%K`a7ITvKU1+tw$dBYnDu9M%0$_w96a7R|Bbwj~svX)pJ IOCiwN0Em*8{{R30 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 0cf1b4eb81ea5975e6769acb65ebe60e95b6e214..8be5396adf32e2ffa9ec33c7675d0d62820ddba4 100644 GIT binary patch delta 8051 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 oVN#LiW+*gx!#|8t{`;A9k(2h);M1& delta 1265 zcmYk5eQXp(6u{@}ZuWL>_wDs)ucZa25rO;R` z1{=ACLWLq9-N+OJ38JYWSfvMU08N|fA13%wqX86c=qB^qH*enC z_cfaw&PXQBUAdkSgiyu53(NPB-6iF@BWPxOk3;S#+>k^Mz%BS2ZfHU68>7ZVghmKK9b^GAI*p%=cT#0#g@)B?mN5^@oRZr7a0hlu z_Xw@CcttP&oF1ev(YjR393*Ud#DT0QEAv@VjTAa6gRE$=6#Au*U+qAt(aNwp?d(ce zytbDga`n18TnSn3XUak^!y)W#Z;G$4Z-0K}+81AFi$^jUo`Jm$@zwEl@%!4Ed)k$W z*qX-ww2LzsE=(PLdRcoqf-%qHqrLobquQu6=E(^A^(+U)*c(Z+Ce6hYOhtb<7NaK4 zvv~!+Ic;)?T$AmFQwUDU%kvjpm&5-NF2OnY8qU~L58&6OnM<4+!c`Nc zdX1#u!#IeOvdTJxnO=tMt}k&C=egA)1Ou?d=+uwv&3d7GpWCbL&>ZTJS|p7X)h_xT zUn$hD!7p$XF2Y&334fl*BA-H#r@O^7CpyF@rRhyGV}hzDP4h#OD7Cgy!S2A^J_Orf zk}+UBs$bSS^c?qLcZGINTca_xTQyvJTvL^|l*xQA&vU-*EO87v=CTnsoA${>mQ%a0 zja<>h^?K$Z>r9cgkravJ^(+$9m13l!Kaev(kHJ#oxb9NVkYeRAyvvm#)eDqfC8|Vs zKQc)?m?TR@?kXA(+v9YhD>ue{%fh&Go`@t#w)klk^@zb$v{_VdB75zEF0vKdSGJM$ zj49n{ig

$#Kqq&gqMmia-<2-qS@2nCxzkoN>3hY0e7n3y1L*p9nS5VjQ-gXr%dA zCtheumzrq^+gn=b2I8Eb z_>9pPzZNd+5JIw a8{^`^F;;tm(c92JO8qH+at!2;@Basotxn4T diff --git a/backend/shop/__pycache__/admin.cpython-312.pyc b/backend/shop/__pycache__/admin.cpython-312.pyc index db1d0fe1c9ae09b5c268f267d4ca31806e65b7ee..8285aa9b94bc242e93fcfd7551507efd44b5d8ee 100644 GIT binary patch delta 5748 zcmahN4OCm@^}YNO5(p3=;V%RVLMR1FftG5!Vh_c&B`xYYrPGk+J=(M(X}<(2om+za zwVwXyXSM!JTGu~H*Vfh6YOPj}^PC;GiF2Fmol|u?4PfM&g40P6Lj9J<}7%| zd$ZeenscHwYqM3*i~QR8xODu-X&ZSL3{yztjY=d;Az{uR5m9groRGMM6O5vE?IP+b zGtpo0amE5hPXc-}&=-%;b8chw6rh{L_+F)~rMXDK!A8sJi+rut9>xC&cu5q`SJY~! zNAXpFmyXCQYO`j@_=r8RrwU!U^|pz=;F){WMG8KT{#^SIZ##2CTgj)CfDU2@AgLQg!Q&#ObdE0X zHb$-jvYnUs61#?!p7d@@F6yOR;yU`=87J~sY3k73lsT#$gi`-7X(9f@7)P=xz*i{oPLh^*Q zq!wwlw8fAin^H?dh9~Ea*&g}H2o?a4)Z#-y;v#iO7;gosM_N5?Ot8wD>gk4ro~kCG z+7)COGM58rHvouqzc%pviO;s~8oYRt>9Q+h5~PVb60d6577cX1@t!I~C9T`*3IxP} ztl+s9hE8vm71()lHwZX-v}4ape`l;sn_!^>#E65Mk~(Dz9n@vYpuI`QSBuD50{~1F z5N`l*GiK_QHxCYUosvye%Mz_9wiBJoLR}Z{`X>+l>Gshn|fbf8xqTI&^l+z-xQSJld2}-83Nz7%EBC z>L+dFLDb{5 zyqkz%zu(ZMh7*dZIkWlp!?Hanzj+BkjPt*`3Qoo2%^+5;$766o~7B$$aGhFUIQ~! zB}=n)G7kjgfS!DDV8<(RKp`ewK$i>H7H&6>^>yGcq|u~2SnbwZoRXOCk+lk{%Z>YTyO z^&)A7ov2_b2}NBQ3-*)1Art@v`w7HH31jj=JtjMt5H4U!S>6ww{B7j?j!_?@1}Vwo z3j|5KJLo6QpeN`RB_mK8Mo5Afa7)Q!WLzCCXlhd04@k~-;%&$DmUoF9dO5Or4>hI~ zm#}ieq8&Q*#^ABXhE5$B*t`{v@e~@j4~@{e{k|Y{d3wuI$`d*x!xF-Lb=b%C4it|i zD=V|fGWKzlL$DXV0Sx9A?o(4BytF^HF5K9kQWIYCsm|PQNbO5+99Akanynnw$K)cc+_0@Z^-Q$fl&bKO{?t3ejk2@Rt#h#J@%I6= zB^cvl&h2J1AS?4*UZ5FSGc*EfJxZ5m{hQCFi?XMc763ug5^+Pj7zj#n@F1N5F-X`< zR{_&zvxVic{u14nozK5b&u8Dsm(W|;1#zc=53RI;7UjH}${d5u2rdljpg-vHlJoRN zPAy+R%d9yXykU@Iw8@&s|DFb{O`u{!ZU+6>T4p^4EN~2(LM{RbC;-@O@~$UnNv_@c zK9WrU!d$=6^pP?517q$1<%h{xU7j&*n?c)aQS%(4b-D9lE%1DQ}@q7uM)y6OsBZ?Jq3RGuJ?vxlQ?$&(Dh6 zK%8#BApV0^-1g=I)=Jl+a2)_SId>n6y!vM3mBW#(uYmjU5=d}4V_rG`?BIo-!3*6( zCl7*^Cun(*RZZ3bn?6{yFoaFSo;+#0ZvhtBgWx3uIs_ORasq&q99ZKgK_@mc;#nVr z+kotW9+Vh0L7sgzNSh5{?5*$!64eM_r-j8|%46^deP5SBEA7kf*OD*_umN}oFiGQT z^SAqglG-bFh+esNSsklo{EKno(TWB$U0yK&BOn0R6*ll_c}-48()o!X5+~F*sTO<>m8YLxN5lE8?qI#Bd)mR_4-z8L1%wrY4(|@qp9RFGDAcjLk6G*9AaIfNQtY6BOIz8v+)we7Kl5nCIo2 z%Kfn*$zE55cjw&xH0%CA_oinyb%&k_^;ygNOci~GiiymRa?3x+E$_2d^qFS%8D{nybJ&Dr zmJYrAn=7xLmMtZZqG`AR{4Dz#@dE&`dAoc;nr{G_leK)pKMf%VY)l4mOfB{`H~{S^|WAwdjM| zAUqgB6bP7{nkadCYet5KrS1-zS@wsNw_xSCp8q}lSJ^cFM>M+}=R`yKVY>~qmdoGW4e+`Qel{w$s`VKL;Wl$ zS1#g@%xF-Qcmaf#NjhW4kdx=+p`lYFu~ov6K!qBC2EqNXCs~PioSUpBo<8^4(=W>^ zp~=Yy2d2ZWpETGlxSU0t1^at^Wx?i9xeH$>MEqN%A(#Ua0bB)rk*=D#OAgn!(QrMd zVJ&(KJykh-g&Z|05gh@2-Q!-ze$1qU25>Y>_0fh77qL{_I}L!Ok4{s(SH})R5>Q}( z-Z5+Gv`b(Bj8R6xtRQaz7#Cm@?VXj+ucCjNm1SmOLO(^`V1D0!cJSzFVt%5^Vy;6) zs;FvI6t%y+%B)|9@3K|2smfe~h7$_{BZ4FV4!hz1Ihejsm7y`=f+2db%4~I^)(PHC zK{kSBdadeNE!qpOv`=TDz14m*n`p7tFbr(CCVUB=$e^og8j3F=K}inbvj5={EB$*- z0sk=lt|kw1-Zpz$s16sZN32lhWv zJqpGVaW&k{q1+rO?ncmo01Jfu9%8q0!mcw{k;WPr%W39%X5jdkNO5z>% delta 4640 zcma)9dvKK1760zHkGyuXdA}g+ZW2f~fQIl4q#+;%MG`=Zby8)!?DvH%Zg#`{HcBjY zH^@t^0v;StL_iUZTCk0R)=_5qhjhly6w0)wpQVHj*@O;iDl*zmr}muth0Vj*X(s#I zbI(2Z-1EBU-W}*-eGjL9Y`3RM^cQ_HaCY|I^a3_#Z~4wBlN3ow2^KXM;dg3qQrqO_ z$-}(j=3*sHWgBb7vN7Ow-W)2n?~!mNB<$ZK;YJB?FiW~DEtiz^ZIa?plQu4c5S!#E z6}$}OWg_nm;uTL7ye#BptEq384E@d1WC`^*!EsiTR547)Sq)rbR@Ma5VGjsN=NUmmC9yFX+An2AxRSEd`ba*<& zy+*KSBiqAtR_U?wnIv}$!L0-oaZdBP-Rt!SeVXQ1gF&yioAGkwY7C)X=;ehZTKG(= z(<+n}!nNszJ$0!bW_8g_8MLK6y?mVdG@Q{bZDBeiNk8dNl)M^Ab)rgNTgr&B+wSv18=bJp{@pcn?+*-bXcm$>R(QF9Yq_1V0CG|@Q!PsO0{L!{= z-8d^VY2ji7-J))ea38NH##qbvGU5>sU!x`4q+VN5?Dk%p9l^e~tHIb}8T8F6gg*P8 zIjd2~Bl9Nw;;Rrk$fo|ozmC7~=GE;_4SaAyY~(SGNbuEgEdB4+dRox{`yBVMdKh%L zvezJ^$&)lX3NR_7CU=~TjkK;20vVk~jvC?efu1Q5CN*f)OB=hyZUTsvZHeU~r)$$ITSj zpHs)~gg@oX%UDYy4-y0k!Z10vZ1y<&h0(&^i7eD;od#O&4#PGLzRmsRvIugqu)An7 zO0bn+nENxLA4Sk@?XBUE>J4?Ytyeh~Xqzt(L}Oz@7{1DLFRw=u6$u?en=yx6Bf%$L zitjpYBp8!sDD{Wi+I^u;Z^+lC^2bn=Z-e{uOMZywp3G?KP99xrG(R>kx&z~>ghRnj z@9?pn$nh<-a0~n)KVUe%MI@!qUJlin1=VSa?FG3-ZZS7oI~|@3-G1-V+rk}OQt{!^HBkO3(Lr_&z6b;in6-I|BZ?X33(RZ#sd#3;3 zhoVl#omP?`DH2iM69sHH$q|r=+D3$JQeXDQ*vj6VC9%fdta-7!`cm?HEw-FLFPdx5cVt?-?lk((?w-q&^B7wX-xSZZinIP4OrP>kHU*|SOJ`7T)RVZnsYBHw zdNO|TPECy%-QJ9oE|)82H2jw#;w)ir!hYvc<^kPVmi!XVvmUtHy#`gRE-8c$+_y}59;doxPNwja2%3!Gav8h6 z2q`6=Denh-ODsWJH+Ar!W_GGTFrlmCiV zGA^|AAU($9L-1+Y^eJbFJ<4SFd58N*=>^BMbIH@Vr>AV{x3ZON=BL0@v6AJ(LlyJV z5;5TK!s&|2G!eBIQ0kEq=qsC)yor1Lczb>f4$ql-aUmu;mMGr%)35cvcc%aJEB)I~ zV`dJKep)Ujue|&G!28_;?;jpKaTJYz5z;HCWanjy(}(Ro)h*TJ~g4 zc;-c0dx{jlOproA8RRb_=$Tq;m`A*ds`+`KJ%UP&ARQ!$ggHjzo=d!W2&11B2Z(7Q zJPzMfer`011NgXTO}28;2DgH5Ms`$`+vuKoi560`GJ2+rruc%Y)~<35JrkWxPYH8H zLBH$S#&$SS24^wJ?-0-I)*8$Ms8=E&}b%l1j& zoNtANc|{G1O|dHW0Bezzbfg?awOE>yIx{?(dg>~2&S;h?MyMXeEp@9u9BK({Fs=x) z7`m%m(UC}*3^zs?=8?tXF3=qhtNvDB#EWks1+Shk1}}zsM4Nke8Sr%9&Z*HG}ADEJMrRj_cOlifwl6AsPFg^B_j z9GRX1XJ=&_dANe_w@b4`rOEs=)TCR(?P>^1$gEp8s3D9wAENCj$wuv2_?Hq4RTo-C zVQt5z^Si9?bcxU5v)QH04ViPOv((Oc#d94MjA=(n{?>57j}aNulD?22b#4hOhnaI{ z+(N}BE~R+v)&5;?jILtc+@YauWHSGn;JO4m=Q^VaV?~??PaGROc{9!?G#mxLBDg_t zA3-z14SE~K_n*4D|7{~DSp9}~$ZztbeSfQXfK-Tn@i|eSmJ@zKFwu>7jV1_+VCB36 zMh32725O*m{!gvqw06O<`Sb5J5~8P*k9fKR{s+a|#e(_97>a|LINMm1CggK7f}S?q zL%rDRM_l0lrj2`H)`FF#qPfckM?~*Cxs&0@f|BB8s7}kmb#y6Z?cV(O=1&~1^A6X! zWEb8^mlotkttcU$1pT{qT-_Jz-+N%}!`j%Cb~ZdiN+cz|@6^D-C*sGq4!*hzz4fp;YVAAV$|C2h{68a6=}j{n)J+uYqcLaev8w(90>D# zQg;qM`oY3zA@Ncg>8Ef5{m!AEJjSn^WUg*N@A2q163<7V5o{r#*B}>{BwZ|`$BFYI zf-fu@cMIvswfKX(BdFGKH=2b1k~T-KlB6LMW9*{jx+r-B7F?35FG^FtmL;}P#)gqV z>`QE8&)an=EH9f`_pG=s;n#JOBo__INDWoOzZU-_FAt}Ru1okuQ~5(OQbSd+sh*oD G9{&ZNw2uh@ diff --git a/backend/shop/__pycache__/admin.cpython-313.pyc b/backend/shop/__pycache__/admin.cpython-313.pyc index b314ad664b84c63d6636ad12aa4c0edb20d9f7d3..b311a91e27a8f138755065d6011cf727f106aeb8 100644 GIT binary patch delta 5773 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 diff --git a/backend/shop/__pycache__/models.cpython-312.pyc b/backend/shop/__pycache__/models.cpython-312.pyc index 1961d41e9147f6129627e97af892d702b91a3389..01bc3a19f9b7a9a46e28edd7cbbe3cada8fc427e 100644 GIT binary patch delta 4236 zcmZu!YgAKL7S6pSfe;9sqC9*7Dr#ez8!H;X-Vhb*)O1xX_$bhW z)wZ_9)>f1(z{VCVmNKr+jMJGlE37gFGxNi)Wdf{rRv&}YPG`+@_P)Uw+um?L&e{9y zea=1i>~HUT>L&HqF-rEQ$jER3T-m2j)eTp6$kf#CYZ*)Fa9XfNz$P3Mu!$XAi6T{V z2xD;Vp-o!qzUx!nRhl|a?1`(Xvt%f77xl7BXBZOdUj#SySb)ja8$QQn?`y-pt``iM z)tky0j*ywuzVNf4^oW}3nykdVP(?mT^HRO!YI;pt4=A_-1Si5Vgf0M&q|V&fU~Dke zTeyoPEhClcBl|b&#g}owWm2CpL0xv0Wa>jCylbJkk>x!li(GJhzLwLO#z;&8}iZoD{6MC4kUu~mXlMr?VjqlU4G@H*VK|B^rCRVcp^2rB4}H2m8}@1$o0g19GW zM;ZA(PapRyDx#ThqlU>yw#N1D{7D*~@WuQN(Od#OoTh=B+_U!TTM2b*HGD?U2 zUq3B}L*TTOidmsk)FUFxtCJ`(`Jz%K6|zD$boTVDktd5;sWYO7Ce9^El1Ns@MmeM5 zn7k%Wii?3`63fOp-6z6lOOd~F~XoA3&fWvtfsiQ(azZ{ zK^9Ye!;{^oh<-&1xv_kG=6#Go@a4Vs1_YqDv)9{kK1k+|OOUx%tk^?QpOGso7gPTs z-j!EH5G{iLk%XsH>GOc;$!IDfdmX25vOczc0+ z%XatPLlCT`^ac!I*+!a#tU4Gn5u4(wT(y>>Zt?MYZS@G9!P7hL&ZEQ*0=pPsve$8M z=oELwb-uKSg5dt!+H&z3kaO)Md)->fN!%MXqYBMu?N6;r;KqA7|bC97R$D_%v4 znYST6VFU&Ty+YrGGv1RI8G81elP5RK(_IPSFmds`>gU=W*oBP|+$X&o>Z88I?h^=K z0bC(-HlB<64qBd2cmoIO%oYwGFo((~KW!WoX<<2q(QnRG{DA>gr3 zY{;xBG88wmbq!B3Bxg&P^a|)W{;e9woh{^O@l@}7-oX~`Yc#}T?eBXpU7xzx=@-0z z%r|h&FFq`b>-;_r`VaO^3IwV{)i%`s(SgZ=Uv%Aa*4&*mo{?vu$8AHvVuL#kV8>!%-qgkJ$x9!4kDr;keuJw62yN=2q&V*=ti|F`T?ot7>%%@M&mAAwwRpRwm5y3ZgVQY zlO1`krr6K7cC36Jbci7E5w- zOtsG~luxLc^Wt$eGpc6X3)ZfTfoOmE*ku1p_QrgvA3$lP0>9aA24tiKzsZAl*w*N%p3yPoV9jug1J92^VJ zKjbLB8+_>E)rge;+D_qZ%!RF=?HH7Ij$svDE- zb*v%7+h3!mi6L1-7tmxhZa?h|BcYWkp%OOS83qMVhxBoxtdvV+tn^_K6eWeaIWiE> zF(jr^Bb7WXjEHV&8kI>_Rw`g5RMY`R;)@-zMG0jJuMU+$nGI_4aYT|x3UsI;`_`u% zmGWf`#sgfnT2=={63-Kf9*E>&S)*%CWDK5Yf6iasz%^Q}_3$R`DPYF4ivKYBPQK%9 zKa=Zyse7vLt*PFA??-2OEhB`SQVL`35&-Y;@u}xJCtX9#QQ9NHLt!#ESy;br;61;N zi^bDBf;Je9vo84(!y~l>R$${k0WH^p<1p2FFB4b+*NVbGo~q&$=xi4XPXf%YqW50e z#}~bA1DqVRJQYlyePycKk?Xzmg3r~>FPPhjMuF_cDbdkC&MF7G3RF`)F)Mjmb(KYwnV#)oQYKRk9SKJgykNvY)%-0tD=N$i?a|<7qFH`s;gWt;qgxaYOF9t*S6WG2Y znLCf? zj*LJe_et_{tloJ>mHi^dMKTa(`ahWNJ(}2mZhG`Nr@LiMBTx52PX4=OB=%Llnvi$k zQbB9}7<$qQ2ICC&FnALoo)YW`cltX6+eA7FO@)ie1Ozg&HAk4k5!hGwJvjo`9<;~$ z-6|#S;H9NMlG6|>s?dxuk5e#Kv{rM1O;3Qbc$%Dmt0p_wN+7MR1j?h_2;<9!M_<#mR$%w}*9 z|I&xB+Gd7l%a^HV6$(=W?6x@|_rCSyEW3o*-uq@aatwApFs%Lpy*~kC#Q`!2?^HZQ zreZfMT*Ukp(maWCL7|>cp?_?FC12_2;5Z;_@`+ZJZ(0o{8y=iW|t`yt0UJcSyBTStL$o{sNR|fw{#|*Q83=A zp9X$z&nGf#Vys!Nss>|v9zBuSbz}Mb&DM+8h=C*Dg71xsvR_5V2cbc;$NC1u)of&yrgf3-|M<# z2kNs4xge|dyN2^zq1;?j(eeLn1&Ii&;LJqtPiF?`vDoRAH3Z+d+mAMCUO=7>LQ~@- zWE?uZ_H@Hqk?sg~h$Vi%)J6UN4EVTluUd&Jw196_wMXtlqZk!eu@*#^vjoQ(B%2dG z%FHl>m*KNjWtu~5dKkj~LhWrfWuSbUXTWMhH!%1aAp%#{ zY%^YG=0*lL5O@^?sx1&)yW8+9n(=$^ve|*De16Diwvci-CuV`S`Jg%-&wW2UvuT-I z-niUc49Ly(WH~;S?K#ldGuMoNhx!(e1^bGEN(|LWS1bfthjr3}jOP7VVRah9!M*GXvu5@PQM01m`o(T@M+f7c{%f-C# znKyw4^}-;L7elPmU~CH&I(g4P;uI6@^c;>1*1 zCyv${r#CDFJ2%66->B{w(sOrg1J&c8uem;PJaMXz-bB6`+BbN(zZLMyh9%Wh79=Z4 z;=SEB4jx8X-u=`ex6{}mKSuHhVpkv-l00-yjV%2TgJEcTqJdKo2zzY~@G1~r~>U_mFZQYl&PxX3X_R0Mk=FD3GwmiL9{))@WImL~J+d>PS;c&k0*61+OPDZ04 z4&tXVF_&ab(#}i-h8ReQM{P|~iJRK$?$i`a@76?XX^ySU>Qu;V)y&q+zV|haBs-Qq zzwf>Gz3=$H_j~X4Cxcv1kdu8Ym5K%M=ikqEyYE)_%5>bJ!GiTdu}+X9;4=*ZKCAa? zmPp&2V6_aj2=D2+@8~kqe*?`iFXpZfRh#=b?j8Clvxe)V8?z2@Zx8ilbtQ6FXm+lV zdyB5m?dST2zRAtzxZ6~oe<1H;*o&-3GK!=J$qgWpRCi-XyQAIN<|6&{Vty`nm&z15 zNjFj9CVh~9kGo0#DAo^sS`Zb&UcY|gA@{HJwZ)s0`cUIv=zlEUoAenDKckzLM7YmT znd_v3LN%49nP_v-p43!6l^2&Kku;i-rcaRUPofzWDg~d$CxIX)lI~BcGtpj&S}x(G zyo{HZB@j9AEXYeu=QDT(uPjTTwqj$dYQKnA^BP`DjS}MMSOyH1t1+@gA`h)7T+9f=%?6p0Lo52w?=9G6C-IJXRFI|*hM@Y?WB)NOUk$* zy0v6Gr>C3p4fOpImHIBKv$y&qj|n%>dWR zMTG9A3TrC)PneMhw0(6Z_YFO_daLnUU_?}VT=o{Hx7K;g-qC)@-CXQ(9&vq1AFlqh z`d7Gsty+a6E0S;NU&;)IKf}<&0oh0jk^3M{hH3Gd|K%Q_9|=tfG<&g*_A6H;i}<86 z5fOt6>5}+la19OJRH#f7@ghDY=0r42WlG|~f69ckL}g3`FXF*-z-y#CjZy`8jgnXK z>as+lKt~iQ<3^-tL^J1!Dkf8ZPo|xdX_!p=J(+G!rlazm`ZPUn;ElWqyjPENxq>&( z%?+?>CG+M?;ADZvnovV(fxSh1HlG9MGBb{zaTYO7<^oPG<76QxBc_wbI2N$H87;#- z^7#U=oD=8BMSLM|19p2ruF{N`e-jf}yY&wNF{S$}5h?_ub}h?qR< zpYflaIdyL8){S~=;({od50FSogWcZYBOcd+71RB1kBBQ07FUs-O?CM{^ zz%xsKzqz0!*wPyGJ{OW6^H+>Zm1EM(ur%|P;{y%DjzP!g%Y%FN1)qHm;;T|vi;*bX zC8U4f+`*00u{!gR!>;4FG5U3>eyI0JwNU6zpn-}51y{g2d!OiQwPx5G-_ zLT(a}sT=-@(QYzAZCf7#C}maF0VS0?5?=)&y;onEc%37ow4_!~3#!sNHC*i=#c+x}jw|{>pJ?YO8j_;d-l$SwC=-$=AI`F?_zExJy3gs zE2Cf6ZWpZqc^S3svKO+WuEo`>&<@LKY_+7ZqwTP>{a7rS>FAYRHk}4leu~P>L(p#s zf-fD^7K?2tL}DJ&*O@q>ja^#!nUH}_O_4zik(dP0lL9HLM6XRw=2ObJ7zWAsQ~(q) zBrypDE@n0z{d-Z081M#S2I9}aVjZ#;uL27)RKV265*H>nWQc(Q*^Af4Fe(kP1JHvv zmnBjgU>43Y0V@)SDt3Z2BX40?kwDaFKmp)m(A8fo+R%+YEpK41E3xeldWi zZhr9+zKAc5gOk+8wp)tZ5d$p&_`(fK*oH<1Jf%!y8PhPKMk;D7=U4D6Y3F)#myut^ zuZHVo@nrxn7M6n7Knh+vpMtabbtI>Gy|tW(;Gx~+Kzv#70mKbOH$X0ZpBs--;1cG= z8}R~&lenrY&^A;=1-5q`R=W8s>mLl+7c*}>_6rTXd zvUov7Vnkv^K`aL(=9?$L7&%A58aM zj2XH4i^<`^nA&lbyWQg@zD92c@z|V4oL(pVU++`TnH@O;3!~5XSPCabUYz*ks+BM- z!*`4sh>N=(LmGP$wrT9ie7T@K+5OJs>u00au0%ijAo__v+S5BV+B5U3mmYrd(j+|v zk5gP+Y)x1I6nJrVI{?h7_iy_$VIF_GspsFHxg1zxnfzs7`rP>?mgzgMb2J-)cf<#oDSJvM^yPF_cXwH|qsep$a&xQL_jDxD!co>z+<4){P0#{s-s9uQiS zXQcO6Xz86=Ns)A3Z6{i71bN&ISQ7Rj@`6vqGBT&l&#C|wR3n8*7URaNkl27!TNgoY z)ptdd_L|*w{1!+3Q?*+h_R5+n`xc%gpxQs`8`5Gob9j#(c12Q}e1ruX*$ny-2~@&R z20K8d-5Y^ejC)%o{qfl0b$eT(>A(dU?)I*@jw9xfESE*p3-n`rhmW*IB}5 z!Ru_EOLAy08Iqyb1$)CO;iEa+ZO&#_1is|_1kI3zsW1`l$#P*A0u@6m1S`)iAg2X? z5(-5rkCGCNNx?+zBZ&ZN1c@muF$EGvibIkr{}xu4=Y$nG1G11}!(5$?^T5I2nw`Nt z&fwt_Aw?Gm^N|HnapuXGs1tf91GRKc>tNEAzVoKN&d@QdawSjAv-a zGP1)N*@3!|mSF9VLK*w~+s3u#F>OIuTQE?1w>r50*^u_f!9>-#E^|z04ePAI<=cXF z4Z)|Kj|76BaFtlC=&OXmMD@5S=dGG6H3PXL^}*VvkjWKH)Q@XSW19T1CO=SrR~FoR zBBbf^*L2+%r;mvZVX@)r!GXlPtAl%vhQ!bL?e`^$F-cZff(Lhgo)LV?8EoqeNqz=* zm#M~NnPFMx)%p=xu+ANlJ?r0kKi;SWi#Oc$;+b5>LW<+?02qLLva{uFZW8Y_J{m~ z2^WfUy*?FZ^Ub7`HIu->R#L+fvK$(O$4!9*+DaM76?5w~k_BCY5;Ck_1ev$)3D#;z zU@3H+kWe4@0goj*CV^f>q8CXZryJ<6CFU*(q)QWQVC2n^AWe_E`}qWE<}D<%Iop~O zNw>3lc1t_yXl-o+?{&9;@A)tzh-0xl`Qkf}rfks*f$9FYr*8~IZ(R~w6QTU71|452 zkm&7G(|9R-mC&)MtH2&-sauuhFOFIrQhbdGwJwncg=mFVyCwVX;S4NLYDn(Eae; z;^_H2)Q?7m@w#!$x+Y{@6D(XCl&=e>tXl}C>(&2nH3QYt@pCMEc?SIp85F=(v=3BI zV`=&fLyon0D|Wp3(v;}%wCj+p2NFr0Yf9WrtWJ+4w*g)PLj9vkb?#>ZFp4#6?QS%J z1bxGUxo;kvM4BXj^&^=8qA}LLJU6g%xO}iYSo=({?RZG~Q|N6}hA~xcSd}}FaJMj| z+V0;;57*`8r)$R2E#Y)aVDmuJ=f+U_ZhzInn3jiRl@QSMYN>{9uv=0gDwTHpEdtt~ zJc!I^bOqY=|C1yXEt?epiRBkOqg6-K_nfG`i7&$HZ}vOK1DvuZjFs}N3TH3J?uIET zW&wW|h@`A*>uB<|y2^qNxgT-oD=zQo-7zb`@$7Ovmw*1y<<{QT zSpf`ZR~KoE5-uww|+a%g!IU+}YdtodAYx G=l=u9pewQf delta 2349 zcmZuy3v83u74~%;=V3dJ?bz`njuYo;@^BJFs7XRY3`vPy68s0XYH1yl7}qq3%XLg) zLnWIwjmJ<(4=6=IAf2|dwaXwLZBe4FcWTd30K3qF;zR~cVKNb z%l>r!bIyMr_k8zw{04jbJj?q+rOK7yXUo%v!pioEJR@s8U)3g+g)$t)@fW36O)Lga z75xLzYpr39_-^YoV=sf**2AV?hpnHTj$gCwld zCF2ShR~VFbiI5}Y3JO8#%7F6Q6BT>~ogGE$3)0;9cwJ?_ZJ_Hd{^8rT)Q2De=Q z(!WBH#Bi%gau6)SU)Gq-|3op$5ZZY)X(^ouuYT0iIgw zCTxCqfALKeV@iaD17oSN1F7#HzH@2jr&3NTJe>A+pT6&|I&Dv$pGqAaOCFs`PP_yq z?Pc&*dlO#xzqhYqECpp9E$rXm@7T-k!R?NRGZi?ddr;!8WaZ1Gw8OZ xvl)@(n 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 X+0L?!NB1VCEsjrH{9g<{QThEZ9+#R~ delta 3760 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 diff --git a/backend/shop/__pycache__/serializers.cpython-313.pyc b/backend/shop/__pycache__/serializers.cpython-313.pyc index 01642097584fceda04a20bbf3562d82140654807..76f8aff82b0ac0763ce6588f220a852e6ea0fbce 100644 GIT binary patch literal 15858 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ˈpwHo0pxH)on zdLchVw$YC}ZL4+zfg?qpsl!OD=xmoG>>^}*N09VKppZp*3QO#!>T@jZq|^7N^Lyy( zz0>)2+Pya$F3!HacOL(A|E0a}DfpB9$AjPHHP0cbNbB}xhj0=hun!;xSJ&oqyrk-I z`3^b^Qap26&Q2F`bp+)1V2L#hlgTLr$q2@Ehvp_Fwzz}Naqq?gI+$E$gZO6b0t{vy zb6){^eP1ew_>X;d{&zHI|04-#hG4NDA>o$({{1hH-te*a6sfDbSc4*yqx8w{f>023 z!^9?CP;|tPX9eXP;JB!j*lP-T$pPf?I~-Pbhmi7p-8pEI4D2RPkZ

VvlYB#sFTm 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 delta 9260 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 R`(Q@dKt|c$1zzP||37EXYo7oB diff --git a/backend/shop/__pycache__/views.cpython-313.pyc b/backend/shop/__pycache__/views.cpython-313.pyc index 8cd5bcac4a7719d2920cc743edded45cc6066da8..4eee818eed7b20325ade1d7eb3f50a084d06ba37 100644 GIT binary patch delta 13800 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 DwJm)A delta 9606 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 I?{W|LKXd<;ng9R* 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() { } /> } /> } /> - } /> + } /> + } /> } /> } /> } /> diff --git a/frontend/src/api.js b/frontend/src/api.js index f01ac76..2603283 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -19,7 +19,9 @@ export const confirmPayment = (orderId) => api.post(`/orders/${orderId}/confirm_ export const getServices = () => api.get('/services/'); export const getServiceDetail = (id) => api.get(`/services/${id}/`); export const createServiceOrder = (data) => api.post('/service-orders/', data); -export const getVBCourses = () => api.get('/courses/'); +export const getVCCourses = () => api.get('/courses/'); +export const getVCCourseDetail = (id) => api.get(`/courses/${id}/`); +export const enrollCourse = (data) => api.post('/course-enrollments/', data); export const sendSms = (data) => api.post('/auth/send-sms/', data); export const queryMyOrders = (data) => api.post('/orders/my_orders/', data); diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx index 6adaa09..268a24c 100644 --- a/frontend/src/components/Layout.jsx +++ b/frontend/src/components/Layout.jsx @@ -36,7 +36,7 @@ const Layout = ({ children }) => { { key: '/courses', icon: , - label: 'VB 课程', + label: 'VC 课程', }, { key: '/my-orders', diff --git a/frontend/src/pages/VCCourseDetail.jsx b/frontend/src/pages/VCCourseDetail.jsx new file mode 100644 index 0000000..95b366b --- /dev/null +++ b/frontend/src/pages/VCCourseDetail.jsx @@ -0,0 +1,285 @@ +import React, { useEffect, useState } from 'react'; +import { useParams, useNavigate, useSearchParams } from 'react-router-dom'; +import { Typography, Button, Spin, Empty, Descriptions, Tag, Row, Col, Modal, Form, Input, message } from 'antd'; +import { ArrowLeftOutlined, ClockCircleOutlined, UserOutlined, BookOutlined, FormOutlined } from '@ant-design/icons'; +import { getVCCourseDetail, createOrder } from '../api'; +import { motion } from 'framer-motion'; + +const { Title, Paragraph } = Typography; + +const VCCourseDetail = () => { + const { id } = useParams(); + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const [course, setCourse] = useState(null); + const [loading, setLoading] = useState(true); + const [isModalOpen, setIsModalOpen] = useState(false); + const [submitting, setSubmitting] = useState(false); + const [form] = Form.useForm(); + + // 优先从 URL 获取,如果没有则从 localStorage 获取 + const refCode = searchParams.get('ref') || localStorage.getItem('ref_code'); + + useEffect(() => { + const fetchDetail = async () => { + try { + const response = await getVCCourseDetail(id); + setCourse(response.data); + } catch (error) { + console.error("Failed to fetch course detail:", error); + } finally { + setLoading(false); + } + }; + fetchDetail(); + }, [id]); + + const handleEnroll = async (values) => { + setSubmitting(true); + try { + const orderData = { + course: course.id, + customer_name: values.customer_name, + phone_number: values.phone_number, + ref_code: refCode, + quantity: 1, + // 将其他信息放入收货地址字段中 + shipping_address: `[课程报名] 微信号: ${values.wechat_id || '无'}, 邮箱: ${values.email || '无'}, 备注: ${values.message || '无'}` + }; + + await createOrder(orderData); + message.success('报名咨询已提交,我们的课程顾问将尽快与您联系!'); + setIsModalOpen(false); + } catch (error) { + console.error(error); + message.error('提交失败,请重试'); + } finally { + setSubmitting(false); + } + }; + + if (loading) { + return ( +

+ +
+ ); + } + + if (!course) { + return ( +
+ + +
+ ); + } + + return ( +
+ + + + + +
+
+ {course.tag && {course.tag}} + + {course.course_type_display || (course.course_type === 'hardware' ? '硬件课程' : '软件课程')} + +
+ + {course.title} + + + {course.description} + + +
+ + <div style={{ width: 4, height: 18, background: '#00f0ff', marginRight: 10, borderRadius: 2 }} /> + 课程信息 + + + 讲师}> +
+ {course.instructor_avatar_url && ( + avatar + )} + {course.instructor} + {course.instructor_title && ( + + {course.instructor_title} + + )} +
+
+ 时长}> + {course.duration} + + 课时}> + {course.lesson_count} 课时 + +
+ + {/* 讲师简介 */} + {course.instructor_desc && ( +
+ 讲师简介: + {course.instructor_desc} +
+ )} +
+ + {/* 课程详细内容区域 */} + {course.content && ( +
+ 课程大纲与详情 +
+ {course.content} +
+
+ )} +
+ + {course.display_detail_image ? ( +
+ {course.title} +
+ ) : null} + + + +
+
+ 报名咨询 + +
+ {parseFloat(course.price) > 0 ? ( + <> + ¥{course.price} + + ) : ( + 免费咨询 + )} +
+ + +

+ * 提交后我们的顾问将尽快与您联系确认 +

+
+
+ +
+
+ + {/* Enroll Modal */} + setIsModalOpen(false)} + footer={null} + destroyOnHidden + > +

请填写您的联系方式,我们将为您安排课程顾问。

+
+ + + + + + + + + + + + + + + + +
+ + +
+
+
+
+ ); +}; + +export default VCCourseDetail; \ No newline at end of file diff --git a/frontend/src/pages/VBCourses.jsx b/frontend/src/pages/VCCourses.jsx similarity index 90% rename from frontend/src/pages/VBCourses.jsx rename to frontend/src/pages/VCCourses.jsx index 910c81f..cd48a70 100644 --- a/frontend/src/pages/VBCourses.jsx +++ b/frontend/src/pages/VCCourses.jsx @@ -1,22 +1,24 @@ import React, { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; import { motion } from 'framer-motion'; import { Button, Typography, Spin, Row, Col, Empty, Tag } from 'antd'; import { ReadOutlined, ClockCircleOutlined, UserOutlined, BookOutlined } from '@ant-design/icons'; -import { getVBCourses } from '../api'; +import { getVCCourses } from '../api'; const { Title, Paragraph } = Typography; -const VBCourses = () => { +const VCCourses = () => { const [courses, setCourses] = useState([]); const [loading, setLoading] = useState(true); + const navigate = useNavigate(); useEffect(() => { const fetchCourses = async () => { try { - const res = await getVBCourses(); + const res = await getVCCourses(); setCourses(res.data); } catch (error) { - console.error("Failed to fetch VB Courses:", error); + console.error("Failed to fetch VC Courses:", error); } finally { setLoading(false); } @@ -30,10 +32,10 @@ const VBCourses = () => {
- VB <span style={{ color: '#00f0ff' }}>CODING COURSES</span> + VC <span style={{ color: '#00f0ff' }}>CODING COURSES</span> - 探索 Vibe Coding 软件与硬件课程,开启您的编程之旅。 + 探索 VB Coding 软件与硬件课程,开启您的编程之旅。
@@ -50,6 +52,8 @@ const VBCourses = () => { animate={{ opacity: 1, y: 0 }} transition={{ delay: index * 0.1 }} whileHover={{ scale: 1.02 }} + onClick={() => navigate(`/courses/${item.id}`)} + style={{ cursor: 'pointer' }} >
{ ); }; -export default VBCourses; +export default VCCourses; diff --git a/miniprogram/src/pages/courses/detail.scss b/miniprogram/src/pages/courses/detail.scss index 1adc137..fb0e3e9 100644 --- a/miniprogram/src/pages/courses/detail.scss +++ b/miniprogram/src/pages/courses/detail.scss @@ -1,74 +1,256 @@ .page-container { - padding: 20px; background-color: #000; min-height: 100vh; - box-sizing: border-box; -} - -.title { - color: #fff; - font-size: 40px; - font-weight: bold; - margin-bottom: 20px; - display: block; -} - -.meta-info { display: flex; - flex-wrap: wrap; - gap: 20px; - margin-bottom: 30px; - align-items: center; + flex-direction: column; +} - .tag { - background: rgba(0, 240, 255, 0.2); - color: #00f0ff; - padding: 6px 16px; - border-radius: 4px; - font-size: 24px; - border: 1px solid #00f0ff; +.scroll-content { + flex: 1; + height: calc(100vh - 100px); /* 留出底部栏高度 */ +} + +.cover-image { + width: 100%; + height: 420px; + object-fit: cover; +} + +.content-wrapper { + padding: 30px; + background: #000; + border-radius: 30px 30px 0 0; + margin-top: -30px; + position: relative; + z-index: 10; +} + +.header-section { + margin-bottom: 40px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + padding-bottom: 30px; + + .title { + color: #fff; + font-size: 40px; + font-weight: bold; + margin-bottom: 20px; + display: block; } - .info { - color: #888; - font-size: 26px; + .tags-row { + display: flex; + gap: 16px; + margin-bottom: 20px; + + .tag { + background: rgba(255, 255, 255, 0.1); + color: #aaa; + padding: 6px 16px; + border-radius: 8px; + font-size: 24px; + + &.highlight { + background: rgba(0, 240, 255, 0.2); + color: #00f0ff; + border: 1px solid rgba(0, 240, 255, 0.3); + } + } + } + + .price { + font-size: 48px; + color: #00f0ff; + font-weight: bold; } } -.desc { +.section { + margin-bottom: 50px; + + .section-title { + color: #fff; + font-size: 32px; + font-weight: bold; + margin-bottom: 24px; + display: block; + position: relative; + padding-left: 20px; + + &::before { + content: ''; + position: absolute; + left: 0; + top: 8px; + bottom: 8px; + width: 6px; + background: #00f0ff; + border-radius: 3px; + } + } +} + +.instructor-section { + .instructor-row { + display: flex; + align-items: center; + background: #111; + padding: 20px; + border-radius: 16px; + + .avatar-placeholder { + width: 100px; + height: 100px; + background: #333; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin-right: 20px; + flex-shrink: 0; + + text { + color: #666; + font-size: 24px; + } + } + + .avatar { + width: 100px; + height: 100px; + border-radius: 50%; + margin-right: 20px; + border: 2px solid #333; + flex-shrink: 0; + } + + .instructor-info { + flex: 1; + .name { + color: #fff; + font-size: 30px; + font-weight: bold; + display: block; + margin-bottom: 8px; + display: flex; + align-items: center; + + .title-tag { + font-size: 20px; + color: #000; + background: #00f0ff; + padding: 2px 10px; + border-radius: 8px; + margin-left: 10px; + font-weight: normal; + } + } + .desc { + color: #888; + font-size: 24px; + line-height: 1.4; + } + } + } +} + +.info-grid { + display: flex; + justify-content: space-between; + background: #111; + padding: 30px; + border-radius: 16px; + + .grid-item { + text-align: center; + flex: 1; + border-right: 1px solid rgba(255, 255, 255, 0.1); + + &:last-child { + border-right: none; + } + + .label { + color: #666; + font-size: 24px; + margin-bottom: 10px; + display: block; + } + + .value { + color: #fff; + font-size: 30px; + font-weight: bold; + } + } +} + +.desc-text { color: #aaa; font-size: 28px; - margin-bottom: 40px; - display: block; + line-height: 1.6; } -.course-placeholder { - width: 100%; - height: 500px; +.detail-images { + .detail-long-image { + width: 100%; + border-radius: 16px; + display: block; + } + + .placeholder-box { + width: 100%; + height: 300px; + background: #111; + border: 2px dashed #333; + border-radius: 16px; + display: flex; + align-items: center; + justify-content: center; + color: #666; + } +} + +.bottom-bar { + height: 120px; background: #111; - border: 2px dashed #333; - border-radius: 16px; + border-top: 1px solid #222; display: flex; align-items: center; - justify-content: center; - flex-direction: column; + padding: 0 30px; + padding-bottom: constant(safe-area-inset-bottom); + padding-bottom: env(safe-area-inset-bottom); - .icon { - font-size: 80px; - color: #444; - margin-bottom: 20px; + .price-container { + flex: 1; + + .label { + color: #aaa; + font-size: 24px; + margin-right: 10px; + } + + .amount { + color: #00f0ff; + font-size: 40px; + font-weight: bold; + } } - .text { - color: #666; - font-size: 28px; + .btn-buy { + width: 240px; + height: 80px; + line-height: 80px; + background: linear-gradient(90deg, #00f0ff, #0099ff); + color: #000; + font-size: 30px; + font-weight: bold; + border-radius: 40px; + border: none; + margin: 0; + + &::after { + border: none; + } } } - -.btn-launch { - margin-top: 60px; - background: #00f0ff; - color: #000; - font-weight: bold; - border-radius: 45px; -} diff --git a/miniprogram/src/pages/courses/detail.tsx b/miniprogram/src/pages/courses/detail.tsx index ab29a86..f5d5787 100644 --- a/miniprogram/src/pages/courses/detail.tsx +++ b/miniprogram/src/pages/courses/detail.tsx @@ -1,4 +1,4 @@ -import { View, Text, Button, Image } from '@tarojs/components' +import { View, Text, Button, Image, ScrollView } from '@tarojs/components' import Taro, { useLoad } from '@tarojs/taro' import { useState } from 'react' import { getVBCourseDetail } from '../../api' @@ -31,9 +31,9 @@ export default function CourseDetail() { } const handleLaunch = () => { - Taro.showToast({ - title: '课程内容准备中', - icon: 'none' + if (!detail) return + Taro.navigateTo({ + url: `/pages/order/checkout?id=${detail.id}&type=course` }) } @@ -42,29 +42,89 @@ export default function CourseDetail() { return ( - {detail.title} - - - {typeMap[detail.course_type] || '软件课程'} - 讲师: {detail.instructor} - 时长: {detail.duration} - 课时: {detail.lesson_count} - - - {detail.description} - - - {detail.display_detail_image ? ( - - ) : ( - <> - 📚 - 课程大纲与视频内容加载区域 - + + {/* 封面图 */} + {detail.cover_image_url && ( + )} - - + + {/* 标题区 */} + + {detail.title} + + {typeMap[detail.course_type] || 'VB课程'} + {detail.tag && {detail.tag}} + + ¥{detail.price} + + + {/* 讲师信息 */} + + 讲师介绍 + + {detail.instructor_avatar_url ? ( + + ) : ( + + 讲师 + + )} + + {detail.instructor} {detail.instructor_title} + {detail.instructor_desc} + + + + + {/* 课程信息 */} + + + 时长 + {detail.duration} + + + 课时 + {detail.lesson_count}节 + + + 难度 + 中级 + + + + {/* 课程简介 */} + + 课程简介 + {detail.description} + + + {/* 详情长图 */} + + 课程详情 + {detail.display_detail_image || detail.detail_image_url ? ( + + ) : ( + + 暂无详情长图 + + )} + + + + + {/* 底部栏 */} + + + 总价: + ¥{detail.price} + + + ) } diff --git a/miniprogram/src/pages/courses/index.tsx b/miniprogram/src/pages/courses/index.tsx index b984c0f..eda05fd 100644 --- a/miniprogram/src/pages/courses/index.tsx +++ b/miniprogram/src/pages/courses/index.tsx @@ -57,7 +57,7 @@ export default function CourseIndex() { {item.title} {item.description} - + )) diff --git a/miniprogram/src/pages/order/checkout.tsx b/miniprogram/src/pages/order/checkout.tsx index 34f4aa6..5996fdd 100644 --- a/miniprogram/src/pages/order/checkout.tsx +++ b/miniprogram/src/pages/order/checkout.tsx @@ -1,7 +1,7 @@ import { View, Text, Image, ScrollView, Button } from '@tarojs/components' import Taro, { useRouter, useLoad } from '@tarojs/taro' import { useState, useMemo } from 'react' -import { getConfigDetail, createOrder } from '../../api' +import { getConfigDetail, createOrder, getVBCourseDetail } from '../../api' import { getSelectedItems, removeItem } from '../../utils/cart' import './checkout.scss' @@ -34,15 +34,28 @@ export default function Checkout() { setLoading(false) } else if (params.id) { try { - const res = await getConfigDetail(params.id) - setItems([{ - id: res.id, - name: res.name, - price: res.price, - image: res.static_image_url || res.detail_image_url, - quantity: Number(params.quantity) || 1, - description: res.description - }]) + let res: any = null + if (params.type === 'course') { + res = await getVBCourseDetail(Number(params.id)) + setItems([{ + id: res.id, + name: res.title, + price: res.price, + image: res.cover_image_url || res.detail_image_url, + quantity: 1, + description: res.description + }]) + } else { + res = await getConfigDetail(params.id) + setItems([{ + id: res.id, + name: res.name, + price: res.price, + image: res.static_image_url || res.detail_image_url, + quantity: Number(params.quantity) || 1, + description: res.description + }]) + } } catch (err) { console.error(err) Taro.showToast({ title: '商品加载失败', icon: 'none' }) @@ -93,7 +106,8 @@ export default function Checkout() { quantity: item.quantity, customer_name: address.userName, phone_number: address.telNumber, - shipping_address: `${address.provinceName}${address.cityName}${address.countyName}${address.detailInfo}` + shipping_address: `${address.provinceName}${address.cityName}${address.countyName}${address.detailInfo}`, + type: params.type || 'config' } return createOrder(orderData) })