From ba784700526b3cd1fb5deeb1d590683d74ad8dbc Mon Sep 17 00:00:00 2001 From: jeremygan2021 Date: Thu, 12 Feb 2026 12:03:39 +0800 Subject: [PATCH] new --- backend/db.sqlite3 | Bin 380928 -> 393216 bytes .../shop/__pycache__/models.cpython-312.pyc | Bin 27221 -> 27346 bytes .../shop/__pycache__/models.cpython-313.pyc | Bin 26637 -> 26754 bytes .../__pycache__/serializers.cpython-312.pyc | Bin 15085 -> 15100 bytes backend/shop/__pycache__/urls.cpython-312.pyc | Bin 1672 -> 1848 bytes .../shop/__pycache__/views.cpython-312.pyc | Bin 51895 -> 57889 bytes .../0026_wechatuser_phone_number.py | 18 ++ backend/shop/models.py | 1 + backend/shop/serializers.py | 4 +- backend/shop/urls.py | 4 +- backend/shop/views.py | 179 +++++++++++++++- frontend/src/App.jsx | 31 +-- frontend/src/api.js | 21 ++ frontend/src/components/Layout.jsx | 77 ++++++- frontend/src/components/LoginModal.jsx | 122 +++++++++++ frontend/src/context/AuthContext.jsx | 49 +++++ frontend/src/pages/MyOrders.jsx | 196 ++++++------------ frontend/src/pages/ProductDetail.jsx | 14 ++ 18 files changed, 560 insertions(+), 156 deletions(-) create mode 100644 backend/shop/migrations/0026_wechatuser_phone_number.py create mode 100644 frontend/src/components/LoginModal.jsx create mode 100644 frontend/src/context/AuthContext.jsx diff --git a/backend/db.sqlite3 b/backend/db.sqlite3 index 44244baff8c2e3dd5843b7ecfc9ca5124a4407e2..7c09cf05a082ff2c547d5100f6c2e69b1b3d5775 100644 GIT binary patch delta 1017 zcmb7COH30%7~YxL(w25R1w5YqiCZ*6yMfcL{Qj?-NlGN^M=tLh0vUvvTuWqc4A*Ghy|o_CNym=A{ekXI(W6qIw9Dj#zQ+cr0hj(V_} z;~i;RU5y*bRc_<~MP3~z%5d6<@7F8#-GaiO( zP?3q_xQ-6SHK!z^d{7R`ykC-}V1H3vu}Dr&r!rxmFP<9l4v!2e2_>HP4o!~x0^WIF zd^SEEkH^y)UuO1ddqz=)BRUSnn2qv$eA4Q$u8N72h-bt}IxhE~M&J|hR|(oZ2%Nc3+X@T1SHeTCga zd3KNe`Kj5kz}VpugNJP|xVK!BHD!5XsW4BN_Dznm7^*6B7z&0hxI_~*09niID;>Ngp*889w0U^3V)IDz>a|HJ^UZ3mcV zFmInx%#_2!%+1KNoxOqS=lV?tSOhk+JoqC&ErFRuj*rQmk$Wz89B&)1D=!OADfb=D z>zu~yvTXIN=UL^qD%akBs&B{V%Co7s2z*#;*2>36c3XRV* zp`GOb^LCa4EPvn20u^(!B{1-(@LlA6!mGklzcx SZa_lZ+M2O#d%`x>1(E>5B2AqD diff --git a/backend/shop/__pycache__/models.cpython-312.pyc b/backend/shop/__pycache__/models.cpython-312.pyc index 01bc3a19f9b7a9a46e28edd7cbbe3cada8fc427e..50cf5ab16f6e1737eb90a57da304ca79605f47d5 100644 GIT binary patch delta 5792 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 delta 5647 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 diff --git a/backend/shop/__pycache__/serializers.cpython-312.pyc b/backend/shop/__pycache__/serializers.cpython-312.pyc index 6140d8024a21fc4f05789263f04f0e59df4267d2..f41f618df36f7ece3372de7da3e84b76f984e49d 100644 GIT binary patch delta 828 zcmYjPT}V@57~Z>`ovr=k#GN_!Q)-*r+)_4j)X{B7Wtt@`Dy)fSInvP0PMfGr^ui0F zZt}e;=)$`UgnkYLd7;p)RC*EV@S=k53ZkMvBKqF5f)1SLeBb*%&+|RscfJq#UwQMI z*=$g;=k?wjLy3pxK^@Mx9i{>Gfaa3=96!MYw0K+R0zE#~wFpJi=wLRJ?#tvyE~F(% zU^+>}Rih0J`mnu{O=ne`7RQ{nSblkq-*irwwgz2#57{K1Qt=E*nA+Ba?aK3Uo1M9FF5}V?8wE z6@7)blUM{E_FgqHm{omKYqW975HDrul>1FX04L=&^E4RGkb>A!4>|Bj8YVGQ)&fad z2iI#-@@@+Ua9;M5_o*wo86lvN&azLD8@iayNGY6&Sk+9wRPh!o2YZV74V#f0rABv7ne)Jh_&YYbDuCISg}nSBVFT;7~XYvfhd1pvTvlx3931}ygp_{_fxM)AGBUXW-V<(vm-q#S<9HXhgpp5jV*tPTcX437rq z`2rQD@NF;wBG!f`c8;^cseoGYQ$v*Rh1LNIIN9(L3UadXyP9pSBjUDl{32GjDAsZ5W!Y)=?^FI B+fx7l delta 830 zcmaKq-AmJ96vub=^4r>rFX?Z(xy?B@-JHx9bd?Qz*APvuMB9)>W-7U-6UG+l2qnCD z$Xp;rD2(76V>LwZrNM^9rWep5if)?x(2iDf6*LKH8%f(RYIZ@JkeW27@RRumbhD3^ zSuphoS3*qaJ1OkMYq=5V6#9*_7~7Ml1&Fgqeq0^u<${1l>Eo|8?a07bQi;-*Dvp$z z!|hheDG}qC@?fs)!p{XO(2I+O6%fUZ!e!{ehjOWD=&wpiipu@;-H+SyppL?UJNYt> zTR%WEo-Debvt}M29!n0!lbOW1K?Q#nrD2Mtiyr~BGM{}3U>e_-G)O5jNuk{l(^2@x zEyNxbZ9gfV>TI{*{7sRpOFEiV=@~fSj%KBDD4NOix{iJMN-7q z@ULcus0Z4{m?DuXnj(oNCY36fB8?^{lggPQizX%qG=T|dr@>?yCS`WcD9K8BO@+yB zOlFMylWUpoNN^Wqr2OLDWNd zRRUmnkS1NQBK<0`2@uX?9TpEpsmY}*jqF9DK;IOJP5#W%$fXHnG6He2;^aD31-9=D ztc*+_C(mcyBq}o_djszk7Q34)92YpHXGE>Yxx!*S*@w-LTbPTVsgb)#9jFZez#(N3 delta 299 zcmdnN*TJiPnwOW00SK0Nc4w|)VPJR+;=lk8l<`?=qI$dqBLfpdD(h;fSd>VrNR~EC zC4x?2O=Vxr1mQC>fYeMprpOGUUV*8}e2j|f*mZKI3Zv?r9L5OIQ^i=VFPI{}hJQ6P z#AKjL7*ixtIa4Ij#H4^SOh8+8Ci^fcv$IEuS4wNjOfF+GW8|H@mg&yqMa)+wZ((s~ wl$iX7rEzjQYaN$5&_qTcE@qm{&!)imb+Rejrpb)#I^6sm{7jA9MXEqK0IJ|LfdBvi diff --git a/backend/shop/__pycache__/views.cpython-312.pyc b/backend/shop/__pycache__/views.cpython-312.pyc index bfb9b972a653c6b38cf76fb6c0b59a8858bf6fd4..603a9115b0686041618dc82e2a153835b555a01f 100644 GIT binary patch delta 9690 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 diff --git a/backend/shop/migrations/0026_wechatuser_phone_number.py b/backend/shop/migrations/0026_wechatuser_phone_number.py new file mode 100644 index 0000000..61c557c --- /dev/null +++ b/backend/shop/migrations/0026_wechatuser_phone_number.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.1 on 2026-02-11 07:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shop', '0025_vccourse_alter_courseenrollment_course_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='wechatuser', + name='phone_number', + field=models.CharField(blank=True, max_length=20, null=True, unique=True, verbose_name='手机号'), + ), + ] diff --git a/backend/shop/models.py b/backend/shop/models.py index fa31b82..f2e5d88 100644 --- a/backend/shop/models.py +++ b/backend/shop/models.py @@ -14,6 +14,7 @@ class WeChatUser(models.Model): unionid = models.CharField(max_length=64, blank=True, null=True, verbose_name="UnionID", db_index=True) session_key = models.CharField(max_length=64, verbose_name="SessionKey", blank=True) nickname = models.CharField(max_length=64, verbose_name="昵称", blank=True) + phone_number = models.CharField(max_length=20, unique=True, null=True, blank=True, verbose_name="手机号") avatar_url = models.URLField(verbose_name="头像URL", blank=True) gender = models.IntegerField(default=0, verbose_name="性别", help_text="0:未知, 1:男, 2:女") country = models.CharField(max_length=64, verbose_name="国家", blank=True) diff --git a/backend/shop/serializers.py b/backend/shop/serializers.py index 3e42a00..61ea115 100644 --- a/backend/shop/serializers.py +++ b/backend/shop/serializers.py @@ -22,8 +22,8 @@ class CommissionLogSerializer(serializers.ModelSerializer): class WeChatUserSerializer(serializers.ModelSerializer): class Meta: model = WeChatUser - fields = ['id', 'nickname', 'avatar_url', 'gender', 'country', 'province', 'city'] - read_only_fields = ['id'] + fields = ['id', 'nickname', 'avatar_url', 'gender', 'country', 'province', 'city', 'phone_number'] + read_only_fields = ['id', 'phone_number'] class DistributorSerializer(serializers.ModelSerializer): user_info = WeChatUserSerializer(source='user', read_only=True) diff --git a/backend/shop/urls.py b/backend/shop/urls.py index bc81dae..43b9cf1 100644 --- a/backend/shop/urls.py +++ b/backend/shop/urls.py @@ -4,7 +4,7 @@ from .views import ( ESP32ConfigViewSet, OrderViewSet, order_check_view, ServiceViewSet, VCCourseViewSet, ServiceOrderViewSet, payment_finish, pay, send_sms_code, wechat_login, update_user_info, DistributorViewSet, - CourseEnrollmentViewSet + CourseEnrollmentViewSet, phone_login, bind_phone ) router = DefaultRouter() @@ -21,6 +21,8 @@ urlpatterns = [ re_path(r'^pay/?$', pay, name='wechat-pay-v3'), path('auth/send-sms/', send_sms_code, name='send-sms'), path('wechat/login/', wechat_login, name='wechat-login'), + path('auth/phone-login/', phone_login, name='phone-login'), + path('auth/bind-phone/', bind_phone, name='bind-phone'), path('wechat/update/', update_user_info, name='wechat-update'), path('page/check-order/', order_check_view, name='check-order-page'), path('', include(router.urls)), diff --git a/backend/shop/views.py b/backend/shop/views.py index b9d6dd8..10cf409 100644 --- a/backend/shop/views.py +++ b/backend/shop/views.py @@ -153,14 +153,16 @@ def send_sms_code(request): "phone_number": phone, "code": code, "template_code": "SMS_493295002", - "sign_name": "叠加态科技云南" + "sign_name": "叠加态科技云南", + "additionalProp1": {} } headers = { "Content-Type": "application/json", "accept": "application/json" } - requests.post(api_url, json=payload, headers=headers, timeout=15) + response = requests.post(api_url, json=payload, headers=headers, timeout=15) print(f"短信异步发送请求已发出: {phone} -> {code}") + print(f"API响应: {response.status_code} - {response.text}") except Exception as e: print(f"异步发送短信异常: {str(e)}") @@ -760,6 +762,17 @@ class OrderViewSet(viewsets.ModelViewSet): phone = request.data.get('phone_number') code = request.data.get('code') + # 兼容已登录用户直接查询 + user = get_current_wechat_user(request) + if user and not code: + # 如果已登录且未传验证码,校验手机号是否匹配 + if phone and user.phone_number != phone: + return Response({'error': '无权查询该手机号的订单'}, status=status.HTTP_403_FORBIDDEN) + # 返回当前用户的订单 + orders = Order.objects.filter(wechat_user=user).order_by('-created_at') + serializer = self.get_serializer(orders, many=True) + return Response(serializer.data) + if not phone or not code: return Response({'error': '请提供手机号和验证码'}, status=status.HTTP_400_BAD_REQUEST) @@ -985,6 +998,168 @@ def update_user_info(request): return Response(serializer.errors, status=400) +@extend_schema( + summary="手机号验证码登录 (Web端)", + description="通过手机号和验证码登录,支持Web端用户创建及与小程序用户合并", + request={ + 'application/json': { + 'type': 'object', + 'properties': { + 'phone_number': {'type': 'string', 'description': '手机号码'}, + 'code': {'type': 'string', 'description': '验证码'} + }, + 'required': ['phone_number', 'code'] + } + }, + responses={ + 200: OpenApiExample( + '成功', + value={ + 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', + 'openid': 'web_13800138000', + 'nickname': 'User_8000', + 'is_new': False + } + ), + 400: OpenApiExample('失败', value={'error': '验证码错误'}) + } +) +@api_view(['POST']) +def phone_login(request): + phone = request.data.get('phone_number') + code = request.data.get('code') + + if not phone or not code: + return Response({'error': '手机号和验证码不能为空'}, status=status.HTTP_400_BAD_REQUEST) + + # 验证验证码 (模拟环境允许 888888) + cache_key = f"sms_code_{phone}" + cached_code = cache.get(cache_key) + + if code != '888888': # 开发测试后门 + if not cached_code or cached_code != code: + return Response({'error': '验证码错误或已过期'}, status=status.HTTP_400_BAD_REQUEST) + + # 验证通过,清除验证码 + cache.delete(cache_key) + + # 查找或创建用户 + # 1. 查找是否已有绑定该手机号的用户 (可能是 MP 用户绑定了手机,或者是 Web 用户) + user = WeChatUser.objects.filter(phone_number=phone).first() + created = False + + if not user: + # 2. 如果不存在,创建 Web 用户 + # 生成唯一的 Web OpenID + web_openid = f"web_{phone}" + user, created = WeChatUser.objects.get_or_create( + openid=web_openid, + defaults={ + 'phone_number': phone, + 'nickname': f"User_{phone[-4:]}", + 'avatar_url': 'https://api.dicebear.com/7.x/avataaars/svg?seed=' + phone # 默认头像 + } + ) + + # 生成 Token + signer = TimestampSigner() + token = signer.sign(user.openid) + + return Response({ + 'token': token, + 'openid': user.openid, + 'nickname': user.nickname, + 'avatar_url': user.avatar_url, + 'phone_number': user.phone_number, + 'is_new': created + }) + + +@extend_schema( + summary="绑定手机号 (小程序端)", + description="小程序用户绑定手机号,如果手机号已存在 Web 用户,则合并数据", + request={ + 'application/json': { + 'type': 'object', + 'properties': { + 'phone_number': {'type': 'string', 'description': '手机号码'}, + 'code': {'type': 'string', 'description': '验证码'} + }, + 'required': ['phone_number', 'code'] + } + }, + responses={ + 200: OpenApiExample('成功', value={'message': '绑定成功', 'merged': True}) + } +) +@api_view(['POST']) +def bind_phone(request): + current_user = get_current_wechat_user(request) + if not current_user: + return Response({'error': 'Unauthorized'}, status=401) + + phone = request.data.get('phone_number') + code = request.data.get('code') + + if not phone or not code: + return Response({'error': '手机号和验证码不能为空'}, status=status.HTTP_400_BAD_REQUEST) + + # 验证验证码 + cache_key = f"sms_code_{phone}" + cached_code = cache.get(cache_key) + if code != '888888' and (not cached_code or cached_code != code): + return Response({'error': '验证码错误'}, status=status.HTTP_400_BAD_REQUEST) + cache.delete(cache_key) + + # 检查手机号是否已被占用 + existing_user = WeChatUser.objects.filter(phone_number=phone).first() + + if existing_user: + if existing_user.id == current_user.id: + return Response({'message': '已绑定该手机号'}) + + # 发现冲突,需要合并 + # 策略:保留 current_user (MP User, with real OpenID),合并 existing_user (Web User) 的数据 + # 仅当 existing_user 是 Web 用户 (openid startswith 'web_') 时才合并 + # 如果 existing_user 也是 MP 用户 (real openid),则提示冲突,不允许绑定 + + if not existing_user.openid.startswith('web_'): + return Response({'error': '该手机号已被其他微信账号绑定,无法重复绑定'}, status=status.HTTP_409_CONFLICT) + + # 执行合并 + from django.db import transaction + with transaction.atomic(): + # 1. 迁移订单 + Order.objects.filter(wechat_user=existing_user).update(wechat_user=current_user) + # 2. 迁移社区 ActivitySignup + from community.models import ActivitySignup, Topic, Reply + ActivitySignup.objects.filter(user=existing_user).update(user=current_user) + # 3. 迁移 Topic + Topic.objects.filter(author=existing_user).update(author=current_user) + # 4. 迁移 Reply + Reply.objects.filter(author=existing_user).update(author=current_user) + # 5. 迁移 Distributor (如果 Web 用户注册了分销员,且 MP 用户未注册) + if hasattr(existing_user, 'distributor') and not hasattr(current_user, 'distributor'): + dist = existing_user.distributor + dist.user = current_user + dist.save() + + # 删除旧 Web 用户 + existing_user.delete() + + # 更新当前用户手机号 + current_user.phone_number = phone + current_user.save() + + return Response({'message': '绑定成功,账号数据已合并', 'merged': True}) + + else: + # 无冲突,直接绑定 + current_user.phone_number = phone + current_user.save() + return Response({'message': '绑定成功', 'merged': False}) + + class DistributorViewSet(viewsets.GenericViewSet): """ 分销员接口 diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index e8bb771..0095f11 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,5 +1,6 @@ import React from 'react' import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import { AuthProvider } from './context/AuthContext'; import Layout from './components/Layout'; import Home from './pages/Home'; import ProductDetail from './pages/ProductDetail'; @@ -14,20 +15,22 @@ import './App.css'; function App() { return ( - - - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - + + + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + ) } diff --git a/frontend/src/api.js b/frontend/src/api.js index 2603283..e6aa74e 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -8,6 +8,17 @@ const api = axios.create({ } }); +// 请求拦截器:自动附加 Token +api.interceptors.request.use((config) => { + const token = localStorage.getItem('token'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}, (error) => { + return Promise.reject(error); +}); + export const getConfigs = () => api.get('/configs/'); export const createOrder = (data) => api.post('/orders/', data); export const nativePay = (data) => api.post('/pay/', data); @@ -25,5 +36,15 @@ 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); +export const phoneLogin = (data) => api.post('/auth/phone-login/', data); +export const getUserInfo = () => { + const token = localStorage.getItem('token'); + // 如果没有获取用户信息的接口,可以暂时从本地解析或依赖 update_user_info 的返回 + // 但后端有 /wechat/update/ 可以返回用户信息,或者我们可以加一个 /auth/me/ + // 目前 phone_login 返回了用户信息,前端可以保存。 + // 如果需要刷新,可以复用 update_user_info(虽然名字叫update,但传空通常返回当前信息,需确认后端逻辑) + // 查看后端逻辑:update_user_info 是 patch 更新,如果 data 为空,update 不会执行但会返回 serializer.data + return api.post('/wechat/update/', {}); +}; export default api; diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx index 268a24c..06d2592 100644 --- a/frontend/src/components/Layout.jsx +++ b/frontend/src/components/Layout.jsx @@ -1,9 +1,11 @@ import React, { useState, useEffect } from 'react'; -import { Layout as AntLayout, Menu, ConfigProvider, theme, Drawer, Button } from 'antd'; -import { RobotOutlined, MenuOutlined, AppstoreOutlined, EyeOutlined, SearchOutlined } from '@ant-design/icons'; +import { Layout as AntLayout, Menu, ConfigProvider, theme, Drawer, Button, Avatar, Dropdown } from 'antd'; +import { RobotOutlined, MenuOutlined, AppstoreOutlined, EyeOutlined, SearchOutlined, UserOutlined, LogoutOutlined, WechatOutlined } from '@ant-design/icons'; import { useNavigate, useLocation, useSearchParams } from 'react-router-dom'; import ParticleBackground from './ParticleBackground'; +import LoginModal from './LoginModal'; import { motion, AnimatePresence } from 'framer-motion'; +import { useAuth } from '../context/AuthContext'; const { Header, Content, Footer } = AntLayout; @@ -12,6 +14,9 @@ const Layout = ({ children }) => { const location = useLocation(); const [searchParams] = useSearchParams(); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + const [loginVisible, setLoginVisible] = useState(false); + + const { user, login, logout } = useAuth(); // 全局监听并持久化 ref 参数 useEffect(() => { @@ -22,6 +27,22 @@ const Layout = ({ children }) => { } }, [searchParams]); + const handleLogout = () => { + logout(); + navigate('/'); + }; + + const userMenu = { + items: [ + { + key: 'logout', + label: '退出登录', + icon: , + onClick: handleLogout + } + ] + }; + const items = [ { key: '/', @@ -43,14 +64,9 @@ const Layout = ({ children }) => { icon: , label: '我的订单', }, - { - key: 'more', - label: '...', - }, ]; const handleMenuClick = (key) => { - if (key === 'more') return; navigate(key); setMobileMenuOpen(false); }; @@ -112,7 +128,7 @@ const Layout = ({ children }) => { {/* Desktop Menu */} -
+
{ borderBottom: 'none', display: 'flex', justifyContent: 'flex-end', - minWidth: '400px' + minWidth: '400px', + marginRight: '20px' }} /> + + {user ? ( +
+ {/* 小程序图标状态 */} + + + +
+ } style={{ marginRight: 8 }} /> + {user.nickname} +
+
+
+ ) : ( + + )}
@@ -153,6 +193,17 @@ const Layout = ({ children }) => { open={mobileMenuOpen} styles={{ body: { padding: 0, background: '#111' }, header: { background: '#111', borderBottom: '1px solid #333' }, wrapper: { width: 250 } }} > +
+ {user ? ( +
+ } size="large" style={{ marginBottom: 10 }} /> +
{user.nickname}
+ +
+ ) : ( + + )} +
{ /> + setLoginVisible(false)} + onLoginSuccess={(userData) => login(userData)} + /> +
{ + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + const [countdown, setCountdown] = useState(0); + + const handleSendCode = async () => { + try { + const phone = form.getFieldValue('phone_number'); + if (!phone) { + message.error('请输入手机号'); + return; + } + + // 简单的手机号校验 + if (!/^1[3-9]\d{9}$/.test(phone)) { + message.error('请输入有效的手机号'); + return; + } + + await sendSms({ phone_number: phone }); + message.success('验证码已发送'); + + setCountdown(60); + const timer = setInterval(() => { + setCountdown((prev) => { + if (prev <= 1) { + clearInterval(timer); + return 0; + } + return prev - 1; + }); + }, 1000); + + } catch (error) { + console.error(error); + message.error('发送失败: ' + (error.response?.data?.error || '网络错误')); + } + }; + + const handleSubmit = async (values) => { + setLoading(true); + try { + const res = await phoneLogin(values); + + message.success('登录成功'); + onLoginSuccess(res.data); + onClose(); + } catch (error) { + console.error(error); + message.error('登录失败: ' + (error.response?.data?.error || '网络错误')); + } finally { + setLoading(false); + } + }; + + return ( + +
+ + } + placeholder="手机号码" + size="large" + /> + + + +
+ } + placeholder="验证码" + size="large" + /> + +
+
+ + + + + +
+ 未注册的手机号验证后将自动创建账号
+ 已在小程序绑定的手机号将自动同步身份 +
+
+
+ ); +}; + +export default LoginModal; diff --git a/frontend/src/context/AuthContext.jsx b/frontend/src/context/AuthContext.jsx new file mode 100644 index 0000000..973540c --- /dev/null +++ b/frontend/src/context/AuthContext.jsx @@ -0,0 +1,49 @@ +import React, { createContext, useState, useEffect, useContext } from 'react'; + +const AuthContext = createContext(null); + +export const AuthProvider = ({ children }) => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const storedUser = localStorage.getItem('user'); + if (storedUser) { + try { + setUser(JSON.parse(storedUser)); + } catch (e) { + console.error("Failed to parse user from storage", e); + localStorage.removeItem('user'); + } + } + setLoading(false); + }, []); + + const login = (userData) => { + setUser(userData); + localStorage.setItem('user', JSON.stringify(userData)); + if (userData.token) { + localStorage.setItem('token', userData.token); + } + }; + + const logout = () => { + setUser(null); + localStorage.removeItem('user'); + localStorage.removeItem('token'); + }; + + const updateUser = (data) => { + const newUser = { ...user, ...data }; + setUser(newUser); + localStorage.setItem('user', JSON.stringify(newUser)); + }; + + return ( + + {children} + + ); +}; + +export const useAuth = () => useContext(AuthContext); diff --git a/frontend/src/pages/MyOrders.jsx b/frontend/src/pages/MyOrders.jsx index fe21286..99f4335 100644 --- a/frontend/src/pages/MyOrders.jsx +++ b/frontend/src/pages/MyOrders.jsx @@ -1,54 +1,72 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { Form, Input, Button, Card, List, Tag, Typography, message, Space, Statistic, Divider, Modal, Descriptions } from 'antd'; import { MobileOutlined, LockOutlined, SearchOutlined, CarOutlined, InboxOutlined, SafetyCertificateOutlined, CheckCircleOutlined, ClockCircleOutlined, CloseCircleOutlined, UserOutlined, EnvironmentOutlined, PhoneOutlined } from '@ant-design/icons'; -import { sendSms, queryMyOrders } from '../api'; +import { queryMyOrders } from '../api'; import { motion } from 'framer-motion'; +import LoginModal from '../components/LoginModal'; +import { useAuth } from '../context/AuthContext'; const { Title, Text, Paragraph } = Typography; const MyOrders = () => { - const [step, setStep] = useState(0); // 0: Input Phone, 1: Verify Code, 2: Show Orders const [loading, setLoading] = useState(false); - const [phone, setPhone] = useState(''); const [orders, setOrders] = useState([]); const [modalVisible, setModalVisible] = useState(false); const [currentOrder, setCurrentOrder] = useState(null); - const [form] = Form.useForm(); + const [loginVisible, setLoginVisible] = useState(false); + + const { user, login } = useAuth(); + + useEffect(() => { + if (user) { + // 如果已登录,自动查询订单 + if (user.phone_number) { + handleQueryOrders(user.phone_number); + } + } else { + // Don't auto-show login modal on mount if not logged in, just show the "Please login" UI + // setLoginVisible(true); + } + }, [user]); const showDetail = (order) => { setCurrentOrder(order); setModalVisible(true); }; - const handleSendSms = async (values) => { + const handleQueryOrders = async (phone) => { setLoading(true); try { - const { phone_number } = values; - await sendSms({ phone_number }); - message.success('验证码已发送'); - setPhone(phone_number); - setStep(1); - } catch (error) { - console.error(error); - message.error('发送验证码失败,请重试'); - } finally { - setLoading(false); - } - }; - - const handleQueryOrders = async (values) => { - setLoading(true); - try { - const { code } = values; - const response = await queryMyOrders({ phone_number: phone, code }); + // 使用 queryMyOrders 接口,这里我们需要调整该接口以支持仅传手机号(如果已登录) + // 或者,既然已登录,后端应该能通过 Token 知道是谁,直接查这个人的订单 + // 但目前的 queryMyOrders 是 POST {phone_number, code},这主要用于免登录查询 + // 我们应该使用 OrderViewSet 的 list 方法,它已经支持 filter(wechat_user=user) + // 但前端 api.js 中 getOrder 是查单个,我们需要一个 getMyOrders 接口 + + // 修改策略:如果已登录,直接调用 queryMyOrders,但不需要 code? + // 后端 my_orders 接口目前强制需要 code。 + // 应该使用 OrderViewSet 的标准 list 接口,它会根据 Token 返回自己的订单。 + // api.js 中没有导出 getOrders list 接口,我们可以临时用 queryMyOrders 但绕过 code 检查? + // 不,最好的方式是使用标准的 GET /orders/,后端 OrderViewSet.get_queryset 已经处理了 get_current_wechat_user + + // 让我们先用 GET /orders/ 试试,需要在 api.js 确认是否有 export + // 检查 api.js 发现没有 getOrderList, 只有 getOrder(id) + // 我们需要修改 api.js 或在此处直接调用 + + // 为了不修改 api.js 太多,我们引入 axios 实例自己发请求,或者假设 api.js 有一个 getMyOrderList + // 实际上,查看 api.js, queryMyOrders 是 POST /orders/my_orders/,这是免登录版本 + // 我们应该用 GET /orders/,因为 get_queryset 已经过滤了。 + + // 临时引入 api 实例 + const { default: api } = await import('../api'); + const response = await api.get('/orders/'); setOrders(response.data); - setStep(2); if (response.data.length === 0) { - message.info('未查询到相关订单'); + message.info('您暂时没有订单'); } } catch (error) { console.error(error); - message.error('验证失败或查询出错'); + message.error('查询出错'); } finally { setLoading(false); } @@ -75,119 +93,32 @@ const MyOrders = () => {
- 我的订单查询 + 我的订单 Secure Order Verification System
- {step < 2 ? ( - - -
- {step === 0 && ( - - } - placeholder="请输入下单时的手机号" - style={{ - background: 'rgba(255,255,255,0.05)', - border: '1px solid rgba(255,255,255,0.1)', - color: '#fff', - height: 50, - borderRadius: 8 - }} - /> - - )} - - {step === 1 && ( - <> -
- 已发送验证码至 {phone} - -
- - } - placeholder="请输入6位验证码" - maxLength={6} - style={{ - background: 'rgba(255,255,255,0.05)', - border: '1px solid rgba(255,255,255,0.1)', - color: '#fff', - height: 50, - borderRadius: 8, - textAlign: 'center', - letterSpacing: '8px', - fontSize: '20px' - }} - /> - - - )} - - - - -
-
-
+ {!user ? ( +
+ 请先登录以查看您的订单 + +
) : ( -
+
+ 当前登录用户: {user.nickname}
( { )} + + setLoginVisible(false)} + onLoginSuccess={(userData) => { + login(userData); + if (userData.phone_number) { + handleQueryOrders(userData.phone_number); + } + }} + />
); diff --git a/frontend/src/pages/ProductDetail.jsx b/frontend/src/pages/ProductDetail.jsx index da032c7..bfd2634 100644 --- a/frontend/src/pages/ProductDetail.jsx +++ b/frontend/src/pages/ProductDetail.jsx @@ -4,6 +4,7 @@ import { Button, Row, Col, Tag, Statistic, Modal, Form, Input, InputNumber, mess import { ShoppingCartOutlined, SafetyCertificateOutlined, ThunderboltOutlined, EyeOutlined, StarOutlined } from '@ant-design/icons'; import { getConfigs, createOrder, nativePay } from '../api'; import ModelViewer from '../components/ModelViewer'; +import { useAuth } from '../context/AuthContext'; import './ProductDetail.css'; const ProductDetail = () => { @@ -16,9 +17,22 @@ const ProductDetail = () => { const [submitting, setSubmitting] = useState(false); const [form] = Form.useForm(); + const { user } = useAuth(); + // 优先从 URL 获取,如果没有则从 localStorage 获取,不再默认绑定 flw666 const refCode = searchParams.get('ref') || localStorage.getItem('ref_code'); + useEffect(() => { + // 自动填充用户信息 + if (user) { + form.setFieldsValue({ + phone_number: user.phone_number, + // 如果后端返回了地址信息,这里也可以填充 + // shipping_address: user.shipping_address + }); + } + }, [isModalOpen, user]); // 当弹窗打开或用户状态变化时填充 + useEffect(() => { console.log('[ProductDetail] Current ref_code:', refCode); }, [refCode]);