优惠券

创建优惠券子应用

创建coupon子应用

  1. git checkout master
  2. git merge feature/order
  3. git push
  4. git checkout -b feature/coupon
  5. cd luffycityapi/apps
  6. python ../../manage.py startapp coupon

注册子应用,settings/dev.py,代码:

  1. INSTALLED_APPS = [
  2. # 子应用
  3. 。。。
  4. 'coupon',
  5. ]

子路由,coupon/urls.py,代码:

  1. from django.urls import path
  2. from . import views
  3. urlpatterns = [
  4. ]

总路由,luffycityapi/urls.py,代码:

  1. path("coupon/", include("coupon.urls")),

优惠券模型

模型分析:

chapter7.2-优惠券与积分 - 图1

coupon/models.py,模型创建,代码:

  1. from models import BaseModel, models
  2. from courses.models import CourseDirection, CourseCategory, Course
  3. from users.models import User
  4. from orders.models import Order
  5. # Create your models here.
  6. class Coupon(BaseModel):
  7. discount_choices = (
  8. (1, '减免'),
  9. (2, '折扣'),
  10. )
  11. type_choices = (
  12. (0, '通用类型'),
  13. (1, '指定方向'),
  14. (2, '指定分类'),
  15. (3, '指定课程'),
  16. )
  17. get_choices = (
  18. (0, "系统赠送"),
  19. (1, "自行领取"),
  20. )
  21. discount = models.SmallIntegerField(choices=discount_choices, default=1, verbose_name="优惠方式")
  22. coupon_type = models.SmallIntegerField(choices=type_choices, default=0, verbose_name="优惠券类型")
  23. total = models.IntegerField(blank=True, default=100, verbose_name="发放数量")
  24. has_total = models.IntegerField(blank=True, default=100, verbose_name="剩余数量")
  25. start_time = models.DateTimeField(verbose_name="启用时间")
  26. end_time = models.DateTimeField(verbose_name="过期时间")
  27. get_type = models.SmallIntegerField(choices=get_choices, default=0, verbose_name="领取方式")
  28. condition = models.IntegerField(blank=True, default=0, verbose_name="满足使用优惠券的价格条件")
  29. per_limit = models.SmallIntegerField(default=1, verbose_name="每人限制领取数量")
  30. sale = models.TextField(verbose_name="优惠公式", help_text="""
  31. *号开头表示折扣价,例如*0.82表示八二折;<br>
  32. -号开头表示减免价,例如-10表示在总价基础上减免10元<br>
  33. """)
  34. class Meta:
  35. db_table = "ly_coupon"
  36. verbose_name = "优惠券"
  37. verbose_name_plural = verbose_name
  38. class CouponDirection(models.Model):
  39. direction = models.ForeignKey(CourseDirection, on_delete=models.CASCADE, related_name="to_coupon", verbose_name="学习方向", db_constraint=False)
  40. coupon = models.ForeignKey(Coupon, on_delete=models.CASCADE, related_name="to_direction", verbose_name="优惠券", db_constraint=False)
  41. created_time = models.DateTimeField(auto_now_add=True, verbose_name="添加时间")
  42. class Meta:
  43. db_table = "ly_coupon_course_direction"
  44. verbose_name = "优惠券与学习方向"
  45. verbose_name_plural = verbose_name
  46. class CouponCourseCat(models.Model):
  47. category = models.ForeignKey(CourseCategory, on_delete=models.CASCADE, related_name="to_coupon", verbose_name="课程分类", db_constraint=False)
  48. coupon = models.ForeignKey(Coupon, on_delete=models.CASCADE, related_name="to_category", verbose_name="优惠券", db_constraint=False)
  49. created_time = models.DateTimeField(auto_now_add=True, verbose_name="添加时间")
  50. class Meta:
  51. db_table = "ly_coupon_course_category"
  52. verbose_name = "优惠券与课程分类"
  53. verbose_name_plural = verbose_name
  54. class CouponCourse(models.Model):
  55. course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name="to_coupon", verbose_name="课程", db_constraint=False)
  56. coupon = models.ForeignKey(Coupon, on_delete=models.CASCADE, related_name="to_course", verbose_name="优惠券", db_constraint=False)
  57. created_time = models.DateTimeField(auto_now_add=True, verbose_name="添加时间")
  58. class Meta:
  59. db_table = "ly_coupon_course"
  60. verbose_name = "优惠券与课程信息"
  61. verbose_name_plural = verbose_name
  62. class CouponLog(BaseModel):
  63. use_choices = (
  64. (0, "未使用"),
  65. (1, "已使用"),
  66. (2, "已过期"),
  67. )
  68. name = models.CharField(null=True, blank=True, max_length=100, verbose_name="名称/标题")
  69. user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="to_coupon", verbose_name="用户",
  70. db_constraint=False)
  71. coupon = models.ForeignKey(Coupon, on_delete=models.CASCADE, related_name="to_user", verbose_name="优惠券",
  72. db_constraint=False)
  73. order = models.ForeignKey(Order, null=True, blank=True, default=None, on_delete=models.CASCADE,
  74. related_name="to_coupon", verbose_name="订单", db_constraint=False)
  75. use_time = models.DateTimeField(null=True, blank=True, verbose_name="使用时间")
  76. use_status = models.SmallIntegerField(choices=use_choices, null=True, blank=True, default=0, verbose_name="使用状态")
  77. class Meta:
  78. db_table = "ly_coupon_log"
  79. verbose_name = "优惠券发放和使用日志"
  80. verbose_name_plural = verbose_name

数据迁移,终端下执行:

  1. cd ../..
  2. python manage.py makemigrations
  3. python manage.py migrate

把当前子应用注册到Admin管理站点

coupon/apps.py,代码:

  1. from django.apps import AppConfig
  2. class CouponConfig(AppConfig):
  3. default_auto_field = 'django.db.models.BigAutoField'
  4. name = 'coupon'
  5. verbose_name = "优惠券管理"
  6. verbose_name_plural = verbose_name

coupon/admin.py,代码:

  1. from django.contrib import admin
  2. from .models import Coupon, CouponDirection, CouponCourseCat, CouponCourse, CouponLog
  3. # Register your models here.
  4. class CouponDirectionInLine(admin.TabularInline): # admin.StackedInline
  5. """学习方向的内嵌类"""
  6. model = CouponDirection
  7. fields = ["id", "direction"]
  8. class CouponCourseCatInLine(admin.TabularInline): # admin.StackedInline
  9. """课程分类的内嵌类"""
  10. model = CouponCourseCat
  11. fields = ["id", "category"]
  12. class CouponCourseInLine(admin.TabularInline): # admin.StackedInline
  13. """课程信息的内嵌类"""
  14. model = CouponCourse
  15. fields = ["id", "course"]
  16. class CouponModelAdmin(admin.ModelAdmin):
  17. """优惠券的模型管理器"""
  18. list_display = ["id", "name", "start_time", "end_time", "total", "has_total", "coupon_type", "get_type", ]
  19. inlines = [CouponDirectionInLine, CouponCourseCatInLine, CouponCourseInLine]
  20. admin.site.register(Coupon, CouponModelAdmin)
  21. class CouponLogModelAdmin(admin.ModelAdmin):
  22. """优惠券发放和使用日志"""
  23. list_display = ["id", "user", "coupon", "order", "use_time", "use_status"]
  24. admin.site.register(CouponLog, CouponLogModelAdmin)

提交代码版本

  1. /home/moluo/Desktop/luffycity
  2. git add .
  3. git commit -m "feature: 创建优惠券子应用并设计优惠券的存储数据模型"
  4. git push --set-upstream origin feature/coupon

实现后台管理员给用户分发优惠券时自动记录到redis中。

settings/dev.py,代码:

  1. # 设置redis缓存
  2. CACHES = {
  3. # 。。。
  4. # 提供存储优惠券
  5. "coupon": {
  6. "BACKEND": "django_redis.cache.RedisCache",
  7. "LOCATION": "redis://:@127.0.0.1:6379/5",
  8. "OPTIONS": {
  9. "CLIENT_CLASS": "django_redis.client.DefaultClient",
  10. }
  11. },
  12. }

coupon/admin.py,代码:

  1. from django.contrib import admin
  2. from django_redis import get_redis_connection
  3. from .models import Coupon,CouponDirection,CouponCourseCat,CouponCourse,CouponLog
  4. from django.utils.timezone import datetime
  5. import json
  6. class CouponDirectionInLine(admin.TabularInline): # admin.StackedInline
  7. """学习方向的内嵌类"""
  8. model = CouponDirection
  9. fields = ["id","diretion"]
  10. class CouponCourseCatInLine(admin.TabularInline): # admin.StackedInline
  11. """课程分类的内嵌类"""
  12. model = CouponCourseCat
  13. fields = ["id","category"]
  14. class CouponCourseInLine(admin.TabularInline): # admin.StackedInline
  15. """课程信息的内嵌类"""
  16. model = CouponCourse
  17. fields = ["id","course"]
  18. class CouponModelAdmin(admin.ModelAdmin):
  19. """优惠券的模型管理器"""
  20. list_display = ["id","name","start_time","end_time","total","has_total","coupon_type","get_type",]
  21. inlines = [CouponDirectionInLine, CouponCourseCatInLine, CouponCourseInLine]
  22. admin.site.register(Coupon, CouponModelAdmin)
  23. class CouponLogModelAdmin(admin.ModelAdmin):
  24. """优惠券发放和使用记录"""
  25. list_display = ["id","user","coupon","order","use_time","use_status"]
  26. def save_model(self, request, obj, form, change):
  27. """
  28. 保存或更新记录时自动执行的钩子
  29. request: 本次客户端提交的请求对象
  30. obj: 本次操作的模型实例对象
  31. form: 本次客户端提交的表单数据
  32. change: 值为True,表示更新数据,值为False,表示添加数据
  33. """
  34. obj.save()
  35. # 同步记录到redis中
  36. redis = get_redis_connection("coupon")
  37. # print(obj.use_status , obj.use_time)
  38. if obj.use_status == 0 and obj.use_time == None:
  39. # 记录优惠券信息到redis中
  40. pipe = redis.pipeline()
  41. pipe.multi()
  42. pipe.hset(f"{obj.user.id}:{obj.id}","coupon_id", obj.coupon.id)
  43. pipe.hset(f"{obj.user.id}:{obj.id}","name", obj.coupon.name)
  44. pipe.hset(f"{obj.user.id}:{obj.id}","discount", obj.coupon.discount)
  45. pipe.hset(f"{obj.user.id}:{obj.id}","get_discount_display", obj.coupon.get_discount_display())
  46. pipe.hset(f"{obj.user.id}:{obj.id}","coupon_type", obj.coupon.coupon_type)
  47. pipe.hset(f"{obj.user.id}:{obj.id}","get_coupon_type_display", obj.coupon.get_coupon_type_display())
  48. pipe.hset(f"{obj.user.id}:{obj.id}","start_time", obj.coupon.start_time.strftime("%Y-%m-%d %H:%M:%S"))
  49. pipe.hset(f"{obj.user.id}:{obj.id}","end_time", obj.coupon.end_time.strftime("%Y-%m-%d %H:%M:%S"))
  50. pipe.hset(f"{obj.user.id}:{obj.id}","get_type", obj.coupon.get_type)
  51. pipe.hset(f"{obj.user.id}:{obj.id}","get_get_type_display", obj.coupon.get_get_type_display())
  52. pipe.hset(f"{obj.user.id}:{obj.id}","condition", obj.coupon.condition)
  53. pipe.hset(f"{obj.user.id}:{obj.id}","sale", obj.coupon.sale)
  54. pipe.hset(f"{obj.user.id}:{obj.id}","to_direction", json.dumps(list(obj.coupon.to_direction.values("direction__id","direction__name"))))
  55. pipe.hset(f"{obj.user.id}:{obj.id}","to_category", json.dumps(list(obj.coupon.to_category.values("category__id","category__name"))))
  56. pipe.hset(f"{obj.user.id}:{obj.id}","to_course", json.dumps(list(obj.coupon.to_course.values("course__id","course__name"))))
  57. # 设置当前优惠券的有效期
  58. pipe.expire(f"{obj.user.id}:{obj.id}", int(obj.coupon.end_time.timestamp() - datetime.now().timestamp()))
  59. pipe.execute()
  60. else:
  61. redis.delete(f"{obj.user.id}:{obj.id}")
  62. def delete_model(self, request, obj):
  63. """删除记录时自动执行的钩子"""
  64. # 如果系统后台管理员删除当前优惠券记录,则redis中的对应记录也被删除
  65. print(obj, "详情页中删除一个记录")
  66. redis = get_redis_connection("coupon")
  67. redis.delete(f"{obj.user.id}:{obj.id}")
  68. obj.delete()
  69. def delete_queryset(self, request, queryset):
  70. """在列表页中进行删除优惠券记录时,也要同步删除容redis中的记录"""
  71. print(queryset, "列表页中删除多个记录")
  72. redis = get_redis_connection("coupon")
  73. for obj in queryset:
  74. redis.delete(f"{obj.user.id}:{obj.id}")
  75. queryset.delete()
  76. admin.site.register(CouponLog, CouponLogModelAdmin)

添加测试数据,代码:

  1. -- 优惠券测试数据
  2. truncate table ly_coupon;
  3. INSERT INTO ly_coupon (id, name, is_deleted, orders, is_show, created_time, updated_time, discount, coupon_type, total, has_total, start_time, end_time, get_type, `condition`, per_limit, sale) VALUES (1, '30元通用优惠券', 0, 1, 1, '2022-05-04 10:35:40.569417', '2022-06-30 10:25:00.353212', 1, 0, 10000, 10000, '2022-05-04 10:35:00', '2023-01-02 10:35:00', 0, 100, 1, '-30'),(2, '前端学习通用优惠券', 0, 1, 1, '2022-05-04 10:36:58.401527', '2022-05-04 10:36:58.401556', 1, 1, 100, 100, '2022-05-04 10:36:00', '2022-08-04 10:36:00', 0, 0, 1, '-50'),(3, 'Typescript课程专用券', 0, 1, 1, '2022-05-04 10:38:36.134581', '2022-05-04 10:38:36.134624', 2, 3, 1000, 1000, '2022-05-04 10:38:00', '2022-08-04 10:38:00', 0, 0, 1, '*0.88'),(4, 'python七夕专用券', 0, 1, 1, '2022-05-04 10:40:08.022904', '2022-06-30 10:25:46.949197', 1, 2, 200, 200, '2022-05-04 10:39:00', '2022-11-15 10:39:00', 1, 0, 1, '-99'),(5, '算法学习优惠券', 0, 1, 1, '2021-08-05 10:05:07.837008', '2022-06-30 10:26:12.133812', 2, 2, 1000, 1000, '2022-08-05 10:04:00', '2022-12-25 10:04:00', 0, 200, 1, '*0.85');
  4. -- 优惠券与学习方向的关系测试数据
  5. truncate table ly_coupon_course_direction;
  6. INSERT INTO ly_coupon_course_direction (id, created_time, coupon_id, direction_id) VALUES (1, '2022-05-04 10:36:58.414461', 2, 1);
  7. -- 优惠券与课程分类的关系测试数据
  8. truncate table ly_coupon_course_category;
  9. INSERT INTO .ly_coupon_course_category (id, created_time, category_id, coupon_id) VALUES (1, '2022-05-04 10:40:08.029505', 20, 4),(2, '2022-05-04 10:40:08.042891', 21, 4),(3, '2021-08-05 10:05:07.966221', 33, 5);
  10. -- 优惠券与课程信息的关系测试数据
  11. truncate table ly_coupon_course;
  12. INSERT INTO ly_coupon_course (id, created_time, coupon_id, course_id) VALUES (1, '2022-05-04 10:38:36.140929', 3, 1),(2, '2022-05-04 10:38:36.143166', 3, 2);
  13. -- 优惠券的发放和使用日志的测试数据
  14. truncate table ly_coupon_log;
  15. INSERT INTO luffycity.ly_coupon_log (id, is_deleted, orders, is_show, created_time, updated_time, name, use_time, use_status, coupon_id, order_id, user_id) VALUES (5, 0, 1, 1, '2022-05-04 12:00:25.051976', '2022-06-30 10:25:17.681298', '30元通用优惠券222', null, 0, 1, null, 1),(8, 0, 1, 1, '2022-05-04 12:03:24.331024', '2022-06-30 10:22:45.834401', '前端学习通用优惠券', null, 0, 2, null, 1),(9, 0, 1, 1, '2022-05-04 12:03:31.692397', '2022-06-30 10:23:41.492205', 'Typescript课程专用券', null, 0, 3, null, 1),(10, 0, 1, 1, '2022-05-04 12:03:38.225438', '2022-06-30 10:25:49.797318', 'python七夕专用券', null, 0, 4, null, 1),(11, 0, 1, 1, '2022-05-04 12:09:25.406437', '2022-06-30 10:23:55.832262', '前端学习通用优惠券', null, 0, 2, null, 1),(12, 0, 1, 1, '2021-08-05 10:06:06.036230', '2022-06-30 10:26:20.723668', '算法学习优惠券', null, 0, 5, null, 1);

注意:添加测试数据完成以后,因为是通过SQL语句来添加的。务必在Admin站点中对优惠券的发放和使用日志这功能中每一条数据进行一次的更新操作,打开数据详情页不需要修改任何数据,保存即可,这样才能让用户的优惠券信息同步到redis中!!!注意:如果是已经过期的优惠券,则不会被同步到redis中。

提交代码版本

  1. cd /home/moluo/Desktop/luffycity
  2. git add .
  3. git commit -m "feature: 实现后台管理员给用户分发优惠券时自动记录到redis中"
  4. git push --set-upstream origin feature/coupon

获取用户本次下单的可用优惠券

封装工具函数,获取当前用户拥有的所有优惠券以及本次下单的可用优惠券列表,coupon/services.py,代码:

  1. import json
  2. from django_redis import get_redis_connection
  3. from courses.models import Course
  4. def get_user_coupon_list(user_id):
  5. """获取指定用户拥有的所有优惠券列表"""
  6. redis = get_redis_connection("coupon")
  7. coupon_list = redis.keys(f"{user_id}:*")
  8. try:
  9. coupon_id_list = [item.decode() for item in coupon_list]
  10. except:
  11. coupon_id_list = []
  12. coupon_data = []
  13. # 遍历redis中所有的优惠券数据并转换数据格式
  14. for coupon_key in coupon_id_list:
  15. coupon_item = {"user_coupon_id": int(coupon_key.split(":")[-1])}
  16. coupon_hash = redis.hgetall(coupon_key)
  17. for key, value in coupon_hash.items():
  18. key = key.decode()
  19. value = value.decode()
  20. if key in ["to_course", "to_category", "to_direction"]:
  21. value = json.loads(value)
  22. coupon_item[key] = value
  23. coupon_data.append(coupon_item)
  24. return coupon_data
  25. def get_user_enable_coupon_list(user_id):
  26. """
  27. 获取指定用户本次下单的可用优惠券列表
  28. # 根据当前本次客户端购买商品课程进行比较,获取用户的当前可用优惠券。
  29. """
  30. redis = get_redis_connection("cart")
  31. # 先获取所有的优惠券列表
  32. coupon_data = get_user_coupon_list(user_id)
  33. # 获取指定用户的购物车中的勾选商品[与优惠券的适用范围进行比对,找出能用的优惠券]
  34. cart_hash = redis.hgetall(f"cart_{user_id}")
  35. # 获取被勾选的商品课程的ID列表
  36. course_id_list = {int(key.decode()) for key, value in cart_hash.items() if value == b'1'}
  37. # 获取被勾选的商品课程的模型对象列表
  38. course_list = Course.objects.filter(pk__in=course_id_list, is_deleted=False, is_show=True).all()
  39. category_id_list = set()
  40. direction_id_list = set()
  41. for course in course_list:
  42. # 获取被勾选的商品课程的父类课程分类id列表,并保证去重
  43. category_id_list.add(int(course.category.id))
  44. # # 获取被勾选的商品课程的父类学习方向id列表,并保证去重
  45. direction_id_list.add(int(course.direction.id))
  46. # 创建一个列表用于保存所有的可用优惠券
  47. enable_coupon_list = []
  48. for item in coupon_data:
  49. coupon_type = int(item.get("coupon_type"))
  50. if coupon_type == 0:
  51. # 通用类型优惠券
  52. item["enable_course"] = "__all__"
  53. enable_coupon_list.append(item)
  54. elif coupon_type == 3:
  55. # 指定课程优惠券
  56. coupon_course = {int(course_item["course__id"]) for course_item in item.get("to_course")}
  57. # 并集处理
  58. ret = course_id_list & coupon_course
  59. if len(ret) > 0:
  60. item["enable_course"] = {int(course.id) for course in course_list if course.id in ret}
  61. enable_coupon_list.append(item)
  62. elif coupon_type == 2:
  63. # 指定课程分配优惠券
  64. coupon_category = {int(category_item["category__id"]) for category_item in item.get("to_category")}
  65. # 并集处理
  66. ret = category_id_list & coupon_category
  67. if len(ret) > 0:
  68. item["enable_course"] = {int(course.id) for course in course_list if course.category.id in ret}
  69. enable_coupon_list.append(item)
  70. elif coupon_type == 1:
  71. # 指定学习方向的优惠券
  72. coupon_direction = {int(direction_item["direction__id"]) for direction_item in item.get("to_direction")}
  73. # 并集处理
  74. ret = direction_id_list & coupon_direction
  75. if len(ret) > 0:
  76. item["enable_course"] = {int(course.id) for course in course_list if course.direction.id in ret}
  77. enable_coupon_list.append(item)
  78. return enable_coupon_list

coupon/views.py,代码:

  1. from rest_framework.views import APIView
  2. from rest_framework.permissions import IsAuthenticated
  3. from rest_framework.response import Response
  4. from .services import get_user_coupon_list, get_user_enable_coupon_list
  5. class CouponListAPIView(APIView):
  6. permission_classes = [IsAuthenticated]
  7. def get(self, request):
  8. """获取用户拥有的所有优惠券"""
  9. user_id = request.user.id
  10. coupon_data = get_user_coupon_list(user_id)
  11. return Response(coupon_data)
  12. class EnableCouponListAPIView(APIView):
  13. permission_classes = [IsAuthenticated]
  14. def get(self, request):
  15. """获取用户本次拥有的本次下单可用所有优惠券"""
  16. user_id = request.user.id
  17. coupon_data = get_user_enable_coupon_list(user_id)
  18. return Response(coupon_data)

coupon/urls.py,代码:

  1. from django.urls import path
  2. from . import views
  3. urlpatterns = [
  4. path("", views.CouponListAPIView.as_view()),
  5. path("enable/", views.EnableCouponListAPIView.as_view()),
  6. ]

提交代码版本

  1. cd /home/moluo/Desktop/luffycity
  2. git add .
  3. git commit -m "feature: 服务端实现获取用户所有优惠券与本次下单的可用优惠券列表"
  4. git push feature/coupon

客户端展示用户拥有的可用优惠券

api/order.js,代码:

  1. import http from "../utils/http";
  2. import {reactive} from "vue";
  3. const order = reactive({
  4. total_price: 0, // 勾选商品的总价格
  5. discount_price: 0, // 本次下单的优惠抵扣价格
  6. discount_type: 0, // 0表示优惠券,1表示积分
  7. use_coupon: false, // 用户是否使用优惠
  8. coupon_list:[], // 用户拥有的可用优惠券列表
  9. select: -1, // 当前用户选中的优惠券下标,-1表示没有选择
  10. credit: 0, // 当前用户选择抵扣的积分,0表示没有使用积分
  11. fixed: true, // 底部订单总价是否固定浮动
  12. pay_type: 0, // 支付方式
  13. create_order(token){
  14. // 生成订单
  15. return http.post("/orders/",{
  16. pay_type: this.pay_type
  17. },{
  18. headers:{
  19. Authorization: "jwt " + token,
  20. }
  21. })
  22. },
  23. get_enable_coupon_list(token){
  24. // 获取本次下单的可用优惠券列表
  25. return http.get("/coupon/enable/",{
  26. headers:{
  27. Authorization: "jwt " + token,
  28. }
  29. })
  30. }
  31. })
  32. export default order;

views/Order.vue,代码:

  1. <transition name="el-zoom-in-top">
  2. <div class="coupon-del-box" v-if="order.use_coupon">
  3. <div class="coupon-switch-box">
  4. <div class="switch-btn ticket" :class="{'checked': order.discount_type===0}" @click="order.discount_type=0">优惠券 (4)<em><i class="imv2-check"></i></em></div>
  5. <div class="switch-btn code" :class="{'checked': order.discount_type===1}" @click="order.discount_type=1">积分<em><i class="imv2-check"></i></em></div>
  6. </div>
  7. <div class="coupon-content ticket" v-if="order.discount_type===0">
  8. <p class="no-coupons" v-if="order.coupon_list.length<1">暂无可用优惠券</p>
  9. <div class="coupons-box" v-else>
  10. <div class="content-box">
  11. <ul class="nouse-box">
  12. <li class="l" :class="{select: order.select === key}" @click="order.select = (order.select === key?-1:key)" v-for="(coupon,key) in order.coupon_list" :key="key">
  13. <div class="detail-box more-del-box">
  14. <div class="price-box">
  15. <p class="coupon-price l" v-if="coupon.discount === '1'"> ¥{{Math.abs(coupon.sale)}} </p>
  16. <p class="coupon-price l" v-if="coupon.discount === '2'"> {{coupon.sale.replace("*0.","")}}折 </p>
  17. <p class="use-inst l" v-if="coupon.condition>0">满{{coupon.condition}}元可用</p>
  18. <p class="use-inst l" v-else>任意使用</p>
  19. </div>
  20. <div class="use-detail-box">
  21. <div class="use-ajust-box">适用于:{{coupon.name}}</div>
  22. <div class="use-ajust-box">有效期:{{coupon.start_time.split(" ")[0].replaceAll("-",".")}}-{{coupon.end_time.split(" ")[0].replaceAll("-",".")}}</div>
  23. </div>
  24. </div>
  25. </li>
  26. </ul>
  27. <!-- <ul class="use-box">-->
  28. <!-- <li class="l useing">-->
  29. <!-- <div class="detail-box more-del-box">-->
  30. <!-- <div class="price-box">-->
  31. <!-- <p class="coupon-price l"> ¥100 </p>-->
  32. <!-- <p class="use-inst l">满499可用</p>-->
  33. <!-- </div>-->
  34. <!-- <div class="use-detail-box">-->
  35. <!-- <div class="use-ajust-box">适用于:全部实战课程</div>-->
  36. <!-- <div class="use-ajust-box">有效期:2021.06.01-2021.06.18</div>-->
  37. <!-- </div>-->
  38. <!-- </div>-->
  39. <!-- </li>-->
  40. <!-- <li class="l">-->
  41. <!-- <div class="detail-box more-del-box">-->
  42. <!-- <div class="price-box">-->
  43. <!-- <p class="coupon-price l"> ¥248 </p>-->
  44. <!-- <p class="use-inst l">满999可用</p>-->
  45. <!-- </div>-->
  46. <!-- <div class="use-detail-box">-->
  47. <!-- <div class="use-ajust-box">适用于:全部实战课程</div>-->
  48. <!-- <div class="use-ajust-box">有效期:2021.06.01-2021.06.18</div>-->
  49. <!-- </div>-->
  50. <!-- </div>-->
  51. <!-- </li>-->
  52. <!-- </ul>-->
  53. <!-- <ul class="overdue-box">-->
  54. <!-- <li class="l useing">-->
  55. <!-- <div class="detail-box more-del-box">-->
  56. <!-- <div class="price-box">-->
  57. <!-- <p class="coupon-price l"> ¥100 </p>-->
  58. <!-- <p class="use-inst l">满499可用</p>-->
  59. <!-- </div>-->
  60. <!-- <div class="use-detail-box">-->
  61. <!-- <div class="use-ajust-box">适用于:全部实战课程</div>-->
  62. <!-- <div class="use-ajust-box">有效期:2021.06.01-2021.06.18</div>-->
  63. <!-- </div>-->
  64. <!-- </div>-->
  65. <!-- </li>-->
  66. <!-- <li class="l">-->
  67. <!-- <div class="detail-box more-del-box">-->
  68. <!-- <div class="price-box">-->
  69. <!-- <p class="coupon-price l"> ¥248 </p>-->
  70. <!-- <p class="use-inst l">满999可用</p>-->
  71. <!-- </div>-->
  72. <!-- <div class="use-detail-box">-->
  73. <!-- <div class="use-ajust-box">适用于:全部实战课程</div>-->
  74. <!-- <div class="use-ajust-box">有效期:2021.06.01-2021.06.18</div>-->
  75. <!-- </div>-->
  76. <!-- </div>-->
  77. <!-- </li>-->
  78. <!-- </ul>-->
  79. </div>
  80. </div>
  81. </div>
  82. <div class="coupon-content code" v-else>
  83. <div class="input-box">
  84. <el-input-number placeholder="10积分=1元" v-model="order.credit" :step="1" :min="0" :max="1000"></el-input-number>
  85. <a class="convert-btn">兑换</a>
  86. </div>
  87. <div class="converted-box">
  88. <p>使用积分:<span class="code-num">200</span></p>
  89. <p class="course-title">课程:<span class="c_name">3天JavaScript入门</span>
  90. <span class="discount-cash">100积分抵扣:<em>10</em></span>
  91. </p>
  92. <p class="course-title">课程:<span class="c_name">3天JavaScript入门</span>
  93. <span class="discount-cash">100积分抵扣:<em>10</em></span>
  94. </p>
  95. </div>
  96. <p class="error-msg">本次订单最多可以使用1000积分,您当前拥有200积分。(10积分=1元)</p>
  97. <p class="tip">说明:每笔订单只能使用一次积分,并只有在部分允许使用积分兑换的课程中才能使用。</p>
  98. </div>
  99. </div>
  100. </transition>
  1. <script setup>
  2. import {reactive,watch} from "vue"
  3. import Header from "../components/Header.vue"
  4. import Footer from "../components/Footer.vue"
  5. import {useStore} from "vuex";
  6. import cart from "../api/cart"
  7. import order from "../api/order";
  8. import {ElMessage} from "element-plus";
  9. import router from "../router";
  10. // let store = useStore()
  11. const get_select_course = ()=>{
  12. // 获取购物车中的勾选商品列表
  13. let token = sessionStorage.token || localStorage.token;
  14. cart.get_select_course(token).then(response=>{
  15. cart.select_course_list = response.data.cart
  16. if(response.data.cart.length === 0){
  17. ElMessage.error("当前购物车中没有下单的商品!请重新重新选择购物车中要购买的商品~");
  18. router.back();
  19. }
  20. }).catch(error=>{
  21. if(error?.response?.status===400){
  22. ElMessage.error("登录超时!请重新登录后再继续操作~");
  23. }
  24. })
  25. }
  26. get_select_course();
  27. const commit_order = ()=>{
  28. // 生成订单
  29. let token = sessionStorage.token || localStorage.token;
  30. order.create_order(token).then(response=>{
  31. console.log(response.data.order_number) // todo 订单号
  32. console.log(response.data.pay_link) // todo 支付链接
  33. // 成功提示
  34. ElMessage.success("下单成功!马上跳转到支付页面,请稍候~")
  35. // 扣除掉被下单的商品数量,更新购物车中的商品数量
  36. store.commit("set_cart_total", store.state.cart_total - cart.select_course_list.length);
  37. }).catch(error=>{
  38. if(error?.response?.status===400){
  39. ElMessage.success("登录超时!请重新登录后再继续操作~");
  40. }
  41. })
  42. }
  43. // 获取本次下单的可用优惠券
  44. const get_enable_coupon_list = ()=>{
  45. let token = sessionStorage.token || localStorage.token;
  46. order.get_enable_coupon_list(token).then(response=>{
  47. order.coupon_list = response.data
  48. })
  49. }
  50. get_enable_coupon_list()
  51. // 监听用户选择的支付方式
  52. watch(
  53. ()=>order.pay_type,
  54. ()=>{
  55. console.log(order.pay_type)
  56. }
  57. )
  58. // 底部订单总价信息固定浮动效果
  59. window.onscroll = ()=>{
  60. let cart_body_table = document.querySelector(".cart-body-table")
  61. let offsetY = window.scrollY
  62. let maxY = cart_body_table.offsetTop+cart_body_table.offsetHeight
  63. order.fixed = offsetY < maxY
  64. }
  65. </script>

用户勾选优惠券后调整订单实付价格

  1. <div class="pay-box" :class="{fixed:order.fixed}">
  2. <div class="row-bottom">
  3. <div class="row">
  4. <div class="goods-total-price-box">
  5. <p class="r rw price-num"><em></em><span>{{cart.total_price.toFixed(2)}}</span></p>
  6. <p class="r price-text"><span><span>{{cart.select_course_list?.length}}</span>件商品,</span>商品总金额:</p>
  7. </div>
  8. </div>
  9. <div class="coupons-discount-box">
  10. <p class="r rw price-num">-<em></em><span>{{order.discount_price.toFixed(2)}}</span></p>
  11. <p class="r price-text">优惠券/积分抵扣:</p>
  12. </div>
  13. <div class="pay-price-box clearfix">
  14. <p class="r rw price"><em></em><span id="js-pay-price">{{ (cart.total_price-order.discount_price).toFixed(2)}}</span></p>
  15. <p class="r price-text">应付:</p>
  16. </div>
  17. <span class="r btn btn-red submit-btn" @click="commit_order">提交订单</span>
  18. </div>
  19. <div class="pay-add-sign">
  20. <ul class="clearfix">
  21. <li>支持花呗</li>
  22. <li>可开发票</li>
  23. <li class="drawback">7天可退款</li>
  24. </ul>
  25. </div>
  26. </div>
  1. <script setup>
  2. import {reactive,watch} from "vue"
  3. import Header from "../components/Header.vue"
  4. import Footer from "../components/Footer.vue"
  5. import {useStore} from "vuex";
  6. import cart from "../api/cart"
  7. import order from "../api/order";
  8. import {ElMessage} from "element-plus";
  9. import router from "../router";
  10. // let store = useStore()
  11. const get_select_course = ()=>{
  12. // 获取购物车中的勾选商品列表
  13. let token = sessionStorage.token || localStorage.token;
  14. cart.get_select_course(token).then(response=>{
  15. cart.select_course_list = response.data.cart
  16. if(response.data.cart.length === 0){
  17. ElMessage.error("当前购物车中没有下单的商品!请重新重新选择购物车中要购买的商品~");
  18. router.back();
  19. }
  20. // 计算本次下单的总价格
  21. let sum = 0
  22. response.data.cart?.forEach((course,key)=>{
  23. if(course.discount.price > 0 || course.discount.price === 0){
  24. sum+=course.discount.price
  25. }else{
  26. sum+=course.price
  27. }
  28. })
  29. cart.total_price = sum;
  30. }).catch(error=>{
  31. if(error?.response?.status===400){
  32. ElMessage.error("登录超时!请重新登录后再继续操作~");
  33. }
  34. })
  35. }
  36. get_select_course();
  37. const commit_order = ()=>{
  38. // 生成订单
  39. let token = sessionStorage.token || localStorage.token;
  40. order.create_order(token).then(response=>{
  41. console.log(response.data.order_number) // todo 订单号
  42. console.log(response.data.pay_link) // todo 支付链接
  43. // 成功提示
  44. ElMessage.success("下单成功!马上跳转到支付页面,请稍候~")
  45. // 扣除掉被下单的商品数量,更新购物车中的商品数量
  46. store.commit("set_cart_total", store.state.cart_total - cart.select_course_list.length);
  47. }).catch(error=>{
  48. if(error?.response?.status===400){
  49. ElMessage.success("登录超时!请重新登录后再继续操作~");
  50. }
  51. })
  52. }
  53. // 获取本次下单的可用优惠券
  54. const get_enable_coupon_list = ()=>{
  55. let token = sessionStorage.token || localStorage.token;
  56. order.get_enable_coupon_list(token).then(response=>{
  57. order.coupon_list = response.data
  58. })
  59. }
  60. get_enable_coupon_list()
  61. // 监听用户选择的支付方式
  62. watch(
  63. ()=>order.pay_type,
  64. ()=>{
  65. console.log(order.pay_type)
  66. }
  67. )
  68. // 监听用户选择的优惠券
  69. watch(
  70. ()=>order.select,
  71. ()=>{
  72. order.discount_price = 0;
  73. // 如果没有选择任何的优惠券,则select 为-1,那么不用进行计算优惠券折扣的价格了
  74. if (order.select === -1) {
  75. return // 阻止代码继续往下执行
  76. }
  77. // 根据下标select,获取当前选中的优惠券信息
  78. let current_coupon = order.coupon_list[order.select]
  79. console.log(current_coupon);
  80. // 针对折扣优惠券,找到最大优惠的课程
  81. let max_discount = -1;
  82. for(let course of cart.select_course_list) { // 循环本次下单的勾选商品
  83. // 找到当前优惠券的可用课程
  84. if(current_coupon.enable_course === "__all__") { // 如果当前优惠券是通用优惠券
  85. if(max_discount !== -1){
  86. if(course.price > max_discount.price){ // 在每次循环中,那当前循环的课程的价格与之前循环中得到的最大优惠课程的价格进行比较
  87. max_discount = course
  88. }
  89. }else{
  90. max_discount = course
  91. }
  92. }else if((current_coupon.enable_course.indexOf(course.id) > -1) && (course.price >= parseFloat(current_coupon.condition))){
  93. // 判断 当前优惠券如果包含了当前课程, 并 课程的价格 > 当前优惠券的使用门槛
  94. // 只允许没有参与其他优惠券活动的课程使用优惠券,基本所有的平台都不存在折上折的。
  95. if( course.discount.price === undefined ) {
  96. if(max_discount !== -1){
  97. if(course.price > max_discount.price){
  98. max_discount = course
  99. }
  100. }else{
  101. max_discount = course
  102. }
  103. }
  104. }
  105. }
  106. if(max_discount !== -1){
  107. if(current_coupon.discount === '1') { // 抵扣优惠券[抵扣的价格就是当前优惠券的价格]
  108. order.discount_price = parseFloat( Math.abs(current_coupon.sale) )
  109. }else if(current_coupon.discount === '2') { // 折扣优惠券]抵扣的价格就是(1-折扣百分比) * 课程原价]
  110. order.discount_price = parseFloat(max_discount.price * (1-parseFloat(current_coupon.sale.replace("*",""))) )
  111. }
  112. }else{
  113. order.select = -1
  114. order.discount_price = 0
  115. ElMessage.error("当前课程商品已经参与了其他优惠活动,无法再次使用当前优惠券!")
  116. }
  117. })
  118. // 底部订单总价信息固定浮动效果
  119. window.onscroll = ()=>{
  120. let cart_body_table = document.querySelector(".cart-body-table")
  121. let offsetY = window.scrollY
  122. let maxY = cart_body_table.offsetTop+cart_body_table.offsetHeight
  123. order.fixed = offsetY < maxY
  124. }
  125. </script>

提交代码版本

  1. cd /home/moluo/Desktop/luffycity
  2. git add .
  3. git commit -m "feature: 客户端展示用户本次下单的可用优惠券并重新调整价格"
  4. git push

客户端发送请求附带优惠券记录ID

客户端下单以后,本次请求附带使用的 用户优惠券记录ID到服务端,服务端进行验证计算,得到正确的实付价格,并从redis中删除用户使用的优惠券。

api/order.js,代码:

  1. import http from "../utils/http";
  2. import {reactive} from "vue";
  3. const order = reactive({
  4. total_price: 0, // 勾选商品的总价格
  5. discount_price: 0, // 本次下单的优惠抵扣价格
  6. discount_type: 0, // 0表示优惠券,1表示积分
  7. use_coupon: false, // 用户是否使用优惠
  8. coupon_list:[], // 用户拥有的可用优惠券列表
  9. select: -1, // 当前用户选中的优惠券下标,-1表示没有选择
  10. credit: 0, // 当前用户选择抵扣的积分,0表示没有使用积分
  11. fixed: true, // 底部订单总价是否固定浮动
  12. pay_type: 0, // 支付方式
  13. create_order(user_coupon_id, token){
  14. // 生成订单
  15. return http.post("/orders/",{
  16. pay_type: this.pay_type,
  17. user_coupon_id,
  18. },{
  19. headers:{
  20. Authorization: "jwt " + token,
  21. }
  22. })
  23. },
  24. get_enable_coupon_list(token){
  25. // 获取本次下单的可用优惠券列表
  26. return http.get("/coupon/enable/",{
  27. headers:{
  28. Authorization: "jwt " + token,
  29. }
  30. })
  31. }
  32. })
  33. export default order;

views/Order.vue,代码:

  1. <script setup>
  2. // 中间代码省略....
  3. const commit_order = ()=>{
  4. // 生成订单
  5. let token = sessionStorage.token || localStorage.token;
  6. // 当用户选择了优惠券,则需要获取当前选择的优惠券发放记录的id
  7. let user_coupon_id = -1;
  8. if(order.select !== -1){
  9. user_coupon_id = order.coupon_list[order.select].user_coupon_id;
  10. }
  11. order.create_order(user_coupon_id, token).then(response=>{
  12. console.log(response.data.order_number) // todo 订单号
  13. console.log(response.data.pay_link) // todo 支付链接
  14. // 成功提示
  15. ElMessage.success("下单成功!马上跳转到支付页面,请稍候~")
  16. // 扣除掉被下单的商品数量,更新购物车中的商品数量
  17. store.commit("set_cart_total", store.state.cart_total - cart.select_course_list.length);
  18. }).catch(error=>{
  19. if(error?.response?.status===400){
  20. ElMessage.success("登录超时!请重新登录后再继续操作~");
  21. }
  22. })
  23. }
  24. // 中间代码省略....
  25. </script>

服务端接收并验证优惠券发送记录ID再重新计算本次下单的实付价格

order/serializers.py,代码:

  1. from datetime import datetime
  2. from rest_framework import serializers
  3. from django_redis import get_redis_connection
  4. from django.db import transaction
  5. from .models import Order, OrderDetail, Course
  6. from coupon.models import CouponLog
  7. import logging
  8. logger = logging.getLogger("django")
  9. class OrderModelSerializer(serializers.ModelSerializer):
  10. pay_link = serializers.CharField(read_only=True)
  11. user_coupon_id = serializers.IntegerField(write_only=True, default=-1)
  12. class Meta:
  13. model = Order
  14. fields = ["pay_type", "id", "order_number", "pay_link", "user_coupon_id"]
  15. read_only_fields = ["id", "order_number"]
  16. extra_kwargs = {
  17. "pay_type": {"write_only": True},
  18. }
  19. def create(self, validated_data):
  20. """创建订单"""
  21. redis = get_redis_connection("cart")
  22. user_id = self.context["request"].user.id # 1
  23. # 判断用户如果使用了优惠券,则优惠券需要判断验证
  24. user_coupon_id = validated_data.get("user_coupon_id")
  25. # 本次下单时,用户使用的优惠券
  26. user_coupon = None
  27. if user_coupon_id != -1:
  28. user_coupon = CouponLog.objects.filter(pk=user_coupon_id, user_id=user_id).first()
  29. # 开启事务操作,保证下单过程中的所有数据库的原子性
  30. with transaction.atomic():
  31. # 设置事务的回滚点标记
  32. t1 = transaction.savepoint()
  33. try:
  34. # 创建订单记录
  35. order = Order.objects.create(
  36. name="购买课程", # 订单标题
  37. user_id=user_id, # 当前下单的用户ID
  38. # order_number = datetime.now().strftime("%Y%m%d%H%M%S") + ("%08d" % user_id) + "%08d" % random.randint(1,99999999) # 基于随机数生成唯一订单号
  39. order_number=datetime.now().strftime("%Y%m%d") + ("%08d" % user_id) + "%08d" % redis.incr("order_number"), # 基于redis生成分布式唯一订单号
  40. pay_type=validated_data.get("pay_type"), # 支付方式
  41. )
  42. # 记录本次下单的商品列表
  43. cart_hash = redis.hgetall(f"cart_{user_id}")
  44. if len(cart_hash) < 1:
  45. raise serializers.ValidationError(detail="购物车没有要下单的商品")
  46. # 提取购物车中所有勾选状态为b'1'的商品
  47. course_id_list = [int(key.decode()) for key, value in cart_hash.items() if value == b'1']
  48. # 添加订单与课程的关系
  49. course_list = Course.objects.filter(pk__in=course_id_list, is_deleted=False, is_show=True).all()
  50. detail_list = []
  51. total_price = 0 # 本次订单的总价格
  52. real_price = 0 # 本次订单的实付总价
  53. # 用户使用优惠券或积分以后,需要在服务端计算本次使用优惠券或积分的最大优惠额度
  54. total_discount_price = 0 # 总优惠价格
  55. max_discount_course = None # 享受最大优惠的课程
  56. for course in course_list:
  57. discount_price = course.discount.get("price", None) # 获取课程原价
  58. if discount_price is not None:
  59. discount_price = float(discount_price)
  60. discount_name = course.discount.get("type", "")
  61. detail_list.append(OrderDetail(
  62. order=order,
  63. course=course,
  64. name=course.name,
  65. price=course.price,
  66. real_price=course.price if discount_price is None else discount_price,
  67. discount_name=discount_name,
  68. ))
  69. # 统计订单的总价和实付总价
  70. total_price += float(course.price)
  71. real_price += float(course.price if discount_price is None else discount_price)
  72. # 在用户使用了优惠券,并且当前课程没有参与其他优惠活动时,找到最佳优惠课程
  73. if user_coupon and discount_price is None:
  74. if max_discount_course is None:
  75. max_discount_course = course
  76. else:
  77. if course.price >= max_discount_course.price:
  78. max_discount_course = course
  79. # 在用户使用了优惠券以后,根据循环中得到的最佳优惠课程进行计算最终抵扣金额
  80. if user_coupon:
  81. # 优惠公式
  82. sale = float(user_coupon.coupon.sale[1:])
  83. if user_coupon.coupon.discount == 1:
  84. """减免优惠券"""
  85. total_discount_price = sale
  86. elif user_coupon.coupon.discount == 2:
  87. """折扣优惠券"""
  88. total_discount_price = float(max_discount_course.price) * (1 - sale)
  89. # 一次性批量添加本次下单的商品记录
  90. OrderDetail.objects.bulk_create(detail_list)
  91. # 保存订单的总价格和实付价格
  92. order.total_price = real_price
  93. order.real_price = float(real_price - total_discount_price)
  94. order.save()
  95. # todo 支付链接地址[后面实现支付功能的时候,再做]
  96. order.pay_link = ""
  97. # 删除购物车中被勾选的商品,保留没有被勾选的商品信息
  98. cart = {key: value for key, value in cart_hash.items() if value == b'0'}
  99. pipe = redis.pipeline()
  100. pipe.multi()
  101. # 删除原来的购物车
  102. pipe.delete(f"cart_{user_id}")
  103. # 重新把未勾选的商品记录到购物车中
  104. if cart: # 判断如果是空购物,则不需要再次添加cart购物车数据了。
  105. pipe.hmset(f"cart_{user_id}", cart)
  106. pipe.execute()
  107. # 如果有使用了优惠券,则把优惠券和当前订单进行绑定
  108. if user_coupon:
  109. user_coupon.order = order
  110. user_coupon.save()
  111. # 把优惠券从redis中移除
  112. print(f"{user_id}:{user_coupon_id}")
  113. redis = get_redis_connection("coupon")
  114. redis.delete(f"{user_id}:{user_coupon_id}")
  115. return order
  116. except Exception as e:
  117. # 1. 记录日志
  118. logger.error(f"订单创建失败:{e}")
  119. # 2. 事务回滚
  120. transaction.savepoint_rollback(t1)
  121. # 3. 抛出异常,通知视图返回错误提示
  122. raise serializers.ValidationError(detail="订单创建失败!")

提交代码版本

  1. /home/moluo/Desktop/luffycity
  2. git add .
  3. git commit -m "feature: 服务端在用户选择优惠券以后重新计算订单实付价格"
  4. git push

积分

  1. 实现积分功能,必须具备以下条件:
  2. 1. 用户模型中必须有积分字段credit[积分不会过期]
  3. 2. 在服务端必须有一个常量配置,表示积分与现金的换算比例
  4. 3. 订单模型中新增一个积分字段, 用于记录积分的消费和积分折算的价格
  5. 4. 新增一个积分流水模型, 用于记录积分的收支记录
  6. operation 操作类型
  7. number 积分数量
  8. user 用户ID

我们之前在自定义用户模型的时候,已经声明了积分字段,所以此处为了方便后面开发积分功能的时候,能够在admin管理站点中进行积分的调整使用,所以我们此处在users/admin.py后台站点配置文件中,配置user用户模型的模型管理器。

先新增积分流水模型

users/models.py,代码:

  1. from django.db import models
  2. from django.contrib.auth.models import AbstractUser
  3. from stdimage import StdImageField
  4. from django.utils.safestring import mark_safe
  5. from models import BaseModel
  6. # Create your models here.
  7. class User(AbstractUser):
  8. mobile = models.CharField(max_length=15, unique=True, verbose_name='手机号')
  9. money = models.DecimalField(max_digits=9, default=0.0, decimal_places=2, verbose_name="钱包余额")
  10. credit = models.IntegerField(default=0, verbose_name="积分")
  11. # avatar = models.ImageField(upload_to="avatar/%Y", null=True, default="", verbose_name="个人头像")
  12. avatar = StdImageField(variations={
  13. 'thumb_400x400': (400, 400), # 'medium': (400, 400),
  14. 'thumb_50x50': (50, 50, True), # 'small': (50, 50, True),
  15. }, delete_orphans=True, upload_to="avatar/%Y", blank=True, null=True, verbose_name="个人头像")
  16. nickname = models.CharField(max_length=50, default="", null=True, verbose_name="用户昵称")
  17. class Meta:
  18. db_table = 'lf_users'
  19. verbose_name = '用户信息'
  20. verbose_name_plural = verbose_name
  21. def avatar_small(self):
  22. if self.avatar:
  23. return mark_safe( f'<img style="border-radius: 100%;" src="{self.avatar.thumb_50x50.url}">' )
  24. return ""
  25. avatar_small.short_description = "个人头像(50x50)"
  26. avatar_small.allow_tags = True
  27. avatar_small.admin_order_field = "avatar"
  28. def avatar_medium(self):
  29. if self.avatar:
  30. return mark_safe( f'<img style="border-radius: 100%;" src="{self.avatar.thumb_400x400.url}">' )
  31. return ""
  32. avatar_medium.short_description = "个人头像(400x400)"
  33. avatar_medium.allow_tags = True
  34. avatar_medium.admin_order_field = "avatar"
  35. class Credit(BaseModel):
  36. """积分流水"""
  37. opera_choices = (
  38. (0, "业务增值"),
  39. (1, "购物消费"),
  40. (2, "系统赠送"),
  41. )
  42. operation = models.SmallIntegerField(choices=opera_choices, default=1, verbose_name="积分操作类型")
  43. number = models.IntegerField(default=0, verbose_name="积分数量", help_text="如果是扣除积分则需要设置积分为负数,如果消费10积分,则填写-10,<br>如果是添加积分则需要设置积分为正数,如果获得10积分,则填写10。")
  44. user = models.ForeignKey(User, related_name='user_credits', on_delete=models.CASCADE, db_constraint=False, verbose_name="用户")
  45. remark = models.CharField(max_length=500, null=True, blank=True, verbose_name="备注信息")
  46. class Meta:
  47. db_table = 'ly_credit'
  48. verbose_name = '积分流水'
  49. verbose_name_plural = verbose_name
  50. def __str__(self):
  51. if self.number > 0:
  52. oper_text = "获得"
  53. else:
  54. oper_text = "减少"
  55. return "[%s] %s 用户%s %s %s积分" % (self.get_operation_display(),self.created_time.strftime("%Y-%m-%d %H:%M:%S"), self.user.username, oper_text, abs(self.number))

订单模型新增积分字段,orders/models.py,代码:

  1. class Order(BaseModel):
  2. """订单基本信息模型"""
  3. status_choices = (
  4. (0, '未支付'),
  5. (1, '已支付'),
  6. (2, '已取消'),
  7. (3, '超时取消'),
  8. )
  9. pay_choices = (
  10. (0, '支付宝'),
  11. (1, '微信'),
  12. (2, '余额'),
  13. )
  14. total_price = models.DecimalField(default=0, max_digits=10, decimal_places=2, verbose_name="订单总价")
  15. real_price = models.DecimalField(default=0, max_digits=10, decimal_places=2, verbose_name="实付金额")
  16. order_number = models.CharField(max_length=64, verbose_name="订单号")
  17. order_status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="订单状态")
  18. pay_type = models.SmallIntegerField(choices=pay_choices, default=1, verbose_name="支付方式")
  19. order_desc = models.TextField(null=True, blank=True, max_length=500, verbose_name="订单描述")
  20. pay_time = models.DateTimeField(null=True, blank=True, verbose_name="支付时间")
  21. user = models.ForeignKey(User, related_name='user_orders', on_delete=models.DO_NOTHING,verbose_name="下单用户")
  22. credit = models.IntegerField(default=0, null=True, blank=True, verbose_name="积分")
  23. class Meta:
  24. db_table = "fg_order"
  25. verbose_name = "订单记录"
  26. verbose_name_plural = verbose_name
  27. def __str__(self):
  28. return "%s,总价: %s,实付: %s" % (self.name, self.total_price, self.real_price)

数据迁移

  1. cd /home/moluo/Desktop/luffycity/luffycityapi
  2. python manage.py makemigrations
  3. python manage.py migrate

当管理员在admin运营后台中, 给用户新增积分时,需要自动生成对应的流水记录。

users/admin.py,代码:

  1. from django.contrib import admin
  2. from .models import User,Credit
  3. # Register your models here.
  4. class UserModelAdmin(admin.ModelAdmin):
  5. """用户的模型管理器"""
  6. list_display = ["id","username","avatar_image","money","credit"]
  7. list_editable = ["credit"]
  8. def save_model(self, request, obj, form, change):
  9. if change:
  10. """更新数据"""
  11. user = User.objects.get(pk=obj.id)
  12. has_credit = user.credit # 原来用户的积分数据
  13. new_credit = obj.credit # 更新后用户的积分数据
  14. Credit.objects.create(
  15. user=user,
  16. number=int(new_credit - has_credit),
  17. operation=2,
  18. )
  19. obj.save()
  20. if not change:
  21. """新增数据"""
  22. Credit.objects.create(
  23. user=obj.id,
  24. number=obj.credit,
  25. operation=2,
  26. )
  27. admin.site.register(User, UserModelAdmin)
  28. class CreditModelAdmin(admin.ModelAdmin):
  29. """积分流水的模型管理器"""
  30. list_display = ["id","user","number","__str__"]
  31. admin.site.register(Credit,CreditModelAdmin)

课程模型新增积分字段,courses/models.py,代码:

  1. class Course(BaseModel):
  2. # ....省略
  3. price = models.DecimalField(blank=True, null=True, max_digits=10, decimal_places=2, default=0, verbose_name="课程原价")
  4. credit= models.IntegerField(blank=True, null=True, default=0, verbose_name="积分")

数据迁移

  1. python manage.py makemigrations
  2. python manage.py migrate

接下来,在课程详情展示页面中新增显示当前课程可以抵扣的积分数量。courses/serializers.py,代码:

  1. class CourseRetrieveModelSerializer(serializers.ModelSerializer):
  2. """课程详情的序列化器"""
  3. diretion_name = serializers.CharField(source="diretion.name")
  4. # diretion = serializers.SlugRelatedField(read_only=True, slug_field='name')
  5. category_name = serializers.CharField(source="category.name")
  6. # 序列化器嵌套
  7. teacher = CourseTearchModelSerializer()
  8. class Meta:
  9. model = Course
  10. fields = [
  11. "name", "course_cover", "course_video", "level", "get_level_display",
  12. "description", "pub_date", "status", "get_status_display", "students","discount", "credit",
  13. "lessons", "pub_lessons", "price", "diretion", "diretion_name", "category", "category_name", "teacher","can_free_study"
  14. ]

因为课程模型新增了credit字段在elasticsearch搜索引擎中是没有对应的。所以我们需要在es索引模型文件新增credit字段,并在终端下手动重建索引。

apps/courses/search_indexes.py,代码:

  1. from haystack import indexes
  2. from .models import Course
  3. class CourseIndex(indexes.SearchIndex, indexes.Indexable):
  4. # 中间字段声明省略
  5. price = indexes.DecimalField(model_attr="price")
  6. credit = indexes.IntegerField(model_attr="credit") # 新增积分字段
  7. # 中间字段声明省略

重建es索引

  1. python manage.py rebuild_index

接下来,我们就可以直接在admin管理站点中对课程的抵扣积分进行设置了。

客户端中展示积分相关信息,views/Info.vue,代码:

  1. <p class="course-price" v-if="course.info.discount.price >= 0">
  2. <span>活动价</span>
  3. <span class="discount">¥{{parseFloat(course.info.discount.price).toFixed(2)}}</span>
  4. <span class="original">¥{{parseFloat(course.info.price).toFixed(2)}}</span>
  5. </p>
  6. <p class="course-price" v-if="course.info.credit>0">
  7. <span>抵扣积分</span>
  8. <span class="discount">{{course.info.credit}}</span>
  9. </p>

效果:

chapter7.2-优惠券与积分 - 图2

在购物车和确定订单页面中,服务端返回的购物车商品列表的数据以及勾选商品列表数据中增加返回credit积分字段。

cart/views.py,代码:

  1. from rest_framework.views import APIView
  2. from rest_framework.permissions import IsAuthenticated
  3. from rest_framework.response import Response
  4. from rest_framework import status
  5. from django_redis import get_redis_connection
  6. from courses.models import Course
  7. # Create your views here.
  8. class CartAPIView(APIView):
  9. permission_classes = [IsAuthenticated] # 保证用户必须时登录状态才能调用当前视图
  10. def post(self, request):
  11. """添加课程商品到购物车中"""
  12. # 1. 接受客户端提交的商品信息:用户ID,课程ID,勾选状态
  13. # 用户ID 可以通过self.request.user.id 或 request.user.id 来获取
  14. user_id = request.user.id
  15. course_id = request.data.get("course_id", None)
  16. selected = 1 # 默认商品是勾选状态的
  17. print(f"user_id={user_id},course_id={course_id}")
  18. try:
  19. # 判断课程是否存在
  20. # todo 同时,判断用户是否已经购买了
  21. course = Course.objects.get(is_show=True, is_deleted=False, pk=course_id)
  22. except:
  23. return Response({"errmsg": "当前课程不存在!"}, status=status.HTTP_400_BAD_REQUEST)
  24. # 3. 添加商品到购物车
  25. redis = get_redis_connection("cart")
  26. """
  27. cart_用户ID: {
  28. 课程ID: 勾选状态
  29. }
  30. """
  31. redis.hset(f"cart_{user_id}", course_id, selected)
  32. # 4. 获取购物车中的商品课程数量
  33. cart_total = redis.hlen(f"cart_{user_id}")
  34. # 5. 返回结果给客户端
  35. return Response({"errmsg": "成功添加商品课程到购物车!", "cart_total": cart_total}, status=status.HTTP_201_CREATED)
  36. def get(self,request):
  37. """获取购物车中的商品列表"""
  38. user_id = request.user.id
  39. redis = get_redis_connection("cart")
  40. cart_hash = redis.hgetall(f"cart_{user_id}")
  41. """
  42. cart_hash = {
  43. // b'商品课程ID': b'勾选状态',
  44. b'2': b'1',
  45. b'4': b'1',
  46. b'5': b'1'
  47. }
  48. """
  49. if len(cart_hash) < 1:
  50. return Response({"errmsg":"购物车没有任何商品。"})
  51. cart = [(int(key.decode()), bool(value.decode())) for key, value in cart_hash.items()]
  52. # cart = [ (2,True) (4,True) (5,True) ]
  53. course_id_list = [item[0] for item in cart]
  54. course_list = Course.objects.filter(pk__in=course_id_list, is_deleted=False, is_show=True).all()
  55. print(course_list)
  56. data = []
  57. for course in course_list:
  58. data.append({
  59. "id": course.id,
  60. "name": course.name,
  61. "course_cover": course.course_cover.url,
  62. "price": float(course.price),
  63. "credit": course.credit,
  64. "discount": course.discount,
  65. "course_type": course.get_course_type_display(),
  66. # 勾选状态:把课程ID转换成bytes类型,判断当前ID是否在购物车字典中作为key存在,如果存在,判断当前课程ID对应的值是否是字符串"1",是则返回True
  67. "selected": (str(course.id).encode() in cart_hash) and cart_hash[ str(course.id).encode()].decode() == "1"
  68. })
  69. return Response({"errmsg": "ok!", "cart": data})
  70. def patch(self, request):
  71. """切换购物车中商品勾选状态"""
  72. # 谁的购物车?user_id
  73. user_id = request.user.id
  74. # 获取购物车的课程ID与勾选状态
  75. course_id = int(request.data.get("course_id", 0))
  76. selected = int(bool(request.data.get("selected", True)))
  77. redis = get_redis_connection("cart")
  78. try:
  79. Course.objects.get(pk=course_id, is_show=True, is_deleted=False)
  80. except Course.DoesNotExist:
  81. redis.hdel(f"cart_{user_id}", course_id)
  82. return Response({"errmsg": "当前商品不存在或已经被下架!!"})
  83. redis.hset(f"cart_{user_id}", course_id, selected)
  84. return Response({"errmsg": "ok"})
  85. def put(self,request):
  86. """"全选 / 全不选"""
  87. user_id = request.user.id
  88. selected = int(bool(request.data.get("selected", True)))
  89. redis = get_redis_connection("cart")
  90. # 获取购物车中所有商品课程信息
  91. cart_hash = redis.hgetall(f"cart_{user_id}")
  92. """
  93. cart_hash = {
  94. # b'商品课程ID': b'勾选状态',
  95. b'2': b'1',
  96. b'4': b'1',
  97. b'5': b'1'
  98. }
  99. """
  100. if len(cart_hash) < 1:
  101. return Response({"errmsg": "购物车没有任何商品。"}, status=status.HTTP_204_NO_CONTENT)
  102. # 把redis中的购物车课程ID信息转换成普通列表
  103. cart_list = [int(course_id.decode()) for course_id in cart_hash]
  104. # 批量修改购物车中素有商品课程的勾选状态
  105. pipe = redis.pipeline()
  106. pipe.multi()
  107. for course_id in cart_list:
  108. pipe.hset(f"cart_{user_id}", course_id, selected)
  109. pipe.execute()
  110. return Response({"errmsg": "ok"})
  111. def delete(self, request):
  112. """从购物车中删除指定商品"""
  113. user_id = request.user.id
  114. # 因为delete方法没有请求体,所以改成地址栏传递课程ID,Django restframework中通过request.query_params来获取
  115. course_id = int(request.query_params.get("course_id", 0))
  116. redis = get_redis_connection("cart")
  117. redis.hdel(f"cart_{user_id}", course_id)
  118. return Response(status=status.HTTP_204_NO_CONTENT)
  119. class CartOrderAPIView(APIView):
  120. """购物车确认下单接口"""
  121. # 保证用户必须是登录状态才能调用当前视图
  122. permission_classes = [IsAuthenticated]
  123. def get(self,request):
  124. """获取勾选商品列表"""
  125. # 查询购物车中的商品课程ID列表
  126. user_id = request.user.id
  127. redis = get_redis_connection("cart")
  128. cart_hash = redis.hgetall(f"cart_{user_id}")
  129. """
  130. cart_hash = {
  131. # b'商品课程ID': b'勾选状态',
  132. b'2': b'1',
  133. b'4': b'1',
  134. b'5': b'1'
  135. }
  136. """
  137. if len(cart_hash) < 1:
  138. return Response({"errmsg": "购物车没有任何商品。"}, status=status.HTTP_204_NO_CONTENT)
  139. # 把redis中的购物车勾选课程ID信息转换成普通列表
  140. cart_list = [int(course_id.decode()) for course_id, selected in cart_hash.items() if selected == b'1']
  141. course_list = Course.objects.filter(pk__in=cart_list, is_deleted=False, is_show=True).all()
  142. # 把course_list进行遍历,提取课程中的信息组成列表
  143. data = []
  144. for course in course_list:
  145. data.append({
  146. "id": course.id,
  147. "name": course.name,
  148. "course_cover": course.course_cover.url,
  149. "price": float(course.price),
  150. "credit": course.credit,
  151. "discount": course.discount,
  152. "course_type": course.get_course_type_display(),
  153. })
  154. # 返回客户端
  155. return Response({"errmsg": "ok!", "cart": data})

客户端购物车与确认订单页面中的商品列表展示当前可以使用的积分数量.

views/Cart.vue,和 views/Order.vue,代码:

  1. <div class="item-2">
  2. <router-link :to="`/project/${course_info.id}`" class="img-box l">
  3. <img :src="course_info.course_cover">
  4. </router-link>
  5. <dl class="l has-package">
  6. <dt>【{{course_info.course_type}}】 {{course_info.name}}</dt>
  7. <p class="package-item" v-if="course_info.discount.type">{{ course_info.discount.type }}</p>
  8. <p class="package-item" v-if="course_info.credit>0">{{course_info.credit}}积分抵扣</p>
  9. </dl>
  10. </div>

客户端返回积分抵扣现金的数据。

utils/constants.py,代码:

  1. # 积分抵扣现金的比例,n积分:1元
  2. CREDIT_TO_MONEY = 10

coupon/views.py,代码:

  1. import constants
  2. from rest_framework.views import APIView
  3. from rest_framework.permissions import IsAuthenticated
  4. from rest_framework.response import Response
  5. from .services import get_user_coupon_list, get_user_enable_coupon_list
  6. class CouponListAPIView(APIView):
  7. permission_classes = [IsAuthenticated]
  8. def get(self, request):
  9. """获取用户拥有的所有优惠券"""
  10. user_id = request.user.id
  11. coupon_data = get_user_coupon_list(user_id)
  12. return Response(coupon_data)
  13. class EnableCouponListAPIView(APIView):
  14. permission_classes = [IsAuthenticated]
  15. def get(self, request):
  16. """获取用户本次拥有的本次下单可用所有优惠券"""
  17. user_id = request.user.id
  18. coupon_data = get_user_enable_coupon_list(user_id)
  19. return Response({
  20. "errmsg":"ok",
  21. 'has_credit': request.user.credit,
  22. 'credit_to_money': constants.CREDIT_TO_MONEY,
  23. "coupon_list": coupon_data
  24. })

客户端获取当前用户本地下单时可用优惠券列表并获取当前用户拥有的积分。

api/order.js,新增属性,credit_to_moneyhas_credit,代码:

  1. import http from "../utils/http";
  2. import {reactive} from "vue";
  3. const order = reactive({
  4. total_price: 0, // 勾选商品的总价格
  5. discount_price: 0, // 本次下单的优惠抵扣价格
  6. discount_type: 0, // 0表示优惠券,1表示积分
  7. use_coupon: false, // 用户是否使用优惠
  8. coupon_list:[], // 用户拥有的可用优惠券列表
  9. select: -1, // 当前用户选中的优惠券下标,-1表示没有选择
  10. credit: 0, // 当前用户选择抵扣的积分,0表示没有使用积分
  11. fixed: true, // 底部订单总价是否固定浮动
  12. pay_type: 0, // 支付方式
  13. credit_to_money: 0, // 积分兑换现金的比例
  14. has_credit: 0, // 用户拥有的积分
  15. create_order(user_coupon_id, token){
  16. // 生成订单
  17. return http.post("/orders/",{
  18. pay_type: this.pay_type,
  19. user_coupon_id,
  20. },{
  21. headers:{
  22. Authorization: "jwt " + token,
  23. }
  24. })
  25. },
  26. get_enable_coupon_list(token){
  27. // 获取本次下单的可用优惠券列表
  28. return http.get("/coupon/enable/",{
  29. headers:{
  30. Authorization: "jwt " + token,
  31. }
  32. })
  33. }
  34. })
  35. export default order;

views/Order.vue,代码:

  1. <script setup>
  2. // ... 代码省略
  3. // 获取本次下单的可用优惠券
  4. const get_enable_coupon_list = ()=>{
  5. let token = sessionStorage.token || localStorage.token;
  6. order.get_enable_coupon_list(token).then(response=>{
  7. order.coupon_list = response.data.coupon_list;
  8. // 获取积分相关信息
  9. order.credit_to_money = response.data.credit_to_money;
  10. order.has_credit = response.data.has_credit;
  11. })
  12. }
  13. get_enable_coupon_list()
  14. // ... 代码省略
  15. </script>

提交代码版本

  1. cd /home/moluo/Desktop/luffycity
  2. git add .
  3. git commit -m "feature: 积分功能实现-上"
  4. git push

在确认订单页面中,查询当前本次购买可使用积分抵扣的商品列表以及最大抵扣积分数量。

获取用户本次下单能使用的最大抵扣积分,需要考虑当前用户拥有的积分数量。

  1. 1. 当用户积分 > 本次下单可使用积分抵扣总数量:
  2. 用户最高可使用积分=本次下单的可使用积分数量
  3. 2. 当用户积分 < 本次购课可使用积分抵扣总数量:
  4. 用户最高可使用积分=用户拥有的所有积分

客户端切换不同的优惠类型时,重置积分和优惠券的选择信息,同时当用户选择了积分抵扣时,发送积分数量到服务端。

views/Order.vue,代码:

  1. <div class="coupon-content code" v-else>
  2. <div class="input-box">
  3. <el-input-number v-model="order.credit" :step="1" :min="0" :max="order.max_use_credit"></el-input-number>
  4. <a class="convert-btn" @click="conver_credit">兑换</a>
  5. <a class="convert-btn" @click="max_conver_credit">最大积分兑换</a>
  6. </div>
  7. <div class="converted-box">
  8. <p class="course-title" v-for="course in order.credit_course_list">
  9. 课程:<span class="c_name">{{course.name}}</span>
  10. <span class="discount-cash">{{course.credit}}积分抵扣:<em>{{ (course.credit/order.credit_to_money).toFixed(2) }}</em></span>
  11. </p>
  12. </div>
  13. <p class="error-msg">本次订单最多可以使用{{order.max_use_credit}}积分,您当前拥有{{order.has_credit}}积分。({{order.credit_to_money}}积分=1元)</p>
  14. <p class="tip">说明:每笔订单只能使用一次积分,并只有在部分允许使用积分兑换的课程中才能使用。</p>
  15. </div>
  1. <script setup>
  2. import {reactive,watch} from "vue"
  3. import Header from "../components/Header.vue"
  4. import Footer from "../components/Footer.vue"
  5. import {useStore} from "vuex";
  6. import cart from "../api/cart"
  7. import order from "../api/order";
  8. import {ElMessage} from "element-plus";
  9. import router from "../router";
  10. // let store = useStore()
  11. const get_select_course = ()=>{
  12. // 获取购物车中的勾选商品列表
  13. let token = sessionStorage.token || localStorage.token;
  14. cart.get_select_course(token).then(response=>{
  15. cart.select_course_list = response.data.cart
  16. if(response.data.cart.length === 0){
  17. ElMessage.error("当前购物车中没有下单的商品!请重新重新选择购物车中要购买的商品~");
  18. router.back();
  19. }
  20. // 计算本次下单的总价格
  21. let sum = 0
  22. let credit_course_list= [] // 可使用积分抵扣的课程列表
  23. let max_use_credit = 0 // 本次下单最多可以用于抵扣的积分
  24. response.data.cart?.forEach((course,key)=>{
  25. if(course.discount.price > 0 || course.discount.price === 0){
  26. sum+=course.discount.price
  27. }else{
  28. sum+=course.price
  29. }
  30. if(course.credit > 0){
  31. max_use_credit = max_use_credit + course.credit
  32. credit_course_list.push(course)
  33. }
  34. })
  35. cart.total_price = sum;
  36. order.credit_course_list = credit_course_list
  37. order.max_use_credit = max_use_credit // 本次下单最多可以用于抵扣的积分
  38. console.log(`order.max_use_credit=${order.max_use_credit}`);
  39. // 本次订单最多可以使用的积分数量
  40. // 如果用户积分不足,则最多只能用完自己的积分
  41. if(order.max_use_credit > order.has_credit){
  42. order.max_use_credit = order.has_credit
  43. }
  44. }).catch(error=>{
  45. if(error?.response?.status===400){
  46. ElMessage.error("登录超时!请重新登录后再继续操作~");
  47. }
  48. })
  49. }
  50. get_select_course();
  51. const commit_order = ()=>{
  52. // 生成订单
  53. let token = sessionStorage.token || localStorage.token;
  54. // 当用户选择了优惠券,则需要获取当前选择的优惠券发放记录的id
  55. let user_coupon_id = -1;
  56. if(order.select !== -1){
  57. user_coupon_id = order.coupon_list[order.select].user_coupon_id;
  58. }
  59. order.create_order(user_coupon_id, token).then(response=>{
  60. console.log(response.data.order_number) // todo 订单号
  61. console.log(response.data.pay_link) // todo 支付链接
  62. // 成功提示
  63. ElMessage.success("下单成功!马上跳转到支付页面,请稍候~")
  64. // 扣除掉被下单的商品数量,更新购物车中的商品数量
  65. store.commit("set_cart_total", store.state.cart_total - cart.select_course_list.length);
  66. }).catch(error=>{
  67. if(error?.response?.status===400){
  68. ElMessage.success("登录超时!请重新登录后再继续操作~");
  69. }
  70. })
  71. }
  72. // 获取本次下单的可用优惠券
  73. const get_enable_coupon_list = ()=>{
  74. let token = sessionStorage.token || localStorage.token;
  75. order.get_enable_coupon_list(token).then(response=>{
  76. order.coupon_list = response.data.coupon_list;
  77. // 获取积分相关信息
  78. order.credit_to_money = response.data.credit_to_money;
  79. order.has_credit = response.data.has_credit;
  80. })
  81. }
  82. get_enable_coupon_list()
  83. // 积分兑换抵扣
  84. const conver_credit = ()=>{
  85. order.discount_price = parseFloat( (order.credit / order.credit_to_money).toFixed(2) )
  86. }
  87. // 本次下单的最大兑换积分
  88. const max_conver_credit = ()=>{
  89. order.credit=order.max_use_credit
  90. conver_credit();
  91. }
  92. // 监听用户选择的支付方式
  93. watch(
  94. ()=>order.pay_type,
  95. ()=>{
  96. console.log(order.pay_type)
  97. }
  98. )
  99. // 监听用户选择的优惠券
  100. watch(
  101. ()=>order.select,
  102. ()=>{
  103. order.discount_price = 0;
  104. // 如果没有选择任何的优惠券,则select 为-1,那么不用进行计算优惠券折扣的价格了
  105. if (order.select === -1) {
  106. return // 阻止代码继续往下执行
  107. }
  108. // 根据下标select,获取当前选中的优惠券信息
  109. let current_coupon = order.coupon_list[order.select]
  110. console.log(current_coupon);
  111. // 针对折扣优惠券,找到最大优惠的课程
  112. let max_discount = -1;
  113. for(let course of cart.select_course_list) { // 循环本次下单的勾选商品
  114. // 找到当前优惠券的可用课程
  115. if(current_coupon.enable_course === "__all__") { // 如果当前优惠券是通用优惠券
  116. if(max_discount !== -1){
  117. if(course.price > max_discount.price){ // 在每次循环中,那当前循环的课程的价格与之前循环中得到的最大优惠课程的价格进行比较
  118. max_discount = course
  119. }
  120. }else{
  121. max_discount = course
  122. }
  123. }else if((current_coupon.enable_course.indexOf(course.id) > -1) && (course.price >= parseFloat(current_coupon.condition))){
  124. // 判断 当前优惠券如果包含了当前课程, 并 课程的价格 > 当前优惠券的使用门槛
  125. // 只允许没有参与其他优惠券活动的课程使用优惠券,基本所有的平台都不存在折上折的。
  126. if( course.discount.price === undefined ) {
  127. if(max_discount !== -1){
  128. if(course.price > max_discount.price){
  129. max_discount = course
  130. }
  131. }else{
  132. max_discount = course
  133. }
  134. }
  135. }
  136. }
  137. if(max_discount !== -1){
  138. if(current_coupon.discount === '1') { // 抵扣优惠券[抵扣的价格就是当前优惠券的价格]
  139. order.discount_price = parseFloat( Math.abs(current_coupon.sale) )
  140. }else if(current_coupon.discount === '2') { // 折扣优惠券]抵扣的价格就是(1-折扣百分比) * 课程原价]
  141. order.discount_price = parseFloat(max_discount.price * (1-parseFloat(current_coupon.sale.replace("*",""))) )
  142. }
  143. }else{
  144. order.select = -1
  145. order.discount_price = 0
  146. ElMessage.error("当前课程商品已经参与了其他优惠活动,无法再次使用当前优惠券!")
  147. }
  148. })
  149. // 在切换不同的优惠类型,重置积分和优惠券信息
  150. watch(
  151. ()=>order.discount_type,
  152. ()=>{
  153. order.select = -1
  154. order.credit = 0
  155. order.discount_price = 0
  156. }
  157. )
  158. // 底部订单总价信息固定浮动效果
  159. window.onscroll = ()=>{
  160. let cart_body_table = document.querySelector(".cart-body-table")
  161. let offsetY = window.scrollY
  162. let maxY = cart_body_table.offsetTop+cart_body_table.offsetHeight
  163. order.fixed = offsetY < maxY
  164. }
  165. </script>

src/api/order.js,代码:

import http from "../utils/http";
import {reactive} from "vue";

const order = reactive({
  total_price: 0,      // 勾选商品的总价格
  discount_price: 0,   // 本次下单的优惠抵扣价格
  discount_type: 0,    // 0表示优惠券,1表示积分
  use_coupon: false,   // 用户是否使用优惠
  coupon_list:[],      // 用户拥有的可用优惠券列表
  select: -1,          // 当前用户选中的优惠券下标,-1表示没有选择
  credit: 0,           // 当前用户选择抵扣的积分,0表示没有使用积分
  fixed: true,         // 底部订单总价是否固定浮动
  pay_type: 0,         // 支付方式
  credit_to_money: 0,  // 积分兑换现金的比例
  has_credit: 0,       // 用户拥有的积分
  max_use_credit: 0,   // 当前用户本次下单可用最大积分数量
  credit_course_list:[], // 可使用积分抵扣的课程列表
  create_order(user_coupon_id, token){
    // 生成订单
    return http.post("/orders/",{
        pay_type: this.pay_type,
        user_coupon_id,
        credit: this.credit,
    },{
        headers:{
            Authorization: "jwt " + token,
        }
    })
  },
  get_enable_coupon_list(token){
    // 获取本次下单的可用优惠券列表
    return http.get("/coupon/enable/",{
        headers:{
            Authorization: "jwt " + token,
        }
    })
  }
})

export default order;

提交代码版本

cd /home/moluo/Desktop/luffycity
git add .
git commit -m "feature: 积分功能实现-中"
git push

服务端在下单时 如果用户使用积分,则重新计算最终实付价格

序列化器,orders/serializers.py,代码:

import logging
import constants

from datetime import datetime
from rest_framework import serializers
from django_redis import get_redis_connection
from django.db import transaction
from .models import Order, OrderDetail, Course
from coupon.models import CouponLog

logger = logging.getLogger("django")


class OrderModelSerializer(serializers.ModelSerializer):
    pay_link = serializers.CharField(read_only=True)
    user_coupon_id = serializers.IntegerField(write_only=True, default=-1)

    class Meta:
        model = Order
        fields = ["pay_type", "id", "order_number", "pay_link", "user_coupon_id", "credit"]
        read_only_fields = ["id", "order_number"]
        extra_kwargs = {
            "pay_type": {"write_only": True},
        }

    def create(self, validated_data):
        """创建订单"""
        redis = get_redis_connection("cart")
        user = self.context["request"].user
        user_id = user.id

        # 判断用户如果使用了优惠券,则优惠券需要判断验证
        user_coupon_id = validated_data.get("user_coupon_id")
        # 本次下单时,用户使用的优惠券
        user_coupon = None
        if user_coupon_id != -1:
            user_coupon = CouponLog.objects.filter(pk=user_coupon_id, user_id=user_id).first()

        # 本次下单时使用的积分数量
        use_credit = validated_data.get("credit", 0)
        if use_credit > 0 and use_credit > user.credit:
            raise serializers.ValidationError(detail="您拥有的积分不足以抵扣本次下单的积分,请重新下单!")

        # 开启事务操作,保证下单过程中的所有数据库的原子性
        with transaction.atomic():
            # 设置事务的回滚点标记
            t1 = transaction.savepoint()
            try:
                # 创建订单记录
                order = Order.objects.create(
                    name="购买课程",  # 订单标题
                    user_id=user_id,  # 当前下单的用户ID
                    # order_number = datetime.now().strftime("%Y%m%d%H%M%S") + ("%08d" % user_id) + "%08d" % random.randint(1,99999999) # 基于随机数生成唯一订单号
                    order_number=datetime.now().strftime("%Y%m%d") + ("%08d" % user_id) + "%08d" % redis.incr("order_number"), # 基于redis生成分布式唯一订单号
                    pay_type=validated_data.get("pay_type"),  # 支付方式
                )

                # 记录本次下单的商品列表
                cart_hash = redis.hgetall(f"cart_{user_id}")
                if len(cart_hash) < 1:
                    raise serializers.ValidationError(detail="购物车没有要下单的商品")

                # 提取购物车中所有勾选状态为b'1'的商品
                course_id_list = [int(key.decode()) for key, value in cart_hash.items() if value == b'1']

                # 添加订单与课程的关系
                course_list = Course.objects.filter(pk__in=course_id_list, is_deleted=False, is_show=True).all()
                detail_list = []
                total_price = 0 # 本次订单的总价格
                real_price = 0  # 本次订单的实付总价

                # 用户使用优惠券或积分以后,需要在服务端计算本次使用优惠券或积分的最大优惠额度
                total_discount_price = 0    # 总优惠价格
                max_discount_course = None  # 享受最大优惠的课程

                # 本次下单最多可以抵扣的积分
                max_use_credit = 0

                for course in course_list:
                    discount_price = course.discount.get("price", None)  # 获取课程原价
                    if discount_price is not None:
                        discount_price = float(discount_price)
                    discount_name = course.discount.get("type", "")
                    detail_list.append(OrderDetail(
                        order=order,
                        course=course,
                        name=course.name,
                        price=course.price,
                        real_price=course.price if discount_price is None else discount_price,
                        discount_name=discount_name,
                    ))

                    # 统计订单的总价和实付总价
                    total_price += float(course.price)
                    real_price += float(course.price if discount_price is None else discount_price)

                    # 在用户使用了优惠券,并且当前课程没有参与其他优惠活动时,找到最佳优惠课程
                    if user_coupon and discount_price is None:
                        if max_discount_course is None:
                            max_discount_course = course
                        else:
                            if course.price >= max_discount_course.price:
                                max_discount_course = course

                    # 添加每个课程的可用积分
                    if use_credit > 0 and course.credit > 0:
                        max_use_credit += course.credit

                # 在用户使用了优惠券以后,根据循环中得到的最佳优惠课程进行计算最终抵扣金额
                if user_coupon:
                    # 优惠公式
                    sale = float(user_coupon.coupon.sale[1:])
                    if user_coupon.coupon.discount == 1:
                        """减免优惠券"""
                        total_discount_price = sale
                    elif user_coupon.coupon.discount == 2:
                        """折扣优惠券"""
                        total_discount_price = float(max_discount_course.price) * (1 - sale)

                if use_credit > 0:
                    if max_use_credit < use_credit:
                        raise serializers.ValidationError(detail="本次使用的抵扣积分数额超过了限制!")

                    # 当前订单添加积分抵扣的数量
                    order.credit = use_credit
                    total_discount_price = float(use_credit / constants.CREDIT_TO_MONEY)

                    # todo 扣除用户拥有的积分,后续在订单超时未支付,则返还订单中对应数量的积分给用户。如果订单成功支付,则添加一个积分流水记录。
                    user.credit = user.credit - use_credit
                    user.save()

                # 一次性批量添加本次下单的商品记录
                OrderDetail.objects.bulk_create(detail_list)

                # 保存订单的总价格和实付价格
                order.total_price = real_price
                order.real_price =  float(real_price - total_discount_price)
                order.save()

                # 删除购物车中被勾选的商品,保留没有被勾选的商品信息
                cart = {key: value for key, value in cart_hash.items() if value == b'0'}
                pipe = redis.pipeline()
                pipe.multi()
                # 删除原来的购物车
                pipe.delete(f"cart_{user_id}")
                # 重新把未勾选的商品记录到购物车中
                if cart:
                    pipe.hmset(f"cart_{user_id}", cart)
                pipe.execute()

                # 如果有使用了优惠券,则把优惠券和当前订单进行绑定
                if user_coupon:
                    user_coupon.order = order
                    user_coupon.save()
                    # 把优惠券从redis中移除
                    redis = get_redis_connection("coupon")
                    redis.delete(f"{user_id}:{user_coupon_id}")

                # todo 支付链接地址[后面实现支付功能的时候,再做]
                order.pay_link = ""

                return order
            except Exception as e:
                # 1. 记录日志
                logger.error(f"订单创建失败:{e}")
                # 2. 事务回滚
                transaction.savepoint_rollback(t1)
                # 3. 抛出异常,通知视图返回错误提示
                raise serializers.ValidationError(detail="订单创建失败!")
关于积分扣除和优惠券的使用问题!
我们下单的时候就要扣除积分或者记录优惠券和订单的关系,在用户如果取消订单或者订单超时以后,我们则返还扣除的积分或清除优惠券使用记录的订单号,如果结算支付成功,则记录积分的流水或者优惠券使用记录的状态。

提交代码版本

cd /home/moluo/Desktop/luffycity
git add .
git commit -m "feature: 积分功能实现-中"
git push