From 1ae31c89da9166afa837e4a4bf2d9fdd9f24131f Mon Sep 17 00:00:00 2001 From: sdz <2334724531@qq.com> Date: Mon, 10 Jun 2024 18:32:46 +0800 Subject: [PATCH] sdz --- .idea/.gitignore | 8 + .idea/dailyfresh-master.iml | 34 + .idea/dataSources.xml | 14 + .idea/encodings.xml | 6 + .idea/inspectionProfiles/Project_Default.xml | 24 + .../inspectionProfiles/profiles_settings.xml | 6 + .idea/misc.xml | 7 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + README.md | 198 ++++++ apps/cart/admin.py | 3 + apps/cart/apps.py | 5 + apps/cart/models.py | 3 + apps/cart/tests.py | 3 + apps/cart/urls.py | 11 + apps/cart/views.py | 161 +++++ apps/goods/admin.py | 54 ++ apps/goods/apps.py | 5 + apps/goods/migrations/0001_initial.py | 150 ++++ apps/goods/models.py | 125 ++++ apps/goods/search_indexes.py | 19 + apps/goods/tests.py | 3 + apps/goods/urls.py | 10 + apps/goods/views.py | 197 ++++++ apps/order/admin.py | 3 + apps/order/alipay_public_key.pem | 3 + apps/order/app_private_key.pem | 27 + apps/order/apps.py | 5 + apps/order/migrations/0001_initial.py | 51 ++ .../migrations/0002_auto_20190726_1854.py | 40 ++ apps/order/models.py | 65 ++ apps/order/tests.py | 62 ++ apps/order/urls.py | 12 + apps/order/views.py | 552 +++++++++++++++ apps/user/admin.py | 3 + apps/user/apps.py | 5 + apps/user/migrations/0001_initial.py | 69 ++ apps/user/models.py | 49 ++ apps/user/tests.py | 3 + apps/user/urls.py | 19 + apps/user/views.py | 215 ++++++ celery_tasks/tasks.py | 73 ++ dailyfresh/settings.py | 202 ++++++ dailyfresh/urls.py | 37 + dailyfresh/wsgi.py | 16 + db/base_model.py | 11 + img.png | Bin 0 -> 2086 bytes img_1.png | Bin 0 -> 2086 bytes img_2.png | Bin 0 -> 2509 bytes img_3.png | Bin 0 -> 2509 bytes manage.py | 21 + my_files/my_nginx/my_nginx01.txt | 51 ++ my_files/my_nginx/my_nginx02.txt | 21 + my_files/my_redis/my_redis01.txt | 215 ++++++ my_files/my_redis/my_redis02.txt | 25 + requirements.txt | Bin 0 -> 2356 bytes static/cart.html | 109 +++ static/css/main.css | 642 ++++++++++++++++++ static/css/reset.css | 27 + static/detail.html | 178 +++++ static/images/adv01.jpg | Bin 0 -> 13988 bytes static/images/adv02.jpg | Bin 0 -> 15859 bytes static/images/banner01.jpg | Bin 0 -> 9919 bytes static/images/banner02.jpg | Bin 0 -> 18351 bytes static/images/banner03.jpg | Bin 0 -> 13013 bytes static/images/banner04.jpg | Bin 0 -> 10823 bytes static/images/banner05.jpg | Bin 0 -> 16081 bytes static/images/banner06.jpg | Bin 0 -> 14257 bytes static/images/down.png | Bin 0 -> 1064 bytes static/images/fruit.jpg | Bin 0 -> 566641 bytes static/images/goods.jpg | Bin 0 -> 6100 bytes static/images/goods/goods001.jpg | Bin 0 -> 8349 bytes static/images/goods/goods002.jpg | Bin 0 -> 9102 bytes static/images/goods/goods003.jpg | Bin 0 -> 9612 bytes static/images/goods/goods004.jpg | Bin 0 -> 7213 bytes static/images/goods/goods005.jpg | Bin 0 -> 8626 bytes static/images/goods/goods006.jpg | Bin 0 -> 8672 bytes static/images/goods/goods007.jpg | Bin 0 -> 6596 bytes static/images/goods/goods008.jpg | Bin 0 -> 8822 bytes static/images/goods/goods009.jpg | Bin 0 -> 6761 bytes static/images/goods/goods010.jpg | Bin 0 -> 5791 bytes static/images/goods/goods011.jpg | Bin 0 -> 8364 bytes static/images/goods/goods012.jpg | Bin 0 -> 7864 bytes static/images/goods/goods013.jpg | Bin 0 -> 9950 bytes static/images/goods/goods014.jpg | Bin 0 -> 9584 bytes static/images/goods/goods015.jpg | Bin 0 -> 7369 bytes static/images/goods/goods016.jpg | Bin 0 -> 8301 bytes static/images/goods/goods017.jpg | Bin 0 -> 8770 bytes static/images/goods/goods018.jpg | Bin 0 -> 9424 bytes static/images/goods/goods019.jpg | Bin 0 -> 9457 bytes static/images/goods/goods020.jpg | Bin 0 -> 9320 bytes static/images/goods/goods021.jpg | Bin 0 -> 14649 bytes static/images/goods02.jpg | Bin 0 -> 52723 bytes static/images/goods_detail.jpg | Bin 0 -> 57765 bytes static/images/icons.png | Bin 0 -> 5931 bytes static/images/icons02.png | Bin 0 -> 2071 bytes static/images/interval_line.png | Bin 0 -> 1123 bytes static/images/left_bg.jpg | Bin 0 -> 1367 bytes static/images/login_banner.png | Bin 0 -> 156498 bytes static/images/logo.png | Bin 0 -> 9987 bytes static/images/logo02.png | Bin 0 -> 18818 bytes static/images/pay_icons.png | Bin 0 -> 11917 bytes static/images/register_banner.png | Bin 0 -> 54894 bytes static/images/shop_cart.png | Bin 0 -> 1317 bytes static/images/slide.jpg | Bin 0 -> 43271 bytes static/images/slide02.jpg | Bin 0 -> 46855 bytes static/images/slide03.jpg | Bin 0 -> 70593 bytes static/images/slide04.jpg | Bin 0 -> 62570 bytes static/index.html | 352 ++++++++++ static/js/jquery-1.12.4.min.js | 5 + static/js/jquery-ui.min.js | 8 + static/js/jquery.cookie.js | 117 ++++ static/js/register.js | 93 +++ static/js/slide.js | 130 ++++ static/list.html | 267 ++++++++ static/login.html | 56 ++ static/place_order.html | 150 ++++ static/register.html | 76 +++ static/user_center_info.html | 136 ++++ static/user_center_order.html | 142 ++++ static/user_center_site.html | 103 +++ static/页面说明.txt | 29 + templates/cart.html | 313 +++++++++ templates/detail.html | 283 ++++++++ templates/index.html | 152 +++++ templates/list.html | 151 ++++ templates/login.html | 58 ++ templates/order_comment.html | 116 ++++ templates/place_order.html | 176 +++++ templates/register.html | 78 +++ .../search/indexes/goods/goodssku_text.txt | 4 + templates/search/search.html | 131 ++++ templates/static_index.html | 144 ++++ templates/user_center_info.html | 113 +++ templates/user_center_order.html | 179 +++++ templates/user_center_site.html | 117 ++++ utils/fdfs/storage.py | 42 ++ utils/mixin.py | 9 + whoosh_index/MAIN_8gchhuors4r4wl2z.seg | Bin 0 -> 5451 bytes whoosh_index/MAIN_cv27rpuzg7pb1moa.seg | Bin 0 -> 5452 bytes whoosh_index/MAIN_j087x4bxh62fq63q.seg | Bin 0 -> 12026 bytes whoosh_index/MAIN_kv5ri4upimb585kg.seg | Bin 0 -> 5452 bytes whoosh_index/MAIN_rerhqn47ququ75xh.seg | Bin 0 -> 5451 bytes whoosh_index/_MAIN_5.toc | Bin 0 -> 2576 bytes 144 files changed, 7601 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/dailyfresh-master.iml create mode 100644 .idea/dataSources.xml create mode 100644 .idea/encodings.xml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 README.md create mode 100644 apps/cart/admin.py create mode 100644 apps/cart/apps.py create mode 100644 apps/cart/models.py create mode 100644 apps/cart/tests.py create mode 100644 apps/cart/urls.py create mode 100644 apps/cart/views.py create mode 100644 apps/goods/admin.py create mode 100644 apps/goods/apps.py create mode 100644 apps/goods/migrations/0001_initial.py create mode 100644 apps/goods/models.py create mode 100644 apps/goods/search_indexes.py create mode 100644 apps/goods/tests.py create mode 100644 apps/goods/urls.py create mode 100644 apps/goods/views.py create mode 100644 apps/order/admin.py create mode 100644 apps/order/alipay_public_key.pem create mode 100644 apps/order/app_private_key.pem create mode 100644 apps/order/apps.py create mode 100644 apps/order/migrations/0001_initial.py create mode 100644 apps/order/migrations/0002_auto_20190726_1854.py create mode 100644 apps/order/models.py create mode 100644 apps/order/tests.py create mode 100644 apps/order/urls.py create mode 100644 apps/order/views.py create mode 100644 apps/user/admin.py create mode 100644 apps/user/apps.py create mode 100644 apps/user/migrations/0001_initial.py create mode 100644 apps/user/models.py create mode 100644 apps/user/tests.py create mode 100644 apps/user/urls.py create mode 100644 apps/user/views.py create mode 100644 celery_tasks/tasks.py create mode 100644 dailyfresh/settings.py create mode 100644 dailyfresh/urls.py create mode 100644 dailyfresh/wsgi.py create mode 100644 db/base_model.py create mode 100644 img.png create mode 100644 img_1.png create mode 100644 img_2.png create mode 100644 img_3.png create mode 100644 manage.py create mode 100644 my_files/my_nginx/my_nginx01.txt create mode 100644 my_files/my_nginx/my_nginx02.txt create mode 100644 my_files/my_redis/my_redis01.txt create mode 100644 my_files/my_redis/my_redis02.txt create mode 100644 requirements.txt create mode 100644 static/cart.html create mode 100644 static/css/main.css create mode 100644 static/css/reset.css create mode 100644 static/detail.html create mode 100644 static/images/adv01.jpg create mode 100644 static/images/adv02.jpg create mode 100644 static/images/banner01.jpg create mode 100644 static/images/banner02.jpg create mode 100644 static/images/banner03.jpg create mode 100644 static/images/banner04.jpg create mode 100644 static/images/banner05.jpg create mode 100644 static/images/banner06.jpg create mode 100644 static/images/down.png create mode 100644 static/images/fruit.jpg create mode 100644 static/images/goods.jpg create mode 100644 static/images/goods/goods001.jpg create mode 100644 static/images/goods/goods002.jpg create mode 100644 static/images/goods/goods003.jpg create mode 100644 static/images/goods/goods004.jpg create mode 100644 static/images/goods/goods005.jpg create mode 100644 static/images/goods/goods006.jpg create mode 100644 static/images/goods/goods007.jpg create mode 100644 static/images/goods/goods008.jpg create mode 100644 static/images/goods/goods009.jpg create mode 100644 static/images/goods/goods010.jpg create mode 100644 static/images/goods/goods011.jpg create mode 100644 static/images/goods/goods012.jpg create mode 100644 static/images/goods/goods013.jpg create mode 100644 static/images/goods/goods014.jpg create mode 100644 static/images/goods/goods015.jpg create mode 100644 static/images/goods/goods016.jpg create mode 100644 static/images/goods/goods017.jpg create mode 100644 static/images/goods/goods018.jpg create mode 100644 static/images/goods/goods019.jpg create mode 100644 static/images/goods/goods020.jpg create mode 100644 static/images/goods/goods021.jpg create mode 100644 static/images/goods02.jpg create mode 100644 static/images/goods_detail.jpg create mode 100644 static/images/icons.png create mode 100644 static/images/icons02.png create mode 100644 static/images/interval_line.png create mode 100644 static/images/left_bg.jpg create mode 100644 static/images/login_banner.png create mode 100644 static/images/logo.png create mode 100644 static/images/logo02.png create mode 100644 static/images/pay_icons.png create mode 100644 static/images/register_banner.png create mode 100644 static/images/shop_cart.png create mode 100644 static/images/slide.jpg create mode 100644 static/images/slide02.jpg create mode 100644 static/images/slide03.jpg create mode 100644 static/images/slide04.jpg create mode 100644 static/index.html create mode 100644 static/js/jquery-1.12.4.min.js create mode 100644 static/js/jquery-ui.min.js create mode 100644 static/js/jquery.cookie.js create mode 100644 static/js/register.js create mode 100644 static/js/slide.js create mode 100644 static/list.html create mode 100644 static/login.html create mode 100644 static/place_order.html create mode 100644 static/register.html create mode 100644 static/user_center_info.html create mode 100644 static/user_center_order.html create mode 100644 static/user_center_site.html create mode 100644 static/页面说明.txt create mode 100644 templates/cart.html create mode 100644 templates/detail.html create mode 100644 templates/index.html create mode 100644 templates/list.html create mode 100644 templates/login.html create mode 100644 templates/order_comment.html create mode 100644 templates/place_order.html create mode 100644 templates/register.html create mode 100644 templates/search/indexes/goods/goodssku_text.txt create mode 100644 templates/search/search.html create mode 100644 templates/static_index.html create mode 100644 templates/user_center_info.html create mode 100644 templates/user_center_order.html create mode 100644 templates/user_center_site.html create mode 100644 utils/fdfs/storage.py create mode 100644 utils/mixin.py create mode 100644 whoosh_index/MAIN_8gchhuors4r4wl2z.seg create mode 100644 whoosh_index/MAIN_cv27rpuzg7pb1moa.seg create mode 100644 whoosh_index/MAIN_j087x4bxh62fq63q.seg create mode 100644 whoosh_index/MAIN_kv5ri4upimb585kg.seg create mode 100644 whoosh_index/MAIN_rerhqn47ququ75xh.seg create mode 100644 whoosh_index/_MAIN_5.toc diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..35410ca --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/dailyfresh-master.iml b/.idea/dailyfresh-master.iml new file mode 100644 index 0000000..400a4de --- /dev/null +++ b/.idea/dailyfresh-master.iml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..f089a37 --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,14 @@ + + + + + mysql.8 + true + true + $PROJECT_DIR$/dailyfresh/settings.py + com.mysql.cj.jdbc.Driver + jdbc:mysql://127.0.0.1:3306/dailyfresh + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..2cb56ea --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..b250b4c --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,24 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..6214f44 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..dd0cffb --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f8b4f9c --- /dev/null +++ b/README.md @@ -0,0 +1,198 @@ +# Python-Django-天天生鲜项目 + +初学django框架时按照传智播客python教程所学习的项目,该项目包含了实际开发中的电商项目中大部分的功能开发和知识点实践。 + +功能:用户注册,用户登录,购物车,用户中心,首页,订单系统,地址信息管理,商品列表,商品详情,支付功能等等,是一个完整的电商项目流程 + +## 技术栈 +python + django + mysql + redis + celery + FastDFS(分布式图片服务器) + nginx + + +## 目标功能: +- [x] 功能模块 + - [x] 用户模块 + - [x] 注册 + - [x] 登录 + - [x] 激活(celery) + - [x] 退出 + - [x] 个人中心 + - [x] 地址管理 + - [x] 商品模块 + - [x] 首页(celery) + - [x] 商品详情 + - [x] 商品列表 + - [x] 搜索功能(haystack+whoose) + - [x] 购物车模块(redis) + - [x] 增加 + - [x] 删除 + - [x] 修改 + - [x] 查询 + - [x] 订单模块 + - [x] 确认订单页面 + - [x] 订单创建 + - [x] 请求支付(支付宝) + - [x] 查询支付结果 + - [x] 评论 + + + +- 项目启动: + - **注意: 项目启动前请先安装好各个环境,mysql+redis+nginx+fastDFS+celery等** + ``` + 项目包安装 + pip install -r requirements.txt + + Django启动命令 + python manage.py runserver + ``` +- uwsgi web服务器启动: + - **注意: uwsgi开启需要修改[配置文件](./dailyfresh/settings.py)中的DEBUG和ALLOWED_HOSTS** + ``` + 启动: uwsgi --ini 配置文件路径 / uwsgi --ini uwsgi.ini + 停止: uwsgi --stop uwsgi.pid路径 / uwsgi --stop uwsgi.pid + ``` +- celery分布式任务队列启动 + ``` + celery -A celery_tasks.tasks worker -l info + ``` +- redis服务端启动 + ``` + sudo redis-server /etc/redis/redis.conf + ``` +- FastDFS服务启动 + ``` + Trackerd服务 + sudo /usr/bin/fdfs_trackerd /etc/fdfs/tracker.conf start + + storge服务 + sudo /usr/bin/fdfs_storaged /etc/fdfs/storage.conf start + ``` +- nginx + ``` + 启动nginx + sudo /usr/local/nginx/sbin/nginx + 重启nginx + sudo /usr/local/nginx/sbin/nginx -s reload + ``` +- 建立索引文件--搜索引擎 + 新环境需要配置jieba分词,生成[whoose_cn_backend]()文件 + ``` + python manage.py rebuild_index + ``` +- mysql事务隔离级别设置 + ``` + sudo vim /etc/mysql/mysql.conf.d/mysql.cnf + transaction-isolation = READ-COMMITTED (读已提交) + ``` +## 项目包介绍 +``` +alipay-sdk-python==3.7.137 +amqp==5.2.0 +asgiref==3.8.1 +asn1crypto==0.24.0 +async-timeout==4.0.3 +Authlib==0.5.1 +billiard==4.2.0 +Brotli==1.1.0 +celery==5.4.0 +certifi==2024.2.2 +cffi==1.16.0 +chardet==3.0.4 +charset-normalizer==3.3.2 +click==8.1.7 +click-didyoumean==0.3.1 +click-plugins==1.1.1 +click-repl==0.3.0 +colorama==0.4.6 +configparser==3.5.0 +construct==2.5.3 +cryptography==42.0.7 +Django==3.2.13 +django-filter==24.2 +django-haystack==3.2.1 +django-redis==5.4.0 +django-redis-sessions==0.5.6 +django-tinymce==4.0.0 +djangorestframework==3.15.1 +djangorestframework-jwt==1.11.0 +haystack==0.42 +idna==3.7 +itsdangerous==2.2.0 +jieba==0.42.1 +kombu==5.3.7 +mysqlclient==2.2.4 +pefile==2023.2.7 +Pillow==9.5.0 +prompt_toolkit==3.0.45 +pyasn1==0.6.0 +pycparser==2.22 +pycryptodome==3.20.0 +pycryptodomex==3.20.0 +pygame==2.5.2 +PyJWT==1.7.1 +PyMySQL==1.0.2 +pyOpenSSL==24.1.0 +python-alipay-sdk==3.3.0 +python-dateutil==2.9.0.post0 +python-ptrace==0.9.9 +pytz==2024.1 +redis==5.0.4 +requests==2.32.3 +rsa==4.9 +six==1.16.0 +sqlparse==0.5.0 +typing_extensions==4.12.0 +tzdata==2024.1 +urllib3==2.2.1 +vine==5.1.0 +wcwidth==0.2.13 +Whoosh==2.7.4 +wincertstore==0.2.1 + +``` +## 注意点 +pip install fdfs_client-py-master 存在bug,需要下载特定版本 +redis版本需要2.10.6 否则会报错,因为使用django的版本过低问题 +如果使用乐观锁,需要修改mysql事务的隔离级别设置 + +## 总结 +需求分析 +1.1 用户模块 +1) 注册页 + 注册时校验用户名是否已被注册。 + 完成用户信息的注册 + 给用户的注册邮箱发送邮件,用户点击邮件中的激活链接完成用户账户的激活。 +2)登陆页 + 实现用户的登录功能 +3)用户中心 + 用户中心信息页,显示登录用户的信息,包括用户名、电话和地址,同时页面下方显示出用户最近浏览的商品信息。 + 用户中心地址页:显示登陆用户的默认收件地址,页面下方的表单可以新增用户的收货地址。 + 用户中心订单页:显示登录用户的订单信息。 +4)其他 + 如果用户已经登陆,页面顶部显示用户的订单信息。 +1.2 商品模块 +1)首页 + 动态指定首页轮播商品信息。 + 动态指定首页活动信息。 + 动态获取商品的种类信息并显示。 + 动态指定首页显示的每个种类的商品(包括图片商品的文字商品)。 + 点击某一个商品时跳转到商品的详情页面。 +2)商品详情页 + 显示出某个商品的详细信息。 + 页面下方显示出该商品的两个新品信息。 +3)商品列表页 + 显示出某一个种类的商品的列表数据,分页显示并支持按照默认、价格和人气进行排序。 + 页面下方显示出该商品的两个新品信息。 +4)其他 + 通过搜索框搜索商品信息。 +1.3 购物车模块 + 列表页和详情页将商品添加到购物车。 + 用户登录后,首页,详情页,列表页显示用户购物车中的商品数目。 + 购物车页面:对用户购物车中的商品操作。如选择某件商品,增加或减少购物车中的商品数目。 +1.4 订单相关 + 提交订单页面:显示用户准备购买的商品信息。 + 点击提交订单完成订单的创建。 + 用户中心订单页显示用户的订单信息。 + 点击支付完成订单的支付。 + 点击评价完成订单的评价。 + diff --git a/apps/cart/admin.py b/apps/cart/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/apps/cart/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/cart/apps.py b/apps/cart/apps.py new file mode 100644 index 0000000..18a184c --- /dev/null +++ b/apps/cart/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class CartConfig(AppConfig): + name = 'apps.cart' diff --git a/apps/cart/models.py b/apps/cart/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/apps/cart/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/apps/cart/tests.py b/apps/cart/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/apps/cart/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/cart/urls.py b/apps/cart/urls.py new file mode 100644 index 0000000..4160d9e --- /dev/null +++ b/apps/cart/urls.py @@ -0,0 +1,11 @@ +from django.urls import path +from apps.cart.views import CartAddView, CartInfoView, CartUpdateView, CartDeleteView + +app_name = 'cart' + +urlpatterns = [ + path('add', CartAddView.as_view(), name='add'), # 添加购物车记录 + path('', CartInfoView.as_view(), name='show'), # 显示购物车页面 + path('update', CartUpdateView.as_view(), name='update'), # 更新购物车记录 + path('delete', CartDeleteView.as_view(), name='delete'), # 删除购物车记录 +] diff --git a/apps/cart/views.py b/apps/cart/views.py new file mode 100644 index 0000000..49a957e --- /dev/null +++ b/apps/cart/views.py @@ -0,0 +1,161 @@ +from django.http import JsonResponse +from django.shortcuts import render +from django.views.generic.base import View +from django_redis import get_redis_connection + +from apps.goods.models import GoodsSKU +from utils.mixin import LoginRequiredMixin + + +# 购物车视图 + +class CartAddView(View): + """购物车记录添加""" + + def post(self, request): + """处理购物车记录添加请求""" + user = request.user + if not user.is_authenticated: + return JsonResponse({'res': 0, 'errmsg': '请先登录'}) + + # 接收数据 + sku_id = request.POST.get('sku_id') + count = request.POST.get('count') + + # 数据校验 + if not all([sku_id, count]): + return JsonResponse({'res': 1, 'errmsg': '数据不完整'}) + + # 校验添加商品的数量 + try: + count = int(count) + except ValueError: + return JsonResponse({'res': 2, 'errmsg': '商品数量出错'}) + + # 校验商品是否存在 + try: + sku = GoodsSKU.objects.get(id=sku_id) + except GoodsSKU.DoesNotExist: + return JsonResponse({'res': 3, 'errmsg': '商品不存在'}) + + # 业务处理:添加购物车记录 + conn = get_redis_connection('default') + cart_key = f'cart_{user.id}' + cart_count = conn.hget(cart_key, sku_id) + if cart_count: + count += int(cart_count) + + # 校验商品的库存 + if count > sku.stock: + return JsonResponse({'res': 4, 'errmsg': '商品库存不足'}) + + conn.hset(cart_key, sku_id, count) + # 获取购物车商品条目数 + total_count = conn.hlen(cart_key) + + # 返回应答 + return JsonResponse({'res': 5, 'message': '添加成功', 'total_count': total_count}) + + +class CartInfoView(LoginRequiredMixin, View): + """显示购物车页面""" + + def get(self, request): + """处理显示购物车页面请求""" + user = request.user + + # 获取用户购物车中的商品信息 + conn = get_redis_connection('default') + cart_key = f'cart_{user.id}' + cart_dict = conn.hgetall(cart_key) + + skus = [] + total_count = 0 + total_price = 0 + + # 遍历获取商品的信息 + for sku_id, count in cart_dict.items(): + sku = GoodsSKU.objects.get(id=sku_id) + count = int(count) + amount = sku.price * count + sku.amount = amount + sku.count = count + skus.append(sku) + + total_count += count + total_price += amount + + context = { + 'total_count': total_count, + 'total_price': total_price, + 'skus': skus, + } + + return render(request, 'cart.html', context) + + +class CartUpdateView(View): + """购物车记录更新""" + + def post(self, request): + """处理购物车记录更新请求""" + user = request.user + if not user.is_authenticated: + return JsonResponse({'res': 0, 'errmsg': '请先登录'}) + + sku_id = request.POST.get('sku_id') + count = request.POST.get('count') + + if not all([sku_id, count]): + return JsonResponse({'res': 1, 'errmsg': '数据不完整'}) + + try: + count = int(count) + except ValueError: + return JsonResponse({'res': 2, 'errmsg': '商品数量出错'}) + + try: + sku = GoodsSKU.objects.get(id=sku_id) + except GoodsSKU.DoesNotExist: + return JsonResponse({'res': 3, 'errmsg': '商品不存在'}) + + conn = get_redis_connection('default') + cart_key = f'cart_{user.id}' + + if count > sku.stock: + return JsonResponse({'res': 4, 'errmsg': '商品库存不足'}) + + conn.hset(cart_key, sku_id, count) + + total_count = sum(int(val) for val in conn.hvals(cart_key)) + + return JsonResponse({'res': 5, 'message': '更新成功', 'total_count': total_count}) + + +class CartDeleteView(View): + """购物车记录删除""" + + def post(self, request): + """处理购物车记录删除请求""" + user = request.user + if not user.is_authenticated: + return JsonResponse({'res': 0, 'errmsg': '请先登录'}) + + sku_id = request.POST.get('sku_id') + + if not sku_id: + return JsonResponse({'res': 1, 'errmsg': '数据不完整'}) + + try: + sku = GoodsSKU.objects.get(id=sku_id) + except GoodsSKU.DoesNotExist: + return JsonResponse({'res': 2, 'errmsg': '商品不存在'}) + + conn = get_redis_connection('default') + cart_key = f'cart_{user.id}' + + conn.hdel(cart_key, sku_id) + + total_count = sum(int(val) for val in conn.hvals(cart_key)) + + return JsonResponse({'res': 5, 'message': '删除成功', 'total_count': total_count}) diff --git a/apps/goods/admin.py b/apps/goods/admin.py new file mode 100644 index 0000000..ea25c87 --- /dev/null +++ b/apps/goods/admin.py @@ -0,0 +1,54 @@ +from django.contrib import admin +from django.core.cache import cache + +from apps.goods.models import GoodsType, IndexPromotionBanner, IndexGoodsBanner, IndexTypeGoodsBanner, GoodsSKU, GoodsSPU, GoodsImage +# Register your models here. + + +class BaseModelAdmin(admin.ModelAdmin): + + def save_model(self, request, obj, form, change): + """新增或更新表中的数据时调用""" + super().save_model(request, obj, form, change) + + # 发出任务,让celery worker重新生成首页静态页面 + from celery_tasks.tasks import generate_static_index_html + generate_static_index_html.delay() + + # 清除缓存 + cache.delete('index_page_data') + + def delete_model(self, request, obj): + """新增或更新表中的数据时调用""" + super().delete_model(request, obj) + + # 发出任务,让celery worker重新生成首页静态页面 + from celery_tasks.tasks import generate_static_index_html + generate_static_index_html.delay() + # 清除缓存 + cache.delete('index_page_data') + + +class IndexPromotionBannerAdmin(BaseModelAdmin): + pass + + +class GoodsTypeAdmin(BaseModelAdmin): + pass + + +class IndexTpyeGoodsBannerAdmin(BaseModelAdmin): + pass + + +class IndexGoodsBannerAdmin(BaseModelAdmin): + pass + + +admin.site.register(GoodsType, GoodsTypeAdmin) +admin.site.register(IndexPromotionBanner, IndexPromotionBannerAdmin) +admin.site.register(IndexGoodsBanner, IndexGoodsBannerAdmin) +admin.site.register(IndexTypeGoodsBanner, IndexGoodsBannerAdmin) +admin.site.register(GoodsImage) +admin.site.register(GoodsSPU) +admin.site.register(GoodsSKU) \ No newline at end of file diff --git a/apps/goods/apps.py b/apps/goods/apps.py new file mode 100644 index 0000000..447f79c --- /dev/null +++ b/apps/goods/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class GoodsConfig(AppConfig): + name = 'apps.goods' diff --git a/apps/goods/migrations/0001_initial.py b/apps/goods/migrations/0001_initial.py new file mode 100644 index 0000000..d3f2237 --- /dev/null +++ b/apps/goods/migrations/0001_initial.py @@ -0,0 +1,150 @@ +# Generated by Django 2.2.3 on 2019-07-26 10:54 + +from django.db import migrations, models +import django.db.models.deletion +import tinymce.models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='GoodsSKU', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('is_delete', models.BooleanField(default=False, verbose_name='删除标记')), + ('name', models.CharField(max_length=20, verbose_name='商品名称')), + ('desc', models.CharField(max_length=256, verbose_name='商品简介')), + ('price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='商品价格')), + ('unite', models.CharField(max_length=20, verbose_name='商品单位')), + ('image', models.ImageField(upload_to='goods', verbose_name='商品图片')), + ('stock', models.IntegerField(default=1, verbose_name='商品库存')), + ('sales', models.IntegerField(default=0, verbose_name='商品销量')), + ('status', models.SmallIntegerField(choices=[(0, '下线'), (1, '上线')], default=1, verbose_name='商品状态')), + ], + options={ + 'verbose_name': '商品', + 'verbose_name_plural': '商品', + 'db_table': 'df_goods_sku', + }, + ), + migrations.CreateModel( + name='GoodsSPU', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('is_delete', models.BooleanField(default=False, verbose_name='删除标记')), + ('name', models.CharField(max_length=20, verbose_name='商品SPU名称')), + ('detail', tinymce.models.HTMLField(blank=True, verbose_name='商品详情')), + ], + options={ + 'verbose_name': '商品SPU', + 'verbose_name_plural': '商品SPU', + 'db_table': 'df_goods_spu', + }, + ), + migrations.CreateModel( + name='GoodsType', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('is_delete', models.BooleanField(default=False, verbose_name='删除标记')), + ('name', models.CharField(max_length=20, verbose_name='种类名称')), + ('logo', models.CharField(max_length=20, verbose_name='标识')), + ('image', models.ImageField(upload_to='type', verbose_name='商品类型图片')), + ], + options={ + 'verbose_name': '商品种类', + 'verbose_name_plural': '商品种类', + 'db_table': 'df_goods_type', + }, + ), + migrations.CreateModel( + name='IndexPromotionBanner', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('is_delete', models.BooleanField(default=False, verbose_name='删除标记')), + ('url', models.URLField(verbose_name='活动链接')), + ('name', models.CharField(max_length=20, verbose_name='活动名称')), + ('image', models.ImageField(upload_to='goods', verbose_name='图片路径')), + ('index', models.SmallIntegerField(default=0, verbose_name='展示顺序')), + ], + options={ + 'verbose_name': '主页促销活动', + 'verbose_name_plural': '主页促销活动', + 'db_table': 'df_index_promotion', + }, + ), + migrations.CreateModel( + name='IndexTypeGoodsBanner', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('is_delete', models.BooleanField(default=False, verbose_name='删除标记')), + ('display_type', models.SmallIntegerField(choices=[(0, '标题'), (1, '图片')], default=1, verbose_name='商品显示方式')), + ('index', models.SmallIntegerField(default=0, verbose_name='展示顺序')), + ('sku', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='goods.GoodsSKU', verbose_name='商品')), + ('type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='goods.GoodsType', verbose_name='商品类型')), + ], + options={ + 'verbose_name': '主页分类展示商品', + 'verbose_name_plural': '主页分类展示商品', + 'db_table': 'df_index_type_goods', + }, + ), + migrations.CreateModel( + name='IndexGoodsBanner', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('is_delete', models.BooleanField(default=False, verbose_name='删除标记')), + ('image', models.ImageField(upload_to='banner', verbose_name='图片')), + ('index', models.SmallIntegerField(default=0, verbose_name='展示顺序')), + ('sku', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='goods.GoodsSKU', verbose_name='商品')), + ], + options={ + 'verbose_name': '首页轮播图片', + 'verbose_name_plural': '首页轮播图片', + 'db_table': 'df_index_banner', + }, + ), + migrations.AddField( + model_name='goodssku', + name='goods_spu', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='goods.GoodsSPU', verbose_name='商品SPU'), + ), + migrations.AddField( + model_name='goodssku', + name='type', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='goods.GoodsType', verbose_name='商品种类'), + ), + migrations.CreateModel( + name='GoodsImage', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('is_delete', models.BooleanField(default=False, verbose_name='删除标记')), + ('image', models.ImageField(upload_to='goods', verbose_name='图片路径')), + ('sku', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='goods.GoodsSKU', verbose_name='商品')), + ], + options={ + 'verbose_name': '商品图片', + 'verbose_name_plural': '商品图片', + 'db_table': 'df_goods_image', + }, + ), + ] diff --git a/apps/goods/models.py b/apps/goods/models.py new file mode 100644 index 0000000..870fd22 --- /dev/null +++ b/apps/goods/models.py @@ -0,0 +1,125 @@ +from django.db import models +from tinymce.models import HTMLField +from db.base_model import BaseModel + +# Create your models here. + +class GoodsType(BaseModel): + """商品类型模型类""" + name = models.CharField(max_length=20, verbose_name='种类名称') + logo = models.CharField(max_length=20, verbose_name='标识') + image = models.ImageField(upload_to='type', verbose_name='商品类型图片') + + class Meta: + db_table = 'df_goods_type' + verbose_name = '商品种类' + verbose_name_plural = verbose_name + + def __str__(self): + return self.name + + +class GoodsSPU(BaseModel): + """商品SPU模型类""" + name = models.CharField(max_length=20, verbose_name='商品SPU名称') + detail = HTMLField(blank=True, verbose_name='商品详情') + + class Meta: + db_table = 'df_goods_spu' + verbose_name = '商品SPU' + verbose_name_plural = verbose_name + + def __str__(self): + return self.name + + +class GoodsSKU(BaseModel): + """商品SKU模型类""" + STATUS_CHOICES = ( + (0, '下线'), + (1, '上线'), + ) + + type = models.ForeignKey('GoodsType', on_delete=models.CASCADE, verbose_name='商品种类') + goods_spu = models.ForeignKey('GoodsSPU', on_delete=models.CASCADE, verbose_name='商品SPU') + name = models.CharField(max_length=20, verbose_name='商品名称') + desc = models.CharField(max_length=256, verbose_name='商品简介') + price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='商品价格') + unite = models.CharField(max_length=20, verbose_name='商品单位') + image = models.ImageField(upload_to='goods', verbose_name='商品图片') + stock = models.IntegerField(default=1, verbose_name='商品库存') + sales = models.IntegerField(default=0, verbose_name='商品销量') + status = models.SmallIntegerField(default=1, choices=STATUS_CHOICES, verbose_name='商品状态') + + class Meta: + db_table = 'df_goods_sku' + verbose_name = '商品' + verbose_name_plural = verbose_name + + def __str__(self): + return self.name + + +class GoodsImage(BaseModel): + """商品图片模型类""" + sku = models.ForeignKey('GoodsSKU', on_delete=models.CASCADE, verbose_name='商品') + image = models.ImageField(upload_to='goods', verbose_name='图片路径') + + class Meta: + db_table = 'df_goods_image' + verbose_name = '商品图片' + verbose_name_plural = verbose_name + + def __str__(self): + return self.sku.name + + +class IndexGoodsBanner(BaseModel): + """首页轮播商品展示模型类""" + sku = models.ForeignKey('GoodsSKU', on_delete=models.CASCADE, verbose_name='商品') + image = models.ImageField(upload_to='banner', verbose_name='图片') + index = models.SmallIntegerField(default=0, verbose_name='展示顺序') + + class Meta: + db_table = 'df_index_banner' + verbose_name = '首页轮播图片' + verbose_name_plural = verbose_name + + def __str__(self): + return self.sku.name + + +class IndexTypeGoodsBanner(BaseModel): + """首页分类商品展示模型类""" + DISPLAY_TYPE_CHOICES = ( + (0, '标题'), + (1, '图片'), + ) + type = models.ForeignKey('GoodsType', on_delete=models.CASCADE, verbose_name='商品类型') + sku = models.ForeignKey('GoodsSKU', on_delete=models.CASCADE, verbose_name='商品') + display_type = models.SmallIntegerField(default=1, choices=DISPLAY_TYPE_CHOICES, verbose_name='商品显示方式') + index = models.SmallIntegerField(default=0, verbose_name='展示顺序') + + class Meta: + db_table = 'df_index_type_goods' + verbose_name = '主页分类展示商品' + verbose_name_plural = verbose_name + + def __str__(self): + return self.sku.name + + +class IndexPromotionBanner(BaseModel): + """首页促销活动模型类""" + url = models.CharField(max_length=256, verbose_name='活动链接') + name = models.CharField(max_length=20, verbose_name='活动名称') + image = models.ImageField(upload_to='goods', verbose_name='图片路径') + index = models.SmallIntegerField(default=0, verbose_name='展示顺序') + + class Meta: + db_table = 'df_index_promotion' + verbose_name = '主页促销活动' + verbose_name_plural = verbose_name + + def __str__(self): + return self.name \ No newline at end of file diff --git a/apps/goods/search_indexes.py b/apps/goods/search_indexes.py new file mode 100644 index 0000000..4993b3f --- /dev/null +++ b/apps/goods/search_indexes.py @@ -0,0 +1,19 @@ +# 定义索引类 +from haystack import indexes +# 导入模型类 +from apps.goods.models import GoodsSKU +# 指定对于某个类的某些数据建立索引 +# 索引类名格式:模型类名+Index + + + +class GoodsSKUIndex(indexes.SearchIndex, indexes.Indexable): + # 索引字段 指定根据表中哪些字段建立索引文件 + text = indexes.CharField(document=True, use_template=True) + + def get_model(self): + return GoodsSKU + + # 建立索引的数据 + def index_queryset(self, using=None): + return self.get_model().objects.all() \ No newline at end of file diff --git a/apps/goods/tests.py b/apps/goods/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/apps/goods/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/goods/urls.py b/apps/goods/urls.py new file mode 100644 index 0000000..6882308 --- /dev/null +++ b/apps/goods/urls.py @@ -0,0 +1,10 @@ +from django.urls import path, re_path +from apps.goods.views import IndexView, DetailView, ListView + +app_name = 'goods' + +urlpatterns = [ + path('index/', IndexView.as_view(), name='index'), # Index view + re_path(r'^goods/(?P\d+)$', DetailView.as_view(), name='detail'), # Detail view + re_path(r'^goods/list/(?P\d+)/(?P\d+)$', ListView.as_view(), name='list'), # List view +] diff --git a/apps/goods/views.py b/apps/goods/views.py new file mode 100644 index 0000000..4b86bca --- /dev/null +++ b/apps/goods/views.py @@ -0,0 +1,197 @@ +from django.core.paginator import Paginator +from django.shortcuts import render, redirect, reverse +from django.views.generic.base import View +from django_redis import get_redis_connection +from django.core.cache import cache +from apps.goods.models import GoodsType, IndexGoodsBanner, IndexTypeGoodsBanner, IndexPromotionBanner, GoodsSKU +from apps.order.models import OrderGoods + +class IndexView(View): + """首页""" + def get(self, request): + """首页""" + # 获取用户信息 + user = request.user + + # 尝试获取缓存数据 + context = cache.get('index_page_data') + if not context: + # 获取商品种类信息 + types = GoodsType.objects.all() + + # 获取首页轮播商品信息 + goods_banners = IndexGoodsBanner.objects.all().order_by('index') + + # 获取首页促销商品信息 + promotion_banners = IndexPromotionBanner.objects.all().order_by('index') + + # 获取分类商品展示信息 + for type in types: + image_goods_banners = IndexTypeGoodsBanner.objects.filter(type=type, display_type=1) + font_goods_banners = IndexTypeGoodsBanner.objects.filter(type=type, display_type=0) + type.image_goods_banners = image_goods_banners + type.font_goods_banners = font_goods_banners + + # 组织上下文 + context = { + 'types': types, + 'goods_banners': goods_banners, + 'promotion_banners': promotion_banners, + } + # 设置缓存 + cache.set('index_page_data', context, 3600) + + # 获取购物车中商品数量 + cart_count = self._get_cart_count(user) + + context.update(user=user, cart_count=cart_count) + + return render(request, 'index.html', context) + + def _get_cart_count(self, user): + """获取用户购物车商品数量""" + if user.is_authenticated: + conn = get_redis_connection('default') + cart_key = f'cart_{user.id}' + return conn.hlen(cart_key) + return 0 + + +class DetailView(View): + """详情页面""" + def get(self, request, goods_id): + """显示详情页面""" + # 获取商品SKU信息 + try: + sku = GoodsSKU.objects.get(id=goods_id) + except GoodsSKU.DoesNotExist: + return redirect(reverse('goods:index')) + + # 获取商品的分类信息 + types = GoodsType.objects.all() + + # 获取商品的评论信息 + sku_orders = OrderGoods.objects.filter(sku=sku) + + # 获取新商品信息 + new_skus = GoodsSKU.objects.filter(type=sku.type).order_by('-create_time')[:2] + + # 获取同一个SPU的其他商品 + same_spu_skus = GoodsSKU.objects.filter(goods_spu=sku.goods_spu).exclude(id=goods_id) + + # 获取用户购物车中的商品数目 + user = request.user + cart_count = self._get_cart_count(user) + + # 添加用户的历史浏览记录 + if user.is_authenticated: + self._add_user_history(user, goods_id) + + # 组织上下文 + context = { + 'sku': sku, + 'types': types, + 'sku_orders': sku_orders, + 'new_skus': new_skus, + 'same_spu_skus': same_spu_skus, + 'cart_count': cart_count, + } + + return render(request, 'detail.html', context) + + def _get_cart_count(self, user): + """获取用户购物车商品数量""" + if user.is_authenticated: + conn = get_redis_connection('default') + cart_key = f'cart_{user.id}' + return conn.hlen(cart_key) + return 0 + + def _add_user_history(self, user, goods_id): + """添加用户浏览历史记录""" + conn = get_redis_connection('default') + history_key = f'history_{user.id}' + conn.lrem(history_key, 0, goods_id) + conn.lpush(history_key, goods_id) + conn.ltrim(history_key, 0, 4) + + +class ListView(View): + """列表页""" + def get(self, request, type_id, page): + """显示列表页""" + # 获取种类信息 + try: + type = GoodsType.objects.get(id=type_id) + except GoodsType.DoesNotExist: + return redirect(reverse('goods:index')) + + # 获取排序方式 + sort = request.GET.get('sort', 'default') + skus = self._get_skus_by_sort(type, sort) + + # 对商品进行分页 + paginator = Paginator(skus, 1) + page = self._get_valid_page(paginator, page) + skus_page = paginator.page(page) + pages = self._get_page_range(paginator, page) + + # 获取商品的分类信息 + types = GoodsType.objects.all() + + # 获取新商品信息 + new_skus = GoodsSKU.objects.filter(type=type).order_by('-create_time')[:2] + + # 获取用户购物车中的商品数目 + user = request.user + cart_count = self._get_cart_count(user) + + # 组织上下文 + context = { + 'type': type, + 'skus_page': skus_page, + 'types': types, + 'new_skus': new_skus, + 'cart_count': cart_count, + 'sort': sort, + 'pages': pages, + } + + return render(request, 'list.html', context) + + def _get_skus_by_sort(self, type, sort): + """获取排序后的商品SKU""" + if sort == 'price': + return GoodsSKU.objects.filter(type=type).order_by('price') + elif sort == 'hot': + return GoodsSKU.objects.filter(type=type).order_by('-sales') + return GoodsSKU.objects.filter(type=type).order_by('-id') + + def _get_valid_page(self, paginator, page): + """获取有效的分页页码""" + try: + page = int(page) + except ValueError: + page = 1 + if page > paginator.num_pages: + page = 1 + return page + + def _get_page_range(self, paginator, page): + """获取分页页码范围""" + num_pages = paginator.num_pages + if num_pages < 5: + return range(1, num_pages + 1) + if page <= 3: + return range(1, 6) + if num_pages - page <= 2: + return range(num_pages - 4, num_pages + 1) + return range(page - 2, page + 3) + + def _get_cart_count(self, user): + """获取用户购物车商品数量""" + if user.is_authenticated: + conn = get_redis_connection('default') + cart_key = f'cart_{user.id}' + return conn.hlen(cart_key) + return 0 diff --git a/apps/order/admin.py b/apps/order/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/apps/order/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/order/alipay_public_key.pem b/apps/order/alipay_public_key.pem new file mode 100644 index 0000000..1bfab86 --- /dev/null +++ b/apps/order/alipay_public_key.pem @@ -0,0 +1,3 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjvgjL5EnRL1KrPqLFTZ0D4TyTFbSmmg/UWgkm1QyuqOYHZZ679beAsy6d2oY6WIcp5AqNrFIRiPz6jejRMNE9zhMEXQOG8zi/ttmJlyBlyW77FN5zrG5GPtGeZcr9cPEjc7Mh1srQLPHRYwDyyEHWp3VKe+tYCZMnPvJSch3iVCwY1jP9ApdozBCrLCs83m5p4mgoJLvrV0fp1sJKODDqazm8JixjOTZc5iZwI/ue7AGrFgxzSaU+pzhJiqBCoriq6w3hhzkGryGp4cXWNmjytn9rEK1wXOjaJPbWwtDtKtZiH4ou35pqCjTIWesqod+vEit5BUQuFoSzrM19LUx1QIDAQAB +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/apps/order/app_private_key.pem b/apps/order/app_private_key.pem new file mode 100644 index 0000000..88b312d --- /dev/null +++ b/apps/order/app_private_key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAr3dngISSMTXZmWPosXgeJSTz/lSnNT/zXRXeCLSWfTSq6yML +J9uuFxoiJJbHZdS1wFEnbqO8ZLX7Bqhm70/XITfrM8GDTjh8ITYvAnx60F/+7Fmk +f8FTEt9KUW4DQQyf45EiIKW82CTClns5/d6hqt0Lxac/4jP1nhhL4Jde5DLggYdv +sf2IlBOtqnhAcHOGFDq1ZBkp1uW457zjX0GujwbHKtI7YYf43aGtHwUtVBctyEXC +NMJMpioSQQuFo4+SGp211lVIPTpzv++zCmL67R+yrXTGFqbqJpoJnrE93Fuy8Qlg +HgHeTr75jic83+GwqOpN3KLy0Hpg595EoKx/PwIDAQABAoIBAA0rgipESRDGgPGh +bRq88E5LasDhK7e0eBi5hnPS0iTNqjKB69lvBK8ZOAzVAFxlTcsEjFgilAZfHltO +koNN09DbeJzm3msllDON9JNUMoenXOPyioVIRmr5NYPNJRNh1jJnd09KAVWb1Lsk +vqKObkX712Fbf1EEI2BdZHyT//xSsV/xTZ5kXHJIIYzGuXtP5/h6m3s7qkAOOVzG +LcFrCtGzifxZsprh8ztL0+PFvZywikGIg+GkBMaVA1Xz28WT9pggsa8KuY8dBI+K +XRgsbKuVwccA3tIkgkgvdKKCH5b6Vbco8dxY4f1ZMt0Id3ouEVY2cq2vFmrGbFOP +PIfucrkCgYEA43ZFFJDOon2yv2SydGlqXzKIEtJsRet0tpXu9pZFswt32vmIM/lX +RHYFiq5ulFPZiYw3HFr6CwDM44eIn0/VJDh3/qMUptFhlQtg2WurIvNmZ7HqDK3S +sXVr0jsFn1mD4t6PwMLNboCm6lfnQxvJXfHKaV/RW7RuVIcop6S/TFMCgYEAxXsd +2DR+A0AI/JbXbRlVsUvV7B0/UH8Cz+PPJXsjazY0H6s+HQbXPJa0sG8mehCgXsDU +wRz5n6gUy3ryBqzW/e8XTztZlHiRU+CXJNiqyfmzmfHGr+KKZvluFSWYdlQcu9qL +43cqUqYddfwnEo6Q+/Hkdwi5BUq88APjJW6uw+UCgYA74zzG8GVnRN8WI0YU/lhC +XkSTaBGXyyl8lTdId0I8pM1WuxJQVNrULJrC67Azn2wMGf28mntxADHxyhJ/l35P +vgph4cAjN8eQfWFvfTieyCTzMlWkJvPtQzQzMtUFIoVl6yFAKEn8SSUpWCGMerlm +4a1gVxkBIx1VZgyfLvIq/wKBgD+nyN35Rak8iekJolU7dmDZBhK+9rq2xixGzW3S +fH9BkJmotDPdEaIpHgNFQMzV8Su50pqRAXHSVymj7sHyErb1y7ixc9Wk64ty+KVa +5eqG/7qesaHeTyiUPES6wqNZx41SDAd9UPolK5fteJbFt7xOo4svF5y6E572UdCu +Fc11AoGAAitTUwM3rDHjgutUL6/ffE56rgrkh50Up9vyR+gH2WO7AfIYgB3RRS08 +TQGnuS/CGCK8Q8tYS75W4wIrfdqVXAQbDf9skhIVUsVqKe9cC15nrmVZGTwToprt +lHvbvCgxjoVti9tBv7mSDet8TqxqB7n+kkzk/cgzp5ht4i6xdMk= +-----END RSA PRIVATE KEY----- diff --git a/apps/order/apps.py b/apps/order/apps.py new file mode 100644 index 0000000..47baace --- /dev/null +++ b/apps/order/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class OrderConfig(AppConfig): + name = 'apps.order' diff --git a/apps/order/migrations/0001_initial.py b/apps/order/migrations/0001_initial.py new file mode 100644 index 0000000..5441280 --- /dev/null +++ b/apps/order/migrations/0001_initial.py @@ -0,0 +1,51 @@ +# Generated by Django 2.2.3 on 2019-07-26 10:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='OrderGoods', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('is_delete', models.BooleanField(default=False, verbose_name='删除标记')), + ('count', models.IntegerField(default=1, verbose_name='商品数目')), + ('price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='商品价格')), + ('comment', models.CharField(max_length=256, verbose_name='评论')), + ], + options={ + 'verbose_name': '订单商品', + 'verbose_name_plural': '订单商品', + 'db_table': 'df_order_goods', + }, + ), + migrations.CreateModel( + name='OrderInfo', + fields=[ + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('is_delete', models.BooleanField(default=False, verbose_name='删除标记')), + ('order_id', models.CharField(max_length=20, primary_key=True, serialize=False, verbose_name='订单编号')), + ('pay_method', models.SmallIntegerField(choices=[(1, '货到付款'), (2, '微信支付'), (3, '支付宝'), (4, '银联支付')], default=3, verbose_name='支付方式')), + ('total_count', models.IntegerField(default=1, verbose_name='商品数量')), + ('total_price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='商品总价')), + ('transit_price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='运费')), + ('order_status', models.SmallIntegerField(choices=[(1, '待支付'), (2, '待发货'), (3, '待收货'), (4, '待评价'), (5, '已完成')], default=1, verbose_name='订单状态')), + ('trade_no', models.CharField(max_length=20, verbose_name='支付编号')), + ], + options={ + 'verbose_name': '订单信息', + 'verbose_name_plural': '订单信息', + 'db_table': 'df_order_info', + }, + ), + ] diff --git a/apps/order/migrations/0002_auto_20190726_1854.py b/apps/order/migrations/0002_auto_20190726_1854.py new file mode 100644 index 0000000..2664a6b --- /dev/null +++ b/apps/order/migrations/0002_auto_20190726_1854.py @@ -0,0 +1,40 @@ +# Generated by Django 2.2.3 on 2019-07-26 10:54 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('goods', '0001_initial'), + ('order', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('user', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='orderinfo', + name='address', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='user.Address', verbose_name='地址'), + ), + migrations.AddField( + model_name='orderinfo', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='用户'), + ), + migrations.AddField( + model_name='ordergoods', + name='order', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='order.OrderInfo', verbose_name='订单'), + ), + migrations.AddField( + model_name='ordergoods', + name='sku', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='goods.GoodsSKU', verbose_name='商品SKU'), + ), + ] diff --git a/apps/order/models.py b/apps/order/models.py new file mode 100644 index 0000000..14f852f --- /dev/null +++ b/apps/order/models.py @@ -0,0 +1,65 @@ +from django.db import models +from db.base_model import BaseModel + +# Create your models here. + + +class OrderInfo(BaseModel): + """订单信息模型类""" + + PAY_METHOD = { + '1': '货到付款', + '2': '微信支付', + '3': '支付宝', + '4': '银联支付', + } + + PAY_METHOD_CHOICES = ( + (1, '货到付款'), + (2, '微信支付'), + (3, '支付宝'), + (4, '银联支付'), + ) + ORDER_STATUS = { + '1': '待支付', + '2': '待发货', + '3': '待收货', + '4': '待评价', + '5': '已完成', + } + + ORDER_STATUS_CHOICES = ( + (1, '待支付'), + (2, '待发货'), + (3, '待收货'), + (4, '待评价'), + (5, '已完成'), + ) + + order_id = models.CharField(max_length=20, primary_key=True, verbose_name='订单编号') + user = models.ForeignKey('user.User', on_delete=models.CASCADE, verbose_name='用户') + address = models.ForeignKey('user.Address', on_delete=models.CASCADE, verbose_name='地址') + pay_method = models.SmallIntegerField(default=3, choices=PAY_METHOD_CHOICES, verbose_name='支付方式') + total_count = models.IntegerField(default=1, verbose_name='商品数量') + total_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='商品总价') + transit_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='运费') + order_status = models.SmallIntegerField(default=1, choices=ORDER_STATUS_CHOICES, verbose_name='订单状态') + trade_no = models.CharField(max_length=20, default='', verbose_name='支付编号') + + class Meta: + db_table = 'df_order_info' + verbose_name = '订单信息' + verbose_name_plural = verbose_name + +class OrderGoods(BaseModel): + """订单商品模型类""" + order = models.ForeignKey('OrderInfo', on_delete=models.CASCADE, verbose_name='订单') + sku = models.ForeignKey('goods.GoodsSKU', on_delete=models.CASCADE, verbose_name='商品SKU') + count = models.IntegerField(default=1, verbose_name='商品数目') + price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='商品价格') + comment = models.CharField(max_length=256, default='', verbose_name='评论') + + class Meta: + db_table = 'df_order_goods' + verbose_name = '订单商品' + verbose_name_plural = verbose_name \ No newline at end of file diff --git a/apps/order/tests.py b/apps/order/tests.py new file mode 100644 index 0000000..491e4aa --- /dev/null +++ b/apps/order/tests.py @@ -0,0 +1,62 @@ +import os +import traceback +import requests +from alipay.aop.api.AlipayClientConfig import AlipayClientConfig +from alipay.aop.api.DefaultAlipayClient import DefaultAlipayClient +from alipay.aop.api.domain.AlipayTradePagePayModel import AlipayTradePagePayModel +from alipay.aop.api.request.AlipayTradePagePayRequest import AlipayTradePagePayRequest +from alipay.aop.api.request.AlipayTradeQueryRequest import AlipayTradeQueryRequest +from alipay.aop.api.response.AlipayTradePagePayResponse import AlipayTradePagePayResponse +from alipay.aop.api.response.AlipayTradePayResponse import AlipayTradePayResponse +from django.conf import settings +from django.test import TestCase +import django, json +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dailyfresh.settings') +# django.setup() +# Create your tests here. + +transit_price = 10 +order_id = '201911' +total_price = 20 + +""" +设置配置,包括支付宝网关地址、app_id、应用私钥、支付宝公钥等,其他配置值可以查看AlipayClientConfig的定义。 +""" +alipay_client_config = AlipayClientConfig() +alipay_client_config.server_url = 'https://openapi.alipaydev.com/gateway.do' +alipay_client_config.app_id = '2016100100641374' +app_private_key = '' +with open(os.path.join(settings.BASE_DIR, 'apps/order/app_private_key.pem'), 'r') as f: + for line in f: + app_private_key += line +alipay_client_config.app_private_key = app_private_key +alipay_public_key = '' +with open(os.path.join(settings.BASE_DIR, 'apps/order/alipay_public_key.pem'), 'r') as f: + for line in f: + alipay_public_key += line +alipay_client_config.alipay_public_key = alipay_public_key +""" +得到客户端对象。 +注意,一个alipay_client_config对象对应一个DefaultAlipayClient,定义DefaultAlipayClient对象后,alipay_client_config不得修改,如果想使用不同的配置,请定义不同的DefaultAlipayClient。 +logger参数用于打印日志,不传则不打印,建议传递。 +""" +client = DefaultAlipayClient(alipay_client_config=alipay_client_config) + +total_pay = transit_price + total_price +model = AlipayTradePagePayModel() +model.out_trade_no = order_id +model.total_amount = 0.01 +model.subject = "天天生鲜{0}".format(order_id) +model.product_code = "FAST_INSTANT_TRADE_PAY" +request = AlipayTradePagePayRequest(biz_model=model) + +response = client.page_execute(request, http_method="GET") +print(response) + + +# request = AlipayTradeQueryRequest(biz_model=model) +# response = client.page_execute(request, http_method="GET") +# data = requests.get(response) +# print(json.loads(data.text)) +# data = json.loads(data.text) +# print(data.get('alipay_trade_query_response').get('code')) diff --git a/apps/order/urls.py b/apps/order/urls.py new file mode 100644 index 0000000..dd7d281 --- /dev/null +++ b/apps/order/urls.py @@ -0,0 +1,12 @@ +from django.urls import path, re_path +from apps.order.views import OrderPlaceView, OrderCommitView, OrderPayView, CheckPayView, CommentView + +app_name = 'order' + +urlpatterns = [ + path('place', OrderPlaceView.as_view(), name='place'), # 提交订单页面显示 + path('commit', OrderCommitView.as_view(), name='commit'), # 订单创建 + path('pay', OrderPayView.as_view(), name='pay'), # 订单支付 + path('check', CheckPayView.as_view(), name='check'), # 查看订单支付状态 + re_path(r'^comment/(?P.+)$', CommentView.as_view(), name='comment'), # 订单评论 +] diff --git a/apps/order/views.py b/apps/order/views.py new file mode 100644 index 0000000..56aa4fa --- /dev/null +++ b/apps/order/views.py @@ -0,0 +1,552 @@ +import json +from datetime import datetime + +import requests +from alipay.aop.api.domain.AlipayTradePagePayModel import AlipayTradePagePayModel +from alipay.aop.api.request.AlipayTradePagePayRequest import AlipayTradePagePayRequest +from alipay.aop.api.request.AlipayTradeQueryRequest import AlipayTradeQueryRequest +from django.db import transaction +from django.http import JsonResponse +from django.shortcuts import render, redirect +from django.urls import reverse +from django.views.generic.base import View, logger +from django_redis import get_redis_connection +from apps.goods.models import GoodsSKU +from apps.order.models import OrderInfo, OrderGoods +from apps.user.models import Address +from utils.mixin import LoginRequiredMixin +from alipay.aop.api.AlipayClientConfig import AlipayClientConfig +from alipay.aop.api.DefaultAlipayClient import DefaultAlipayClient +from django.conf import settings +import os + +# /order/place +class OrderPlaceView(LoginRequiredMixin, View): + """提交订单页面显示""" + def post(self, request): + """提交订单页面显示""" + + # 获取登录的用户 + user = request.user + # 获取参数sku_ids + sku_ids = request.POST.getlist('sku_ids') + # 校验数据 + if not sku_ids: + # 跳转到购物车页面 + return redirect(reverse('cart:show')) + + # 遍历sku_ids获取用户要购买的商品信息 + conn = get_redis_connection('default') + cart_key = 'cart_{0}'.format(user.id) + + total_count = 0 + total_price = 0 + skus = list() + for sku_id in sku_ids: + sku = GoodsSKU.objects.get(id=sku_id) + count = conn.hget(cart_key, sku_id) + count = count.decode() + # 计算商品小计 + amount = int(count) * sku.price + # 动态给sku增加amount,count属性 + sku.count = count + sku.amount = amount + + skus.append(sku) + total_count += int(count) + total_price += amount + + # 运费: 实际开发需要单独设计,这里写死 + transit_price = 10 + + # 实付款 + total_pay = total_price + transit_price + + # 获取用户的收件地址 + addrs = Address.objects.filter(user=user) + + # 组织上下文 + sku_ids = ','.join(sku_ids) + context = { + 'skus': skus, + 'total_count': total_count, + 'total_price': total_price, + 'transit_price': transit_price, + 'total_pay': total_pay, + 'addrs': addrs, + 'sku_ids': sku_ids, + } + + # 使用模板 + return render(request, 'place_order.html', context) + + +# ajax post +# 地址id:addr_id,支付方式: pay_method,商品id字符串: sku_ids +# /order/commit + +# 悲观锁 +class OrderCommitView(View): + """订单创建""" + @transaction.atomic + def post(self, request): + """订单创建""" + # 判断用户登录 + user = request.user + if not user.is_authenticated: + return JsonResponse({'res': 0, 'errmsg': '用户未登陆'}) + + # 接收参数 + addr_id = request.POST.get('addr_id') + pay_method = request.POST.get('pay_method') + sku_ids = request.POST.get('sku_ids') + + # 校验参数 + if not all([addr_id, pay_method, sku_ids]): + return JsonResponse({'res': 1, 'errmsg': '参数不完整'}) + + # 校验支付方式 + if pay_method not in OrderInfo.PAY_METHOD.keys(): + print(pay_method,type(pay_method)) + return JsonResponse({'res': 2, 'errmsg': '无效支付方式'}) + + # 校验地址 + try : + addr = Address.objects.get(id=addr_id) + except Exception as e: + return JsonResponse({'res': 3, 'errmsg': '无效地址'}) + + # todo: 创建订单核心业务 + # 组织参数 + # 订单id:20190805181630+用户id + order_id = datetime.now().strftime('%Y%m%d%H%M%S') + str(user.id) + + # 运费 + transit_price = 10 + + # 总数目和总金额 + total_count = 0 + total_price = 0 + + # 设置保存点 + save_id = transaction.savepoint() + try: + # todo: 向df_order_info表中添加一条记录 + order = OrderInfo.objects.create(order_id=order_id, + user=user, + address=addr, + pay_method=pay_method, + total_count=total_count, + total_price=total_price, + transit_price=transit_price) + + + # todo: 向df_order_goods表中添加记录 + conn = get_redis_connection('default') + cart_key = 'cart_{0}'.format(user.id) + + sku_ids = sku_ids.split(',') + for sku_id in sku_ids: + # 获取商品信息 + try: + # 加悲观锁 + # select * from df_goods_sku where id=sku_id for update; + sku = GoodsSKU.objects.select_for_update().get(id=sku_id) + except Exception as e: + transaction.savepoint_rollback(save_id) + return JsonResponse({'res': 4, 'errmsg': '商品不存在'}) + + # 从redis中获取商品的数量 + count = conn.hget(cart_key, sku_id) + + # todo: 判断某一个商品的库存 + if int(count) > sku.stock: + transaction.savepoint_rollback(save_id) + return JsonResponse({'res': 6, 'errmsg': '商品库存不足'}) + + # todo: 向df_order_goods表中添加一条记录 + OrderGoods.objects.create(order=order, + sku=sku, + count=count, + price=sku.price) + + # todo: 更新商品的库存和销量 + sku.stock -= int(count) + sku.sales += int(count) + sku.save() + + # todo: 累加计算订单商品的总数量和总价格 + amount = sku.price * int(count) + total_count += int(count) + total_price += amount + + # todo: 更新订单信息表中的商品的总数量和总价格 + order.total_price = total_price + order.total_count = total_count + order.save() + except Exception as e: + transaction.savepoint_rollback(save_id) + return JsonResponse({'res': 7, 'errmsg': '下单失败'}) + + # 提交事务 + transaction.savepoint_commit(save_id) + + # todo: 清楚用户车中对应的记录 + conn.hdel(cart_key, *sku_ids) + + # 返回应答 + return JsonResponse({'res': 5, 'message': '创建成功'}) + +# 乐观锁 +class OrderCommitView1(View): + """订单创建""" + @transaction.atomic + def post(self, request): + """订单创建""" + # 判断用户登录 + user = request.user + if not user.is_authenticated: + return JsonResponse({'res': 0, 'errmsg': '用户未登陆'}) + + # 接收参数 + addr_id = request.POST.get('addr_id') + pay_method = request.POST.get('pay_method') + sku_ids = request.POST.get('sku_ids') + + # 校验参数 + if not all([addr_id, pay_method, sku_ids]): + return JsonResponse({'res': 1, 'errmsg': '参数不完整'}) + + # 校验支付方式 + if pay_method not in OrderInfo.PAY_METHOD.keys(): + return JsonResponse({'res': 2, 'errmsg': '无效支付方式'}) + + # 校验地址 + try : + addr = Address.objects.get(id=addr_id) + except Exception as e: + return JsonResponse({'res': 3, 'errmsg': '无效地址'}) + + # todo: 创建订单核心业务 + # 组织参数 + # 订单id:20190805181630+用户id + order_id = datetime.now().strftime('%Y%m%d%H%M%S') + str(user.id) + + # 运费 + transit_price = 10 + + # 总数目和总金额 + total_count = 0 + total_price = 0 + + # 设置保存点 + save_id = transaction.savepoint() + try: + # todo: 向df_order_info表中添加一条记录 + order = OrderInfo.objects.create(order_id=order_id, + user=user, + address=addr, + pay_method=pay_method, + total_count=total_count, + total_price=total_price, + transit_price=transit_price) + + + # todo: 向df_order_goods表中添加记录 + conn = get_redis_connection('default') + cart_key = 'cart_{0}'.format(user.id) + + sku_ids = sku_ids.split(',') + for sku_id in sku_ids: + # 使用乐观锁,需多重复几次,需要数据库的隔离级别为:提交读Read committed。 + for i in range(3): + # 获取商品信息 + try: + sku = GoodsSKU.objects.get(id=sku_id) + except Exception as e: + transaction.savepoint_rollback(save_id) + return JsonResponse({'res': 4, 'errmsg': '商品不存在'}) + + # 从redis中获取商品的数量 + count = conn.hget(cart_key, sku_id) + + # todo: 判断某一个商品的库存 + if int(count) > sku.stock: + transaction.savepoint_rollback(save_id) + return JsonResponse({'res': 6, 'errmsg': '商品库存不足'}) + + # todo: 更新商品的库存和销量 + orgin_stock = sku.stock + orgin_sales = sku.sales + new_stock = orgin_stock - int(count) + new_sales = orgin_sales + int(count) + + # 加乐观锁 + # update df_goods_sku set stock=new_stock, sales=new_sales + # where id=sku_id and stock=orgin_stock; + res = GoodsSKU.objects.filter(id=sku_id, stock=orgin_stock).update(stock=new_stock, sales=new_sales) + if res == 0: + if i == 2: + transaction.savepoint_rollback(save_id) + return JsonResponse({'res': 7, 'errmsg': '下单失败2'}) + continue + + # todo: 向df_order_goods表中添加一条记录 + OrderGoods.objects.create(order=order, + sku=sku, + count=count, + price=sku.price) + + + + # todo: 累加计算订单商品的总数量和总价格 + amount = sku.price * int(count) + total_count += int(count) + total_price += amount + + # 如果成功了,跳出循环 + break + + # todo: 更新订单信息表中的商品的总数量和总价格 + order.total_price = total_price + order.total_count = total_count + order.save() + except Exception as e: + transaction.savepoint_rollback(save_id) + return JsonResponse({'res': 7, 'errmsg': '下单失败'}) + + # 提交事务 + transaction.savepoint_commit(save_id) + + # todo: 清楚用户车中对应的记录 + conn.hdel(cart_key, *sku_ids) + + # 返回应答 + return JsonResponse({'res': 5, 'message': '创建成功'}) + + +# ajax post +# 订单id:order_id +# /order/pay +class OrderPayView(View): + '''订单支付''' + def post(self, request): + '''订单支付''' + # 判断用户登录 + user = request.user + if not user.is_authenticated: + return JsonResponse({'res': 0, 'errmsg': '用户未登陆'}) + + # 接收参数 + order_id = request.POST.get('order_id') + + # 校验参数 + if not order_id: + return JsonResponse({'res': 1, 'errmsg': '参数不完整'}) + + try: + order = OrderInfo.objects.get(order_id=order_id, + user=user, + pay_method=3, + order_status=1) + except OrderInfo.DoesNotExist: + return JsonResponse({'res': 2, 'errmsg': '无效的订单id'}) + + """ + 设置配置,包括支付宝网关地址、app_id、应用私钥、支付宝公钥等,其他配置值可以查看AlipayClientConfig的定义。 + """ + alipay_client_config = AlipayClientConfig() + alipay_client_config.server_url = 'https://openapi.alipaydev.com/gateway.do' + alipay_client_config.app_id = '2016100100641374' + app_private_key = '' + with open(os.path.join(settings.BASE_DIR, 'apps/order/app_private_key.pem'), 'r') as f: + for line in f: + app_private_key += line + alipay_client_config.app_private_key = app_private_key + alipay_public_key = '' + with open(os.path.join(settings.BASE_DIR, 'apps/order/alipay_public_key.pem'), 'r') as f: + for line in f: + alipay_public_key += line + alipay_client_config.alipay_public_key = alipay_public_key + """ + 得到客户端对象。 + 注意,一个alipay_client_config对象对应一个DefaultAlipayClient,定义DefaultAlipayClient对象后,alipay_client_config不得修改,如果想使用不同的配置,请定义不同的DefaultAlipayClient。 + logger参数用于打印日志,不传则不打印,建议传递。 + """ + client = DefaultAlipayClient(alipay_client_config=alipay_client_config, logger=logger) + + total_pay = order.transit_price + order.total_price + total_pay = round(float(total_pay), 2) + model = AlipayTradePagePayModel() + model.out_trade_no = order_id + model.total_amount = total_pay + model.subject = "天天生鲜{0}".format(order_id) + model.product_code = "FAST_INSTANT_TRADE_PAY" + + request = AlipayTradePagePayRequest(biz_model=model) + response = client.page_execute(request, http_method="GET") + # 访问支付页面 + return JsonResponse({'res': 3, 'response': response}) + + +# ajax post +# 订单id: order_id +# /order/check +class CheckPayView(View): + """查看订单支付状态""" + def post(self, request): + """查看订单支付状态""" + # 判断用户登录 + user = request.user + if not user.is_authenticated: + return JsonResponse({'res': 0, 'errmsg': '用户未登陆'}) + + # 接收参数 + order_id = request.POST.get('order_id') + + # 校验参数 + if not order_id: + return JsonResponse({'res': 1, 'errmsg': '参数不完整'}) + + try: + order = OrderInfo.objects.get(order_id=order_id, + user=user, + pay_method=3, + order_status=1) + except OrderInfo.DoesNotExist: + return JsonResponse({'res': 2, 'errmsg': '无效的订单id'}) + + """ + 设置配置,包括支付宝网关地址、app_id、应用私钥、支付宝公钥等,其他配置值可以查看AlipayClientConfig的定义。 + """ + alipay_client_config = AlipayClientConfig() + alipay_client_config.server_url = 'https://openapi.alipaydev.com/gateway.do' + alipay_client_config.app_id = '2016100100641374' + app_private_key = '' + with open(os.path.join(settings.BASE_DIR, 'apps/order/app_private_key.pem'), 'r') as f: + for line in f: + app_private_key += line + alipay_client_config.app_private_key = app_private_key + alipay_public_key = '' + with open(os.path.join(settings.BASE_DIR, 'apps/order/alipay_public_key.pem'), 'r') as f: + for line in f: + alipay_public_key += line + alipay_client_config.alipay_public_key = alipay_public_key + """ + 得到客户端对象。 + 注意,一个alipay_client_config对象对应一个DefaultAlipayClient,定义DefaultAlipayClient对象后,alipay_client_config不得修改,如果想使用不同的配置,请定义不同的DefaultAlipayClient。 + logger参数用于打印日志,不传则不打印,建议传递。 + """ + client = DefaultAlipayClient(alipay_client_config=alipay_client_config, logger=logger) + + total_pay = order.transit_price + order.total_price + total_pay = round(float(total_pay), 2) + model = AlipayTradePagePayModel() + model.out_trade_no = order_id + model.total_amount = total_pay + model.subject = "天天生鲜{0}".format(order_id) + model.product_code = "FAST_INSTANT_TRADE_PAY" + + while True: + request = AlipayTradeQueryRequest(biz_model=model) + response = client.page_execute(request, http_method="GET") + data = requests.get(response) + data = json.loads(data.text) + code = data.get('alipay_trade_query_response').get('code') + + trade_status = data.get('alipay_trade_query_response').get('trade_status') + print(code, trade_status) + + if code == '10000' and trade_status == 'TRADE_SUCCESS': + # 支付成功 + # 获取支付宝交易号 + trade_no = data.get('alipay_trade_query_response').get('code') + # 更新订单状态 + order.trade_no = trade_no + order.order_status = 4 + order.save() + # 返回结果 + return JsonResponse({'res': 3, 'message': '支付成功'}) + + elif code=='40004' or (code == '10000' and trade_status == 'WAIT_BUYER_PAY'): + # 等待卖家付款 + import time + time.sleep(5) + continue + else: + # 支付出错 + return JsonResponse({'res': 4, 'errmsg': '支付失败'}) + + +class CommentView(LoginRequiredMixin, View): + """订单评论""" + def get(self, request, order_id): + """提供订单评论页面""" + user = request.user + + # 校验参数 + if not order_id: + return redirect(reverse('user:order')) + + try: + order = OrderInfo.objects.get(order_id=order_id, + user=user, + ) + except OrderInfo.DoesNotExist: + return redirect(reverse('user:order')) + + order.status_name = OrderInfo.ORDER_STATUS[str(order.order_status)] + + # 获取订单的商品信息 + order_skus = OrderGoods.objects.filter(order=order) + for order_sku in order_skus: + # 计算商品小计 + amount = order_sku.price * order_sku.count + order_sku.amount = amount + + order.order_skus = order_skus + + return render(request, 'order_comment.html', {'order': order}) + + def post(self, request, order_id): + """处理评论内容""" + user = request.user + + # 校验参数 + if not order_id: + return redirect(reverse('user:order')) + + try: + order = OrderInfo.objects.get(order_id=order_id, + user=user, + ) + except OrderInfo.DoesNotExist: + return redirect(reverse('user:order')) + + # 获取评论条数 + total_count = request.POST.get('total_count') + total_count = int(total_count) + + for i in range(1, total_count+1): + # 获取评论的商品的id + sku_id = request.POST.get('sku_{0}'.format(i)) + + # 获取评论内容 + content = request.POST.get('content_{0}'.format(i)) + + try: + order_goods = OrderGoods.objects.get(order=order, sku_id=sku_id) + except OrderGoods.DoesNotExist: + continue + + order_goods.comment = content + order_goods.save() + + order.order_status = 5 + order.save() + + return redirect(reverse('user:order', kwargs={'page': 1})) + + + + diff --git a/apps/user/admin.py b/apps/user/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/apps/user/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/user/apps.py b/apps/user/apps.py new file mode 100644 index 0000000..850997f --- /dev/null +++ b/apps/user/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class UserConfig(AppConfig): + name = 'apps.user' diff --git a/apps/user/migrations/0001_initial.py b/apps/user/migrations/0001_initial.py new file mode 100644 index 0000000..448bc52 --- /dev/null +++ b/apps/user/migrations/0001_initial.py @@ -0,0 +1,69 @@ +# Generated by Django 2.2.3 on 2019-07-26 10:54 + +from django.conf import settings +import django.contrib.auth.models +import django.contrib.auth.validators +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0011_update_proxy_permissions'), + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('is_delete', models.BooleanField(default=False, verbose_name='删除标记')), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name': '用户', + 'verbose_name_plural': '用户', + 'db_table': 'df_user', + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + migrations.CreateModel( + name='Address', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('is_delete', models.BooleanField(default=False, verbose_name='删除标记')), + ('receiver', models.CharField(max_length=20, verbose_name='收件人')), + ('addr', models.CharField(max_length=256, verbose_name='收货地址')), + ('zip_code', models.CharField(max_length=6, null=True, verbose_name='邮政编码')), + ('phone', models.CharField(max_length=11, verbose_name='联系电话')), + ('is_default', models.BooleanField(default=False, verbose_name='是否默认')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='所属用户')), + ], + options={ + 'verbose_name': '地址', + 'verbose_name_plural': '地址', + 'db_table': 'df_address', + }, + ), + ] diff --git a/apps/user/models.py b/apps/user/models.py new file mode 100644 index 0000000..c956821 --- /dev/null +++ b/apps/user/models.py @@ -0,0 +1,49 @@ +from django.conf import settings +from django.contrib.auth.models import AbstractUser +from itsdangerous import URLSafeTimedSerializer as Serializer +from db.base_model import BaseModel +from django.db import models + +# AddressManager Class +class AddressManager(models.Manager): + """地址模型管理类""" + def get_default_address(self, user): + """获取用户的默认地址""" + try: + address = self.get(user=user, is_default=True) + except self.model.DoesNotExist: + address = None + return address + +# User Model +class User(AbstractUser, BaseModel): + """用户模型类""" + def generate_active_token(self): + """生成用户签名字符串""" + serializer = Serializer(settings.SECRET_KEY, 3600) + info = {'confirm': self.id} + token = serializer.dumps(info) + return token.decode() + + class Meta: + db_table = 'df_user' + verbose_name = '用户' + verbose_name_plural = verbose_name + +# Address Model +class Address(BaseModel): + """地址模型类""" + user = models.ForeignKey('User', on_delete=models.CASCADE, verbose_name='所属用户') + receiver = models.CharField(max_length=20, verbose_name='收件人') + addr = models.CharField(max_length=256, verbose_name='收货地址') + zip_code = models.CharField(max_length=6, null=True, verbose_name='邮政编码') + phone = models.CharField(max_length=11, verbose_name='联系电话') + is_default = models.BooleanField(default=False, verbose_name='是否默认') + + # 自定义一个模型管理器对象 + objects = AddressManager() + + class Meta: + db_table = 'df_address' + verbose_name = '地址' + verbose_name_plural = verbose_name diff --git a/apps/user/tests.py b/apps/user/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/apps/user/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/user/urls.py b/apps/user/urls.py new file mode 100644 index 0000000..e7a5d69 --- /dev/null +++ b/apps/user/urls.py @@ -0,0 +1,19 @@ +from django.urls import path, re_path +from apps.user.views import ( + RegisterView, ActiveView, LoginView, UserInfoView, + UserOrderView, UserSiteView, LogoutView +) +from apps.user import views + +app_name = 'user' + +urlpatterns = [ + path('index/', views.index, name='index'), + path('register/', RegisterView.as_view(), name='register'), # Register + re_path(r'active/(?P.*)/$', ActiveView.as_view(), name='active'), # User Activation + path('login/', LoginView.as_view(), name='login'), # Login + path('logout/', LogoutView.as_view(), name='logout'), # Logout + path('', UserInfoView.as_view(), name='user'), # User Info + re_path(r'^order/(?P\d+)/$', UserOrderView.as_view(), name='order'), # Orders + path('address/', UserSiteView.as_view(), name='address'), # Address +] diff --git a/apps/user/views.py b/apps/user/views.py new file mode 100644 index 0000000..38277b4 --- /dev/null +++ b/apps/user/views.py @@ -0,0 +1,215 @@ +import re +from django.contrib.auth import authenticate, login, logout +from django.core.paginator import Paginator +from django.shortcuts import render, redirect, reverse, HttpResponse +from django.views.generic import View +from itsdangerous import URLSafeTimedSerializer as Serializer, SignatureExpired +from django.conf import settings +from django.core.mail import send_mail +from celery_tasks.tasks import send_register_active_email +from utils.mixin import LoginRequiredMixin +from django_redis import get_redis_connection +from apps.goods.models import GoodsSKU +from apps.order.models import OrderInfo, OrderGoods +from apps.user.models import User, Address, AddressManager + + +# Index View +def index(request): + return render(request, 'index.html') + + +# Function-based Register View (Commented out) +# def register(request): +# """显示注册页面""" +# if request.method == 'GET': +# return render(request, 'register.html') +# elif request.method == 'POST': +# # Handle registration logic +# pass + +# Class-based Register View +class RegisterView(View): + """注册""" + + def get(self, request): + return render(request, 'register.html') + + def post(self, request): + # 接受数据 + username = request.POST.get('user_name') + password = request.POST.get('pwd') + email = request.POST.get('email') + allow = request.POST.get('allow') + + # 数据处理 + if not all([username, password, email]): + return render(request, 'register.html', {'errmsg': '数据不完整'}) + + if not re.match(r'^[a-z0-9][\w.\-]*@[a-z0-9\-]+(\.[a-z]{2,5}){1,2}$', email): + return render(request, 'register.html', {'errmsg': '邮箱格式不正确'}) + + if allow != 'on': + return render(request, 'register.html', {'errmsg': '请同意协议'}) + + # 校验用户名是否重复 + try: + user = User.objects.get(username=username) + except User.DoesNotExist: + user = None + + if user: + return render(request, 'register.html', {'errmsg': '用户名已存在'}) + + # 进行用户注册 + user = User.objects.create_user(username, email, password) + user.is_active = 0 + user.is_superuser = False + user.save() + + # 发送激活邮件,包含激活链接 + serializer = Serializer(settings.SECRET_KEY, 3600) + info = {'confirm': user.id} + token = serializer.dumps(info).decode() + + # 为处理者分配任务 + send_register_active_email.delay(email, username, token) + + return redirect(reverse('goods:index')) + + +# User Activation View +class ActiveView(View): + """用户激活""" + + def get(self, request, token): + try: + serializer = Serializer(settings.SECRET_KEY, 3600) + token = token.encode() + info = serializer.loads(token) + user_id = info['confirm'] + user = User.objects.get(id=user_id) + user.is_active = 1 + user.save() + return redirect(reverse('user:login')) + except SignatureExpired: + return HttpResponse('激活链接已过期') + + +# Login View +class LoginView(View): + """登录""" + + def get(self, request): + if 'username' in request.COOKIES: + username = request.COOKIES.get('username') + checked = 'checked' + else: + username = '' + checked = '' + context = {'username': username, 'checked': checked} + return render(request, 'login.html', context) + + def post(self, request): + username = request.POST.get('username') + password = request.POST.get('pwd') + if not all([username, password]): + return render(request, 'login.html', {'errmsg': '数据不完整'}) + user = authenticate(username=username, password=password) + if user: + if user.is_active: + login(request, user) + next_url = request.GET.get('next', reverse('goods:index')) + response = redirect(next_url) + remember = request.POST.get('remember') + if remember == 'on': + response.set_cookie('username', username, max_age=7 * 24 * 3600) + else: + response.delete_cookie('username') + return response + else: + return render(request, 'login.html', {'errmsg': '账户未激活'}) + else: + return render(request, 'login.html', {'errmsg': '用户名或密码错误'}) + + +# Logout View +class LogoutView(View): + """退出登录""" + + def get(self, request): + logout(request) + return redirect(reverse('goods:index')) + + +# User Info View +class UserInfoView(LoginRequiredMixin, View): + """用户中心——信息页""" + + def get(self, request): + user = request.user + address = Address.objects.get_default_address(user) + con = get_redis_connection('default') + history_key = f'history_{user.id}' + sku_ids = con.lrange(history_key, 0, 4) + goods_list = [GoodsSKU.objects.get(id=i) for i in sku_ids] + context = {'page': 'user', 'user': user, 'address': address, 'goods_list': goods_list} + return render(request, 'user_center_info.html', context) + + +# User Order View +class UserOrderView(LoginRequiredMixin, View): + """用户中心——订单页""" + + def get(self, request, page): + user = request.user + orders = OrderInfo.objects.filter(user=user) + for order in orders: + order_skus = OrderGoods.objects.filter(order_id=order.order_id) + for order_sku in order_skus: + order_sku.amount = order_sku.price * order_sku.count + order.order_skus = order_skus + order.status_name = OrderInfo.ORDER_STATUS[str(order.order_status)] + paginator = Paginator(orders, 1) + try: + page = int(page) + except Exception: + page = 1 + order_page = paginator.page(page) + num_pages = paginator.num_pages + if num_pages < 5: + pages = range(1, num_pages + 1) + elif page <= 3: + pages = range(1, 6) + elif num_pages - page <= 2: + pages = range(num_pages - 4, num_pages + 1) + else: + pages = range(page - 2, page + 3) + context = {'order_page': order_page, 'pages': pages, 'page': 'order'} + return render(request, 'user_center_order.html', context) + + +# User Address View +class UserSiteView(LoginRequiredMixin, View): + """用户中心——地址页""" + + def get(self, request): + user = request.user + address = Address.objects.get_default_address(user) + return render(request, 'user_center_site.html', {'address': address, 'user': user}) + + def post(self, request): + receiver = request.POST.get('receiver') + addr = request.POST.get('addr') + zip_code = request.POST.get('zip_code') + phone = request.POST.get('phone') + if not all([receiver, addr, phone]): + return render(request, 'user_center_site.html', {'errmsg': '数据不完整'}) + if not re.match(r'^1[3|4|5|7|8][0-9]{9}$', phone): + return render(request, 'user_center_site.html', {'errmsg': '手机格式错误'}) + user = request.user + address = Address.objects.get_default_address(user) + is_default = not bool(address) + Address.objects.create(user=user, receiver=receiver, addr=addr, zip_code=zip_code, phone=phone, + is_default=is_default) + return redirect(reverse('user:address')) diff --git a/celery_tasks/tasks.py b/celery_tasks/tasks.py new file mode 100644 index 0000000..88e5c45 --- /dev/null +++ b/celery_tasks/tasks.py @@ -0,0 +1,73 @@ +# 使用celery + +# # 在任务一段加这几句代码 +# import os +# import django +# os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dailyfresh.settings') +# django.setup() +import os +import time + +from django.template import loader + +from dailyfresh import settings +from celery import Celery +from django_redis import get_redis_connection + +from apps.goods.models import GoodsType, IndexGoodsBanner, IndexTypeGoodsBanner, IndexPromotionBanner +# 创建一个Celery类的实例对象 +from django.core.mail import send_mail +app = Celery('celery_tasks.tasks', broker='redis://192.168.209.130:6379/8') + +# 定义任务函数 +@app.task +def send_register_active_email(to_email, username, token): + """发送激活邮件""" + # 发送邮件 + subject = '天天生鲜欢迎信息' + message = '' + sender = settings.EMAIL_FROM + receiver = [to_email] + html_message = '

{0}, 欢迎您成为天天生鲜注册会员

请点击下面链接激活您的账户
http://127.0.0.1/user/active/{2}'.format( + username, token, token) + send_mail(subject, message, sender, receiver, html_message=html_message) + time.sleep(5) + + +@app.task +def generate_static_index_html(): + """产生首页静态页面""" + + # 获取商品种类信息 + types = GoodsType.objects.all() + + # 获取首页轮播商品信息 + goods_banners = IndexGoodsBanner.objects.all().order_by('index') + + # 获取首页促销商品信息 + promotion_banners = IndexPromotionBanner.objects.all().order_by('index') + + # 获取分类商品展示信息 + for type in types: + image_goods_banners = IndexTypeGoodsBanner.objects.filter(type=type, display_type=1) + font_goods_banners = IndexTypeGoodsBanner.objects.filter(type=type, display_type=0) + type.image_goods_banners = image_goods_banners + type.font_goods_banners = font_goods_banners + + # 组织上下文 + context = { + 'types': types, + 'goods_banners': goods_banners, + 'promotion_goods': promotion_banners, + } + + # s使用模板 + # 1. 加载模板文件,返回模板对象 + temp = loader.get_template('static_index.html') + # 2. 渲染模板 + static_index_html = temp.render(context) + + # 生成对应静态文件 + save_path = os.path.join(settings.BASE_DIR, 'static/index.html') + with open(save_path, 'w') as f: + f.write(static_index_html) \ No newline at end of file diff --git a/dailyfresh/settings.py b/dailyfresh/settings.py new file mode 100644 index 0000000..56f19c3 --- /dev/null +++ b/dailyfresh/settings.py @@ -0,0 +1,202 @@ +""" +Django settings for dailyfresh project. + +Generated by 'django-admin startproject' using Django 2.2.3. + +For more information on this file, see +https://docs.djangoproject.com/en/2.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/2.2/ref/settings/ +""" + +import os +import sys + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +sys.path.insert(0, os.path.join(BASE_DIR, 'apps')) + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'qi1pgrrshxdstv3v71b8ax%@2p)zh#&_hy5mo633xh3$+g-bs(' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'tinymce', # 富文本编辑器 + 'haystack', # 全文检索框架 + # sys.path.insert(0, os.path.join(BASE_DIR, 'apps') + 'apps.goods', # 商品模块 + 'apps.cart', # 购物车模块 + 'apps.order', # 订单模块 + 'apps.user', # 用户模块 +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'dailyfresh.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [os.path.join(BASE_DIR, 'templates')], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'dailyfresh.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/2.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': 'dailyfresh', + 'USER': 'root', + 'PASSWORD': '1234567', + 'HOST': '127.0.0.1', + 'POST': '3306', + } +} + +# django认证系统使用的模型类 +AUTH_USER_MODEL='user.User' + +# Password validation +# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/2.2/topics/i18n/ + +LANGUAGE_CODE = 'zh-hans' + +TIME_ZONE = 'Asia/Shanghai' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/2.2/howto/static-files/ + +STATIC_URL = '/static/' +STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')] + +# 富文本编辑器配置 +TINYMCE_DEFAULT_CONFIG = { + 'theme': 'advanced', + 'width': 600, + 'height': 400, +} + +# 发送邮件配置 +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' + +# EMAIL_USE_TLS = False +# smpt服务地址 +EMAIL_HOST = 'smtp.163.com' +EMAIL_PORT = 25 +# 邮箱 +EMAIL_HOST_USER = 'a1691795341@163.com' +# 授权密码 +EMAIL_HOST_PASSWORD = 'w809326582' +# DEFAULT_FROM_EMAIL = 'w1691795341@163.com' +EMAIL_FROM = '天天生鲜' + +# Django的缓存配置 +CACHES = { + 'default': { + 'BACKEND': 'django_redis.cache.RedisCache', + 'LOCATION': 'redis://192.168.209.130:6379/1', + 'OPTIONS': { + 'CLIENT_CLASS': 'django_redis.client.DefaultClient', + } + } +} + + +# 配置SESSION的存储 +SESSION_ENGINE = "django.contrib.sessions.backends.cache" +SESSION_CACHE_ALIAS = "default" + +# 配置登录url地址 +LOGIN_URL = '/user/login' + +# 设置django的默认文件存储类 +DEFAULT_FILE_STORAGE = 'utils.fdfs.storage.FDFSStorage' + +# 设置fdfs使用的client.conf的文件路径 +FDFS_CLIENT_CONF = '/etc/fdfs/client.conf' + +# 设置fdfs存储服务器上Nginx的ip和port +FDFS_URL = 'http://192.168.209.130:8888/' + +# 全文检索框架配置 +HAYSTACK_CONNECTIONS = { + 'default': { + # 使用whoosh + 'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine', + # 索引文件路径 + 'PATH': os.path.join(BASE_DIR, 'whoosh_index'), + } +} + +# 当添加、修改、删除数据时,自动生成索引 +HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor' +# 指定搜索每页显示条数 默认20 +HAYSTACK_SEARCH_RESULTS_PER_PAGE = 1 diff --git a/dailyfresh/urls.py b/dailyfresh/urls.py new file mode 100644 index 0000000..27c80bb --- /dev/null +++ b/dailyfresh/urls.py @@ -0,0 +1,37 @@ +"""dailyfresh URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/3.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.urls import path +from django.contrib import admin +from django.urls import path, include +from apps.user import views +from apps.user.views import RegisterView, ActiveView, LoginView, UserInfoView, UserOrderView, UserSiteView, LogoutView + +urlpatterns = [ + path('', views.index, name='index'), # 根路径指向index视图 + # path('index/', views.index, name='index'), + path('admin/', admin.site.urls), + # path('login/', views.login_view, name='login'), + # path('register/', views.register_view, name='register'), + path('login/', LoginView.as_view(), name='login'), # 登录 + path('logout/', LogoutView.as_view(), name='logout'), # 退出登录 + path('register/', RegisterView.as_view(), name='register'), # 注册 + path('tinymce/', include('tinymce.urls')), # 富文本编辑器 + path('search/', include('haystack.urls')), # 全文检索 + path('user/', include(('apps.user.urls', 'user'), namespace='user')), # 用户模块 + path('cart/', include(('apps.cart.urls', 'cart'), namespace='cart')), # 购物车模块 + path('order/', include(('apps.order.urls', 'order'), namespace='order')), # 订单模块 + path('goods/', include(('apps.goods.urls', 'goods'), namespace='goods')), # 商品模块 +] diff --git a/dailyfresh/wsgi.py b/dailyfresh/wsgi.py new file mode 100644 index 0000000..17559a7 --- /dev/null +++ b/dailyfresh/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for dailyfresh project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dailyfresh.settings') + +application = get_wsgi_application() diff --git a/db/base_model.py b/db/base_model.py new file mode 100644 index 0000000..300cf31 --- /dev/null +++ b/db/base_model.py @@ -0,0 +1,11 @@ +from django.db import models + +class BaseModel(models.Model): + '''模型抽象基类''' + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + is_delete = models.BooleanField(default=False, verbose_name='删除标记') + + class Meta: + # 说明是一个抽象基类 + abstract = True \ No newline at end of file diff --git a/img.png b/img.png new file mode 100644 index 0000000000000000000000000000000000000000..8fff0af0d4b8d8a36f379315ec468e0ef8c02de0 GIT binary patch literal 2086 zcmeAS@N?(olHy`uVBq!ia0y~yV9I1*V2R^k0*Wy2F<8aG!2Z_L#WAEJ?(IQCMxfxK z1z-Jhx$GAJnWJDd1V%$(Gz3ONU^E0qLtt2hK!wE2n+%L+wA^=syyNNW=d#Wzp$Pyp C!4&}j literal 0 HcmV?d00001 diff --git a/img_1.png b/img_1.png new file mode 100644 index 0000000000000000000000000000000000000000..8fff0af0d4b8d8a36f379315ec468e0ef8c02de0 GIT binary patch literal 2086 zcmeAS@N?(olHy`uVBq!ia0y~yV9I1*V2R^k0*Wy2F<8aG!2Z_L#WAEJ?(IQCMxfxK z1z-Jhx$GAJnWJDd1V%$(Gz3ONU^E0qLtt2hK!wE2n+%L+wA^=syyNNW=d#Wzp$Pyp C!4&}j literal 0 HcmV?d00001 diff --git a/img_2.png b/img_2.png new file mode 100644 index 0000000000000000000000000000000000000000..4a3ce8aeb0a28e49deddc6a7d8afeeca408a034a GIT binary patch literal 2509 zcmeAS@N?(olHy`uVBq!ia0y~yU{+>eU=iS80*ciA5SC|P;GE*=;uum9_x2zoFHrE% zg75yheC7*)%uz5J0;3@?8UmvsFd71*Aut*Oqai@25O{E%!QmDI<16EeU=iS80*ciA5SC|P;GE*=;uum9_x2zoFHrE% zg75yheC7*)%uz5J0;3@?8UmvsFd71*Aut*Oqai@25O{E%!QmDI<16E ping +PONG +127.0.0.1:6379> select 5 +OK +127.0.0.1:6379[5]> keys * +(empty list or set) +127.0.0.1:6379[5]> set name itchat +OK +127.0.0.1:6379[5]> get name +"itchat" +127.0.0.1:6379[5]> set name itchat2 +OK +127.0.0.1:6379[5]> get name +"itchat2" +127.0.0.1:6379[5]> keys * +1) "name" +127.0.0.1:6379[5]> setex aa 3 ii +OK +127.0.0.1:6379[5]> keys * +1) "name" +127.0.0.1:6379[5]> setex aa 3 ii +OK +127.0.0.1:6379[5]> get aa +"ii" +127.0.0.1:6379[5]> get aa +(nil) +127.0.0.1:6379[5]> mset a1 java a2 python a3 c +OK +127.0.0.1:6379[5]> get a1 +"java" +127.0.0.1:6379[5]> get a2 +"python" +127.0.0.1:6379[5]> get a3 +"c" +127.0.0.1:6379[5]> append a1 haha +(integer) 8 +127.0.0.1:6379[5]> get a1 +"javahaha" +127.0.0.1:6379[5]> exists a +(integer) 0 +127.0.0.1:6379[5]> exists a2 +(integer) 1 +127.0.0.1:6379[5]> type name +string +127.0.0.1:6379[5]> mget a1 a2 a3 +1) "javahaha" +2) "python" +3) "c" +127.0.0.1:6379[5]> del a1 a2 a3 +(integer) 3 +127.0.0.1:6379[5]> keys * +1) "name" +127.0.0.1:6379[5]> set a1 python +OK +127.0.0.1:6379[5]> expire a1 3 +(integer) 1 +127.0.0.1:6379[5]> get a1 +(nil) +127.0.0.1:6379[5]> get a1 +(nil) +127.0.0.1:6379[5]> expire a1 3 +(integer) 0 +127.0.0.1:6379[5]> get a1 +(nil) +127.0.0.1:6379[5]> hset user name itthema +(integer) 1 +127.0.0.1:6379[5]> keys * +1) "name" +2) "user" +127.0.0.1:6379[5]> hmset u2 name itcast age 12 +OK +127.0.0.1:6379[5]> type u2 +hash +127.0.0.1:6379[5]> hkeys user +1) "name" +127.0.0.1:6379[5]> hkeys u2 +1) "name" +2) "age" +127.0.0.1:6379[5]> keys name +1) "name" +127.0.0.1:6379[5]> keys * +1) "name" +2) "user" +3) "u2" +127.0.0.1:6379[5]> hget u2 name +"itcast" +127.0.0.1:6379[5]> hget u2 age +"12" +127.0.0.1:6379[5]> hvals u2 +1) "itcast" +2) "12" +127.0.0.1:6379[5]> hdel u2 name +(integer) 1 +127.0.0.1:6379[5]> hkeys u2 +1) "age" +127.0.0.1:6379[5]> set a1 +(error) ERR wrong number of arguments for 'set' command +127.0.0.1:6379[5]> lpush a1 a b c +(integer) 3 +127.0.0.1:6379[5]> type a1 +list +127.0.0.1:6379[5]> lrange a1 +(error) ERR wrong number of arguments for 'lrange' command +127.0.0.1:6379[5]> lrange a1 0 3 +1) "c" +2) "b" +3) "a" +127.0.0.1:6379[5]> rpush a1 0 1 +(integer) 5 +127.0.0.1:6379[5]> lrange a1 0 5 +1) "c" +2) "b" +3) "a" +4) "0" +5) "1" +127.0.0.1:6379[5]> lrange a1 0 4 +1) "c" +2) "b" +3) "a" +4) "0" +5) "1" +127.0.0.1:6379[5]> lrange a1 0 3 +1) "c" +2) "b" +3) "a" +4) "0" +127.0.0.1:6379[5]> linsert a1 before b 3 +(integer) 6 +127.0.0.1:6379[5]> lrange a1 0 5 +1) "c" +2) "3" +3) "b" +4) "a" +5) "0" +6) "1" +127.0.0.1:6379[5]> lrange a1 0 -1 +1) "c" +2) "3" +3) "b" +4) "a" +5) "0" +6) "1" +127.0.0.1:6379[5]> lset a1 1 z +OK +127.0.0.1:6379[5]> lrange a1 0 -1 +1) "c" +2) "z" +3) "b" +4) "a" +5) "0" +6) "1" +127.0.0.1:6379[5]> sadd a3 1 2 3 4 +(integer) 4 +127.0.0.1:6379[5]> smembers a2 +(empty list or set) +127.0.0.1:6379[5]> smembers a3 +1) "1" +2) "2" +3) "3" +4) "4" +127.0.0.1:6379[5]> sadd a2 zhangsan wanger lisi +(integer) 3 +127.0.0.1:6379[5]> smembers a2 +1) "wanger" +2) "zhangsan" +3) "lisi" +127.0.0.1:6379[5]> smembers a3 +1) "1" +2) "2" +3) "3" +4) "4" +127.0.0.1:6379[5]> srem a3 3 +(integer) 1 +127.0.0.1:6379[5]> smembers a3 +1) "1" +2) "2" +3) "4" +127.0.0.1:6379[5]> zadd a4 4 lisi 5 wangwu 6 zhaoliu 3 zhangsan +(integer) 4 +127.0.0.1:6379[5]> keys * +1) "name" +2) "a2" +3) "a1" +4) "user" +5) "a4" +6) "a3" +7) "u2" +127.0.0.1:6379[5]> type a4 +zset +127.0.0.1:6379[5]> zrange a4 0 -1 +1) "zhangsan" +2) "lisi" +3) "wangwu" +4) "zhaoliu" +127.0.0.1:6379[5]> zscore a4 zhangsan +"3" +127.0.0.1:6379[5]> zrem z4 zhangsan +(integer) 0 +127.0.0.1:6379[5]> zrem a4 zhangsan +(integer) 1 +127.0.0.1:6379[5]> zrange a4 0 -1 +1) "lisi" +2) "wangwu" +3) "zhaoliu" +127.0.0.1:6379[5]> zremrangebyscore a4 5 6 +(integer) 2 +127.0.0.1:6379[5]> zrange a4 0 -1 +1) "lisi" + diff --git a/my_files/my_redis/my_redis02.txt b/my_files/my_redis/my_redis02.txt new file mode 100644 index 0000000..cb9e4d0 --- /dev/null +++ b/my_files/my_redis/my_redis02.txt @@ -0,0 +1,25 @@ +from redis import StrictRedis + +if __name__=="__main__": + try: + sr = StrictRedis() + res = sr.set('name', 'itheima') + print(res) # True + + res = sr.get('name') + print(res) # b'itheima' + + res = sr.set('name', 'itheima2') + print(res) # True + + res = sr.delete('name') + print(res) # 1 + + res = sr.delete('a1', 'a2') + print(res) # 0 + + res = sr.keys() + print(res) + except Exception as e: + print(e) + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..1920c75298d713a7986851ec2fb369c882a37daa GIT binary patch literal 2356 zcma);TW=Ck6ot>Ti9dy=(3?K^pz+CQtf@&Kn_-~PmRn(v(jTv`Z?8Es(@+ybrcm~6 z?X}lFhd;lIG)s4Bm5Q`ay|hWA^q}{Z{;Se7Ez=@>(CZ`}r3*cMeY!|*(;NBE)1&;b zp2;%Mb6i_CX`-C#G)}io^t*_5>=^_tWTQ*kl7&KkP%r&%#=$>Zr-kqW+}sm&y$r+R*G4w3`-5>9Ijv)D->6b0J{H2d79*bCbJPbH zQ9QXK-ISUrHcx;#!dMxdV(W>IJ3aeg7nX~9>eMHjIqXKP6upfpFQl0+Ux&Wv0Dru4 zMk}RM#aVQMnu^8)X3uMsC=k{r$^k<#untddq>GeSEBvuHq*0E0l9u zPxQ1_aOUT8x(^;E!k3vP8yf(BqmI272xqF9vE|txdNRj-;4GTScw(97mziOY`Iu#T zX?6E6Pxg4MXQMdyb1d8wZ)RV{vR@0~nhVo7`px@gcDvpng6G>{5WVbThE|#NLLDpN z{~~*0b63yIyTM=AevL72hEkK(_ z#{ZzWxvbmBZ!Kkw%wv*|(y(`9tXi$^sN(HiDL)$K1OcT_hdg!Ty$n0sunV8}il&-5 zSOxxhoR&ALvD9m(-KERiw~?QPIyt`!b-U0Muw!da8?5EQKA9DBYLE7O9t8_VZ`@?e z*vNM1`_v$#->&yW*wBr+aG$PwDP&*Lb@~}LG~io@J;L@)&sF-7zBYC;x0t`HE2eOz zmwmj6t(xd8-)ueMIK2HbpY+*w*v6nKHl3S@ndUB-7-&}Fiv4&kmfMbVCvO@4#Jz8( zR;A}UW4=MWM~CMCvDO#1N8#~adfS=iNt|guuQ)lqBW$+oaXxrT-nmvG8|9mI6p}U6 zDzXxfE5);2H>&&;oN(53&m3y?>ywok(w+G;miC@>ms_#pdwMAJnOS?+ + + + + 天天生鲜-购物车 + + + + +
+
+
欢迎来到天天生鲜!
+
+ + + +
+
+
+ + + +
全部商品2
+
    +
  • 商品名称
  • +
  • 商品单位
  • +
  • 商品价格
  • +
  • 数量
  • +
  • 小计
  • +
  • 操作
  • +
+
    +
  • +
  • +
  • 奇异果
    25.80元/500g
  • +
  • 500g
  • +
  • 25.80元
  • +
  • +
    + + + + - +
    +
  • +
  • 25.80元
  • +
  • 删除
  • +
+ +
    +
  • +
  • +
  • 大兴大棚草莓
    16.80元/500g
  • +
  • 500g
  • +
  • 16.80元
  • +
  • +
    + + + + - +
    +
  • +
  • 16.80元
  • +
  • 删除
  • +
+ + +
    +
  • +
  • 全选
  • +
  • 合计(不含运费):¥42.60
    共计2件商品
  • +
  • 去结算
  • +
+ + + + + \ No newline at end of file diff --git a/static/css/main.css b/static/css/main.css new file mode 100644 index 0000000..7dffb45 --- /dev/null +++ b/static/css/main.css @@ -0,0 +1,642 @@ +body{font-family:'Microsoft Yahei';font-size:12px;color:#666;} +html,body{height:100%} +/* 顶部样式 */ +.header_con{ + background-color:#f7f7f7; + height:29px; + border-bottom:1px solid #dddddd +} + +.header{ + width:1200px; + height:29px; + margin:0 auto; +} + +.welcome,.login_info,.login_btn,.user_link{ + line-height:29px; +} + +.login_info{ + display:none; +} + +.login_info em{color:#ff8800} + +.login_btn a,.user_link a{ + color:#666; +} + +.login_btn a:hover,.user_link a:hover{ + color:#ff8800; +} + +.login_btn span,.user_link span{ + color:#cecece; + margin:0 10px; +} + + +/* logo、搜索框、购物车样式 */ + +.search_bar{width:1200px;height:115px;margin:0 auto;} +.logo{width:150px;height:59px;margin:29px 0 0 17px;} + +.search_con{width:616px;height:38px;border:1px solid #37ab40;margin:34px 0 0 124px;background:url(../images/icons.png) 10px -338px no-repeat;} +.search_con .input_text{width:470px;height:34px;border:0px;margin:2px 0 0 36px;outline:none;font-size:12px;color:#737272;font-family:'Microsoft Yahei'} + +.search_con .input_btn{ + width:100px;height:38px;background-color:#37ab40;border:0px;font-size:14px;color:#fff;font-family:'Microsoft Yahei';outline:none;cursor:pointer; +} + +.guest_cart{ + width:200px;height:40px;margin-top:34px; +} + +.guest_cart .cart_name{ + width:158px;height:38px;line-height:38px;border:1px solid #dddddd;display:block;background:url(../images/icons.png) 13px -300px no-repeat;font-size:14px;color:#37ab40;text-indent:56px; +} + +.guest_cart .goods_count{ + width:40px;height:40px;text-align:center;line-height:40px;font-size:18px; + font-weight:bold;color:#fff;background-color:#ff8800; +} + + +/* 菜单、幻灯片样式 */ + +.navbar_con{height:40px;border-bottom:2px solid #39a93e} +.navbar{width:1200px;margin:0 auto;} +.navbar h1{width:200px;height:40px;line-height:40px;text-align: center;font-size:14px;color:#fff;background-color:#39a93e;} + +.navbar .subnav_con{width:200px;height:40px;background-color:#39a93e;position:relative;cursor:pointer;} + +.navbar .subnav_con h1{position:absolute;left:0;top:0;text-align:left;text-indent:40px} +.navbar .subnav_con span{display:block;width:16px;height:9px;background:url(../images/down.png) no-repeat;position:absolute;right:27px;top:16px;transition:all 300ms ease-in; +} + +.navbar .subnav_con:hover span{transform:rotateZ(180deg)} + +.navbar .subnav_con .subnav{position:absolute;left:0;top:40px;display:none;border-top:2px solid #39a93e;} +.navbar .subnav_con:hover .subnav{display:block;} + + +.navlist{margin-left:34px;} +.navlist li{height:40px;float:left;line-height:40px;} +.navlist li a{color:#666;font-size:14px} +.navlist li a:hover{color:#ff8800} +.navlist .interval{margin:0 15px;} + + +.center_con{width:1200px;height:270px;margin:0 auto;} +.subnav{width:198px;height:270px;border-left:1px solid #eee;border-right:1px solid #eee;} +.subnav li{height:44px;border-bottom:1px solid #eee;background:url(../images/icons.png) 178px -257px no-repeat #fff;} + +.subnav li a{display:block;height:44px;line-height:44px;text-indent:71px;font-size:14px;color:#333} +.subnav li a:hover{color:#ff8800} + +.subnav li .fruit{background:url(../images/icons.png) 28px 0px no-repeat;} +.subnav li .seafood{background:url(../images/icons.png) 28px -43px no-repeat;} +.subnav li .meet{background:url(../images/icons.png) 28px -86px no-repeat;} +.subnav li .egg{background:url(../images/icons.png) 28px -132px no-repeat;} +.subnav li .vegetables{background:url(../images/icons.png) 28px -174px no-repeat;} +.subnav li .ice{background:url(../images/icons.png) 28px -220px no-repeat;} + + +.slide{width:760px;height:270px;position:relative;overflow:hidden;} +.slide .slide_pics{position:relative;left:0;top:0;width:760px;height:270px;} +.slide .slide_pics li{width:760px;height:270px;position:absolute;left:0;top:0} +.slide .prev,.slide .next{width:17px;height:23px;background:url(../images/icons.png) left -388px no-repeat;position:absolute;left:11px;top:122px;cursor:pointer;} +.slide .next{background-position:left -428px;left:732px;} +.points{width:100%;height:11px;position:absolute;left:0;top:250px;text-align:center;} +.points li{display:inline-block;width:11px;height:11px;margin:0 5px;background-color:#9f9f9f;border-radius:50%;cursor:pointer;} +.points li.active{background-color:#cecece} + +.adv{width:240px;height:270px; overflow:hidden; background-color:gold;} +.adv a{display:block;float:left;} + + +/* 商品列表样式 */ + +.list_model{width:1200px;height:340px;margin:15px auto 0;} +.list_title{height:40px;border-bottom:2px solid #42ad46} +.list_title h3{height:40px;line-height:40px;font-size:16px;color:#37ab40;font-weight:bold;} +.list_title .subtitle{height:20px;line-height:20px;margin-top:15px;} +.list_title .subtitle span{color:#666;margin:0 10px 0 20px;} +.list_title .subtitle a{color:#666;margin:0 5px;} +.list_title .subtitle a:hover,.goods_more:hover{color:#ff8800} +.goods_more{height:20px;margin-top:15px;color:#666} + +.goods_con{height:300px;} +.goods_banner{width:200px;height:300px;} +.goods_banner img{width:200px;height:300px;} + +.goods_list{width:1000px;height:299px;border-bottom:1px solid #ededed} +.goods_list li{height:299px;width:249px;border-right:1px solid #ededed;float:left} +.goods_list li:hover{width:248px;height:297px;border:1px solid gold;} +.goods_list li:hover img{opacity:0.8} + +.goods_list li h4{width:200px;height:50px;margin:20px auto 0;text-align:center;} +.goods_list li h4 a{font-size:14px;color:#666;font-weight:normal;line-height:24px;} +.goods_list li h4 a:hover{color:#ff8800} + +.goods_list li img{display:block;width:180px;height:180px;margin:0 auto;} +.goods_list li .prize{text-align:center;font-size:20px;color:#c40000;margin-top:5px;} + +/* 页面底部样式 */ +.footer{ + border-top:2px solid #42ad46; + margin:30px 0; +} + +.foot_link{text-align:center;margin-top:30px;} +.foot_link a,.foot_link span{color:#4e4e4e;} +.foot_link a:hover{color:#ff8800} +.foot_link span{padding:0 10px} +.footer p{text-align:center; margin-top:10px;} + + +/* 二级页面面包屑导航 */ +.breadcrumb{ + width:1200px;height:40px;margin:0 auto; +} +.breadcrumb a{line-height:40px;color:#37ab40} +.breadcrumb a:hover{color:#ff8800} +.breadcrumb span{line-height:40px;color:#666;padding:0 5px;} + + +.main_wrap{width:1200px;margin:0 auto;} +.l_wrap{width:200px;} +.r_wrap{width:980px;} + + +/* 新品推荐样式 */ + +.new_goods{ + border:1px solid #ededed; + border-top:2px solid #37ab40; + padding-bottom:10px; +} + +.new_goods h3{ + height:33px;line-height:33px;background-color:#fcfcfc;border-bottom:1px solid #ededed;font-size:14px;font-weight:normal;text-indent:10px; +} + +.new_goods ul{width:160px;margin:0 auto;overflow:hidden;} +.new_goods li{border-bottom:1px solid #ededed;margin-bottom:-1px;} +.new_goods li img{display:block;width:150px;height:150px;margin:10px auto;} +.new_goods li h4{width:160px;margin:0 auto;} +.new_goods li h4 a{font-weight:normal;color:#666;display:block;width:160px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;} +.new_goods li .prize{font-size:14px;color:#da260e;margin:10px auto;} + + + +/* 商品列表样式 */ + +.sort_bar{height:30px;background-color:#f0fdec} +.sort_bar a{display:block;height:30px;line-height:30px;padding:0 20px;float:left;color:#000} +.sort_bar .active{background-color:#37ab40;color:#fff;} + + +.goods_type_list{ + margin:10px auto 0; +} + +.goods_type_list li{ + width:196px; + float:left; + margin-bottom:10px +} + +.goods_type_list li img{width:160px;height:160px;display:block;margin:10px auto;} +.goods_type_list li h4{width:160px;margin:0 auto;} +.goods_type_list li h4 a{font-weight:normal;color:#666;display:block;width:160px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;} +.operate{width:160px;margin:10px auto;position:relative;} +.goods_type_list .operate .prize{color:#da260e; font-size:14px;} +.goods_type_list .operate .unit{color:#999;padding-left:5px;} +.goods_type_list .operate .add_goods{display:inline-block;width:15px;height:15px;background:url(../images/shop_cart.png);position:absolute;right:0;top:3px;} + + +/* 分页样式 */ + +.pagenation{height:32px;text-align:center;font-size:0;margin:30px auto;} +.pagenation a{display:inline-block;border:1px solid #d2d2d2;background-color:#f8f6f7;font-size:12px;padding:7px 10px;color:#666;margin:5px} + +.pagenation .active{background-color:#fff;color:#43a200} + + +/* 商品详情样式 */ +.goods_detail_con{ + width:1198px; + height:398px; + border:1px solid #ededed; + margin:0 auto 20px; +} + +.goods_detail_pic{width:350px;height:350px;margin:24px 0 0 24px;} +.goods_detail_list{ + width:730px;height:350px;margin:24px 24px 0 0; +} +.goods_detail_list h3{font-size:24px;line-height:24px;color:#666;font-weight:normal;} +.goods_detail_list p{color:#666;line-height:40px;} +.prize_bar{height:72px;background-color:#fff5f5;line-height:72px;} +.prize_bar .show_pirze{font-size:20px;color:#ff3e3e;padding-left:20px} +.prize_bar .show_pirze em{font-style:normal;font-size:36px;padding-left:10px} +.prize_bar .show_unit{padding-left:150px} + +.goods_num{height:52px;margin-top:19px;} +.goods_num .num_name{width:70px;height:52px;line-height:52px;} +.goods_num .num_add{width:75px;height:50px;border:1px solid #dddddd} +.goods_num .num_add input{width:49px;height:50px;text-align:center;line-height:50px;border:0px;outline:none;font-size:14px;color:#666} +.goods_num .num_add .add,.goods_num .num_add .minus{width:25px;line-height:25px;text-align:center;border-left:1px solid #ddd;border-bottom:1px solid #ddd;color:#666;font-size:14px} +.goods_num .num_add .minus{border-bottom:0px} + +.total{height:35px;line-height:35px;margin-top:25px;} +.total em{font-style:normal;color:#ff3e3e;font-size:18px} + +.operate_btn{height:40px;margin-top:35px;font-size:0;position:relative;} +.operate_btn .buy_btn,.operate_btn .add_cart{display:inline-block;width:178px;height:38px;border:1px solid #c40000;font-size:14px;color:#c40000;line-height:38px;text-align:center;background-color:#ffeded;} +.operate_btn .add_cart{background-color:#c40000;color:#fff;margin-left:10px;position:relative;z-index:10;} + +.add_jump{width:20px;height:20px;background-color:#c40000;position:absolute;left:268px;top:10px;border-radius:50%;z-index:9;display:none;} + +.detail_tab{ + height:35px; + border-bottom:1px solid #37ab40 +} + +.detail_tab li{height:34px;line-height:34px;padding:0 30px;font-size:14px;color:#333333;float:left;border:1px solid #e8e8e8;border-bottom:0px;cursor:pointer;background-color:#faf8f8} + +.detail_tab li.active{border-top:2px solid #37ab40;position:relative;background-color:#fff;border-left:1px solid #37ab40;border-right:1px solid #37ab40;top:-1px;height:35px;} + +.tab_content dt{margin-top:10px;font-size:16px;color:#044d39} +.tab_content dd{line-height:24px;margin-top:5px;} + + +/* 登录页 */ + +.login_top{width:960px;height:130px;margin:0 auto;} +.login_logo{display:block;width:193px;height:76px;margin-top:30px;} +.login_form_bg{height:480px;background-color:#518e17} +.no-mp{margin-top:0px;} +.login_form_wrap{width:960px;height:480px;margin:0 auto;} +.login_banner{width:381px;height:322px;background:url(../images/login_banner.png) no-repeat;margin-top:90px;} +.slogan{width:40px;height:300px;font-size:30px;color:#f0f9e8;text-align:center;line-height:36px;margin:80px 0 0 120px} +.login_form{width:368px;height:378px;border:1px solid #c6c6c5;background-color:#fff; margin-top:50px;} + +.login_title{height:60px;width:308px;margin:10px auto;border-bottom:1px solid #e0e0e0;} + +.login_title h1{font-size:24px;height:60px;line-height:60px;color:#a8a8a8;float:left;font-weight:bold;margin-left:44px;} +.login_title a{width:100px;height:20px;display:block;font-size:16px;color:#5fb42a;text-indent:26px;background:url(../images/icons02.png) left 5px no-repeat;float:left;margin:20px 0 0 36px} + +.form_input{width:308px;height:250px;margin:20px auto;position:relative;} +.name_input,.pass_input{width:306px;height:36px;border:1px solid #e0e0e0;background:url(../images/icons02.png) 280px -41px no-repeat #f8f8f8;outline:none;font-size:14px;text-indent:10px;position: absolute;left:0;top:0} +.pass_input{top:65px;background-position:280px -95px;} + +.user_error,.pwd_error{color:#f00;position:absolute;left:0;top:43px;display:none} + +.pwd_error{top:110px;} + +.more_input{position:absolute;left:0;top:130px;width:100%} + +.more_input input{float:left;margin-top:2px;} +.more_input label{float:left;margin-left:10px;} +.more_input a{float:right;color:#666} +.more_input a:hover{color:#ff8800} + +.input_submit{width:100%;height:40px;position:absolute;left:0;top:180px;background-color:#47aa34;color:#fff;font-size:22px;border:0px;font-family:'Microsoft Yahei';cursor:pointer;} + + +/* 注册页面 */ +.register_con{ + width:700px; + height:560px; + margin:50px auto 0; + background:url(../images/interval_line.png) 300px top no-repeat; +} + +.l_con{width:300px;} +.reg_logo{width:200px;height:76px;float:right;margin-right:30px;} +.reg_slogan{width:300px;height:30px;float:right;text-align:right;font-size:24px;color:#69a81e;margin:20px 30px 0 0;} +.reg_banner{width:251px;height:329px;background:url(../images/register_banner.png) no-repeat;float:right; margin:20px 10px 0 0;opacity:0.5} + + +.r_con{width:400px;} +.reg_title{width:360px;height:50px;float:left;margin-left:30px;border-bottom:1px solid #e0e0e0} +.reg_title h1{height:50px;line-height:50px;float:left;font-size:24px;color:#a8a8a8;font-weight:bold;} +.reg_title a{float:right;height:20px;line-height:20px;font-size:16px;color:#5fb42a;padding-right:20px;background:url(../images/icons02.png) 35px 3px no-repeat;margin-top:15px} + +.reg_form{width:360px;margin:30px 0 0 30px;float:left;position:relative;} +.reg_form li{height:70px;} +.reg_form li label{width:70px;height:40px;line-height:40px;float:left;font-size:14px;color:#a8a8a8} +.reg_form li input{width:288px;height:38px;border:1px solid #e0e0e0;float:left;outline:none;text-indent:10px;background-color:#f8f8f8} +.reg_form li.agreement input{width:15px;height:15px;float:left;margin-top:13px} +.reg_form li.agreement label{width:300px;float:left;margin-left:10px;} +.reg_form li.reg_sub input{width:360px;height:40px;background-color:#47aa34;font-size:18px;color:#fff;font-family:'Microsoft Yahei';cursor:pointer;} +.reg_form li .error_tip{float:left;height:30px;line-height:30px;margin-left:70px;color:#e62e2e;display:none;} +.reg_form li .error_tip2{float:left;height:20px;line-height:20px;color:#e62e2e;display:none;} + + +.sub_page_name{font-size:18px;color:#666;margin:50px 0 0 20px} + +.total_count{ + width:1200px;margin:0 auto;height:40px;line-height:40px;font-size:14px; +} +.total_count em{ + font-size:16px;color:#ff4200;margin:0 5px; +} + +.cart_list_th{width:1198px;border:1px solid #ddd;background-color:#f7f7f7;margin:0 auto;} +.cart_list_th li{height:40px;line-height:40px;float:left;text-align:center;} +.cart_list_th .col01{width:26%;} +.cart_list_th .col02{width:16%;} +.cart_list_th .col03{width:13%;} +.cart_list_th .col04{width:12%;} +.cart_list_th .col05{width:15%;} +.cart_list_th .col06{width:18%;} + +.cart_list_td{width:1198px;border:1px solid #ddd;background-color:#edfff9;margin:0 auto;margin-top:-1px;} +.cart_list_td li{height:120px;line-height:120px;float:left;text-align:center;} + +.cart_list_td .col01{width:4%;} +.cart_list_td .col02{width:12%;} +.cart_list_td .col03{width:10%;} +.cart_list_td .col04{width:16%;} +.cart_list_td .col05{width:13%;} +.cart_list_td .col06{width:12%;} +.cart_list_td .col07{width:15%;} +.cart_list_td .col08{width:18%;} + +.cart_list_td .col02 img{width:100px;height:100px;border:1px solid #ddd;display:block;margin:10px auto 0;} +.cart_list_td .col03{height:48px;text-align:left;line-height:24px;margin-top:38px;} +.cart_list_td .col03 em{color:#999} +.cart_list_td .col08 a{color:#666} + +.cart_list_td .col06 .num_add{width:98px;height:28px;border:1px solid #ddd;margin:40px auto 0;} +.cart_list_td .col06 .num_add a{width:29px;height:28px;line-height:28px;background-color:#f3f3f3;font-size:14px;color:#666} +.cart_list_td .col06 .num_add input{width:38px;height:28px;text-align:center;line-height:30px;border:0px;display:block;float:left;outline:none;border-left:1px solid #ddd;border-right:1px solid #ddd;} + + +.settlements{width:1198px;height:78px;border:1px solid #ddd;background-color:#fff4e8;margin:-1px auto 0;} +.settlements li{line-height:78px;float:left;} +.settlements .col01{width:4%;text-align:center} +.settlements .col02{width:12%;} +.settlements .col03{width:69%; height:48px; line-height:28px;text-align:right;margin-top:10px;} +.settlements .col03 span{color:#ff0000;padding-right:5px} +.settlements .col03 em{color:#ff3d3d;font-size:22px;font-weight:bold;} +.settlements .col03 span{color:#ff0000;} +.settlements .col03 b{color:#ff0000;font-size:14px;padding:0 5px;} + +.settlements .col04{width:14%;text-align:center;float:right;} +.settlements .col04 a{display:block;height:78px;background-color:#ff3d3d;text-align:center;line-height:78px;color:#fff;font-size:24px} + + +.common_title{width:1200px;margin:20px auto 0;font-size:14px;} + +.common_list_con{width:1200px;border:1px solid #dddddd;border-top:2px solid #00bc6f;margin:10px auto 0;background-color:#f7f7f7;position:relative;} + +.common_list_con dl{margin:20px;} +.common_list_con dt{font-size:14px;font-weight:bold;margin-bottom:10px} +.common_list_con dd input{vertical-align:bottom;margin-right:10px} + +.edit_site{position:absolute; right:20px;top:30px;width:100px;height:30px;background-color:#37ab40;text-align:center;line-height:30px;color:#fff} + +.pay_style_con{margin:20px;} +.pay_style_con input{float:left;margin:14px 7px 0 0;} +.pay_style_con label{float:left;border:1px solid #ccc;background-color:#fff;padding:10px 10px 10px 40px;margin-right:25px} + +.pay_style_con .cash{background:url(../images/pay_icons.png) 8px top no-repeat #fff;} +.pay_style_con .weixin{background:url(../images/pay_icons.png) 6px -36px no-repeat #fff;} + +.pay_style_con .zhifubao{background:url(../images/pay_icons.png) 12px -72px no-repeat #fff;width:50px;height:16px} + +.pay_style_con .bank{background:url(../images/pay_icons.png) 6px -108px no-repeat #fff;} + + +.goods_list_th{height:40px;border-bottom:1px solid #ccc} +.goods_list_th li{float:left;line-height:40px;text-align:center;} +.goods_list_th .col01{width:25%} +.goods_list_th .col02{width:20%} +.goods_list_th .col03{width:25%} +.goods_list_th .col04{width:15%} +.goods_list_th .col05{width:15%} + +.goods_list_td{height:80px;border-bottom:1px solid #eeeded} +.goods_list_td li{float:left;line-height:80px;text-align:center;} +.goods_list_td .col01{width:4%} +.goods_list_td .col02{width:6%} +.goods_list_td .col03{width:15%} +.goods_list_td .col04{width:20%} +.goods_list_td .col05{width:25%} +.goods_list_td .col06{width:15%} +.goods_list_td .col07{width:15%} + +.goods_list_td .col02{text-align:right} +.goods_list_td .col02 img{width:63px;height:63px;border:1px solid #ddd;display:block;margin:7px 0;float:right;} +.goods_list_td .col03{text-align:left;text-indent:20px} + + +.settle_con{margin:10px} +.total_goods_count,.transit,.total_pay{line-height:24px;text-align:right} +.total_goods_count em,.total_goods_count b,.transit b,.total_pay b{font-size:14px;color:#ff4200;padding:0 5px;} + +.order_submit{width:1200px;margin:20px auto;} +.order_submit a{width:160px;height:40px;line-height:40px;text-align:center;background-color:#47aa34;color:#fff;font-size:16px;display:block;float:right} + + +.order_list_th{width:1198px;border:1px solid #ddd;background-color:#f7f7f7;margin:20px auto 0;} +.order_list_th li{float:left;height:30px;line-height:30px} +.order_list_th .col01{width:20%;margin-left:20px} +.order_list_th .col02{width:20%} + + +.order_list_table{ + width:1200px; + border-collapse:collapse; + border-spacing:0px; + border:1px solid #ddd; + margin:-1px auto 0; +} + +.order_list_table td{ + border:1px solid #ddd; + text-align:center; +} + +.order_goods_list{border-bottom:1px solid #ddd;margin-bottom:-2px;} +.order_goods_list li{float:left; height:80px;line-height:80px;} +.order_goods_list .col01{width:20%} +.order_goods_list .col01 img{width:60px;height:60px;border:1px solid #ddd;margin:10px auto;} +.order_goods_list .col02{width:50%;text-align:left;} +.order_goods_list .col02 em{color:#999;margin-left:10px} +.order_goods_list .col03{width:10%} +.order_goods_list .col04{width:20%} + +.oper_btn{display:inline-block;border:1px solid #ddd;color:#666;padding:5px 10px} + +.popup_con{display:none;} +.popup{width:300px;height:150px;border:1px solid #dddddd;border-top:2px solid #00bc6f;background-color:#f7f7f7;position:fixed; + left:50%; + margin-left:-150px; + top:50%; + margin-top:-75px; + z-index:1000; +} + +.popup p{height:150px;line-height:150px;text-align:center;font-size:18px;} + +.mask{width:100%;height:100%;position:fixed;left:0;top:0;background-color:#000;opacity:0.3;z-index:999;} + + +.main_con{ + width:1200px; + margin:0 auto; + background:url(../images/left_bg.jpg) repeat-y; +} + +.left_menu_con{ + width:200px; + float:left; +} + +.left_menu_con h3{ + font-size:16px; + line-height:40px; + border-bottom:1px solid #ddd; + text-align:center; + margin-bottom:10px; +} + +.left_menu_con ul li{ + line-height:40px; + text-align:center; + font-size:14px; +} + +.left_menu_con ul li a{ + color:#666; +} + +.left_menu_con ul li .active{ + color:#ff8800; + font-weight:bold; +} + +.right_content{ + width:980px; + float:right; + min-height:500px; +} + +.w980{ + width:980px; +} + +.w978{ + width:978px; +} + + +.common_title2{height:20px;line-height:20px;font-size:16px;margin:10px 0;} +.user_info_list{ + background-color:#f9f9f9; + margin:10px 0 15px; + padding:10px 0; + height:90px; +} + +.user_info_list li{ + line-height:30px; + text-indent:30px; + font-size:14px; +} + +.user_info_list li span{ + width:100px; + float:left; + text-align:right; +} + +.info_con{ + width:980px; +} + +.info_l{ + width:600px; + float:left; +} + +.info_r{ + width:360px; + float:right; +} + +.site_con{ + background-color:#f9f9f9; + padding:10px 0; + margin-bottom:20px; +} + +.site_con dt{ + font-size:14px; + line-height:30px; + text-indent:30px; + font-weight:bold; +} + +.site_con dd{ + font-size:14px; + line-height:30px; + text-indent:30px; +} + +.site_con .form_group{ + height:40px; + line-height:40px; + margin-top:10px; +} + +.site_con .form_group label{ + width:100px; + float:left; + text-align:right; + font-size:14px; + height:40px; + line-height:40px; +} + +.site_con .form_group input{ + width:300px; + height:25px; + border:1px solid #ddd; + float:left; + outline:none; + margin-top:7px; + text-indent:10px; +} + +.site_con .form_group2{ + height:90px; +} + +.site_area{ + width:280px; + height:60px; + border:1px solid #ddd; + outline:none; + padding:10px; +} +.info_submit{ + width:80px; + height:30px; + background-color:#37ab40; + border:0px; + color:#fff; + margin:10px 0 10px 100px; + cursor:pointer; + font-family:'Microsoft Yahei' +} + +.stress{ + color:#ff8800; +} \ No newline at end of file diff --git a/static/css/reset.css b/static/css/reset.css new file mode 100644 index 0000000..e0d41f1 --- /dev/null +++ b/static/css/reset.css @@ -0,0 +1,27 @@ +/* 把标签默认的间距设为0 */ +body,ul,ol,p,h1,h2,h3,h4,h5,h6,dl,dd,select,input,textarea,form{margin:0;padding:0} + +/* 让h标签文字大小继承body的文字设置 */ +h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;} + +/* 去掉列表默认的图标 */ +ul,ol{list-style:none;} + +/* 去掉em默认的斜体 */ +em{font-style: normal;} + +/* 去掉a标签默认的下划线 */ +a{text-decoration:none;} + + +/* 去掉加链接时产生的框线 */ +img{border:0;} + +/* 清除浮动 */ +.clearfix:before,.clearfix:after{content:"";display:table} +.clearfix:after{clear:both;} +.clearfix{zoom:1} + +/* 浮动 */ +.fl{float:left} +.fr{float:right} \ No newline at end of file diff --git a/static/detail.html b/static/detail.html new file mode 100644 index 0000000..9b7b94e --- /dev/null +++ b/static/detail.html @@ -0,0 +1,178 @@ + + + + + 天天生鲜-商品详情 + + + + + +
+
+
欢迎来到天天生鲜!
+
+ + + +
+
+
+ + + + + + + +
+
+ +
+

大兴大棚草莓

+

草莓浆果柔软多汁,味美爽口,适合速冻保鲜贮藏。草莓速冻后,可以保持原有的色、香、味,既便于贮藏,又便于外销。

+
+ ¥16.80 + 单 位:500g +
+
+
数 量:
+
+ + + + - +
+
+
总价:16.80元
+ +
+
+ +
+
+
+

新品推荐

+ +
+
+ +
+
    +
  • 商品介绍
  • +
  • 评论
  • +
+ +
+
+
商品详情:
+
草莓采摘园位于北京大兴区 庞各庄镇四各庄村 ,每年1月-6月面向北京以及周围城市提供新鲜草莓采摘和精品礼盒装草莓,草莓品种多样丰富,个大香甜。所有草莓均严格按照有机标准培育,不使用任何化肥和农药。草莓在采摘期间免洗可以直接食用。欢迎喜欢草莓的市民前来采摘,也欢迎各大单位选购精品有机草莓礼盒,有机草莓礼盒是亲朋馈赠、福利送礼的最佳选择。
+
+
+ +
+
+ + +
+ + + + + + \ No newline at end of file diff --git a/static/images/adv01.jpg b/static/images/adv01.jpg new file mode 100644 index 0000000000000000000000000000000000000000..70bac68f303191043f263755fff0cb080d91923d GIT binary patch literal 13988 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 literal 0 HcmV?d00001 diff --git a/static/images/adv02.jpg b/static/images/adv02.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bdc1028a6e1c33f95ac3494b0ccdc138197a5262 GIT binary patch literal 15859 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

    Zr2VuC!aD+HP(SNX>ZMa5kFb~1gP8ydq4fq& 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

    j&k;=drU@JtZuH?N4Vh)MO}Urou{THoEJP&`Xy1 z+1GrBm80)7e!maV|369ndLPCgaLG{BbaOS zYX*B$wBL+NKukF9J%1fBHNuj-S$kPzRmOXTD8#h zxjm1jt;}z1tD@)!IaK=6>(9w++qg91qzK@GI^a@=Ydi8gA$~VNbLSD?4X|iGo5QSW zIu`&ZFYmwt8qGgeQbqbrE4BM$s(ezn{K!wgk>sb?r_ssh(U0o(DmBh3OJaJ``qeIj)z6(iydD1#fU?>i?YzDL zAOvc8`%f}RNQ+q36@?bD;>lACXWv8Jk(q>xp!&>wc+7R65h%d9r=Ic%$49@AUA)Cgoj7r?{m=B{- zLdD@ldv511MHcfTaZ{1Q&z6Gi2|p&c2C zD1J%1&#%9SH$|zEus-i-B-nj;7ne9nKLtfqiff?+_HZ4GrF67zeu=c#SwaOvRyOmIQKhqQi_+X_o+bKG(5!rDHw<=6* zsH;-o6KqHh#0ipw-tN+^hL<_}w-7=N=`AxNU)%Ed%j>Ogdvg@|uJ(HZs#+DU7GFuQ zf9zv|KDXubzWxcd-{faeAZJwzR1}NMw58~LMB{21oj>i__m_`MywIS*Nguw$hErDE z{a&N@ZErGl7G3q1O>{^{39F)vLX5*}pnVNG$|+PH!537nc;Cq=4gG6+AFbHSh%&q2 zXWY`GC+&xv-p9A-Dt5Xn3xfSFZAJALmhq)r&QQq0`jcyW+errk!xT4S_I zdjk|3mOhM|TuhG*>=j(2c&tCV9kT+PbvwJ=ixZa}5x7{lk&8CKV9s12w$$uLhnBSCFz@QmG$HjV9+VkD!p-cXu1$jxTLJe-SNALlo{!7xIn-U(({4se?$iH1K!9^P@2`npQEo0Xq3qZTUnES3!9X*iT~`>lCDj zi#{rCXq7zVK(A`PDXy!qGF)L7_+mXFWK#MoAXzKHqB)E;St{@}uiffr9M{i#>={9ANS%PofkHFK)Q*4P@70akswRTp&kKjFi)ag6a8*Q=S zBp|EvcEoagWc3l1x}9VJxE}=(zz~5B?}yo>alyq*aLd@T6J@A04qAp~F%fvn_^}4e zbfdUm4%Ef8#N+ASG1Xg-MMY7Ll6>l$%5U2Dc%>30w_(AXSw?Cq3@LsGoaV&AykUFB zLBk&vwpE$MyfM2WeUIgtmW0k!aze_W&_wN~*V%~?7S&sfds&Kj;U0rKHvYJ_x++gI zd%*3nlJJN-DLB=6=M6-c0- Ta-1`i;8Hw%s;Ltlte^fDLHcS( literal 0 HcmV?d00001 diff --git a/static/images/banner02.jpg b/static/images/banner02.jpg new file mode 100644 index 0000000000000000000000000000000000000000..17cfe2e2b8da8258f71b4005c711014701c3d8d6 GIT binary patch literal 18351 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