集成ptt web对接示例

This commit is contained in:
fengge 2026-04-05 10:27:44 +08:00
parent 5113126d56
commit 4cee2fd195
3 changed files with 340 additions and 0 deletions

3
.gitignore vendored
View File

@ -22,3 +22,6 @@ local.properties
/第三方/安卓demo和说明/ptt.rar
.idea/
.idea/
/第三方/安卓demo和说明/POC_Manual.txt
/第三方/安卓demo和说明/POC_SingleCall.md
/第三方/安卓demo和说明/POC_SOS.md

175
PTT_API_DOC.md Normal file
View File

@ -0,0 +1,175 @@
# PTT 平台外部接口详细文档
**基础路径**: `/api/external/ptt`
**说明**: 该控制器是与第三方 PTT 语音平台对接的核心入口,涵盖了频道管理、成员状态、录音存取及语音识别等全量功能。
---
## 1. 频道与会话管理
### 三、创建频道
* **接口**: `POST /group`
* **描述**: 在平台创建一个新的普通频道,支持邀请账号列表或用户 UID 列表。
* **参数**:
* `gname` (Query, 必填): 群组/频道名称。
* `accounts` (Body, 可选): 参与成员账号列表 (`List<String>`)。
* `uids` (Query, 可选): 参与成员 UID 列表 (`List<Integer>`)。
* **返回**: `Result<PttModels.OpData>`
### 四、删除频道
* **接口**: `DELETE /group/{gid}`
* **描述**: 删除指定的普通频道。
* **参数**:
* `gid` (Path, 必填): 频道 ID。
* **返回**: `Result<Boolean>`
### 五、修改频道
* **接口**: `PUT /group/{gid}`
* **描述**: 修改频道的名称信息。
* **参数**:
* `gid` (Path, 必填): 频道 ID。
* `newName` (Query, 必填): 新的频道名称。
* **返回**: `Result<Boolean>`
### 六、拉成员入频道
* **接口**: `POST /group/{gid}/members`
* **描述**: 将指定的账号列表加入到特定的频道中。
* **参数**:
* `gid` (Path, 必填): 频道 ID。
* `accounts` (Body, 必填): 待加入的成员账号列表 (`List<String>`)。
* **返回**: `Result<Boolean>`
### 七、成员踢出频道
* **接口**: `DELETE /group/{gid}/members`
* **描述**: 将指定的账号列表从频道中移除。
* **参数**:
* `gid` (Path, 必填): 频道 ID。
* `accounts` (Body, 必填): 待移除的成员账号列表 (`List<String>`)。
* **返回**: `Result<Boolean>`
### 二十、创建临时会话
* **接口**: `POST /temp-group`
* **描述**: 发起一个新的临时群组通话。
* **参数**:
* `gname` (Query, 必填): 群组名称。
* `accounts` (Body, 必填): 参与成员账号列表 (`List<String>`)。
* `creatorid` (Query, 必填): 创建者 UID。
* **返回**: `Result<PttModels.OpData>`
### 二十一、删除临时会话
* **接口**: `DELETE /temp-group/{gid}`
* **描述**: 注销/结束一个临时群组。
* **参数**:
* `gid` (Path, 必填): 临时群组 ID。
* **返回**: `Result<Boolean>`
---
## 2. 列表与成员查询
### 八、频道成员列表
* **接口**: `GET /group/{gid}/members`
* **描述**: 查询指定普通频道内的成员列表,支持搜索和分页。
* **参数**:
* `gid` (Path, 必填): 频道 ID。
* `page`, `limit` (Query, 可选): 分页参数。
* `search` (Query, 可选): 搜索关键词(用户名/账号)。
* **返回**: `Result<PttResult<List<PttModels.Member>>>`
### 九、频道列表查询
* **接口**: `GET /groups`
* **描述**: 获取系统内所有的普通频道列表。
* **参数**: `page`, `limit` (Query, 可选)。
* **返回**: `Result<PttResult<List<PttModels.Group>>>`
### 十、临时会话列表查询
* **接口**: `GET /temp-groups`
* **描述**: 获取所有的临时群组列表。
* **参数**: `page`, `limit` (Query, 可选)。
* **返回**: `Result<PttResult<List<PttModels.Group>>>`
### 十一、成员可见频道列表查询
* **接口**: `GET /user/groups`
* **描述**: 查询指定成员有权限查看的频道。
* **参数**: `account`, `uid`, `search` (Query, 可选)。
* **返回**: `Result<List<PttModels.Group>>`
### 二十九、查询某个临时群组下的成员
* **接口**: `GET /temp-group/{tempgid}/members`
* **描述**: 获取指定临时群组内的所有成员详情。
* **参数**: `tempgid` (Path, 必填)。
* **返回**: `Result<List<PttModels.Member>>`
### 三十一、分页查询群组成员
* **接口**: `GET /group/{gid}/members-paged`
* **描述**: 按在线状态分页查询群组成员。
* **参数**:
* `gid` (Path, 必填)。
* `condition` (Query, 可选): 在线状态筛选 (1:在线, 2:离线, 3:在线不在组)。
* `page`, `limit` (Query, 可选)。
* **返回**: `Result<PttResult<List<PttModels.Member>>>`
---
## 3. 录音记录与处理 (核心功能)
### 十二、查询频道内成员ptt录音
* **接口**: `POST /group/{gid}/records`
* **描述**: 获取普通频道内指定成员在特定时间段的 PTT 录音记录。
* **参数**:
* `gid` (Path, 必填)。
* `uids` (Body, 可选): 筛选的成员 UID 列表。
* `start`, `end` (Query, 可选): 时间范围 (yyyy-MM-dd HH:mm:ss)。
* `page`, `limit` (Query, 可选)。
* **返回**: `Result<PttResult<List<PttModels.Record>>>`
### 十三、查询临时会话下的录音
* **接口**: `POST /temp-group/{tempgid}/records`
* **描述**: 获取临时会话下特定时间段的 PTT 录音记录。
* **参数**: 同上,针对临时群组 ID。
* **返回**: `Result<PttResult<List<PttModels.Record>>>`
### 获取录音转文字 (新增集成)
* **接口**: `GET /record/text`
* **描述**: 根据录音地址获取并转为文字。
* **特性**:
1. 内部自动处理音频下载与本地缓存。
2. 已转义过的记录将直接从数据库返回,避免重复调用 ASR。
* **参数**: `path` (Query, 必填): 录音文件远程相对路径。
* **返回**: `Result<String>` (识别后的文本内容)。
### 播放录音 (流式处理)
* **接口**: `GET /record/play`
* **描述**: 在线播放录音文件,首次播放会自动缓存到服务器本地。
* **特性**:
1. **流式输出**: 直接写入 Response 输出流,无内存占用压力。
2. **拖动支持**: 支持 HTTP Range 请求,可在浏览器进度条中任意拖动。
3. **优先播放**: 默认 `inline` 模式,谷歌浏览器直接调用内置播放器。
* **参数**: `path` (Query, 必填): 录音文件远程相对路径。
* **返回**: 二进制音频流 (`audio/wav`)。
---
## 4. 系统工具
### 手动执行平台登录
* **接口**: `POST /login`
* **描述**: 刷新 PTT 平台的会话 Token 并重新登录。用于故障恢复或手动同步。
* **返回**: `Result<String>`
### 二、退出登录
* **接口**: `POST /logout`
* **描述**: 注销当前 PTT 会话。
* **返回**: `Result<Boolean>`
---
## 响应规范
`playRecord` 这种流式接口外,所有接口均遵循统一响应体:
```json
{
"code": 200, // 状态码 (200: 成功)
"msg": "操作成功", // 提示信息
"data": { ... } // 业务载荷
}
```

View File

@ -44,6 +44,12 @@
.log-time { color: #94a3b8; margin-right: 5px; }
.log-msg { word-break: break-all; }
/* PTT Module Styles */
.ptt-card { border-top: 2px solid #10B981 !important; margin-top: 20px; }
.ptt-label { font-size: 11px; color: #64748b; margin-top: 5px; display: block; }
.ptt-btn-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-top: 10px; }
.ptt-btn { margin: 0 !important; padding: 8px !important; font-size: 12px !important; }
/* Custom Modal Styles */
.modal-overlay {
display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
@ -112,6 +118,55 @@
</div>
</div>
<!-- PTT 业务测试模块 -->
<div class="ws-card ptt-card">
<div style="display: flex; align-items: center; justify-content: space-between;">
<strong style="color: #047857;">PTT 业务接口测试</strong>
</div>
<div class="ws-input-group" style="margin-top: 10px;">
<div style="display: flex; gap: 8px;">
<div style="flex: 1;">
<span class="ptt-label">POC 账号:</span>
<input type="text" id="ptt-account" class="ws-input" value="test">
</div>
<div style="flex: 1;">
<span class="ptt-label">频道 ID:</span>
<input type="text" id="ptt-gid" class="ws-input" value="126953">
</div>
</div>
<div style="display: flex; gap: 8px;">
<div style="flex: 1;">
<span class="ptt-label">频道/群组名称:</span>
<input type="text" id="ptt-name" class="ws-input" value="测试频道">
</div>
<div style="flex: 1;">
<span class="ptt-label">录音相对路径:</span>
<input type="text" id="ptt-record-path" class="ws-input" placeholder="path/to/record.wav">
</div>
</div>
</div>
<div class="ptt-btn-grid">
<button class="btn ptt-btn btn-green" onclick="pttCreateGroup()">创建频道</button>
<button class="btn ptt-btn btn-red" onclick="pttDeleteGroup()">删除频道</button>
<button class="btn ptt-btn" style="background: #3b82f6;" onclick="pttAddMembers()">拉人入频道</button>
<button class="btn ptt-btn btn-orange" onclick="pttKickMembers()">踢出频道</button>
<button class="btn ptt-btn" style="background: #6366f1;" onclick="pttGetGroups()">频道列表</button>
<button class="btn ptt-btn" style="background: #8b5cf6;" onclick="pttGetTempGroups()">临时列表</button>
<button class="btn ptt-btn" style="background: #ec4899;" onclick="pttGetRecords()">查询录音</button>
<button class="btn ptt-btn" style="background: #06b6d4;" onclick="pttGetRecordText()">录音转文字</button>
<button class="btn ptt-btn btn-orange" style="background: #f59e0b;" onclick="pttPlayRecord()">播放录音</button>
<button class="btn ptt-btn btn-green" onclick="pttCreateTempGroup()">创建临时频道</button>
<button class="btn ptt-btn btn-red" onclick="pttDeleteTempGroup()">删除临时频道</button>
<button class="btn ptt-btn" style="background: #4f46e5;" onclick="pttGetTempMembers()">查临时成员</button>
</div>
<div id="ptt-result" class="log-container" style="background: #0f172a; margin-top: 10px; height: 120px;">
<div class="log-item">PTT 接口响应将在此展示...</div>
</div>
</div>
<div class="bill-preview" id="printArea">
<div style="text-align: center; font-weight: bold; font-size: 18px;">测试签收单</div>
<hr>
@ -463,6 +518,113 @@
}
setTimeout(loadHistory, 500);
// === PTT 接口测试逻辑 ===
const PTT_BASE = "http://127.0.0.1:8080/api/external/ptt";
function showPttResult(msg) {
const container = document.getElementById('ptt-result');
const time = new Date().toLocaleTimeString();
const item = document.createElement('div');
item.className = 'log-item';
let displayMsg = typeof msg === 'string' ? msg : JSON.stringify(msg, null, 2);
item.innerHTML = `<span class="log-time">[${time}]</span><span class="log-msg">${displayMsg}</span>`;
container.insertBefore(item, container.firstChild);
}
async function pttRequest(path, method = 'GET', query = {}, body = null) {
let url = PTT_BASE + path;
const qParams = new URLSearchParams(query).toString();
if (qParams) url += "?" + qParams;
const options = {
method: method,
headers: { 'Content-Type': 'application/json' }
};
if (body) options.body = JSON.stringify(body);
try {
const response = await fetch(url, options);
if (path === '/record/play') {
showPttResult("流式播放接口已调用,请查看网络请求或直接点击预览链接");
return;
}
const data = await response.json();
showPttResult(data);
return data;
} catch (e) {
showPttResult("请求失败: " + e.message);
console.error(e);
}
}
function pttCreateGroup() {
const name = document.getElementById('ptt-name').value;
const account = document.getElementById('ptt-account').value;
pttRequest('/group', 'POST', { gname: name }, [account]);
}
function pttDeleteGroup() {
const gid = document.getElementById('ptt-gid').value;
pttRequest('/group/' + gid, 'DELETE');
}
function pttAddMembers() {
const gid = document.getElementById('ptt-gid').value;
const account = document.getElementById('ptt-account').value;
pttRequest('/group/' + gid + '/members', 'POST', {}, [account]);
}
function pttKickMembers() {
const gid = document.getElementById('ptt-gid').value;
const account = document.getElementById('ptt-account').value;
pttRequest('/group/' + gid + '/members', 'DELETE', {}, [account]);
}
function pttGetGroups() {
pttRequest('/groups', 'GET', { page: 1, limit: 10 });
}
function pttGetTempGroups() {
pttRequest('/temp-groups', 'GET', { page: 1, limit: 10 });
}
function pttGetRecords() {
const gid = document.getElementById('ptt-gid').value;
const end = new Date().toISOString().replace('T', ' ').substring(0, 19);
const start = new Date(Date.now() - 24 * 3600000).toISOString().replace('T', ' ').substring(0, 19);
pttRequest('/group/' + gid + '/records', 'POST', { start, end, page: 1, limit: 5 }, []);
}
function pttGetRecordText() {
const path = document.getElementById('ptt-record-path').value;
if (!path) return alert("请输入录音路径");
pttRequest('/record/text', 'GET', { path: path });
}
function pttPlayRecord() {
const path = document.getElementById('ptt-record-path').value;
if (!path) return alert("请输入录音路径");
const url = PTT_BASE + '/record/play?path=' + encodeURIComponent(path);
window.open(url, '_blank');
showPttResult("已尝试打开播放链接: " + url);
}
function pttCreateTempGroup() {
const name = document.getElementById('ptt-name').value;
const account = document.getElementById('ptt-account').value;
pttRequest('/temp-group', 'POST', { gname: name, creatorid: 1001 }, [account]);
}
function pttDeleteTempGroup() {
const gid = document.getElementById('ptt-gid').value;
pttRequest('/temp-group/' + gid, 'DELETE');
}
function pttGetTempMembers() {
const gid = document.getElementById('ptt-gid').value;
pttRequest('/temp-group/' + gid + '/members', 'GET');
}
</script>
</body>
</html>