This commit is contained in:
shufeng2012 committed 2026-07-24 06:36:26 +08:00
1 parent 03d7b07bf6
commit 400f527a88
7 files changed

No files matched your search

+112
View File
@@ -0,0 +1,112 @@
#prjs
*.db
*.bin
*.build
*.dist
*.onefile-build
nuitka-crash-report.xml
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 卿松
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+147
View File
@@ -0,0 +1,147 @@
# 跨数据库通用查询服务(基于 SQLAlchemy Core)
一个安全的 HTTP 服务,通过结构化 JSON 描述查询意图,后端自动转换为对应数据库的 SQL,无需手写 SQL,即可无缝切换 SQLite、PostgreSQL、MySQL 等数据库。
## 功能特点
- **跨数据库**:基于 SQLAlchemy Core 表达式语言,一套接口适配多种数据库
- **零 SQL 注入**:所有条件、值均通过 SQLAlchemy 参数绑定处理
- **统一 JSON 接口**:`action` + `table` + `conditions` 模式
- **支持增删改查**:select, insert, update, delete 全覆盖
- **单线程本地监听**:waitress + 127.0.0.1:8005,简单可靠
## 快速开始
### 安装依赖
```bash
pip install flask sqlalchemy waitress
# 根据需要安装数据库驱动,例如:
# pip install psycopg2-binary # PostgreSQL
# pip install pymysql # MySQL
```
### 启动服务(默认 SQLite)
```bash
python app.py
```
服务运行在 `http://127.0.0.1:8005`。
使用其他数据库:
```bash
export DATABASE_URL="postgresql://user:pass@localhost/dbname"
python app.py
```
**注意**:新接口依赖表已存在(通过反射获取结构)。首次使用时请先手动建表:
```bash
sqlite3 database.db "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)"
```
## API 文档
### 请求格式
**端点**:`POST /execute`
**Content-Type**:`application/json`
```json
{
"action": "操作类型", // select | insert | update | delete
"table": "表名",
"columns": ["列1", "列2"], // select/insert 时可选
"values": { // insert/update 时必填
"列名": "值"
},
"where": { // 条件,支持组合操作符
"age": {"ge": 18, "lt": 60},
"name": "Alice"
},
"order_by": [{"column": "age", "desc": true}],
"limit": 10,
"offset": 0
}
```
### where 条件支持的运算符
| 运算符 | 说明 | 示例 |
|--------|------|------|
| 直接值 | 等于 | `"name": "Alice"` |
| eq | 等于 | `"age": {"eq": 30}` |
| ne | 不等于 | `"status": {"ne": "deleted"}` |
| gt | 大于 | `"score": {"gt": 80}` |
| ge | 大于等于 | `"score": {"ge": 60}` |
| lt | 小于 | `"price": {"lt": 100}` |
| le | 小于等于 | `"price": {"le": 200}` |
| like | 模糊匹配 | `"name": {"like": "%Smith%"}` |
| in | 包含于列表 | `"id": {"in": [1,2,3]}` |
多个条件自动使用 **AND** 连接。
### 响应格式
成功:
```json
{
"success": true,
"data": [ // 查询结果数组(insert/update/delete 时包含影响行数或 lastrowid)
{"id": 1, "name": "Alice"}
]
}
```
失败:
```json
{
"success": false,
"error": "错误描述"
}
```
## 使用示例(curl)
**插入**:
```bash
curl -X POST http://127.0.0.1:8005/execute \
-H "Content-Type: application/json" \
-d '{"action":"insert","table":"users","values":{"name":"Bob","age":25}}'
```
**查询**:
```bash
curl -X POST http://127.0.0.1:8005/execute \
-H "Content-Type: application/json" \
-d '{"action":"select","table":"users","where":{"age":{"gt":20}},"order_by":[{"column":"id","desc":true}],"limit":5}'
```
**更新**:
```bash
curl -X POST http://127.0.0.1:8005/execute \
-H "Content-Type: application/json" \
-d '{"action":"update","table":"users","values":{"age":26},"where":{"name":"Bob"}}'
```
**删除**:
```bash
curl -X POST http://127.0.0.1:8005/execute \
-H "Content-Type: application/json" \
-d '{"action":"delete","table":"users","where":{"id":{"in":[1,2]}}}'
```
## 安全机制
- 强制使用 SQLAlchemy 参数绑定,杜绝 SQL 注入。
- 仅反射已有表,无法创建或删除表(保证结构安全)。
- 所有异常自动回滚事务。
- 默认监听 `127.0.0.1`,仅本机可访问。
## 切换数据库
只需修改环境变量 `DATABASE_URL`,并安装对应驱动:
- SQLite:`sqlite:///database.db`
- PostgreSQL:`postgresql://user:pass@localhost/dbname` (需 `pip install psycopg2`)
- MySQL:`mysql+pymysql://user:pass@localhost/dbname` (需 `pip install pymysql`)
SQLAlchemy 会自动生成适配各数据库的 SQL 方言,无需修改接口调用。
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
cd "$(dirname "$0")" || exit
source ./venv/bin/activate
exec ./main.bin
#exec python3 main.py
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
cd "$(dirname "$0")" || exit
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
nuitka --onefile --jobs=1 main.py
+151
View File
@@ -0,0 +1,151 @@
import os
from flask import Flask, request, jsonify, g
from sqlalchemy import (
create_engine, MetaData, Table, select, insert, update, delete, text
)
from sqlalchemy.exc import SQLAlchemyError
from waitress import serve
app = Flask(__name__)
# 数据库配置
DATABASE_URL = os.environ.get('DATABASE_URL', 'sqlite:///database.db')
engine = create_engine(DATABASE_URL)
# 缓存反射的元数据(避免每次请求都反射)
metadata = MetaData()
def get_table(table_name):
"""反射表对象(若已缓存则直接返回)"""
if table_name not in metadata.tables:
Table(table_name, metadata, autoload_with=engine)
return metadata.tables[table_name]
def build_where(table, conditions):
"""将条件字典转换为 SQLAlchemy 条件表达式"""
if not conditions:
return None
clauses = []
for col, val in conditions.items():
column = table.columns[col]
if isinstance(val, dict):
# 支持多种比较操作
if 'eq' in val:
clauses.append(column == val['eq'])
if 'ne' in val:
clauses.append(column != val['ne'])
if 'gt' in val:
clauses.append(column > val['gt'])
if 'lt' in val:
clauses.append(column < val['lt'])
if 'ge' in val:
clauses.append(column >= val['ge'])
if 'le' in val:
clauses.append(column <= val['le'])
if 'like' in val:
clauses.append(column.like(val['like']))
if 'in' in val:
clauses.append(column.in_(val['in']))
else:
# 直接等值比较
clauses.append(column == val)
from sqlalchemy import and_
return and_(*clauses)
@app.route('/execute', methods=['POST'])
def execute_structured():
data = request.get_json()
if not data:
return jsonify({'success': False, 'error': '请求体不能为空'}), 400
action = data.get('action', 'select')
table_name = data.get('table')
if not table_name:
return jsonify({'success': False, 'error': '缺少 table 字段'}), 400
try:
table = get_table(table_name)
except Exception as e:
return jsonify({'success': False, 'error': f'表加载失败: {str(e)}'}), 400
conn = None
try:
conn = engine.connect()
result = []
if action == 'select':
columns = data.get('columns', None)
if columns:
cols = [table.columns[c] for c in columns]
else:
cols = [table] # select *
stmt = select(*cols)
where_cond = build_where(table, data.get('where'))
if where_cond is not None:
stmt = stmt.where(where_cond)
# 排序
order_by = data.get('order_by')
if order_by:
for item in order_by:
col = table.columns[item['column']]
stmt = stmt.order_by(col.desc() if item.get('desc') else col.asc())
# 分页
limit = data.get('limit')
if limit is not None:
stmt = stmt.limit(limit)
offset = data.get('offset')
if offset is not None:
stmt = stmt.offset(offset)
res = conn.execute(stmt)
rows = res.fetchall()
result = [dict(row._mapping) for row in rows]
elif action == 'insert':
values = data.get('values', {})
stmt = insert(table).values(**values)
res = conn.execute(stmt)
conn.commit()
result = [{'lastrowid': res.lastrowid}] if hasattr(res, 'lastrowid') else []
elif action == 'update':
values = data.get('values', {})
where_cond = build_where(table, data.get('where'))
stmt = update(table).values(**values)
if where_cond is not None:
stmt = stmt.where(where_cond)
res = conn.execute(stmt)
conn.commit()
result = [{'rowcount': res.rowcount}]
elif action == 'delete':
where_cond = build_where(table, data.get('where'))
stmt = delete(table)
if where_cond is not None:
stmt = stmt.where(where_cond)
res = conn.execute(stmt)
conn.commit()
result = [{'rowcount': res.rowcount}]
else:
return jsonify({'success': False, 'error': f'不支持的操作: {action}'}), 400
return jsonify({'success': True, 'data': result})
except SQLAlchemyError as e:
if conn:
conn.rollback()
return jsonify({'success': False, 'error': str(e)}), 400
except Exception as e:
if conn:
conn.rollback()
return jsonify({'success': False, 'error': str(e)}), 400
finally:
if conn:
conn.close()
if __name__ == '__main__':
# 单线程单进程,绑定 127.0.0.1
serve(app, host='127.0.0.1', port=8005, threads=1)
+3
View File
@@ -0,0 +1,3 @@
nuitka
flask
waitress