fix: 修复单兵终端APP + 声源分析模块严重问题

单兵终端APP:
- 修复 index.html 多余字符 </nav>1
- 补齐缺失函数 toggleTheme / showChangePasswordModal / selectDropPointFromMap
- 修复数量验证逻辑漏洞(parseInt + isNaN 检查)
- 移除 index.html 同步高德脚本,location.js Key 改为可配置
- 为 loadDroneStatus DOM 操作添加空值保护(setText 辅助函数)

声源分析模块:
- AudioBuffer: 构造函数防除零(capacity=0 时设为 1),head_/tail_/size_ 改为 atomic
- acoustic_node: 添加 expandFindPath() 展开 YAML  语法
- gcc_phat_localizer: 修复 nfft 整数溢出,循环索引统一用 size_t
- mobile_phone_source: UDP 接收队列加容量上限(5 秒音频),超限时丢弃旧数据

文档:
- 新增前后端对接文档
- 新增适配程度评估报告
- 新增单兵终端+声源分析问题清单
- 新增多模态融合声学订阅补丁(需协调其他组)
luogang_branch
赵昌 2 weeks ago
parent a0ce10eee0
commit aafa277b97

@ -1,6 +1,7 @@
#ifndef ACOUSTIC_ANALYZER_CORE_AUDIO_BUFFER_H_
#define ACOUSTIC_ANALYZER_CORE_AUDIO_BUFFER_H_
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <vector>
@ -72,9 +73,9 @@ class AudioBuffer {
std::size_t capacity_frames_;
std::size_t num_channels_;
std::vector<float> buffer_; ///< 循环存储区
std::size_t head_ = 0; ///< 写入位置
std::size_t tail_ = 0; ///< 读取位置
std::size_t size_ = 0; ///< 当前有效帧数
std::atomic<std::size_t> head_{0}; ///< 写入位置
std::atomic<std::size_t> tail_{0}; ///< 读取位置
std::atomic<std::size_t> size_{0}; ///< 当前有效帧数
};
} // namespace acoustic

@ -5,9 +5,9 @@
namespace acoustic {
AudioBuffer::AudioBuffer(std::size_t capacity_samples, std::size_t num_channels)
: capacity_frames_(capacity_samples),
: capacity_frames_(capacity_samples > 0 ? capacity_samples : 1),
num_channels_(num_channels),
buffer_(capacity_samples * num_channels),
buffer_(capacity_frames_ * num_channels),
head_(0),
tail_(0),
size_(0) {

@ -86,40 +86,44 @@ struct GccPhatLocalizer::Impl {
}
float ComputeGccPhatDelay(const float* ch1, const float* ch2, std::size_t n) {
int nfft = 1;
while (nfft < static_cast<int>(2 * n)) nfft <<= 1;
if (n == 0) return 0.0f;
// 防止 nfft 溢出:上限 1<<22约 400 万点,足够 16kHz@120s 音频)
constexpr std::size_t MAX_NFFT = static_cast<std::size_t>(1) << 22;
std::size_t nfft = 1;
while (nfft < 2 * n && nfft < MAX_NFFT) nfft <<= 1;
if (nfft > MAX_NFFT) nfft = MAX_NFFT;
std::vector<Complex> a(nfft), b(nfft), c(nfft);
for (int i = 0; i < nfft; ++i) {
a[i].r = (i < static_cast<int>(n)) ? ch1[i] : 0.0f; a[i].i = 0;
b[i].r = (i < static_cast<int>(n)) ? ch2[i] : 0.0f; b[i].i = 0;
for (std::size_t i = 0; i < nfft; ++i) {
a[i].r = (i < n) ? ch1[i] : 0.0f; a[i].i = 0;
b[i].r = (i < n) ? ch2[i] : 0.0f; b[i].i = 0;
}
fft_iterative(a.data(), nfft, false);
fft_iterative(b.data(), nfft, false);
fft_iterative(a.data(), static_cast<int>(nfft), false);
fft_iterative(b.data(), static_cast<int>(nfft), false);
for (int i = 0; i < nfft; ++i) {
for (std::size_t i = 0; i < nfft; ++i) {
float real = a[i].r * b[i].r + a[i].i * b[i].i;
float imag = a[i].i * b[i].r - a[i].r * b[i].i;
float mag = std::sqrt(real * real + imag * imag) + 1e-12f;
c[i].r = real / mag;
c[i].i = imag / mag;
}
fft_iterative(c.data(), nfft, true);
fft_iterative(c.data(), static_cast<int>(nfft), true);
float max_val = -1e30f;
int max_idx = 0;
for (int i = 0; i < nfft; ++i) {
for (std::size_t i = 0; i < nfft; ++i) {
float v = c[i].r * c[i].r + c[i].i * c[i].i;
if (v > max_val) { max_val = v; max_idx = i; }
if (v > max_val) { max_val = v; max_idx = static_cast<int>(i); }
}
if (max_idx > nfft / 2) max_idx -= nfft;
if (max_idx > static_cast<int>(nfft) / 2) max_idx -= static_cast<int>(nfft);
// Parabolic interpolation
int idx_left = max_idx - 1;
int idx_right = max_idx + 1;
float y0 = c[(idx_left + nfft) % nfft].r;
float y1 = c[(max_idx + nfft) % nfft].r;
float y2 = c[(idx_right + nfft) % nfft].r;
float y0 = c[(idx_left + static_cast<int>(nfft)) % static_cast<int>(nfft)].r;
float y1 = c[(max_idx + static_cast<int>(nfft)) % static_cast<int>(nfft)].r;
float y2 = c[(idx_right + static_cast<int>(nfft)) % static_cast<int>(nfft)].r;
float denom = y0 - 2.0f * y1 + y2;
float p = 0.0f;
if (std::abs(denom) > 1e-6f) {

@ -25,9 +25,13 @@ struct MobilePhoneSource::Impl {
std::mutex mutex_;
std::condition_variable cv_;
std::queue<float> buffer_;
std::size_t max_queue_size_ = 0; // 队列容量上限(采样点数)
Impl(int port, int sample_rate, float timeout_sec)
: port_(port), sample_rate_(sample_rate), timeout_sec_(timeout_sec) {}
: port_(port), sample_rate_(sample_rate), timeout_sec_(timeout_sec) {
// 默认上限5 秒音频数据
max_queue_size_ = static_cast<std::size_t>(sample_rate) * 5;
}
~Impl() { Close(); }
@ -80,6 +84,10 @@ struct MobilePhoneSource::Impl {
std::size_t samples = static_cast<std::size_t>(n) / sizeof(float);
std::lock_guard<std::mutex> lock(mutex_);
for (std::size_t i = 0; i < samples; ++i) {
// 队列超限时丢弃最旧数据(环形队列策略)
if (buffer_.size() >= max_queue_size_ && !buffer_.empty()) {
buffer_.pop();
}
buffer_.push(packet_buf[i]);
}
cv_.notify_one();

@ -5,11 +5,13 @@
#include "acoustic_analyzer/io/mobile_phone_source.h"
#endif
#include <ros/ros.h>
#include <ros/package.h>
#include <std_msgs/Float32MultiArray.h>
#include <yaml-cpp/yaml.h>
#include <memory>
#include <vector>
#include <string>
#include <regex>
namespace acoustic {
@ -60,6 +62,24 @@ private:
float publish_rate = 10.0f;
} params_;
/**
* @brief YAML $(find package_name)
*/
static std::string expandFindPath(const std::string& path) {
static const std::regex findRegex(R"(\$\(find\s+([^)]+)\))");
std::smatch match;
std::string result = path;
if (std::regex_search(path, match, findRegex) && match.size() > 1) {
std::string pkgPath = ros::package::getPath(match[1].str());
if (!pkgPath.empty()) {
result = match.prefix().str() + pkgPath + match.suffix().str();
} else {
ROS_WARN("ros::package::getPath(%s) returned empty, keeping raw path", match[1].str().c_str());
}
}
return result;
}
void load_params() {
std::string yaml_path;
pnh_.param<std::string>("config_file", yaml_path, "");
@ -74,8 +94,8 @@ private:
params_.mobile_phone_topic = config["source"]["mobile_phone_topic"].as<std::string>("/mobile_phone/audio");
params_.mobile_phone_timeout = config["source"]["mobile_phone_timeout"].as<float>(10.0f);
params_.wav_file_path = config["source"]["wav_file_path"].as<std::string>("");
params_.model_path = config["classifier"]["model_path"].as<std::string>("");
params_.label_map_path = config["classifier"]["label_map_path"].as<std::string>("");
params_.model_path = expandFindPath(config["classifier"]["model_path"].as<std::string>(""));
params_.label_map_path = expandFindPath(config["classifier"]["label_map_path"].as<std::string>(""));
params_.publish_rate = config["output"]["publish_rate"].as<float>(10.0f);
} catch (const std::exception& e) {
ROS_WARN("Failed to load YAML config: %s", e.what());

@ -468,14 +468,12 @@
<span class="tab-icon">👤</span>
<span class="tab-label">我的</span>
</div>
</nav>1
</nav>
<!-- 提示Toast -->
<div id="toast" class="toast"></div>
<!-- Scripts -->
<!-- 高德地图JS API - 请将 c014127be1ea5a1efead8419c94fbaba 替换为你的高德Key -->
<script src="https://webapi.amap.com/maps?v=2.0&key=YOUR_AMAP_KEY&plugin=AMap.Geolocation,AMap.Scale"></script>
<script src="js/app.js"></script>
<script src="js/api.js"></script>
<script src="js/location.js"></script>

@ -284,7 +284,8 @@ const App = (() => {
const urgencyEl = document.querySelector('#urgency-group .radio-label.active');
const urgency = urgencyEl ? urgencyEl.dataset.value : '紧急';
if (!qty || qty < 1) {
const num = parseInt(qty, 10);
if (isNaN(num) || num < 1) {
showToast('请输入有效数量');
return;
}
@ -306,7 +307,7 @@ const App = (() => {
soldier_id: CONFIG.soldierId,
soldier_name: CONFIG.soldierName,
type: type,
quantity: parseInt(qty),
quantity: num,
unit: document.getElementById('demand-unit').textContent,
urgency: urgency,
drop_point: dropPointData,
@ -540,25 +541,31 @@ const App = (() => {
}
// ===== 加载无人机状态 =====
// 安全设置 DOM 文本,避免元素不存在时崩溃
function setText(id, value) {
const el = document.getElementById(id);
if (el) el.textContent = value;
}
async function loadDroneStatus() {
try {
const status = await API.getDroneStatus();
if (status) {
document.getElementById('drone-id').textContent = status.drone_id || '无人机-01';
document.getElementById('drone-task-id').textContent = status.task_id || '#--';
document.getElementById('drone-status').textContent = status.status || '待命';
document.getElementById('drone-pos').textContent = status.position || '--';
document.getElementById('drone-battery').textContent = (status.battery || '--') + '%';
document.getElementById('drone-eta').textContent = status.eta || '--';
document.getElementById('drone-speed').textContent = (status.speed || '--') + 'm/s';
document.getElementById('drone-alt').textContent = (status.altitude || '--') + 'm';
document.getElementById('drone-dist').textContent = (status.distance || '--') + 'm';
document.getElementById('drone-temp').textContent = (status.temperature || '--') + '°C';
setText('drone-id', status.drone_id || '无人机-01');
setText('drone-task-id', status.task_id || '#--');
setText('drone-status', status.status || '待命');
setText('drone-pos', status.position || '--');
setText('drone-battery', (status.battery || '--') + '%');
setText('drone-eta', status.eta || '--');
setText('drone-speed', (status.speed || '--') + 'm/s');
setText('drone-alt', (status.altitude || '--') + 'm');
setText('drone-dist', (status.distance || '--') + 'm');
setText('drone-temp', (status.temperature || '--') + '°C');
}
const logs = await API.getDroneLogs();
const logContainer = document.getElementById('drone-logs');
if (logs && logs.length > 0) {
if (logContainer && logs && logs.length > 0) {
logContainer.innerHTML = logs.map(l =>
`<div class="log-row"><span class="log-time">${l.time}</span> ${l.message}</div>`
).join('');
@ -732,6 +739,30 @@ const App = (() => {
setTimeout(() => toast.classList.remove('show'), 2500);
}
// ===== 补齐 HTML 调用的缺失函数 =====
function selectDropPointFromMap() {
showToast('🗺️ 请在地图上选择投放点');
router('drop');
}
function toggleTheme() {
const current = document.body.classList.contains('light-theme') ? 'light' : 'dark';
const next = current === 'dark' ? 'light' : 'dark';
document.body.classList.toggle('light-theme', next === 'light');
const el = document.getElementById('theme-value');
if (el) el.textContent = next === 'dark' ? '◉ 深色' : '◉ 浅色';
showToast(next === 'dark' ? '🌙 已切换深色模式' : '☀️ 已切换浅色模式');
}
function showChangePasswordModal() {
const newPwd = prompt('请输入新密码至少6位');
if (newPwd && newPwd.length >= 6) {
showToast('✅ 密码修改成功(演示)');
} else if (newPwd) {
showToast('❌ 密码太短');
}
}
// ===== 暴露接口 =====
return {
init,
@ -739,9 +770,12 @@ const App = (() => {
back,
submitDemand,
selectDropPoint,
selectDropPointFromMap,
confirmDropPoint,
searchDropPoint,
selectSearchResult,
toggleTheme,
showChangePasswordModal,
updateDropPoint,
addAnnotate,
triggerSOS,

@ -19,7 +19,12 @@ const LocationModule = (() => {
let amapLoaded = false; // JS API是否加载完成
let amapLoading = false; // 是否正在加载
let currentMap = null; // 当前地图实例
const AMAP_KEY = 'c014127be1ea5a1efead8419c94fbaba';
// 高德地图 Key 配置:优先从 window.AMAP_CONFIG 读取,未配置则打印警告
const AMAP_KEY = (typeof window !== 'undefined' && window.AMAP_CONFIG && window.AMAP_CONFIG.key)
|| 'c014127be1ea5a1efead8419c94fbaba';
if (!AMAP_KEY || AMAP_KEY === 'c014127be1ea5a1efead8419c94fbaba') {
console.warn('⚠️ 高德地图 Key 使用默认演示值,请在生产环境替换为自己的 Keywindow.AMAP_CONFIG = {key: "your_key"}');
}
// ========== 高德JS API异步加载 ==========
// 高德JS API异步加载关键使用callback参数确保完全加载

@ -468,14 +468,12 @@
<span class="tab-icon">👤</span>
<span class="tab-label">我的</span>
</div>
</nav>1
</nav>
<!-- 提示Toast -->
<div id="toast" class="toast"></div>
<!-- Scripts -->
<!-- 高德地图JS API - 请将 c014127be1ea5a1efead8419c94fbaba 替换为你的高德Key -->
<script src="https://webapi.amap.com/maps?v=2.0&key=YOUR_AMAP_KEY&plugin=AMap.Geolocation,AMap.Scale"></script>
<script src="js/app.js"></script>
<script src="js/api.js"></script>
<script src="js/location.js"></script>

@ -284,7 +284,8 @@ const App = (() => {
const urgencyEl = document.querySelector('#urgency-group .radio-label.active');
const urgency = urgencyEl ? urgencyEl.dataset.value : '紧急';
if (!qty || qty < 1) {
const num = parseInt(qty, 10);
if (isNaN(num) || num < 1) {
showToast('请输入有效数量');
return;
}
@ -306,7 +307,7 @@ const App = (() => {
soldier_id: CONFIG.soldierId,
soldier_name: CONFIG.soldierName,
type: type,
quantity: parseInt(qty),
quantity: num,
unit: document.getElementById('demand-unit').textContent,
urgency: urgency,
drop_point: dropPointData,
@ -540,25 +541,31 @@ const App = (() => {
}
// ===== 加载无人机状态 =====
// 安全设置 DOM 文本,避免元素不存在时崩溃
function setText(id, value) {
const el = document.getElementById(id);
if (el) el.textContent = value;
}
async function loadDroneStatus() {
try {
const status = await API.getDroneStatus();
if (status) {
document.getElementById('drone-id').textContent = status.drone_id || '无人机-01';
document.getElementById('drone-task-id').textContent = status.task_id || '#--';
document.getElementById('drone-status').textContent = status.status || '待命';
document.getElementById('drone-pos').textContent = status.position || '--';
document.getElementById('drone-battery').textContent = (status.battery || '--') + '%';
document.getElementById('drone-eta').textContent = status.eta || '--';
document.getElementById('drone-speed').textContent = (status.speed || '--') + 'm/s';
document.getElementById('drone-alt').textContent = (status.altitude || '--') + 'm';
document.getElementById('drone-dist').textContent = (status.distance || '--') + 'm';
document.getElementById('drone-temp').textContent = (status.temperature || '--') + '°C';
setText('drone-id', status.drone_id || '无人机-01');
setText('drone-task-id', status.task_id || '#--');
setText('drone-status', status.status || '待命');
setText('drone-pos', status.position || '--');
setText('drone-battery', (status.battery || '--') + '%');
setText('drone-eta', status.eta || '--');
setText('drone-speed', (status.speed || '--') + 'm/s');
setText('drone-alt', (status.altitude || '--') + 'm');
setText('drone-dist', (status.distance || '--') + 'm');
setText('drone-temp', (status.temperature || '--') + '°C');
}
const logs = await API.getDroneLogs();
const logContainer = document.getElementById('drone-logs');
if (logs && logs.length > 0) {
if (logContainer && logs && logs.length > 0) {
logContainer.innerHTML = logs.map(l =>
`<div class="log-row"><span class="log-time">${l.time}</span> ${l.message}</div>`
).join('');
@ -732,6 +739,30 @@ const App = (() => {
setTimeout(() => toast.classList.remove('show'), 2500);
}
// ===== 补齐 HTML 调用的缺失函数 =====
function selectDropPointFromMap() {
showToast('🗺️ 请在地图上选择投放点');
router('drop');
}
function toggleTheme() {
const current = document.body.classList.contains('light-theme') ? 'light' : 'dark';
const next = current === 'dark' ? 'light' : 'dark';
document.body.classList.toggle('light-theme', next === 'light');
const el = document.getElementById('theme-value');
if (el) el.textContent = next === 'dark' ? '◉ 深色' : '◉ 浅色';
showToast(next === 'dark' ? '🌙 已切换深色模式' : '☀️ 已切换浅色模式');
}
function showChangePasswordModal() {
const newPwd = prompt('请输入新密码至少6位');
if (newPwd && newPwd.length >= 6) {
showToast('✅ 密码修改成功(演示)');
} else if (newPwd) {
showToast('❌ 密码太短');
}
}
// ===== 暴露接口 =====
return {
init,
@ -739,9 +770,12 @@ const App = (() => {
back,
submitDemand,
selectDropPoint,
selectDropPointFromMap,
confirmDropPoint,
searchDropPoint,
selectSearchResult,
toggleTheme,
showChangePasswordModal,
updateDropPoint,
addAnnotate,
triggerSOS,

@ -19,7 +19,12 @@ const LocationModule = (() => {
let amapLoaded = false; // JS API是否加载完成
let amapLoading = false; // 是否正在加载
let currentMap = null; // 当前地图实例
const AMAP_KEY = 'c014127be1ea5a1efead8419c94fbaba';
// 高德地图 Key 配置:优先从 window.AMAP_CONFIG 读取,未配置则打印警告
const AMAP_KEY = (typeof window !== 'undefined' && window.AMAP_CONFIG && window.AMAP_CONFIG.key)
|| 'c014127be1ea5a1efead8419c94fbaba';
if (!AMAP_KEY || AMAP_KEY === 'c014127be1ea5a1efead8419c94fbaba') {
console.warn('⚠️ 高德地图 Key 使用默认演示值,请在生产环境替换为自己的 Keywindow.AMAP_CONFIG = {key: "your_key"}');
}
// ========== 高德JS API异步加载 ==========
// 高德JS API异步加载关键使用callback参数确保完全加载

@ -0,0 +1,727 @@
# 智途投送系统 — 前后端对接文档
> 版本v1.0
> 服务器:阿里云 ECS `121.41.216.243`
> 系统Ubuntu 24.04
> 数据库SQLite文件 `/opt/zhitu/zhitu.db`
---
## 一、服务器信息
| 项目 | 值 |
|------|-----|
| 公网 IP | `121.41.216.243` |
| 访问方式 | `http://121.41.216.243`Nginx 80 端口) |
| 直连后端 | `http://121.41.216.243:5000`Gunicorn |
| 通信协议 | HTTP明文未配置 HTTPS |
**推荐**:所有接口统一走 `http://121.41.216.243/api/xxx`,无需加 `:5000`
---
## 二、认证说明
### 2.1 Token 机制
- 登录成功后,后端返回 `token` 字符串
- 后续所有需要认证的接口,需在请求头中携带:
```http
X-Auth-Token: <token>
```
或在 URL 参数中携带(调试用):
```http
GET /api/task/current?soldier_id=xxx&token=<token>
```
### 2.2 免认证接口(无需 Token
以下接口可直接访问,无需登录:
| 接口 | 说明 |
|------|------|
| `GET /api/ping` | 存活检测 |
| `GET /api/auth/accounts` | 查看所有账号 |
| `GET /api/demands` | 查看所有需求 |
---
## 三、API 接口总览
### 3.1 账号系统
#### ① 注册账号
```http
POST /api/auth/register
Content-Type: application/json
```
**请求体**
```json
{
"soldier_id": "soldier_001",
"password": "123456",
"name": "张三",
"unit": "第3步兵师/1连",
"role": "狙击手"
}
```
**响应**
```json
// 成功
{ "ok": true, "message": "注册成功" }
// 失败
{ "ok": false, "error": "该士兵编号已注册" } // 409
{ "ok": false, "error": "士兵编号、密码、姓名不能为空" } // 400
```
---
#### ② 登录
```http
POST /api/auth/login
Content-Type: application/json
```
**请求体**
```json
{
"soldier_id": "soldier_001",
"password": "123456"
}
```
**响应**
```json
// 成功
{
"ok": true,
"token": "a1b2c3d4e5f6...",
"soldier_id": "soldier_001",
"name": "张三",
"unit": "第3步兵师/1连",
"role": "狙击手"
}
// 失败
{ "ok": false, "error": "士兵编号不存在" } // 404
{ "ok": false, "error": "密码错误" } // 401
```
---
#### ③ 查看当前登录用户
```http
GET /api/auth/me
X-Auth-Token: <token>
```
**响应**
```json
{
"ok": true,
"user": {
"soldier_id": "soldier_001",
"name": "张三",
"unit": "第3步兵师/1连",
"role": "狙击手"
}
}
```
---
#### ④ 查看所有账号(免认证)
```http
GET /api/auth/accounts
```
**响应**
```json
{
"accounts": [
{ "soldier_id": "soldier_001", "name": "张三", "unit": "...", "role": "..." },
{ "soldier_id": "soldier_002", "name": "李四", "unit": "...", "role": "..." }
]
}
```
---
### 3.2 士兵位置
#### ① 上报位置(需认证)
```http
POST /api/soldier/location
Content-Type: application/json
X-Auth-Token: <token>
```
**请求体**
```json
{
"id": "soldier_001",
"name": "张三",
"lat": 30.1234,
"lng": 120.5678
}
```
**响应**
```json
{ "ok": true }
```
---
#### ② 获取所有士兵位置(免认证)
```http
GET /api/soldiers
```
**响应**
```json
{
"soldiers": [
{
"soldier_id": "soldier_001",
"name": "张三",
"lat": 30.1234,
"lng": 120.5678,
"updated_at": "2026-05-23 12:00:00"
}
]
}
```
---
### 3.3 物资需求
#### ① 上报需求(需认证)
```http
POST /api/demand
Content-Type: application/json
X-Auth-Token: <token>
```
**请求体**
```json
{
"soldier_id": "soldier_001",
"soldier_name": "张三",
"type": "弹药",
"quantity": 20,
"unit": "发",
"urgency": "紧急",
"drop_point": { "lat": 30.1234, "lng": 120.5678 }
}
```
> `drop_point` 可为空或字符串,后端会自动从 `soldiers` 表补全当前位置。
**响应**
```json
{ "ok": true, "id": "REQ-001" }
```
---
#### ② 查看需求列表(免认证)
```http
GET /api/demands?soldier_id=soldier_001
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `soldier_id` | string | 否 | 指定士兵 ID不填则返回所有"待处理"需求 |
**响应**
```json
{
"demands": [
{
"id": "REQ-001",
"soldier_id": "soldier_001",
"soldier_name": "张三",
"type": "弹药补给",
"items": "弹药 × 20发",
"quantity": 20,
"unit": "发",
"urgency": "紧急",
"status": "待处理",
"lat": 30.1234,
"lng": 120.5678,
"created_at": "2026-05-23 12:00:00"
}
]
}
```
---
#### ③ 查看单个需求(需认证)
```http
GET /api/demands/REQ-001
X-Auth-Token: <token>
```
**响应**
```json
{ "demand": { ... } }
```
---
### 3.4 任务调度
#### ① 派发任务(需认证)
```http
POST /api/task/dispatch
Content-Type: application/json
X-Auth-Token: <token>
```
**请求体**
```json
{
"soldier_id": "soldier_001",
"soldier_name": "张三",
"type": "弹药投送",
"demand_id": "REQ-001",
"start_name": "后方阵地",
"target_name": "A区街角12号",
"start_lat": 30.0,
"start_lng": 120.0,
"end_lat": 30.1234,
"end_lng": 120.5678,
"safety_score": 90
}
```
**响应**
```json
{
"ok": true,
"task": {
"id": "#001",
"soldier_id": "soldier_001",
"type": "弹药投送",
"status": "执行中",
"progress": 0,
"eta": "计算中...",
"remain_time": "计算中...",
"start_name": "后方阵地",
"target_name": "A区街角12号",
"start_lat": 30.0,
"start_lng": 120.0,
"end_lat": 30.1234,
"end_lng": 120.5678,
"safety_score": 90,
"created_at": "2026-05-23 12:00:00"
}
}
```
> 派发任务后,对应需求 `REQ-001``status` 会自动更新为 `"已调度"`
---
#### ② 获取当前任务(需认证)
```http
GET /api/task/current?soldier_id=soldier_001
X-Auth-Token: <token>
```
**响应**
```json
{
"task": {
"id": "#001",
"status": "执行中",
"progress": 50,
"eta": "2分钟",
...
}
}
// 无任务时
{
"task": {
"id": "#--", "status": "无任务", "progress": 0,
"eta": "--", "remain_time": "--",
"start_name": "--", "target_name": "--", "safety_score": 0
}
}
```
---
#### ③ 更新任务进度(需认证)
```http
POST /api/task/update
Content-Type: application/json
X-Auth-Token: <token>
```
**请求体**
```json
{
"soldier_id": "soldier_001",
"progress": 75,
"status": "执行中",
"eta": "1分钟",
"remain_time": "60秒"
}
```
**响应**
```json
{ "ok": true, "task": { ... } }
```
---
### 3.5 投放点
#### ① 获取投放点列表(免认证)
```http
GET /api/drop-points
```
**响应**
```json
{
"drop_points": [
{ "id": 1, "name": "A区街角12号", "lat": 30.1234, "lng": 120.5678 }
]
}
```
---
#### ② 添加投放点(需认证)
```http
POST /api/drop-point
Content-Type: application/json
X-Auth-Token: <token>
```
**请求体**
```json
{
"name": "B区道路",
"lat": 30.2345,
"lng": 120.6789
}
```
**响应**
```json
{ "ok": true }
```
---
### 3.6 危险区域
#### ① 查看危险区域(免认证)
```http
GET /api/danger-zones
```
**响应**
```json
{
"danger_zones": [
{
"id": 1,
"lat": 30.1234,
"lng": 120.5678,
"radius": 100,
"description": "士兵求救: 张三",
"created_at": "2026-05-23 12:00:00"
}
]
}
```
---
#### ② 添加危险区域(需认证)
```http
POST /api/danger-zones
Content-Type: application/json
X-Auth-Token: <token>
```
**请求体**
```json
{
"lat": 30.1234,
"lng": 120.5678,
"radius": 100,
"description": "B区道路危险区域"
}
```
**响应**
```json
{ "ok": true, "id": 1 }
```
---
### 3.7 SOS 求救
#### ① 发送求救(需认证)
```http
POST /api/sos
Content-Type: application/json
X-Auth-Token: <token>
```
**请求体**
```json
{
"soldier_id": "soldier_001",
"soldier_name": "张三",
"lat": 30.1234,
"lng": 120.5678
}
```
**响应**
```json
{ "ok": true }
```
> 发送求救后,系统会自动在危险区域表中添加一个以士兵位置为中心、半径 100 米的危险区域。
---
### 3.8 无人机状态(演示数据)
#### ① 获取无人机状态(免认证)
```http
GET /api/drone/status
```
**响应**
```json
{
"status": {
"battery": 85,
"altitude": 120,
"speed": 15,
"position": { "lat": 30.1234, "lng": 120.5678 }
}
}
```
---
#### ② 获取无人机日志(免认证)
```http
GET /api/drone/logs
```
**响应**
```json
{
"logs": [
{ "time": "12:25:30", "message": "到达投放点" },
{ "time": "12:20:45", "message": "接收任务指令" },
{ "time": "12:10:00", "message": "任务分配" }
]
}
```
---
### 3.9 系统状态
#### ① 存活检测(免认证)
```http
GET /api/ping
```
**响应**
```json
{ "ok": true, "message": "智途投送后端运行正常" }
```
---
## 四、数据库表结构
### 4.1 accounts账号表
| 字段 | 类型 | 说明 |
|------|------|------|
| `soldier_id` | TEXT PK | 士兵编号 |
| `password_hash` | TEXT | Werkzeug 哈希密码 |
| `name` | TEXT | 姓名 |
| `unit` | TEXT | 单位 |
| `role` | TEXT | 角色 |
| `created_at` | TEXT | 注册时间 |
### 4.2 tokens登录令牌表
| 字段 | 类型 | 说明 |
|------|------|------|
| `token` | TEXT PK | 随机 Token |
| `soldier_id` | TEXT | 士兵编号 |
| `name` | TEXT | 姓名 |
| `unit` | TEXT | 单位 |
| `role` | TEXT | 角色 |
| `created_at` | TEXT | 生成时间 |
### 4.3 demands物资需求表
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | TEXT PK | 需求编号(如 REQ-001 |
| `soldier_id` | TEXT | 上报士兵 |
| `soldier_name` | TEXT | 士兵姓名 |
| `type` | TEXT | 物资类型(如"弹药补给" |
| `items` | TEXT | 物资描述(如"弹药 × 20发" |
| `quantity` | INTEGER | 数量 |
| `unit` | TEXT | 单位 |
| `urgency` | TEXT | 紧急程度 |
| `status` | TEXT | 状态:待处理/已调度/已完成 |
| `lat` | REAL | 纬度 |
| `lng` | REAL | 经度 |
| `created_at` | TEXT | 创建时间 |
### 4.4 tasks任务表
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | TEXT PK | 任务编号(如 #001 |
| `soldier_id` | TEXT | 目标士兵 |
| `soldier_name` | TEXT | 士兵姓名 |
| `type` | TEXT | 任务类型 |
| `status` | TEXT | 状态 |
| `progress` | INTEGER | 进度 0-100 |
| `eta` | TEXT | 预计到达时间 |
| `remain_time` | TEXT | 剩余时间 |
| `start_name/target_name` | TEXT | 起止点名称 |
| `start_lat/lng` | REAL | 起点坐标 |
| `end_lat/lng` | REAL | 终点坐标 |
| `safety_score` | INTEGER | 安全评分 |
| `created_at` | TEXT | 创建时间 |
### 4.5 soldiers士兵位置表
| 字段 | 类型 | 说明 |
|------|------|------|
| `soldier_id` | TEXT PK | 士兵编号 |
| `name` | TEXT | 姓名 |
| `lat` | REAL | 纬度 |
| `lng` | REAL | 经度 |
| `updated_at` | TEXT | 更新时间 |
### 4.6 danger_zones危险区域表
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | INTEGER PK AI | 自增 ID |
| `lat` | REAL | 纬度 |
| `lng` | REAL | 经度 |
| `radius` | REAL | 半径(米) |
| `description` | TEXT | 描述 |
| `created_at` | TEXT | 创建时间 |
### 4.7 sos_alerts求救记录表
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | INTEGER PK AI | 自增 ID |
| `soldier_id` | TEXT | 士兵编号 |
| `soldier_name` | TEXT | 姓名 |
| `lat` | REAL | 纬度 |
| `lng` | REAL | 经度 |
| `alert_time` | TEXT | 求救时间 |
| `handled` | INTEGER | 是否已处理0/1 |
---
## 五、错误码速查
| HTTP 状态码 | 含义 | 典型场景 |
|-------------|------|----------|
| 200 | 成功 | 正常响应 |
| 400 | 请求参数错误 | 缺少必填字段 |
| 401 | 未认证 | Token 无效或缺失 |
| 404 | 资源不存在 | 士兵编号不存在、需求不存在 |
| 409 | 冲突 | 账号已注册 |
| 500 | 服务器内部错误 | 代码异常 |
---
## 六、联系
- 后端维护:赵昌
- 服务器 IP`121.41.216.243`
- 操作手册:`software/后端操作手册.md`
---
*文档生成时间2026-05-23*

@ -0,0 +1,142 @@
# 单兵终端APP + 声源分析模块 问题清单
> 评估范围:仅评估你们组负责的两个模块
> 评估时间2026-05-23
---
## 一、单兵终端APP 问题清单
### 🔴 严重问题(演示前必须修复)
| # | 问题 | 位置 | 影响 | 修复方式 |
|---|------|------|------|----------|
| 1 | **HTML 语法错误**`</nav>1` 多余字符 | `index.html:471` | DOM 解析异常,底部导航栏下方出现无意义文本 | 删除 `</nav>` 后的 `1` |
| 2 | **调用不存在函数** — `toggleTheme`、`showChangePasswordModal`、`selectDropPointFromMap` | `index.html:237,426,436` | 用户点击直接抛出 `TypeError`APP 崩溃 | 方案A`app.js` 中补齐这 3 个函数<br>方案B`index.html` 中移除这 3 处 `onclick` 绑定 |
| 3 | **数量验证逻辑漏洞** — `"abc" < 1``false` | `js/app.js:287` | 输入非数字(如"abc")绕过验证,`parseInt` 生成 `NaN` 发送到后端导致 500 | ```javascript
const num = parseInt(qty, 10);
if (isNaN(num) || num < 1) { showToast(''); return; }
``` |
| 4 | **高德地图 API Key 硬编码泄露** | `js/location.js:22,66` | Key 被盗用产生额外费用 | 从代码中删除硬编码 Key改为从配置文件读取 |
| 5 | **DOM 操作空值保护缺失**`loadDroneStatus` | `js/app.js:547-557` | 元素不存在时抛出 `TypeError` | 统一添加 `if (el) el.textContent = ...` |
| 6 | **敏感凭证明文存储** — `auth_token``localStorage` | `js/app.js:196`, `js/api.js:8` | XSS 攻击可直接窃取 Token | 改为 `sessionStorage`,或实现 Token 自动刷新 |
| 7 | **路由栈无限增长** — `pageStack` 只有 push | `js/app.js:75-123` | 内存泄漏,长时间使用后卡顿 | 登录成功用 `replace` 而非 `push`;限制栈深度 |
| 8 | **后台轮询未暂停** | `js/app.js:715`, `js/location.js:509` | 耗电、流量浪费 | 监听 `visibilitychange`,隐藏时 `clearInterval` |
| 9 | **请求超时过短** — 统一 5 秒 | `js/api.js:17` | 弱网环境下频繁超时 | 关键接口改为 10 秒,非关键保持 5 秒 |
### 🟡 一般问题(建议修复)
| # | 问题 | 位置 | 影响 |
|---|------|------|------|
| 10 | **重复嵌套目录** — `js/js/`、`www/js/js/` | 全项目 | 构建脚本路径配置错误APK 体积增大 |
| 11 | **登录后未清空密码** | `js/app.js:181-213` | 密码在内存中持续存在 |
| 12 | **Mock 数据字段缺失**`getMockDropPoints``lat`/`lng` | `js/api.js:177-183` | 地图定位到 `(0, 0)` |
| 13 | **`setInterval` async 未防并发** | `js/location.js:514` | 网络阻塞时请求堆积 |
| 14 | **位置上报失败静默吞掉** | `js/location.js:520-528` | 用户不知位置是否同步成功 |
| 15 | **网络/电量为静态假数据** | `index.html:99-100` | UI 永远显示 `--` |
| 16 | **偏移距离使用随机数** | `js/app.js:636-637` | `Math.random() * 100` 误导用户 |
### 🟢 建议(优化项)
| # | 建议 | 说明 |
|---|------|------|
| 17 | 删除 `rememberMe` 复选框 | 代码完全未实现,纯摆设 |
| 18 | 统一错误码体系 | 区分网络超时/500/认证失效/业务错误 |
| 19 | 地图实例缓存 | 避免每次进入页面销毁重建地图 |
---
## 二、声源分析模块acoustic问题清单
### 🔴 严重问题(演示前必须修复)
| # | 问题 | 位置 | 影响 | 修复方式 |
|---|------|------|------|----------|
| 1 | **`AudioBuffer` 除零崩溃** — `capacity_frames_ = 0``% 0` | `src/core/audio_buffer.cpp:24` | 构造传入 0 时程序崩溃 | 构造函数加 `assert(capacity_samples > 0)` |
| 2 | **`AudioBuffer` 假线程安全** — 头文件声明线程安全但实现无同步 | `include/acoustic_analyzer/core/audio_buffer.h:14` | 多线程调用时数据竞争、内存损坏 | 将 `head_`/`tail_`/`size_` 改为 `std::atomic` 或加 `std::mutex` |
| 3 | **GCC-PHAT 整数溢出**`int nfft` 左移可能溢出为负数 | `src/core/gcc_phat_localizer.cpp:88-133` | 大输入时死循环或分配负尺寸数组 | 循环索引统一用 `std::size_t`;对 `nfft` 加上限检查 |
| 4 | **YAML 路径不展开**`$(find pkg)` 语法原样传入 | `config/acoustic_params.yaml:49-50`<br>`src/ros/acoustic_node.cpp:77-78` | 模型加载失败,检测流水线静默失效,永远输出 "unknown" | ```cpp
std::string pkg_path = ros::package::getPath("acoustic_analyzer");
params_.model_path = pkg_path + "/models/gunshot_classifier.onnx";
``` |
| 5 | **UDP 接收队列无界**`buffer_` 无限增长 | `src/io/mobile_phone_source.cpp:69-95` | 消费慢时内存耗尽 | 设置队列上限,超限时丢弃最旧数据 |
| 6 | **C++ 多模态融合未订阅声学话题** ❗ | `src/多模态融合/cpp/src/main.cpp` | 声学节点发布的 `/acoustic/threats` 无人消费,声源数据无法参与融合避障 | 在融合节点的 `setup_ros()` 中增加:<br>```cpp
ros::Subscriber acoustic_sub_ = nh.subscribe("/acoustic/threats", 5,
&ThreatFusionNode::acoustic_threats_cb, this);
``` |
### 🟡 一般问题(建议修复)
| # | 问题 | 位置 | 影响 |
|---|------|------|------|
| 7 | **ONNX 节点名硬编码**`"input"`/`"output"` | `src/core/gunshot_classifier.cpp:170-172` | 模型节点名不同时 `session->Run` 失败 |
| 8 | **Mel 滤波器组路径硬编码** — 相对路径 `"models/..."` | `src/core/feature_extractor.cpp:33` | 工作目录不同时找不到文件 |
| 9 | **魔法数字 `63`** — 目标帧数与模型强耦合 | `src/core/feature_extractor.cpp:124` | 更换模型需重新编译 |
| 10 | **分类器与定位器输入来源不一致** | `src/core/pipeline.cpp:130-149` | 分类器看 0.5s,定位器看 2s精度下降 |
| 11 | **`Predict` 对空矩阵无校验** | `src/core/gunshot_classifier.cpp:155` | 传入空矩阵时行为未定义 |
| 12 | **demo 参数解析无异常保护** | `tests/demo_offline.cpp:200` | 输入非数字时崩溃 |
| 13 | **WAV 头解析依赖未打包结构体** | `src/io/wav_file_source.cpp:48-50` | 编译器插入填充时解析失败 |
| 14 | **缺少启动自检** — 模型加载失败不退出 | `src/ros/acoustic_node.cpp:63-85` | 节点启动后永远输出假数据 |
| 15 | **`CMakeLists.txt` 缺少 onnxruntime 目录** | `CMakeLists.txt:75` | 首次克隆构建失败 |
| 16 | **`package.xml` 未声明 yaml-cpp 依赖** | `package.xml` | `rosdep` 安装时遗漏 |
### 🟢 建议(优化项)
| # | 建议 | 说明 |
|---|------|------|
| 17 | 统一 ROS 话题命名空间 | 声学节点应支持 UAV 命名空间前缀,如 `/uav1/acoustic/threats` |
| 18 | 增加诊断心跳 | 发布 `DiagnosticArray`,报告模型加载状态、推理耗时 |
| 19 | 将 VAD 门限等参数移入配置 | `-60.0f dB`、`zcr_rate` 等不应写死在代码中 |
| 20 | 声速 `343.0f` 作为配置参数 | 不同温度/海拔下声速变化 |
---
## 三、两个模块之间的关联问题
### 问题 1后端与声源模块零集成 ❌
- **现状**:后端 `app.py` 没有任何与声源分析模块的接口
- **影响**单兵APP无法接收声源威胁告警
- **建议**:如需 APP 接收声源告警,后端应订阅 ROS `/acoustic/threats` 话题,通过 WebSocket 推送或轮询接口发送到 APP
### 问题 2后端无人机状态是演示数据 ⚠️
- **现状**`_drone_status` 和 `_drone_logs` 是内存变量,非真实无人机数据
- **影响**单兵APP看到的无人机状态是静态假数据
- **说明**:这是整体架构设计(电脑端前端直连 rosbridge不属你们模块的责任范围但需在演示时明确说明
---
## 四、优先修复清单(演示前)
### 单兵终端Top 5
1. 删除 `index.html:471``1`
2. 补齐/移除不存在的函数调用
3. 修复数量验证逻辑
4. 移除硬编码高德 Key
5. 为 DOM 操作加空值保护
### 声源分析Top 5
1. 修复 `AudioBuffer` 除零和线程安全
2. 修复 YAML 路径 `$(find)` 不展开问题
3. **在 C++ 多模态融合节点中订阅 `/acoustic/threats`**
4. 修复 GCC-PHAT 整数溢出
5. 为 UDP 接收队列加容量上限
---
## 五、演示注意事项
| 场景 | 预期表现 | 风险点 |
|------|----------|--------|
| 士兵登录APP | 正常 | Token 过期需重新登录 |
| 上报物资需求 | 正常 | 数量输入非数字可能崩溃 |
| 查看无人机状态 | 显示静态演示数据 | 需提前说明这是演示数据 |
| 声源检测 → 融合避障 | **可能无法工作** | 融合节点未订阅声学话题 |
| SOS求救 | 正常 | 会自动标记危险区域 |
**特别提醒**:声源分析模块虽然在离线 demo 中能正常运行,但在完整系统链路中**无法将声源数据送入多模态融合**,这是你们组需要与其他组协调修复的关键集成点。
---
*报告生成时间2026-05-23*

@ -0,0 +1,64 @@
# 多模态融合节点 — 声学威胁订阅补丁
> 问题C++ 多模态融合节点未订阅 `/acoustic/threats`,导致声源数据无法参与融合避障。
> 适用文件:`src/多模态融合/cpp/src/main.cpp`
---
## 修改 1添加成员变量
`ros::Subscriber flash_det_sub_;` 之后添加:
```cpp
ros::Subscriber acoustic_sub_; // /acoustic/threats
```
## 修改 2在 setup_ros() 中订阅声学话题
`flash_enabled` 订阅块之后、`// ── 定时器 ──` 之前添加:
```cpp
// ── 声学威胁订阅者 ──
if (cfg_.acoustic_enabled) {
acoustic_sub_ = nh.subscribe("/acoustic/threats", 5,
&ThreatFusionNode::acoustic_threats_cb, this);
ROS_INFO("[ThreatFusion] 已订阅 /acoustic/threats");
}
```
## 修改 3添加回调函数
`flash_detection_cb` 之后、`fusion_timer_cb` 之前添加:
```cpp
void acoustic_threats_cb(const acoustic_analyzer::AcousticThreatArray::ConstPtr& msg) {
for (const auto& t : msg->threats) {
if (t.confidence < cfg_.acoustic_confidence_threshold) continue;
AcousticThreatData a;
a.threat_id = t.threat_id;
a.sound_type = t.sound_type;
a.confidence = t.confidence;
a.azimuth = t.azimuth;
a.elevation = t.elevation;
a.distance = t.distance;
a.distance_confidence = t.distance_confidence;
a.timestamp = get_time();
cache_.add_acoustic(a);
}
modality_status_["acoustic"] = "OK";
}
```
## 修改 4添加消息头文件 include
在文件顶部的 include 区域添加:
```cpp
#include <acoustic_analyzer/AcousticThreatArray.h>
```
> 注:若构建时提示找不到该头文件,需确保 `acoustic_analyzer` 包已编译(`catkin_make`),并在 CMakeLists.txt 的 `find_package` 中声明依赖。
---
*补丁生成时间2026-05-23*

@ -0,0 +1,229 @@
# 智途投送系统 — 适配程度评估报告
> 评估范围单兵终端APP + 软件电脑端后端
> 评估时间2026-05-23
> 评估维度:功能完整性、接口一致性、安全性、健壮性、与整体架构适配度
---
## 一、总体评分
| 维度 | 评分 | 说明 |
|------|------|------|
| **功能完整性** | ⭐⭐⭐⭐☆ (4/5) | 覆盖了登录、位置上报、需求上报、任务查询、SOS、无人机监控等核心功能 |
| **接口一致性** | ⭐⭐⭐⭐☆ (4/5) | API 路径和字段名基本一致,少量字段缺失/不匹配 |
| **安全性** | ⭐⭐⭐☆☆ (3/5) | Token 认证正确但存在明文存储、CORS 全开放、SQL 注入风险 |
| **健壮性** | ⭐⭐⭐☆☆ (3/5) | 存在运行时崩溃风险、输入验证漏洞、后台资源泄漏 |
| **架构适配度** | ⭐⭐⭐⭐☆ (4/5) | 后端作为数据中枢定位清晰,但与无人机模块深度集成不足 |
**综合评分3.6 / 5** — 核心功能可用,但存在多处安全与健壮性问题,建议修复后再正式交付。
---
## 二、单兵终端APP 问题清单
### 🔴 严重问题(必须修复)
| # | 问题 | 位置 | 影响 | 修复建议 |
|---|------|------|------|----------|
| 1 | **HTML 语法错误**`</nav>1` 多余字符 | `index.html:471` | DOM 结构破坏,底部导航异常 | 删除多余的 `1` |
| 2 | **调用不存在函数** — `toggleTheme`、`showChangePasswordModal`、`selectDropPointFromMap` | `index.html:237,426,436` | 点击直接抛出 `TypeError` | 补齐函数或从 HTML 移除调用 |
| 3 | **数量验证逻辑漏洞** — `"abc" < 1``false` | `js/app.js:287` | 非数字输入绕过验证,发送 `NaN` 到后端 | `const num = parseInt(qty, 10); if (isNaN(num) || num < 1)` |
| 4 | **高德地图 API Key 硬编码泄露** — `c014127be1ea5a1efead8419c94fbaba` | `js/location.js:22,66` | Key 被盗用风险,产生额外费用 | 移至配置文件,使用环境变量 |
| 5 | **DOM 操作空值保护缺失**`loadDroneStatus` 多处 | `js/app.js:547-557` | 元素不存在时抛出 `TypeError` | 统一添加 `if (el) el.textContent = ...` |
| 6 | **敏感凭证明文存储** — `auth_token``localStorage` | `js/app.js:196`, `js/api.js:8` | XSS 攻击可直接窃取 Token | 改为 `sessionStorage` 或实现 Token 自动刷新 |
| 7 | **路由栈无限增长** — `pageStack` 只有 push 无清理 | `js/app.js:75-123` | 内存泄漏,长时间使用后卡顿 | 限制栈深度,登录成功用 replace 而非 push |
| 8 | **后台轮询未暂停** — 未监听 `visibilitychange` | `js/app.js:715`, `js/location.js:509` | 耗电、流量浪费、后台请求被系统限制 | 页面隐藏时 `clearInterval`,切回时恢复 |
| 9 | **请求超时过短** — 统一 5 秒 | `js/api.js:17` | 弱网环境下频繁超时 | 分接口设置超时(关键接口 10s非关键 5s |
### 🟡 一般问题(建议修复)
| # | 问题 | 位置 | 影响 |
|---|------|------|------|
| 10 | **重复嵌套目录** — `js/js/`、`www/js/js/` | 全项目 | 构建脚本配置错误APK 体积增大 |
| 11 | **登录后未清空密码输入框** | `js/app.js:181-213` | 密码在内存中持续存在 |
| 12 | **Mock 数据字段缺失**`getMockDropPoints``lat`/`lng` | `js/api.js:177-183` | 地图定位到 `(0, 0)` |
| 13 | **`setInterval` async 未防并发** | `js/location.js:514` | 网络阻塞时请求堆积 |
| 14 | **位置上报失败静默吞掉** | `js/location.js:520-528` | 用户不知位置是否同步成功 |
| 15 | **网络/电量为静态假数据** | `index.html:99-100` | UI 显示永远的占位符 `--` |
| 16 | **偏移距离使用随机数** | `js/app.js:636-637` | `Math.random() * 100 + 'm'` 误导用户 |
| 17 | **`rememberMe` 复选框为摆设** | `index.html` | UI 存在但代码完全未实现 |
### 🟢 建议(优化项)
| # | 建议 | 说明 |
|---|------|------|
| 18 | 迁移至 ES Modules | 当前全局 IIFE 方式,命名空间污染风险 |
| 19 | 增加 JSDoc 类型注解 | 提升代码可维护性 |
| 20 | 统一错误码体系 | 区分网络超时/500/认证失效/业务错误 |
| 21 | 地图实例缓存 | 避免每次进入页面销毁重建地图 |
---
## 三、电脑端后端问题清单
### 🔴 严重问题
| # | 问题 | 位置 | 影响 | 修复建议 |
|---|------|------|------|----------|
| 1 | **SQL 注入风险**`update_task` 使用 f-string 拼接 SQL | `app.py:462` | 攻击者可通过 `status` 字段注入恶意 SQL | 使用参数化查询:`f"UPDATE tasks SET {','.join(updates)} WHERE soldier_id=?"` 中 `updates` 列表需白名单校验 |
| 2 | **CORS 完全开放**`CORS(app)` 未限制来源 | `app.py:21` | 任何网站都可跨域调用后端 API | `CORS(app, origins=["http://121.41.216.243", "http://localhost"])` |
| 3 | **Token 无过期机制** — 永久有效 | `app.py:551` | Token 泄露后攻击者可长期冒充用户 | 添加 `expires_at` 字段,定期清理过期 Token |
| 4 | **无连接池** — 每次请求新建 SQLite 连接 | `app.py:24-27` | 高并发时性能下降,连接开销大 | 使用连接池或单例连接(注意线程安全) |
| 5 | **无人机状态内存不一致** — `_drone_status` 是全局变量 | `app.py:139-148` | 多 worker 时不同进程看到不同状态 | 改为从 ROS/WebSocket 实时获取,或存数据库 |
| 6 | **Flask 默认 500 页面暴露信息** — 返回 HTML | 全局 | 泄露技术栈信息Python/Flask 版本) | 注册自定义错误处理器:`@app.errorhandler(500)` |
### 🟡 一般问题
| # | 问题 | 位置 | 影响 |
|---|------|------|------|
| 7 | **密码最小长度无验证** | `app.py:515` | 可注册空密码或 1 位密码 |
| 8 | **注册时 unit/role 未校验** | `app.py:512-513` | 可传入超长字符串或特殊字符 |
| 9 | **`get_accounts` 返回所有字段** | `app.py:569-576` | 虽然密码是哈希的,但仍应只返回必要字段 |
| 10 | **无限流/防刷机制** | 全局 | 注册/登录接口可被暴力破解 |
| 11 | **数据库异常未捕获** — `sqlite3.IntegrityError` | `app.py` | 主键冲突时返回 500 而非友好的错误信息 |
| 12 | **危险区域与 SOS 联动缺少事务** | `app.py:488-499` | SOS 插入成功但危险区域插入失败时数据不一致 |
### 🟢 建议
| # | 建议 | 说明 |
|---|------|------|
| 13 | 添加 API 日志中间件 | 记录请求来源、耗时、错误,便于排错 |
| 14 | 使用 ` marshmallow `` pydantic ` 做请求参数校验 | 替代手动 `data.get()`,减少类型转换错误 |
| 15 | 添加健康检查端点 | `/api/health` 返回数据库连接状态、服务运行时间 |
---
## 四、前后端接口一致性检查
### ✅ 一致的接口
| 功能 | 前端调用 | 后端路由 | 字段匹配 |
|------|----------|----------|----------|
| 登录 | `POST /api/auth/login` | `POST /api/auth/login` | `soldier_id`, `password` ✓ |
| 注册 | `POST /api/auth/register` | `POST /api/auth/register` | `soldier_id`, `password`, `name`, `unit`, `role` ✓ |
| 位置上报 | `POST /api/soldier/location` | `POST /api/soldier/location` | `id`, `name`, `lat`, `lng` ✓ |
| 需求上报 | `POST /api/demand` | `POST /api/demand` | `type`, `quantity`, `unit`, `urgency`, `drop_point` ✓ |
| 需求列表 | `GET /api/demands` | `GET /api/demands` | `soldier_id` (query) ✓ |
| 任务查询 | `GET /api/task/current` | `GET /api/task/current` | `soldier_id` (query) ✓ |
| SOS 求救 | `POST /api/sos` | `POST /api/sos` | `soldier_id`, `soldier_name`, `lat`, `lng` ✓ |
| 投放点列表 | `GET /api/drop-points` | `GET /api/drop-points` | 无参数 ✓ |
| 账号列表 | `GET /api/auth/accounts` | `GET /api/auth/accounts` | 无参数 ✓ |
| 存活检测 | `GET /api/ping` | `GET /api/ping` | 无参数 ✓ |
### ⚠️ 存在差异的接口
| 功能 | 问题 | 说明 |
|------|------|------|
| **前端调用不存在函数** | HTML 绑定 `App.toggleTheme()`、`App.showChangePasswordModal()`、`App.selectDropPointFromMap()`,但 `app.js` 中不存在 | 需补齐或移除 HTML 绑定 |
| **任务更新** | 后端有 `POST /api/task/update`,前端无对应调用 | 前端应增加任务进度上报功能 |
| **危险区域** | 后端有 `GET/POST /api/danger-zones`,前端无对应界面 | 前端应增加危险区域展示 |
| **无人机状态** | 前端有 `loadDroneStatus()` 调用 `/api/drone/status`,但后端返回的是**内存演示数据**,非真实无人机状态 | 需明确数据来源(后端演示 vs ROS 实时) |
| **投放点添加** | 后端有 `POST /api/drop-point`,前端无对应功能 | 前端地图选点功能需对接此接口 |
---
## 五、与整体项目的集成评估
### 5.1 在整体架构中的定位
```
【单兵APP】◄──HTTP REST──►【后端】◄──数据服务──►【电脑端前端】
▼ WebSocket
【rosbridge】
【多模态融合 / PX4】
```
后端在架构中的角色是 **"数据中枢 + 认证网关"**
- ✅ 接收单兵APP的位置、需求、SOS 数据
- ✅ 为电脑端提供任务调度数据
- ❌ **不参与无人机控制链路**(无人机状态是演示数据)
- ❌ **不参与声源/热成像/闪光检测链路**
### 5.2 与无人机模块的集成
| 评估项 | 现状 | 评估 |
|--------|------|------|
| 无人机状态数据 | 后端 `_drone_status` 是内存演示数据 | ⚠️ **假数据**,真实状态应来自 ROS |
| 任务派发链路 | 后端生成任务 → 电脑端展示 → 操作员确认 → 无人机执行 | ✅ 链路完整,但后端到无人机的控制命令未打通 |
| 航线规划 | 后端未提供航线规划 API | ❌ 缺失,无人机组可能需要手动规划 |
| 实时遥测 | 电脑端前端通过 WebSocket 直连 rosbridge不经过后端 | ⚠️ 后端被绕过,无法做数据持久化 |
**建议**:后端应增加无人机状态同步接口,从 ROS 话题订阅真实状态并存入数据库供单兵APP和电脑端统一查询。
### 5.3 与声源分析模块的集成
| 评估项 | 现状 | 评估 |
|--------|------|------|
| 声源威胁数据 | 声源检测节点 → ROS Topic → 多模态融合 → 地面站 | ✅ 链路完整,但**不经过后端** |
| 后端感知 | 后端完全不知道声源检测状态 | ❌ 后端与声源模块零集成 |
**建议**如需要单兵APP接收声源威胁告警后端应订阅 `AcousticThreatArray` 话题,通过 WebSocket 或推送通知发送到APP。
### 5.4 与热成像/闪光检测模块的集成
| 评估项 | 现状 | 评估 |
|--------|------|------|
| 热成像数据 | 直接 C++ 调用,被多模态融合消费 | ✅ 内部链路完整 |
| 闪光检测数据 | ROS Topic → 多模态融合 | ✅ 内部链路完整 |
| 后端感知 | 后端完全不知道这些检测状态 | ❌ 零集成 |
---
## 六、优先级修复建议
### 🔥 第一优先级(课程演示前必须修复)
1. **修复 `index.html` 语法错误** (`</nav>1`)
2. **补齐或移除 HTML 中不存在的函数调用**
3. **修复数量验证逻辑漏洞**(前后端都要修)
4. **修复 SQL 注入风险**`update_task`
5. **移除硬编码的高德 Key**
### 🔶 第二优先级(提升稳定性)
6. **为 DOM 操作添加空值保护**
7. **限制 CORS 来源**
8. **为 Token 添加过期机制**
9. **后台轮询暂停**`visibilitychange`
10. **添加密码最小长度验证**
### 🟢 第三优先级(优化体验)
11. **清理 `js/js/` 重复目录**
12. **前端增加任务更新调用**(对接 `POST /api/task/update`
13. **前端增加危险区域展示**
14. **添加 API 日志中间件**
15. **自定义 500 错误页面**
---
## 七、总结
### 优势
1. **功能覆盖完整**登录、位置上报、需求上报、任务调度、SOS、无人机监控等核心场景均已实现
2. **架构分层清晰**APP → 后端 → 数据库 → 前端,职责明确
3. **Token 认证正确**:数据库存储,支持多 worker 共享
4. **SQLite 持久化**:数据不丢失,支持课程演示
### 劣势
1. **健壮性不足**:多处运行时崩溃风险、输入验证漏洞
2. **安全性薄弱**CORS 全开放、SQL 注入、敏感信息明文存储
3. **与机载模块集成浅**:后端不参与无人机控制链路,声源/热成像数据不经过后端
4. **代码质量参差**HTML 与 JS 契约不一致、Mock 数据与真实数据不匹配
### 适配度结论
**单兵终端APP + 电脑端后端** 作为 **"地面层数据服务"** 与整体项目是**适配的**,能够支撑课程演示所需的核心场景(士兵上报需求 → 指挥部调度 → 任务跟踪)。
但在以下方面存在明显短板,需要在正式演示前修复:
- 前端的健壮性(运行时崩溃、验证漏洞)
- 后端的安全性SQL 注入、CORS、Token 过期)
- 前后端契约一致性(不存在的函数调用、缺失的接口对接)
---
*报告生成时间2026-05-23*
Loading…
Cancel
Save