DC's blog DC's blog
首页
  • 计算机基础
  • linux基础
  • mysql
  • git
  • 数据结构与算法
  • axure
  • english
  • docker
  • opp
  • oop
  • 网络并发编程
  • 不基础的py基础
  • 设计模式
  • html
  • css
  • javascript
  • jquery
  • UI
  • 第一次学vue
  • 第二次学vue
  • Django
  • drf
  • drf_re
  • 温故知新
  • flask
  • 前后端不分离

    • BBS
    • 订单系统
    • CRM
  • 前后端部分分离

    • pear-admin-flask
    • pear-admin-django
  • 前后端分离

    • 供应链系统
  • 理论基础
  • py数据分析包
  • 机器学习
  • 深度学习
  • 华中科大的网课
  • cursor
  • deepseek
  • 杂文
  • 罗老师语录
  • 关于我

    • me
  • 分类
  • 归档
GitHub (opens new window)

DC

愿我一生欢喜,不为世俗所及.
首页
  • 计算机基础
  • linux基础
  • mysql
  • git
  • 数据结构与算法
  • axure
  • english
  • docker
  • opp
  • oop
  • 网络并发编程
  • 不基础的py基础
  • 设计模式
  • html
  • css
  • javascript
  • jquery
  • UI
  • 第一次学vue
  • 第二次学vue
  • Django
  • drf
  • drf_re
  • 温故知新
  • flask
  • 前后端不分离

    • BBS
    • 订单系统
    • CRM
  • 前后端部分分离

    • pear-admin-flask
    • pear-admin-django
  • 前后端分离

    • 供应链系统
  • 理论基础
  • py数据分析包
  • 机器学习
  • 深度学习
  • 华中科大的网课
  • cursor
  • deepseek
  • 杂文
  • 罗老师语录
  • 关于我

    • me
  • 分类
  • 归档
GitHub (opens new window)
  • BBS

  • 订单平台

    • 单点知识
    • 表结构
    • 用户名登陆
    • 短信登陆
    • 菜单和权限
    • 级别管理
    • 客户管理
      • 关联数据的处理
      • 客户列表
      • 添加客户
        • 界面展示
        • 验证保存
      • 编辑客户
      • 重置密码
      • 删除客户
        • 弹出对话框
        • 业务逻辑
        • request.is_ajax()
    • 分页和搜索
    • 价格策略
    • 交易中心
    • message组件
    • 我的交易列表
    • worker
    • 部署之代码同步
    • 部署之线上运行
    • 张sir的部署
  • CRM

  • flask+layui

  • django+layui

  • 供应链

  • 实战
  • 订单平台
DC
2023-06-09
目录

客户管理

# 关联数据的处理

客户表Customer有个ForeignKey的外键关联字段level..

注意, 客户是可以被删除的! 客户的active默认为1, 逻辑删除后, 设置为0..
Ps: 有些业务逻辑会逻辑删除到回车站, 然后再确认是否进一步的进行物理删除..

思考题: 级别逻辑删除后, 与之关联的客户如何处理?!!

若此处的级别是物理删除,还好办,删除级别后,可使该级别关联的客户级联删除.
但订单系统中的级别删除是逻辑删除,仅仅是将该级别/该记录的active字段至为0.那此时该级别下关联的客户如何处理?
▲ 解决思路一:修改级别删除的逻辑
     在前面的级别关联中,针对级别的删除,只执行了该语句 `models.Level.objects.filter(id=pk).update(active=0)`
     我们不直接逻辑删除,应该先查询是否有关联数据,若有关联数据,则不允许被删除!
     exists = models.Customer.objects.filter(level_id=pk).exists()
     if not exists:
         models.Level.objects.filter(id=pk).update(active=0)
▲ 解决思路二:级别被逻辑删除了,给与之关联的客户记录的ForeignKey外键关联字段level.设置默认值. 例如:NULL
     注意!若设置默认值为null,那么Customer表的level字段得允许为空!! null=True,blank=True
     models.Customer.objects.filter(level_id=pk).update(level=None)
     models.Level.objects.filter(id=pk).update(active=0)
▲ 解决思路三:不做任何处理.
     在前面的级别关联中,针对级别的删除,依旧只执行该语句 `models.Level.objects.filter(id=pk).update(active=0)`
     但在后续的客户查询,就得这么写:
        queryset = models.Customer.objects.filter(active=1, level__active=1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

订单系统中, 我们选择的是 方案一的解决思路.


# 客户列表

客户列表 跟 级别列表的 前端html展示页面的代码差不多!!

def customer_list(request):
    # 1.获取所有的客户数据
    queryset = models.Customer.objects.filter(active=1).select_related('level', 'creator')
    context = {
        "queryset": queryset,
    }
    return render(request, 'customer_list.html', context)
1
2
3
4
5
6
7

image-20230405174207155

1> 想要解决 用户列表中 "级别"和"创建者" 显示的是对象的问题. - 主动跨表!
2> 想要自定义 用户列表中 "创建时间" 的格式

[级别 和 创建者]
方案一: 在对象的model表中,加上__str__!!
方案二: 在customer_list.html中进行跨表查询.  '★ 一定要主动跨表,能大大提高效率 '
    首先要明白, `queryset = models.Customer.objects.filter(active=1)`该行代码实现的是对Customer表的单表查询!!
    而在Django中模版中进行的跨表,会再次查询一次数据库!!
    比如有300条客户数据,那么{{ row.level.title }} {{ row.level.percent }} 就会额外查询600次数据库.
    {% for row in queryset %}
        <td>{{ row.level.title }} ({{ row.level.percent }}%)</td>
        <td>{{ row.create_date }}</td>
        <td>{{ row.creator.username }}</td>
    {% endfor %}
    ★ So!确认要在模版中做跨表查询!!记得在orm的查询语句里加上select_related("跨表字段1","跨表字段2")
      下方的该语句,会进行一次联表查询!! 这样在模版中的跨表就不会做额外的查询啦!! 
      queryset = models.Customer.objects.filter(active=1).select_related('level', 'creator')
      
[创建时间]
    在表模型中,表明创建时间的create_date字段是用的DateTimeField类型,在页面上展示时,默认就是 xx年xx月xx日 xx:xx
    可以使用模版语言中的过滤器自定义在页面上显示的时间的格式
    <td>{{ row.create_date|date:"Y-m-d H:i:s" }}</td>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 添加客户

# 界面展示

image-20230407102148475

1.编写CustomerModelForm类使得添加客户页面自动生成字段.
    1> 添加客户页面只用展示表中的 用户名、密码、手机号、级别 4个字段; 
       余额不用,因为新创建的用户余额默认为0. 创建日期不用管,会自动创建.
       至于creator,应该将当前登陆的用户在后端自动添加进去!!
    2> 针对密码, 页面上不仅要展示表中的字段password; 我们还得在CustomerModelForm类中手动添加一个重复密码.
       confirm_password = forms.CharField(
           label="重复密码",
           widget=forms.PasswordInput
       )
2.CustomerModelForm类继承的BootStrapForm使得页面变得好看!!
3.在添加客户页面展示的level级别字段是一个下拉选择框,其值是 Level object(1)、Level object(2)..
  解决办法1:
      在Customer模型里添加 __str__ 方法!
4.使用ModelForm展示的字段,会将所有与之关联的数据都展示出来. -- ModelForm显示关联数据.
  So在添加客户页面展示的level级别字段还有一个问题,它将级别表中已经逻辑删除的也展示出来啦!!
  你想定制它的显示的话,有两种方式.
  - 解决办法1: 给模型Customer表里的level字段,添加一个limit_choices_to的属性.
            在这里,它会去读取Level这张表里active等于1的记录.
            ★ So,基于modelform做添加时,若某些关联数据需要通过筛选进行展示.即可通过指定limit_choices_to进行展示!
            ▲ 注意,该条件是写死的,若需要用的参数跟当前的登陆用户/当前登陆用户传入的参数等有关.使用limit_choices_to是做不了的!!
      level = models.ForeignKey(
          verbose_name="级别", 
          to="Level", 
          on_delete=models.CASCADE, 
          limit_choices_to={"active": 1}  # {"active": 1, "id__gt": 2} 条件可写多个.
      )
  - 解决办法2: 在CustomerModelForm类里重写__init__方法!
    def __init__(self, request, *args, **kwargs):
        # request参数是在 CustomerModelForm类实例化的时候传入的!! 视图函数里,form = CustomerModelForm(request)
        super().__init__(*args, **kwargs)
        self.fields['level'].queryset = models.Level.objects.filter(active=1)
5.在添加客户页面展示的level级别字段默认是一个下拉选择框,想要其变成redio单选框.
  那么,就得进一步定制 level 字段的插件.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

关键代码

"""
bootstrap.py
"""
class BootStrapForm:
    exclude_filed_list = []  # 指定字段不添加form-control的样式.

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        for name, field in self.fields.items():
            if name in self.exclude_filed_list:
                continue
            field.widget.attrs['class'] = "form-control"
            field.widget.attrs['placeholder'] = "请输入{}".format(field.label)
            if name == "title":
                field.widget.attrs['style'] = "width:10%;"
            else:
                field.widget.attrs['style'] = "width:25%;"

                
  
"""
customer.py
"""
from django.shortcuts import render
from web import models
from django import forms
from utils.bootstrap import BootStrapForm


class CustomerModelForm(BootStrapForm, forms.ModelForm):
    exclude_filed_list = ["level"]
    confirm_password = forms.CharField(
        label="重复密码",
         # 若提交返回错误信息,提交刷新页面,render_value=True使得密码保留原来的值.
        widget=forms.PasswordInput(render_value=True), 
    )

    class Meta:
        model = models.Customer
        fields = ["username", "mobile", "password", "confirm_password", "level"]
        widgets = {
            "password": forms.PasswordInput(render_value=True),  # 也可通过重写password字段来实现
            "level": forms.RadioSelect(attrs={'class': 'form-radio'}),  # 自定义form-radio样式
        }

    def __init__(self, request, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['level'].queryset = models.Level.objects.filter(active=1)
        

def customer_add(request):
    if request.method == "GET":
        form = CustomerModelForm(request)
        return render(request, "form.html", {"form": form})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

# 验证保存

image-20230407132423731

1.手机号格式的校验.
  方式1> 在CustomerModelForm类里自定义mobile字段,添加覆盖默认的mobile字段.
     mobile = forms.CharField(
         label="手机号",
         validators=[RegexValidator(r'^\d{11}$', '手机号格式错误.'), ]
     )
  方式2> 在CustomerModelForm类里定义mobile字段的钩子方法.
     def clean_mobile(self):
        mobile = self.cleaned_data['mobile']
        import re
        from django.core.exceptions import ValidationError
        if not re.match(r'^\d{11}$', mobile):
            raise ValidationError("手机格式错误.")
  方式3> 若使用的是ModelForm,还可以在Customer这个model模型"数据库"的mobile字段中添加validators参数
      mobile = models.CharField(
          verbose_name="手机号", 
          max_length=11, 
          db_index=True,
          validators=[RegexValidator(r'^\d{11}$', '手机号格式错误.'),]
      )
2.新增客户的密码需要经过md5加密后再存储到数据库中.因为登陆的验证用到了md5加密. (我们采用的是方式1)
  方式1> 写password字段的钩子函数,通过钩子函数的返回值控制
      def clean_password(self):
          password = self.cleaned_data['password']
          return md5(password)
  方式2> 在存入数据库之前稍微修改下存入的值
      关键代码如下:
      password = form.cleaned_data["password"]
      form.instance.password = md5(password)
      form.instance.creator_id = request.nb_user.id
      form.save()
3.两次密码需填写一致的问题 通过写confirm_password字段的钩子函数解决.
  def clean_confirm_password(self):
      # 因为class Meta的fields列表中,confirm_password在password字段后面
      # So,若password字段校验成功,就能在执行confirm_password的钩子函数时拿到
      # 注意:此处的password的值是通过md5加密过的.
      password = self.cleaned_data.get("password")
      confirm_password = md5(self.cleaned_data.get("confirm_password",""))
      if password != confirm_password:
          raise ValidationError("两次密码输入不一致.")
      return confirm_password
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

# 编辑客户

编辑客户的页面相对于添加客户的页面, 少了密码相关的内容.

image-20230407135754319

class CustomerEditModelForm(BootStrapForm, forms.ModelForm):
    exclude_filed_list = ["level"]

    class Meta:
        model = models.Customer
        fields = ["username", "mobile", "level"]
        widgets = {
            "level": forms.RadioSelect(attrs={'class': 'form-radio'}),  # 自定义form-radio样式
        }

    def __init__(self, request, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['level'].queryset = models.Level.objects.filter(active=1)
        
        
        
def customer_edit(request, pk):
    instance = models.Customer.objects.filter(pk=pk, active=1).first()
    if request.method == "GET":
        form = CustomerEditModelForm(request, instance=instance)
        return render(request, "form.html", {'form': form})

    form = CustomerEditModelForm(request, data=request.POST, instance=instance)
    if not form.is_valid():
        return render(request, "form.html", {'form': form})

    # 不用再添加数据,因为instance里有数据,此次只是更新.
    form.save()
    return redirect(reverse("customer_list"))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 重置密码

在 客户列表的页面 添加一个重置密码的功能. 点击, 跳转重置密码的页面.

image-20230407143459918

注: 在重置密码的页面, 用户名字段是不能编辑的. - 用 disabled 和 initial 来实现的!!

image-20230407143655047

class CustomerResetModelForm(BootStrapForm, forms.ModelForm):
    confirm_password = forms.CharField(
        label="重复密码",
        widget=forms.PasswordInput(render_value=True),  # 定义插件的方式一
    )

    class Meta:
        model = models.Customer
        fields = ["username", "password", "confirm_password"]
        widgets = {  # 定义插件的方式二
            "username": forms.TextInput(attrs={"disabled": True}),
            "password": forms.PasswordInput(render_value=True),
        }

    def clean_password(self):
        password = self.cleaned_data['password']
        return md5(password)

    def clean_confirm_password(self):
        password = self.cleaned_data.get("password")
        confirm_password = md5(self.cleaned_data.get("confirm_password", ""))
        if password != confirm_password:
            raise ValidationError("两次密码输入不一致.")
        return confirm_password

      
def customer_reset(request, pk):
    """ 重置客户密码 """
    instance = models.Customer.objects.filter(pk=pk, active=1).first()
    if request.method == "GET":
        form = CustomerResetModelForm(initial={'username': instance.username})
        return render(request, "form.html", {"form": form})
    form = CustomerResetModelForm(data=request.POST, instance=instance)
    if not form.is_valid():
        return render(request, "form.html", {'form': form})
    form.save()
    return redirect(reverse("customer_list"))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

# 删除客户

点击删除按钮, 弹出一个模态框.

# 弹出对话框

弹出对话框的两种方式

在BootStrap官网的JavaScript插件里找到模态框. copy复制粘贴.
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
... ...
</div>
让模态框显示,有两种方式.
方式一:通过bootstrap的标签属性
    # data-target="#deleteModal" 找到id=“deleteModal”的盒子触发显示模态框的点击事件. bootstrap里面已经写好了.
    <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#deleteModal">
        Launch demo modal
    </button>
方式二:给盒子绑定一个点击事件,自己写js让其展示出来. 
   具体步骤,给客户列表展示页面的删除按钮都加上名为btn-delete的类选择器;编写js代码.
   <script>
       $(function () {
           bindDeleteEvent();
       });
       function bindDeleteEvent() {
           $(".btn-delete").click(function () {
               $("#deleteModal").modal("show");
           });
       }
   </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 业务逻辑

在删除按钮上 自定义属性,cid={{ row.id }}
在触发删除按钮的点击事件时,可以通过 DELETE_ID = $(this).attr("cid") 获取到当前删除客户的id,并赋值给js的全局变量DELETE_ID.
当点击删除按钮弹出的对话框中的确定按钮时,触发执行的函数中可以使用全局变量DELETE_ID!! 成功将客户的id进行了传递!
此处,点击确认按钮后,将通过全局变量DELETE_ID得到的客户ID传递到后端.
用什么方式传递数据呢? 表单 和 ajax. 因为表单会刷新.这里,我们使用ajax,将id偷偷的发送到后台,后台一接受一删除即可.

后端进行细致的判断. cid是否取到/url中有无cid参数;要删除的数据是否存在.. 并使用封装好的返回类返回json数据给前端.
前端根据res.status的真假来处理
   若删除失败,展示错误信息;
   若删除成功,不想刷新页面的话,就找到当前删除的行在页面上手动删除.(在tr标签上自定义了属性 row-id)
1
2
3
4
5
6
7
8
9
10

关键代码 web/views/customer.py

def customer_delete(request):
    """ 删除数据 """
    cid = request.GET.get('cid', '')
    if not cid:
        res = BaseResponse(status=False, detail="请选择要删除的数据")
        return JsonResponse(res.dict)

    exists = models.Customer.objects.filter(id=cid, active=1).exists()
    if not exists:
        res = BaseResponse(status=False, detail="要删除的数据不存在")
        return JsonResponse(res.dict)

    models.Customer.objects.filter(id=cid, active=1).update(active=0)
    res = BaseResponse(status=True)
    return JsonResponse(res.dict)
    """可以不做那么细致的判断,通过try..except..来实现!
    try:
        cid = request.GET.get("cid")
        models.Customer.objects.filter(pk=cid, active=1).update(active=0)
        res = BaseResponse(status=True)
        return JsonResponse(res.dict)
    except Exception as e:
        res = BaseResponse(status=True, detail=e)
        return JsonResponse(res.dict)
    """
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

关键代码 web/templates/customer_list.html

# 删除按钮
<a href="#" cid="{{ row.id }}" class="btn btn-danger btn-xs btn-delete">删除</a>
# 对话框
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">...</div>
# tr标签
<tr row-id="{{ row.id }}">...</tr>


{% block js %}
    <script>
        let DELETE_ID;
        $(function () {
            bindDeleteEvent();
            bindConfirmDeleteEvent();  // 确认删除
        });

        function bindDeleteEvent() {
            // 触发点击事件,对话框显示
            $(".btn-delete").click(function () {
                $("#deleteError").empty();  // 先删除对话框里的错误信息
                $("#deleteModal").modal("show");
                DELETE_ID = $(this).attr("cid")
            });

            // 点击点击事件,对话框消失
            $("#btnCancelDelete").click(function () {
                $("#deleteModal").modal('hide');
            });
        }

        function bindConfirmDeleteEvent() {
            $("#btnConfirmDelete").click(function () {
                // 举个例子,ajax发送get请求 /customer/delete/?cid=2
                $.ajax({
                    url: "{% url 'customer_delete' %}",
                    type: "GET",
                    data: {cid: DELETE_ID},
                    dataType: "JSON",
                    success: function (res) {
                        if (res.status) {
                            // -- 删除成功
                            // 方式一:页面的刷新
                            // location.reload();
                            // 方式二:在tr标签上自定义row-id属性 便于找到当前数据行,删除标签
                            $("tr[row-id='" + DELETE_ID + "']").remove();
                            $("#deleteModal").modal('hide'); // 删除后,对话框消失
                        } else {
                            // -- 删除失败,错误信息在页面展示.
                            $("#deleteError").text(res.detail);
                        }
                    }
                })
            });
        }
    </script>
{% endblock %}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

# request.is_ajax()

关于不同的请求(form / ajax),处理其返回值的问题.

我们先在settings里不给角色删除客户的权限.
当我们登陆后,点击删除客户的按钮,再在弹出的对话框里点击确认按钮,数据发过去了,但页面不会有没有任何的反应..
打开浏览器的开发者模式,看该ajax请求的返回是什么?! 是一堆html的代码.当然不会有反应啦!!
ajax请求里,dataType: "JSON",希望返回的是json数据!
而且可以观察到,ajax请求的请求头中,`X-Requested-With: XMLHttpRequest`,ajax在发送请求时底层用的就是该对象!
So,请求头里它就是ajax请求,没有就不是ajax请求.可以用form表单发送请求,查看请求头进行验证!!
  

为什么无权限,返回的是html代码呢? 因为在中间件里,我们进行了权限判定时的逻辑导致的!!
关键代码如下:
   user_permission_dict = settings.NB_PERMISSION.get(request.nb_user.role)
   if current_name not in user_permission_dict:
       # 当无权时,返回的就是 一个无权访问的html页面!!
       return render(request, "permission.html")
上述的代码只适用于form表单发送请求时无权访问 -- 返回html页面! ajax请求无权访问时 -- 应该返回json字符串!!!
关键代码的修改如下:
   user_permission_dict = settings.NB_PERMISSION.get(request.nb_user.role)
   if current_name not in user_permission_dict:
       if request.is_ajax():
           # ajax
           return JsonResponse({"status": False, "detail": "您无权访问!"})
       else:
           # form
           return render(request, "permission.html")
        

综上:(特别是针对权限判断时,应该根据请求方式的不对,无权访问时,做不同的处理)
    - ajax请求,不应返回页面,应返回json数据
    - form请求,重定向到无权访问的页面.
    
    
举一反三:
  遇到请求相关的问题.或者request中包含某个值,忘记了,不要到处搜索,打印类型--找到类--看看包含了哪些数据/成员.
  # <WSGIRequest: GET '/customer/list/'> <class 'django.core.handlers.wsgi.WSGIRequest'>
  print(request,type(request)) 
  from django.core.handlers.wsgi import WSGIRequest # 点进去查看WSGIRequest的源码!
  WSGIRequest类中没有,就去它继承的父类HttpRequest中找. 找到了一个is_ajax的方法! 问题得以解决!
  def is_ajax(self):
      warnings.warn(
          'request.is_ajax() is deprecated. See Django 3.1 release notes '
          'for more details about this deprecation.',
          RemovedInDjango40Warning,
          stacklevel=2,
      )
      return self.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest'  # True or False
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

image-20230407195135831


级别管理
分页和搜索

← 级别管理 分页和搜索→

最近更新
01
deepseek本地部署+知识库
02-17
02
实操-微信小程序
02-14
03
教学-cursor深度探讨
02-13
更多文章>
Theme by Vdoing | Copyright © 2023-2025 DC | One Piece
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式