【flask + vue 前后端分離博客】設計 User 用戶(三)


本章基於 token 認證,添加 創建用戶、獲取單個/所有用戶、修改用戶、刪除用戶API 接口,測試工具 HTTPie/Postman

1. 拉取最新代碼

# 查看遠程地址
$ git remote -v
origin  https://gitee.com/hubery_jun/flask-vuejs-madblog (fetch)
origin  https://gitee.com/hubery_jun/flask-vuejs-madblog (push)

# 類似於 git pull,也是用於拉取最新代碼
$ git fetch
# 或拉取指定的遠程主機上的分支,如 origin 上的 master
$ git fetch origin master

git fetch 與 git pull 的區別

  • git fetch
    • 遠端跟蹤分支:可以更改遠端跟蹤分支
    • 拉取:會將數據拉取到本地倉庫,但是不會自動合並或修改當前的工作
    • commitID:本地庫中 mastercommitID 不變,還是等於 1
  • git pull
    • 遠端跟蹤分支:無法對遠端跟蹤分支操作,必須先切回到本地分支然后創建一個新的 commit 提交
    • 拉取:從遠處獲取最新版本,並合並到本地,會自動合並或修改當前的工作
    • commitID:本地庫中 mastercommitID 發生改變,變成了 2

創建 dev 分支

git checkout -b dev
git branch

2. 用戶模型設計

2.1 使用 ORM SQLAlchemy

兩個插件:

  • flask-sqlalchemyORM 相關
  • Flask-Migrate:用於遷移數據表結構

1、安裝:

pip install flask-sqlalchemy flask-migrate
pip freeze > requirements.txt

2、配置 SQLite 數據庫,修改 back-end/config.py

import os
from dotenv import load_dotenv

basedir = os.path.abspath(os.path.dirname(__file__))
load_dotenv(os.path.join(basedir, '.env'), encoding='utf-8')


class Config(object):
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
                              'sqlite:///' + os.path.join(basedir, 'app.db')
    SQLALCHEMY_TRACK_MODIFICATIONS = False

注意:遷移成功后,會生成一個 back-end/app.db 數據庫文件,可以使用 Navicat 可視化工具打開!

3、初始化數據庫,app/__init__.py

# 數據庫相關
db = SQLAlchemy()
migrate = Migrate()

def create_app(config_class=Config):
    app = Flask(__name__)
    app.config.from_object(config_class)

    # 跨域
    CORS(app)

    # 初始化數據庫
    db.init_app(app)
    migrate.init_app(app, db)

    # 注冊藍圖 blueprint
    from app.api import bp as api_bp
    app.register_blueprint(api_bp, url_prefix="/api")

    return app


from app import models

2.2 定義用戶模型

1、創建 app/models.py

class User(db.Model):
    """用戶對象"""

    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), index=True, unique=True)  # index 創建索引
    email = db.Column(db.String(120), index=True, unique=True)
    password_hash = db.Column(db.String(128))  # 密碼加密(hash),不存明文

    def __str__(self):
        return '<User {}>'.format(self.username)

2、創建遷移存儲庫:

(flask-vuejs)  F:\My Projects\flask-vuejs-madblog\back-end> flask db init

3、生成遷移腳本:

# -m 參數:添加記錄
(flask-vuejs)  F:\My Projects\flask-vuejs-madblog\back-end> flask db migrate -m "add users table"

2、將遷移腳本應用到數據庫中:

# flask db upgrade 還可以回滾到上次的遷移,需要指定
(flask-vuejs)  F:\My Projects\flask-vuejs-madblog\back-end> flask db upgrade

2.3 密碼哈希

在數據表中,不能直接保存明文密碼,這里我們將使用 werkzeug.security 庫的 generate_password_hashcheck_password_hash 來創建哈希密碼和驗證密碼的 hash 是否一致。

更新 app/models.py

from werkzeug.security import generate_password_hash, check_password_hash


class User(PaginationAPIMixin, db.Model):
    """用戶對象"""
    ...

    def generate_password(self, password):
        """密碼哈希"""
        self.password_hash = generate_password_hash(password)

    def check_password(self, password):
        """檢查密碼是否正確"""
        return check_password_hash(self.password_hash, password)

配置 Flask shell 環境

flask shell 可以與項目環境進行交互(會啟動一個 Python 解釋器包含應用的上下文),默認不支持 db 數據庫模型的使用,需要額外配置。

1、修改 back-end/madblog.py

from app import create_app, db
from app.models import User

app = create_app()


@app.shell_context_processor
def make_shell_context():
    """配置flask shell 上下文"""
    return {'db': db, 'User': User}

2、在終端進入 flask shell

(flask-vuejs)  F:\My Projects\flask-vuejs-madblog\back-end>flask shell
Python 3.6.8 (tags/v3.6.8:3c6b436a57, Dec 24 2018, 00:16:47) [MSC v.1916 64 bit (AMD64)] on win32
App: app [production]
Instance: F:\My Projects\flask-vuejs-madblog\back-end\instance
>>> app
<Flask 'app'>
>>> db
<SQLAlchemy engine=sqlite:///F:\My Projects\flask-vuejs-madblog\back-end\app.db>
>>> User
<class 'app.models.User'>
>>> u = User(username='rose', email='rose@qq.com')
>>> u.generate_password('123456')
>>> u.check_password('123456')
True

注意:需要先進入項目虛擬環境!

3. 用戶相關 API 設計

用戶資源相關的 api

HTTP方法 資源URL 說明
GET /api/users 返回所有用戶的集合
POST /api/users 注冊一個新用戶
GET /api/users/ 返回一個用戶
PUT /api/users/ 修改一個用戶
DELETE /api/users/ 刪除一個用戶

新建:app/api/users.py

from app.api import bp


@bp.route('/users', methods=['POST'])
def create_user():
    '''注冊一個新用戶'''
    pass


@bp.route('/users', methods=['GET'])
def get_users():
    '''返回所有用戶的集合'''
    pass


@bp.route('/users/<int:id>', methods=['GET'])
def get_user(id):
    '''返回一個用戶'''
    pass


@bp.route('/users/<int:id>', methods=['PUT'])
def update_user(id):
    '''修改一個用戶'''
    pass


@bp.route('/users/<int:id>', methods=['DELETE'])
def delete_user(id):
    '''刪除一個用戶'''
    pass

記得要將 users 添加到 api/__init__.py

from app.api import ping, users

3.1 用戶對象轉換成 JSON

因為 API 接口返回給前端的數據為 json 數據,所以封裝 User 模型為 json 形式,方便傳遞,app/models.py 新增:

class User(db.Model):
    """用戶對象"""

    ...
    def to_dict(self, include_email=False):
        """
        封裝 User 對象,傳遞給前端只能是 json 格式,不能是實例對象
        :param include_email: 只有當用戶請求自己數據時,才包含 email
        :return:
        """
        data = {
            'id': self.id,
            'username': self.username,
            '_links': {
                'self': url_for('api.get_user', id=self.id)
            }
        }
        if include_email:
            data['email'] = self.email

        return data

include_email 用來標記 email 字段是否在字典中,只有當用戶請求自己的數據時,才包含。

3.2 用戶集合轉換為 JSON

當獲取所有用戶數據時也需要封裝為 json 形式,另外還包含了分頁信息,為了后續能夠重復利用,將其設計為通用設計類,app/models.py

import base64
import os
from datetime import datetime, timedelta

from flask import url_for

from app import db
from werkzeug.security import generate_password_hash, check_password_hash

class PaginationAPIMixin:
    @staticmethod
    def to_collection_dict(query, page, per_page, endpoint, **kwargs):
        # 分頁查詢,error_out 表示頁數不是 int 或 超過總頁數時,會報錯,並返回 404,默認為 True
        resources = query.paginate(page, per_page, error_out=False)
        data = {
            'items': [item.to_dict() for item in resources.items],
            '_meta': {
                'page': page,
                'per_page': per_page,
                'total_pages': resources.pages,  # 總頁數
                'total_items': resources.total  # 總條數
            },
            '_links': {
                'self': url_for(endpoint, page=page, per_page=per_page, **kwargs),  # "/api/users?page=1&per_page=10"
                'next': url_for(endpoint, page=page + 1, per_page=per_page, **kwargs) if resources.has_next
                else None,
                'prev': url_for(endpoint, page=page - 1, per_page=per_page, **kwargs) if resources.has_prev
                else None
            }
        }
        return data

然后 User 類只需集成它即可:

class User(PaginationAPIMixin, db.Model):
    """用戶對象"""

3.3 JSON 轉換為用戶對象

將前端傳過來的 JSON 數據轉換為 User 對象,app/models.py

class User(PaginationAPIMixin, db.Model):
    """用戶對象"""
    ....

    def from_dict(self, data, new_user=False):
    """
    將前端發送過來的 json 對象轉換為 User 對象
    :param data:
    :param new_user:
    :return:
    """
    for field in ['username', 'email']:
        if field in data:
            # 給實例對象添加屬性字典
            setattr(self, field, data[field])
        if new_user and 'password' in data:
            self.generate_password(data['password'])

3.4 錯誤處理

創建 app/api/errors.py

from flask import jsonify
from werkzeug.http import HTTP_STATUS_CODES

from app import db
from app.api import bp


def error_response(status_code, message=None):
    payload = {'error': HTTP_STATUS_CODES.get(status_code, 'Unknow error')}
    if message:
        payload['message'] = message

    response = jsonify(payload)
    response.status_code = status_code
    return response

def bad_request(message):
    """
    異常請求,如:400
    :param message:
    :return:
    """
    return error_response(400, message)

3.5 創建新用戶

編輯 app/api/users.py

@bp.route('/users', methods=['POST'])
def create_user():
    """創建一個新用戶"""
    data = request.get_json()
    if not data:
        return bad_request("post 必須是 json 數據!")

    message = {}
    username = data.get('username', None)
    email = data.get('email', None)
    password = data.get('password', None)

    # 判斷是否為空
    if 'username' not in data or not username:
        message['username'] = "請提供一個有效的用戶名!"
    pattern = '^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$'
    if 'email' not in data or not re.match(pattern, email):
        message['email'] = "請提供一個有效的郵箱地址!"
    if 'password' not in data or not password:
        message['password'] = "請提供一個有效的密碼!"

    # 檢查數據庫中是否有該用戶
    if User.query.filter(or_(User.username == username, User.email == email)).first():
        message['username'] = "用戶名或郵箱已存在!"

    if message:
        return bad_request(message)

    # 創建新用戶
    user = User()
    user.from_dict(data, new_user=True)
    db.session.add(user)
    db.session.commit()
    response = jsonify(user.to_dict())
    response.status_code = 201

    response.headers['Location'] = url_for('api.get_user', id=user.id)  # /api/users/1
    return response

使用 HTTPie 模塊來測試 API 接口:

pip install --upgrade httpie
pip freeze > requirements.txt

測試結果:

(flask-vuejs)  F:\Envs\flask-vuejs\Scripts>http POST http://127.0.0.1:5000/api/users username=john password=123456 email=john@qq.com
HTTP/1.0 201 CREATED
Access-Control-Allow-Origin: *
Content-Length: 60
Content-Type: application/json
Date: Mon, 31 Aug 2020 02:42:49 GMT
Location: http://127.0.0.1:5000/api/users/3
Server: Werkzeug/1.0.1 Python/3.6.8

{
    "_links": {
        "self": "/api/users/3"
    },
    "id": 3,
    "username": "john"
}

3.6 查詢單個用戶

編輯 app/api/users.py

@bp.route('/users/<int:id>', methods=['GET'])
def get_user(id):
    """返回一個用戶"""
    return jsonify(User.query.get_or_404(id).to_dict())

測試結果:

(flask-vuejs)  F:\Envs\flask-vuejs\Scripts>http GET http://127.0.0.1:5000/api/users/3
HTTP/1.0 200 OK
Access-Control-Allow-Origin: *
Content-Length: 60
Content-Type: application/json
Date: Mon, 31 Aug 2020 02:43:34 GMT
Server: Werkzeug/1.0.1 Python/3.6.8

{
    "_links": {
        "self": "/api/users/3"
    },
    "id": 3,
    "username": "john"
}

可以看到返回的就是 to_dict() 封裝的數據。

構造查詢不存在時返回的數據

當查詢不存在的用戶,也返回一個 JSON 數據,修改 app/api/errors.py,新增:

@bp.app_errorhandler(404)
def not_found_error(error):
    return error_response(404)


@bp.app_errorhandler(500)
def internal_error(error):
    db.session.rollback()
    return error_response(500)

測試結果:

# 測試不存在的用戶

(flask-vuejs)  F:\Envs\flask-vuejs\Scripts>http GET http://127.0.0.1:5000/api/users/36
HTTP/1.0 404 NOT FOUND
Access-Control-Allow-Origin: *
Content-Length: 22
Content-Type: application/json
Date: Mon, 31 Aug 2020 02:43:53 GMT
Server: Werkzeug/1.0.1 Python/3.6.8

{
    "error": "Not Found"
}

3.7 查詢所有用戶

編輯 app/api/users.py,新增:

@bp.route('/users', methods=['GET'])
def get_users():
    """用戶集合,分頁"""
    page = request.args.get('page', 1, type=int)
    per_page = min(request.args.get('per_page', 10, type=int), 100)
    data = User.to_collection_dict(User.query, page, per_page, 'api.get_users')
    return jsonify(data)

page 為當前頁碼數,per_page 為每頁要顯示的條數。

測試結果:

(flask-vuejs)  F:\Envs\flask-vuejs\Scripts>http GET http://127.0.0.1:5000/api/users
HTTP/1.0 200 OK
Access-Control-Allow-Origin: *
Content-Length: 331
Content-Type: application/json
Date: Mon, 31 Aug 2020 02:44:57 GMT
Server: Werkzeug/1.0.1 Python/3.6.8

{
    "_links": {
        "next": null,
        "prev": null,
        "self": "/api/users?page=1&per_page=10"
    },
    "_meta": {
        "page": 1,
        "per_page": 10,
        "total_items": 3,
        "total_pages": 1
    },
    "items": [
        {
            "_links": {
                "self": "/api/users/1"
            },
            "id": 1,
            "username": "rose"
        },
        {
            "_links": {
                "self": "/api/users/2"
            },
            "id": 2,
            "username": "lila"
        },
        {
            "_links": {
                "self": "/api/users/3"
            },
            "id": 3,
            "username": "john"
        }
    ]
}

3.8 修改用戶

編輯 app/api/users.py,新增:

@bp.route('/users/<int:id>', methods=['PUT'])
def update_user(id):
    """修改一個用戶"""
    user = User.query.get_or_404(id)
    data = request.get_json()
    if not data:
        return bad_request("post 必須是 json 數據!")

    message = {}
    username = data.get('username', None)
    email = data.get('email', None)

    # 判斷是否為空
    if 'username' in data and not username:
        message['username'] = "請提供一個有效的用戶名!"
    pattern = '^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$'
    if 'email' in data and not re.match(pattern, email):
        message['email'] = "請提供一個有效的郵箱地址!"

    if 'username' in data and data['username'] != user.username and \
            User.query.filter_by(username=data['username']).first():
        message['username'] = '請使用一個不同的用戶名!'
    if 'email' in data and data['email'] != user.email and \
            User.query.filter_by(email=data['email']).first():
        message['email'] = '請使用一個不同的郵箱!'

    if message:
        return bad_request(message)

    user.from_dict(data, new_user=False)
    db.session.commit()
    return jsonify(user.to_dict())

測試結果:

# 輸入要修改的用戶 ID 和要修改的字段

(flask-vuejs)  F:\Envs\flask-vuejs\Scripts>http PUT http://127.0.0.1:5000/api/users/3 email=john@outlook.com
HTTP/1.0 200 OK
Access-Control-Allow-Origin: *
Content-Length: 60
Content-Type: application/json
Date: Mon, 31 Aug 2020 02:49:37 GMT
Server: Werkzeug/1.0.1 Python/3.6.8

{
    "_links": {
        "self": "/api/users/3"
    },
    "id": 3,
    "username": "john"
}

4. API 認證

所謂 API 認證,即只有得到認證過的請求,才能訪問特定的 API,比如(登錄認證、token 認證等),這里采用的是 Flask-HTTPAuth 模塊。

它需要使用用戶名和密碼進行 Basic Auth 驗證,然后獲得一個臨時 token。只要 token 有效,客戶端就可以發送附帶 token 的 API 請求以通過認證。一旦 token 到期,需要申請新的 token

安裝:

pip install flask-httpauth
pip freeze > requirements.txt

4.1 User 用戶模型添加 token

編輯 app/models.py

import base64
import os
from datetime import datetime, timedelta


class User(PaginationAPIMixin, db.Model):
    """用戶對象"""
    ....

    # token 驗證 API(需要登錄才能請求)
    token = db.Column(db.String(32), index=True, unique=True)
    token_expiration = db.Column(db.DateTime)   # token 過期時間

    def get_token(self, expires_in=3600):
        now = datetime.utcnow()
        # 大於 一分鍾
        if self.token and self.token_expiration > now + timedelta(seconds=60):
            return self.token

        self.token = base64.b64encode(os.urandom(24)).decode('utf-8')   # 生成 token
        self.token_expiration = now + timedelta(seconds=expires_in)
        db.session.add(self)
        return self.token

    def revoke_token(self):
        """撤銷 token,當前 utc 時間減去 1 秒"""
        self.token_expiration = datetime.utcnow() - timedelta(seconds=1)

    @staticmethod
    def check_token(token):
        """檢查 token"""
        user = User.query.filter_by(token=token).first()
        # 若沒有 token 或者 token 已過期,返回 None,不准請求
        if user is None or user.token_expiration < datetime.utcnow():
            return None
        return user

因為新增了字段,所以需要遷移生成新的數據表:

flask db migrate -m "user add tokens"
flask db upgrade

4.2 HTTP Basic Authentication

創建 app/api/auth.py

from flask import g
from flask_httpauth import HTTPBasicAuth
from app.models import User
from app.api.errors import error_response

basic_auth = HTTPBasicAuth()


@basic_auth.verify_password
def verify_password(username, password):
    '''用於檢查用戶提供的用戶名和密碼'''
    user = User.query.filter_by(username=username).first()
    if user is None:
        return False
    g.current_user = user
    return user.check_password(password)


@basic_auth.error_handler
def basic_auth_error():
    '''用於在認證失敗的情況下返回錯誤響應'''
    return error_response(401)

4.3 客戶端申請 token

上面我們已經實現了 Basic Auth 驗證的支持,新增添加一條 token 路由,創建 app/api/tokens.py

from app import db
from app.api import bp
from app.api.auth import basic_auth


@bp.route('/tokens', methods=['POST'])
@basic_auth.login_required
def get_token():
    token = g.current_user.get_token()
    db.session.commit()
    return jsonify({'token': token})

裝飾器 @basic_auth.login_required 將指示 Flask-HTTPAuth 驗證身份,當通過 Basic Auth 驗證后,才使用用戶模型的 get_token() 方法來生成 token,數據庫提交在生成 token 后發出,以確保 token 及其到期時間被寫回到數據庫。

修改 app/api/__init__.py,在末尾添加:

from app.api import ping, users, tokens

測試

測試生成一個token,直接請求,會提示需要登錄:

(flask-vuejs)  F:\Envs\flask-vuejs\Scripts>http POST http://127.0.0.1:5000/api/tokens
HTTP/1.0 401 UNAUTHORIZED
Access-Control-Allow-Origin: *
Content-Length: 25
Content-Type: application/json
Date: Mon, 31 Aug 2020 02:50:32 GMT
Server: Werkzeug/1.0.1 Python/3.6.8
WWW-Authenticate: Basic realm="Authentication Required"

{
    "error": "Unauthorized"
}

需要帶上用戶登錄信息:

(flask-vuejs)  F:\Envs\flask-vuejs\Scripts>http --auth john:123456 POST http://127.0.0.1:5000/api/tokens
HTTP/1.0 200 OK
Access-Control-Allow-Origin: *
Content-Length: 45
Content-Type: application/json
Date: Mon, 31 Aug 2020 02:51:15 GMT
Server: Werkzeug/1.0.1 Python/3.6.8

{
    "token": "G4d8FwoEdOODyhBBe8nz30vCe0X+YUAI"
}

4.4 HTTP Token Authentication

用戶通過 Basic Auth 獲取到 token 后,之后的請求需要帶上這個 token 才能訪問其他 API,修改 app/api/auth.py

from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth
...

token_auth = HTTPTokenAuth()
...

@token_auth.verify_token
def verify_token(token):
    '''用於檢查用戶請求是否有token,並且token真實存在,還在有效期內'''
    g.current_user = User.check_token(token) if token else None
    return g.current_user is not None


@token_auth.error_handler
def token_auth_error():
    '''用於在 Token Auth 認證失敗的情況下返回錯誤響應'''
    return error_response(401)

4.5 使用 Token 機制保護 API 路由

除了創建用戶不用保護以后,其他路由都需要 Token 保護,app/api/users.py

@bp.route('/users', methods=['GET'])
@token_auth.login_required
def get_users():
    ...

@bp.route('/users/<int:id>', methods=['GET'])
@token_auth.login_required
def get_user(id):
    ...

...

只需給視圖函數添加 @token_auth.login_required 裝飾器即可。

測試

為攜帶 token 的請求,會得到一個 401 的錯誤:

(flask-vuejs)  F:\Envs\flask-vuejs\Scripts>http GET http://127.0.0.1:5000/api/users/3
HTTP/1.0 401 UNAUTHORIZED
Access-Control-Allow-Origin: *
Content-Length: 25
Content-Type: application/json
Date: Mon, 31 Aug 2020 02:54:38 GMT
Server: Werkzeug/1.0.1 Python/3.6.8
WWW-Authenticate: Bearer realm="Authentication Required"

{
    "error": "Unauthorized"
}

攜帶 token 的請求,返回 200:

# 需要添加 Authorization 頭部
(flask-vuejs)  F:\Envs\flask-vuejs\Scripts>http GET http://127.0.0.1:5000/api/users/3 "Authorization:Bearer G4d8FwoEdOODyhBBe8nz30vCe0X+YUAI"
HTTP/1.0 200 OK
Access-Control-Allow-Origin: *
Content-Length: 60
Content-Type: application/json
Date: Mon, 31 Aug 2020 02:55:20 GMT
Server: Werkzeug/1.0.1 Python/3.6.8

{
    "_links": {
        "self": "/api/users/3"
    },
    "id": 3,
    "username": "john"
}

4.6 撤銷 token

修改 app/api/tokens.py

from app.api.auth import basic_auth, token_auth
...

@bp.route('/tokens', methods=['DELETE'])
@token_auth.login_required
def revoke_token():
    g.current_user.revoke_token()
    db.session.commit()
    return '', 204

測試:

# 刪除 token
(flask-vuejs)  F:\Envs\flask-vuejs\Scripts>http DELETE http://127.0.0.1:5000/api/tokens "Authorization:Bearer G4d8FwoEdOODyhBBe8nz30vCe0X+YUAI"
HTTP/1.0 204 NO CONTENT
Access-Control-Allow-Origin: *
Content-Type: text/html; charset=utf-8
Date: Mon, 31 Aug 2020 03:02:08 GMT
Server: Werkzeug/1.0.1 Python/3.6.8


# 再使用這條 token 進行請求,發現請求失敗
(flask-vuejs)  F:\Envs\flask-vuejs\Scripts>http GET http://127.0.0.1:5000/api/users/3 "Authorization:Bearer G4d8FwoEdOODyhBBe8nz30vCe0X+YUAI"
HTTP/1.0 401 UNAUTHORIZED
Access-Control-Allow-Origin: *
Content-Length: 25
Content-Type: application/json
Date: Mon, 31 Aug 2020 03:02:18 GMT
Server: Werkzeug/1.0.1 Python/3.6.8
WWW-Authenticate: Bearer realm="Authentication Required"

{
    "error": "Unauthorized"
}

5. 提交代碼

項目結構:

back-end/
├─app
│  ├─api
│  │  └─__init__.py
│  │  └─auth.py
│  │  └─errors.py
│  │  └─ping.py
│  │  └─tokens.py
│  │  └─users.py
│  └─__init__.py__
│  └─models.py__
├─migrations
└─.env
└─.gitignore
└─app.db
└─config.py
└─madblog.py
└─requirements.txt

合並分支並推送到遠端

$ git add .
$ git commit -m "3. Flask設計User用戶相關API"
$ git checkout master
$ git merge dev
$ git branch -d dev

$ git push -u origin master

打標簽

$ git tag v0.3

hj@DESKTOP-JUS39UG MINGW32 /f/My Projects/flask-vuejs-madblog (master)
$ git push origin v0.3
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0
remote: Powered by GITEE.COM [GNK-5.0]
To https://gitee.com/hubery_jun/flask-vuejs-madblog
 * [new tag]         v0.3 -> v0.3


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM