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
+467
View File
@@ -0,0 +1,467 @@
{% extends "admin/base.html" %}
{% block title %}配置端点流程 - Webhook{% endblock %}
{% block content %}
<div class="row mb-3">
<div class="col">
<h3>配置端点流程: <span class="badge bg-info text-dark">{{ ep.namespace }}</span></h3>
<p class="text-muted small">{{ ep.description or '无描述' }}</p>
</div>
<div class="col-auto">
<a href="/admin/endpoints" class="btn btn-outline-secondary">返回列表</a>
</div>
</div>
<div class="alert alert-light border">
<strong>规则引擎说明 (树状逻辑)</strong>
规则自上而下、从外向内匹配。只有父规则匹配成功,才会检查子规则。<br>
子规则会继承父规则设置的模板变量(如支付方式名称)。
</div>
<!-- Recursive Rule Macro -->
{% macro render_rule(rule, level=0) %}
<div class="card mb-3 shadow-sm" style="margin-left: {{ level * 2 }}rem; border-left: 4px solid {% if level == 0 %}#0d6efd{% else %}#6c757d{% endif %};">
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<div>
{% if level > 0 %}
<span class="text-muted me-2">↳ 并且</span>
{% else %}
<span class="fw-bold me-2">根条件:</span>
{% endif %}
字段 <code>{{ rule.match_field }}</code>
<span class="badge bg-secondary">{{ rule.operator or '等于' }}</span>
<span class="badge bg-primary">{{ rule.match_value }}</span>
</div>
<div>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#addRuleModal" data-parent-id="{{ rule.id }}">
+ 添加子规则
</button>
<button class="btn btn-outline-primary"
data-bs-toggle="modal"
data-bs-target="#editRuleModal"
data-rule-id="{{ rule.id }}"
data-match-field="{{ rule.match_field }}"
data-operator="{{ rule.operator }}"
data-match-value="{{ rule.match_value }}"
data-priority="{{ rule.priority or 0 }}"
data-parent-id="{{ rule.parent_rule_id or '' }}">
编辑
</button>
<button class="btn btn-outline-success"
data-bs-toggle="modal"
data-bs-target="#duplicateRuleModal"
data-rule-id="{{ rule.id }}"
data-parent-id="{{ rule.parent_rule_id or '' }}">
复制
</button>
<form action="/admin/rules/delete" method="post" style="display:inline" onsubmit="return confirm('确定删除此规则及其所有子规则?')">
<input type="hidden" name="id" value="{{ rule.id }}">
<input type="hidden" name="endpoint_id" value="{{ ep.id }}">
<button type="submit" class="btn btn-outline-danger">删除</button>
</form>
</div>
</div>
</div>
<div class="card-body py-2">
{% if rule.actions %}
<div class="list-group mb-2">
{% for action in rule.actions %}
<div class="list-group-item d-flex justify-content-between align-items-center py-1">
<div class="small">
{% if action.action_type == 'forward' %}
<span class="badge bg-primary me-2">转发</span>
目标: <strong>{{ action.target.name }}</strong>
{% else %}
<span class="badge bg-success me-2">通知</span>
渠道: <strong>{{ action.channel.name }}</strong> | 模板: <strong>{{ action.template.name }}</strong>
{% if action.template_vars %}
<code class="ms-2">{{ action.template_vars }}</code>
{% endif %}
{% endif %}
</div>
<div class="d-flex align-items-center gap-2">
<button class="btn btn-sm btn-link text-decoration-none p-0"
data-bs-toggle="modal"
data-bs-target="#editActionModal"
data-action-id="{{ action.id }}"
data-action-type="{{ action.action_type }}"
data-target-id="{{ action.target.id if action.target else '' }}"
data-channel-id="{{ action.channel.id if action.channel else '' }}"
data-template-id="{{ action.template.id if action.template else '' }}"
data-template-vars='{{ action.template_vars | tojson | safe if action.template_vars else "" }}'>
编辑
</button>
<form action="/admin/actions/duplicate" method="post" style="display:inline">
<input type="hidden" name="id" value="{{ action.id }}">
<input type="hidden" name="endpoint_id" value="{{ ep.id }}">
<button type="submit" class="btn btn-sm btn-link text-decoration-none p-0">复制</button>
</form>
<form action="/admin/actions/delete" method="post" style="display:inline" onsubmit="return confirm('确定移除此动作?')">
<input type="hidden" name="id" value="{{ action.id }}">
<input type="hidden" name="endpoint_id" value="{{ ep.id }}">
<button type="submit" class="btn btn-sm btn-link text-danger text-decoration-none p-0"></button>
</form>
</div>
</div>
{% endfor %}
</div>
{% endif %}
<div class="d-flex gap-2 mb-2">
<button class="btn btn-sm btn-link text-decoration-none p-0" data-bs-toggle="modal" data-bs-target="#addActionModal"
data-rule-id="{{ rule.id }}" data-type="forward">+ 添加转发</button>
<button class="btn btn-sm btn-link text-decoration-none p-0" data-bs-toggle="modal" data-bs-target="#addActionModal"
data-rule-id="{{ rule.id }}" data-type="notify">+ 添加通知/变量</button>
</div>
</div>
</div>
<!-- Render Children -->
{% for child in rule.child_rules %}
{{ render_rule(child, level + 1) }}
{% endfor %}
{% endmacro %}
<!-- Root Rules List -->
{% for rule in root_rules %}
{{ render_rule(rule) }}
{% endfor %}
<!-- Add Root Rule Button -->
<div class="card border-dashed text-center py-4 mb-4">
<div class="card-body">
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addRuleModal" data-parent-id="">
添加根规则 (Root Rule)
</button>
</div>
</div>
<!-- Add Rule Modal -->
<div class="modal fade" id="addRuleModal" tabindex="-1">
<div class="modal-dialog">
<form id="ruleForm" action="/admin/endpoints/{{ ep.id }}/rules" method="post">
<input type="hidden" name="parent_rule_id" id="parentRuleId">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="ruleModalTitle">添加规则</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">匹配字段 (Match Field)</label>
<input type="text" class="form-control" name="match_field" required placeholder="trans_order_info.remark">
<div class="form-text">JSON字段路径,例如: <code>event_define_no</code><code>body.status</code></div>
</div>
<div class="mb-3">
<label class="form-label">操作符</label>
<select class="form-select" name="operator">
<option value="eq">等于 (Equal)</option>
<option value="neq">不等于 (Not Equal)</option>
<option value="contains">包含 (Contains)</option>
<option value="startswith">开头是 (Starts With)</option>
<option value="regex">正则匹配 (Regex)</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">匹配值 (Match Value)</label>
<input type="text" class="form-control" name="match_value" required placeholder="success">
</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>
<!-- Add Action Modal (Same as before) -->
<div class="modal fade" id="addActionModal" tabindex="-1">
<div class="modal-dialog">
<form id="actionForm" action="/admin/rules/0/actions" method="post">
<input type="hidden" name="endpoint_id" value="{{ ep.id }}">
<input type="hidden" name="action_type" id="actionTypeInput">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="actionModalTitle">添加动作</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<!-- Forward Section -->
<div id="forwardSection" class="d-none">
<div class="mb-3">
<label class="form-label">选择转发目标</label>
<select class="form-select" name="target_id">
<option value="">- 不转发 -</option>
{% for t in targets %}
<option value="{{ t.id }}">{{ t.name }} ({{ t.url }})</option>
{% endfor %}
</select>
</div>
</div>
<!-- Notify Section -->
<div id="notifySection" class="d-none">
<div class="mb-3">
<label class="form-label">选择通知渠道</label>
<select class="form-select" name="channel_id">
<option value="">- 不发送通知 (仅设置变量) -</option>
{% for c in channels %}
<option value="{{ c.id }}">{{ c.name }} ({{ c.channel_type }})</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label class="form-label">选择消息模板</label>
<select class="form-select" name="template_id">
<option value="">- 无模板 -</option>
{% for t in templates %}
<option value="{{ t.id }}">{{ t.name }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label class="form-label">自定义模板变量 (JSON)</label>
<textarea class="form-control" name="template_vars_str" id="templateVars" rows="2" placeholder='{"pay_method": "微信支付"}'></textarea>
<div class="form-text">
定义特有的变量值,例如将 "pay.wx_scaned" 映射为 "微信支付"。<br>
<a href="#" onclick="insertExample('wx'); return false;">[插入微信示例]</a>
<a href="#" onclick="insertExample('ali'); return false;">[插入支付宝示例]</a>
</div>
</div>
</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>
<!-- Edit Rule Modal -->
<div class="modal fade" id="editRuleModal" tabindex="-1">
<div class="modal-dialog">
<form id="editRuleForm" action="/admin/rules/update" method="post">
<input type="hidden" name="id" id="editRuleId">
<input type="hidden" name="endpoint_id" value="{{ ep.id }}">
<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">匹配字段</label>
<input type="text" class="form-control" name="match_field" id="editMatchField" required>
</div>
<div class="mb-3">
<label class="form-label">操作符</label>
<select class="form-select" name="operator" id="editOperator">
<option value="eq">等于</option>
<option value="neq">不等于</option>
<option value="contains">包含</option>
<option value="startswith">开头是</option>
<option value="regex">正则匹配</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">匹配值</label>
<input type="text" class="form-control" name="match_value" id="editMatchValue" required>
</div>
<div class="mb-3">
<label class="form-label">优先级</label>
<input type="number" class="form-control" name="priority" id="editPriority" value="0">
</div>
<div class="mb-3">
<label class="form-label">父规则ID(为空为根)</label>
<input type="text" class="form-control" name="parent_rule_id" id="editParentId">
</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>
<!-- Duplicate Rule Modal -->
<div class="modal fade" id="duplicateRuleModal" tabindex="-1">
<div class="modal-dialog">
<form id="duplicateRuleForm" action="/admin/rules/duplicate" method="post">
<input type="hidden" name="rule_id" id="dupRuleId">
<input type="hidden" name="endpoint_id" value="{{ ep.id }}">
<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">父规则ID(为空为根)</label>
<input type="text" class="form-control" name="parent_rule_id" id="dupParentId">
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="true" id="dupIncludeChildren" name="include_children" checked>
<label class="form-check-label" for="dupIncludeChildren">
包含子规则和动作(深拷贝)
</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="submit" class="btn btn-success">复制</button>
</div>
</div>
</form>
</div>
</div>
<!-- Edit Action Modal -->
<div class="modal fade" id="editActionModal" tabindex="-1">
<div class="modal-dialog">
<form id="editActionForm" action="/admin/actions/update" method="post">
<input type="hidden" name="id" id="editActionId">
<input type="hidden" name="endpoint_id" value="{{ ep.id }}">
<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">动作类型</label>
<select class="form-select" name="action_type" id="editActionType">
<option value="forward">转发</option>
<option value="notify">通知/变量</option>
</select>
</div>
<div id="editForwardSection" class="mb-3 d-none">
<label class="form-label">选择转发目标</label>
<select class="form-select" name="target_id" id="editTargetId">
<option value="">- 不转发 -</option>
{% for t in targets %}
<option value="{{ t.id }}">{{ t.name }} ({{ t.url }})</option>
{% endfor %}
</select>
</div>
<div id="editNotifySection" class="mb-3 d-none">
<label class="form-label">选择通知渠道</label>
<select class="form-select" name="channel_id" id="editChannelId">
<option value="">- 不发送通知 -</option>
{% for c in channels %}
<option value="{{ c.id }}">{{ c.name }} ({{ c.channel_type }})</option>
{% endfor %}
</select>
<label class="form-label mt-2">选择消息模板</label>
<select class="form-select" name="template_id" id="editTemplateId">
<option value="">- 无模板 -</option>
{% for t in templates %}
<option value="{{ t.id }}">{{ t.name }}</option>
{% endfor %}
</select>
<label class="form-label mt-2">模板变量 (JSON)</label>
<textarea class="form-control" name="template_vars_str" id="editTemplateVars" rows="2"></textarea>
</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>
{% endblock %}
{% block scripts %}
<script>
var ruleModal = document.getElementById('addRuleModal');
ruleModal.addEventListener('show.bs.modal', function (event) {
var button = event.relatedTarget;
var parentId = button.getAttribute('data-parent-id');
document.getElementById('parentRuleId').value = parentId || "";
if (parentId) {
document.getElementById('ruleModalTitle').textContent = '添加子规则';
} else {
document.getElementById('ruleModalTitle').textContent = '添加根规则';
}
});
var actionModal = document.getElementById('addActionModal');
actionModal.addEventListener('show.bs.modal', function (event) {
var button = event.relatedTarget;
var ruleId = button.getAttribute('data-rule-id');
var type = button.getAttribute('data-type');
var form = document.getElementById('actionForm');
form.action = '/admin/rules/' + ruleId + '/actions';
document.getElementById('actionTypeInput').value = type;
if (type === 'forward') {
document.getElementById('actionModalTitle').textContent = '添加转发动作';
document.getElementById('forwardSection').classList.remove('d-none');
document.getElementById('notifySection').classList.add('d-none');
} else {
document.getElementById('actionModalTitle').textContent = '添加通知/变量动作';
document.getElementById('forwardSection').classList.add('d-none');
document.getElementById('notifySection').classList.remove('d-none');
}
});
function insertExample(type) {
var field = document.getElementById('templateVars');
if (type === 'wx') {
field.value = '{"pay_method_name": "微信支付"}';
} else if (type === 'ali') {
field.value = '{"pay_method_name": "支付宝"}';
}
}
var editRuleModal = document.getElementById('editRuleModal');
editRuleModal.addEventListener('show.bs.modal', function (event) {
var button = event.relatedTarget;
document.getElementById('editRuleId').value = button.getAttribute('data-rule-id');
document.getElementById('editMatchField').value = button.getAttribute('data-match-field') || '';
document.getElementById('editOperator').value = button.getAttribute('data-operator') || 'eq';
document.getElementById('editMatchValue').value = button.getAttribute('data-match-value') || '';
document.getElementById('editPriority').value = button.getAttribute('data-priority') || '0';
document.getElementById('editParentId').value = button.getAttribute('data-parent-id') || '';
});
var duplicateRuleModal = document.getElementById('duplicateRuleModal');
duplicateRuleModal.addEventListener('show.bs.modal', function (event) {
var button = event.relatedTarget;
document.getElementById('dupRuleId').value = button.getAttribute('data-rule-id');
document.getElementById('dupParentId').value = button.getAttribute('data-parent-id') || '';
document.getElementById('dupIncludeChildren').checked = true;
});
var editActionModal = document.getElementById('editActionModal');
editActionModal.addEventListener('show.bs.modal', function (event) {
var button = event.relatedTarget;
var type = button.getAttribute('data-action-type') || 'notify';
document.getElementById('editActionId').value = button.getAttribute('data-action-id');
document.getElementById('editActionType').value = type;
document.getElementById('editTargetId').value = button.getAttribute('data-target-id') || '';
document.getElementById('editChannelId').value = button.getAttribute('data-channel-id') || '';
document.getElementById('editTemplateId').value = button.getAttribute('data-template-id') || '';
document.getElementById('editTemplateVars').value = button.getAttribute('data-template-vars') || '';
if (type === 'forward') {
document.getElementById('editForwardSection').classList.remove('d-none');
document.getElementById('editNotifySection').classList.add('d-none');
} else {
document.getElementById('editForwardSection').classList.add('d-none');
document.getElementById('editNotifySection').classList.remove('d-none');
}
});
</script>
{% endblock %}