Files
2026-10-18 20:36:40 +08:00

206 lines
10 KiB
HTML
Raw Permalink 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.
<!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>&nbsp;&nbsp;<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>