WebhockTransfer/templates/admin/endpoint_detail.html
houhuan 2bc7460f1f feat: 初始化Webhook中继系统项目
- 添加FastAPI应用基础结构,包括主入口、路由和模型定义
- 实现Webhook接收端点(/webhook/{namespace})和健康检查(/health)
- 添加管理后台路由和模板,支持端点、目标、渠道和模板管理
- 包含SQLite数据库模型定义和初始化逻辑
- 添加日志记录和统计服务
- 包含Dockerfile和配置示例文件
- 添加项目文档,包括设计、流程图和验收标准
2025-12-21 18:43:12 +08:00

468 lines
24 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% 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 %}