206 lines
10 KiB
HTML
206 lines
10 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>OrderSupportTicket 测试控制台</title>
|
||
<style>
|
||
:root{--bg:#f4f6f8;--panel:#fff;--line:#d9e0e7;--text:#202a33;--muted:#667685;--brand:#98638f;--brand2:#774d70;--ok:#237a4b;--err:#b3261e}
|
||
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:14px/1.5 "Microsoft YaHei UI","PingFang SC",Segoe UI,sans-serif}
|
||
header{padding:18px 24px;background:linear-gradient(120deg,#5b3a57,#98638f);color:#fff;display:flex;justify-content:space-between;gap:16px;align-items:center}
|
||
header h1{margin:0;font-size:21px}header p{margin:3px 0 0;opacity:.85;font-size:13px}header a{color:#fff}
|
||
main{max-width:1320px;margin:0 auto;padding:18px;display:grid;grid-template-columns:minmax(420px,1fr) minmax(380px,1fr);gap:16px}
|
||
.card{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:16px}.card h2{margin:0 0 12px;font-size:16px}
|
||
.grid{display:grid;grid-template-columns:130px 1fr;gap:9px 12px;align-items:center}.grid label{color:var(--muted);font-size:13px}
|
||
input,textarea,button{font:inherit}input,textarea{width:100%;border:1px solid var(--line);border-radius:5px;padding:7px 9px;background:#fff;color:var(--text)}
|
||
textarea{min-height:68px;resize:vertical}.actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}
|
||
button{border:1px solid var(--brand);border-radius:5px;background:var(--brand);color:#fff;padding:7px 12px;cursor:pointer}button:hover{background:var(--brand2)}button.ghost{background:#fff;color:var(--brand)}button.danger{border-color:var(--err);background:var(--err)}
|
||
button:disabled{opacity:.45;cursor:not-allowed}.hint{color:var(--muted);font-size:12px;margin:8px 0 0}
|
||
pre{min-height:260px;max-height:560px;overflow:auto;margin:0;padding:12px;border-radius:6px;background:#111820;color:#d8e2eb;white-space:pre-wrap;word-break:break-word;font:12px/1.55 Consolas,monospace}
|
||
#sseLog{max-height:280px}.logline{padding:4px 0;border-bottom:1px dashed var(--line);word-break:break-all}.logline:last-child{border-bottom:0}
|
||
.state{display:inline-flex;align-items:center;gap:6px;font-size:12px;margin-left:8px;color:var(--muted)}.dot{width:8px;height:8px;border-radius:50%;background:#8896a2}.dot.on{background:var(--ok)}.dot.err{background:var(--err)}
|
||
@media(max-width:900px){main{grid-template-columns:1fr}header{align-items:flex-start;flex-direction:column}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<div><h1>OrderSupportTicket 测试控制台</h1><p>纯内存数据 · 重启后恢复种子数据 · <a href="/swagger" target="_blank">打开 Swagger</a></p></div>
|
||
<div><a href="/health" target="_blank">健康检查</a> <a href="/api/test/state" target="_blank">当前内存状态</a></div>
|
||
</header>
|
||
<main>
|
||
<section class="card">
|
||
<h2>请求参数</h2>
|
||
<div class="grid">
|
||
<label>purchaseAccount</label><input id="account" value="18990991234">
|
||
<label>appIds</label><input id="appIds" value="wx316b1291351558aa,wx7999743c3d64c05c">
|
||
<label>appId</label><input id="appId" value="wx316b1291351558aa">
|
||
<label>wechatOrderId</label><input id="orderId" value="9000000000000000001">
|
||
<label>创建描述</label><textarea id="description">测试新建工单:商品什么时候发货?</textarea>
|
||
<label>客户留言</label><textarea id="messageText">补充说明:请尽快处理,谢谢。</textarea>
|
||
<label>clientRequestId</label><input id="clientRequestId" placeholder="留空自动生成">
|
||
<label>clientMessageId</label><input id="clientMessageId" placeholder="留空自动生成">
|
||
<label>已读 sequenceNo</label><input id="sequenceNo" type="number" min="1" value="1">
|
||
</div>
|
||
<p class="hint">种子数据:账号 18990991234 下有两个已有工单;订单 9000000000000000001 已有一条客服回复,适合测试未读与 SSE。</p>
|
||
<div class="actions">
|
||
<button onclick="listOrders()">查询工单列表</button>
|
||
<button onclick="getMessages()">查询留言</button>
|
||
<button onclick="createTicket()">创建工单</button>
|
||
<button onclick="sendMessage()">发送客户留言</button>
|
||
<button onclick="markRead()">标记已读</button>
|
||
<button class="ghost" onclick="supportReply()">模拟客服回复</button>
|
||
<button class="ghost" onclick="completeTicket()">模拟完结</button>
|
||
<button class="ghost" onclick="loadState()">查看内存状态</button>
|
||
<button class="danger" onclick="resetData()">重置数据</button>
|
||
</div>
|
||
</section>
|
||
<section class="card">
|
||
<h2>接口响应</h2>
|
||
<pre id="output">等待操作…</pre>
|
||
</section>
|
||
<section class="card" style="grid-column:1/-1">
|
||
<h2>SSE 实时通知 <span class="state"><span id="sseDot" class="dot"></span><span id="sseText">未连接</span></span></h2>
|
||
<div class="actions" style="margin-top:0;margin-bottom:12px">
|
||
<button onclick="connectSse()">连接 SSE</button>
|
||
<button class="ghost" onclick="disconnectSse()">断开</button>
|
||
<button class="ghost" onclick="clearSseLog()">清空日志</button>
|
||
</div>
|
||
<div id="sseLog" class="logline" style="max-height:280px;overflow:auto">暂无事件。连接后可点击“模拟客服回复”或“模拟完结”观察推送。</div>
|
||
</section>
|
||
</main>
|
||
<script>
|
||
const $ = id => document.getElementById(id);
|
||
const base = '/api/open/v2/customer-work-orders';
|
||
let source = null;
|
||
|
||
function newId(prefix) {
|
||
const random = (crypto.randomUUID ? crypto.randomUUID() : String(Date.now()) + Math.random())
|
||
.replaceAll('-', '');
|
||
return prefix + '-' + random;
|
||
}
|
||
function splitAppIds() {
|
||
return $('appIds').value.split(/[,,;\s]+/).map(x => x.trim()).filter(Boolean);
|
||
}
|
||
function commonBody() {
|
||
return {
|
||
purchaseAccount: $('account').value.trim(),
|
||
appId: $('appId').value.trim()
|
||
};
|
||
}
|
||
async function call(method, path, body) {
|
||
$('output').textContent = '请求中…\n' + method + ' ' + path;
|
||
try {
|
||
const response = await fetch(path, {
|
||
method,
|
||
headers: body === undefined ? undefined : {'Content-Type': 'application/json'},
|
||
body: body === undefined ? undefined : JSON.stringify(body)
|
||
});
|
||
const text = await response.text();
|
||
let parsed;
|
||
try { parsed = text ? JSON.stringify(JSON.parse(text), null, 2) : '(空响应)'; }
|
||
catch { parsed = text; }
|
||
$('output').textContent = 'HTTP ' + response.status + ' ' + response.statusText + '\n' + method + ' ' + path + '\n\n' + parsed;
|
||
return {response, text};
|
||
} catch (error) {
|
||
$('output').textContent = '请求失败:' + error;
|
||
throw error;
|
||
}
|
||
}
|
||
function listOrders() {
|
||
const body = {...commonBody(), appIds: splitAppIds(), limit: 100, cursor: null};
|
||
return call('POST', base + '/list', body);
|
||
}
|
||
function getMessages() {
|
||
return call('POST', base + '/messages', {
|
||
...commonBody(),
|
||
wechatOrderId: $('orderId').value.trim(),
|
||
limit: 100,
|
||
before: null
|
||
});
|
||
}
|
||
async function createTicket() {
|
||
const id = $('clientRequestId').value.trim() || newId('create');
|
||
$('clientRequestId').value = id;
|
||
await call('POST', base, {
|
||
...commonBody(),
|
||
wechatOrderId: $('orderId').value.trim(),
|
||
description: $('description').value.trim(),
|
||
clientRequestId: id
|
||
});
|
||
}
|
||
function sendMessage() {
|
||
const id = $('clientMessageId').value.trim() || newId('send');
|
||
$('clientMessageId').value = id;
|
||
return call('POST', base + '/orders/' + encodeURIComponent($('orderId').value.trim()) + '/messages', {
|
||
...commonBody(),
|
||
clientMessageId: id,
|
||
text: $('messageText').value.trim()
|
||
});
|
||
}
|
||
function markRead() {
|
||
return call('POST', base + '/orders/' + encodeURIComponent($('orderId').value.trim()) + '/read', {
|
||
...commonBody(),
|
||
throughSequenceNo: Number($('sequenceNo').value)
|
||
});
|
||
}
|
||
function supportReply() {
|
||
return call('POST', '/api/test/support-replies', {
|
||
...commonBody(),
|
||
wechatOrderId: $('orderId').value.trim(),
|
||
text: $('messageText').value.trim() || '这是测试客服回复。',
|
||
senderName: '测试客服'
|
||
});
|
||
}
|
||
function completeTicket() {
|
||
return call('POST', '/api/test/orders/complete', {
|
||
...commonBody(),
|
||
wechatOrderId: $('orderId').value.trim()
|
||
});
|
||
}
|
||
function resetData() {
|
||
if (!confirm('确定重置全部内存数据?')) return;
|
||
return call('POST', '/api/test/reset');
|
||
}
|
||
function loadState() { return call('GET', '/api/test/state'); }
|
||
|
||
function appendSse(text, isError) {
|
||
const log = $('sseLog');
|
||
if (log.dataset.initial !== 'false') { log.textContent = ''; log.dataset.initial = 'false'; }
|
||
const line = document.createElement('div');
|
||
line.className = 'logline';
|
||
line.style.color = isError ? 'var(--err)' : '';
|
||
line.textContent = new Date().toLocaleTimeString() + ' ' + text;
|
||
log.appendChild(line);
|
||
log.scrollTop = log.scrollHeight;
|
||
}
|
||
function setSse(state, text) {
|
||
$('sseDot').className = 'dot' + (state === 'on' ? ' on' : state === 'err' ? ' err' : '');
|
||
$('sseText').textContent = text;
|
||
}
|
||
function connectSse() {
|
||
disconnectSse();
|
||
const query = new URLSearchParams();
|
||
query.set('purchaseAccount', $('account').value.trim());
|
||
splitAppIds().forEach(id => query.append('appId', id));
|
||
source = new EventSource(base + '/events?' + query.toString());
|
||
setSse('', '连接中…');
|
||
source.onopen = () => { setSse('on', '已连接'); appendSse('SSE 已连接'); };
|
||
source.onerror = () => { setSse('err', '连接异常/重连中'); appendSse('SSE 连接异常或正在重连', true); };
|
||
source.onmessage = event => appendSse(event.data);
|
||
for (const type of ['relay.ticket.message_added', 'relay.ticket.status_changed', 'resync_required']) {
|
||
source.addEventListener(type, event => appendSse(event.type + '\n' + event.data));
|
||
}
|
||
}
|
||
function disconnectSse() {
|
||
if (source) { source.close(); source = null; setSse('', '已断开'); }
|
||
}
|
||
function clearSseLog() { $('sseLog').textContent = '暂无事件。'; $('sseLog').dataset.initial = 'false'; }
|
||
$('appId').addEventListener('change', () => {
|
||
const id = $('appId').value.trim();
|
||
if (id.endsWith('1558aa')) $('orderId').value = '9000000000000000001';
|
||
if (id.endsWith('64c05c')) $('orderId').value = '9000000000000000002';
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|