# 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!') % ''
Markup('Hello <blink>hacker</blink>!')
>>> Markup.escape('')
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`