feat: 初始化Webhook中继系统项目

- 添加FastAPI应用基础结构,包括主入口、路由和模型定义
- 实现Webhook接收端点(/webhook/{namespace})和健康检查(/health)
- 添加管理后台路由和模板,支持端点、目标、渠道和模板管理
- 包含SQLite数据库模型定义和初始化逻辑
- 添加日志记录和统计服务
- 包含Dockerfile和配置示例文件
- 添加项目文档,包括设计、流程图和验收标准
This commit is contained in:
2025-12-21 18:43:12 +08:00
commit 2bc7460f1f
42 changed files with 3177 additions and 0 deletions
+124
View File
@@ -0,0 +1,124 @@
{% extends "admin/base.html" %}
{% block title %}端点管理 - Webhook{% endblock %}
{% block content %}
<div class="row mb-3">
<div class="col">
<h3>接收端点 (Endpoints)</h3>
</div>
<div class="col-auto">
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addModal">添加端点</button>
</div>
</div>
<div class="alert alert-info alert-dismissible fade show" role="alert">
<strong>什么是“接收端点”?</strong>
<p class="mb-0">
接收端点(Endpoints)是<strong>外部系统向本服务发送Webhook数据的入口地址</strong><br>
<strong>Namespace</strong>:URL路径的唯一标识。例如创建 namespace 为 <code>client_a</code>,则外部请求地址为 <code>/webhook/client_a</code><br>
<strong>流程配置</strong>:点击列表中的 <span class="badge bg-info text-dark">Namespace</span><button class="btn btn-sm btn-outline-primary py-0 px-1">⚙️ 配置</button> 按钮,进入端点的规则编排页面。
</p>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<table class="table table-striped">
<thead>
<tr>
<th>ID</th>
<th>Namespace</th>
<th>完整URL示例</th>
<th>描述</th>
<th>状态</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for e in endpoints %}
<tr>
<td>{{ e.id }}</td>
<td><a href="/admin/endpoints/{{ e.id }}" class="text-decoration-none"><span class="badge bg-info text-dark fs-6">{{ e.namespace }}</span></a></td>
<td>
<code>/webhook/{{ e.namespace }}</code>
<button class="btn btn-sm btn-link text-decoration-none copy-btn" data-path="/webhook/{{ e.namespace }}" title="复制完整URL">📋</button>
</td>
<td>{{ e.description or '-' }}</td>
<td>
{% if e.is_active %}
<span class="badge bg-success">启用</span>
{% else %}
<span class="badge bg-secondary">禁用</span>
{% endif %}
</td>
<td>{{ e.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
<td>
<a href="/admin/endpoints/{{ e.id }}" class="btn btn-sm btn-outline-primary">⚙️ 配置</a>
<form action="/admin/endpoints/toggle" method="post" style="display:inline">
<input type="hidden" name="id" value="{{ e.id }}">
{% if e.is_active %}
<button type="submit" class="btn btn-sm btn-warning">禁用</button>
{% else %}
<button type="submit" class="btn btn-sm btn-success">启用</button>
{% endif %}
</form>
<form action="/admin/endpoints/delete" method="post" style="display:inline" onsubmit="return confirm('确定删除?')">
<input type="hidden" name="id" value="{{ e.id }}">
<button type="submit" class="btn btn-sm btn-danger">删除</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<!-- Add Modal -->
<div class="modal fade" id="addModal" tabindex="-1">
<div class="modal-dialog">
<form action="/admin/endpoints" method="post">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">创建新端点</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Namespace (URL路径的一部分)</label>
<input type="text" class="form-control" name="namespace" required placeholder="my-service-1">
<div class="form-text">只能包含字母、数字、下划线或连字符。</div>
</div>
<div class="mb-3">
<label class="form-label">描述</label>
<input type="text" class="form-control" name="description" placeholder="用于XX业务接收">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="submit" class="btn btn-primary">保存</button>
</div>
</div>
</form>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.copy-btn').forEach(btn => {
btn.addEventListener('click', function() {
const path = this.getAttribute('data-path');
// Construct full URL based on current window location
const fullUrl = window.location.origin + path;
navigator.clipboard.writeText(fullUrl).then(() => {
const originalContent = this.innerHTML;
this.innerHTML = '✅';
setTimeout(() => {
this.innerHTML = originalContent;
}, 1500);
}).catch(err => {
console.error('Failed to copy: ', err);
alert('复制失败,请手动复制');
});
});
});
});
</script>
{% endblock %}