- 添加FastAPI应用基础结构,包括主入口、路由和模型定义
- 实现Webhook接收端点(/webhook/{namespace})和健康检查(/health)
- 添加管理后台路由和模板,支持端点、目标、渠道和模板管理
- 包含SQLite数据库模型定义和初始化逻辑
- 添加日志记录和统计服务
- 包含Dockerfile和配置示例文件
- 添加项目文档,包括设计、流程图和验收标准
468 lines
24 KiB
HTML
468 lines
24 KiB
HTML
{% 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 %}
|