PythonOther

Git

  1. git是一个分布式的版本控制软件,版本控制最早期是文件的复制粘贴、下一个阶段是集中管理、现在是分布式管理。

  2. 集中管理是将文件存储到一个地方,优点是解决了共同开发,缺点也很明显就是如果存储的地方出现问题就无法使用了。

  3. 分布式管理就很好了在网络中的每个点都存储一遍,优点是哪个节点出现问题都不影响其他节点。

  4. 安装:去https://git-scm.com/下载后,点下一步即可。

1.基本使用

  1. 右键点击bash here,版本查看 git --version

  2. 进入要管理的文件夹打开Bash Here后,输入git init作用是初始化,初始化后需要查看一下文件夹中的文件使用git status

  3. 查看后你会发现文件是红色的,红色代表未在工作区,我们需要将其添加到缓存区使用git add "FileName"或全部添加git add .,文件修改后并不在管理范围之内,所以需要再次添加并生成新的版本。

  4. 我们第一次使用git去生成版本时会出现一个提示,说是要你填写Email and Name,填写这两个是为了让git记住你。

    git config --global user.email "you@example.com"
    git config --global user.name "Your Name"
  5. 添加完之后我们需要生成版本使用git commit -m "VersionName",生成版本后我们肯定要查看的所以我们使用git log查看。

  6. 版本生成后你就会发现,如果有一天需要将一个功能取消,你不可能一行一行去删,这里只能用到回滚了。

    git add "FileName" # 添加到缓存区
    git commit "versionName" # 添加到版本库
    git reset --soft "VersionNum" # 退回到缓存区
    git reset head "FileName" # 退回到工作区(exit)
    git check out "FileName" # exit退回到start
    git reset --mix "VersionName" # 版本库退回到工作区exit
    git reset --hard "VersionName" # 版本库退回到工作区start(想退回到上一个版本,就查看历史版本后,使用这条命令)
    git reflog # 查看历史版本
  7. 你有没有想过版本库的存储方式是什么,如果我有两个功能要开发你需要找两个人开发,而版本库需要使用分支来存储,分支的好处在于两个人的开发并不会互相影响。有多个分支要合并时,如果两个分支修改同一行代码时,会出现提示需要手动删除不需要的合并部分。(合并时先转到主分支)

    git branch # 查看分支
    git branch "BranchName" # 创建分支
    git checkout "BranchName" # 切换分支
    git merge "BranchName" # 合并分支
    git branch -d "BranchName" # 删除分支
  8. 在公司我们开发需要至少创建两个分支,一个是正式分支、另一个是开发分支,只有开发分支稳定后再去合并到正式分支中。

2.远程仓库(GitHub)

  1. 远程仓库GitHub存储你写的代码前需要创建账户和仓库,创建后拿到仓库RUL后到自己的电脑上使用以下命令即可。

    git remote add "origin" "URL" # 给仓库创建别名
    git push -u "origin" "master" # 推送分支到仓库
    git clone "URL" # 克隆远程的仓库并帮你给仓库起别名为origin,创建别名只需要一次即可。
    git pull "origin" "dev" # 更新本地代码
  2. git pull "origin" "dev"等于git fetch "origin" "dev"(是将github上的代码拉到版本库的)+git merge origin/dev(是将拉到版本库的代码放到工作区)。

  3. 在工作中代码需要合并的,这里我们在合并有三种应用场景。

    1. 在工作中版本太多后,给老板给时不可能交给多个版本,我们只能将版本合并起来交给老板。

      git rebase -i "VersionNum" # 将该码以上的全部合并
      git rebase -i HEAD~3 # 合并当前版本下3个,版本可以随意。
      # 注意:
      修改前面为s,s代表将s代表的合并到pick版本上。
      版本合并时会出现描述,你需要写一下描述。
    2. 在两个分支便携时会出现,分支合并并不会直接合并只会让它新生产一个版本合并的,而你想合并成一条时可以如下操作。

      git checkout dev
      git rebase master
      git chekcout master
      git merge dev
      git log --graph --pretty-format:"%h %s" # 简化显示
    3. 在GitHub上的代码是最新的,而在你的电脑上的并不是,你需要拉下来但是拉下来会新创建一个版本,我们可以使用一下代码将其合并。

      git fetch "origin" "dev"
      git checkout "dev"
      git rebase "master"
      git rebase --continue
      # 注意:修改冲突带代码,后并添加到暂存区后继续执行下去。
  4. 我们在公司开发时,git的使用应该是一个主分支,一个开发分支,在开发分支下又有多个功能分支,功能分支完成开发后合并到开发分支,开发分支需要先合并到测试分支,测试完之后再合并到主分支即可。

3.其他

3.1 快速解决冲突

  1. 快速解决冲突我们需要使用的一个软件,“beyond compare”用来快速解决冲突代码。

  2. 我们需要在git中配置该文件后,才可以使用。

    git config --local merge.tool bc3 # 起名字为bc3
    git config --local mergetool.path "PATH" # 指定软件路径
    git config --local mergetool.keepBackup false # 解决冲突后并不保留,备份
  3. 剩下的就是使用了,用git mergetool启动beyond compare。

3.2 git配置文件

  1. git有三个配置文件,项目、所有项目和系统三种,使用不同的命令配置。

  2. 配置文件的位置。

    # 项目配置文件"project/.git/config"
    git config --local
    # 全局配置文件"~/.gitconfig"
    git config --global
    # 系统配置文件"/etc/.gitconfig"
    git config --system

3.3 GitHub免密登录

  1. URL中免密,是使用将用户名密码拼接在连接中并配置在,git配置文件中实现的。

    git remote add origin https://UserName:Password@github.com/...
  2. SSH实现免密,使用公钥和私钥将其实现免密,是将公钥放到GitHub的settings中配置后即可使用。

    # 生成公钥和私钥(默认放在~/.ssh目录下的,id_rsa.pub公钥、id_rsa私钥)
    ssh-keygen
    git remote add origin git@github.com/...

3.4 git忽略文件

  1. git想要忽略文件就需要创建一个文件gitignore并在里面写如需要忽略的文件。(*.h:应用.h结尾文件、!a.h:不应用这个文件、files/:忽略该文件夹下的所有文件、a.py[a|b]:忽略a.pya和a.pyb的文件)。

rabbmitMQ

  1. 消息队列分为两种一种是ssh只需要在服务以上安装下面的机器都可以使用,另一种是agent下面的每个机子都需要安装。但是agent的效率高,速度快,服务器只需要下达命令,它们就会去队列中自己去取,执行完后自动将数据放到消息队列。

  2. 公司在什么情况下才会用到消息队列?只有在任务处理不过来的时候才会用到,避免程序堵塞。

1.基本配置

  1. 安装

    # 服务器端
    # 安装配置epel源
      $ rpm -ivh http://dl.fedoraproject.org/pub/epel/6/i386/epel-release-6-8.noarch.rpm
       # 安装erlang
    $ yum -y install erlang
    # 安装RabbitMQ
    $ yum -y install rabbitmq-server
    # 客户端
    pip3 install pika
  2. 启动

    service rabbitmq-server start/stop
  3. 设置用户密码

    sudo rabbitmqctl add_user wupeiqi 1234
    # 设置用户为administrator
    sudo rabbitmqctl set_user_tags wupeiqi administrator
    # 设置权限
    sudo rabbitmqctl set_permissions -p "/" root ".*" ".*" ".*"

2.生产者和消费者

  1. 生产者

    import pika
    # 无密码
    # connection = pika.BlockingConnection(pika.ConnectionParameters("localhost")) # 创建连接
    # 有密码
    credentials = pika.PlainCrredentials("root","123")
    connection = pika.BlockingConnection(pika.ConnectionParameters("localhost",credentials=credentials))# 创建连接
    channel = connection.channel()
    channel.queue_declare(queue="hello") # 声明一个队列
    channel.basic_publish(exchange="",
                        routing_key="hello", # 队列名
                        body="asdfasdf") # 数据
    connection.close() # 关闭连接
  2. 消费者

    import pika
    # 有密码
    credentials = pika.PlainCrredentials("root","123")
    connection = pika.BlockingConnection(pika.ConnectionParameters("localhost",credentials=credentials))# 创建连接
    channel = connection.channel()
    channel.queue_declare(queue="hello") # 声明一个队列
    def callback(ch, method, properties, body):
       print("消费者接收到任务:%r" % body)
    channel.basic_consune(callback, queue="hello", no_ack=True) # 接收后调用函数
    channel.start_consuning() # 开始接收

3.ack回复和持久化

  1. 生产者

    import pika
    # 无密码
    # connection = pika.BlockingConnection(pika.ConnectionParameters("localhost")) # 创建连接
    # 有密码
    credentials = pika.PlainCrredentials("root","123")
    connection = pika.BlockingConnection(pika.ConnectionParameters("localhost",credentials=credentials))# 创建连接
    channel = connection.channel()
    channel.queue_declare(queue="hello", durable=True) # 声明一个队列,协商durable=True后就支持持久化了,不过需要重新创建一个队列才可以使用
    channel.basic_publish(exchange="",
                        routing_key="hello", # 队列名
                        body="asdfasdf", # 数据
                        properties=pika.BasicProperties(
                        delivery_mode = 2, # 这里也需要配置才可以持久化
                        ))
    connection.close() # 关闭连接
  2. 消费者

    import pika
    # 有密码
    credentials = pika.PlainCrredentials("root","123")
    connection = pika.BlockingConnection(pika.ConnectionParameters("localhost",credentials=credentials))# 创建连接
    channel = connection.channel()
    channel.queue_declare(queue="hello") # 声明一个队列
    def callback(ch, method, properties, body):
       print("消费者接收到任务:%r" % body)
       ch.basic_ack(delivery_tag=method.delivery_tag) # 回复服务器可以删除值了
    channel.basic_qos(prefetch_count=1) # 在rabbmitMQ中是均匀分配的,但是有一种情况是一个节点执行的慢,这个节点肯定不能使用均匀分配了,只要加上这句就可以实现空闲分配了。
    channel.basic_consune(callback, queue="hello", no_ack=False) # 接收后调用函数,你在不适用ack是消费者会直接将数据取走,当你启用ack后你去取数据时会先复制一份你先用,当你回复后再将数据删除。
    channel.start_consuning() # 开始接收

4.durable

  1. 生产者

    import pika
    # 有密码
    credentials = pika.PlainCrredentials("root","123")
    connection = pika.BlockingConnection(pika.ConnectionParameters("localhost",credentials=credentials))# 创建连接
    channel = connection.channel()
    channel.exchange_declare(exchange="m1", exchange_type="fanout") # 防止生产者先运行,这样可以实现一个生产者发送,多个消费者接收的情况。
    channel.basic_publish(exchange="m1",
                        routing_key="sb", # 使用了exchange后对列名就变成了关键名了,被定义的人才可以接收到消息。
                        body="asdfasdf", # 数据
                        )
    connection.close() # 关闭连接
  2. 消费者

    import pika
    # 有密码
    credentials = pika.PlainCrredentials("root","123")
    connection = pika.BlockingConnection(pika.ConnectionParameters("localhost",credentials=credentials))# 创建连接
    channel = connection.channel()
    channel.exchange_declare(exchange="m1", exchange_type="fanout") # exchange是消费者的名称,exchange_type消费者的工作模式
    # 用于生成随机队列名称
    result = channel.queue_declare(exclusive=True)
    queue_name = result.method.queue
    # 让exchange和queque进行绑定,这里的列表名是关健名了,只有使用了的人才可以收到制定的消息。
    channel.queue_bind(exchange="m1", queue=queue_name, routing_key="sb.*") # 当你不知道发送的人是谁,可以使用模糊查找来发送*代表一个单词,#号代表多个单词。
    def callback(ch, method, properties, body):
       print(method.routing_key) # 显示发过来的key
       print("消费者接收到任务:%r" % body)
       
    channel.basic_consune(callback, queue=queue_name, no_ack=True)
    channel.start_consuning() # 开始接收

垃圾回收和内存管理

  1. 引用计数器为主,标记清除和分代回收为辅。

引用计数器

环形双向链表 Refchain

  1. 在python程序中创建的任何对象都会放在refchain链表中。

  2. 每个对象会常见基本的资格对象(上一个对象、下一个对象、类型、引用个数),每个类型中还有不同的对象。如:字典由itme。

  3. 在c源码中如何体现每个对象中都有的相同的值:PyObject结构体(4个值)。

  4. 由多个元素组成的对象:PyObject结构体+ob_size。

  5. c中编写的数据类型

    #define PyObject_HEAD PyObject ob_base;
    #define PyObject_VAR_HEAD PyVarObject ob_base;
    // 宏定义,包含上一个、下一,用于构造双向链表用。
    #define _PyObject_HEAD_EXTRA
    struct _object *_ob_next;
    struct _object *_ob_prev;
    typedef struct _object {
       _PyObject_HEAD_EXTRA // 用于构造双向链表
       Py_ssize_t ob_refcnt; // 引用计数器
       struct _typeobject *ob_type // 数据类型
    } PyObject;
    typedef struct {
       PyObject ob_base;
       Py_ssize_t ob_size;
    }PyVarObject;
    // float
    typedef struct {
       PyObject_HEAD
       double ob_fval;
    } PyFloatObject;
    // int
    struct _longobject {
       PyObject_VAR_HEAD
       digit ob_digit[1];
    };
    typedef struct _longobject PyLongObject;
    // list
    typedef struct {
       PyObject_VAR_HEAD
       PyOBject **ob_item;
       Py_ssize_t allocated;
    }
    // tuple
    typedef struct {
       PyObject_VAR_HEAD
       PyObject *ob_item[1];
    } PyTupleObject;
    // dict
    typedef struct {
       PyObject_HEAD
       Py_ssize_t ma_used;
       PyDictKeysObject *ma_keys;
       PyObject **ma_values;
    } PyDictObject;
  6. 引用计数器是你常见了一个对象后,被其他变量所应用(并不是复制),引用会被增加或减少del删除引用后就会减少。

标记清除

  1. 在对象引用中我们会发现,列表在取出所有的应用后,依然存在于内存之中。这是应为计数器循环引用的问题。

  2. 在python的底层再维护一个链表,链表中专门放哪些可能存在循环引用的对象。

  3. 在python内部某种情况下触发,回去扫描可能存在循环引用的链表中的每个元素,检查是否有循环引用,如果有则让双方的引用计数器-1;如果是0则垃圾回收。

分代回收

  1. 将可能存在循环引用的对象维护成3个链表:

    1. 0代:0代中对象个数达到700个扫描一次。

    2. 1代:0代扫描10次,则1代扫描一次。

    3. 2代:1代扫描10次,则2代扫描一次。

缓存机制

  1. 为了避免重复创建和销毁一些常见对象,所以出现了池。

  2. 池中会创建常用的一些对象,当你调用这个对象是,他们的内存地址是指定的。

  3. 当一个对象的引用计数器为0时,按理来说应该回收,内部不会直接回收,而是将对象添加到free_list列表中当缓存。以后再去创建对象时,不再重新开辟内存,而是直接使用free_list中的对象。free_list(80),销毁的内存地址和创建的内存地址是一样的。

信息发布系统

  • ajax

    $.ajax({
      url:">>>"
      type:"GET",
      data:{},
      dataType:"JSON"
      success:function(arg){
      //成功时执行
      }
      error:function(){
      //失败时执行
      }
    })
  • 队列(先进先出)

    import queue
    #创建一个队列
    q = queue.Queue()
    #往队列中放数据
    q.put("alex1")
    q.put("alex2")
    #去队列中取数据
    v1 = q.get()
    v2 = q.get()
    print(v1,v2)
    #alex1 alex2
    try:
       #数列中没有值后再去取值就会等待,等到queue对象中有值时拿到返回。
       #加上timeout后指定多少秒后结束等待,拿到值正常返回,没达到报错。
       v3 = q.get(timeout=3)
       print(v3)
    #可以指定错误
    except queue.Empty as e:
       pass
  • 递归

    #python时官方给出的是递归1000次
    def func():
       func()
    #js不受限
    function fi(){
       $.ajax({
           url:"",
           success:function(arg){
               f1()
          }
      })
    }

     

1.服务端向客户端主动推送消息

1.1 轮询

  • 客户机每几秒通过ajax发一次请求,访问后立即断开。

  • 数据会错过,导致延迟和发送请求次数高。

1.2 长轮询

  • 客户机每几秒通过ajax发一次请求,服务端会为每个客户端创建一个队列,如果有数据拿到后立即断开,没有数据等待多少秒后断开。

  • 降低了查询次数高和延迟的问题。

1.2.1 聊天室

  • urls.py

    from django.contrib import admin
    from django.urls import path
    from app01 import views
    urlpatterns = [
       path('admin/', admin.site.urls),
       path('home/', views.home),
       path('send/message/',views.send_message),
       path('get/message/',views.get_message),
    ]
  • app01/views.py

    from django.shortcuts import render,HttpResponse
    from django.http import JsonResponse
    import queue
    import json
    # Create your views here.
    QUEUE_DICT = {}
    def home(request):
       name = request.GET.get("name")
       QUEUE_DICT[name] = queue.Queue()
       return render(request,"home.html",{"name":name})
    def send_message(request):
       """
      接收客户端浏览器发送的消息
      """
       msg = request.POST.get("msg")
       for q in QUEUE_DICT.values():
           q.put(msg)
       return HttpResponse("sss")
    def get_message(request):
       """
      浏览器获取消息
      """
       name = request.GET.get("name")
       q = QUEUE_DICT[name]
       result = {"status":True,"data":None}
       data = q.get(timeout=100)
       result["data"] = data
       """
      try:
           
      except queue.Empty as e:
          result["status"] = False
      """
       #这两句是相等的
       #return HttpResponse(json.dumps(result))
       return JsonResponse(result)
  • home.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8"/>
    <title></title>
    </head>
    <body>
    <h1>Web聊天室({{name}})</h1>
    <div>
    <input id="txt" type="text" placeholder="请输入消息" />
    <input id="btn" type="button" value="发送" onclick="sendMessage();" />
    </div>
    <div>
    <h3>聊天记录</h3>
    <div id="content"></div>
    </div>
    <script src="/static/js/jquery-3.3.1.min.js"></script>
    <script>
    function sendMessage() {
    $.ajax({
    url:"/send/message/",
    type:"POST",
    data:{msg:$("#txt").val()},
    success:function(arg){
    console.log(arg)
    }
    })
    }
    function getMessage() {
    $.ajax({
    url:"/get/message/",
    type:"GET",
    data:{name:"{{name}}"},
    dataType:"JSON",
    success:function(arg){
    if(arg.status){
    //创建新的div标签
    var tag=$("<div>");
    tag.text(arg.data);
    //将div标签添加到content标签中
    $("#content").append(tag);
    }else{
    }
    //需要写上递归,不然执行一遍就不执行了
    getMessage();
    }
    })
    }
    $(function(){
    getMessage();
    });
    </script>
    </body>
    </html>

1.3 websocket

  • WebSocket协议是基于TCP的一种新的协议。WebSocket最初在HTML5规范中被引用为TCP连接,作为基于TCP的套接字API的占位符。它实现了浏览器与服务器全双工(full-duplex)通信。其本质是保持TCP连接,在浏览器和服务端通过Socket进行通信。

  • web、写网站让浏览器和服务端进行交互。socket,让网络上的两端创造链接并进行收发数据。websocket是一个网咯协议(让浏览器和服务端创建链接支持,默认不在断开,俩端就可以完成互相之间收发数据)。websocket协议的诞生,可以让我们真正实现服务端向客户端推送消息。

1.3.1 原理

握手环节,验证服务端是否支持websocket协议。
浏览器生成一个随机字符串,将随机字符串发送给服务端。
服务端接收到随机字符串之后,让它和magic string拼接,然后再进行sha1/base64加密将密文返回到用户浏览器
用户浏览器自动化会进行验证
收发数据,密文
数据解密时需要读取数据第二个字节的后7位,如果是<=127、=126、>=125时做出不同的操作。

1.3.2 实现链接

  • python

    """
    请求【握手】信息为:
    GET /chatsocket HTTP/1.1
    Host: 127.0.0.1:8002
    Connection: Upgrade
    Pragma: no-cache
    Cache-Control: no-cache
    Upgrade: websocket
    Origin: http://localhost:63342
    Sec-WebSocket-Version: 13
    Sec-WebSocket-Key: mnwFxiOlctXFN/DeMt1Amg==
    Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
    ...
    ...
    提取Sec-WebSocket-Key值并加密:
    """
    import socket
    import base64
    import hashlib
    def get_headers(data):
       """
      将请求头格式化成字典
      :param data:
      :return:
      """
       header_dict = {}
       data = str(data, encoding='utf-8')
       for i in data.split('\r\n'):
           print(i)
       header, body = data.split('\r\n\r\n', 1)
       header_list = header.split('\r\n')
       for i in range(0, len(header_list)):
           if i == 0:
               if len(header_list[i].split(' ')) == 3:
                   header_dict['method'], header_dict['url'], header_dict['protocol'] = header_list[i].split(' ')
           else:
               k, v = header_list[i].split(':', 1)
               header_dict[k] = v.strip()
       return header_dict
    def get_data(info):
       """
      4.客户端和服务端收发数据
    客户端和服务端传输数据时,需要对数据进行【封包】和【解包】。客户端的JavaScript类库已经封装【封包】和【解包】过程,但 Socket服务端需要手动实现。
    """
    #第一步:获取客户端发送的数据【解包】
       payload_len = info[1] & 127
       if payload_len == 126:
           extend_payload_len = info[2:4]
           mask = info[4:8]
           decoded = info[8:]
       elif payload_len == 127:
           extend_payload_len = info[2:10]
           mask = info[10:14]
           decoded = info[14:]
       else:
           extend_payload_len = None
           mask = info[2:6]
           decoded = info[6:]
       bytes_list = bytearray()
       for i in range(len(decoded)):
           chunk = decoded[i] ^ mask[i % 4]
           bytes_list.append(chunk)
       body = str(bytes_list, encoding='utf-8')
       return body
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind(('127.0.0.1', 8002))
    sock.listen(5)
    conn, address = sock.accept()
    data = conn.recv(1024)
    headers = get_headers(data) # 提取请求头信息
    # 对请求头中的sec-websocket-key进行加密
    response_tpl = "HTTP/1.1 101 Switching Protocols\r\n" \
         "Upgrade:websocket\r\n" \
         "Connection: Upgrade\r\n" \
         "Sec-WebSocket-Accept: %s\r\n" \
         "WebSocket-Location: ws://%s%s\r\n\r\n"
    magic_string = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
    #从请求【握手】信息中提取 Sec-WebSocket-Key
    value = headers['Sec-WebSocket-Key'] + magic_string
    #利用magic_string 和 Sec-WebSocket-Key 进行hmac1加密,再进行base64加密
    ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest())
    response_str = response_tpl % (ac.decode('utf-8'), headers['Host'], headers['url'])
    # 响应【握手】信息
    #将加密结果响应给客户端
    #注:magic string为:258EAFA5-E914-47DA-95CA-C5AB0DC85B11
    conn.send(bytes(response_str, encoding='utf-8'))
    while True:
       data = conn.recv(8096)
       value=get_data(data)
       print(value)
  • html

    <!DOCTYPE HTML>
    <html>
    <head>
       <meta charset="UTF-8">
       <title></title>
    </head>
    <body>
       <script>
           //自动创建随机字符串
           //并将随机字符串发送给服务端
           //密文进行校验
      var socket = new WebSocket("ws://127.0.0.1:8002/xxoo");
       </script>
    </html>
       
    <!--在CONsole中输入ws.send("..")是给后端发信息-->

1.4 django实现websocket

1.4.1 channels

  • 在django中如果想要开发websocket相关的功能,就需要先安装和卸载:

    • pip3 install channels

    • pip3 uninstall channels

    • 建议:在python3.6的环境下运行。

  • 在channels的内部已经帮助我们写了助手/加密/解密等所有环节。

  • 注意:不是所有的服务端都支持websocket

    • django默认不支持,需要使用第三方channels

    • flask默认不支持,需要使用第三方geventwebsocket

    • tornado默认支持。

1.4.2 channels配置

  • 项目创建名channel_demo

  • settings.py

    #配置你应该会吧,算料,不了不你急着头秃掉了
    # Application definition
    INSTALLED_APPS = [
       'django.contrib.admin',
       'django.contrib.auth',
       'django.contrib.contenttypes',
       'django.contrib.sessions',
       'django.contrib.messages',
       'django.contrib.staticfiles',
       "app01.apps.App01Config",
       #项目中要使用channels做websocket了
       "channels",
    ]
    #第一个是配置文件的文件夹名,routing.py自己创建
    ASGI_APPLICATION = "channel_demo.routing.application"
  • /channel_demo/routiong.py

    #这里专门写websocket的url
    #用http协议发送的还是在原来的urls中写
    #!/usr/bin/env python
    # -*- coding:utf-8 -*-
    from channels.routing import ProtocolTypeRouter,URLRouter
    from django.urls import re_path
    from app01 import consumers
    application = ProtocolTypeRouter({
       'websocket': URLRouter([
           re_path(r'^chat/$', consumers.ChatConsumer.as_asgi()),
      ])
    })
  • app01/consumers.py

    #必须要写的
    from channels.generic.websocket import WebsocketConsumer
    from channels.exceptions import StopConsumer
    class ChatConsumer(WebsocketConsumer):
       def websocket_connect(self, message):
           """
          客户端发来链接请求之后就会被触发
          """
           #服务端接收链接,向客户端浏览器发送一个加密字符串
           self.accept()
       def websocket_receive(self, message):
           """
          客户端浏览器向服务端发送消息,此方法自动触发
          """
           print('接收到消息', message)
           #服务端给客户端回一条消息
           self.send(text_data='收到了')
       def websocket_disconnect(self, message):
           """
          客户端主动断开连接
          """
           print('客户端断开连接了')
           #服务端断开链接
           raise StopConsumer()
  • 本质:channels会把原来支持http协议的wsgi,换成支持http和websocket协议的asgi。

1.4.3 编写聊天室(非主流型)

  • app01/consumers.py

    #!/usr/bin/env python
    # -*- coding:utf-8 -*-
    from channels.generic.websocket import WebsocketConsumer
    from channels.exceptions import StopConsumer
    CONSUMER_OBJECT_LIST = []
    class ChatConsumer(WebsocketConsumer):
       def websocket_connect(self, message):
           """
          客户端发来链接请求之后就会被触发
          """
           #服务端接收链接,向客户端浏览器发送一个加密字符串
           self.accept()
           #访问时将访问对象添加到列表中
           CONSUMER_OBJECT_LIST.append(self)
           print("建立了链接")
       def websocket_receive(self, message):
           """
          客户端浏览器向服务端发送消息,此方法自动触发
          """
           #message是发送来你拿到的数据
           #print('接收到消息', message)
           #服务端给客户端回一条消息
           #self.send(text_data='收到了')
           #拿到返回的所有信息
           #print(self.send(text_data=message))
           #拿到返回的文本信息
           #self.send(text_data=message["text"])
           #拿到前端发来的数据后
           #循环每个访问者后推送数据
           for obj in CONSUMER_OBJECT_LIST:
               obj.send(text_data=message["text"])
           #self.send(message)
       def websocket_disconnect(self, message):
           """
          客户端主动断开连接
          """
           print('客户端断开连接了')
           #访问者主动断开时删除列表的用户对象
           CONSUMER_OBJECT_LIST.remove(self)
           #服务端断开链接
           raise StopConsumer()
  • template/index.html

    {% load static %}
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8"/>
    <title></title>
    </head>
    <body>
    <h1>Web聊天室</h1>
    <div>
    <input id="txt" type="text" placeholder="请输入消息" />
    <input  type="button" value="发送" onclick="sendMsg();" />
    <input  type="button" value="断开" onclick="closes();" />
    </div>
    <div>
    <h3>聊天记录</h3>
    <div id="content"></div>
    </div>
    <script src="{% static 'js/jquery-3.3.1.min.js' %}"></script>
    <script>
    ws = new WebSocket("ws://127.0.0.1:8000/chat/");
    //客户端在握手环节验证成功之后,自动执行此方法。
    ws.onopen = function(){
    console.log("连接成功");
    }
    //当服务端给客户端主动推送消息自动执行,回调函数
    ws.onmessage = function(event){
    //event.data就是数据
    var tag = $("<div>");
    tag.text(event.data);
    $("#content").append(tag);
    }
    function sendMsg(){
    //向后台发送数据
    ws.send($("#txt").val());
    }
    function closes(){
    //客户端主动发起断开连接
    ws.close();
    }
    </script>
    </body>
    </html>

1.4.4 l

2.gojs插件

2.1 示例

  1. 只是常用的不代表全部,可以下载它的site.zip后查看。

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <title>Title</title>
</head>
<body>
<div id="myDiagramDiv" style="width:500px; height:350px; background-color: #DAE4E4;"></div>
<script src="gojs/go.js"></script>
<script>
   //百分号代表这个语句
   var $ = go.GraphObject.make;
   // 第一步:创建图表,第二个是标签id值
   var myDiagram = $(go.Diagram, "myDiagramDiv"); // 创建图表,用于在页面上画图
   // 第二步:创建一个节点,类型是文字,内容为武沛齐
   var node = $(go.Node, $(go.TextBlock, {text: "武沛齐"}));
   // 第三步:将节点添加到图表中
   myDiagram.add(node)
</script>
</body>
</html>

2.2 TextBlock文字

<script src="gojs/go.js"></script>
<script>
   var $ = go.GraphObject.make;
   // 第一步:创建图表
   var myDiagram = $(go.Diagram, "myDiagramDiv"); // 创建图表,用于在页面上画图
   //创建一个节点,(类型是文字,内容为武沛齐(不加默认是黑色))
   var node1 = $(go.Node, $(go.TextBlock, {text: "武沛齐"}));
   myDiagram.add(node1);
//stroke是颜色
   var node2 = $(go.Node, $(go.TextBlock, {text: "武沛齐", stroke: 'red'}));
   myDiagram.add(node2);
//background是背景色
   var node3 = $(go.Node, $(go.TextBlock, {text: "武沛齐", background: 'lightblue'}));
   myDiagram.add(node3);
</script>

2.3 Shape图形

<script src="gojs/go.js"></script>
<script src="gojs/Figures.js"></script>
<script>
   var $ = go.GraphObject.make;
   var myDiagram = $(go.Diagram, "myDiagramDiv"); // 创建图表,用于在页面上画图
   var node1 = $(go.Node,
       //Shape类型为图形,Ellipse形状为圆形,宽高为40
       $(go.Shape, {figure: "Ellipse", width: 40, height: 40})
  );
   myDiagram.add(node1);
   var node2 = $(go.Node,
       //RoundedRectangle形状为圆角正方形,fill是颜色,stroke是边框颜色
       $(go.Shape, {figure: "RoundedRectangle", width: 40, height: 40, fill: 'green',stroke:'red'})
  );
   myDiagram.add(node2);
   var node3 = $(go.Node,
       //Rectangle形状为正方形,fill为无色
       $(go.Shape, {figure: "Rectangle", width: 40, height: 40, fill: null})
  );
   myDiagram.add(node3);
   var node4 = $(go.Node,
       $(go.Shape, {figure: "Diamond", width: 40, height: 40, fill: '#ddd'})
  );
   myDiagram.add(node4);
   // 需要引入Figures.js
   var node5 = $(go.Node,
       //特殊形状
       $(go.Shape, {figure: "Club", width: 40, height: 40, fill: 'red'})
  );
   myDiagram.add(node5);
</script>

2.4 Node搭配

<script src="gojs/go.js"></script>
<script src="gojs/Figures.js"></script>
<script>
   var $ = go.GraphObject.make;
   var myDiagram = $(go.Diagram, "myDiagramDiv"); // 创建图表,用于在页面上画图
   var node1 = $(go.Node,
       //上下关系
       "Vertical",
      {
      //节点颜色
           background: 'yellow',
      //节点与图形的余值
           padding: 8
      },
       $(go.Shape, {figure: "Ellipse", width: 40, height: 40}),
       $(go.TextBlock, {text: "武沛齐"})
  );
   myDiagram.add(node1);
   var node2 = $(go.Node,
       //左右关系
       "Horizontal",
      {
           background: 'white',
           padding: 5
      },
       $(go.Shape, {figure: "RoundedRectangle", width: 40, height: 40}),
       $(go.TextBlock, {text: "武沛齐"})
  );
   myDiagram.add(node2);
   var node3 = $(go.Node,
      //居中
       "Auto",
       $(go.Shape, {figure: "Ellipse", width: 80, height: 80, background: 'green', fill: 'red'}),
       $(go.TextBlock, {text: "武沛齐"})
  );
   myDiagram.add(node3);
</script>

2.5 Link连接

<script src="gojs/go-debug.js"></script>
<script>
   var $ = go.GraphObject.make;
   var myDiagram = $(go.Diagram, "myDiagramDiv",
      {layout: $(go.TreeLayout, {angle: 0})}
  ); // 创建图表,用于在页面上画图
//图块
   var startNode = $(go.Node, "Auto",
       $(go.Shape, {figure: "Ellipse", width: 40, height: 40, fill: '#79C900', stroke: '#79C900'}),
       $(go.TextBlock, {text: '开始', stroke: 'white'})
  );
   myDiagram.add(startNode);
//图块
   var downloadNode = $(go.Node, "Auto",
       $(go.Shape, {figure: "RoundedRectangle", height: 40, fill: '#79C900', stroke: '#79C900'}),
       $(go.TextBlock, {text: '下载代码', stroke: 'white'})
  );
   myDiagram.add(downloadNode);
//使用连接类型
   var startToDownloadLink = $(go.Link,
//fromNode开头,toNode结尾
      {fromNode: startNode, toNode: downloadNode},
//箭身宽为1
       $(go.Shape, {strokeWidth: 1}),
//箭头
       $(go.Shape, {toArrow: "OpenTriangle", fill: null, strokeWidth: 1})
  );
   myDiagram.add(startToDownloadLink);
//图块
   var zipNode = $(go.Node, "Auto",
       $(go.Shape, {figure: "RoundedRectangle", height: 40, fill: '#79C900', stroke: '#79C900'}),
       $(go.TextBlock, {text: '本地打包', stroke: 'white'})
  );
   myDiagram.add(zipNode);
//连接图块
   var downloadToZipLink = $(go.Link,
      {fromNode: downloadNode, toNode: zipNode},
       $(go.Shape, {strokeWidth: 1}),
       $(go.Shape, {toArrow: "OpenTriangle", fill: null, strokeWidth: 1})
  );
   myDiagram.add(downloadToZipLink);
//循环六次
   for (var i = 1; i < 6; i++) {
       //图块
       var node = $(go.Node, "Auto",
           $(go.Shape, {figure: "RoundedRectangle", height: 40, fill: 'lightgray', stroke: 'lightgray'}),
           $(go.TextBlock, {text: '服务器' + i, stroke: 'white', margin: 5})
      );
       myDiagram.add(node);
   //连接图块
       var nodeToZipLink = $(go.Link,
          {fromNode: zipNode, toNode: node, routing: go.Link.Orthogonal},
           $(go.Shape, {strokeWidth: 1, stroke: 'lightgray'}),
           $(go.Shape, {toArrow: "OpenTriangle", fill: null, strokeWidth: 1, stroke: 'lightgray'})
      );
       myDiagram.add(nodeToZipLink);
  }
</script>

2.5 数据绑定

<script src="gojs/go-no-logo.js"></script>
<script>
   var $ = go.GraphObject.make;
   var diagram = $(go.Diagram, "diagramDiv", {
       //加上他会排开
       layout: $(go.TreeLayout, {
           angle: 0,
           nodeSpacing: 20,
           layerSpacing: 70
      })
  });
   diagram.nodeTemplate = $(go.Node, "Auto",
       $(go.Shape, {
           figure: "RoundedRectangle",
           fill: 'lightgray',
           stroke: 'lightgray'
       //new go.binding("figure","figure1") 回去指定的字典中找,找到就替换找不到就用默认写好的
      }, new go.Binding("figure", "figure"), new go.Binding("fill", "color"), new go.Binding("stroke", "color")),
       $(go.TextBlock, {margin: 8}, new go.Binding("text", "text"))
  );
   diagram.linkTemplate = $(go.Link,
      {routing: go.Link.Orthogonal},
       $(go.Shape, {stroke: 'lightgray'}, new go.Binding('stroke', 'link_color')),
       $(go.Shape, {toArrow: "OpenTriangle", stroke: 'lightgray'}, new go.Binding('stroke', 'link_color')),
       $(go.TextBlock, {font: '8pt serif', segmentOffset: new go.Point(0, -10)}, new go.Binding("text", "link_text"))
  );
//这是被绑定的值
   var nodeDataArray = [
      {key: "start", text: '开始', figure: 'Ellipse', color: "lightgreen"},
      {key: "download", parent: 'start', text: '下载代码', color: "lightgreen", link_text: '执行中...'},
      {key: "compile", parent: 'download', text: '本地编译', color: "lightgreen"},
      {key: "zip", parent: 'compile', text: '打包', color: "red", link_color: 'red'},
      {key: "c1", text: '服务器1', parent: "zip"},
      {key: "c11", text: '服务重启', parent: "c1"},
      {key: "c2", text: '服务器2', parent: "zip"},
      {key: "c21", text: '服务重启', parent: "c2"},
      {key: "c3", text: '服务器3', parent: "zip"},
      {key: "c31", text: '服务重启', parent: "c3"}
  ];
   //值用过它传入
   diagram.model = new go.TreeModel(nodeDataArray);
   /*
  diagram.model.addNodeData({key: "c4", text: '服务器3', parent: "c3", color: "lightgreen"})
  var c1 = diagram.model.findNodeDataForKey("c1");
  diagram.model.setDataProperty(c1, "color", "red");
  */
</script>

3.paramiko模块,封装类

3.1安装

pip3 install paramiko

3.2 SSHClient

  • 基于用户名密码连接

    import paramiko
     
    # 创建SSH对象
    ssh = paramiko.SSHClient()
    # 允许连接不在know_hosts文件中的主机
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    # 连接服务器
    ssh.connect(hostname='c1.salt.com', port=22, username='wupeiqi', password='123')
    # 执行命令
    stdin, stdout, stderr = ssh.exec_command('df')
    # 获取命令结果
    result = stdout.read()
     
    # 关闭连接
    ssh.close()
  • 基于公钥密钥连接

    import paramiko
    private_key = paramiko.RSAKey.from_private_key_file('/home/auto/.ssh/id_rsa')
    # 创建SSH对象
    ssh = paramiko.SSHClient()
    # 允许连接不在know_hosts文件中的主机
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    # 连接服务器
    ssh.connect(hostname='c1.salt.com', port=22, username='wupeiqi', key=private_key)
    # 执行命令
    stdin, stdout, stderr = ssh.exec_command('df')
    # 获取命令结果
    result = stdout.read()
    # 关闭连接
    ssh.close()

3.3 SFTPClient

用于连接远程服务器并执行上传下载

  • 基于用户名密码上传下载

    import paramiko
    transport = paramiko.Transport(('hostname',22))
    transport.connect(username='wupeiqi',password='123')
    sftp = paramiko.SFTPClient.from_transport(transport)
    # 将location.py 上传至服务器 /tmp/test.py
    sftp.put('/tmp/location.py', '/tmp/test.py')
    # 将remove_path 下载到本地 local_path
    sftp.get('remove_path', 'local_path')
    transport.close()
  • 基于公钥密钥上传下载

    import paramiko
    private_key = paramiko.RSAKey.from_private_key_file('/home/auto/.ssh/id_rsa')
    transport = paramiko.Transport(('hostname', 22))
    transport.connect(username='wupeiqi', pkey=private_key )
    sftp = paramiko.SFTPClient.from_transport(transport)
    # 将location.py 上传至服务器 /tmp/test.py
    sftp.put('/tmp/location.py', '/tmp/test.py')
    # 将remove_path 下载到本地 local_path
    sftp.get('remove_path', 'local_path')
    transport.close()

3.4 封装类

import paramiko
class SSHProxy(object):
   def __init__(self, hostname, port, username, private_key_path):
       self.hostname = hostname
       self.port = port
       self.username = username
       self.private_key_path = private_key_path
       self.transport = None
   def open(self):
       private_key = paramiko.RSAKey.from_private_key_file(self.private_key_path)
       self.transport = paramiko.Transport((self.hostname, self.port))
       self.transport.connect(username=self.username, pkey=private_key)
   def close(self):
       self.transport.close()
   def command(self, cmd):
       ssh = paramiko.SSHClient()
       ssh._transport = self.transport
       stdin, stdout, stderr = ssh.exec_command(cmd)
       result = stdout.read()
       #ssh.close()
       return result
   def upload(self, local_path, remote_path):
       sftp = paramiko.SFTPClient.from_transport(self.transport)
       sftp.put(local_path, remote_path)
       sftp.close()
   def __enter__(self):
       self.open()
       return self
   def __exit__(self, exc_type, exc_val, exc_tb):
       self.close()
if __name__ == '__main__':
   #使用with时自动触发__enter__方法,ssh就是__enter__的返回值
   with SSHProxy('10.211.55.25', 22, 'root', '/Users/wupeiqi/.ssh/id_rsa') as ssh:
       # v1 = ssh.command('sudo ifconfig')
       # print(v1)
       ssh.upload('your.tar', '/data/your.tar')  
  #不使用时自动调用__exit__方法.

3.4.1 补充with

class SSHProxy(object):
   def __enter__(self):
       return self
   #如果记不住也可以写成(self,*args,**kwargs):
   def __exit__(self, exc_type, exc_val, exc_tb):
       pass
#with调用的是个对象也可以写成
#obj =SSHProxy()
#with obj as e:    
with SSHProxy('10.211.55.25', 22, 'root', '/Users/wupeiqi/.ssh/id_rsa') as ssh:
pass

4.python执行git命令

4.1安装

pip install gitpython

4.2 Git版本管理

  • 很多公司在使用git的tag进行版本的管理。

    git tag -n                          查看本地Tag
    git tag -l 'v1.4.2.*'               查看本地Tag,模糊匹配
    git show v1.0                       查看
    git tag -a v1.0 -m '版本介绍'        本地创建Tag
    git tag -d v1.0                     删除Tag
    git checkout v.10                   切换tag
    git push origin  --tags             推送本地tag到远程
    git pull origin  --tags             获取远程tag
    git clone -b v0.10  http://...      指定版本克隆代码

4.3 Python操作git

"""
基于Python实现对git仓库进行操作,使用前需要安装模块:gitpython
  pip3 install gitpython
"""
# ############## 1. clone下载代码 ##############
"""
import os
from git.repo import Repo
download_path = os.path.join('code', 'fuck')
Repo.clone_from('https://gitee.com/wupeiqi/fuck.git', to_path=download_path, branch='master')
"""
# ############## 2. pull最新代码 ##############
"""
import os
from git.repo import Repo
local_path = os.path.join('code', 'fuck')
repo = Repo(local_path)
repo.git.pull()
"""
# ############## 3. 获取所有分支 ##############
"""
import os
from git.repo import Repo
local_path = os.path.join('code', 'fuck')
repo = Repo(local_path)
branches = repo.remote().refs
for item in branches:
  print(item.remote_head)
"""
# ############## 4. 获取所有版本 ##############
"""
import os
from git.repo import Repo
local_path = os.path.join('code', 'fuck')
repo = Repo(local_path)
for tag in repo.tags:
  print(tag.name)
"""
# ############## 5. 获取所有commit ##############
"""
import os
from git.repo import Repo
local_path = os.path.join('code', 'fuck')
repo = Repo(local_path)
commit_log = repo.git.log('--pretty={"commit":"%h","author":"%an","summary":"%s","date":"%cd"}', max_count=50,
                        date='format:%Y-%m-%d %H:%M')
log_list = commit_log.split("\n")
real_log_list = [eval(item) for item in log_list]
print(real_log_list)
"""
# ############## 6. 切换分支 ##############
"""
import os
from git.repo import Repo
local_path = os.path.join('code', 'fuck')
repo = Repo(local_path)
before = repo.git.branch()
print(before)
repo.git.checkout('master')
after = repo.git.branch()
print(after)
repo.git.reset('--hard', '854ead2e82dc73b634cbd5afcf1414f5b30e94a8')
"""
# ############## 7. 打包代码 ##############
"""
with open(os.path.join('code', 'fuck.tar'), 'wb') as fp:
  repo.archive(fp)
"""

4.4 git相关操作类

import os
os.environ["GIT_PYTHON_REFRESH"] = "quiet"
from git.repo import Repo
from git.repo.fun import is_git_dir
class GitRepository(object):
   """
  git仓库管理
  """
   def __init__(self, local_path, repo_url, branch='master'):
       self.local_path = local_path
       self.repo_url = repo_url
       self.repo = None
       self.initial(repo_url, branch)
   def initial(self, repo_url, branch):
       """
      初始化git仓库
      :param repo_url:
      :param branch:
      :return:
      """
       if not os.path.exists(self.local_path):
           os.makedirs(self.local_path)
       git_local_path = os.path.join(self.local_path, '.git')
       if not is_git_dir(git_local_path):
           self.repo = Repo.clone_from(repo_url, to_path=self.local_path, branch=branch)
       else:
           self.repo = Repo(self.local_path)
   def pull(self):
       """
      从线上拉最新代码
      :return:
      """
       self.repo.git.pull()
   def branches(self):
       """
      获取所有分支
      :return:
      """
       branches = self.repo.remote().refs
       return [item.remote_head for item in branches if item.remote_head not in ['HEAD', ]]
   def commits(self):
       """
      获取所有提交记录
      :return:
      """
       commit_log = self.repo.git.log('--pretty={"commit":"%h","author":"%an","summary":"%s","date":"%cd"}',
                                      max_count=50,
                                      date='format:%Y-%m-%d %H:%M')
       log_list = commit_log.split("\n")
       return [eval(item) for item in log_list]
   def tags(self):
       """
      获取所有tag
      :return:
      """
       return [tag.name for tag in self.repo.tags]
   def change_to_branch(self, branch):
       """
      切换分值
      :param branch:
      :return:
      """
       self.repo.git.checkout(branch)
   def change_to_commit(self, branch, commit):
       """
      切换commit
      :param branch:
      :param commit:
      :return:
      """
       self.change_to_branch(branch=branch)
       self.repo.git.reset('--hard', commit)
   def change_to_tag(self, tag):
       """
      切换tag
      :param tag:
      :return:
      """
       self.repo.git.checkout(tag)
if __name__ == '__main__':
   local_path = os.path.join('codes', 'luffycity')
   repo = GitRepository(local_path, 'https://gitee.com/wupeiqi/fuck.git')
   branch_list = repo.branches()
   print(branch_list)
   repo.change_to_branch('dev')
   repo.pull()
git相关操作类

5.代码发布

  1. 创建名为deploy

  2. 创建web的程序

  3. settings.py

    from pathlib import Path
    import os
    # Application definition
    INSTALLED_APPS = [
       'django.contrib.admin',
       'django.contrib.auth',
       'django.contrib.contenttypes',
       'django.contrib.sessions',
       'django.contrib.messages',
       'django.contrib.staticfiles',
       "web.apps.WebConfig",
    ]
    TEMPLATES = [
      {
           'BACKEND': 'django.template.backends.django.DjangoTemplates',
           'DIRS': [os.path.join(BASE_DIR,"template")],
           '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',
              ],
          },
      },
    ]

5.1 django 服务器表管理

  1. web/models.py

    from django.db import models
    # Create your models here.
    class Server(models.Model):
       hostname = models.CharField(verbose_name="主机名",max_length=32)
       def __str__(self):
           return self.hostname
  2. web/views/server.py

    from django.shortcuts import render,redirect
    from web.forms.server import ServerModelForm
    from django.http import JsonResponse
    from web import models
    # Create your views here.
    def server_list(request):
       queryset = models.Server.objects.all()
       return render(request,"server_list.html",{"queryset":queryset})
           
    def server_add(request):
       if request.method == "GET":
           form = ServerModelForm()
           return render(request,"form.html",{"form":form})
       form = ServerModelForm(data=request.POST)
       if form.is_valid():
           #保存数据库
           form.save()
           return redirect("/server/list/")
       else:
           return render(request,"form.html",{"form":form})
    def server_edit(request,pk):
       server_object = models.Server.objects.filter(id=pk).first()
       if request.method == "GET":
           form = ServerModelForm(instance=server_object)
           return render(request,"form.html",{"form":form})
       form = ServerModelForm(data=request.POST,instance=server_object)
       if form.is_valid():
           form.save()
           return redirect("/server/list/")
       else:
           return render(request,"form.html",{"form":form})
    def server_del(request,pk):
       models.Server.objects.filter(id=pk).delete()
       return JsonResponse({"status":True})
  3. web/forms/base.py

    from django.forms import ModelForm
    class BootStrapModelForm(ModelForm):
       #不给谁家就装谁
       exclude_bootstrap = []
       def __init__(self,*args,**kwargs):
           #默认找不到也是去执行父类的__init__,这样写可以自己定义一些功能。
           #执行父类的__init__
           super().__init__(*args,**kwargs)
           #里面就是个字典,拿出指定的并添加插件
           #self.fields["hostname"].widget.attrs["class"] = "form-control"
           for k,v in self.fields.items():
               if k in self.exclude_bootstrap:
                   continue
               v.widget.attrs["class"] = "form-control"
  4. web/forms/server.py

    from .base import BootStrapModelForm
    from web import models
    # Create your views here.
    class ServerModelForm(BootStrapModelForm):
       #不给谁家就装谁
       exclude_bootstrap = []
       
       class Meta:
           model = models.Server
           fields = "__all__"
  5. web/templates/layout.html

    {% load static %}
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8"/>
    <title></title>
    <link rel="stylesheet" href="{% static 'plugins/bootstrap-3.4.1-dist/css/bootstrap.css' %} "/>
    {% block css %}{% endblock %}
    </head>
    <body>
    <nav class="navbar navbar-default">
     <div class="container-fluid">
    <!-- Brand and toggle get grouped for better mobile display -->
    <div class="navbar-header">
     <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
    <span class="sr-only">Toggle navigation</span>
    <span class="icon-bar"></span>
    <span class="icon-bar"></span>
    <span class="icon-bar"></span>
     </button>
     <a class="navbar-brand" href="#">信息发布系统</a>
    </div>
    <!-- Collect the nav links, forms, and other content for toggling -->
    <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
     <ul class="nav navbar-nav">
    <li class="active"><a href="/server/list/">服务器管理 <span class="sr-only">(current)</span></a></li>
    <li><a href="/project/list/">项目管理</a></li>
     </ul>
     <ul class="nav navbar-nav navbar-right">
    <li><a href="#">Link</a></li>
    <li class="dropdown">
     <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Dropdown <span class="caret"></span></a>
     <ul class="dropdown-menu">
    <li><a href="#">Action</a></li>
    <li><a href="#">Another action</a></li>
    <li><a href="#">Something else here</a></li>
    <li role="separator" class="divider"></li>
    <li><a href="#">Separated link</a></li>
     </ul>
    </li>
     </ul>
    </div><!-- /.navbar-collapse -->
     </div><!-- /.container-fluid -->
    </nav>
    {% block content %}{% endblock %}
    <script src="{% static 'js/jquery-3.3.1.min.js' %}"></script>
    {% block js %}{% endblock %}
    </body>
    </html>
  6. web/templates/server_list.html

    {% extends "layout.html" %}
    {% block content %}
    <a class="btn btn-default" href="/server/add/">添加</a>
    <table class="table">
    <thead>
    <tr>
    <th>id</th>
    <th>server_name</th>
    <th>功能</th>
    </tr>
    </thead>
    <tbody>
    {% for row in queryset%}
    <tr>
    <th>{{row.id}}</th>
    <th>{{row.hostname}}</th>
    <th>
    <a href="{% url 'server_edit' pk=row.id %}">编辑</a>
    <a onclick="removeRow(this,{{row.id}});">删除</a>
    </th>
    </tr>
    {% endfor %}
    </tbody>
    </table>
    {% endblock %}
    {% block js %}
    <script>
    function removeRow(ths,rid){
    var result = confirm("是否删除");
    if (result){
    $.ajax({
    url:"/server/del/"+rid+"/",
    type:"get",
    dataType:"JSON",
    success:function(res){
    if (res.status){
    $(ths).parent().parent().remove();
    }
    }
    });
    }
    }
    </script>
    {% endblock %}
  7. web/templates/form.html

    {% extends "layout.html" %}
    {% block content %}
    <form class="form-horizontal" method="post" novalidate>
    {% csrf_token %}
    {% for field in form %}
     <div class="form-group">
    <label for="inputEmail3" class="col-sm-2 control-label">{{field.label}}</label>
    <div class="col-sm-10">
    {{field}}
     <span>{{form.field.errors.0}}</span>
    </div>
     </div>
    {% endfor %}
     <div class="form-group">
    <div class="col-sm-offset-2 col-sm-10">
     <button type="submit" class="btn btn-default">提交</button>
    </div>
     </div>
    </form>
    {% endblock %}

5.2 django 项目表管理

  1. web/models.py

    class Project(models.Model):
       title = models.CharField(verbose_name="项目名",max_length=32)
       repo = models.CharField(verbose_name="仓库地址",max_length=128)
       env_choices = (
              ("prod","正式"),
              ("test","测试"),
          )
       env = models.CharField(verbose_name="环境",max_length=16,choices=env_choices,default="test")
       path = models.CharField(verbose_name="线上项目地址",max_length=128)
       server = models.ManyToManyField(verbose_name="关联服务器",to="Server")
  2. web/forms/project.py

    from .base import BootStrapModelForm
    from web import models
    # Create your views here.
    class ServerModelForm(BootStrapModelForm):
       #不给谁家就装谁
       exclude_bootstrap = []
       
       class Meta:
           model = models.Server
           fields = "__all__"
  3. web/views/project.py

    from django.shortcuts import render,redirect
    from django.http import JsonResponse
    from web.forms.project import ProjectModelForm
    from web import models
    # Create your views here.
    def project_list(request):
       queryset = models.Project.objects.all()
       return render(request,"project_list.html",{"queryset":queryset})
           
    def project_add(request):
       if request.method == "GET":
           form = ProjectModelForm()
           return render(request,"form.html",{"form":form})
       form = ProjectModelForm(data=request.POST)
       if form.is_valid():
           #保存数据库
           form.save()
           return redirect("/project/list/")
       else:
           return render(request,"form.html",{"form":form})
    def project_edit(request,pk):
       project_object = models.Project.objects.filter(id=pk).first()
       if request.method == "GET":
           form = ProjectModelForm(instance=project_object)
           return render(request,"form.html",{"form":form})
       form = ProjectModelForm(data=request.POST,instance=project_object)
       if form.is_valid():
           form.save()
           return redirect("/project/list/")
       else:
           return render(request,"form.html",{"form":form})
    def project_del(request,pk):
       models.Project.objects.filter(id=pk).delete()
       return JsonResponse({"status":True})
  4. web/templates/project_list.html

    {% extends "layout.html" %}
    {% block content %}
    <a class="btn btn-default" href="/project/add/">添加</a>
    <table class="table">
    <thead>
    <tr>
    <th>id</th>
    <th>title</th>
    <th>repo</th>
    <th>env</th>
    <th>上线地址</th>
    <th>nnn</th>
    <th>功能</th>
    </tr>
    </thead>
    <tbody>
    {% for row in queryset%}
    <tr>
    <th>{{row.id}}</th>
    <th>{{row.title}}</th>
    <th>{{row.repo}}</th>
    <th>{{row.get_env_display}}</th>
    <th>{{row.path}}</th>
    <th>
    {% for item in row.server.all %}
    <span style="border:1px solid #ddd;padding:5px;">{{item}}<span>
    {% endfor %}
    </th>
    <th>
    <a href="{% url 'project_edit' pk=row.id %}">编辑</a>
    <a onclick="removeRow(this,{{row.id}});">删除</a>
    </th>
    </tr>
    {% endfor %}
    </tbody>
    </table>
    {% endblock %}
    {% block js %}
    <script>
    function removeRow(ths,rid){
    var result = confirm("是否删除");
    if (result){
    $.ajax({
    url:"/project/del/"+rid+"/",
    type:"get",
    dataType:"JSON",
    success:function(res){
    if (res.status){
    $(ths).parent().parent().remove();
    }
    }
    });
    }
    }
    </script>
    {% endblock %}

5.3 django 发布任务管理

  1. deploy/settings.py

    #LANGUAGE_CODE = 'en-us'
    #错误信息默认是英文,改成他后就变成了中文
    LANGUAGE_CODE = 'zh-hans'
  2. deploy/urls.py

    from django.contrib import admin
    from django.urls import path,re_path
    from web.views import server
    from web.views import project
    from web.views import task
    urlpatterns = [
       re_path(r'^task/list/(?P<project_id>\d+)/$', task.task_list,name="task_list"),
       re_path(r'^task/add/(?P<project_id>\d+)/$', task.task_add,name="task_add"),
       re_path(r'^hook/template/(?P<tid>\d+)/$', task.hook_template,name="hook_template"),
    ]
  3. web/models.py

    class DeployTask(models.Model):
       """发布任务单"""
       uid = models.CharField(verbose_name="标识",max_length=64)
       project = models.ForeignKey(verbose_name="项目环境",to="Project",on_delete=models.CASCADE)
       tag = models.CharField(verbose_name="版本",max_length=32)
       status_choices = (
              (1,"待发布"),
              (2,"发布中"),
              (3,"成功"),
              (4,"失败"),
          )
       status = models.IntegerField(verbose_name="状态",choices=status_choices,default=1)
       before_download_seript = models.TextField(verbose_name="下载前脚本",null=True,blank=True)
       after_download_seript = models.TextField(verbose_name="下载后脚本",null=True,blank=True)
       before_deploy_seript = models.TextField(verbose_name="发布前脚本",null=True,blank=True)
       after_deploy_seript = models.TextField(verbose_name="发布后脚本",null=True,blank=True)
  4. web/forms/task.py

    from .base import BootStrapModelForm
    from django import forms
    from web import models
    import datetime
    # Create your views here.
    class TaskModelForm(BootStrapModelForm):
       #不给谁家就装谁
       exclude_bootstrap = ["before_download_template","after_download_template","before_deploy_template","after_deploy_template"]
       before_download_select = forms.ChoiceField(required=False,label="下载前")
       before_download_title = forms.CharField(required=False,label="模板名称")
       before_download_template = forms.BooleanField(required=False,widget=forms.CheckboxInput,label="是否保存为模板")
       after_download_select = forms.ChoiceField(required=False,label="下载后")
       after_download_title = forms.CharField(required=False,label="模板名称")
       after_download_template = forms.BooleanField(required=False,widget=forms.CheckboxInput,label="是否保存为模板")
       before_deploy_select = forms.ChoiceField(required=False,label="发布前")
       before_deploy_title = forms.CharField(required=False,label="模板名称")
       before_deploy_template = forms.BooleanField(required=False,widget=forms.CheckboxInput,label="是否保存为模板")
       after_deploy_select = forms.ChoiceField(required=False,label="发布后")
       after_deploy_title = forms.CharField(required=False,label="模板名称")
       after_deploy_template = forms.BooleanField(required=False,widget=forms.CheckboxInput,label="是否保存为模板")
       
       class Meta:
           model = models.DeployTask
           #fields = "__all__"
           #不掉出某个字段
           exclude = ["uid","project","status"]
       def __init__(self,project_object,*args,**kwargs):
           #你自己执行了init可是,它也需要执行,所以你要掉用一下。
           super().__init__(*args,**kwargs)
           self.project_object = project_object
           self.init_hook()
       def init_hook(self):
           #找到字段后给字段的某个属性赋值
           
           num={"2":"before_download_select",
                "4":"after_download_select",
                "6":"before_deploy_select",
                "8":"after_deploy_select"}
           for k,v in num.items():
               before_download = [(0,"请选择"),]
               before_download.extend(models.HookTemplate.objects.filter(hook_type=k).values_list("id","title"))
               self.fields[v].choices= before_download
       def save(self,commit=True):
           #指定字段并赋值
           self.instance.uid = self.create_uid()
           self.instance.project_id = self.project_object.id
           #和上面的意思差不多,你用了他的名字就要承担他的责任。
           super().save(commit)
           #钩子模板
           #从用户提交的里面取数据
           before_template1={
           "2":["before_download_template","before_download_title","before_download_seript"],
           "4":["after_download_template","after_download_title","after_download_seript"],
           "6":["before_deploy_template","before_deploy_title","before_deploy_seript"],
           "8":["after_deploy_template","after_deploy_title","after_deploy_seript"]}
           for k,v in before_template1.items():
               if self.cleaned_data.get(v[0]):
                   title = self.cleaned_data.get(v[1])
                   content = self.cleaned_data.get(v[2])
                   models.HookTemplate.objects.create(title=title,content=content,hook_type=k)
                   
       def create_uid(self):
           title = self.project_object.title
           env = self.project_object.env
           tag = self.cleaned_data.get("tag")
           date = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
           return "{0}-{1}-{2}-{3}".format(title,env,tag,date)
           
       def clean(self):
           #django提供了很多用于钩子的方法,会被自动调用
           template_not_null = [
          ["before_download_template","before_download_title"],
          ["after_download_template","after_download_title"],
          ["before_deploy_template","before_deploy_title"],
          ["after_deploy_template","after_deploy_title"]]
           for row in template_not_null:
               if self.cleaned_data.get(row[0]):
                   title=self.cleaned_data.get(row[1])
                   if not title:
                       self.add_error(row[1],"请输入模板名称")
  5. web/views/task.py

    from django.shortcuts import render,redirect
    from web.forms.task import TaskModelForm
    from django.http import JsonResponse
    from django.urls import reverse
    from web import models
    # Create your views here.
    def task_list(request,project_id):
       project_object = models.Project.objects.filter(id=project_id).first()
       task_list = models.DeployTask.objects.filter(project_id=project_id).all()
       return render(request,"task_list.html",{"project_object":project_object,"task_list":task_list})
           
    def task_add(request,project_id):
       project_object = models.Project.objects.filter(id=project_id).first()
       if request.method == "GET":
           form = TaskModelForm(project_object)
           return render(request,"task_form.html",{"form":form,"project_object":project_object})
       form = TaskModelForm(project_object,data=request.POST)
       if form.is_valid():
           form.save()
           #reverse("ist",kwargs={"id":id}))
           #如果反向想用url又想传参数就加上他reverse
           return redirect(reverse("task_list",kwargs={"project_id":project_id}))
       else:
           return render(request,"task_form.html",{"form":form,"project_object":project_object})
    def hook_template(request,tid):
       hook_template_object = models.HookTemplate.objects.filter(id=tid).first()
       return JsonResponse({"status":True,"content":hook_template_object.content})
  6. web/templates/task_list.html

    {% extends "layout.html" %}
    {% block content %}
    <h1>发布任务单:{{project_object.title}}-{{project_object.get_env_display}}</h1>
    <a class="btn btn-default" href="{% url 'task_add' project_id=project_object.id %}">添加</a>
    <table class="table">
    <thead>
    <tr>
    <th>id</th>
    <th>标识</th>
    <th>状态</th>
    <th>发布</th>
    <th>功能</th>
    </tr>
    </thead>
    <tbody>
    {% for row in task_list %}
    <tr>
    <th>{{row.id}}</th>
    <th>{{row.uid}}</th>
    <th>{{row.get_status_display}}</th>
    <th>
    <a href="{% url 'deploy' task_id=row.id %}">发布</a>
    </th>
    <th>
    <a href="{% url 'server_edit' pk=row.id %}">编辑</a>
    <a onclick="removeRow(this,{{row.id}});">删除</a>
    </th>
    </tr>
    {% endfor %}
    </tbody>
    </table>
    {% endblock %}
    {% block js %}
    <script>
    function removeRow(ths,rid){
    var result = confirm("是否删除");
    if (result){
    $.ajax({
    url:"/server/del/"+rid+"/",
    type:"get",
    dataType:"JSON",
    success:function(res){
    if (res.status){
    $(ths).parent().parent().remove();
    }
    }
    });
    }
    }
    </script>
    {% endblock %}
  7. web/templates/task_form.html

    {% extends "layout.html" %}
    {% block css %}
    <style>
    .video .series .module {
    line-height: 100px;
    vertical-align: middle;
    }
    .video .series .module .item .line {
    float: left;
    }
    .video .series .module .item .liner {
    float: right;
    }
    .video .series .module .item .line hr {
    margin-top: 49px
    }
    .video .series .module .item .icon {
    float: left;
    color: #dddddd;
    position: relative;
    }
    .video .series .module .item .icon .up, .video .series .module .item .icon .down {
    position: absolute;
    line-height: 49px;
    min-width: 90px;
    left: 0;
    text-align: center;
    margin-left: -38px;
    color: #337ab7;
    }
    .video .series .module .item:hover .icon, .video .series .module .item:hover .icon {
    color: green;
    }
    .video .series .module .item .icon .up {
    top: 0;
    }
    .video .series .module .item .icon .down {
    bottom: 0;
    }
    </style>
    {% endblock %}
    {% block content %}
    <div class="container">
    <table class="table table-bordered">
    <tbody>
    <tr>
    <td>项目名称:{{project_object.title}}</td>
    <td>环境:{{project_object.get_env_display}}</td>
    </tr>
    <tr><!--代表一个人占一行-->
    <td colspan="2">仓库地址:{{project_object.repo}}</td>
    </tr>
    <tr>
    <td colspan="2">上线地址:{{project_object.path}}</td>
    </tr>
    <tr>
    <td colspan="2">
    <ul>
    {% for item in project_object.server.all %}
    <li>{{item}}</li>
    {% endfor %}
    </ul>
    </td>
    </tr>
    </tbody>
    </table>
    <form method="post" novalidate>
    {% csrf_token %}
    <div class="panel panel-default">
     <div class="panel-heading">
    <h3 class="panel-title">基本配置</h3>
     </div>
     <div class="panel-body">
    <div class="form-group">
    <label for="inputEmail3" class="col-sm-1 control-label">{{form.tag.label}}</label>
    <div class="col-sm-10">
    {{form.tag}}
     <span>{{form.tag.errors.0}}</span>
    </div>
     </div>
     </div>
    </div>
    <div class="panel panel-default video">
    <div class="panel-heading"><i class="fa fa-list-ul" aria-hidden="true"></i> 发布流程和钩子</div>
    <div class="series">
    <div class="panel-body">
    <div class="module clearfix">
    <div>
    <div class="item left active">
    <div class="line" style="width: 60px;">
    <hr/>
    </div>
    <div class="icon">
    <span class="glyphicon glyphicon-record" aria-hidden="true"></span>
    <a class="down">01开始</a>
    </div>
    </div>
    <div class="item left">
    <div class="line" style="width: 60px;">
    <hr/>
    </div>
    <div class="icon">
    <span class="glyphicon glyphicon-record" aria-hidden="true"></span>
    <a class="up">02 下载前</a>
    </div>
    </div>
    <div class="item left">
    <div class="line" style="width: 60px;">
    <hr/>
    </div>
    <div class="icon">
    <span class="glyphicon glyphicon-record" aria-hidden="true"></span>
    <a class="down">03 下载代码</a>
    </div>
    </div>
    <div class="item left">
    <div class="line" style="width: 60px;">
    <hr/>
    </div>
    <div class="icon">
    <span class="glyphicon glyphicon-record" aria-hidden="true"></span>
    <a class="up">04 下载后</a>
    </div>
    </div>
    <div class="item left">
    <div class="line" style="width: 60px;">
    <hr/>
    </div>
    <div class="icon">
    <span class="glyphicon glyphicon-record" aria-hidden="true"></span>
    <a class="down">05 打包上传</a>
    </div>
    </div>
    <div class="item left">
    <div class="line" style="width: 60px;">
    <hr/>
    </div>
    <div class="icon">
    <span class="glyphicon glyphicon-record" aria-hidden="true"></span>
    <a class="up">06 发布前</a>
    </div>
    </div>
    <div class="item left">
    <div class="line" style="width: 60px;">
    <hr/>
    </div>
    <div class="icon">
    <span class="glyphicon glyphicon-record" aria-hidden="true"></span>
    <a class="down">07 发布</a>
    </div>
    </div>
    <div class="item left">
    <div class="line" style="width: 60px;">
    <hr/>
    </div>
    <div class="icon">
    <span class="glyphicon glyphicon-record" aria-hidden="true"></span>
    <a class="up">08 发布后</a>
    </div>
    <div class="line" style="width: 60px;">
    <hr/>
    </div>
    </div>
    </div>
    </div>
    <div class="hooks">
    <div class="col-md-6" >
    <div class="panel panel-default">
     <div class="panel-heading"><h3 class="panel-title">02 下载前</h3></div>
     <div class="panel-body form-horizontal">
    <div class="form-group">
    <div class="col-sm-12">
    {{form.before_download_select}}
    </div>
    </div>
    <div class="form-group">
    <div class="col-sm-12">
    {{form.before_download_seript}}
    </div><span>{{form.before_download_seript.errors.0}}</span>
    </div>
    <div class="form-group" style="height:80px;">
    <div class="col-sm-3">
    <div class="checkbox">
    <label>
    {{form.before_download_template}} 保存为模板
    </label>
    </div>
    </div>
    <div class="col-sm-9">
    {{form.before_download_title}}
    <span>{{form.before_download_title.errors.0}}</span>
    </div>
    </div>
     </div>
    </div>
    </div>
    <div class="col-md-6" >
    <div class="panel panel-default">
     <div class="panel-heading"><h3 class="panel-title">04 下载后</h3></div>
     <div class="panel-body form-horizontal">
    <div class="form-group">
    <div class="col-sm-12">
    {{form.after_download_select}}
    </div>
    </div>
    <div class="form-group">
    <div class="col-sm-12">
    {{form.after_download_seript}}
    </div><span>{{form.after_download_seript.errors.0}}</span>
    </div>
    <div class="form-group" style="height:80px;">
    <div class="col-sm-3">
    <div class="checkbox">
    <label>
    {{form.after_download_template}} 保存为模板
    </label>
    </div>
    </div>
    <div class="col-sm-9">
    {{form.after_download_title}}
    <span>{{form.after_download_title.errors.0}}</span>
    </div>
    </div>
     </div>
    </div>
    </div>
    <div class="col-md-6" >
    <div class="panel panel-default">
     <div class="panel-heading"><h3 class="panel-title">06 发布前</h3></div>
     <div class="panel-body form-horizontal">
    <div class="form-group">
    <div class="col-sm-12">
    {{form.before_deploy_select}}
    </div>
    </div>
    <div class="form-group">
    <div class="col-sm-12">
    {{form.before_deploy_seript}}
    </div><span>{{form.before_deploy_seript.errors.0}}</span>
    </div>
    <div class="form-group" style="height:80px;">
    <div class="col-sm-3">
    <div class="checkbox">
    <label>
    {{form.before_deploy_template}} 保存为模板
    </label>
    </div>
    </div>
    <div class="col-sm-9">
    {{form.before_deploy_title}}
    <span>{{form.before_deploy_title.errors.0}}</span>
    </div>
    </div>
     </div>
    </div>
    </div>
    <div class="col-md-6" >
    <div class="panel panel-default">
     <div class="panel-heading"><h3 class="panel-title">08 发布后</h3></div>
     <div class="panel-body form-horizontal">
    <div class="form-group">
    <div class="col-sm-12">
    {{form.after_deploy_select}}
    </div>
    </div>
    <div class="form-group">
    <div class="col-sm-12">
    {{form.after_deploy_seript}}
    </div><span>{{form.after_deploy_seript.errors.0}}</span>
    </div>
    <div class="form-group"style="height:80px;">
    <div class="col-sm-3">
    <div class="checkbox">
    <label>
    {{form.after_deploy_template}} 保存为模板
    </label>
    </div>
    </div>
    <div class="col-sm-9">
    {{form.after_deploy_title}}
    <span>{{form.after_deploy_title.errors.0}}</span>
    </div>
    </div>
     </div>
    </div>
    </div>
    <button type="submit" class="btn btn-default">提交</button>
    </div>
    </div>
    </div>
    </div>
    </form>
    </div>
    {% endblock %}
    {% block js %}
    <script>
    $(".hooks").find("select").change(function(){
    that = $(this);
    if ($(this).val() == 0){
    that.parent().parent().next().find("textarea").val(null);
    }else{
    $.ajax({
    url:"/hook/template/"+$(this).val()+"/",
    type:"GET",
    dataType:"JSON",
    success:function(res){
    that.parent().parent().next().find("textarea").val(res.content);
    }
    })
    }
    })
    </script>
    {% endblock %}

5.4 django + channels 发布/部署

  1. deploy/settings.py

    ASGI_APPLICATION = "deploy.routing.application"
    #使用channel layers 就需要加上他
    #帮你开辟群聊的模式
    CHANNEL_LAYERS = {
       "default":{
           "BACKEND":"channels.layers.InMemoryChannelLayer",
      }
       
    }
  2. deploy/routing.py

    from channels.routing import ProtocolTypeRouter, URLRouter
    from django.urls import re_path
    from web import consumers
    application = ProtocolTypeRouter({
  3. deploy/urls.py

    from django.contrib import admin
    from django.urls import path,re_path
    from web.views import server
    from web.views import project
    from web.views import task
    urlpatterns = [
       re_path(r'^task/list/(?P<project_id>\d+)/$', task.task_list,name="task_list"),
       re_path(r'^task/add/(?P<project_id>\d+)/$', task.task_add,name="task_add"),
       re_path(r'^hook/template/(?P<tid>\d+)/$', task.hook_template,name="hook_template"),
       re_path(r'^deploy/(?P<task_id>\d+)/$', task.deploy,name="deploy"),
    ]

     

  4. web/models.py

    class HookTemplate(models.Model):
       """钩子模板"""
       title = models.CharField(verbose_name="标题",max_length=32)
       content = models.TextField(verbose_name="脚本内容")
       hook_type_choices = ((2,"下载前"),(4,"下载后"),(6,"发布前"),(8,"发布后"))
       hook_type = models.IntegerField(verbose_name="狗子类型",choices=hook_type_choices)
  5. web/views/task.py

    from django.shortcuts import render,redirect
    from web.forms.task import TaskModelForm
    from django.http import JsonResponse
    from django.urls import reverse
    from web import models
    # Create your views here.
    def deploy(request,task_id):
       task_object = models.DeployTask.objects.filter(id=task_id).first()
       return render(request,"deploy.html",{"task_object":task_object})
  6. web/views/consumers.py

    from channels.generic.websocket import WebsocketConsumer
    from asgiref.sync import async_to_sync
    from channels.exceptions import StopConsumer
    from web import models
    import threading
    import json
    def create_node(task_object,task_id):
       db_node_object_list = models.Node.objects.filter(task_id=task_id)
       if db_node_object_list:
           return db_node_object_list
       node_object_list = []
       #创建节点,并返回给前端
       start_node = models.Node.objects.create(text="开始",task_id=task_id)
       node_object_list.append(start_node)
       #判断第一个钩子是否自定以脚本
       if task_object.before_download_seript:
           start_node = models.Node.objects.create(text="下载前",task_id=task_id,parent=start_node)
           node_object_list.append(start_node)
                       
       download_node = models.Node.objects.create(text="下载",task_id=task_id,parent=start_node)
       node_object_list.append(download_node)
                   
       if task_object.after_download_seript:
           download_node = models.Node.objects.create(text="下载后",task_id=task_id,parent=download_node)
           node_object_list.append(download_node)
                       
       upload_node = models.Node.objects.create(text="上传",task_id=task_id,parent=download_node)
       node_object_list.append(upload_node)
                   
       for server_object in task_object.project.server.all():
           server_node = models.Node.objects.create(text=server_object.hostname,task_id=task_id,parent=upload_node,server=server_object)
           node_object_list.append(server_node)
           if task_object.before_deploy_seript:
               server_node = models.Node.objects.create(text="发布前",task_id=task_id,parent=server_node,server=server_object)
               node_object_list.append(server_node)
           deploy_node = models.Node.objects.create(text="发布",task_id=task_id,parent=server_node,server=server_object)
           node_object_list.append(deploy_node)
           if task_object.after_deploy_seript:
               deploy_node = models.Node.objects.create(text="发布后",task_id=task_id,parent=deploy_node,server=server_object)
               node_object_list.append(deploy_node)
       return node_object_list
    def convert_object_to_gojs(node_object_list):
       node_list = []
       for node_object in node_object_list:
           temp = {"key":str(node_object.id),"text":node_object.text,"color":node_object.status,}
           if node_object.parent:
               temp["parent"] = str(node_object.parent_id)
           node_list.append(temp)
       return node_list
    class DeployConsumer(WebsocketConsumer):
       def websocket_connect(self, message):
           task_id = self.scope["url_route"]["kwargs"].get("task_id")
           self.accept()
           print("建立了链接")    
       def websocket_receive(self, message):
           #从url中获取值如果有关键字匹配用kwargs没有就用args,前面的时固定写法
           task_id = self.scope["url_route"]["kwargs"].get("task_id")
           txt = message["text"]
           task_object = models.DeployTask.objects.filter(id=task_id).first()
           if txt == "init":
               """
              node_list = [
                  {"key": "start", "text": '开始', "figure": 'Ellipse'},
                  {"key": "download", "parent": 'start', "text": '下载代码'},
                  {"key": "compile", "parent": 'download', "text": '本地编译'},
                  {"key": "zip", "parent": 'compile', "text": '打包'},
              ]
              """
               #实现数据的初始化,并判断初始化后不会再次初始化
               node_object_list = create_node(task_object,task_id)
               node_list = convert_object_to_gojs(node_object_list)
               self.send(text_data=json.dumps({"code":"init","data":node_list}))
       def xx_oo(self,event):
           #在这的点需要用下划线来代替
           #在9999房间xx.oo给每个人发message的数据
           message = event["message"]
           self.send(json.dumps(message))
       def websocket_disconnect(self, message):
           task_id = self.scope["url_route"]["kwargs"].get("task_id")
           print('客户端断开连接了')
           raise StopConsumer()
  7. web/templates/deploy.html

    {% extends "layout.html" %}
    {% block content %}
    <div class="container">
    <h1>channles发布</h1>
    <input type="button" class="btn btn-primary" value="初始化图表" onclick="createDiagram();" />
    <input type="button" class="btn btn-primary" value="代码发布"/>
    <table class="table table-bordered">
    <tbody>
    <tr>
    <td>项目名称:{{task_object.project.title}}</td>
    <td>环境:{{task_object.project.get_env_display}}</td>
    </tr>
    <tr>
    <td>版本:{{task_object.tag}}</td>
    <td>状态:{{task_object.get_status_display}}</td>
    </tr>
    <tr><!--代表一个人占一行-->
    <td colspan="2">仓库地址:{{task_object.project.repo}}</td>
    </tr>
    <tr>
    <td colspan="2">上线地址:{{task_object.project.path}}</td>
    </tr>
    <tr>
    <td colspan="2">
    <ul>
    {% for item in task_object.project.server.all %}
    <li>{{item}}</li>
    {% endfor %}
    </ul>
    </td>
    </tr>
    </tbody>
    </table>
    <div id="content" style="height:350px;background-color:#DAE4E4;"></div>
    </div>
    {% endblock %}
    {% block js %}
    <script>
    var diagram;
    var ws;
    $(function(){
    initWebSocket();
    initDiagram();
    });
    function initDiagram(){
    var $ = go.GraphObject.make;
    diagram = $(go.Diagram, "content", {
    //加上他会排开
    layout: $(go.TreeLayout, {
    angle: 0,
    nodeSpacing: 20,
    layerSpacing: 70
    })
    });
    diagram.nodeTemplate = $(go.Node, "Auto",
    $(go.Shape, {
    figure: "RoundedRectangle",
    fill: 'lightgray',
    stroke: 'lightgray'
    //new go.binding("figure","figure1") 回去指定的字典中找,找到就替换找不到就用默认写好的
    }, new go.Binding("figure", "figure"), new go.Binding("fill", "color"), new go.Binding("stroke", "color")),
    $(go.TextBlock, {margin: 8}, new go.Binding("text", "text"))
    );
    diagram.linkTemplate = $(go.Link,
    {routing: go.Link.Orthogonal},
    $(go.Shape, {stroke: 'lightgray'}, new go.Binding('stroke', 'link_color')),
    $(go.Shape, {toArrow: "OpenTriangle", stroke: 'lightgray'}, new go.Binding('stroke', 'link_color')),
    $(go.TextBlock, {font: '8pt serif', segmentOffset: new go.Point(0, -10)}, new go.Binding("text", "link_text"))
    );
    }
    function initWebSocket(){
    ws = new WebSocket("ws://127.0.0.1:8000/publish/{{task_object.id}}/");
    ws.onmessage = function(event){
    var result = JSON.parse(event.data);
    if (result.code === "init"){
    diagram.model = new go.TreeModel(result.data);
    }
    }
    }
    function createDiagram(){
    ws.send("init")
    }
    </script>
    {% endblock %}

5.5 默认显示钩子节点和channel-layers

  1. deploy/settings.py

    'websocket': URLRouter([
           re_path(r'^publish/(?P<task_id>\d+)/$', consumers.DeployConsumer.as_asgi()),
    ])
  2. web/models.py

    class Node(models.Model):
       task = models.ForeignKey(verbose_name="发布任务单",to="DeployTask",on_delete=models.CASCADE)
       text = models.CharField(verbose_name="节点文字",max_length=32)
       status_choices = [
          ("lightgrey","待发布"),
          ("green","成功"),
          ("red","失败"),
      ]
       status = models.CharField(verbose_name="状态",max_length=16,choices=status_choices,default="lightgrey")
       parent = models.ForeignKey(verbose_name="父节点",to="self",null=True,blank=True,on_delete=models.CASCADE)
       server = models.ForeignKey(verbose_name="服务器",to="Server",null=True,blank=True,on_delete=models.CASCADE)
  3. web/views/consumers.py

    from channels.generic.websocket import WebsocketConsumer
    from asgiref.sync import async_to_sync
    from channels.exceptions import StopConsumer
    from web import models
    import threading
    import json
    def create_node(task_object,task_id):
       db_node_object_list = models.Node.objects.filter(task_id=task_id)
       if db_node_object_list:
           return db_node_object_list
       node_object_list = []
       #创建节点,并返回给前端
       start_node = models.Node.objects.create(text="开始",task_id=task_id)
       node_object_list.append(start_node)
       #判断第一个钩子是否自定以脚本
       if task_object.before_download_seript:
           start_node = models.Node.objects.create(text="下载前",task_id=task_id,parent=start_node)
           node_object_list.append(start_node)
                       
       download_node = models.Node.objects.create(text="下载",task_id=task_id,parent=start_node)
       node_object_list.append(download_node)
                   
       if task_object.after_download_seript:
           download_node = models.Node.objects.create(text="下载后",task_id=task_id,parent=download_node)
           node_object_list.append(download_node)
                       
       upload_node = models.Node.objects.create(text="上传",task_id=task_id,parent=download_node)
       node_object_list.append(upload_node)
                   
       for server_object in task_object.project.server.all():
           server_node = models.Node.objects.create(text=server_object.hostname,task_id=task_id,parent=upload_node,server=server_object)
           node_object_list.append(server_node)
           if task_object.before_deploy_seript:
               server_node = models.Node.objects.create(text="发布前",task_id=task_id,parent=server_node,server=server_object)
               node_object_list.append(server_node)
           deploy_node = models.Node.objects.create(text="发布",task_id=task_id,parent=server_node,server=server_object)
           node_object_list.append(deploy_node)
           if task_object.after_deploy_seript:
               deploy_node = models.Node.objects.create(text="发布后",task_id=task_id,parent=deploy_node,server=server_object)
               node_object_list.append(deploy_node)
       return node_object_list
    def convert_object_to_gojs(node_object_list):
       node_list = []
       for node_object in node_object_list:
           temp = {"key":str(node_object.id),"text":node_object.text,"color":node_object.status,}
           if node_object.parent:
               temp["parent"] = str(node_object.parent_id)
           node_list.append(temp)
       return node_list
    class DeployConsumer(WebsocketConsumer):
       def websocket_connect(self, message):
           if txt == "init":
               #实现数据的初始化,并判断初始化后不会再次初始化
               node_object_list = create_node(task_object,task_id)
               
               node_list = convert_object_to_gojs(node_object_list)
       def websocket_disconnect(self, message):
           #断开连接时将他也提走
           async_to_sync(self.channel_layer.group_discard)(task_id,self.channel_name)

5.6 节点动态变化(channels的小别扭)

  1. web/views/consumers.py

    from channels.generic.websocket import WebsocketConsumer
    from asgiref.sync import async_to_sync
    from channels.exceptions import StopConsumer
    from web import models
    import threading
    import json
    class DeployConsumer(WebsocketConsumer):
       def deploy(self,task_object,task_id):
           #代码发布
           #找到数据库中的开始节点,给它变颜色
           start_node = models.Node.objects.filter(text="开始",task_id=task_id).first()
           start_node.status = "green"
           start_node.save()#将修改的数据保存到数据库中
           async_to_sync(self.channel_layer.group_send)(task_id,{"type":"xx.oo","message":{"code":"update","node_id":start_node.id,"color":"green"}})
               
           if task_object.before_download_seript:
               before_download_node = models.Node.objects.filter(text="下载前",task_id=task_id).first()
               before_download_node.status = "green"
               before_download_node.save()#将修改的数据保存到数据库中
               async_to_sync(self.channel_layer.group_send)(task_id,{"type":"xx.oo","message":{"code":"update","node_id":before_download_node.id,"color":"green"}})
           download_node = models.Node.objects.filter(text="下载",task_id=task_id).first()
           download_node.status = "green"
           download_node.save()#将修改的数据保存到数据库中
           async_to_sync(self.channel_layer.group_send)(task_id,{"type":"xx.oo","message":{"code":"update","node_id":download_node.id,"color":"green"}})
           if task_object.after_download_seript:
               after_download_node = models.Node.objects.filter(text="下载后",task_id=task_id).first()
               after_download_node.status = "green"
               after_download_node.save()#将修改的数据保存到数据库中
               async_to_sync(self.channel_layer.group_send)(task_id,{"type":"xx.oo","message":{"code":"update","node_id":after_download_node.id,"color":"green"}})
           upload_node = models.Node.objects.filter(text="上传",task_id=task_id).first()
           upload_node.status = "green"
           upload_node.save()#将修改的数据保存到数据库中
           async_to_sync(self.channel_layer.group_send)(task_id,{"type":"xx.oo","message":{"code":"update","node_id":upload_node.id,"color":"green"}})
           for server_object in task_object.project.server.all():
               server_node = models.Node.objects.filter(text=server_object.hostname,task_id=task_id,server=server_object).first()
               server_node.status = "green"
               server_node.save()#将修改的数据保存到数据库中
               async_to_sync(self.channel_layer.group_send)(task_id,{"type":"xx.oo","message":{"code":"update","node_id":server_node.id,"color":"green"}})
               if task_object.before_deploy_seript:
                   before_deploy_node = models.Node.objects.filter(text="发布前",task_id=task_id,server=server_object).first()
                   before_deploy_node.status = "green"
                   before_deploy_node.save()#将修改的数据保存到数据库中
                   async_to_sync(self.channel_layer.group_send)(task_id,{"type":"xx.oo","message":{"code":"update","node_id":before_deploy_node.id,"color":"green"}})
               deploy_node = models.Node.objects.filter(text="发布",task_id=task_id,server=server_object).first()
               deploy_node.status = "green"
               deploy_node.save()#将修改的数据保存到数据库中
               async_to_sync(self.channel_layer.group_send)(task_id,{"type":"xx.oo","message":{"code":"update","node_id":deploy_node.id,"color":"green"}})
               if task_object.after_deploy_seript:
                   after_deploy__node = models.Node.objects.filter(text="发布后",task_id=task_id,server=server_object).first()
                   after_deploy__node.status = "green"
                   after_deploy__node.save()#将修改的数据保存到数据库中
                   async_to_sync(self.channel_layer.group_send)(task_id,{"type":"xx.oo","message":{"code":"update","node_id":after_deploy__node.id,"color":"green"}})
       
       def websocket_connect(self, message):
       def websocket_receive(self, message):
           #从url中获取值如果有关键字匹配用kwargs没有就用args,前面的时固定写法
           task_id = self.scope["url_route"]["kwargs"].get("task_id")
           txt = message["text"]
           task_object = models.DeployTask.objects.filter(id=task_id).first()
           if txt == "deploy":
               #self.deploy(task_object,task_id)
               #原理
               #channels里面只有一个线程结果会保存到一个队列中,在线程执行过程中没有办法去拿数据。只能所有的执行完再去拿
               #你写了一个子线程,就办他实现了拿取数据。(不中却)
               
               #加上线程就会执行到那,前端就显示到哪(这就是小别扭)
               thread = threading.Thread(target=self.deploy,args=(task_object,task_id))
               thread.start()
       def websocket_disconnect(self, message):
  2. web/templates/deploy.html

    {% extends "layout.html" %}
    {% block content %}
    <div class="container">
    <h1>channles发布</h1>
    <input type="button" class="btn btn-primary" value="初始化图表" onclick="createDiagram();" />
    <input type="button" class="btn btn-primary" value="代码发布"  onclick="doDeploy();"/>
    </div>
    {% endblock %}
    {% block js %}
    <script>
    function initWebSocket(){
    ws = new WebSocket("ws://127.0.0.1:8000/publish/{{task_object.id}}/");
    ws.onmessage = function(event){
    var result = JSON.parse(event.data);
    if (result.code === "init"){
    console.log(result.data);
    diagram.model = new go.TreeModel(result.data);
    }else if(result.code === "update"){
    //要找到节点时我们通过后台传来的node_id来找,应为设置时KEY就等于node_id
    var node = diagram.model.findNodeDataForKey(result.node_id);
    diagram.model.setDataProperty(node,"color",result.color);
    }
    }
    }
    function doDeploy(){
    ws.send("deploy")
    }
    </script>
    {% endblock %}

5.7 实现代码发布的流程

  1. deploy/settings.py

    #自己指定一个发布系统的配置路劲,放在哪里都行
    DEPLOY_CODE_PATH = os.path.join(BASE_DIR,"codes")
    #存放要上传的路劲
    PACKAGE_PATH = os.path.join(BASE_DIR,"packages")
    #ssh端口,看公司要求
    SSH_PORT = 22
    #ssh用户名
    SSH_USER = "root"
    #上传的私钥路径
    PRIVATE_RSA_PATH = os.path.join(BASE_DIR,"id_rsa")
    #远程服务器的文件存储路劲
    SERVER_PACKAGE_PATH = "/data/packages"
  2. utils/repo.py

    import os
    os.environ["GIT_PYTHON_REFRESH"] = "quiet"
    from git.repo import Repo
    from git.repo.fun import is_git_dir
    class GitRepository(object):
       """
      git仓库管理
      """
       def __init__(self, local_path, repo_url, branch='master'):
           self.local_path = local_path
           self.repo_url = repo_url
           self.repo = None
           self.initial(repo_url, branch)
       def initial(self, repo_url, branch):
           """
          初始化git仓库
          :param repo_url:
          :param branch:
          :return:
          """
           if not os.path.exists(self.local_path):
               os.makedirs(self.local_path)
           git_local_path = os.path.join(self.local_path, '.git')
           if not is_git_dir(git_local_path):
               self.repo = Repo.clone_from(repo_url, to_path=self.local_path, branch=branch)
           else:
               self.repo = Repo(self.local_path)
       def pull(self):
           """
          从线上拉最新代码
          :return:
          """
           self.repo.git.pull()
       def branches(self):
           """
          获取所有分支
          :return:
          """
           branches = self.repo.remote().refs
           return [item.remote_head for item in branches if item.remote_head not in ['HEAD', ]]
       def commits(self):
           """
          获取所有提交记录
          :return:
          """
           commit_log = self.repo.git.log('--pretty={"commit":"%h","author":"%an","summary":"%s","date":"%cd"}',
                                          max_count=50,
                                          date='format:%Y-%m-%d %H:%M')
           log_list = commit_log.split("\n")
           return [eval(item) for item in log_list]
       def tags(self):
           """
          获取所有tag
          :return:
          """
           return [tag.name for tag in self.repo.tags]
       def change_to_branch(self, branch):
           """
          切换分值
          :param branch:
          :return:
          """
           self.repo.git.checkout(branch)
       def change_to_commit(self, branch, commit):
           """
          切换commit
          :param branch:
          :param commit:
          :return:
          """
           self.change_to_branch(branch=branch)
           self.repo.git.reset('--hard', commit)
       def change_to_tag(self, tag):
           """
          切换tag
          :param tag:
          :return:
          """
           self.repo.git.checkout(tag)
    if __name__ == '__main__':
       local_path = os.path.join('invincible-grass', 'day02')
       repo = GitRepository(local_path, 'https://gitee.com/wupeiqi/xxoo.git')
       branch_list = repo.branches()
       print(1111111111111111111111111111,branch_list)
       repo.change_to_branch('v1')
       repo.pull()
  3. utils/ssh.py

    import paramiko
    class SSHProxy(object):
       def __init__(self, hostname, port, username, private_key_path):
           self.hostname = hostname
           self.port = port
           self.username = username
           self.private_key_path = private_key_path
           self.transport = None
       def open(self):
           private_key = paramiko.RSAKey.from_private_key_file(self.private_key_path)
           self.transport = paramiko.Transport((self.hostname, self.port))
           self.transport.connect(username=self.username, pkey=private_key)
       def close(self):
           self.transport.close()
       def command(self, cmd):
           ssh = paramiko.SSHClient()
           ssh._transport = self.transport
           stdin, stdout, stderr = ssh.exec_command(cmd)
           result = stdout.read()
           #ssh.close()
           return result
       def upload(self, local_path, remote_path):
           sftp = paramiko.SFTPClient.from_transport(self.transport)
           sftp.put(local_path, remote_path)
           sftp.close()
       def __enter__(self):
           self.open()
           return self
       def __exit__(self, exc_type, exc_val, exc_tb):
           self.close()
    if __name__ == '__main__':
       with SSHProxy('10.211.55.25', 22, 'root', '/Users/wupeiqi/.ssh/id_rsa') as ssh:
           # v1 = ssh.command('sudo ifconfig')
           # print(v1)
           ssh.upload('your.tar', '/data/your.tar')
  4. web/consumers.py

    from channels.generic.websocket import WebsocketConsumer
    from channels.exceptions import StopConsumer
    from asgiref.sync import async_to_sync
    from utils.repo import GitRepository
    from django.conf import settings
    from utils.ssh import SSHProxy
    from web import models
    import subprocess
    import threading
    import shutil
    import json
    import os
    class DeployConsumer(WebsocketConsumer):
       def deploy(self,task_object,task_id):
           """
          为了处理脚本可以创建一个
          codes
          -project name
          --uid
          ---project folder
          ---scripts folder
          ----插件名(里面写插件)
          """
           #代码发布
           #找到数据库中的开始节点,给它变颜色
           start_node = models.Node.objects.filter(text="开始",task_id=task_id).first()
           start_node.status = "green"
           start_node.save()#将修改的数据保存到数据库中
           async_to_sync(self.channel_layer.group_send)(task_id,{"type":"xx.oo","message":{"code":"update","node_id":start_node.id,"color":"green"}})
           #创建脚本目录
           project_name = task_object.project.title
           uid = task_object.uid
           script_folder = os.path.join(settings.DEPLOY_CODE_PATH,project_name,uid,"scripts")
           project_folder = os.path.join(settings.DEPLOY_CODE_PATH,project_name,uid,project_name)
           #需要上传的压缩包路劲
           package_folder = os.path.join(settings.PACKAGE_PATH,project_name)
           #安全操作
           #exists判断此路径是否存在
           if not os.path.exists(script_folder):
               os.makedirs(script_folder)
           if not os.path.exists(project_folder):
               os.makedirs(project_folder)
           if not os.path.exists(package_folder):
               os.makedirs(package_folder)
           
           if task_object.before_download_seript:
               #第一步 将钩子的内容写入到本地脚本文件中
               #第二步 在本地执行这个脚本文件,如果成功 green;否则 red.
               #执行不成功会报错所以加一个异常处理接受错误
               status = "green"
               try:
                   script_name = "before_download_seript.py"
                   script_path = os.path.join(script_folder,script_name)
                   with open(script_path,mode="w",encoding="utf-8") as f:
                       f.write(task_object.before_download_seript)
                   #执行本地计算机的命令,shell是否可以有空格,cwd父路径
                   output = subprocess.check_output("python {0}".format(script_name),shell=True,cwd=script_folder)
               except Exception as e:
                   status = "red"
               before_download_node = models.Node.objects.filter(text="下载前",task_id=task_id).first()
               before_download_node.status = status
               before_download_node.save()#将修改的数据保存到数据库中
               async_to_sync(self.channel_layer.group_send)(task_id,{"type":"xx.oo","message":{"code":"update","node_id":before_download_node.id,"color":status}})
               #节点的向下执行和中断
               if status == "red":
                   return
           status = "green"
           print(task_object.tag)
           try:
               #git clont -b v1 http//xxx v1是要下载的版本号
               GitRepository(project_folder,task_object.project.repo,task_object.tag)
           
           except Exception as e:
               status = "red"
           download_node = models.Node.objects.filter(text="下载",task_id=task_id).first()
           download_node.status = status
           download_node.save()#将修改的数据保存到数据库中
           async_to_sync(self.channel_layer.group_send)(task_id,{"type":"xx.oo","message":{"code":"update","node_id":download_node.id,"color":status}})
           #节点的向下执行和中断
           if status == "red":
               return
           if task_object.after_download_seript:
               status = "green"
               try:
                   script_name = "after_download_seript.py"
                   script_path = os.path.join(script_folder,script_name)
                   with open(script_path,mode="w",encoding="utf-8") as f:
                       f.write(task_object.after_download_seript)
                   #执行本地计算机的命令,shell是否可以有空格,cwd父路径
                   output = subprocess.check_output("python {0}".format(script_name),shell=True,cwd=script_folder)
               except Exception as e:
                   status = "red"
                   
               after_download_node = models.Node.objects.filter(text="下载后",task_id=task_id).first()
               after_download_node.status = status
               after_download_node.save()#将修改的数据保存到数据库中
               async_to_sync(self.channel_layer.group_send)(task_id,{"type":"xx.oo","message":{"code":"update","node_id":after_download_node.id,"color":status}})
               if status == "red":
                   return
           upload_node = models.Node.objects.filter(text="上传",task_id=task_id).first()
           upload_node.status = "green"
           upload_node.save()#将修改的数据保存到数据库中
           async_to_sync(self.channel_layer.group_send)(task_id,{"type":"xx.oo","message":{"code":"update","node_id":upload_node.id,"color":"green"}})
           for server_object in task_object.project.server.all():
               status = "green"
               try:
                   #1.通过python代码对文件进行压缩
                   upload_folder_path = os.path.join(script_path)
                   #zip包的路劲
                   package_path = ret = shutil.make_archive(
                       base_name=os.path.join(script_path+".zip"),  # 压缩包文件路劲
                       format='zip',  # “zip”, “tar”
                       root_dir=upload_folder_path  # 被压缩的文件件
                  )
                   #2.上传代码
                   with SSHProxy(server_object.hostname,settings.SSH_PORT,settings.SSH_USER,settings.PRIVATE_RSA_PATH) as ssh:
                       #保存文件的路劲
                       remote_folder = os.path.join(settings.SERVER_PACKAGE_PATH,project_name)
                       #在服务器上创建路劲
                       ssh.command("mkdir -p {0}".format(remote_folder))
                       ssh.upload(package_path,os.path.join(remote_folder,uid+".zip"))
               except Exception as e:
                   status = "red"
               server_node = models.Node.objects.filter(text=server_object.hostname,task_id=task_id,server=server_object).first()
               server_node.status = status
               server_node.save()#将修改的数据保存到数据库中
               async_to_sync(self.channel_layer.group_send)(task_id,{"type":"xx.oo","message":{"code":"update","node_id":server_node.id,"color":status}})
               if status == "red":
                   continue
               
               if task_object.before_deploy_seript:
                   status = "green"
                   try:
                       script_name = "before_deploy_seript"
                       script_path = os.path.join(script_folder,script_name)
                       with open(script_path,mode="w",encoding="utf-8") as f:
                           f.write(task_object.after_download_seript)
                       #1.通过python代码对文件进行压缩
                       upload_folder_path = os.path.join(settings.DEPLOY_CODE_PATH,project_name,uid)
                       #zip包的路劲
                       package_path = ret = shutil.make_archive(
                           base_name=os.path.join(script_path+".zip"),  # 压缩包文件路劲
                           format='zip',  # “zip”, “tar”
                           root_dir=upload_folder_path  # 被压缩的文件件
                      )
                       #2.上传代码
                       with SSHProxy(server_object.hostname,settings.SSH_PORT,settings.SSH_USER,settings.PRIVATE_RSA_PATH) as ssh:
                           #保存文件的路劲
                           remote_folder = os.path.join(settings.SERVER_PACKAGE_PATH,project_name)
                           #在服务器上创建路劲
                           ssh.command("mkdir -p {0}".format(remote_folder))
                           ssh.upload(package_path,os.path.join(remote_folder,uid+".zip"))
                           shutil._unpack_zipfile(package_path,os.path.join(remote_folder,uid+".zip", remote_folder)
                           ssh.command("python {0}{2}".format(remote_folder,uid+".zip"))
                   except Exception as e:
                       status = "red"
                       
                   before_deploy_node = models.Node.objects.filter(text="发布前",task_id=task_id,server=server_object).first()
                   before_deploy_node.status = status
                   before_deploy_node.save()#将修改的数据保存到数据库中
                   async_to_sync(self.channel_layer.group_send)(task_id,{"type":"xx.oo","message":{"code":"update","node_id":before_deploy_node.id,"color":status}})
                   if status == "red":
                       continue
               deploy_node = models.Node.objects.filter(text="发布",task_id=task_id,server=server_object).first()
               deploy_node.status = "green"
               deploy_node.save()#将修改的数据保存到数据库中
               async_to_sync(self.channel_layer.group_send)(task_id,{"type":"xx.oo","message":{"code":"update","node_id":deploy_node.id,"color":"green"}})
               if task_object.after_deploy_seript:
                   status = "green"
                   try:
                       script_name = "after_deploy_seript"
                       script_path = os.path.join(script_folder,script_name)
                       with open(script_path,mode="w",encoding="utf-8") as f:
                           f.write(task_object.after_download_seript)
                       #1.通过python代码对文件进行压缩
                       upload_folder_path = os.path.join(settings.DEPLOY_CODE_PATH,project_name,uid)
                       #zip包的路劲
                       package_path = ret = shutil.make_archive(
                           base_name=os.path.join(script_path+".zip"),  # 压缩包文件路劲
                           format='zip',  # “zip”, “tar”
                           root_dir=upload_folder_path  # 被压缩的文件件
                      )
                       #2.上传代码
                       with SSHProxy(server_object.hostname,settings.SSH_PORT,settings.SSH_USER,settings.PRIVATE_RSA_PATH) as ssh:
                           #保存文件的路劲
                           remote_folder = os.path.join(settings.SERVER_PACKAGE_PATH,project_name)
                           #在服务器上创建路劲
                           ssh.command("mkdir -p {0}".format(remote_folder))
                           ssh.upload(package_path,os.path.join(remote_folder,uid+".zip"))
                           shutil._unpack_zipfile(package_path,os.path.join(remote_folder,uid+".zip", remote_folder)
                           ssh.command("python {0}{2}".format(remote_folder,uid+".zip"))
                   except Exception as e:
                       status = "red"
                       
                   after_deploy__node = models.Node.objects.filter(text="发布后",task_id=task_id,server=server_object).first()
                   after_deploy__node.status = status
                   after_deploy__node.save()#将修改的数据保存到数据库中
                   async_to_sync(self.channel_layer.group_send)(task_id,{"type":"xx.oo","message":{"code":"update","node_id":after_deploy__node.id,"color":status}})
                   if status == "red":
                       continue
       
       def websocket_connect(self, message):
           
       def websocket_receive(self, message):
       def websocket_disconnect(self, message):

     

6.补充

1.解压和压缩文件

1.1 shutil

import shutil
# 文件压缩
"""
ret = shutil.make_archive(
  base_name="code/www", # 压缩包文件路劲
  format='zip', # “zip”, “tar”
  root_dir='code/fuck' # 被压缩的文件件
)
print(ret)
"""
# 解压文件
"""
shutil._unpack_zipfile('code/www.zip', 'code/new')
shutil._unpack_tarfile('code/www.tar', 'code/new')
"""

1.2 zipfile

import zipfile
# 压缩
z = zipfile.ZipFile('laxi.zip', 'w')
z.write('a.log')
z.write('data.data')
z.close()
# 解压
z = zipfile.ZipFile('laxi.zip', 'r')
z.extractall()
z.close()

1.3 tarfile

import tarfile
# 压缩
tar = tarfile.open('your.tar', 'w')
tar.add('utils/codes/luffycity/a1.py')
tar.add('utils/codes/luffycity/a3.py')
tar.close()
# 解压
tar = tarfile.TarFile('code/www.tar', 'r')
tar.extractall(path='/code/x1/')  # 可设置解压地址
tar.close()

2.执行本地命令

import subprocess
result = subprocess.check_output('ls -l', cwd='/Users/wupeiqi/PycharmProjects', shell=True)
print(result.decode('utf-8'), type(result))

3.执行服务器的命令

#!/usr/bin/env python
# -*- coding:utf-8 -*-
"""
SaltAPI推送文件
"""
# #### 基于SSH:API ####
"""
from salt.client.ssh.client import SSHClient
client = SSHClient()
# 执行命令
# result = client.cmd('*', 'cmd.run', ('ls',))
# 调用grains
# ret = client.cmd('*','grains.items')
# 调用pillar
# ret = client.cmd('*','pillar.items')
# 执行 state
# ret = client.cmd('*','state.sls',('fengfeng','pillar={"xxxx":"luffy"}'))
# 发送文件
# ret = client.cmd('*','cp.get_file',('salt://fengfeng/files/test.conf','/data/s1.conf'))
# 发送文件
# ret = client.cmd('*','cp.get_url',('http://www.pythonav.com/allstatic/imgs/mv/picture/2.jpeg','/data/s1.jpeg'))
"""
# #### 基于Master:API ####
"""
import salt.client
local = salt.client.LocalClient()
# 执行命令
# result = client.cmd('*', 'cmd.run', ('ls',))
# 调用grains
# ret = client.cmd('*','grains.items')
# 调用pillar
# ret = client.cmd('*','pillar.items')
# 执行 state
# ret = client.cmd('*','state.sls',('fengfeng','pillar={"xxxx":"luffy"}'))
# 发送文件
# ret = client.cmd('*','cp.get_file',('salt://fengfeng/files/test.conf','/data/s1.conf'))
# 发送文件
# ret = client.cmd('*','cp.get_url',('http://www.pythonav.com/allstatic/imgs/mv/picture/2.jpeg','/data/s1.jpeg'))
"""

 

n.面试问题

  1. websocket是什么?

  2. websocket的实现机制?

  3. 都将哦如何用websocket?

  4. websocket协议个http协议的区别?

  5. 轮询和长轮询的区别?

  6. gojs和paramiko和gitpython

  7. ajax和队列是什么?

  8. with的用法?

CRM项目

1.权限系统

1.1问题

  • 问:为什么程序需要权限控制?

    答:生活中的权限限制,① 看灾难片电影《2012》中富人和权贵有权登上诺亚方舟,穷苦老百姓只有等着灾难的来临;② 屌丝们,有没有想过为什么那些长得漂亮身材好的姑娘在你身边不存在呢?因为有钱人和漂亮姑娘都是珍贵稀有的,稀有的人在一起玩耍和解锁各种姿势。而你,无权拥有他们,只能自己玩自己了。 程序开发时的权限控制,对于不同用户使用系统时候就应该有不同的功能,如:

    • 普通员工、部门主管、总监、总裁

    所以,只要有不同角色的人员来使用系统,那么就肯定需要权限系统。

  • 问:为什么要开发权限组件?

    答:假设你今年25岁,从今天开始写代码到80岁,每年写5个项目,那么你的一生就会写275个项目,保守估计其中应该有150+个都需要用到权限控制,为了以后不再重复的写代码,所以就开发一个权限组件以便之后55年的岁月中使用。 亲,不要太较真哦,你觉得程序员能到80岁么,哈哈哈哈哈哈哈 偷偷告诉你:老程序员开发速度快,其中一个原因是经验丰富,另外一个就是他自己保留了很多组件,新系统开发时,只需把组件拼凑起来基本就可以完成。

  • 问:web开发中权限指的是什么?

    答:web程序是通过 url 的切换来查看不同的页面(功能),所以权限指的其实就是URL,对url控制就是对权限的控制。

    • 结论:一个人有多少个权限就取决于他有多少个URL的访问权限。

  1. 表的划分

    • 用户表:id,name

    • 角色表:id,title

    • 用户角色关系表:id,userid,角色id

    • 权限表:id,url

    • 角色权限关系表:id,角色id,权限id

1.2 adimin

  • 创建用户和使用

    #创建admin user
    python manage.py createsuperuser
    #adimin中放置表
    from django.contrib import admin
    from app01 import models
    class aa(admin.ModelAdmin):
       #显示
       list_display = ["title","url"]
       #修改
       list_editable = ["url"]
    #将表在django自带的管理中显示出来。
    #aa代表可以显示title和url
    admin.site.register(models.表名,aa)
  • rbac/admin.py

    from django.contrib import admin
    # Register your models here.
    from rbac import models
    class PermissionAdmin(admin.ModelAdmin):
       list_display = ["title","url"]
       list_editable = ["url"]
       
    #将表在django自带的管理中显示出来。
    admin.site.register(models.Permission,PermissionAdmin)
    admin.site.register(models.Role)
    admin.site.register(models.UserInfo)

     

1.3 基本配置

  • mysite

    • __ init __.py

      import pymysql
      pymysql.install_as_MySQLdb()
    • settings.py

      from pathlib import Path
      import os
      INSTALLED_APPS = [
         'django.contrib.admin',
         'django.contrib.auth',
         'django.contrib.contenttypes',
         'django.contrib.sessions',
         'django.contrib.messages',
         'django.contrib.staticfiles',
         "rbac.apps.RbacConfig",
         "web.apps.WebConfig",
         #注册app
      ]
      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',
      ]
      TEMPLATES = [
        {
             'BACKEND': 'django.template.backends.django.DjangoTemplates',
             'DIRS': [os.path.join("%s/%s" %(BASE_DIR,"rbac"),"template"),
                      os.path.join("%s/%s" %(BASE_DIR,"web"),"template")],
             #写模板的路劲
             '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 = 'mysite.wsgi.application'
      # Database
      # https://docs.djangoproject.com/en/4.0/ref/settings/#databases
      DATABASES = {
         'default': {
             'ENGINE': 'django.db.backends.sqlite3',
             'NAME': BASE_DIR / 'db.sqlite3',
        }
      }
      #配置数据库
      DATABASES = {
      "default":{
             "ENGINE":"django.db.backends.mysql",
             "NAME":"CRM",
            "USER":"root",
             "PASSWORD":"1451964253",
             "HOST":"localhost",
             "PORT":3306,
        }  
      }
      # Static files (CSS, JavaScript, Images)
      # https://docs.djangoproject.com/en/4.0/howto/static-files/
      STATIC_URL = 'static/'
      STATICFILES_DIRS =os.path.join(BASE_DIR,"static"),
      # Default primary key field type
      # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
      DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
      from django.core.files.uploadhandler import MemoryFileUploadHandler
      from django.core.files.uploadhandler import TemporaryFileUploadHandler
      # List of upload handler classes to be applied in order.
      FILE_UPLOAD_HANDLERS = [
         'django.core.files.uploadhandler.MemoryFileUploadHandler',
         'django.core.files.uploadhandler.TemporaryFileUploadHandler',
      ]
      # Maximum size, in bytes, of a request before it will be streamed to the
      # file system instead of into memory.
      # 允许内存中上传文件的大小
      #   合法:InMemoryUploadedFile对象(写在内存)         -> 上传文件小于等于 FILE_UPLOAD_MAX_MEMORY_SIZE
      # 不合法:TemporaryUploadedFile对象(写在临时文件)     -> 上传文件大于   FILE_UPLOAD_MAX_MEMORY_SIZE 且 小于 DATA_UPLOAD_MAX_MEMORY_SIZE
      FILE_UPLOAD_MAX_MEMORY_SIZE = 2621440  # i.e. 2.5 MB
      # Maximum size in bytes of request data (excluding file uploads) that will be
      # read before a SuspiciousOperation (RequestDataTooBig) is raised.
      # 允许上传内容的大小(包含文件和其他请求内容)
      DATA_UPLOAD_MAX_MEMORY_SIZE = 2621440  # i.e. 2.5 MB
      # Maximum number of GET/POST parameters that will be read before a
      # SuspiciousOperation (TooManyFieldsSent) is raised.
      # 允许的上传文件数
      DATA_UPLOAD_MAX_NUMBER_FIELDS = 1000
      # Directory in which upload streamed files will be temporarily saved. A value of
      # `None` will make Django use the operating system's default temporary directory
      # (i.e. "/tmp" on *nix systems).
      # 临时文件夹路径
      FILE_UPLOAD_TEMP_DIR = None
      # The numeric mode to set newly-uploaded files to. The value should be a mode
      # you'd pass directly to os.chmod; see https://docs.python.org/3/library/os.html#files-and-directories.
      # 文件权限
      FILE_UPLOAD_PERMISSIONS = None
      # The numeric mode to assign to newly-created directories, when uploading files.
      # The value should be a mode as you'd pass to os.chmod;
      # see https://docs.python.org/3/library/os.html#files-and-directories.
      # 文件夹权限
      FILE_UPLOAD_DIRECTORY_PERMISSIONS = None

       

    • urls.py

      from django.contrib import admin
      from django.urls import path,re_path
      from django.conf.urls import include
      urlpatterns = [
         path('admin/', admin.site.urls),
         re_path("^",include("web.urls")),
         
      ]
  • rbac

    • models.py

      from django.db import models
      # Create your models here.
      class Permission(models.Model):
         """
        权限表
        """
         title = models.CharField(verbose_name='标题', max_length=32)
         url = models.CharField(verbose_name='含正则的URL', max_length=128)
         #为了区分是否是菜单
         is_menu = models.BooleanField(verbose_name="是否可做菜单",default=False)
         icon = models.CharField(max_length=32,null=True,blank=True)
         def __str__(self):
             return self.title
      class Role(models.Model):
         """
        角色
        """
         title = models.CharField(verbose_name='角色名称', max_length=32)
         permissions = models.ManyToManyField(verbose_name='拥有的所有权限', to='Permission', blank=True)
         def __str__(self):
             return self.title
      class UserInfo(models.Model):
         """
        用户表
        """
         name = models.CharField(verbose_name='用户名', max_length=32)
         password = models.CharField(verbose_name='密码', max_length=64)
         email = models.CharField(verbose_name='邮箱', max_length=32)
         roles = models.ManyToManyField(verbose_name='拥有的所有角色', to='Role', blank=True)
         def __str__(self):
             return self.name
    • views.py

    • static

      • css

      • js

      • images

      • plugins

      • rbac

        • rabc.css

  • web

    • models.py

      from django.db import models
      # Create your models here.
      class Customer(models.Model):
         """
        客户表
        """
         name = models.CharField(verbose_name='姓名', max_length=32)
         age = models.CharField(verbose_name='年龄', max_length=32)
         email = models.EmailField(verbose_name='邮箱', max_length=32)
         company = models.CharField(verbose_name='公司', max_length=32)
      class Payment(models.Model):
         """
        付费记录
        """
         customer = models.ForeignKey(verbose_name='关联客户', to='Customer',on_delete=models.CASCADE)
         money = models.IntegerField(verbose_name='付费金额')
         create_time = models.DateTimeField(verbose_name='付费时间', auto_now_add=True)
    • views

      • customer.py

        import os
        import mimetypes
        from django.shortcuts import render, redirect
        from django.http import FileResponse
        from django.conf import settings
        import xlrd
        from web import models
        from web.forms.customer import CustomerForm
        def customer_list(request):
           """
          客户列表
          :return:
          """
           data_list = models.Customer.objects.all()
           return render(request, 'customer_list.html', {'data_list': data_list})
        def customer_add(request):
           """
          编辑客户
          :return:
          """
           if request.method == 'GET':
               form = CustomerForm()
               return render(request, 'customer_edit.html', {'form': form})
           form = CustomerForm(data=request.POST)
           if form.is_valid():
               form.save()
               return redirect('/customer/list/')
           return render(request, 'customer_edit.html', {'form': form})
        def customer_edit(request, cid):
           """
          新增客户
          :return:
          """
           obj = models.Customer.objects.get(id=cid)
           if request.method == 'GET':
               form = CustomerForm(instance=obj)
               return render(request, 'customer_add.html', {'form': form})
           form = CustomerForm(data=request.POST, instance=obj)
           if form.is_valid():
               form.save()
               return redirect('/customer/list/')
           return render(request, 'customer_add.html', {'form': form})
        def customer_del(request, cid):
           """
          删除客户
          :param request:
          :param cid:
          :return:
          """
           models.Customer.objects.filter(id=cid).delete()
           return redirect('/customer/list/')
        def customer_import(request):
           """
          批量导入
          :param request:
          :return:
          """
           if request.method == 'GET':
               return render(request, 'customer_import.html')
           context = {'status': True, 'msg': '导入成功'}
           try:
               customer_excel = request.FILES.get('customer_excel')
               """
              打开上传的Excel文件,并读取内容
              注:打开本地文件时,可以使用:workbook = xlrd.open_workbook(filename='本地文件路径.xlsx')
              """
               workbook = xlrd.open_workbook(file_contents=customer_excel.file.read())
               # sheet = workbook.sheet_by_name('工作表1')
               sheet = workbook.sheet_by_index(0)
               row_map = {
                   0: {'text': '客户姓名', 'name': 'name'},
                   1: {'text': '年龄', 'name': 'age'},
                   2: {'text': '邮箱', 'name': 'email'},
                   3: {'text': '公司', 'name': 'company'},
              }
               object_list = []
               for row_num in range(1, sheet.nrows):
                   row = sheet.row(row_num)
                   row_dict = {}
                   for col_num, name_text in row_map.items():
                       row_dict[name_text['name']] = row[col_num].value
                   object_list.append(models.Customer(**row_dict))
               models.Customer.objects.bulk_create(object_list, batch_size=20)
           except Exception as e:
               context['status'] = False
               context['msg'] = '导入失败'
           return render(request, 'customer_import.html', context)
        def customer_tpl(request):
           """
          下载批量导入Excel列表
          :param request:
          :return:
          """
           tpl_path = os.path.join(settings.BASE_DIR, 'web', 'files', '批量导入客户模板.xlsx')
           content_type = mimetypes.guess_type(tpl_path)[0]
           print(content_type)
           response = FileResponse(open(tpl_path, mode='rb'), content_type=content_type)
           response['Content-Disposition'] = "attachment;filename=%s" % 'customer_excel_tpl.xlsx'
           return response
      • payment.py

        #!/usr/bin/env python
        # -*- coding:utf-8 -*-
        from django.shortcuts import render, redirect
        from web import models
        from web.forms.payment import PaymentForm, PaymentUserForm
        def payment_list(request):
           """
          付费列表
          :return:
          """
           data_list = models.Payment.objects.all()
           return render(request, 'payment_list.html', {'data_list': data_list})
        def payment_add(request):
           """
          编辑付费记录
          :return:
          """
           if request.method == 'GET':
               form = PaymentForm()
               return render(request, 'payment_edit.html', {'form': form})
           form = PaymentForm(data=request.POST)
           if form.is_valid():
               form.save()
               return redirect('/payment/list/')
           return render(request, 'payment_edit.html', {'form': form})
        def payment_edit(request, pid):
           """
          新增付费记录
          :return:
          """
           obj = models.Payment.objects.get(id=pid)
           if request.method == 'GET':
               form = PaymentForm(instance=obj)
               return render(request, 'payment_add.html', {'form': form})
           form = PaymentForm(data=request.POST, instance=obj)
           if form.is_valid():
               form.save()
               return redirect('/payment/list/')
           return render(request, 'payment_add.html', {'form': form})
        def payment_del(request, pid):
           """
          删除付费记录
          :param request:
          :param cid:
          :return:
          """
           models.Payment.objects.filter(id=pid).delete()
           return redirect('/payment/list/')

         

    • urls.py

      from django.contrib import admin
      from django.urls import path,re_path
      from web.views import customer
      from web.views import payment
      from web.views import account
      urlpatterns = [
         re_path("login/",account.login),
         re_path(r'^customer/list/$', customer.customer_list),
         re_path(r'^customer/add/$', customer.customer_add),
         re_path(r'^customer/edit/(?P<cid>\d+)/$', customer.customer_edit),
         re_path(r'^customer/del/(?P<cid>\d+)/$', customer.customer_del),
         re_path(r'^customer/import/$', customer.customer_import),
         re_path(r'^customer/tpl/$', customer.customer_tpl),
         re_path(r'^payment/list/$', payment.payment_list),
         re_path(r'^payment/add/$', payment.payment_add),
         re_path(r'^payment/edit/(?P<pid>\d+)/$', payment.payment_edit),
         re_path(r'^payment/del/(?P<pid>\d+)/$', payment.payment_del),
         
      ]
      """
      客户列表:/customer/list/
      添加客户:/customer/add/
      删除客户:/customer/del/(?P<cid>\d+)/
      修改客户:/customer/edit/(?P<cid>\d+)/
      批量导入:/customer/import/
      下载模板:/customer/tpl/
      账单管理
      账单列表:/payment/list/
      添加账单:/payment/add/
      删除账单:/payment/del/(?P<pid>\d+)/
      修改账单:/payment/edit/<?P<pid>\d+/
      """

1.4 登录和添加session

  • mysite/settings.py

    #自己配置的变量名为了以后方便操作。
    PERMISSION_SESSION_KEY = "permission_list"
  • web/views/account.py

    from django.shortcuts import render,redirect,HttpResponse
    from rbac.service.init_permission import init_permission
    from django.urls import reverse
    from rbac import models
    from django.forms import Form
    from django.forms import fields
    from django.conf import settings
    #引入django的配置文件
    class LoginForm(Form):
       name =  fields.CharField(max_length="32")
       password = fields.CharField(max_length="32")
    def login(request):
       if request.method == "GET":
           obj=LoginForm()
           return render(request,"login.html",{"obj":obj})
       obj = LoginForm(request.POST)
       if not obj.is_valid():
           return render(request,"login.html",{"obj":obj})
       cls = models.UserInfo.objects.filter(**obj.cleaned_data).first()
       if cls:
           request.session["user_info"]=obj.cleaned_data
           init_permission(request,cls)
           return redirect("/customer/list/")
       return render(request,"login.html",{"obj":obj})
           
    # Create your views here.
    """
    class JsonResponse:
      def __init__(self,req,status,msg):
          self.req = req
          self.status = status
          self.msg = msg
      def render(self):
          import json
          ret = {
              "status":self.status,
              "msg":self.msg
          }
          return HttpResponse(json.dumps(ret))
    """
    #return JsonResponse(request,True,"错误信息")
  • rbac/service/init_permission.py

    from django.conf import settings
    def init_permission(request,user):
    #权限和菜单信息初始化,以后使用时,需要在登陆成功后调用该方法将权限和菜单信息放入session。
    #拿到Userinfo表中roles字段与当前用户关联的角色id和title
    #__isnull等于True是允许为空,反之不允许。
    #distinct去重
    #cls.roles.all().filter(permissions__url__isnull=False).values("id","title","permissions__url")
    permission_queryset = user.roles.all().filter(permissions__url__isnull=False).values("permissions__url","permissions__title","permissions__is_menu","permissions__icon").distinct()
    menu_list = []
    permission_list = []
    for row in permission_queryset:
    permission_list.append({"permissions_url":row["permissions__url"]})
    if row["permissions__is_menu"]:
    menu_list.append({"title":row["permissions__title"],"is_menu":row["permissions__is_menu"],"icon":row["permissions__icon"],"url":row["permissions__url"]})
    request.session[settings.PERMISSION_SESSION_KEY]=permission_list
    request.session[settings.MENU_SESSION_KEY]=menu_list
    #从数据库取出无法直接序列化转成python才可以。
    #request.session[settings.PERMISSION_SESSION_KEY]=list(permission_list)
    #models.UserInfo.objects.create(**obj.cleaned_data)

1.5 添加中间件

  • mysite/settings.py

    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',
       "rbac.middleware.rbac.RbacMiddleware"
       #注册中间件
    ]
    #自己配置的变量名为了以后方便操作。
    MENU_SESSION_KEY = "menu_list"
    #白名单
    VALID_URL = [
       "^/login/$",
       "^/admin/.*"
    ]
  • rbac/middleware/rbac.py

    from django.utils.deprecation import MiddlewareMixin
    from django.conf import settings
    from django.shortcuts import redirect,HttpResponse
    import re
    class RbacMiddleware(MiddlewareMixin):
       """
      权限控制中间件
      """
       def process_request(self,request):
           #1.获取当前请求url        
           current_url = request.path_info
           #2.白名单处理
           for reg in settings.VALID_URL:
               if re.match(reg,current_url):
                   return None
                   
           #3.获取当前用户session中所有权限
           permission_list = request.session.get(settings.PERMISSION_SESSION_KEY)
           if not permission_list:
               return redirect("/login/")
           
           #4.进行权限校验
           flag = False
           for item in permission_list:
               reg = "^%s$"% item.get("permissions_url")
               if re.match(reg,current_url):
                   flag = True
                   break
           if not flag:
               return HttpResponse("无权访问")

1.6 编写一级菜单

  • rbac/template/rbac/menu.html

    <div class="static-menu">
    {% for item in menu_list %}
    <a href="{{item.url}}" class="{{item.class}}">
    <span class="icon-wrap"><i class="fa {{item.icon}}"></i></span>{{item.title}}</a>
    {% endfor %}
    </div>
  • rbac/templatetags/rbac.py

    from django.template import Library
    from django.conf import settings
    import re
    register = Library()
    #先去拿模板,然后返回到调用的模板中。
    @register.inclusion_tag("rbac/menu.html")
    def menu(request):
       menu_list = request.session.get(settings.MENU_SESSION_KEY)
       #默认选中
       for item in menu_list:
           reg = "^%s$" % item["url"]
           if re.match(reg,request.path_info):
               item["class"] = "active"
       return {"menu_list":menu_list}

1.7 编写二级菜单

  • rbac/models.py

    from django.db import models
    # Create your models here.
    class Menu(models.Model):
       """
      菜单表
      """
       #需要创建唯一索引
       title = models.CharField(max_length=32,unique=True)
       icon = models.CharField(max_length=32)
       #显示文字,不加的话是对象
       def __str__(self):
           return self.title
    class Permission(models.Model):
       """
      权限表
      """
       title = models.CharField(verbose_name='标题', max_length=32)
       url = models.CharField(verbose_name='含正则的URL', max_length=128)
       menu = models.ForeignKey(verbose_name="菜单",to="Menu",null=True,blank=True,on_delete=models.CASCADE)
       def __str__(self):
           return self.title
  • rbac/service/init_permission

    from django.conf import settings
    def init_permission(request,user):
       permission_queryset = user.roles.all().filter(permissions__url__isnull=False).values(,"permissions__url","permissions__title","permissions__menu_id","permissions__menu__title","permissions__menu__icon").distinct()
       menu_dict = {}#菜单+能成为菜单的权限,用于做菜单显示
       permission_list = []#所有权限,用于做校验
       for i in permission_queryset:
           permission_list.append({,"url":i["permissions__url"],"pid":i["permissions__parent_id"]})
           menu_id = i.get("permissions__menu_id")
           if not menu_id:
               continue
           if menu_id in menu_dict:
               menu_dict[menu_id]["children"].append({,"title":i["permissions__title"],"url":i["permissions__url"]})
           else:
               menu_dict[i["permissions__menu_id"]]={"title":i["permissions__menu__title"],
                                                            "icon":i["permissions__menu__icon"],
                                                            "children":[{,"title":i["permissions__title"],
                                                                        "url":i["permissions__url"]}]}
       request.session[settings.PERMISSION_SESSION_KEY]=permission_list
       request.session[settings.MENU_SESSION_KEY]=menu_dict
  • rbac/templatetags/rbac.py

    from django.template import Library
    from django.conf import settings
    from collections import OrderedDict
    #导入有序字典
    import re
    register = Library()
    @register.inclusion_tag("rbac/menu.html")
    def menu(request):
       menu_dict = request.session.get(settings.MENU_SESSION_KEY)
       ordered_dict = OrderedDict()
       #sorted(降序排列)加reverse=True是升序。
       #python3.7以前字典是无序的所以为了,菜单的顺序不变。需要加上有序字典。
       for key in sorted(menu_dict):
           ordered_dict[key] = menu_dict[key]
           menu_dict[key]["class"] = "hide"
           #设置自动选中
           for node in menu_dict[key]["children"]:
               reg = "^%s$" %node["url"]
               if re.match(reg,request.path_info):
                   node["class"] = "active"
                   menu_dict[key]["class"] = ""
       return {"menu_dict":ordered_dict}
  • rbac/template/rbac/menu.html

    <div class="multi-menu">
    {% for item in menu_dict.values %}
    <div class="item">
               <div class="title"><span class="icon-wrap"><i class="fa {{ item.icon }}"></i></span> {{ item.title }}</div>
               <div class="body {{ item.class }}">
                  {% for per in item.children %}
                       <a class="{{ per.class }}" href="{{ per.url }}">{{ per.title }}</a>
                  {% endfor %}
               </div>
    </div>
      {% endfor %}
    </div>

1.8 非菜单归属之动态选中

  • rbac/models.py

    from django.db import models
    # Create your models here.
    class Menu(models.Model):
       """
      菜单表
      """
       #需要创建唯一索引
       title = models.CharField(max_length=32,unique=True)
       icon = models.CharField(max_length=32)
       #显示文字,不加的话是对象
       def __str__(self):
           return self.title
    class Permission(models.Model):
       """
      权限表
      """
       title = models.CharField(verbose_name='标题', max_length=32)
       url = models.CharField(verbose_name='含正则的URL', max_length=128)
       #是放属于某个一级菜单的id,他和menu互斥的不然后面没法判断。
       parent = models.ForeignKey(verbose_name="父权限",to="permission",null=True,blank=True,on_delete=models.CASCADE)
       menu = models.ForeignKey(verbose_name="菜单",to="Menu",null=True,blank=True,on_delete=models.CASCADE)
       def __str__(self):
           return self.title
  • rbac/middleware/rbac.py

    servicefrom django.utils.deprecation import MiddlewareMixin
    from django.conf import settings
    from django.shortcuts import redirect,HttpResponse
    import re
    class RbacMiddleware(MiddlewareMixin):
       """
      权限控制中间件
      """
           #4.进行权限校验
           flag = False
           for item in permission_list:
               id = item["id"]
               pid = item["pid"]
               reg = "^%s$"% item.get("url")
               if re.match(reg,current_url):
                   flag = True
                   if pid:
                       request.current_menu_id = pid
                   else:
                       request.current_menu_id = id
                   break
           if not flag:
               return HttpResponse("无权访问")
  • rbac/service/init_permission.py

    from django.conf import settings
    def init_permission(request,user):
       permission_queryset = user.roles.all().filter(permissions__url__isnull=False).values(
     "permissions__id",
     "permissions__url",
     "permissions__title",
     "permissions__parent_id",
     "permissions__menu_id",
     "permissions__menu__title",
     "permissions__menu__icon").distinct()
       menu_dict = {}#菜单+能成为菜单的权限,用于做菜单显示
       permission_list = []#所有权限,用于做校验
       for i in permission_queryset:
           permission_list.append({"id":i["permissions__id"],"url":i["permissions__url"],"pid":i["permissions__parent_id"]})
           menu_id = i.get("permissions__menu_id")
           if not menu_id:
               continue
           if menu_id in menu_dict:
               menu_dict[menu_id]["children"].append({"id":i["permissions__id"],"title":i["permissions__title"],"url":i["permissions__url"]})
           else:
               menu_dict[i["permissions__menu_id"]]={"title":i["permissions__menu__title"],
                                                            "icon":i["permissions__menu__icon"],
                                                            "children":[{"id":i["permissions__id"],"title":i["permissions__title"],
                                                                        "url":i["permissions__url"]}]}
       request.session[settings.PERMISSION_SESSION_KEY]=permission_list
       request.session[settings.MENU_SESSION_KEY]=menu_dict
  • rbac/templatetags/rbac.py

    from django.template import Library
    from django.conf import settings
    from collections import OrderedDict
    import re
    #导入有序字典
    register = Library()
    @register.inclusion_tag("rbac/menu.html")
    def menu(request):
       menu_dict = request.session.get(settings.MENU_SESSION_KEY)
       ordered_dict = OrderedDict()
       #sorted(降序排列)加reverse=True是升序。
       for key in sorted(menu_dict):
           ordered_dict[key] = menu_dict[key]
           menu_dict[key]["class"] = "hide"
           #设置自动选中
           for node in menu_dict[key]["children"]:
               if request.current_menu_id == node["id"]:
                   node["class"] = "active"
                   menu_dict[key]["class"] = ""
       return {"menu_dict":ordered_dict}

1.9 导航条

  • rbac/middleware/rbac.py

    from django.utils.deprecation import MiddlewareMixin
    from django.conf import settings
    from django.shortcuts import redirect,HttpResponse
    import re
    class RbacMiddleware(MiddlewareMixin):
       """
      权限控制中间件
      """
                   
           request.breadcrumb_list = [
              {"title":"首页","url":"/"},
          ]
           
           #4.进行权限校验
           flag = False
           for item in permission_dict.values():
               pid = item["pid"]
               id = item["id"]
               pname = item["pname"]
               reg = "^%s$"% item.get("url")
               print(reg,current_url)
               if re.match(reg,current_url):
                   flag = True
                   if pid:
                       request.current_menu_id = pid
                       request.breadcrumb_list.extend([
                          {"title":permission_dict[str(pid)]["title"],"url":permission_dict[str(pid)]["url"]},
                          {"title":item["title"],"url":item["url"]},
                      ])
                   else:
                       request.current_menu_id = id
                       request.breadcrumb_list.extend([
                          {"title":item["title"],"url":item["url"]},])
                   break
           if not flag:
               return HttpResponse("无权访问")
  • /rbac/templatetags/rbac.py

    from django.template import Library
    from django.conf import settings
    from collections import OrderedDict
    import re
    #导入有序字典
    register = Library()
    @register.inclusion_tag("rbac/breadcrumb.html")
    def breadcrumb(request):
       return {"breadcrumb_list":request.breadcrumb_list}
  • web/template/layout.html

    {% load static %}
    <!--导入静态文件-->
    {% load rbac%}
    <!--事先导入-->
    <!DOCTYPE html>
    <html lang="en">
    <head>
       <meta charset="UTF-8">
       <title>路飞学城</title>
    <!--应用静态文件中的其他文件-->
    <link rel="shortcut icon" href="{% static 'imgs/luffy-study-logo.png' %} ">
       <link rel="stylesheet" href="{% static 'plugins/bootstrap-3.4.1-dist/css/bootstrap.css' %} "/>
       <link rel="stylesheet" href="{% static 'plugins/font-awesome-4.7.0/css/font-awesome.css' %} "/>
       <link rel="stylesheet" href="{% static 'css/commons.css' %} "/>
       <link rel="stylesheet" href="{% static 'css/nav.css' %} "/>
    <link rel="stylesheet" href="{% static 'rbac/css/rbac.css' %} "/>
    </head>
    <body>
    <div class="pg-body">
       <div class="left-menu">
           <div class="menu-body">
    {% menu request %}
           </div>
       </div>
       <div class="right-body">
           <div>
    {% breadcrumb request %}
           </div>
          {% block content %} {% endblock %}
       </div>
    </div>
    <script src="{% static 'js/jquery-3.3.1.min.js' %}"></script>
    <script src="{% static 'plugins/bootstrap/js/bootstrap.js' %}"></script>
    <script src="{% static 'rbac/js/rbac.js' %}"></script>
    {% block js %} {% endblock %}
    </body>
    </html>
  • rbac/template/rbac/breadcrumb.html

    <ol class="breadcrumb no-radius no-margin" style="border-bottom: 1px solid #ddd;">
    {% for i in breadcrumb_list %}
    {% if forloop.last %}
    <!--判断i的url是否有当前url-->
    <li class="active">{{i.title}}</li>
    {% else %}
    <li><a href="{{i.url}}">{{i.title}}</a></li>
    {% endif %}
    {% endfor %}
    </ol>

1.10 粒度控制到按钮

  • rbac/models.py

    from django.db import models
    # Create your models here.
    class Menu(models.Model):
       """
      菜单表
      """
       #需要创建唯一索引
       title = models.CharField(max_length=32,unique=True)
       icon = models.CharField(max_length=32)
       #显示文字,不加的话是对象
       def __str__(self):
           return self.title
    class Permission(models.Model):
       """
      权限表
      """
       title = models.CharField(verbose_name='标题', max_length=32)
       url = models.CharField(verbose_name='含正则的URL', max_length=128)
       name = models.CharField(verbose_name='URL别名', max_length=32,null=True,blank=True)
       parent = models.ForeignKey(verbose_name="父权限",to="permission",null=True,blank=True,on_delete=models.CASCADE)
       menu = models.ForeignKey(verbose_name="菜单",to="Menu",null=True,blank=True,on_delete=models.CASCADE)
       def __str__(self):
           return self.title
  • rbac/service/init_permission

    from django.conf import settings
    def init_permission(request,user):
       permission_queryset = user.roles.all().filter(permissions__url__isnull=False).values("permissions__id",
                                                                                            "permissions__url",
                                                                                            "permissions__name",
                                                                                            "permissions__title",
                                                                                            "permissions__parent_id",
                                                                                            "permissions__parent__name",
                                                                                            "permissions__menu_id",
                                                                                            "permissions__menu__title",
                                                                                            "permissions__menu__icon").distinct()
       menu_dict = {}#菜单+能成为菜单的权限,用于做菜单显示
       permission_dict = {}#所有权限,用于做校验
       for i in permission_queryset:
           permission_dict[i["permissions__name"]]={"id":i["permissions__id"],"title":i["permissions__title"],"url":i["permissions__url"],"pid":i["permissions__parent_id"],"pname":i["permissions__parent__name"],}
           menu_id = i.get("permissions__menu_id")
           if not menu_id:
               continue
           if menu_id not in menu_dict:
               menu_dict[i["permissions__menu_id"]]={"title":i["permissions__menu__title"],
                                                            "icon":i["permissions__menu__icon"],
                                                            "children":[{"id":i["permissions__id"],"title":i["permissions__title"],
                                                                        "url":i["permissions__url"]}]}
           else:
               menu_dict[menu_id]["children"].append({"id":i["permissions__id"],"title":i["permissions__title"],"url":i["permissions__url"]})
               
       request.session[settings.PERMISSION_SESSION_KEY]=permission_dict
       request.session[settings.MENU_SESSION_KEY]=menu_dict
  • rbac/middleware/rbac.py

    from django.utils.deprecation import MiddlewareMixin
    from django.conf import settings
    from django.shortcuts import redirect,HttpResponse
    import re
    class RbacMiddleware(MiddlewareMixin):
    """
    权限控制中间件
    """
    #4.进行权限校验
    flag = False
    for item in permission_dict.values():
    pid = item["pid"]
    id = item["id"]
    pname = item["pname"]
    reg = "^%s$"% item.get("url")
    print(reg,current_url)
    if re.match(reg,current_url):
    flag = True
    if pid:
    request.current_menu_id = pid
    request.breadcrumb_list.extend([
    {"title":permission_dict[pname]["title"],"url":permission_dict[pname]["url"]},
    {"title":item["title"],"url":item["url"]},
    ])
    else:
    request.current_menu_id = id
    request.breadcrumb_list.extend([
    {"title":item["title"],"url":item["url"]},])
    break
    if not flag:
    return HttpResponse("无权访问")
  • rbac/templatetags/rbac.py

    from django.template import Library
    from django.conf import settings
    from collections import OrderedDict
    import re
    #导入有序字典
    register = Library()
    @register.filter
    def has_permission(request,name):
    permission_dict = request.session.get(settings.PERMISSION_SESSION_KEY)
    if name in permission_dict:
    return True
  • web/template/customer_list.html

    {% extends 'layout.html' %}
    {% load rbac %}
    {% block content %}
       <div class="luffy-container">
           <div class="btn-group" style="margin: 5px 0">
    {% if request|has_permission:"customer_add" %}
    <a class="btn btn-default" href="{% url 'customer_add' %}">
    <i class="fa fa-plus-square" aria-hidden="true"></i> 添加客户
    </a>
    {% endif %}
    {% if request|has_permission:"customer_import" %}
    <a class="btn btn-default" href="{% url 'customer_import' %}">
    <i class="fa fa-file-excel-o" aria-hidden="true"></i> 批量导入
    </a>
    {% endif %}
           </div>
           <table class="table table-bordered table-hover">
               <thead>
               <tr>
                   <th>ID</th>
                   <th>客户姓名</th>
                   <th>年龄</th>
                   <th>邮箱</th>
                   <th>公司</th>
    {% if request|has_permission:"customer_del" or request|has_permission:"customer_edit"%}
    <th>选项</th>
    {% endif %}
               </tr>
               </thead>
               <tbody>
              {% for row in data_list %}
                   <tr>
                       <td>{{ row.id }}</td>
                       <td>{{ row.name }}</td>
                       <td>{{ row.age }}</td>
                       <td>{{ row.email }}</td>
                       <td>{{ row.company }}</td>
    {% if request|has_permission:"customer_del" or request|has_permission:"customer_edit"%}
    <td>
    {% if request|has_permission:"customer_edit"%}
    <a style="color: #333333;" href="{% url 'customer_edit' cid=row.id %}">
    <i class="fa fa-edit" aria-hidden="true"></i></a>
    |
    {% endif %}
    {% if request|has_permission:"customer_del"%}
    <a style="color: #d9534f;" href="{% url 'customer_del' cid=row.id %}"><i class="fa fa-trash-o"></i></a>
    {% endif %}
    </td>
    {% endif %}
                   </tr>
              {% endfor %}
               </tbody>
           </table>
       </div>
    {% endblock %}

1.11问题总结

  1. 权限有几张表?

  2. 简述权限流程?

  3. 为什么要把权限放入session?

  4. 静态文件和模块文件

  5. 相关技点。

  6. 二级菜单时,如何构造的数据结构?

  7. 非菜单的权限归属?

  8. 层级导航?

  9. 粒度控制到按钮?

  10. 如何实现的权限系统?

  11. 为什么要在中间件中做校验?

  12. 写出流程(思维导图)

2.crm业务

2.1 modelForm

  • views.py

    from django import forms
    def user_list(request):
       user_queryset = models.User.objects.all()
       return render(request,"user_list.html",{"user_queryset":user_queryset})
    class UserForm(forms.ModelForm):
       class Meta:
           #打开User表赋给model
           model = models.User
           #拿出所有字段
           fields = "__all__"
           #也可以拿取指定的字段
           #fields = ["name","depart"]
           #这里也可以写插件
    widgets = {
               "name":forms.TextInput(attrs={"class":"form-con"}),
               "gender":forms.Select(attrs={"class":"form-con"}),
               "roles":forms.SelectMultiple(attrs={"class":"form-con"}),
               
          }
           #可以自己写错误信息
           error_messages = {
               "name":{
                   "required":"用户名不能为空"
              }
          }
    def user_add(request):
       if request.method == "GET":
           form = UserForm()
       else:
           form = UserForm(request.POST)
           if form.is_valid():
               print("通过验证")
               #它自动把你增加不管单表还是多表。
               form.save()
               return redirect("/user/list")
       return render(request,"user_add.html",{"form":form})
    def user_edit(request,uid):
       obj = models.User.objects.filter(id=uid).first()
       if request.method == "GET":
           #设置默认值
           form = UserForm(instance=obj)
           return render(request,"user_edit.html",{"form":form})
       else:
           form = UserForm(request.POST,instance=obj)
           if form.is_valid():
               #如果你要修改,需要加上instance=obj,应为save内部需要判断。
               #不然怎么去区分。
               form.save()
               return redirect("/user/list")
       return render(request,"user_edit.html",{"form":form})
  • models.py

    from django.db import models
    class Depart(models.Model):
    caption = models.CharField(max_length=32)
    class User(models.Model):
    name = models.CharField(max_length=32)
    depart = models.ForeignKey(to="Depart",on_delete=models.CASCADE)
    gender_choices = (
    (1,"男"),
    (2,"女")
    )
    gender = models.IntegerField(choices=gender_choices,default=1)
    roles = models.ManyToManyField(to="Role")

2.2 简单化角色权限管理

  • mysite/urls.py

    from django.contrib import admin
    from django.urls import path,re_path
    from django.conf.urls import include
    urlpatterns = [
    path('admin/', admin.site.urls),
    re_path("^",include("web.urls")),
    #加上namespace后它下面的都会加上rbac:xxx的前缀
    re_path("^rbac/",include("rbac.urls",namespace="rbac")),
    ]
  • rbac/urls.py

    from django.urls import path,re_path
    from rbac.views import role
    from rbac.views import menu
    #在最外面的urls里面写了前缀,在这里必须声明
    app_name = "rbac"
    urlpatterns = [
    re_path(r'^role/list/$', role.role_list,name="role_list"),
    re_path(r'^role/add/$', role.role_add,name="role_add"),
    re_path(r'^role/edit/(?P<rid>\d+)/$', role.role_edit,name="role_edit"),
    re_path(r'^menu/list/$', menu.menu_list,name="menu_list"),
    re_path(r'^menu/add/$', menu.menu_add,name="menu_add"),
    ]
  • rbac/views/role.py

    from django.shortcuts import render,redirect
    #用于反向生成url本质上都是调用的这里。
    from django.urls import reverse
    from rbac import models
    # Create your views here.
    def role_list(request):
    role_queryset = models.Role.objects.all()
    return render(request,"rbac/role_list.html",{"role_queryset":role_queryset})
    from django import forms
    class RoleModelForm(forms.ModelForm):
    class Meta:
    model = models.Role
    fields = ["title"]
    widgets = {
    "title":forms.TextInput(attrs={"class":"form-control"})
    }
    def role_add(request):
    if request.method == "GET":
    form = RoleModelForm()
    else:
    form = RoleModelForm(request.POST)
    if form.is_valid():
    form.save()
    #这样写后就可以反向生成了。
    return redirect(reverse("rbac:role_list"))
    return render(request,"rbac/role_add.html",{"form":form})
    def role_edit(request,rid):
    obj=models.Role.objects.filter(id=rid).first()
    if not obj:
    return HttpResponse("角色不存在")
    if request.method == "GET":
    form = RoleModelForm(instance=obj)
    else:
    form = RoleModelForm(request.POST,instance=obj)
    if form.is_valid():
    form.save()
    return redirect("/rbac/role/list/")
    return render(request,"rbac/role_edit.html",{"form":form})

     

  • rbac/views/menu.py

    from django.shortcuts import render,redirect
    #用于反向生成url本质上都是调用的这里。
    from django.urls import reverse
    from rbac import models
    # Create your views here.
    def menu_list(request):
       menu_queryset = models.Menu.objects.all()
       mid = request.GET.get("mid")
       if mid:
           permission_queryset = models.Permission.objects.filter(menu_id=mid)
       else:
           permission_queryset = []
       return render(
           request,
           "rbac/menu_list.html",
          {
               "menu_queryset":menu_queryset,
               "permission_queryset":permission_queryset
            })
    from django.utils.safestring import mark_safe
    ICON_LIST = [
      ['fa-hand-scissors-o', '<i aria-hidden="true" class="fa fa-hand-scissors-o"></i>'],
      ['fa-hand-spock-o', '<i aria-hidden="true" class="fa fa-hand-spock-o"></i>'],
      ['fa-hand-stop-o', '<i aria-hidden="true" class="fa fa-hand-stop-o"></i>'],
      ['fa-handshake-o', '<i aria-hidden="true" class="fa fa-handshake-o"></i>'],
      ['fa-hard-of-hearing', '<i aria-hidden="true" class="fa fa-hard-of-hearing"></i>'],
      ['fa-hashtag', '<i aria-hidden="true" class="fa fa-hashtag"></i>'],
      ['fa-hdd-o', '<i aria-hidden="true" class="fa fa-hdd-o"></i>'],
      ['fa-headphones', '<i aria-hidden="true" class="fa fa-headphones"></i>'],
      ['fa-heart', '<i aria-hidden="true" class="fa fa-heart"></i>'],
      ['fa-heart-o', '<i aria-hidden="true" class="fa fa-heart-o"></i>'],
      ['fa-heartbeat', '<i aria-hidden="true" class="fa fa-heartbeat"></i>'],
      ['fa-history', '<i aria-hidden="true" class="fa fa-history"></i>'],
      ['fa-home', '<i aria-hidden="true" class="fa fa-home"></i>'],
      ['fa-hotel', '<i aria-hidden="true" class="fa fa-hotel"></i>'],
      ['fa-hourglass', '<i aria-hidden="true" class="fa fa-hourglass"></i>'],
      ['fa-hourglass-1', '<i aria-hidden="true" class="fa fa-hourglass-1"></i>'],
      ['fa-hourglass-2', '<i aria-hidden="true" class="fa fa-hourglass-2"></i>'],
      ['fa-hourglass-3', '<i aria-hidden="true" class="fa fa-hourglass-3"></i>'],
      ['fa-hourglass-end', '<i aria-hidden="true" class="fa fa-hourglass-end"></i>'],
      ['fa-hourglass-half', '<i aria-hidden="true" class="fa fa-hourglass-half"></i>'],
      ['fa-hourglass-o', '<i aria-hidden="true" class="fa fa-hourglass-o"></i>'],
      ['fa-hourglass-start', '<i aria-hidden="true" class="fa fa-hourglass-start"></i>'],
      ['fa-i-cursor', '<i aria-hidden="true" class="fa fa-i-cursor"></i>'],
      ['fa-id-badge', '<i aria-hidden="true" class="fa fa-id-badge"></i>'],
      ['fa-id-card', '<i aria-hidden="true" class="fa fa-id-card"></i>'],
      ['fa-id-card-o', '<i aria-hidden="true" class="fa fa-id-card-o"></i>'],
      ['fa-image', '<i aria-hidden="true" class="fa fa-image"></i>'],
      ['fa-mail-reply-all', '<i aria-hidden="true" class="fa fa-mail-reply-all"></i>'],
      ['fa-reply', '<i aria-hidden="true" class="fa fa-reply"></i>'],
      ['fa-reply-all', '<i aria-hidden="true" class="fa fa-reply-all"></i>'],
      ['fa-retweet', '<i aria-hidden="true" class="fa fa-retweet"></i>'],
      ['fa-wrench', '<i aria-hidden="true" class="fa fa-wrench"></i>']]
    for item in ICON_LIST:
       item[1] = mark_safe(item[1])
    from django import forms
    class MenuModelForm(forms.ModelForm):
       class Meta:
           model = models.Menu
           fields = ["title","icon"]
           widgets = {
               "title":forms.TextInput(attrs={"class":"form-control"}),
               'icon': forms.RadioSelect(
                   choices=ICON_LIST,
                   attrs={'class': 'clearfix'}
              )
          }
    def menu_add(request):
       if request.method == "GET":
           form = MenuModelForm()
       else:
           form = MenuModelForm(request.POST)
           if form.is_valid():
               form.save()
               #这样写后就可以反向生成了。
               return redirect(reverse("rbac:menu_list"))
       return render(request,"rbac/menu_add.html",{"form":form})
    """
    def role_edit(request,rid):
      obj=models.Role.objects.filter(id=rid).first()
      if not obj:
          return HttpResponse("角色不存在")
      if request.method == "GET":
          form = RoleModelForm(instance=obj)
      else:
          form = RoleModelForm(request.POST,instance=obj)
          if form.is_valid():
              form.save()
              return redirect("/rbac/role/list/")
      return render(request,"rbac/role_edit.html",{"form":form})
    """
  • html

    layout.html
    {% load static %}
    <!--导入静态文件-->
    {% load rbac%}
    <!DOCTYPE html>
    <html lang="en">
    <head>
       <meta charset="UTF-8">
       <title>路飞学城</title>
    <!--应用静态文件中的其他文件-->
    <link rel="shortcut icon" href="{% static 'imgs/luffy-study-logo.png' %} ">
       <link rel="stylesheet" href="{% static 'plugins/bootstrap-3.4.1-dist/css/bootstrap.css' %} "/>
       <link rel="stylesheet" href="{% static 'plugins/font-awesome-4.7.0/css/font-awesome.css' %} "/>
       <link rel="stylesheet" href="{% static 'css/commons.css' %} "/>
       <link rel="stylesheet" href="{% static 'css/nav.css' %} "/>
    <link rel="stylesheet" href="{% static 'rbac/css/rbac.css' %} "/>
       <style>
           body {
               margin: 0;
          }
          .no-radius {
               border-radius: 0;
          }
          .no-margin {
               margin: 0;
          }
          .pg-body > .left-menu {
               background-color: #EAEDF1;
               position: absolute;
               left: 1px;
               top: 48px;
               bottom: 0;
               width: 220px;
               border: 1px solid #EAEDF1;
               overflow: auto;
          }
          .pg-body > .right-body {
               position: absolute;
               left: 225px;
               right: 0;
               top: 48px;
               bottom: 0;
               overflow: scroll;
               border: 1px solid #ddd;
               border-top: 0;
               font-size: 13px;
               min-width: 755px;
          }
          .navbar-right {
               float: right !important;
               margin-right: -15px;
          }
          .luffy-container {
               padding: 15px;
          }
       </style>
    {% block css %}{% endblock %}
    </head>
    <body>
    <div class="pg-header">
       <div class="nav">
           <div class="logo-area left">
               <a href="#">
                   <img class="logo" src="{%static 'imgs/logo.svg'%}">
                   <span style="font-size: 18px;">路飞学城 </span>
               </a>
           </div>
           <div class="left-menu left">
               <a class="menu-item">资产管理</a>
               <a class="menu-item">用户信息</a>
               <a class="menu-item">路飞管理</a>
               <div class="menu-item">
                   <span>使用说明</span>
                   <i class="fa fa-caret-down" aria-hidden="true"></i>
                   <div class="more-info">
                       <a href="#" class="more-item">管他什么菜单</a>
                       <a href="#" class="more-item">实在是编不了</a>
                   </div>
               </div>
           </div>
           <div class="right-menu right clearfix">
               <div class="user-info right">
                   <a href="#" class="avatar">
                       <img class="img-circle" src="{% static 'imgs/default.png'%}">
                   </a>
                   <div class="more-info">
                       <a href="#" class="more-item">个人信息</a>
                       <a href="#" class="more-item">注销</a>
                   </div>
               </div>
               <a class="user-menu right">
                   消息
                   <i class="fa fa-commenting-o" aria-hidden="true"></i>
                   <span class="badge bg-success">2</span>
               </a>
               <a class="user-menu right">
                   通知
                   <i class="fa fa-envelope-o" aria-hidden="true"></i>
                   <span class="badge bg-success">2</span>
               </a>
               <a class="user-menu right">
                   任务
                   <i class="fa fa-bell-o" aria-hidden="true"></i>
                   <span class="badge bg-danger">4</span>
               </a>
           </div>
       </div>
    </div>
    <div class="pg-body">
       <div class="left-menu">
           <div class="menu-body">
    {#{% menu request %}#}
           </div>
       </div>
       <div class="right-body">
           <div>
    {#{% breadcrumb request %}#}
           </div>
          {% block content %} {% endblock %}
       </div>
    </div>
    <script src="{% static 'js/jquery-3.3.1.min.js' %}"></script>
    <script src="{% static 'plugins/bootstrap/js/bootstrap.js' %}"></script>
    <script src="{% static 'rbac/js/rbac.js' %}"></script>
    {% block js %} {% endblock %}
    </body>
    </html>
    role_list.html
    {% extends "layout.html"%}
    {% block content %}
    <div class="luffy-container">
           <div class="btn-group" style="margin: 5px 0">
    <a href="{% url 'rbac:role_add' %}" class="btn btn-default" >
    <i class="fa fa-plus-square" aria-hidden="true"></i> 添加客户
    </a>
           </div>
           <table class="table table-bordered table-hover">
               <thead>
               <tr>
                   <th>ID</th>
                   <th>角色</th>
    <th>选项</th>
    <th>个数</th>
               </tr>
               </thead>
               <tbody>
              {% for row in role_queryset %}
                   <tr>
                       <td>{{ row.id }}</td>
                       <td>{{ row.title }}</td>
                       <td>{{ row.permissions.count }}</td>
    <td>
    <a style="color: #333333;" href="{% url 'rbac:role_edit' rid=row.id %}">
    <i class="fa fa-edit" aria-hidden="true" ></i></a>
    |
    <a style="color: #d9534f;" ><i class="fa fa-trash-o" ></i></a>
    </td>
                   </tr>
              {% endfor %}
               </tbody>
           </table>
       </div>
    {% endblock %}
    role_add.html
    {% extends 'layout.html' %}
    {% block content %}
       <div class="luffy-container">
           <form class="form-horizontal clearfix" method="post" novalidate>
              {% csrf_token %}
              {% for field in form %}
                   <div class="form-group col-sm-6 clearfix">
                       <label class="col-sm-3 control-label">{{ field.label }}</label>
                       <div class="col-sm-9">
                          {{ field }} <span style="color:firebrick;">{{ field.errors.0 }}</span>
                       </div>
                   </div>
              {% endfor %}
               <div class="form-group col-sm-12">
                   <div class="col-sm-6">
                       <div class="col-sm-offset-3">
                           <button type="submit" class="btn btn-primary">提 交</button>
                       </div>
                   </div>
               </div>
           </form>
       </div>
    {% endblock %}
    role_edit.html
    {% extends 'layout.html' %}
    {% block content %}
       <div class="luffy-container">
           <form class="form-horizontal clearfix" method="post" novalidate>
              {% csrf_token %}
              {% for field in form %}
                   <div class="form-group col-sm-6 clearfix">
                       <label class="col-sm-3 control-label">{{ field.label }}</label>
                       <div class="col-sm-9">
                          {{ field }} <span style="color:firebrick;">{{ field.errors.0 }}</span>
                       </div>
                   </div>
              {% endfor %}
               <div class="form-group col-sm-12">
                   <div class="col-sm-6">
                       <div class="col-sm-offset-3">
                           <button type="submit" class="btn btn-primary">提 交</button>
                       </div>
                   </div>
               </div>
           </form>
       </div>
    {% endblock %}
    menu_list.html
    {% extends 'layout.html' %}
    {% block css %}
    <style>
    tr.root{
    background-color:#fif7fd;
    }
    </style>
    {% endblock %}
    {% block content %}
       <div class="col-sm-3">
    <div class="panel panel-default">
     <!-- Default panel contents -->
     <div class="panel-heading">
    <i class="fa fa-universal-access" aria-hidden="true"></i>菜单管理
    <a href="{% url 'rbac:menu_add' %}" class = "btn btn-success btn-xs" style="padding:apx 8px;margin:-3px;float:right">
    <i class="fa fa-plus-circle" aria-hidden="true"></i>
    新建
    </a>
     </div>
     <table class="table">
    <thead>
    <th>名称</th>
    <th>图标</th>
    <th>选项</th>
    </thead>
    <tbody>
    {% for row in menu_queryset %}
    <tr><!--不加路劲自动跳到当前页面-->
    <td><a href="?mid={{row.id}}">{{row.title}}</a></td>
    <td><i class="fa {{row.icon}}" aria-hidden="true"></i></td>
    <td>
    <a style="color: #333333;" href="{% url 'rbac:role_edit' rid=row.id %}">
    <i class="fa fa-edit" aria-hidden="true" ></i></a>
    |
    <a style="color: #d9534f;" ><i class="fa fa-trash-o" ></i></a>
    </td>
    </tr>
    {% endfor %}
    </tbody>
     </table>
    </div>
    </div>
    <div class="col-sm-9">
    <div class="panel panel-default">
     <!-- Default panel contents -->
     <div class="panel-heading"><i class="fa fa-universal-access" aria-hidden="true"></i>菜单管理</div>
     <table class="table">
    <thead>
    <th>id</th>
    <th>name</th>
    </thead>
    <tbody>
    {% for row in permission_queryset %}
    <tr class="root">
    <td>{{row.title}}</td>
    <td>{{row.url}}</td>
    <td>
    <a style="color: #333333;">
    <i class="fa fa-edit" aria-hidden="true" ></i></a>
    |
    <a style="color: #d9534f;" ><i class="fa fa-trash-o" ></i></a>
    </td>
    </tr>
    {% endfor %}
    </tbody>
     </table>
    </div>
       </div>
    {% endblock %}
    menu_add.html
    {% extends 'layout.html' %}
    {% block content %}
       <div class="luffy-container">
           <form class="form-horizontal clearfix" method="post" novalidate>
              {% csrf_token %}
              {% for field in form %}
                   <div class="form-group col-sm-6 clearfix">
                       <label class="col-sm-3 control-label">{{ field.label }}</label>
                       <div class="col-sm-9">
                          {{ field }} <span style="color:firebrick;">{{ field.errors.0 }}</span>
                       </div>
                   </div>
              {% endfor
                %}
               <div class="form-group col-sm-12">
                   <div class="col-sm-6">
                       <div class="col-sm-offset-3">
                           <button type="submit" class="btn btn-primary">提 交</button>
                       </div>
                   </div>
               </div>
           </form>
       </div>
    {% endblock %}
 
### 2.3 forloop
- 在{% for %}循环内部,可以访问一个名为forloop的模板变量。这个变量有若干属性,通过它们可以获知循环进程的一些信息。
```python
forloop.counter
forloop.counter 的值是一个整数,表示循环的次数。这个属性的值从 1 开始,因此第一次循环时,forloop.counter 等于 1
 
{% for item in todo_list %}
<p>{{ forloop.counter }}: {{ item }}</p>
{% endfor %}
forloop.counter0
forloop.counter0 与 forloop.counter 类似,不过是从零开始的。第一次循环时,其值为 0
 
 
 
forloop.revcounter
forloop.revcounter的值是一个整数,表示循环中剩余的元素数量。第一次循环时, forloop.revcounter 的值是序列中要遍历的元素总数。最后一次循环时, forloop.revcounter的值为 1
 
 
 
  forloop.revcounter0
forloop.revcounter0 与 forloop.revcounter类似,不过索引是基于零的。第一次循环时, forloop.revcounter0的值是序列中元素数量减去一。最后一次循环时, forloop.revcounter0 的值为 0
 
 
 
forloop.first
forloop.first 是个布尔值,第一次循环时为 True 。需要特殊处理第一个元素时很方便:
 
{% for object in objects %}
    {% if forloop.first %}
        <li class="first">
    {% else %}
        <li>
    {% endif %}
    {{ object }}
    </li>
{% endfor %}
 
forloop.last
 
forloop.last是个布尔值,最后一次循环时为True 。经常用它在一组链接之间放置管道符号:
 
{% for link in links %}
    {{ link }}{% if not forloop.last %} | {% endif %}
{% endfor %}
 
 
上述模板代码的输出可能是:
 
Link1 | Link2 | Link3 | Link4
 
此外,还经常用它在一组单词之间放置逗号:
 
<p>Favorite places:</p>
{% for p in places %}
    {{ p }}{% if not forloop.last %}, {% endif %}
{% endfor %}
 
forloop.parentloop
 
在嵌套的循环中, forloop.parentloop引用父级循环的 forloop 对象。下面举个例子:
 
{% for country in countries %}
    <table>
    {% for city in country.city_list %}
        <tr>
        <td>Country #{{ forloop.parentloop.counter }}</td>
        <td>City #{{ forloop.counter }}</td>
        <td>{{ city }}</td>
        </tr>
    {% endfor %}
    </table>
{% endfor %}
 
 
小贴士
forloop 变量只在循环内部可用。模板解析器遇到 {% endfor %} 时, forloop 随之消失。
上下文和 forloop 变量
在 {% for %} 块中,现有变量会让位,防止覆盖 forloop 变量。Django 把移动的上下文放到
forloop.parentloop 中。通常,你无须担心,但是如果有名为 forloop 的模板变量(不建议这
么做),在 {% for %} 块中会重命名为 forloop.parentloop 。
————————————————
版权声明:本文为CSDN博主「似水@流年」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/mr_hui_/article/details/88310822

2.4 输入框互斥

  • models.py

    from django.db import models
    # Create your models here.
    class Menu(models.Model):
       """
      菜单表
      """
       #需要创建唯一索引
       title = models.CharField(max_length=32,unique=True)
       icon = models.CharField(max_length=32)
       #显示文字,不加的话是对象
       def __str__(self):
           return self.title
    class Permission(models.Model):
       """
      权限表
      """
       title = models.CharField(verbose_name='标题', max_length=32)
       url = models.CharField(verbose_name='含正则的URL', max_length=128)
       name = models.CharField(verbose_name='URL别名', max_length=32,null=True,blank=True)
       #limit_choices_to在显示前做筛选,显示parent为空的
       parent = models.ForeignKey(verbose_name="父权限",to="permission",null=True,blank=True,on_delete=models.CASCADE,limit_choices_to={"parent__isnull":True})
       menu = models.ForeignKey(verbose_name="菜单",to="Menu",null=True,blank=True,on_delete=models.CASCADE)
       def __str__(self):
           return self.title
    class Role(models.Model):
       """
      角色
      """
       title = models.CharField(verbose_name='角色名称', max_length=32)
       permissions = models.ManyToManyField(verbose_name='拥有的所有权限', to='Permission', blank=True)
       def __str__(self):
           return self.title
  • role.py

    from django.shortcuts import render,redirect
    #用于反向生成url本质上都是调用的这里。
    from django.urls import reverse
    from rbac import models
    # Create your views here.
    def role_list(request):
       role_queryset = models.Role.objects.all()
       return render(request,"rbac/role_list.html",{"role_queryset":role_queryset})
    from django import forms
    class RoleModelForm(forms.ModelForm):
       class Meta:
           model = models.Role
           fields = ["title"]
           widgets = {
               "title":forms.TextInput(attrs={"class":"form-control"})
          }
           error_messages = {"title":{"required":"用户名不能为空"}}
           #help_texts编写帮助信息
           help_texts = {"title":"sdf"}
           """
          菜单和权限只能选一个,这个函数中没有。
          只是没出写了,随便放个位置
          def clean(self):
              menu = self.cleaned_data.get("menu")
              parent = self.cleaned_data.get("parent")
              if menu and parent:
                  self.add_error("menu","菜单和权限只能选一个")
          """
    def role_add(request):
       if request.method == "GET":
           form = RoleModelForm()
       else:
           form = RoleModelForm(request.POST)
           if form.is_valid():
               form.save()
               #这样写后就可以反向生成了。
               return redirect(reverse("rbac:role_list"))
       return render(request,"rbac/role_add.html",{"form":form})
  • html

    {% extends 'layout.html' %}
    {% block content %}
       <div class="luffy-container">
           <form class="form-horizontal clearfix" method="post" novalidate>
              {% csrf_token %}
              {% for field in form %}
                   <div class="form-group col-sm-6 clearfix">
                       <label class="col-sm-3 control-label">{{ field.label }}</label>
                       <div class="col-sm-9">
                          {{ field }}{{field.help_text}} <span style="color:firebrick;">{{ field.errors.0 }}</span>
                       </div>
                   </div>
              {% endfor %}
               <div class="form-group col-sm-12">
                   <div class="col-sm-6">
                       <div class="col-sm-offset-3">
                           <button type="submit" class="btn btn-primary">提 交</button>
                       </div>
                   </div>
               </div>
           </form>
       </div>
    {% endblock %}

2.5 formset

  • views.py

    #可以实现多次打印和多次输入,从而实现批量操作
    from django.shortcuts import render
    from django import forms
    class UserForm(forms.Form):
       id = forms.CharField(required=True)
       user = forms.CharField()
       pwd = forms.CharField()
       email = forms.CharField()
    """
    # Create your views here.
    def index(request):
      #额外设置extra设置它的打印多少个
      UserFormSet = forms.formset_factory(UserForm,extra=4)
      if request.method == "GET":
          formset = UserFormSet()
          return render(request,"index.html",{"formset":formset})
      formset = UserFormSet(request.POST)
      if formset.is_valid():
          for row in formset.cleaned_data:
              print(row)
      return render(request,"index.html",{"formset":formset})
    """
    def index(request):
       UserFormSet = forms.formset_factory(UserForm,extra=0)
       if request.method == "GET":
           formset = UserFormSet(initial=[{"id":1,"user":"alex","pwd":"123","email":"alex@qq.com"}])
           return render(request,"index.html",{"formset":formset})
       formset = UserFormSet(request.POST)
       if formset.is_valid():
           for row in formset.cleaned_data:
               id = row.pop("id")
               #models.User.objects.filter(id=id).update(**row)
       return render(request,"index.html",{"formset":formset})

     

  • index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8"/>
    <title></title>
    <style>
    .hide{
    display:none;
    }
    </style>
    </head>
    <body>
    <form method="POST">
    {{formset.management_form}}
    <!--写上他才可以使用formset_factory-->
    <table>
    <tr>
    <th>用户名</th>
    <th>密码</th>
    <th>邮箱</th>
    </tr>
    {% for form in formset %}
    <tr>
    {% for field in form %}
    {% if forloop.first %}
    <td class="hide" >{{field}}</td>
    {% else %}
    <td>{{field}} {{field.errors.0}}</td>
    {% endif %}
    {% endfor %}
    </tr>
    {% endfor %}
    </table>
    <input type="submit" value='提交'>
    </form>
    </body>
    </html>

3.其他

3.1 家庭树

  • views.py

    #这个里面最重要的一点就是,内存中存储的位置是固定的
    #我觉得你们应该可以看懂,那个是固定的。
    comment_list = [
      {"id":1,"title":"asdf","pid":None},
      {"id":1,"title":"asdf","pid":1},
      {"id":1,"title":"asdf","pid":2},
      {"id":1,"title":"asdf","pid":3},
    ]
    #每一条中加一个children
    #id为键转为字典
    comment_dict = {}
    for item in comment_list:
       item["children"] = []
       comment_dict[item["id"]] = item
    result = []
    for row in comment_list:
       if not row["pid"]:
    result.append(row)
       else:
           pid = row["pid"]
           comment_dict[pid]["children"].append(row)
    print(result)
    """
    [
    {'id': 1, 'title': 'asdf', 'pid': None, 'children': [
    {'id': 2, 'title': 'asdf', 'pid': 1, 'children': [
    {'id': 3, 'title': 'asdf', 'pid': 2, 'children': [
    {'id': 4, 'title': 'asdf', 'pid': 3, 'children': []}]}]}]}]
    """

微信小程序开发

1.环境搭建

  1. 申请微信公众平台、登录后选择开发管理的开发控制并保存appid、在开发小程序中有开发工具,下载稳定版。

  2. 创建项目:扫码登录、点击小程序添加、指定文件位置和文件名、将appid粘贴,并选择javascript。

  3. 文件介绍

    1. pages文件夹存放页面、utils文件夹存放工具、js写js代码,json写配置,wxss写css代码,wxml写html代码、最外层的是全局页面文件,都可以被应用。

    2. 如果没有需要创建app.json,这里写创建的页面文件路径。

      {
      #放置页面路径
      "pages":[
      "pages/index/index",
      "pages/logs/logs"
      ],
      }
    3. wxml

      <!--代表span-->
      <text></text>
      <!--代表div-->
      <view></view>

2.基本认识

2.1 全局配置

  1. app.json决定页面路劲、窗口表现、设置网络超时时间、设置多tab等。

  2. 配置项

    1. 小程序根目录下的 app.json 文件用来对微信小程序进行全局配置。文件内容为一个 JSON 对象,有以下属性:

      属性 类型 必填 描述
      entryPagePath string 小程序默认启动首页
      pages string 页面路径列表
      window Object 全局的默认窗口表现
      tabBar Object 底部 tab 栏的表现
    2. entryPagePath,指定小程序的默认启动路径(首页),常见情景是从微信聊天列表页下拉启动、小程序列表启动等。如果不填,将默认为 pages 列表的第一项。不支持带页面路径参数。

      {
      "entryPagePath": "pages/index/index"
      }
    3. pages,用于指定小程序由哪些页面组成,每一项都对应一个页面的 路径(含文件名) 信息。文件名不需要写文件后缀,框架会自动去寻找对应位置的 .json, .js, .wxml, .wxss 四个文件进行处理。未指定 entryPagePath 时,数组的第一项代表小程序的初始页面(首页)。则需要在 app.json 中写。

      {
      "pages": ["pages/index/index", "pages/logs/logs"]
      }
    4. window,用于设置小程序的状态栏、导航条、标题、窗口背景色。

      属性 类型 默认值 描述
      navigationBarBackgroundColor HexColor #000000 导航栏背景颜色,如 #000000
      navigationBarTextStyle string white 导航栏标题颜色,仅支持 black / white
      navigationBarTitleText string   导航栏标题文字内容
      navigationStyle string default 导航栏样式,仅支持以下值: default 默认样式 custom 自定义导航栏,只保留右上角胶囊按钮。参见注 2。
      backgroundColor HexColor #ffffff 窗口的背景色
      backgroundTextStyle string dark 下拉 loading 的样式,仅支持 dark / light
      backgroundColorTop string #ffffff 顶部窗口的背景色,仅 iOS 支持
      backgroundColorBottom string #ffffff 底部窗口的背景色,仅 iOS 支持
      enablePullDownRefresh boolean false 是否开启全局的下拉刷新。 详见 Page.onPullDownRefresh
      onReachBottomDistance number 50 页面上拉触底事件触发时距页面底部距离,单位为 px。 详见 Page.onReachBottom
      pageOrientation string portrait 屏幕旋转设置,支持 auto / portrait / landscape 详见 响应显示区域变化
      restartStrategy string homePage 重新启动策略配置
      initialRenderingCache string   页面初始渲染缓存配置,支持 static / dynamic
      visualEffectInBackground string none 切入系统后台时,隐藏页面内容,保护用户隐私。支持 hidden / none
      handleWebviewPreload string static 控制预加载下个页面的时机。支持 static / manual / auto
    5. 如:

      {
      "window": {
      "navigationBarBackgroundColor": "#ffffff",
      "navigationBarTextStyle": "black",
      "navigationBarTitleText": "微信接口功能演示",
      "backgroundColor": "#eeeeee",
      "backgroundTextStyle": "light"
      }
      }
    6. tabBar,如果小程序是一个多 tab 应用(客户端窗口的底部或顶部有 tab 栏可以切换页面),可以通过 tabBar 配置项指定 tab 栏的表现,以及 tab 切换时显示的对应页面。

      属性 类型 必填 默认值 描述
      color HexColor   tab 上的文字默认颜色,仅支持十六进制颜色
      selectedColor HexColor   tab 上的文字选中时的颜色,仅支持十六进制颜色
      backgroundColor HexColor   tab 的背景色,仅支持十六进制颜色
      borderStyle string black tabbar 上边框的颜色, 仅支持 black / white
      list Array   tab 的列表,详见 list 属性说明,最少 2 个、最多 5 个 tab
      position string bottom tabBar 的位置,仅支持 bottom / top
      custom boolean false 自定义 tabBar,见详情
    7. 其中 list 接受一个数组,只能配置最少 2 个、最多 5 个 tab。tab 按数组的顺序排序,每个项都是一个对象,其属性值如下:

      属性 类型 必填 说明
      pagePath string 页面路径,必须在 pages 中先定义
      text string tab 上按钮文字
      iconPath string 图片路径,icon 大小限制为 40kb,建议尺寸为 81px * 81px,不支持网络图片。 positiontop 时,不显示 icon。
      selectedIconPath string 选中时的图片路径,icon 大小限制为 40kb,建议尺寸为 81px * 81px,不支持网络图片。 positiontop 时,不显示 icon。
  3. 配置示例

    {
    "pages": [
    "pages/index/index",
    "pages/logs/index"
    ],
    "window": {
    "navigationBarBackgroundColor": "#FFDAB9",
    "navigationBarTextStyle": "black",
    "navigationBarTitleText": "Demo"
    },
    "tabBar": {
    "list": [{
    "pagePath": "pages/index/index",
    "text": "首页"
    }, {
    "pagePath": "pages/logs/index",
    "text": "我的"
    }]
    }
    }

2.2 组件

1 视图容器

  1. view

    • 功能描述:视图容器

    • 属性说明

      属性 类型 默认值 必填 说明
      hover-class string none 指定按下去的样式类。当 hover-class="none" 时,没有点击态效果
      hover-stop-propagation boolean false 指定是否阻止本节点的祖先节点出现点击态
      hover-start-time number 50 按住后多久出现点击态,单位毫秒
      hover-stay-time number 400 手指松开后点击态保留时间,单位毫秒

2 基本内容

  1. text

    • 功能描述:文本

    • 属性说明

      属性 类型 默认值 必填 说明
      selectable boolean false 文本是否可选 (已废弃)
      user-select boolean false 文本是否可选,该属性会使文本节点显示为 inline-block
      space string   显示连续空格
      decode boolean false 是否解码
    • space

      合法值 说明
      ensp 中文字符空格一半大小
      emsp 中文字符空格大小
      nbsp 根据字体设置的空格大小

3 媒体组件

  1. image

    • 功能描述:图片,支持 JPG、PNG、SVG、WEBP、GIF 等格式,2.3.0 起支持云文件ID。

    • 属性说明

      属性 类型 默认值 必填 说明
      src string   图片资源地址
      mode string scaleToFill 图片裁剪、缩放的模式
      webp boolean false 默认不解析 webP 格式,只支持网络资源
      lazy-load boolean false 图片懒加载,在即将进入一定范围(上下三屏)时才开始加载
      show-menu-by-longpress boolean false 长按图片显示发送给朋友、收藏、保存图片、搜一搜、打开名片/前往群聊/打开小程序(若图片中包含对应二维码或小程序码)的菜单。
      binderror eventhandle   当错误发生时触发,event.detail = {errMsg}
      bindload eventhandle   当图片载入完毕时触发,event.detail = {height, width}
    • mode

      合法值 说明
      scaleToFill 缩放模式,不保持纵横比缩放图片,使图片的宽高完全拉伸至填满 image 元素
      aspectFit 缩放模式,保持纵横比缩放图片,使图片的长边能完全显示出来。也就是说,可以完整地将图片显示出来。
      aspectFill 缩放模式,保持纵横比缩放图片,只保证图片的短边能完全显示出来。也就是说,图片通常只在水平或垂直方向是完整的,另一个方向将会发生截取。
      widthFix 缩放模式,宽度不变,高度自动变化,保持原图宽高比不变
      heightFix 缩放模式,高度不变,宽度自动变化,保持原图宽高比不变
      top 裁剪模式,不缩放图片,只显示图片的顶部区域
      bottom 裁剪模式,不缩放图片,只显示图片的底部区域
      center 裁剪模式,不缩放图片,只显示图片的中间区域
      left 裁剪模式,不缩放图片,只显示图片的左边区域
      right 裁剪模式,不缩放图片,只显示图片的右边区域
      top left 裁剪模式,不缩放图片,只显示图片的左上边区域
      top right 裁剪模式,不缩放图片,只显示图片的右上边区域
      bottom left 裁剪模式,不缩放图片,只显示图片的左下边区域
      bottom right 裁剪模式,不缩放图片,只显示图片的右下边区域

2.3 flex布局

  1. 一种非常方便的布局方式,在容器中记住4个样式即可。

    样式 说明
    display flex flex布局显示
    flex-direction row/column 指定是x或y轴
    justify-content value-1 指定轴的排列方式
    align-items value-1 指定斥轴的排列方式
  2. value-1

    value 说明
    flex-start 左对齐
    flex-end 右对齐
    space-around 平均分布
    space-between 无边框平均分布

2.4 像素

  1. px 写html都见过,是用来指定块级标签大小的。

  2. rpx 应该没有人见过,是为移动端所使用的动态像素,可以通过不同的屏幕大小调整像素大小。

2.5 跳转

1 通过事件跳转

  1. index.wxml

    <!-- bindtap绑定事件,data-是规定后面写变量名 -->
    <view bindtap="clickMe" data-nid="123" >点击跳转</view>
  2. index.js

    Component({
    /**
    * 组件的方法列表
    */
    methods: {
    /**
    * 绑定的事件
    */
    clickMe (e) {
    var nid = e.currentTarget.dataset.nid;
    console.log(nid);
    // 跳转
    wx.navigateTo({
    // 通过拼接来在url上携带数据
    url: '/pages/redirect/redirect?id='+nid,
    })
    }
    }
    })
  3. redirect.js

    Page({
    /**
    * 生命周期函数--监听页面加载
    */
    onLoad(options) {
    console.log(123456789);
    console.log(options);
    }
    })

2 通过标签跳转

  1. index.wxml

    <navigator url="/pages/redirect/redirect?nid=666">跳转页面</navigator>

2.6 数据绑定

  1. bind.wxml

    <!-- 数据绑定就这样写就对了 -->
    <text>李永好:{{message}}</text>
    <!-- 按钮标签 -->
    <button bindtap="changeData">点击知道你</button>
  2. bind.js

    // pages/redirect/redirect.js
    Page({
    /**
    * 页面的初始数据
    */
    data: {
    // 需要在data中写才能做到数据绑定。
    message:"傻到窒息"
    },
    changeData() {
    //this就代表page这个字典
    // 获取数据
    console.log(this.data.message);
    // 修改数据
    this.setData({message:"沙雕"})
    }
    })

2.7 提取用户信息

  1. login.wxml

    <view>图片:
    <image src="{{page}}" style="height:200rpx;width:200rpx;"></image>
    </view>
    <view>名称:{{name}}</view>
    <!-- 绑定事件 -->
    <button bindtap="getUserName">获取信息</button>
  2. login.js

    Page({
    /**
    * 页面的初始数据
    */
    data: {
    // 需要在data中写才能做到数据绑定。
    message:"傻到窒息",
    name:"傻逼",
    page:"/statlc/images/a.jpg"
    },
    getUserName(){
    //wx.openSetting({})
    // 应为调用了函数后this就不等于初始this了
    var that = this;
    // 调用微信的接口,获取当前用户信息
    wx.getUserProfile({
    desc: '获取用户信息',
    success:(res) => {
    // 调用成功后触发
    that.setData({
    // 拿到名称和图片并赋值
    name:res.userInfo.nickName,
    page:res.userInfo.avatarUrl
    })
    },
    fail(res){
    //调用失败后触发
    console.log("fail",res)
    }
    })
    },
  3. 可以通过wx.openSetting({})永久授权(无用)

2.8 选择定位

  • wxml

    <view bindtap="getLocalpath">{{localpath}}</view>
  • js

    // pages/redirect/redirect.js
    Page({
    /**
    * 页面的初始数据
    */
    data: {
    // 需要在data中写才能做到数据绑定。
    message:"傻到窒息",
    name:"傻逼",
    page:"/statlc/images/a.jpg",
    localpath:"请选择位置"
    },
    getLocalpath(){
    var that = this;
    // 选择位置
    wx.chooseLocation({
    success (res) {
    // 位置名称
    that.setData({localpath:res.address});
    }
    })
    },

2.10 for

  1. wxml

    <text>for</text>
    <view>
    <!-- 循环数据, -->
    <view wx:for="{{datalist}}" wx:key="index">{{index}}-{{item}}</view>
    <!-- 给指定的调用名称起别名 -->
    <view wx:for="{{datalist}}" wx:for-index="v" wx:for-item="value">{{v}}-{{value}}</view>
    </view>
    <view>
    <!-- 调用字典 -->
    {{userinfo.name}}
    {{userinfo.age}}
    </view>
    <view>
    <view wx:for="{{userinfo}}">{{index}}-{{item}}</view>
    </view>
  2. js

    data:{
    datalist:["猴","狗","李永昊"],
    userinfo:{
    name:"alex",
    age:18,
    }
    },

2.11 上传图片到内存

  1. wxml

    <!--pages/publish/publish.wxml-->
    <text>pages/publish/publish.wxml</text>
    <view bindtap="uploadImage">请上传图片</view>
    <view>
    <image style="width: 200rpx;height: 200rpx; margin-right: 5rpx;" wx:for="{{imageList}}" src="{{item}}"></image>
    </view>
  2. js

    // pages/publish/publish.js
    Page({
    /**
    * 页面的初始数据
    */
    data: {
    imageList:["/statlc/images/a.jpg",]
    },
    uploadImage(){
    var that = this;
    wx.chooseImage({
    //最大选择数
    count: 9,
    //图片个数original原图,compressed压缩图
    sizeType:["compressed","original"],
    //camera相机取图片,album本地相册
    sourceType:["album","camera"],
    success(res){
    //设置imagelist,页面上自动修改
    // that.setData({
    // imageList:res.tempFilePaths
    // });
    //先列表中添加单条数据
    // that.data.imageList.push("/statlc/images/a.jpg");
    //怎加图片并保留之前图片
    that.setData({
    imageList:that.data.imageList.concat(res.tempFilePaths)
    })
    }
    })
    }
    })

     

2.12 双向绑定

  1. wxml

    <view>电话号码:{{phone}}</view>
    <!-- 输入数据时触发bindinput的指定方法 -->
    <input value="{{phone}}"bindinput="bindPhone"placeholder="请输入手机号"/>
  2. js

    // pages/telphone/telphone.js
    Page({
    /**
    * 页面的初始数据
    */
    data: {
    phone:""
    },
    bindPhone(e){
    //e中会有从input输入的数据
    this.setData({phone:e.detail.value});
    },
    }

2.13 连接服务器

  1. 右上角点击详情里的本地设置。

  2. 选择不验证合法域名。。。

  3. wxml

    <view>验证码:</view>
    <input value="{{code}}"bindinput="bindCode"placeholder="请输入验证码"/>
    <button bindtap="login">登陆</button>
  4. js

    // pages/telphone/telphone.js
    Page({
    /**
    * 页面的初始数据
    */
    data: {
    code:""
    },
    bindCode(e){
    this.setData({code:e.detail.value});
    },
    login(){
    // xw.request向后台发送
    wx.request({
    url: 'http://127.0.0.1:8000/api/login/',
    data: {phone:this.data.phone,code:this.data.code},
    method: "POST",
    dataType:"JSON",
    success: (result) => {
    console.log(result)
    },
    fail: (res) => {},
    })
    },
    }

2.14 弹窗

  1. wx.showToast(Object object)

    属性 类型 默认值 必填 说明
    title string   提示的内容
    icon string success 图标
    image string   自定义图标的本地路径,image 的优先级高于 icon
    duration number 1500 提示的延迟时间
    mask boolean false 是否显示透明蒙层,防止触摸穿透
    success function   接口调用成功的回调函数
    fail function   接口调用失败的回调函数
    complete function   接口调用结束的回调函数(调用成功、失败都会执行)
  2. icon

    合法值 说明
    success 显示成功图标,此时 title 文本最多显示 7 个汉字长度
    error 显示失败图标,此时 title 文本最多显示 7 个汉字长度
    loading 显示加载图标,此时 title 文本最多显示 7 个汉字长度
    none 不显示图标,此时 title 文本最多可显示两行,1.9.0及以上版本支持
  3. js

    wx.showToast({
    // 要显示的错误信息
    title: '错误',
    // 要显示的图片
    icon:"none"
    })

2.15 页面之间数据传递

  1. 父页面

    1. wxml

      <text bindtap="getTopic">{{topicText}}</text>
    2. js

      Page({
      data: {
      topicText:"请选择话题",
      topiId:null
      },
      getTopic (){
      //跳转
      wx.navigateTo({
      url: '/pages/topic/topic',
      })
      },
      setTopicData(res){
      //设置data中的数据
      this.setData({
      topicText:res.title,
      topiId:res.id
      });
      },
      })
  2. 子页面

    1. wxml

      <view class="container">
      <view class="item" wx:for="{{topicList}}" bindtap="chostTopic" data-xx="{{item}}">
      <text>{{item.title}}</text>
      <text>{{item.count}}</text>
      </view>
      </view>
    2. wxss

      .item{
      padding: 40rpx;
      display: flex;
      flex-direction: row;
      justify-content: space-between;
      }
    3. js

      // pages/topic/topic.js
      Page({
      /**
      * 页面的初始数据
      */
      data: {
      topicList:[
      {id:1,title:"你吧把",count:10000},
      {id:1,title:"吧把",count:10},
      {id:1,title:"wo吧把",count:1010},
      {id:1,title:"我吧把",count:1200},
      {id:1,title:"你把",count:100},
      ]
      },
      chostTopic(e){
      //获取触发事件时传入的指定值
      var topicInfo = e.currentTarget.dataset.xx;
      // 把这个值传递给它的副页面
      // 他会按顺序拿到每个页面的对象。
      var pages = getCurrentPages();
      var prevPage = pages[pages.length-2];
      //调用对象的指定事件
      prevPage.setTopicData(topicInfo);
      //放回上级页面
      wx.navigateBack({});
      }
      })

2.16 进度条

  1. wxml

    <!--默认是绿色,也可以修改-->
    <progress percent="{{percent2}}" activeColer="#DC143C" ></progress>
    <view wx:for="{{imageList}}">
    <view>{{item.title}}</view>
    <progress percent="{{item.percent}}"></progress>
    </view>
    <button bindtap="changePercent">点击</button>
  2. js

    data: {
    imageList:[
    {id:1,title:"image1",percent:30},
    {id:2,title:"image2",percent:60},
    {id:3,title:"image3",percent:90},
    ]
    },
    changePercent(){
    var num =2;
    this.setData({
    //要实现嵌套数据的修改,必须使用这种
    //中括号包含字符型路径:数据
    //也可以使用字符拼接路径
    ["imageList[0].percent"]:80,
    ["imageList["+num+"].percent"]:100,
    })
    },

2.17 闭包

  1. 闭包是将数据封装后,异步后数据不会出现错误选定。

    var dataList = ["a","b","c"]
    //每个发出的repuest是一个包(将传入的值装入函数)
    //如果不写一个函数将值传入请求来后答应的会是“c”
    for (var i in dataList){
    (function(data){
    wx.repuest({
    url:"xxx",
    success(res){
    console.log(data);
    }
    })
    })(dataList[i])
    }
  2. 自执行函数

    (function(data){
    console.log(data);
    })("我是你baba")
    //用两个括号第一个括号写函数,第二个写要传入的参数。

2.18 下拉页面刷新

  1. json文件

    1. 全局

      window{
      //是否允许下拉刷新
      enablePullDownRefresh:True
      }
    2. 局部

      {
      //是否允许下拉刷新
      enablePullDownRefresh:True
      }

       

  2. wx.stopPullDownRefresh(Object object)

    1. 功能描述

      • 停止当前页面下拉刷新。

    2. 参数

      属性 类型 默认值 必填 说明
      success function   接口调用成功的回调函数
      fail function   接口调用失败的回调函数
      complete function   接口调用结束的回调函数(调用成功、失败都会执行)
    3. 示例代码

      Page({
      onPullDownRefresh () {
      wx.stopPullDownRefresh()
      }
      })
  3. wx.startPullDownRefresh(Object object)

    1. 功能描述

      • 开始下拉刷新。调用后触发下拉刷新动画,效果与用户手动下拉刷新一致。

    2. 参数

      属性 类型 默认值 必填 说明
      success function   接口调用成功的回调函数
      fail function   接口调用失败的回调函数
      complete function   接口调用结束的回调函数(调用成功、失败都会执行)
    3. 示例代码

      wx.startPullDownRefresh()

2.19 嵌套验证

  1. 后端在验证时只能验证一个值,使用下面的代码可以循环验证。

    from rest_framework import serializers
    class B(serializers.Serializer):
    pass
    class A(serializers.ModelSerializer):
    image = B(many=True) #如果不写many的话只接收一个,写了之后接收一个列表。
    pass

2.20 小程序api管理

  1. api在本地的时候使用的是127.0.0.1,而你要发布的时候需要将其改为自己的域名,使用以下代码可以简便处理api。

    // config/api.js
    var rootURL = "http://127.0.0.1:8000/api/"
    module.exports = { //如果想在其他文件调用就需要在这里写一下。
    indexUrl:rootURL + "news/"
    }
    // pages/news/news.js
    //使用require调用其他文件
    var AAAA = require("../../config/api.js")
    AAAA.indexUrl

2.21 后端数据初始化

  1. 后台数据初始化用于对一些钩子将数据获取后进行,据库的记录。

    import os
    import sys
    import django
    base_dir = os.path.dirname(os.path.dirname(os.path.abspath(_file_)))
    sys.path.append(base_dir)
    os.environ.setdefault("DJANGO_SETTINGS_MODULE","demos.settings")
    django.setup()
    pass
  2. 箭头函数和表的深操作

    function (opt){
    wx.request({
    ...
    success:(res) => { //这里的this就等于外面的this,这样写并不会新创建一个this
    this
    }
    })
    }
    from django.forms import model_to_dict
    model_to_dict(obj.user,fields=["id","nickanme"],exclude=[])
    # 使用它可以很轻松的将其使用,fields是需要的,exclude是不需要的。

2.22 分页器

  1. 在api中也带了,一个分页器帮助我们工作的。

    from rest_framework.pagination import LimitOffsetPagination
    # http://127.0.0.1:8000/api/news/?limit=2&offset=0
    class OldBoyLimitPagination(LimitOffsetPagination):
    default_limit = 5 # 一次几条数据
    max_limit =50 # 最大几条数据
    limit_query_param = "limit"
    offset_query_param = "offset"
    def get_offset(self,request): # 开始值
    return 0
    def get_paginated_response(self,data):
    return Response(data) # 他会帮我们套壳,但是我们不需要所以直接返回即可。
    class NewsView(ListAPIView):
    serializer_class = NewsModelSerializer
    queryset = models.News.objects.all().order_by("-id")
    pagination_class = OldBoyLimitPagination
  2. js的三元运算

    条件 ? 条件成立返回 : 条件不成立返回

2.23 瀑布流

  1. 瀑布流有两种实现方式,一种是靠样式实现,一种是标签实现。

    <!--样式实现-->
    <!--wxcc-->
    .falls{
    -moz-column-count:2;
    -webkit-column-count:2;
    column-count:2;
    -moz-column-gap:20rpx;
    -webkit-column-gap:20rpx;
    column-gap:20rpx;
    }
    .falls .item{
    break-inside:avoid-column;
    -webkit-column-break-inside:avoid;
    }
    <!--wxml-->
    <view class="falls">
    <view class='item'>
    <image src="..."></image>
    </view>
    <view class='item'>
    <image src="..."></image>
    </view>
    </view>
    <!--标签实现-->
    <!--wxcc-->
    .falls{
    display:flex;
    flex-direciton:row;
    }
    .falls .item{
    width:50%;
    }
    <!--wxml-->
    <view class="falls">
    <view class='item'>
    <image src="..."></image>
    </view>
    <view class='item'>
    <image src="..."></image>
    </view>
    </view>

2.24 自定义TabBar

  1. 根目录下创建一个文件夹“component”,在文件夹中创建文件夹"tabbar",在tabbar下右键新建Component。

  2. tabbar

    # tabbar.js
    // Component/tabbar.js
    Component({
    /**
    * 组件的属性列表
    */
    properties: {
    selected: {
    type:Number,
    value:0
    },
    },
    /**
    * 组件的初始数据
    */
    data: {
    color: "#7A7E83",
    selectedColor: "#3cc51f",
    list: [{
    pagePath: "/pages/index/index",
    iconPath: "/statlc/images/a.jpg",
    selectedIconPath: "/statlc/images/a.jpg",
    text: "home"
    }, {
    text: "fabu"
    },{
    pagePath: "/pages/logs/logs",
    iconPath: "/statlc/images/a.jpg",
    selectedIconPath: "/statlc/images/a.jpg",
    text: "Me"
    }]
    },
    /**
    * 组件的方法列表
    */
    methods: {
    switchTab(e) {
    var data = e.currentTarget.dataset
    var url = data.path;
    if(url){
    wx.switchTab({url});
    }else{
    if(false){ # 判断条件,自己写,判断登录的。
    wx.navigateTo({
    url: '/pages/pub/pub',
    })
    }else{
    wx.navigateTo({
    url: "/pages/atuh/atuh",
    })
    }
    }
    }
    }
    })
    # tabbar.json
    {
    "component": true,
    "usingComponents": {}
    }
    # tabbar.wxml
    <!--Component/tabbar.wxml-->
    <view class="tab-bar">
    <view class="tab-bar-border"></view>
    <view wx:for="{{list}}" wx:key="index" class="tab-bar-item" data-path="{{item.pagePath}}" bindtap="switchTab">
    <block wx:if="{{index === 1}}">
    <view>{{item.text}}</view>
    </block>
    <block wx:else>
    <image src="{{selected === index ? item.selectedIconPath : item.iconPath}}"></image>
    <view style="color: {{selected === index ? selectedColor : color}}">{{item.text}}</view>
    </block>
    </view>
    </view>
    # tabbar.wxss
    .tab-bar {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    height: 80px;
    background: white;
    display: flex;
    flex-direction: row;
    padding-bottom: env(safe-area-inset-bottom);
    pointer-events: auto;
    }
    .tab-bar-border {
    background-color: rgba(0, 0, 0, 0.33);
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 1px;
    transform: scaleY(0.5);
    }
    .tab-bar-item {
    flex: 1;
    text-align: center;
    display: flex;
    justify-content: center;
    align-items: center;
    flex-direction: column;
    }
    .tab-bar-item image {
    width: 27px;
    height: 27px;
    }
    .tab-bar-item view {
    font-size: 10px;
    }
  3. Me

    # me.json
    {
    "component": true,
    "usingComponents": {
    "tabbar":"/Component/tabbar/tabbar"
    }
    }
    # me.wxml
    <tabbar selected="{{2}}"></tabbar>
  4. home

    # home.json
    {
    "component": true,
    "usingComponents": {
    # 引入自定义的tabbar
    "tabbar":"/Component/tabbar/tabbar"
    }
    }
    # home.wxml
    <tabbar selected="{{2}}"></tabbar> # 使用自定义的tabbar,selected是传显示判断值的。
  5. app.json

    {
    "pages": [
    "pages/index/index",
    "pages/logs/logs",
    "pages/redirect/redirect",
    "pages/userinfo/userinfo",
    "pages/publish/publish",
    "pages/telphone/telphone",
    "pages/atuh/atuh",
    "pages/pub/pub",
    "pages/topic/topic",
    "pages/progress/progress",
    "pages/logss"
    ],
    "window": {
    "navigationBarBackgroundColor": "#FFDAB9",
    "navigationBarTextStyle": "black",
    "navigationBarTitleText": "Demo"
    },
    "tabBar": {
    "custom": true, # 取消默认tabbar的显示
    "list": [
    {
    "pagePath": "pages/index/index",
    "text": "首页"
    },
    {
    "pagePath": "pages/logs/logs",
    "text": "我的"
    }
    ]
    },
    "sitemapLocation": "sitemap.json"
    }

     

     

3.登录注册

3.1 小程序

1.app设置

  1. app.js

    App({
    /**
    * 当小程序初始化完成时,会触发 onLaunch(全局只触发一次)
    */
    onLaunch: function () {
    //清理在session的数据
    //wx.removeStorageSync('userinfo')
    var userinfo = wx.getStorageSync('userinfo');
    if(userinfo){
    //在内存中存储一份,加快读取速度。
    this.globalData.userinfo=userinfo
    }
    },
    globalData:{
    userinfo:{nickName:"登录",
    url:"/pages/atuh/atuh"
    }
    },
    initUserInfo(res,localInfo){
    var info = {
    token:res.token,
    phone:res.phone,
    nickName:localInfo.nickName,
    avatarUrl:localInfo.avatarUrl,
    url:"null"
    }
    //1.去公共的app.js中调用globalData,里面赋值
    this.globalData.userinfo = info;
    // 在本地“cookie”中赋值(并不是cookie只是功能一样,就这样叫)
    wx.setStorageSync('userinfo',info);
    },
    })

2.我的页面

  1. my.wxml

    <view class="main_candidate">
    <view class="inputbox flex">
    <text class="input-label">手机号</text>
    <input name="name" placeholder="请输入手机号" bindinput="bindPhone" maxlength="11" class="primary" value="{{phone}}"/>
    </view>
    <view class="inputbox flex">
    <text class="input-label" >验证码</text>
    <input name="code" placeholder="请输入验证码" bindinput="bindCode" maxlength="4" class="primary" value="{{code}}"/>
    <button class="getCode" bindtap="messageCode">发送验证码</button>
    </view>
    <button class="login" form-type="submit"bindtap="onClicksubmit">立即登录</button>
    <view class="register">
    <text bindtap="register">没有账号?去注册</text>
    </view>
    </view>
  2. my.wxss

    /* 使用page就是为了保证 满屏 */
    page {
    width: 100%;
    height: 100%;
    }
    .view_contain {
    width: 100%;
    height: 100%;
    background: #f0eeed
    }
    /* 第一部分 */
    .view_1 {
    display: flex;
    justify-content: center;
    width: 100%;
    height: 25%;
    background: #a0deee;
    }
    .view_image_text {
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-direction: column;
    }
    .image_radius {
    height: 50px;
    width: 50px;
    border-radius: 30px;
    }
    /* 第二部分 */
    .view_2 {
    width: 100%;
    height: 15%;
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: center;
    background: white;
    }
    .view_tupianwenzi {
    display: flex;
    flex-direction: column;
    width: 120rpx;
    align-items: center;
    margin-left: 25rpx;
    margin-right: 25rpx;
    }
    .image_tupian {
    display: flex;
    width: 100rpx;
    height: 100rpx;
    }
    /* 第三部分 */
    .view_3 {
    width: 100%;
    height: 50%;
    /* background: #f0eeed; */
    }
    .list-item {
    display: flex;
    flex-direction: row;
    align-items: center;
    width: 100%;
    height: 80rpx;
    margin-top: 20rpx;
    position: relative; /*父元素位置要设置为相对*/
    background: white;
    }
    .item-image {
    width: 50rpx;
    height: 50rpx;
    margin: 20rpx;
    }
    .item-text {
    color: gray;
    font-size: 35rpx;
    margin-left: 20rpx;
    }
    .image-jiantou {
    width: 20rpx;
    height: 35rpx;
    position: absolute; /* 要约束所在位置的子元素的位置要设置成绝对 */
    right: 0; /* 靠右调节 */
    margin-right: 35rpx;
    }
    /* 黑线 使得产生阴影效果 */
    .line {
    width: 100%;
    height: 3rpx;
    background: lightgray;
    margin-left: 90rpx;
    }
  3. my.js

    //获取全局app文件的数据
    var app = getApp();
    Page({
    /**
    * 页面的初始数据
    */
    data: {
    phone:"登录"
    },
    /**
    * 生命周期函数--监听页面加载(第一次访问时执行)
    */
    onLoad(options) {},
    /**
    * 生命周期函数--监听页面初次渲染完成(第一次访问时执行)
    */
    onReady() {},
    /**
    * 生命周期函数--监听页面显示(每次访问时执行)
    */
    onShow() {
    //本地storage中获取值
    var phone = wx.getStorageSync('phone');
    if(phone){
    this.setData({
    //查全局文件的globalData
    // phone:app.globalData.phone
    phone:phone
    })
    }else{
    this.setData({
    phone:"登录"
    })
    }
    },
    })
    // pages/logs/logs.js
    var app = getApp();
    Page({
    /**
    * 页面的初始数据
    */
    data: {
    userinfo:"登录"
    },
    /**
    * 生命周期函数--监听页面显示(每次访问时执行)
    */
    onShow() {
    //本地storage中获取值
    this.setData({
    userinfo:app.globalData.userinfo
    })
    },
    })

3.登录页面

  1. login.wxml

    <view class="main_candidate">
    <view class="inputbox flex">
    <text class="input-label">手机号</text>
    <input name="name" placeholder="请输入手机号" bindinput="bindPhone" maxlength="11" class="primary" value="{{phone}}"/>
    </view>
    <view class="inputbox flex">
    <text class="input-label" >验证码</text>
    <input name="code" placeholder="请输入验证码" bindinput="bindCode" maxlength="4" class="primary" value="{{code}}"/>
    <button class="getCode" bindtap="messageCode">发送验证码</button>
    </view>
    <button class="login" form-type="submit"bindtap="login">立即登录</button>
    <view class="register">
    <text bindtap="register">没有账号?去注册</text>
    </view>
    </view>
  2. login.wxss

    .input-label {
    color: #888;
    font-size: 12pt;
    height: 25rpx;
    line-height: 25rpx;
    padding: 0 25rpx;
    border-right: 1px solid #d8d8d8;
    }
    .main_candidate{
    width: 100%;
    height: 100%;
    background-color: #ffffff;
    margin-top: 30px;
    }
    .inputbox{
    padding-left: 6px;
    box-sizing: border-box;
    border-bottom: 1px solid #dadada;
    width: 100%;
    height: 50px;
    line-height: 50px;
    font-size: 14px;
    background-color: #fff;
    }
    .flex{
    border-radius: 5px;
    border: 2px solid #f4f4f4;
    display: flex;
    align-items: center;
    margin: 40rpx 0;
    }
    .primary{
    flex:1;
    }
    .inputbox button{
    width: 110px;
    height: 38px;
    color:#fff;
    background-color: #5dd5c8;
    font-size: 16px;
    }
    .login{
    margin-top: 20px;
    background-color: #5dd5c8;
    color: #fff;
    font-size: 20px;
    }
    .register{
    color: blue;
    font-size: 16px;
    margin: 0 auto;
    width: 40%;
    margin-top: 10px;
    }
  3. login.js

    // pages/atuh/atuh.js
    var app = getApp();
    Page({
    /**
    * 页面的初始数据
    */
    data: {
    phone:"",
    code:""
    },
    bindPhone(e){
    this.setData({phone:e.detail.value});
    },
    bindCode(e){
    this.setData({code:e.detail.value});
    },
    /**
    * 发送短信验证码
    */
    messageCode(e){
    // 输入的数据不等于11执行
    if (this.data.phone.length !=11){
    //弹窗
    wx.showToast({
    title: '手机号长度错误',
    icon:"none"
    })
    return;
    }
    //正则匹配手机格式
    //"/xxx/"中写正则
    var reg = /^(1[3|4|5|6|7|8|9]\d{9})$/;
    if(!reg.test(this.data.phone)){
    //弹窗
    wx.showToast({
    title: '手机号长度错误',
    icon:"none"
    })
    return;
    }
    //发送短信验证码,登陆成功之后获取jwt和微信用户信息,保存到globalData和本地存储中。
    wx.request({
    url: 'http://127.0.0.1:8000/api/message/',
    data: {phone:this.data.phone},
    method: "GET",
    success: (result)=>{
    if(result.data.status){
    //倒计时计数器
    wx.showToast({title: result.data.message,icon:"none"})
    }else{
    //短信发送失败
    wx.showToast({title: result.data.message,icon:"none"})
    }
    },
    })
    },
    /**
    * 用户登陆
    */
    onClicksubmit(){
    // xw.request向后台发送
    wx.request({
    url: 'http://127.0.0.1:8000/api/login/',
    data: {phone:this.data.phone,code:this.data.code},
    method: "POST",
    header:{Authorization:"token dfgrthtryhtjh"},
    success: (result) => {
    if(result.data.status){
    wx.getUserProfile({
    desc: '获取用户信息',
    success:(res) => {
    // 初始化用户信息
    app.initUserInfo(result.data.data,res.userInfo);
    //登陆成功跳到上一级
    wx.navigateBack({});
    }
    })
    //查看跳转层级记录
    //var pages = getCurrentPages();
    //prevPage = pages[pages.length-2]
    }else{
    wx.showToast({title: result.data.message,})
    }
    }
    })
    }
    })

3.2 后端

1.login_api

  1. models.py

    from django.db import models
    class UserInfo(models.Model):
    #创建唯一索引
    phone = models.CharField(verbose_name="手机号",max_length=11,unique=True)
    token = models.CharField(verbose_name="用户TOKEN",max_length=64,null=True,blank=True)
  2. urls.py

    from django.contrib import admin
    from django.urls import path,re_path,include
    from api1 import views
    urlpatterns = [
    path('admin/', admin.site.urls),
    re_path('^login/', views.ListView.as_view()),
    re_path('^message/', views.MessageView.as_view()),
    re_path('^credential/', views.CredentialView.as_view()),
    ]
  3. views.py

    from rest_framework.response import Response
    from rest_framework.views import APIView
    from . import models
    import random
    import uuid
    # Create your views here.
    # value就是指定字段的指定数据
    from .serializer import account
    class MessageView(APIView):
    def get (self, request, *args, **kwargs):
    # 1.获取手机号
    # 2.手机格式校验
    ser = account.MessageSerialiaer(data=request.query_params)
    if not ser.is_valid():
    return Response({"status":False,"message":"手机格式错误"})
    phone = ser.validated_data.get("phone")
    # 3.生成随机验证码
    random_code = random.randint(1000,9999)
    # 4.验证码发送到手机上,购买服务器进行发送短信:腾讯云
    from .utils.tencent import msg
    result = msg.send_message(phone,random_code)
    if not result:
    return Response({"status":False,"message":"短信发送失败"})
    # 5.吧验证码+手机号保留(过期时间30s)
    # 5.1 搭建redis服务器(云redis)
    # 我这里没redis就不用了
    from django_redis import get_redis_connection
    conn = get_redis_connection()
    conn.set(phone,random_code,ex=60)
    return Response({"status":True,"message":"发送成功"})
    class ListView(APIView):
    def post (self, request, *args, **kwargs):
    print(request.data)
    """
    1.校验手机号是否合法
    2.校验验证码,redis
    验证码获取状态:无数据/有错误/有无错误
    3.去数据库中获取用户信息
    4.将一些信息返回给小程序
    """
    ser = account.LoginSerializer(data=request.data)
    if not ser.is_valid():
    return Response({"status":False,"message":"验证码错误"})
    # 验证后使用它获取数据
    phone = ser.validated_data.get("phone")
    # 会帮你做如果有则返回一个查询对象的参数和一个逻辑值有是真没有是假,如果没有则创建并返回数据和False
    user_object,flag = models.UserInfo.objects. get_or_create(phone=phone)
    # 修改或添加token的值
    user_object.token = str(uuid.uuid4())
    user_object.save()
    return Response({"status":True,"data":{"token":user_object.token,"phone":phone}})
    class CredentialView(APIView):
    def get (self,*args,**kwargs):
    import json
    import os
    from sts.sts import Sts
    config = {
    'url': 'https://sts.tencentcloudapi.com/',
    # 域名,非必须,默认为 sts.tencentcloudapi.com
    'domain': 'sts.tencentcloudapi.com',
    # 临时密钥有效时长,单位是秒
    'duration_seconds': 1800,
    'secret_id': 'AKIDzAB4nInCPOAk5DrMhoheMogIgqjNn35s',
    # 固定密钥
    'secret_key': 'c09IAm9HCxZ4Vu9AwTSFsPq6A2f7MK5b',
    # 换成你的 bucket
    'bucket': 'static-1312898916',
    # 换成 bucket 所在地区
    'region': 'ap-nanjing',
    # 这里改成允许的路径前缀,可以根据自己网站的用户登录态判断允许上传的具体路径
    # 例子: a.jpg 或者 a/* 或者 * (使用通配符*存在重大安全风险, 请谨慎评估使用)
    'allow_prefix': '*',
    # 密钥的权限列表。简单上传和分片需要以下的权限,其他权限列表请看 https://cloud.tencent.com/document/product/436/31923
    'allow_actions': [
    # 简单上传
    'name/cos:PostObject',
    ],
    }
    try:
    sts = Sts(config)
    response = sts.get_credential()
    print('get data : ' + json.dumps(dict(response), indent=4))
    return Response(response)
    except Exception as e:
    print(e)
1.serializer
  1. account.py

    # !/usr/bin/env python
    # -*- coding:utf-8 -*-
    from rest_framework import serializers
    from .validators import phone_validator
    from django_redis import get_redis_connection
    from rest_framework.exceptions import ValidationError
    # 我们没有号码对应的表所以需要继承serializers.Serializer
    class MessageSerialiaer(serializers.Serializer):
    # 数据进来后先去判断是否为空,再去判断列表中的函数
    phone = serializers.CharField(label="手机号",validators=[phone_validator,])
    class LoginSerializer(serializers.Serializer):
    #这些都是自定义的验证字段
    # 数据进来后先去判断是否为空,再去判断列表中的函数
    phone = serializers.CharField(label="手机号",validators=[phone_validator,])
    code = serializers.CharField(label="短信验证码",)
    #钩子验证code字段validate_+字段名
    def validate_code(self,value):
    #应该写开应为主动推送的不一样
    if len(value) !=4 or not value.isdecimal():
    #主动推送错误信息
    raise ValidationError("短信格式错误")
    #在函数内部取值时,使用initial_data应为用了request.data后会将其赋值给它
    phone = self.initial_data.get("phone")
    #去取验证码
    """
    conn = get_redis_connection()
    code = conn.get()
    if not code:
    raise ValidationError("验证码过期")
    if value != code.decode("utf-8"):
    raise ValidationError("验证码错误")
    """
    return value
  2. validators.py

    # !/usr/bin/env python
    # -*- coding:utf-8 -*-
    import re
    from rest_framework.exceptions import ValidationError
    def phone_validator(value):
    if not re.match("^(1[3|4|5|6|7|8|9]\d{9})$",value):
    # 主动推送特定的错误
    raise ValidationError("手机号格式错误")
2.utils
  1. \tencent\msg.py

    # !/usr/bin/env python
    # -*- coding:utf-8 -*-
    from tencentcloud.common import credential
    from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
    from tencentcloud.sms.v20190711 import sms_client,models
    def send_message(phone,code):
    # TODO tencent.send_message(phone,random_code)
    """
    1.注册腾讯云,开通腾讯云短信。
    2.创建应用
    sdk appid = 1400705782
    3.申请签名(个人:小程序)
    476491 python小程序
    4.申请模板
    1473776 普通短信
    5.申请腾讯云api https://console.cloud.tencent.com/cam/capi
    appid 1312898916
    SecretId AKIDzAB4nInCPOAk5DrMhoheMogIgqjNn35s
    SecretKey c09IAm9HCxZ4Vu9AwTSFsPq6A2f7MK5b
    6.调用相关接口去发送短信
    pip install --upgrade tencentcloud-sdk-python
    sdk,写好的工具
    """
    try:
    phone = "{}{}".format("+86",phone)
    cred = credential.Credential("AKIDW3Rgszw84ylQxMzNn7KOJ6kFPSLSL5c5MU","GQSMXmtsjR0QhuIalzTp250nU6digZSD")
    client = sms_client.SmsClient(cred,"ap-guangzhou")
    req = models.SendSmsRequest()
    # 短信应用id
    req.SmsSdkAppid = "1400302209"
    # 短信签名内容
    req.Sign = "python之路"
    # 下发手机号+86标准
    req.PhoneNumberSet = [phone]
    # 模板id
    req.TemplateID = "516680"
    # 模板参数
    req.TemplateParamSet = [code]
    # 发送短信
    resp = client.SendSms(req)
    # 输出json格式的字符串回包
    if resp.SendStatusSet[0].Code == "Ok":
    return True
    #print(resp.to_json_string(indent=2))
    except TencentCloudSDKException as err:
    #print(err)
    pass

2.settings

  1. urls.py

    from django.contrib import admin
    from django.urls import path,re_path,include
    from api1 import urls
    urlpatterns = [
    path('admin/', admin.site.urls),
    re_path("^api/", include("api1.urls")),
    ]
  2. setting.py

    from pathlib import Path
    INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'api1.apps.Api1Config'
    ]
    DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
    # 配置django-redis
    CACHES = {
    "default":{
    "BACKEND":"django_redis.cache.RedisCache",
    # 主机ip和端口
    "LOCATION":"redis://127.0.0.1:6000",
    "OPTIONS":{
    "CLIENT_CLASS":"django_redis.client.DefaultClient",
    # 最大访问数
    "CONNECTION_POOL_KWARGS":{"max_connections":100}
    # 密码
    # "POASSWORD":"密码",
    }
    }
    }

3.3 知识梳理

  1. 全局数据和局部数据

    1. app.js中的app{globalData}中存放全局应用数据。

      1. 用它获取全局数据getApp();

    2. 每个页面中有.js的文件中的page{data}中存放局部数据。

      1. 用它获取局部数据this.data(this要看是谁的对象)

  2. 使用到的api(详细请到官网浏览)

    1. 登录时用到的wx.wx.request({})向后台发送请求。

    2. 提醒用户时用到wx.showToast({})弹出提示框(无交互)。

    3. 在类似cookie中存储数据用到wx.setStorageSync('key',value)(在本地存储,并不是cookie只是功能一样,就这样叫)。

    4. 清理在session的数据用到wx.removeStorageSync('key')。

    5. 获取在session的数据用到wx.getStorageSync('key')。

    6. 用到wx.getUserProfile({})来获取用户信息。

    7. 执行后页面跳回上一个页面用到wx.navigateBack({})在要执行的函数中加入。

  3. 全局事件和局部事件

    1. app.js中有onLaunch: function () {}当小程序初始化完成时,会触发(全局只触发一次)。

    2. 每个页面都会的

      1. 显示前触发

        1. 页面的初始数据data: {}

        2. 生命周期函数--监听页面加载onLoad(options) {}

        3. 生命周期函数--监听页面初次渲染完成onReady() {}

      2. 用户加载/跳转/关闭时触发

        1. 生命周期函数--监听页面显示onShow() {}

        2. 生命周期函数--监听页面隐藏onHide() {}

        3. 生命周期函数--监听页面卸载onUnload() {}

      3. 用户触发

        1. 页面相关事件处理函数--监听用户下拉动作onPullDownRefresh() {}

        2. 页面上拉触底事件的处理函数onReachBottom() {}

        3. 用户点击右上角分享onShareAppMessage() {}

4.云存储

3.1 阶段

  1. 第一阶段:文件服务器,将文件存储在某个指定的服务器上(支持目录结构的划分)。

  2. 第二阶段:分为文件服务器和对象服务器,对象存储优化了存储和操作但是无目录结构。

  3. 第三阶段:使用云服务存储,实现为每个人提供存储服务器。

3.2 小程序

  1. 登录腾讯云并访问https://console.cloud.tencent.com/cos/bucket

  2. 点击存储桶列表并创建桶、右上角选择sdk文档中的小程序中详细介绍。

  3. wxml

    <!--pages/publish/publish.wxml-->
    <text>pages/publish/publish.wxml</text>
    <view bindtap="uploadImage">请上传图片</view>
    <view>
    <image style="width: 200rpx;height: 200rpx; margin-right: 5rpx;" wx:for="{{imageList}}" src="{{item}}"></image>
    </view>
    <view bindtap="uploadFile">点击上传</view>
  4. js

    // pages/publish/publish.js
    var COS = require("../../utils/cos-wx-sdk-v5.js")
    Page({
    /**
    * 页面的初始数据
    */
    data: {
    imageList:[],
    onlineImageList:[]
    },
    uploadImage(){
    var that = this;
    wx.chooseImage({
    count: 9,
    sizeType:["compressed","original"],
    sourceType:["album","camera"],
    success(res){
    //设置imagelist,页面上自动修改
    // that.setData({
    // imageList:res.tempFilePaths
    // });
    //先列表中添加单条数据
    // that.data.imageList.push("/statlc/images/a.jpg");
    //怎加图片并保留之前图片
    that.setData({
    imageList:that.data.imageList.concat(res.tempFilePaths)
    })
    }
    })
    },
    uploadFile(){
    // 存储图片在服务器上的url
    var onlineImageList = [];
    var that = this;
    //创建连接对象
    var cos = new COS({
    // 必选参数
    getAuthorization: function (options, callback) {
    // 服务端其他语言参考 COS STS SDK :https://github.com/tencentyun/qcloud-cos-sts-sdk
    // STS 详细文档指引看:https://cloud.tencent.com/document/product/436/14048
    wx.request({
    url: 'http://127.0.0.1:8000/api/credential/',
    data: {
    // 可从 options 取需要的参数
    },
    success: function (result) {
    var data = result.data;
    var credentials = data && data.credentials;
    if (!data || !credentials) return console.error('credentials invalid');
    callback({
    TmpSecretId: credentials.tmpSecretId,
    TmpSecretKey: credentials.tmpSecretKey,
    // v1.2.0之前版本的sdk使用XCosSecurityToken而不是SecurityToken
    SecurityToken: credentials.sessionToken,
    // 建议返回服务器时间作为签名的开始时间,避免用户浏览器本地时间偏差过大导致签名错误
    StartTime: data.startTime, // 时间戳,单位秒,如:1580000000
    ExpiredTime: data.expiredTime, // 时间戳,单位秒,如:1580000900
    });
    }
    });
    }
    });
    // var cos = new COS({
    // SecretId: 'AKIDzAB4nInCPOAk5DrMhoheMogIgqjNn35s',
    // SecretKey: 'c09IAm9HCxZ4Vu9AwTSFsPq6A2f7MK5b',
    // });
    for(var index in this.data.imageList){
    var filePath = this.data.imageList[index];
    // 先选择文件,得到临时路径
    cos.postObject({
    Bucket:"db-1316378440",
    Region:"ap-nanjing",
    Key:index+"xzx.png",
    FilePath:filePath,
    onProgress(info){
    console.log(JSON.stringify(info));
    }
    }, function (err,data){
    console.log(data);
    onlineImageList.push(data.Location);
    })
    }
    }
    })

3.3 后端

  1. 云存储前后台需要将oss临时上传下载密钥发给小程序。

1 api

  1. urls

    from django.urls import re_path
    from api1 import views
    urlpatterns = [
    re_path('^credential/', views.CredentialView.as_view()),
    ]
  2. view

    from rest_framework.response import Response
    from rest_framework.views import APIView
    class CredentialView(APIView):
    def get (self,*args,**kwargs):
    import json
    import os
    from sts.sts import Sts
    config = {
    'url': 'https://sts.tencentcloudapi.com/',
    # 域名,非必须,默认为 sts.tencentcloudapi.com
    'domain': 'sts.tencentcloudapi.com',
    # 临时密钥有效时长,单位是秒
    'duration_seconds': 1800,
    'secret_id': 'AKIDzAB4nInCPOAk5DrMhoheMogIgqjNn35s',
    # 固定密钥
    'secret_key': 'c09IAm9HCxZ4Vu9AwTSFsPq6A2f7MK5b',
    # 换成你的 bucket
    'bucket': 'static-1312898916',
    # 换成 bucket 所在地区
    'region': 'ap-nanjing',
    # 这里改成允许的路径前缀,可以根据自己网站的用户登录态判断允许上传的具体路径
    # 例子: a.jpg 或者 a/* 或者 * (使用通配符*存在重大安全风险, 请谨慎评估使用)
    'allow_prefix': '*',
    # 密钥的权限列表。简单上传和分片需要以下的权限,其他权限列表请看 https://cloud.tencent.com/document/product/436/31923
    'allow_actions': [
    # 简单上传
    'name/cos:PostObject',
    ],
    }
    try:
    sts = Sts(config)
    response = sts.get_credential()
    print('get data : ' + json.dumps(dict(response), indent=4))
    return Response(response)
    except Exception as e:
    print(e)

3.4 提交和获取新闻

  1. api/url

    path("^news/$",news.NewsView.as_view())
  2. api/view

    class CreateNewsTopicModelSerializer(serializers.Serializer):
    key = serializers.CharField()
    cos_path = serializers.CharField()
    class CreateNewsModelSerializer(serializers.ModelSerializer):
    imageList = CreateNewsTopicModelSerializer(many=True)
    class Meta:
    model = models.News
    exclude = ["user","viewer_count","comment_count"]
    def create(self,validated_data):
    image_list = validated_data.pop("imageList")
    news_object = models.News.objects.create(**validated_data)
    data_list = models.NewsDetail.objects.bulk_create(
    [models.NewsDetail(**info,news=news_object)for info in image_list]
    )
    news_object.imageList = data_list
    if news_object.topic:
    news_object.topic.count +=1
    news_object.save()
    return news_object
    class NewsView(CreateAPIView,ListAPIView):
    queryset=models.News.objects.prefetch_related("user","topic").order_by("-id")
    filter_backends = [ReachBottomFilter,PullDownRefreshFilter]
    def perform_create(self,serializer):
    new_object = serializer.save(user_id=1)
    return new_object
    def get_serializer_class(self):
    if self.request.method == "POST":
    return CreateNewsModelsSerializer
    if self.request.method == "GET":
    return ListNewsModelsSerializer
  3. 当你访问网页时会发现新闻网可以查看新闻,但是点赞或评论时需要你登录后才可以,通过api我们要实现这种其实也不难。在执行认证之前会先执行get_authenticators函数,它会将认证返回。我们可以自己编写和返回需要的认证。

    Class CommentView(APIView):
    def get_authenticators(self):
    if self.request.method == "POST":
    return [UserAuthentication(),]
    return [GeneralAuthentication(),]
    def get(self, request, *args, **kwargs):
    pass
    def post(self, request, *args, **kwargs):
    pass
  4. 当我们登录注册后发现有些页面可以分享但是有些不可以分享,有两种方式可以实现,第一种是找到页面的js文件中的onShareAppMessage将其注释即可,第二种是在onload中写入wx.hideShareMenu({});实现。

5.微信支付

  1. 微信小程序平台需要是企业级、在微信支付中还需要绑定商户平台账号也是企业级的。

5.1 小程序

1.app设置

  1. app.json

    {
    "pages": [
    "pages/home/home",
    "pages/logs/logs"
    ],
    "window": {
    "navigationBarBackgroundColor": "#FFDAB9",
    "navigationBarTextStyle": "black",
    "navigationBarTitleText": "Demo"
    },
    "tabBar": {
    "list": [{
    "pagePath": "pages/home/home",
    "text": "home"
    }, {
    "pagePath": "pages/logs/logs",
    "text": "logs"
    }]
    }
    }

2.主页页面

  1. home.wxml

    <radio-group bindchange="changeGoods">
    <view class="row" wx:for="{{goodsList}}" wx:key="index">
    <text>{{item.title}}-{{item.price}}</text>
    <radio class="radio" value="{{item.id}}"></radio>
    </view>
    </radio-group>
    <button bindtap="doPay">购买</button>
  2. home.wxss

    .row{
    display:flex;
    flex-direction:row;
    justify-content:space-between;
    align-items:center;
    }
  3. home.js

    const app = getApp()
    Page({
    data:{
    goodsList:[],
    seletedId:null
    },
    doPayment:function(){
    this.data.seletedId
    wx.request({
    url:"http://127.0.0.1:8002/payment/",
    data:{
    goodsId:this.data.seletedId
    },
    method:"POST",
    dataType:"json",
    responseType:"text",
    success:(res) =>{
    wx.requestPayment(
    {
    "timeStamp":res.data.timeStamp,
    "nonceStr":res.data.nonceStr,
    "package":res.data.package,
    "signType":res.data.signType,
    "paySign":res.data.paySign,
    "success":function(res){},
    "fail":function(res){},
    "complete":function(res){}
    }
    )
    }
    })
    },
    changeGoods:function(e){
    this.setData({
    seletedId:e.detail.value
    })
    }
    onLoad:function(){
    wx.request({
    url:"http://127.0.0.1:8002/goods/",
    method:"GET",
    dataType:"json",
    responseType:"text",
    success:(res) = >{
    this.setData({
    goodsList:res.data
    })
    },
    })
    }
    })

3.登录页面

  1. logs.wxml

    <input placeholder="请输入手机号" bindinput="inputPhone" value="{{phone}}"></input>
    <button open-type="getUserInfo" bindgetuserinfo="doSubmit">login</button>
  2. logs.wxss

     
  3. logs.js

    data:{
    phone:null
    }
    inputPhone:function(e){
    this.setData({
    phone:e.detail.value
    })
    },
    doSubmit:function(e){
    wx.login({
    success:(result) => {
    wx.request({
    url:"http://127.0.0.1:8002/login",
    data:{
    phone:this.data.phone,
    //获取一个临时凭证(只能用一次/5分钟)
    wx_code:result.code
    },
    method:"POST",
    dataType:"json",
    responseType:"text",
    success:(res)=>{
    console.log(res)
    }
    })
    }
    })
    }

5.2 后端

1.app01

  1. models.py

    from django.db import models
    class UserInfo(models.Model):
    phone = models.CharField(verbose_name="手机号",max_length=11,unique=True)
    token = models.CharField(verbose_name="用户TOKEN",max_length=64,null=True,blank=True)
    openid = models.CharField(verbose_name="微信唯一标识",max_length=32)
    class Goods(models.Model):
    title = models.CharField(verbose_name="商品名称", max_length=32)
    price = models.PositiveIntegerField(verbose_name="价格")
    class Order(models.Model):
    status_choices = (
    (1,"待支付"),
    (2,"已支付")
    )
    status = models.SmallIntegerField(verbose_name="状态",choices=status_choices)
    goods = models.ForeignKey(verbose_name="商品",to="Goods")
    user = models.ForeignKey(verbose_name="用户",to="UserInfo")
    uid = models.Charfield(verbose_name="订阅号", max_length=64)
  2. urls.py

    from django.contrib import admin
    from django.urls import path,re_path,include
    from api1 import views
    urlpatterns = [
    path('admin/', admin.site.urls),
    re_path('^login/', views.ListView.as_view()),
    re_path('^goods/', views.GoodsView.as_view()),
    re_path('^payment/', views.PaymentView.as_view()),
    re_path('^notify/', views.NotifyView.as_view()),
    ]
  3. views.py

    from django.shortcuts import render
    from rest_framework.views import APIView
    from rest_framework.response import Response
    from . import models
    import uuid
    import random
    import requests
    from rest_framework.generics import ListAPIView
    from rest_framework import serializers
    class LoginView(APIView):
    def post(self, request,*args,**kwargs):
    phone = request.data.get("phone")
    wx_code = request.data.get("wx_code")
    # openid的获取:需要拿着wx_code去微信申请
    info = {
    "appid":"wx55cca0b94f723dc7",
    "secret":"c000e3ddc95d2ef723b9b010f0ae05d5",
    "js_code":wx_code,
    "grant_type":"authorization_code",
    }
    result = requests.get(url="https://api.weixin.qq.com/sns/jscode2session",params=info)
    openid = result.json()["openid"]
    exists = models.UserInfo.objects.filter(phone=phone).exists()
    token = str(uuid.uuid4())
    if not exists:
    models.UserInfo.objects.create(
    phone = phone,
    token = token,
    openid = openid
    )
    else:
    models.UserInfo.objects.filter(phone=phone).update(token=token,openid = openid)
    return Response({"token":token})
    class GoodsModelSerializer(serializers.ModelSerializer):
    class Meta:
    model = models.Goods
    filds = "__all__"
    def md5(string):
    import hashlib
    m = hashlib.md5()
    m.update(string.encode("utf-8"))
    return m.hexdigest()
    class GoodsView(ListAPIView):
    queryset = models.Goods.objects
    return Response(queryset)
    class PaymentView(APIView):
    def post(self,request,*args,**kwargs):
    goods_id = request.data.get("goodsId")
    order_random_string = str(int(time.time()))
    user_object = models.UserInfo.objects.filter(id=1).first()
    goods_object = models.Goods.objects.filter(id=goods_id).first()
    order_object = models.Order.objects.create(goods=goods_object,user==user_object,uid=order_random_string,status=1)
    # 按照微信的规则,去生成支付需要的一大堆数据
    # 1.调用支付统一下单
    info = {
    "appid":"wx55cca0b94f723dc7",
    "mch_id":"1526049051",
    "device_info":"wupeiqi-min-program",
    "nonce_str":"".join([chr(random.randit(65, 90)) for _ in range(12)]),
    "sign_type":"MD5",
    "body":"保证经",
    "detail":"这是一个产品详细信息",
    "attach":"微信小程序",
    "out_trade_no":order_random_string,
    "total_fee":goods_object.price,
    "spbill_create_ip":request.META.get("REMOTE_ADDR"),
    #修改为自己的就可以了,返回的信息就可以接收到了
    # http://ip:端口/notify/
    "notify_url":"http://47.93.4.198:8012/pay/notify/",
    "trade_type":"JSAPI",
    "openid":user_object.openid
    }
    # 1.1 签名
    pay_key = "2SzCvaKgYExuItWBdfsadfasdf"
    temp = "&".join(["{0}={1}".format(k, info[k]) for k in sorted(info)] + ["{0}={1}".format("key",pay_key,),])
    pre_sign = md5(temp).upper()
    info["sign"] = sign
    xml_string = "<xml>{0}</xml>".format("".join(["<{0}>{1}</{0}>".format(k,v) for k,v in info.items()]))
    prepay = requests.post("https://api.mch.weixin.qq.com/pay/unifiedorder",data=xml_string.encode("utf-8"))
    # 1.3 从结果xml中提取prepay_id
    from xml.etree import ElementTree as ET
    root = ET.XML(prepay.content.decode("utf-8"))
    prepay_dict = {child.tag:child.text for child in root}
    prepay_id = prepay_id["prepay_id"]
    # 再次签名
    info_dict = {
    "appId":"wx55cca0b94f723dc7",
    "timeStamp":str(int(time.time())),
    "nonceStr":"".join([chr(random.randint(65, 90)) for _ in range(12)]),
    "package":"prepay_id={0}".format(prepay_id),
    "signType":"MD5",
    }
    temp = "&".join(
    ["{0}={1}".format(k, info[k]) for k in sorted(info)]+["{0}={1}".format("key",pay_key,),])
    sign = md5(temp).upper()
    return Response(info_dict)
    class NotifyView(APIView):
    def post(self, request, *args,**kwargs):
    # 1. 腾讯会先给我们发送一个xml格式的数据request.body
    # 2. 将xml转换成json
    # 3. 去json中获取订单号 out_trade_no
    # 4. 校验
    # 1.获取结果吧结果xml转换为字典格式
    root = ET.XML(request.body.decode("utf-8"))
    result = {child.tag:child.text for child in root}
    # 2.校验签名是否正确,防止恶意请求。
    sign = result.pop("sign")
    # key为商户平台设置的密钥
    key = "ndtghjghjhghkgh"
    temp = "&".join(
    ["{0}={1}".format(k, result[k]) for k in sorted(result)]+["{0}={1}".format("key",key,),])
    sign = md5(temp).upper()
    # 签名一致
    if local_sign == sign:
    # 根据订单号,把数据库的订单状态修改
    out_trade_no = result.get("out_trade_no")
    response = """<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>"""
    return HttpResponse(response)

2.settings

  1. urls.py

    from django.contrib import admin
    from django.urls import path,re_path,include
    from api1 import urls
    urlpatterns = [
    path('admin/', admin.site.urls),
    re_path("^app01/", include("app01.urls")),
    ]
  2. setting.py

    from pathlib import Path
    INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'app01.apps.app01Config'
    ]

6.celery

  1. celery是一个基于python开发的模块,可以帮助我们对任务进行分发和处理。

  2. 搭建

    pip3 install celery
    # 安装broker:redis或rabbitMQ
    pip3 install redis/pika
  3. 快速上手

    # s1.py
    import time
    from celery import Celery
    app = Celery('tasks', broker='redis://192.168.10.48:6379', backend='redis://192.168.10.48:6379')
    @app.task
    def xxxxxx(x, y):
    time.sleep(10)
    return x + y
    # s2.py
    #!/usr/bin/env python
    # -*- coding:utf-8 -*-
    from s1 import xxxxxx
    # 立即告知celery去执行xxxxxx任务,并传入两个参数
    result = xxxxxx.delay(4, 4)
    print(result.id)
    # s3.py
    from celery.result import AsyncResult
    from s1 import app
    async = AsyncResult(id="f0b41e83-99cf-469f-9eff-74c8dd600002", app=app)
    if async.successful():
    result = async.get()
    print(result)
    # result.forget() # 将结果删除
    elif async.failed():
    print('执行失败')
    elif async.status == 'PENDING':
    print('任务等待中被执行')
    elif async.status == 'RETRY':
    print('任务异常后正在重试')
    elif async.status == 'STARTED':
    print('任务已经开始被执行')
    # 执行
    celery worker -A s1 -l info
    # 注意windows下会报一个错误,需要安装一个模块才可以
    # pip install eventlet
    # celery worker -A s1 -l info -P eventlet
    python3 s2.py
    python3 s3.py

6.1 Django中应用Celery

  1. 基本使用

    django_celery_demo
    ├── app01
    │ ├── __init__.py
    │ ├── admin.py
    │ ├── apps.py
    │ ├── migrations
    │ ├── models.py
    │ ├── tasks.py
    │ ├── tests.py
    │ └── views.py
    ├── db.sqlite3
    ├── django_celery_demo
    │ ├── __init__.py
    │ ├── celery.py
    │ ├── settings.py
    │ ├── urls.py
    │ └── wsgi.py
    ├── manage.py
    ├── red.py
    └── templates
  2. django_celery_demo/celery.py

    #!/usr/bin/env python
    # -*- coding:utf-8 -*-
    import os
    from celery import Celery
    # set the default Django settings module for the 'celery' program.
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_celery_demo.settings')
    app = Celery('django_celery_demo')
    # Using a string here means the worker doesn't have to serialize
    # the configuration object to child processes.
    # - namespace='CELERY' means all celery-related configuration keys
    # should have a `CELERY_` prefix.
    app.config_from_object('django.conf:settings', namespace='CELERY')
    # Load task modules from all registered Django app configs.
    app.autodiscover_tasks()
  3. django_celery_demo/init.py

    from .celery import app as celery_app
    __all__ = ('celery_app',)
  4. app01/tasks.py

    from celery import shared_task
    @shared_task
    def add(x, y):
    return x + y
    @shared_task
    def mul(x, y):
    return x * y
    @shared_task
    def xsum(numbers):
    return sum(numbers)
  5. django_celery_demo/settings.py

    ...
    ....
    .....
    # ######################## Celery配置 ########################
    CELERY_BROKER_URL = 'redis://10.211.55.20:6379'
    CELERY_ACCEPT_CONTENT = ['json']
    CELERY_RESULT_BACKEND = 'redis://10.211.55.20:6379'
    CELERY_TASK_SERIALIZER = 'json'
  6. app01/views.py

    from django.shortcuts import render, HttpResponse
    from app01 import tasks
    from django_celery_demo import celery_app
    from celery.result import AsyncResult
    def index(request):
    result = tasks.add.delay(1, 8)
    print(result)
    return HttpResponse('...')
    def check(request):
    task_id = request.GET.get('task')
    async = AsyncResult(id=task_id, app=celery_app)
    if async.successful():
    data = async.get()
    print('成功', data)
    else:
    print('任务等待中被执行')
    return HttpResponse('...')
  7. django_celery_demo/urls.py

    """django_celery_demo URL Configuration
    The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/1.11/topics/http/urls/
    Examples:
    Function views
    1. Add an import: from my_app import views
    2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
    Class-based views
    1. Add an import: from other_app.views import Home
    2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
    Including another URLconf
    1. Import the include() function: from django.conf.urls import url, include
    2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
    """
    from django.conf.urls import url
    from django.contrib import admin
    from app01 import views
    urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^index/', views.index),
    url(r'^check/', views.check),
    ]
  8. 定时任务

    1. 安装install django-celery-beat

  9. 注册app

    INSTALLED_APPS = (
    ...,
    'django_celery_beat',
    )
  10. 数据库去迁移生成定时任务相关表

    python manage.py migrate
  11. 设置定时任务

    1. 方式一:代码中配置

      # django_celery_demo/celery.py
      #!/usr/bin/env python
      # -*- coding:utf-8 -*-
      import os
      from celery import Celery
      # set the default Django settings module for the 'celery' program.
      os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_celery_demo.settings')
      app = Celery('django_celery_demo')
      # Using a string here means the worker doesn't have to serialize
      # the configuration object to child processes.
      # - namespace='CELERY' means all celery-related configuration keys
      # should have a `CELERY_` prefix.
      app.config_from_object('django.conf:settings', namespace='CELERY')
      app.conf.beat_schedule = {
      'add-every-5-seconds': {
      'task': 'app01.tasks.add',
      'schedule': 5.0,
      'args': (16, 16)
      },
      }
      # Load task modules from all registered Django app configs.
      app.autodiscover_tasks()
    2. 方式二:数据表录入

  12. 后台进程创建任务

    celery -A django_celery_demo beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler
  13. 启动worker执行任务

    celery -A django_celery_demo worker -l INFO

7.示例

7.1 示例

  1. app.json

    {
     "pages": [
       "pages/index/index",
       "pages/logs/logs"
    ],
     "window": {
       "navigationBarBackgroundColor": "#FFDAB9",
       "navigationBarTextStyle": "black",
       "navigationBarTitleText": "Demo"
    },
     "tabBar": {
       "list": [
        {
           "pagePath": "pages/index/index",
           "text": "首页"
        },
        {
           "pagePath": "pages/logs/logs",
           "text": "我的"
        }
      ]
    },
     "sitemapLocation": "sitemap.json"
    }
  2. index.wxml

    <!--pages/index/index.wxml-->
    <!--1-->
    <view class="v1">
     <text>asdf</text>
     <text>asdf</text>
     <text>asdf</text>
     <text>asdf</text>
     <text>asdf</text>
    </view>
    <!-- 示例二 -->
    <view>示例二</view>
    <view class="auction">
     <view class="item">
       <view class="title">第一场 拍卖牛马兄弟</view>
       <view class="tips">
         <view class="status">2020-10-11</view>
         <view class="count">9999次围观</view>
       </view>
       <view class="big">
         <image src="/statlc/images/a.jpg"></image>
       </view>
       <view class="small">
         <image src="/statlc/images/a.jpg"></image>
         <image src="/statlc/images/a.jpg"></image>
         <image src="/statlc/images/a.jpg"></image>
         <image src="/statlc/images/a.jpg"></image>
         <image src="/statlc/images/a.jpg"></image>
       </view>
     </view>
    </view>
  3. index.wxss

    /* pages/index/index.wxss */
    .v1{
     display:flex;
     justify-content: space-between;
     /* flex-start
    flex-end
    space-around
    space-between */
    }
    .auction .item .title{
     /* 字体大小 */
     font-size: 50rpx;
    }
    .auction .item .tips{
     /* 以flex显示 */
     display: flex;
     /* 以x轴正向增大显示 */
     flex-direction: row;
     /* 设置平均分布不带边框 */
     justify-content: space-between;
     /* 设置字体大小 */
     font-size: 30rpx;
     /* 设置字体颜色 */
     color: #8c8c8c;
    }
    .auction .item .big{
     height: 400rpx;
     /* 超出范围将隐藏 */
     overflow:hidden;
    }
    .auction .item .big .image{
     width: 100%;
     height: 100%;
    }
    .auction .item .small{
     display: flex;
     flex-direction: row;
     justify-content: flex-start;
    }
    .auction .item .small image{
     height: 100rpx;
     width: 100rpx;
     /* 设置右边距为20 */
     padding-right: 20rpx;
    }

 

 

 

posted @   無敗の草  阅读(12)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)
点击右上角即可分享
微信分享提示