一、场景:
当与外部联调或者内部需要走一些固定流程,且重复的事情,往往需要测试经常性的配合且做重复的工作的联调,这时候需要一些工具作为辅助,或者提供给外部
二、框架:
可以通过Python+Flask+Html做一个简单的测试联调工具
三、原理
通过app.py调用我们原有接口进行登录获取token并保存后传给内部接口,并通过Flask框架获取前端传值并传给我们内部接口进行内部接口的调用起到一个web页面操作进行调用内部接口的作用
四、源码:
1、框架结构:
-
app.py:后端代码
-
templates :前端代码
2、后端代码:
python
from flask import Flask, request, session, redirect, url_for, render_template
import requests
app = Flask(__name__)
app.secret_key = 'your_very_secure_secret_key_2024!' # 生产环境需使用强密钥
# 基础配置
app.config.from_mapping(
BASE_URL="https://XXXX.com",
LOGIN_URL="/login",
APP_URL="https://XXXX.com",
)
@app.before_first_request
def debug_routes():
print("\n=== 已注册的路由清单 ===")
for rule in app.url_map.iter_rules():
print(f"端点: {rule.endpoint}, 路径: {rule.rule}")
def get_api_headers():
"""管理端请求头(使用 Bearer 认证)"""
return {
"Authorization": f"Bearer {session.get('auth_token', '')}",
"Content-Type": "application/json"
}
def get_app_headers():
"""APP端动态请求头(使用 token 字段)"""
return {
"host": "XXXX.com",
"clienttype": "WORKER_APP",
"gxd-client": "WORKER_APP_CLIENT",
"platformtype": "iOS",
"token": session.get('auth_token', '') # 统一使用相同 token
}
def login_required(f):
"""增强版登录校验装饰器"""
def wrapper(*args, **kwargs):
if not session.get('logged_in'):
return redirect(url_for('login'))
return f(*args, **kwargs)
return wrapper
@app.route('/', methods=['GET', 'POST'], endpoint='index') # 根路径
@login_required # 装饰器在 @app.route 下方
def index(): # 函数名即端点名称 'index'
result = None
errors = []
if request.method == 'POST':
session['last_form'] = request.form.to_dict()
action = request.form.get('action')
order_no = request.form.get('order_no', '').strip()
raise_fee = request.form.get('raise_fee', '0')
# 操作映射表
action_handlers = {
'raise': lambda: add_raise_price(order_no, raise_fee),
'query': lambda: get_order_detail(order_no),
'complete': lambda: complete_order(order_no),
'upload': lambda: upload_location(),
'take': lambda: take_order(order_no),
'picking': lambda: picking_order(order_no),
'pickup': lambda: pickup_order(order_no),
'worker_complete': lambda: worker_complete_order(order_no)
}
if action in action_handlers:
handler_result = action_handlers[action]()
if 'error' in handler_result:
errors.append(handler_result['error'])
else:
result = handler_result
else:
errors.append("无效的操作类型")
return render_template('index.html', errors=errors, result=result)
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
# 获取登录凭证
username = request.form.get('username')
password = request.form.get('password')
# 构造登录请求
login_url = f"{app.config['BASE_URL']}{app.config['LOGIN_URL']}"
try:
response = requests.post(
login_url,
json={
"password": password,
"phone": username
},
timeout=10
)
login_data = response.json()
print(login_data)
print(login_data.get('code'))
print(login_data["data"]["token"])
# 处理登录响应
if login_data.get('code') == 0: # 根据实际文档调整
# 统一存储 token 到 auth_token
session['auth_token'] = login_data["data"]["token"]
session['logged_in'] = True
session.modified = True # 强制保存 Session
print("DEBUG - 当前 Session 内容:", dict(session)) # 输出 Session 内容
return redirect(url_for('index'))
else:
error_msg = login_data.get('message', '登录失败')
return render_template('login.html', error=error_msg)
except Exception as e:
return render_template('login.html', error=str(e))
return render_template('login.html')
@app.route('/logout')
def logout():
session.clear()
return redirect(url_for('login'))
def handle_api_request(method, url, headers_type='app', payload=None, params=None):
"""统一处理 API 请求(增强版)"""
try:
headers = get_app_headers() if headers_type == 'app' else get_api_headers()
response = requests.request(
method=method,
url=url,
headers=headers,
json=payload,
params=params,
timeout=15
)
# 处理认证失效情况
if response.status_code in (401, 403):
session.clear()
return {"error": "会话已过期,请重新登录"}
response_data = response.json()
return response_data
except requests.exceptions.RequestException as e:
return {"error": f"网络请求失败: {str(e)}"}
# API 操作函数(统一使用 handle_api_request)
def get_order_detail(order_no):
"""查询订单(管理端接口)"""
url = f"{app.config['BASE_URL']}/admin/XXXXX/{order_no}"
return handle_api_request('GET', url, 'api')
def add_raise_price(order_no, raise_fee):
"""加价接口(管理端接口)"""
url = f"{app.config['BASE_URL']}/admin/CCCCCC"
payload = {"orderNo": order_no, "raiseFee": float(raise_fee)}
return handle_api_request('POST', url, 'api', payload)
def upload_location():
"""上传位置信息"""
url = f"{app.config['APP_URL']}/api/XXXXXX"
payload = {"location": "113.941204,22.527776"}
return handle_api_request('POST', url, 'app',payload)
def take_order(order_no):
"""骑手抢单"""
url = f"{app.config['APP_URL']}/api/XXXXXX"
payload = {"signAgreement": 1, "orderNo": order_no, "workerLocation": ""}
return handle_api_request('POST', url, 'app', payload)
def picking_order(order_no):
"""骑手已就位"""
url = f"{app.config['APP_URL']}/api/XXXXXXX"
payload = {"workerLocation": "113.941204,22.527776", "orderNo": order_no}
return handle_api_request('POST', url, 'app', payload)
def pickup_order(order_no):
"""骑手确认取件"""
url = f"{app.config['APP_URL']}/api/XXXXXXX"
payload = {"workerLocation": "113.941204,22.527776", "orderNo": order_no}
return handle_api_request('POST', url, 'app', payload)
def worker_complete_order(order_no):
"""骑手完成订单"""
url = f"{app.config['APP_URL']}/api/XXXXXXX"
payload = {"workerLocation": "113.941204,22.527776", "orderNo": order_no}
return handle_api_request('POST', url, 'app', payload)
def complete_order(order_no):
"""强制签收(管理端接口)"""
url = f"{app.config['BASE_URL']}/api/XXXXXXX/{order_no}"
return handle_api_request('POST', url, 'api')
if __name__ == '__main__':
# 调试时可启用详细日志:
app.run(host='0.0.0.0', port=5001, debug=True)
3、前端登录代码
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>骑手登录 | 即时配送系统</title>
<style>
:root {
--primary-color: #409EFF;
--success-color: #67C23A;
--error-color: #F56C6C;
--bg-color: #f5f7fa;
--text-primary: #303133;
--text-secondary: #909399;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, "PingFang SC", "Hiragino Sans GB",
"Microsoft YaHei", sans-serif;
background-color: var(--bg-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.login-container {
background: white;
padding: 2.5rem;
border-radius: 12px;
box-shadow: 0 6px 30px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 400px;
transition: transform 0.3s ease;
}
.login-container:hover {
transform: translateY(-2px);
}
h2 {
color: var(--text-primary);
font-size: 1.8rem;
margin-bottom: 2rem;
text-align: center;
position: relative;
}
h2::after {
content: '';
display: block;
width: 50px;
height: 3px;
background: var(--primary-color);
margin: 0.8rem auto;
border-radius: 2px;
}
.form-group {
margin-bottom: 1.5rem;
position: relative;
}
label {
display: block;
margin-bottom: 0.8rem;
color: var(--text-secondary);
font-size: 0.9rem;
font-weight: 500;
}
input {
width: 100%;
padding: 0.9rem 1.2rem;
border: 2px solid #e4e7ed;
border-radius: 8px;
font-size: 1rem;
transition: all 0.3s ease;
}
input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.1);
}
input::placeholder {
color: #c0c4cc;
}
.error {
background: #fef0f0;
color: var(--error-color);
padding: 1rem;
border-radius: 8px;
margin: 1.5rem 0;
display: flex;
align-items: center;
gap: 0.8rem;
border: 1px solid #fbc4c4;
}
.error::before {
content: "!";
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
background: var(--error-color);
color: white;
border-radius: 50%;
font-weight: bold;
}
button {
width: 100%;
padding: 1rem;
background: var(--primary-color);
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 1rem;
}
button:hover {
background: #66b1ff;
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
}
@media (max-width: 480px) {
.login-container {
margin: 1rem;
padding: 1.5rem;
}
h2 {
font-size: 1.5rem;
}
}
</style>
</head>
<body>
<div class="login-container">
<h2>🚴 骑手登录</h2>
{% if error %}
<div class="error">
{{ error }}
</div>
{% endif %}
<form method="post">
<div class="form-group">
<label>📱 手机号码</label>
<input
type="text"
name="username"
required
placeholder="请输入骑手手机号码"
pattern="1[3-9]\d{9}"
inputmode="numeric"
>
</div>
<div class="form-group">
<label>🔒 登录密码</label>
<input
type="password"
name="password"
required
placeholder="请输入密码"
minlength="6"
maxlength="20"
>
</div>
<button type="submit">骑手登录 →</button>
</form>
</div>
</body>
</html>
4、前端index页面
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>订单调试工具</title>
<style>
:root {
--primary-color: #409EFF;
--success-color: #67C23A;
--warning-color: #E6A23C;
--danger-color: #F56C6C;
--text-primary: #303133;
--text-regular: #606266;
--border-color: #DCDFE6;
--background-base: #f5f7fa;
}
body {
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", Arial, sans-serif;
background-color: var(--background-base);
color: var(--text-primary);
line-height: 1.5;
margin: 0;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
padding: 24px;
}
.header {
display: flex;
justify-content: flex-end;
margin-bottom: 24px;
}
.header a {
color: var(--primary-color);
text-decoration: none;
padding: 8px 12px;
border-radius: 4px;
transition: all 0.3s;
}
.header a:hover {
background-color: rgba(64,158,255,.1);
}
h1 {
font-size: 24px;
color: var(--text-primary);
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border-color);
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: var(--text-regular);
}
select, input {
width: 100%;
padding: 10px 12px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 14px;
transition: border-color 0.3s;
}
select:focus, input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(64,158,255,.2);
}
button {
background-color: var(--primary-color);
color: white;
border: none;
padding: 10px 24px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
button:hover {
background-color: #66b1ff;
transform: translateY(-1px);
}
.error {
background-color: #fef0f0;
color: var(--danger-color);
padding: 16px;
border-radius: 4px;
margin: 20px 0;
border: 1px solid #fbc4c4;
}
pre {
background: #f8f8f8;
padding: 16px;
border-radius: 4px;
border: 1px solid var(--border-color);
overflow-x: auto;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
}
.transition {
transition: all 0.3s ease;
}
.hidden-field {
height: 0;
opacity: 0;
overflow: hidden;
margin: 0;
padding: 0;
border: none;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<a href="{{ url_for('logout') }}">退出登录</a>
</div>
<h1>📦 订单调试工具</h1>
<form method="POST">
<div class="form-group">
<label>📌 操作类型</label>
<select name="action" id="actionSelector" required class="transition">
<option value="upload" {% if request.form.get('action') == 'upload' %}selected{% endif %}>📍 上传定位</option>
<option value="take" {% if request.form.get('action') == 'take' %}selected{% endif %}>🚴 骑手抢单</option>
<option value="picking" {% if request.form.get('action') == 'picking' %}selected{% endif %}>🛵 骑手已就位</option>
<option value="pickup" {% if request.form.get('action') == 'pickup' %}selected{% endif %}>📦 骑手确认取件</option>
<option value="worker_complete" {% if request.form.get('action') == 'worker_complete' %}selected{% endif %}>✅ 骑手确认送达</option>
</select>
</div>
<div class="form-group transition" id="orderNoField">
<label>🔢 订单号</label>
<input type="text" name="order_no" id="orderNoInput"
value="{{ request.form.get('order_no', '') }}"
placeholder="请输入订单号(上传定位不需要)">
</div>
<div class="form-group transition" id="raiseFeeField">
<label>💰 加价金额</label>
<input type="number" step="0.01" name="raise_fee"
value="{{ request.form.get('raise_fee', '') }}"
placeholder="请输入大于0的数字">
</div>
<button type="submit" class="transition">🚀 提交操作</button>
</form>
{% if errors %}
<div class="error">
<h3>❌ 操作异常</h3>
<ul>
{% for error in errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if result %}
<div class="result">
<h3>📄 接口响应</h3>
<pre>{{ result|tojson(indent=2,ensure_ascii=False) }}</pre>
</div>
{% endif %}
</div>
<script>
// 动态字段控制
const actionSelector = document.getElementById('actionSelector');
const fields = {
orderNo: {
element: document.getElementById('orderNoField'),
requiredActions: ['take', 'picking', 'pickup', 'worker_complete']
},
raiseFee: {
element: document.getElementById('raiseFeeField'),
requiredActions: ['raise']
}
};
function toggleField(field, show) {
field.element.style.height = show ? 'auto' : '0';
field.element.style.opacity = show ? '1' : '0';
field.element.style.margin = show ? '0 0 20px' : '0';
const inputs = field.element.querySelectorAll('input');
inputs.forEach(input => input.required = show);
}
function updateForm() {
const action = actionSelector.value;
// 处理订单号字段
toggleField(fields.orderNo, fields.orderNo.requiredActions.includes(action));
// 处理加价字段
toggleField(fields.raiseFee, fields.raiseFee.requiredActions.includes(action));
}
// 初始化
updateForm();
actionSelector.addEventListener('change', updateForm);
// 提交后自动滚动到结果
{% if result %}
window.scrollTo({
top: document.body.scrollHeight,
behavior: 'smooth'
});
{% endif %}
</script>
</body>
</html>
5、工具页面展示:
登录页面
操作页面