# flaskTest **Repository Path**: humourous94/flask_test ## Basic Information - **Project Name**: flaskTest - **Description**: 学习flask,flask-sqlalchemy,restful的一个小工程 基于flask v3.0.2 - **Primary Language**: Python - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 1 - **Created**: 2023-08-15 - **Last Updated**: 2025-03-17 ## Categories & Tags **Categories**: Uncategorized **Tags**: Flask, Python ## README ## 1.使用方法 双击即可 #### 1-1 配置文件说明 JSON格式如下 : | 配置项 | 功能 | 详细说明 | | ------------------------------ | ---------------- | ---------------------------------------------------------- | | debug | 调试标志 | 开启后会自动填充测试数据至数据库, 终端显示的日志会更多一些 | | ip | 网络地址 | 服务器地址 | | port | 端口号 | 服务器端口号,需注意端口占用的问题 | | secret_key | 秘钥 | 用户不感知的, 是flask自身的一种保护机制 | | testing | 测试模式 | 终端会增加测试日志 | | database | 数据库名称 | 会根据此名称生成一个数据库位于instance路径之下 | | sqlalchemy_echo | 数据库配置项 | 打印日志 | | sqlalchemy_track_modifications | 数据库配置项 | 异常回溯日志 | | sqlalchemy_commit_on_teardown | 数据库配置项 | 自动更新数据库 | | row_config | 序列号列表显示列 | 可以自定义序列号列表中呈现的列信息 | #### 1-2 关键结构说明 | 名称 | 功能 | 详细说明 | | --------------------- | ---------------------- | ------------------------------------------------------------ | | 序列号(serial) | 唯一性的标志编码 | 由多段字段组成, 同时包含有创建时间,指代目标文件名等参数 | | 序列号结构(structure) | 编码结构 | 其中关键的JSON属性有:
1. **编码格式**, 用于组成编码的section属性
2. **参数属性**, 不用于组成编码, 但是需要记录并呈现在列表中的section属性
注意无论是编码格式还是参数属性, 其中的每个字段都需要以section的形式定义其属性和规则 | | 序列号字段(section) | 编码结构中的字段 | 类型有:
**占位类字段:0** 如: - _ . 等
**计算类字段:1** 目前支持: **获取日期:1** **获取时间:2** **获取8位哈希:3**
**选择类字段:2** 例如 **性别字段** 就可以选择**男**或者女
**输入类字段:3** | | 字段可选项(word) | 选择类字段的可选项成员 | 举个例子, 选择性字段**性别**,就应该对应有**男**和**女**两种字段可选项, 每个字段都包含有名称和编码, 名称用于列表显示, 编码则是用于生成序列号时使用 | 大致原理就是: 先定义好**序列号字段**和**字段可选项**, 然后以JSON的形式组织**序列号字段**成**序列号结构**, 接下来就可以 ## 2. API接口文档 #### 2-1 获取序列号列表 - 描述 获取序列号列表信息 - 请求 - 方法: `GET ` - URL: `/Api/SerialList` - 传参: arg 传参 - **page**: 页码, 如果设置page,只获取当前页list; 如果无page传参, 则会获取整个list. - **search**: 搜索信息, 可以直接输入模糊搜索的内容, 也可以`@ `的形式, 其中row为列名称, key为关键字, 如果需要多列关键字搜索可以尝试`@&@ ` - 示例: (注: search后面的@和&被转义成了%40和%26) ``` GET http://127.0.0.1:9527/Api/SerialList?page=1&search=code%40TODO%26id%401 ``` - 响应 - 成功: 200 ``` { "data": { "current_page": 1, "list": [ { "code": "TODO-AAA-MANUL-9527", "data": "", "id": "1", "name": "AAA\u8be6\u7ec6\u8bf4\u660e\u6587\u6863", "sexy": "" }, { "code": "TODO-JJJ-MANUL-9527", "data": "", "id": "10", "name": "JJJ\u8be6\u7ec6\u8bf4\u660e\u6587\u6863", "sexy": "" }, { "code": "TODO-KKK-MANUL-9527", "data": "", "id": "11", "name": "KKK\u8be6\u7ec6\u8bf4\u660e\u6587\u6863", "sexy": "" } ], "total_page": 1 } } ``` - 失败: 404 #### 2-2 创建序列号,添加进入列表 - 描述 创建序列号,添加进入列表 - 请求 - 方法: `POST` - URL: `/Api/SerialList` - 传参: json传参 (Content-Type: application/json) - 其实就是填写接收到的JSON表单, 然后回传至服务器即可 - 示例 ``` POST http://127.0.0.1:9527/Api/SerialList Content-Length: 63 Content-Type: application/json body: { "struct": "类型01", "filename": "bbbb", "name": "11", "sexy": "16" } ``` - 响应 - 成功 200 - 失败 404 #### 2-3 修改已经存在的序列号 (TODO) #### 2-4 从序列号列表中删除序列号 (TODO) #### 2-5 获取序列号结构列表 - 描述 获取序列号结构的列表 - 请求 - 方法: `GET` - URL: `/Api/StructureList` - 传参: arg 传参 - **page**: 页码, 如果设置page,只获取当前页list; 如果无page传参, 则会获取整个list. - **search**: 搜索信息, 可以直接输入模糊搜索的内容, 也可以`@ `的形式, 其中row为列名称, key为关键字, 如果需要多列关键字搜索可以尝试`@&@ ` - 示例: (注: search后面的@和&被转义成了%40和%26) ``` GET /Api/StructureList?page=1&search=code%40TODO%26id%401 ``` - 响应 - 成功 200 ```json { "data": { "list": [ { "code": "[\"name\", \"-\", \"sexy\"]", "id": 1, "json": [ "filename", "name", "sexy" ], "name": "\u7c7b\u578b01", "tip": "\u8fd9\u662f\u4e00\u4e2a\u5e8f\u5217\u53f7\u7684\u7ed3\u6784\u4f53" }, { "code": "[\"name\", \"-\", \"sexy\", \"-\", \"sexy\"]", "id": 2, "json": [ "filename", "name", "sexy" ], "name": "\u7c7b\u578b02", "tip": "\u8fd9\u662f\u4e00\u4e2a\u5e8f\u5217\u53f7\u7684\u7ed3\u6784\u4f53" }, { "code": "[\"name\", \"-\", \"sexy\", \"-\", \"date\"]", "id": 3, "json": [ "filename", "name", "sexy", "-", "date" ], "name": "\u7c7b\u578b03", "tip": "\u8fd9\u662f\u4e00\u4e2a\u5e8f\u5217\u53f7\u7684\u7ed3\u6784\u4f53" }, { "code": "[\"name\", \"-\", \"sexy\", \"-\", \"tip\"]", "id": 4, "json": [ "filename", "name", "sexy", "-", "tip" ], "name": "\u7c7b\u578b04", "tip": "\u8fd9\u662f\u4e00\u4e2a\u5e8f\u5217\u53f7\u7684\u7ed3\u6784\u4f53" }, { "code": "[\"name\", \"-\", \"sexy\", \"-\", \"-\"]", "id": 5, "json": [ "filename", "name", "sexy", "-" ], "name": "\u7c7b\u578b05", "tip": "\u8fd9\u662f\u4e00\u4e2a\u5e8f\u5217\u53f7\u7684\u7ed3\u6784\u4f53" } ] } } ``` - 失败 404 #### 2-6 新建序列号结构, 添加到结构列表中 (TODO) #### 2-7 修改已有的序列号结构信息 (TODO) #### 2-8 删除序列号结构 (TODO) #### 2-9 获取结构字段列表 - 描述 - 请求 - 方法: GET - URL: /Api/SectionList - 传参: arg 传参 - **page**: 页码, 如果设置page,只获取当前页list; 如果无page传参, 则会获取整个list. - **search**: 搜索信息, 可以直接输入模糊搜索的内容, 也可以`@ `的形式, 其中row为列名称, key为关键字, 如果需要多列关键字搜索可以尝试`@&@ ` - **Structure.name**: 序列号结构体名称, 回传新建该结构体需要关联的字段列表 - 示例: (注: 中文字符和符号依然存在转义问题) ``` GET http://127.0.0.1:9527/Api/SectionList?Structure.name=类型01 ``` - 响应 - 成功 200 ```json { "data": { "list": [ { "code": "34", "func": null, "id": 13, "name": "filename", "options": [ null ], "tip": "\u6587\u4ef6\u540d,\u5e94\u8be5\u662f\u6bcf\u4e2a\u6587\u4ef6\u5fc5\u8981\u7684\u5185\u5bb9", "type": 3 }, { "code": "31", "func": null, "id": 1, "name": "name", "options": [ { "code": "liubei", "id": 11, "name": "\u5218\u5907", "section": "name", "tip": "\u6d4b\u8bd5\u7528\u7684\u4eba\u540d" }, { "code": "guanyy", "id": 12, "name": "\u5173\u7fbd", "section": "name", "tip": "\u6d4b\u8bd5\u7528\u7684\u4eba\u540d" }, { "code": "zhangf", "id": 13, "name": "\u5f20\u98de", "section": "name", "tip": "\u6d4b\u8bd5\u7528\u7684\u4eba\u540d" }, { "code": "zaoyun", "id": 14, "name": "\u8d75\u4e91", "section": "name", "tip": "\u6d4b\u8bd5\u7528\u7684\u4eba\u540d" }, { "code": "hzhong", "id": 15, "name": "\u9ec4\u5fe0", "section": "name", "tip": "\u6d4b\u8bd5\u7528\u7684\u4eba\u540d" } ], "tip": "\u59d3\u540d,\u53ef\u9009\u9879\u6709\u5218\u5907,\u5173\u7fbd,\u5f20\u98de,\u8d75\u4e91,\u9ec4\u5fe0", "type": 2 }, { "code": "32", "func": null, "id": 2, "name": "sexy", "options": [ { "code": "nanren", "id": 16, "name": "\u7537\u4eba", "section": "sexy", "tip": "\u6d4b\u8bd5\u7528\u7684\u6027\u522b" }, { "code": "nv-ren", "id": 17, "name": "\u5973\u4eba", "section": "sexy", "tip": "\u6d4b\u8bd5\u7528\u7684\u6027\u522b" }, { "code": "nanhai", "id": 18, "name": "\u7537\u5b69", "section": "sexy", "tip": "\u6d4b\u8bd5\u7528\u7684\u6027\u522b" }, { "code": "nvhaio", "id": 19, "name": "\u5973\u5b69", "section": "sexy", "tip": "\u6d4b\u8bd5\u7528\u7684\u6027\u522b" }, { "code": "wwaixr", "id": 20, "name": "\u5916\u661f\u4eba", "section": "sexy", "tip": "\u6d4b\u8bd5\u7528\u7684\u6027\u522b" } ], "tip": "\u6027\u522b,\u53ef\u9009\u9879\u6709\u7537\u4eba,\u5973\u4eba,\u7537\u5b69,\u5973\u5b69,\u5916\u661f\u4eba", "type": 2 } ] } } ``` - 失败 404 #### 2-10 新建字段,添加到列表 (TODO) #### 2-11 修改已有的结构字段 (TODO) #### 2-12 删除结构字段 (TODO) #### 2-13 获取字段选项列表 (TODO) #### 2-14 新建字段选项,添加到列表 (TODO) #### 2-15 修改已有的字段选项 (TODO) #### 2-16 删除字段选项 (TODO) ## 3 取号流程 #### 3-1 获取序列号结构列表,让用户选择要生成的序列号 #### 3-2 通过选中的序列号结构名称, 获取其序列号结构的字段列表, 生成表单, 让用户填写 #### 3-3 上传表单信息生成序列号 #### 3-4 代码 - `HTML5` ```html {% extends 'base.html' %} {% block content %} 选择类型
{% endblock %} {% block startup %} {% endblock %} ``` - `Javascript` ```javascript // 将 Flask 传递的 IP 和端口信息赋值给 JavaScript 变量 console.log('script.js Server IP: ' + serverIp); console.log('script.js Server Port: ' + serverPort); /* materialize的模块配置函数,materialize依赖此函数去定义一些模块的样式 */ document.addEventListener('DOMContentLoaded', function() { /* 初始化 modal 模块 */ var elems = document.querySelectorAll('.modal'); var ops = {preventScrolling: true}; M.Modal.init(elems, ops); /* 初始化 datepicker 模块 */ var elems = document.querySelectorAll('.datepicker'); var ops = {format: 'yyyy/mm/dd'}; M.Datepicker.init(elems, ops); /* 初始化 dropdown-trigge 模块 */ var elems = document.querySelectorAll('.dropdown-trigger'); M.Dropdown.init(elems); /* 初始化 select 模块 */ var elems = document.querySelectorAll('select'); M.FormSelect.init(elems); /* 初始化 sidenavigation 模块 */ var elems = document.querySelectorAll('.sidenav'); M.Sidenav.init(elems); var elems = document.querySelectorAll('.tabs') M.Tabs.init(elems); }) /** * 初始化列表中的内容 */ function initListContent() { let ops = {method: 'GET'}; let currentUrl = window.location.href; let curl = new URL(currentUrl) let furl = new URL('http://' + serverIp + ':' + serverPort + '/Api/SerialList'); let p = new URLSearchParams(curl.search); if (p.get('page') != null) { furl.searchParams.set('page', String(p.get('page'))); } else { furl.searchParams.set('page', '1'); } if (p.get('search') != null) { furl.searchParams.set('search', String(p.get('search'))); } fetch(furl, ops) .then(response => { return response.json(); }) .then(form => { if ('data' in form) { let lhd = document.getElementById('ListHeader'); let lit = document.getElementById('ListItems'); let pg = document.getElementById('Pagination'); let jk = Object.keys(form.data.list[0]); // 添加列表表头 jk.forEach(it => { let th = document.createElement('th'); th.textContent = it; lhd.appendChild(th); }); let th = document.createElement('th'); th.textContent = 'ops'; lhd.appendChild(th); // 添加列表数据 form.data.list.forEach(it => { let tr = document.createElement('tr'); jk.forEach(iit => { let td = document.createElement('td'); td.textContent = it[iit]; tr.appendChild(td); }); let td = document.createElement('td'); let ed = document.createElement('button'); ed.classList.add('waves-effect'); ed.classList.add('waves-light'); ed.classList.add('btn'); ed.style.margin = '0rem 0.2rem 0rem 0.2rem'; ed.textContent = '修改'; ed.addEventListener('click', () => { let herf = '/Api/SerialList'; updateFormToModal('修改序列号', jk, it, herf, 'PUT'); M.Modal.getInstance(document.getElementById('FormModal')).open(); }); let rm = document.createElement('button'); rm.classList.add('waves-effect'); rm.classList.add('waves-light'); rm.classList.add('btn'); rm.style.margin = '0rem 0.2rem 0rem 0.2rem'; rm.textContent = '删除'; rm.addEventListener('click', () => { let herf = '/Api/SerialList'; updateFormToModal('确认要删除序列号吗?', jk, it, herf, 'DELETE'); M.Modal.getInstance(document.getElementById('FormModal')).open(); }); td.appendChild(ed); td.appendChild(rm); tr.appendChild(td); lit.appendChild(tr); }); // 添加列表页码 { { // 添加 < let l = document.createElement('li'); let a = document.createElement('a'); let i = document.createElement('i'); i.classList.add('material-icons'); i.textContent = 'chevron_left'; if (form.data.current_page == 1) { l.classList.add('disabled'); } else { let url = new URL(currentUrl); url.searchParams.set('page', String(form.data.current_page - 1)); a.href = url; l.classList.add('waves-effect'); } a.id = '<'; a.appendChild(i); l.appendChild(a); pg.appendChild(l); } for (i = 3; i >= 1; i--) { if ((form.data.current_page - i) >= 1) { let l = document.createElement('li'); let a = document.createElement('a'); let url = new URL(currentUrl); url.searchParams.set('page', String(form.data.current_page - i)); a.textContent = String(form.data.current_page - i); a.href = url; a.id = '1'; a.classList.add('waves-effect'); l.appendChild(a); pg.appendChild(l); } } { // 添加 当前页面 let l = document.createElement('li'); let a = document.createElement('a'); l.classList.add('active'); a.textContent = String(form.data.current_page); l.appendChild(a); pg.appendChild(l); } for (i = 1; i <= 3; i++) { if ((form.data.current_page + i) <= form.data.total_page) { let l = document.createElement('li'); let a = document.createElement('a'); let url = new URL(currentUrl); url.searchParams.set('page', String(form.data.current_page + i)); a.textContent = String(form.data.current_page + i); a.href = url; a.id = '3'; l.classList.add('waves-effect'); l.appendChild(a); pg.appendChild(l); } } { // 添加 > let l = document.createElement('li'); let a = document.createElement('a'); let i = document.createElement('i'); i.classList.add('material-icons'); i.textContent = 'chevron_right'; if (form.data.current_page == form.data.total_page) { l.classList.add('disabled'); } else { let url = new URL(currentUrl); url.searchParams.set('page', String(form.data.current_page + 1)); a.href = url; l.classList.add('waves-effect'); } a.appendChild(i); l.appendChild(a); pg.appendChild(l); } } } else { alert('Get Serial List failed: ' + error); curl.searchParams.delete('page'); curl.searchParams.delete('search'); console.log(curl) window.location.href = curl; } }) .catch(error => { alert('Get Serial List failed: ' + error); curl.searchParams.delete('page'); curl.searchParams.delete('search'); console.log(curl) window.location.href = curl; }); } /** * 当表单提交时执行的回调函数 * @param {*} text 结构体名称 * @returns */ function onFormSubmit(text) { return function(event) { // 组装一个 json 用于 POST 请求 // 需要知道的内容:1.序列号结构体类型名称 2.表单数据 let bd = {}; bd['struct'] = text; let form = document.getElementById('createSerialForm'); let array = form.querySelectorAll('#section'); array.forEach(it => { bd[it.keyword] = (it.value).toString(); }) let ops = { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(bd) }; fetch('/Api/SerialList', ops) .then(response => { return response.json(); }) .then(json => { if ('data' in json) { alert('Import Configure success: ' + json.data); } else { alert('Submit FormData failed: ' + json.error); } window.location.reload(); }) .catch(error => { alert('Submit FormData failed: ' + error); }); } } ``` ## HTML 转义 `escape()` 当返回HTML (Flask中的默认响应类型)时,输出中呈现的任何用户提供的值都必须进行转义,以防止注入攻击。稍后介绍的使用`Jinja`呈现的HTML模板将自动执行此操作。这里显示的`escape()`可以手动使用。为了简洁起见,在大多数示例中省略了它,但是您应该始终注意如何使用不受信任的数据。 ```python from markupsafe import escape @app.route("/") def hello(name): return f"Hello, {escape(name)}!" ``` ## 路由 `@app.route()` 现代web应用程序使用有意义的`URL`来帮助用户。用户更有可能喜欢一个页面,如果页面使用了一个有意义的`URL`,他们可以记住并使用它直接访问页面。你可以做得更多!您可以使URL的某些部分是动态的,并将多个规则附加到一个函数中。 ```python @app.route('/') def index(): return 'Index Page' @app.route('/hello') def hello(): return 'Hello, World' ``` ## 可变规则 您可以通过使用标记区段来向URL添加可变区段。然后,函数接收``作为关键字参数。您还可以选择使用转换器来指定参数的类型,例如``。 ```python from markupsafe import escape @app.route('/user/') def show_user_profile(username): # show the user profile for that user return f'User {escape(username)}' @app.route('/post/') def show_post(post_id): # show the post with the given id, the id is an integer return f'Post {post_id}' @app.route('/path/') def show_subpath(subpath): # show the subpath after /path/ return f'Subpath {escape(subpath)}' ``` | `string` | (default) accepts any text without a slash | | -------- | ------------------------------------------ | | `int` | accepts positive integers | | `float` | accepts positive floating point values | | `path` | like `string` but also accepts slashes | | `uuid` | accepts `UUID` strings | ## 重定向和唯一URL区别 ```python # projects endpoint 尾部有/,如果访问时没带/(eg: https://localhost/projects),会自动添加/ @app.route('/projects/') def projects(): return 'The project page' # about endpoint 尾部没有/,如果访问是带/(eg: https://localhost/about/),会产生404 @app.route('/about') def about(): return 'The about page' ``` ## URL 构建 要构建指向特定函数的URL,请使用`url_for()`函数。它接受函数名作为第一个参数和任意数量的关键字参数,每个参数对应于`URL`规则的一个变量部分。未知变量部分作为查询参数追加到`URL`。 这样做的优点: 1. 更具描述性 1. 一次性更改多个`url` 1. 透明地处理特殊字符的转义 1. 生成的路径总是绝对的,避免了浏览器中相对路径的意外行为 1. `URL`根目录之外的页面,`url_for()`会正确地为你处理。 ## HTTP 方法 ### `GET`: ### `POST`: ```python from flask import request # 方式一: @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': return do_the_login() else: return show_the_login_form() # 方式二: @app.get('/login') def login_get(): return show_the_login_form() @app.post('/login') def login_post(): return do_the_login() ``` ## 静态文件 ```python # static/style.css url_for('static', filename='style.css') ``` ## 渲染模板 `render_template()` ```python from flask import render_template @app.route('/hello/') @app.route('/hello/') def hello(name=None): return render_template('hello.html', name=name) ``` 模板内部可以使用的函数 | 函数名 | 说明 | | ----------------------- | ---- | | `config` | | | `request` | | | `session` | | | `g` | | | `url_for()` | | | `get_flashed_message()` | | ### `Markup()`: 用于取消自动转义 ```python >>> from markupsafe import Markup >>> Markup('Hello %s!') % 'hacker' Markup('Hello <blink>hacker</blink>!') >>> Markup.escape('hacker') Markup('<blink>hacker</blink>') >>> Markup('Marked up » HTML').striptags() 'Marked up » HTML' ``` ## 访问请求数据 略 ## 请求对象 `Request Object` 导入头文件: `from flask import request` 他有多种属性: - `requset.method`获取请求的方法类型(`GET`/`POST`). eg: `if request.methods == 'POST'` - `request.form`获取表单(`form`)中的数据. eg: `request.form['username']` - `request.args`可以获取`url`中的传参 eg: `request.args.get('key','')` (`url: https://localhost:5000?uid=12`) - `request.file`文件上传,只是要确保不要忘记在`HTML`表单上设置`enctype="multipart/form-data"`属性,否则浏览器根本不会传输您的文件。`request.file`自带有`save`函数 ```python from werkzeug.utils import secure_filename @app.route('/upload', methods=['GET', 'POST']) def upload_file(): if request.method == 'POST': file = request.files['the_file'] file.save(f"/var/www/uploads/{secure_filename(file.filename)}") ``` - `request.cookies`更安全的使用cookie,包括读`username = request.cookies.get('username')`和写`resp.set_cookie('username', 'the username')` ## 重定向`redirect()` ```python @app.route('/') def index(): return redirect(url_for('login')) ``` ## 响应逻辑 - 正确直接返回页面 - 字符串口返回数据和默认参数 - 字符串或字节的迭代器/生成器,视为流响应 - 如果是字典或者列表,需要用`jsonify()`创建响应对象 - 元组会返回额外的信息,采用`(response, status)`,`(response, headers)`,或`(response, status, headers)`的形式. `status`会覆盖`status`,`header`可以是附加头文件值的列表或字典。 - 如果这些都不起作用,Flask将假定返回值是一个有效的`WSGI`应用程序,并将其转换为响应对象 也可以使用`make_response()`获取`response`,然后手动处理 ```python from flask import make_response @app.errorhandler(404) def not_found(error): resp = make_response(render_template('error.html'), 404) resp.headers['X-Something'] = 'A value' return resp ``` ## 会话对象 `Session Object`