Files
2026-09-10 09:41:13 +08:00

285 lines
44 KiB
Markdown
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.
# 视野 后端服务
野生动物实时识别 App「视野」(Flutter 客户端见 `../flutter_app/`)的支付与授权后端。
## 功能模块
| 模块 | 说明 |
|---|---|
| 账号 | 手机号+密码注册/登录(`POST /api/v1/auth/register` / `login`),密码 bcrypt 哈希存储,登录签发自签名 token(`auth.secret`),授权绑定手机号账号、同账号多设备共享 |
| 订单 | 客户端 `GET /api/v1/plans` 拉取套餐价格 → `POST /api/v1/orders` 创建订单并返回支付参数(微信 prepay / 支付宝 orderStr);未支付订单 2 小时惰性关闭,同账号 2 小时内复用 created 订单防重复扣款 |
| 支付回调 | 微信支付 / 支付宝异步回调验签落授权(`/api/v1/payment` 分组,按渠道应答格式直返,不做统一包装) |
| 订单确认 | 客户端支付成功后 `POST /api/v1/orders/{orderId}/confirm` 幂等通知,加速授权刷新 |
| 授权查询 | `GET /api/v1/license`Bearer token)返回授权状态,App 识别入口强制服务端校验 |
| 标注众包赚时长 | App 用户处理管理端下发的标注任务获取使用时长:任务 = 管理端**勾选的具体图片集合**(图片粒度下发,2026-09-07;已下发图即从「未标注」tab 消失,停用任务释放未领取图回池),每次领取 10 张(锁定向、超时自动释放、已处理过的图不再分配),手机上画框提交 → 图片进「待审核」;每有效提交 10 张**即时到账 30 分钟**(license 到期时间分钟级顺延),**每日上限 2 小时**;质量惩罚:审核通过比例 <80%(已审核样本 ≥5)自动冻结领取资格 24 小时(时间戳对比天然自动解冻、统计重新累计),管理端可手动解冻;服务端 `annotate_record` 记录每用户每张图的处理明细(领取/提交快照/审核结果) |
| 假目标上报(负样本回流,2026-09-08) | App 识别页一键「误报上报」:上报瞬间按一次快照帧(**与喂给 YOLO 的帧同源同尺寸**,整图,jpg q85 重编码剥离全部元数据)+ 当前全部检测框快照(归一化坐标/类别/置信度/来源模型,`detections` JSON),无任何额外选择步骤;服务端落 `false_target_report` 待审(每用户**每日上限 50 条**防灌水);管理端「数据训练 → 假目标上报」tab 审核(radio 带待审数)——**通过 = 整图迁移入负样本库**(`__negative__` 数据集,训练打包自动混入当背景学习,压制同类误报),拒绝 = 删文件;**上报查重(2026-09-09)**:整帧 dHash 与已上报记录近重复(汉明 ≤8)直接拒绝入库不占日限额;**RF-DETR 疑似真目标预判**:上报后异步检测,高分检出标记「疑似真目标」供审核把关;**合规**:首次使用单独同意弹窗(可拒绝且不影响识别),用户协议含反馈图训练使用授权条款 |
| 套餐 | `config.yml` `plans` 节点配置三档套餐(改价 = 改配置重启),价格**整数分**1000 / 5600 / 18000 |
| 后台管理端 | `server_admin/`Vue3 + Element Plus)管理页面:订单查询、账号/授权管理(手动授权/撤销)、App 版本管理;构建产物由后端 `/admin/` 托管,登录页输入 token 后以 `X-Admin-Token` 头鉴权(`config.yml admin.token` |
| 版本管理 | 后台管理端上传 Android APK + 更新说明,APK 存服务器 `app.apkDir`(默认 `./workspace/`,与 `./data` 平级、挂载持久化)**固定文件名 `observer-latest.apk`,上传即覆盖,目录永远只保留最新一个文件**;**版本号从文件名识别**:文件须命名为 `observer-x.y.z.apk`(Flutter 打包产物即此命名,版本号取自 pubspec);客户端启动时 `GET /api/v1/app/update` 检查更新:服务器版本高于本地版本即弹更新提示(不可跳过)。**仅 Android 检查,iOS 不做版本下发**iOS 走 App Store 自行更新)。版本记录可删除:删最新版本联动删除 APK 文件,删历史版本仅删记录 |
| 数据训练(唯一入口) | 后台管理端「数据训练」一个菜单承载数据集全流程,**双 tab(2026-09-07**:「数据集」tab = 数据集卡片列表(封面图/描述/图片数/已标注数/**训练状态徽标**),「负样本」tab = 负样本库图片网格(上传/删除,见技术设计.md「负样本库」——训练打包时统一混入全部物种数据集);**卡片下方直接展示训练任务进度条与状态**(无独立训练页);详情页为**图片与标注一体视图**:分页(每页 20 条)逐行「原图 ‖ 标注图」对照展示;**图片不自动标注(2026-09-04 自动标注退场)**:标注唯一入口 = 勾选图片顶栏「预标」(RF-DETR 四级漏斗检测,见技术设计.md「预标注四级漏斗」),进度条展示在页顶;**预标完成进「待审核」,人工审核通过才「已标注」**(`dataset_image.review_status` 0 未标注/1 待审核/2 已审核 三态,训练集只收已审核图);点击原图/标注图弹窗放大进入标注编辑器(画框/确认/清理,保存即视为已审核);封面(上传/生成统一 1248x704 转 jpg + UUID 命名)/**描述**/AI 生成图片(provider 抽象:dashscope 通义万相付费 API / localai 训练机 local-ai qwen-image`config.yml imageGen` 节点切换,见配置说明);AI 标注端点与训练机 SSH 为**全局配置,直接读 `config.yml`**`localAi` / `training.ssh` 节点,改配置需重启服务);图片落服务器 `app.datasetDir`/`datasets/<数据集名>/`,DB 存元数据 + 标注 JSON**数据清洗(2026-09-02)**:详情页「数据清洗」——按标注目标尺寸细档统计超配,超配桶内整图 dHash 多样性保留、其余进候选清单,执行=打「已排除训练集」标记(可恢复不删图),prepare_yolo 打包跳过 |
| 模型训练 | 从数据集卡片「开始训练」一键触发(参数 imgsz/epochs/batch/device 默认走 `config.yml` `training` 节点,部署级配置);**双档位(2026-09-03)**:一次发起按档位各建一条任务——高识别档 s(基座 `training.model`、imgsz `training.imgsz`=1280/ 高性能档 n(基座 `training.modelN`=yolov8n.pt、imgsz `training.imgszN`=704),请求传 `variants:["s","n"]` 限定(省略=双档;n 档配置缺失时请求报错),epochs/batch/device 双档共用,任务带 `variant` 快照;**综合训练(2026-09-09**`POST /admin/trainings/combined` 勾选 ≥2 个数据集 + 档位,多物种合并训练出**一个综合模型**(全类一张 tflite:类别表 = 各物种名按数据集 id 升序 + 共享 suspect 置末位,打包时类别 id 重映射、负样本只混一份、图片名加 d<id>_ 前缀防跨数据集重名),产物/发布/目录下发走现有链路,文件基名 `combined`combined.tflite / combined_n.tflite);单物种训练流程不变,两种模式并存(详见技术设计.md「综合训练」);**GPU 独占排队(2026-09-03**:并发度 1 不变——已有 running 时新任务落 `queued` 排队(不再拒绝),10s 轮询在 running 结束后自动按创建顺序晋级启动、一次一个(训练机单 GPU 串行跑多档/多数据集),取消 running=杀进程、queued=直接置失败;进度/日志/指标监控(每 epoch 粒度);训练通道 `training` 节点可配置 subprocess(与 Go 服务同机直接起 python)/ ssh(异机执行,SSH 凭据取 `config.yml` `training.ssh` 节点);训练脚本 `server/training/train_server.py`(随项目迁移,2026-08-26)参数化(task.json 传 model/imgsz),产物(best.tflite/best.pt/曲线)拉回服务器;训练收尾自动做 **tflite 产物自检**(输入/输出 shape 校验,原 `inspect_tflite.py` 逻辑内嵌脚本),自检失败任务置失败并带出原因;**增量训练(2026-09-09)**:单物种/综合任务按档位 lineage 自动热启动——上一次成功的 best.pt 存档于 `workspace/trainings/weights/<基名>.pt`(基名同 tflite:单物种 `<前缀或数据集名>[_n]`、综合 `combined[_n]`),下次训练存在即推训练机作基座、不存在回落 config 基座(首次全量),类别数变化自动重建检测头;删该文件即从零重训;`dump_graph.py` 留作训练机人工深度调试 |
| 模型版本与热更新 | **每数据集每档位一个模型**2026-09-03 双档位):训练成功后一键「发布」(训练任务操作列)——tflite 已由训练成功直写最终位置:s 档 `workspace/trainings/<文件名前缀>.tflite`、n 档 `<文件名前缀>_n.tflite`(前缀空回退数据集名),发布仅落 `model_version` 记录(sha256/大小/指标/类别名,带 `variant` 档位列);版本序列每数据集全局共用 m1.0.0 递增(s/n 交替发布走同一序列,无档位独立序列),`is_latest` 按 (数据集, 档位) 各记一条——发布只清同档位旧记录,s/n 两档互不影响,目录可分别发布、分别下发。管理端**无模型管理界面**(版本记录仅支撑客户端下发)。**App 模型热更新**:`GET /api/v1/app/update` 扩展返回 `models` 目录数组,客户端独立检查,新模型下载校验替换,失败回退旧模型——模型迭代不再重打包 APK |
| 模型目录与多模型推理 | `GET /api/v1/models`(登录态)返回全部数据集当前生效模型(数据集/档位 `variant` s|n/版本/类别/大小/sha256/下载地址;**每数据集最多 2 条 = s/n 两档各自的 is_latest**),下载 URL s 档 `/download/trainings/<文件名前缀>.tflite`、n 档 `/download/trainings/<文件名前缀>_n.tflite`(前缀空回退数据集名);**App 模型管理页**用户自由下载/删除/启用模型,识别时**按当前识别档位(s 高识别 / n 高性能,全局切换)加载该档位已启用模型**并行推理 + 跨模型 NMS 合并(按类别名),内置 assets 模型兜底 |
| 标注 | **无自动标注(2026-09-04 退场,用户定案)**:上传/生成入库不触发任何检测,`localAi` 未配置不再阻断入库;标注唯一入口 = 管理端勾选图片顶栏「预标」→ `POST /admin/label-tasks`RF-DETR **四级漏斗**:全图扫描→空检自动升级切片扫描→仍空 VLM 提议候选区+RF-DETR 精修;切片参数走 `localAi.tileSize`/`tileOverlap`/`tileThreshold`,见技术设计.md「预标注四级漏斗」;扫描结果 minIoU 重叠去重后直写 `dataset_image.labels_json`,空检出写 `[]` 且 review_status 保持未标注);**预标完成 →「待审核」(review_status=1),人工审核通过才「已标注」(=2)**,训练集打包只收已审核图(prepareYoloSet 质量闸门);工作台弹窗人工画框/确认后保存即视为已审核;管理端对待审核图批量「通过/拒绝」(拒绝 = 清标注回未标注池,并计入对应 App 用户的低质统计,见「标注众包赚时长」) |
## 架构与数据流
```
Flutter App ── POST /auth/register|login ─► 账号注册/登录,签发 token
├─► POST /orders (Bearer token) ──► 后端下单(校验 planId/channel
│ │
│◄───────┘ orderId + payParams
├─► 拉起微信/支付宝 SDK 支付
├─► 微信/支付宝服务端异步回调 ─► 验签 → 落授权(幂等,绑账号)
└─► POST /orders/{id}/confirm (Bearer token) ─► 幂等确认(回调已落授权则直接返回)
└─► GET /license (Bearer token) ─► 返回 {active, expiresAt}
```
- 授权以**服务端为准**:客户端本地缓存 `expiresAt` 仅用于主页展示,识别入口强制查询服务端
- 有效期语义:**自然日**(day 当天 24:00 失效;week/month 自生效日起自然日累计),服务端计算 `expiresAt`,客户端只读不判权
## 数据表
| 表 | 说明 | 关键字段 |
|---|---|---|
| `payment_order` | 支付订单 | `order_id`(PK)、`phone_num``plan_id``channel`(wechat/alipay)、`amount_cents``status`(created/paid/closed)、`wx_trade_no`(UNIQUE)、`alipay_trade_no`(UNIQUE)、`created_at``paid_at` |
| `license` | 手机号账号与授权 | `phone_num`(PK)、`password`(bcrypt)、`expires_at`(未充值 NULL;标注奖励分钟级顺延)、`annotate_frozen_until`(标注低质冻结到期标记,NULL/过期=正常)、`annotate_stats_since`(通过比例统计基线,冻结时重置实现解冻后重新累计)、`remark`(管理端备注)、`created_at``updated_at` |
| `app_version` | App 版本管理 | `id`(PK)、`version`(x.y.z, UNIQUE)、`notes`(更新说明)、`created_at``updated_at`(下载地址不落表:APK 固定文件 `app.apkDir`/`observer-latest.apk`,默认 `./workspace/` |
| `dataset` | 训练数据集 | `id`(PK)、`name`(UNIQUE)、`source`(manual/ai/**negative**=负样本库,2026-09-07:固定保留名 `__negative__`,训练打包时混入全部物种数据集当背景学习)、`image_count``labeled_count``status`(building/synced/labeled)、`cover`(封面文件名,UUID 命名 jpg,如 `9f2a...-xx.jpg`)、`description``created_at``updated_at`(图片文件在 `app.datasetDir`/`datasets/<name>/`,DB 只存元数据;AI 标注/训练机 SSH 配置走 `config.yml``localAi` / `training.ssh` 节点);**生成参数池(创建时 VLM 自动生成,界面不维护,可 `POST /datasets/gen-pools` 重新生成)**`gen_species`(单值=数据集物种)、`gen_tone`(单值 轮廓色词 深色/浅色)、`gen_heights`(数值 站高cm,距离感公式用)、`gen_scenes`/`gen_actions`/`gen_occlusions`(JSON 数组 各≥3条)、`gen_classes`(单值 第二标注类别名="suspect",第一类别=gen_species,训练 data.yaml names);**单物种规则:每数据集只对应一个物种(生成图片固定按数据集名),不同物种拆到不同数据集** |
| `dataset_image` | 数据集图片 | `id`(PK)、`dataset_id``filename``source`(manual/ai)、`prompt`(AI 生成图记录提示词)、`labels_json`(标注 JSON 数组:YOLO 归一化 xywh+类别+置信度,AI 预标与人工标注同存、人工可修改/清理,null/''/'[]'=无框)、`review_status`(2026-09-04 审核三态:0 未标注/1 待审核/2 已审核;预标与 App 提交→1,人工保存与审核通过→2,拒绝清标注→0;训练集只收 2)、`clean_excluded`(0/12026-09-02 数据清洗排除出训练集标记,prepare_yolo 打包跳过,可恢复)、`annotate_task_id`(2026-09-07 众包下发的任务占用标记,0=未下发;下发即从「未标注」tab 消失,停用任务释放未领取图回 0)、`created_at` |
| `model_training` | 训练任务 | `id`(PK)、`name``status`(queued/running/success/failedqueued=GPU 忙排队中,2026-09-03)、`dataset_id`(综合任务=0)、`variant`(s/n 档位,default s2026-09-03)、`kind`(species/combined2026-09-09 综合)、`dataset_ids`(JSON,综合任务覆盖的数据集列表)、`imgsz`/`epochs`/`batch`/`device`(参数快照)、`current_epoch`/`total_epochs``metrics`(JSON)、`log_tail``pid``error``started_at`/`finished_at``created_at` |
| `model_version` | 模型版本(每数据集全局共用序列) | `id`(PK)、`dataset_id``variant`(s/ndefault s;存量行迁移为 s)、`version`(m1.0.0 递增, 同数据集 UNIQUE,s/n 交替发布共用序列)、`training_id``metrics`(JSON)、`labels`(JSON 类别名数组)、`sha256``size_bytes``is_latest`(按 (数据集,档位) 各记一条)、`kind`(species/combined2026-09-09 综合,综合行 dataset_id=0)、`dataset_ids`(JSON,综合覆盖的数据集)、`notes``created_at`(模型文件不落表:发布即写 `trainings/<文件名前缀>.tflite`s/`<文件名前缀>_n.tflite`(n,前缀空回退数据集名),客户端固定下载,无存档回退) |
| `label_task` | 标注任务 | `id`(PK)、`dataset_id``filenames`(JSON 选中图片列表,NULL=全量)、`status`(running/done)、`total`/`done``created_at``finished_at` |
| `gen_task` | AI 生成任务(异步批量) | `id`(PK)、`dataset_id``status`(running/done/failed)、`total`/`done``error``created_at``finished_at` |
| `annotate_task` | 标注众包任务(2026-09-042026-09-07 图片粒度下发) | `id`(PK)、`dataset_id``name``status`(published/stopped)、`created_at`(任务图集 = `dataset_image.annotate_task_id` 占用本任务 id 的图,不落任务表) |
| `annotate_record` | 用户标注记录(2026-09-04 | `id`(PK)、`phone_num``task_id``dataset_id`(领取时冗余,详情页过滤用)、`image_id``labels_json`(提交快照)、`status`(pending 领取锁/submitted/approved/rejected)、`created_at``submitted_at``reviewed_at`UNIQUE(phone_num,image_id)partial unique(image_id) WHERE status='pending' |
| `reward_log` | 标注时长发放流水(2026-09-04 | `id`(PK)、`phone_num``minutes``task_id``granted_at`(日上限 = 自然日求和) |
| `false_target_report` | 假目标上报(2026-09-08 | `id`(PK)、`phone_num``file`(uuid.jpg 分析帧整图)、`labels_json`(检测框快照:label/class/score/model/归一化 xywh)、`species`(来源模型名去重拼接)、`status`(pending/approved**拒绝即删记录与文件,无 rejected 存量**)、`image_hash`(整帧 dHash 64 位,上报查重用,0=未算/存量回填前)、`suspect_real`(0/1,RF-DETR 高分检出疑似真目标预判标记,2026-09-09)、`created_at``reviewed_at`(文件落 `datasetDir/false_targets/`,审核通过迁移入负样本库图片目录) |
建表与迁移见 `技术设计.md`(新库直接建表;存量库以 `PRAGMA user_version` 版本化迁移)。
## API 清单
响应统一格式 `{"code": 0, "message": "ok", "data": {...}}``code != 0` 视为失败。接口只允许 GET / POST。
### POST /api/v1/auth/register
手机号注册(与登录无关的公开接口,无需 token)。请求:`{"phone": "13800000000", "password": "..."}`。已注册手机号返回错误。注册成功即创建账号(未充值,无额度)。
### POST /api/v1/auth/login
手机号登录(自动注册式:手机号不存在则以输入密码创建账号,存在但密码为空则补写输入密码,仅已有密码时才做密码比对,比对失败报错;创建/补密码后即登录成功)。请求:`{"phone": "13800000000", "password": "..."}`。响应 `data: {"token": "..."}`HMAC 自签名,有效期 `auth.token_ttl`,默认 30 天)。后续登录态接口携带 `Authorization: Bearer <token>`
### GET /api/v1/plans
套餐列表(需 Bearer token)。响应 `data`
```json
{
"list": [
{"planId": "day", "days": 1, "priceCents": 1000},
{"planId": "week", "days": 7, "priceCents": 5600},
{"planId": "month", "days": 30, "priceCents": 18000}
]
}
```
套餐来自 `config.yml` `plans` 节点(静态定价,改价改配置后重启服务生效);客户端不硬编码价格。展示名由客户端按 `days` 拼「N天」派生,无独立 label 字段。
### POST /api/v1/orders
创建订单,返回渠道支付参数(需 Bearer token,手机号由 token 解出)。
请求(JSON body):
```json
{"planId": "week", "channel": "wechat"}
```
- `channel``wechat` | `alipay`
- `planId``day` | `week` | `month`(需在 `config.yml` `plans` 节点存在,金额由服务端按配置锁定,客户端不可改价)
响应 `data`
```json
{
"orderId": "O20260822001",
"payParams": {
"partnerId": "1900xxxxx",
"prepayId": "wx...",
"nonceStr": "...",
"timeStamp": "1728000000",
"sign": "...",
"packageValue": "Sign=WXPay"
}
}
```
- `channel == wechat``payParams` 为微信 APP 支付统一下单返回参数(后端签名)
- `channel == alipay``payParams``{"orderStr": "alipay_sdk=..."}`(后端调 `alipay.trade.app.pay` 生成)
### POST /api/v1/orders/{orderId}/confirm
客户端支付成功后通知(幂等,需 Bearer token)。服务端以异步回调为准落授权,confirm 仅用于加速刷新;回调已到账时直接返回成功。
请求:`{}`(无参数)。响应:`data: {"status": "paid"|"created"}`
返回 `created`(回调未到账)时客户端按 3s → 10s → 30s 递增重试 confirm(最多 3 次),期间禁止重新下单;仍未成功回退 `GET /license` 查询(回调到账后自然变为 active)。回调为唯一授权来源,confirm 仅加速刷新。
### GET /api/v1/license
查询账号授权状态(需 Bearer token,手机号由 token 解出)。
响应 `data`
```json
{"active": true, "expiresAt": "2026-08-29T23:59:59+08:00"}
```
`active: false``expiresAt` 已过期(含未充值账号)→ 客户端展示付费墙/充值引导。
### 支付回调(微信/支付宝,勿直连)
| 渠道 | 路径 | 说明 |
|---|---|---|
| 微信支付(APP 支付 V3) | `POST /api/v1/payment/wechat/notify` | 解密+验签回调,`out_trade_no` → 落授权 |
| 支付宝(APP 支付) | `POST /api/v1/payment/alipay/notify` | RSA2 验签回调,`out_trade_no` → 落授权 |
### GET /api/v1/app/update
App 版本更新检查(公开接口,无需 token,未登录/旧版本均可访问)。返回服务器最新版本记录;无任何记录时 `data` 为空对象,客户端视为无需更新。
响应 `data`
```json
{
"version": "1.1.0",
"notes": "修复识别准确率问题"
}
```
- **仅 Android 客户端调用**iOS 不做版本下发,走 App Store 自行更新)
- 检测到新版本(服务器版本高于本地版本)即**强制更新**,客户端弹不可关闭的全屏提示,必须跳转更新后才能继续使用;本地已是新版本则不提示
- 客户端以「语义化版本号」比较:`1.10.0 > 1.9.9`(按数字段比较,禁止字符串比较)
- 下载地址为固定静态路径:`/download/observer-latest.apk``app.apkDir` 目录下永远只有最新一个文件,由后端静态托管),客户端拼 `apiBaseUrl` 访问
- **模型热更新(与 APK 更新独立通道)**:服务器有已发布模型时响应额外返回 `models` 数组(与 `GET /api/v1/models` 同构:datasetId/datasetName/version/labels/sizeBytes/sha256/downloadUrl);客户端启动与 APK 更新**独立检查**——某数据集服务器版本高于本地已下载版本即下载 `/download/trainings/<文件名前缀>.tflite`(前缀空回退数据集名) 到应用私有目录,sha256 校验后原子替换,下次识别生效;**非强制**,失败回退旧模型下次启动重试。App 模型管理页列出服务器全部可用模型,用户自由下载/删除/启用;识别时加载全部已启用模型**并行推理 + 跨模型 NMS 合并**(按类别名),内置 assets 模型兜底。无发布模型时不返回 models 字段(旧 App 忽略新字段、新 App 兼容旧服务器)
### GET /download-page
APK 下载引导页(静态页面,源码在 `h5/index.html`,由后端 `/download-page` 路径托管):微信内置浏览器会拦截 APK 下载,此页面按打开环境分流——**微信内打开**显示图形引导(点击右上角「···」→「在浏览器打开」+ 复制链接兜底);**手机浏览器打开**直接显示下载按钮,直链 `http://observer.redpowerfuture.com/download/observer-latest.apk`
### 管理端接口(`/api/v1/admin`,需请求头 `X-Admin-Token` = `config.yml admin.token`
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | `/admin/orders` | 订单列表,筛选参数 `phoneNum/status/startAt/endAt` + `page/size`,返回 `{total, list}` |
| GET | `/admin/licenses` | 账号/授权列表,筛选 `phoneNum` + `page/size`(含未充值账号) |
| POST | `/admin/licenses/grant` | 手动授权 `{"phoneNum":"13800000000","planId":"day"}`,自然日叠加 |
| POST | `/admin/licenses/revoke` | 撤销授权 `{"phoneNum":"13800000000"}`(清授权保留账号) |
| POST | `/admin/licenses/remark` | 写账号备注 `{"phoneNum":"13800000000","remark":"..."}`(空串清空,≤200 字) |
| GET | `/admin/app-versions` | 版本记录列表,`page/size` 分页,按下发时间倒序 |
| POST | `/admin/app-versions` | 下发新版本(multipart/form-data):`notes` + `file`APK 文件,仅接受 `.apk`);**版本号从文件名识别**,文件须命名为 `observer-x.y.z.apk`(如 `observer-1.0.1.apk`),格式不符拒绝;版本号不可重复,APK 上传覆盖 `app.apkDir`/`observer-latest.apk`(目录永远只有一个文件);检测到新版本即强制更新 |
| POST | `/admin/app-versions/delete` | 删除版本记录 `{"id":1}`:删**最新版本**时联动删除 APK 文件(客户端不再提示更新、下载 404);删历史版本只删记录不动文件 |
| POST | `/admin/datasets` | 创建数据集 `{"name":"pheasant_v2","namePrefix":"pheasant","source":"manual"\|"ai","cover":"<文件名>"}`(name ≤50 字唯一,目录自动建;namePrefix=AI 生成图文件名前缀,生成图按 `<前缀>_<两位序号>.jpg` 顺序命名;物种=数据集名(单物种规则),创建时同步调 VLM(qwen3.6-35b-a3b) 自动生成物种/场景/动作/遮挡/站高/类别名等生成参数池,响应含 `poolsGenerated`/`poolError`——VLM 失败不阻断创建,参数可事后用 gen-pools 补生成;封面优先用 cover 参数(新建对话框预生成封面回传,跳过自动生成),否则参数池成功后自动生成 16:9(1248x704)封面(1 雄 1 雌并排,响应含 `coverGenerated`/`coverError`,失败可在编辑模式重新生成);模型生成图统一转 jpg 落盘) |
| GET | `/admin/datasets` | 数据集列表:`page/size` 分页,返回 `{total, list}`(含 imageCount/labeledCount/status/cover/description/**training 聚合状态**:最新训练记录的 status/currentEpoch/totalEpochs/etaMinutes——预计剩余分钟,running 且已完成 ≥1 轮才 >0);**排除负样本库**source=negative,仅「负样本」tab 展示) |
| POST | `/admin/datasets/negative` | 获取负样本库(2026-09-07):不存在则自动创建(source=negative 固定名 `__negative__`),返回数据集记录;其 id 供既有 `/admin/datasets/upload``/admin/datasets/images``/admin/datasets/images/delete``/admin/datasets/image` 复用(负样本无标注/审核/清洗流程) |
| POST | `/admin/datasets/negative/generate` | 负样本批量生成 `{"count":100}`(异步任务,2026-09-07):config.yml 内置场景池按序循环组装提示词(negativeScenes 空场景 + negativeHumans 人物/衣物两池两模板;**动物不进统一负样本**——它们将来可能是正式识别目标),空场景图**自动过 RF-DETR 空检、有检出即剔除不入库**(人物图不做空检);进度复用 GET `/admin/datasets/gen-task`(rejected=剔除数),入口在管理端「负样本」tab |
| POST | `/admin/datasets/update` | 更新数据集配置 `{"id":1,"name":"新名","namePrefix":"pheasant","description":"...","cover":"a.jpg"}`:名称(改名)/文件名前缀/描述/封面,空值字段不覆盖原值;gen_* 生成参数池不在此维护(仅 VLM 生成,见 gen-pools);改名同步迁移图片目录与模型文件,标注/训练进行中拒绝 |
| POST | `/admin/datasets/gen-pools` | 重新生成数据集生成参数池 `{"datasetId":1}`:按数据集名(物种)调 VLM(qwen3.6-35b-a3b) 生成轮廓色/站高/场景/动作/遮挡并写表(覆盖旧值);失败报错保留旧值 |
| POST | `/admin/datasets/images/vlm-review` | VLM 藏匿位补检 `{"datasetId":1,"imageId":5}`(两阶段标注第二阶段,**须与图像生成显存互斥**):qwen3.6-35b-a3b(+mmproj) 排除已确认框,按环境/季节/时间/天气/光线/地形/习性综合判读画面推理藏身位,追加 ≤3 个疑似框(class 1)进同一 labels_json |
| POST | `/admin/datasets/cover` | 上传数据集封面(multipart`datasetId`+`file`jpg/jpeg/png ≤10MB):**自动缩放 1248x704 + 转 jpg + UUID 命名**落盘并覆盖旧封面 |
| POST | `/admin/datasets/cover/generate` | 生成数据集封面 `{"datasetId":1}`z-image 文生图:16:91248x704)、1 雄 1 雌,物种取 gen_species 空回退数据集名):覆盖旧封面,返回 `{cover}` 新文件名;`datasetId=0` 时传 `{"name":"家鸽"}` 新建预生成(数据集未创建,封面仅落盘不写库,创建请求带 cover 回传写库);生成任务进行中亦可直接调用(与任务图片同走 z-image、由 local-ai 服务端排队串行,2026-09-02 放开整任务拒绝),失败不覆盖旧封面 |
| GET | `/admin/datasets/cover` | 封面文件(静态字节流,`datasetId` 定位;`datasetId=0` 时按 `name`+`filename` 直读——新建对话框预生成封面回显) |
| POST | `/admin/datasets/cover/delete` | 删除数据集封面 `{"datasetId":1}`:删文件 + 清 cover 字段 |
| POST | `/admin/datasets/upload` | 上传图片(multipart/form-data`datasetId` + `files` 多张,仅接受 `.jpg/.jpeg/.png`),存 `app.datasetDir`/`datasets/<name>/`,逐张入库 |
| POST | `/admin/datasets/generate` | AI 生成图片 `{"datasetId":1,"count":1,"distance":25}`(**物种固定取数据集名,表单无需填写**scene/action/occlusion 后端从数据集表池随机,light 走 config 通用池;**性别每张随机雄/雌**(两性体型外观差异大,随机让训练数据覆盖两性形态);prompt 可手填覆盖模板;**每张固定 1 个目标**(数量词 "1只"animal_count=1 供标注裁剪);distance 必填注入提示词距离描述;距离校验已取消;文件名前缀取数据集属性)(prompt 留空时按 `imageGen.promptTemplate` 组装:**物种=数据集名,场景/动作/遮挡/站高从 dataset 表生成参数池读取(创建数据集时 VLM 生成,无 config 兜底——池为空报错提示补参数或手填提示词)**;手填覆盖)(**count 1..1000,异步任务**:校验通过即返回 `{taskId, total}`,后台协程逐张生成(不设调用超时,失败由 provider 真实返回判定),进度/结果轮询 GET `/admin/datasets/gen-task`):调 `imageGen` providerdashscope / localaiconfig 切换)逐张生成落盘 + 入库(记录 prompt);完成或部分失败后自动触发标注;任意失败任务置 failed(已生成图保留,付费资产原则);`namePrefix` 可选,按 `<前缀>_<两位序号>.jpg` 顺序命名并续接已有最大序号(如 pigeon_01.jpg),留空用时间戳命名;`animalCount` 仅作为自动标注框数上限(按置信度裁剪 ≤N),不参与生成校验 |
| GET | `/admin/datasets/gen-task` | 生成任务进度 `{"datasetId":1}`:返回最近一次任务 `{id, status(running/done/failed), total, done, error, createdAt, finishedAt}`;无任务返回 null |
| GET | `/admin/datasets/images` | 数据集图片列表 `{"datasetId":1}`,返回图片元数据(文件名/来源/prompt/创建时间) |
| GET | `/admin/datasets/image` | 图片文件(静态字节流,`datasetId` + `filename` 定位,供缩略图/查看) |
| POST | `/admin/datasets/images/delete` | 删除图片 `{"datasetId":1,"ids":[1,2]}`:删文件 + 删记录(AI 生成图是付费资产,前端确认文案提示) |
| GET | `/admin/datasets/export` | 导出数据集 zip`datasetId`,打包图片目录为 zip 下载(标注衔接用) |
| POST | `/admin/datasets/sync` | 同步数据集到训练机 `{"id":1}`:按 `training` 通道推送图片到训练机 `datasetDir/<name>/`subprocess 同机 cp、ssh 异机 scp/rsync);训练启动前自动执行 |
| POST | `/admin/datasets/clean/preview` | 数据清洗预览 `{"datasetId":1,"quotas":{...}}`(quotas 各档配额缺省用代码默认):按标注框高占比细档(<2/2-3/3-4/4-6/6-8/8-12/12-20/>20%)统计分布与超配量,超配桶按图内最小确认目标尺寸升序优先保留(小目标样本稀缺先保)、再整图 dHash 去近重复,其余进 `candidates`(含主/最小目标占比 + 目标数 + 实拍生成标识);返回各档分布 + 候选清单 + 已排除列表;<2% 极远档固定豁免,仅 class1 疑似图/空检 `[]` 不入清单 |
| POST | `/admin/datasets/clean/apply` | 清洗执行 `{"imageIds":[1,2],"exclude":true}`:批量置/清 `clean_excluded`(排除=打标记,prepare_yolo 打包跳过;false=恢复),不删文件不删标注 |
| POST | `/admin/label-tasks` | 发起预标注 `{"datasetId":1,"filenames":["a.jpg","b.jpg"]}`(详情页勾选图片「预标」按钮入口,**2026-09-04 起为标注唯一触发方式,入库不再自动标注**):**filenames 缺省=全量**,指定图则只扫描选中图(全量重标/单张修补);调 `config.yml` `localAi` 配置的 AI 端点 RF-DETR 逐张推理(common 池并行,未配置报错),扫描结果做**重叠去重**(NMS 风格按置信度降序保留,重叠比 > `localAi.overlapThreshold` 默认 0.3 的框剔除,同目标只留置信度最高者)后**直写 `dataset_image.labels_json`(重跑覆盖该图标注)**并置 review_status=1 待审核(空检出 `[]` 保持未标注),`label_task` 记录进度 |
| GET | `/admin/label-tasks` | 标注任务列表:`page/size` 分页(含 status/total/done |
| GET | `/admin/label-tasks/detail` | 标注任务详情:返回数据集全部图片 + 每张标注框(`boxes`YOLO 归一化 xywh + 置信度 + 类别) |
| POST | `/admin/label-tasks/save` | 保存单张标注 `{"datasetId":1,"filename":"a.jpg","boxes":[{"class":0,"cx":0.5,"cy":0.4,"w":0.1,"h":0.2}]}`:整体覆写该图 `labels_json`(空 boxes=清空标注),返回该数据集当前 `labeledCount` |
| POST | `/admin/trainings` | 发起训练 `{"datasetId":1,"name":"...","variants":["s","n"]}``variants` 限定档位(省略=双档 s+n 各建一条任务;只补跑高性能档传 `["n"]`;请求的档位 n 未配置时报错);校验数据集有标注 → 落任务返回 `{id}`(首条任务 id);**GPU 独占排队**:并发度 1 不变——已有 running 时不拒绝、新任务落 queued,running 结束后轮询自动按创建顺序晋级启动(一次一个);同数据集同档位已有任务(running/queued)时拒绝(防重复提交) |
| POST | `/admin/trainings/combined` | 综合训练发起 `{"datasetIds":[1,2],"variants":["s","n"]}`:勾选 ≥2 个数据集合并训练一个多物种模型;类别表 = 各物种名(按数据集 id 升序,gen_species 回退数据集名)+ 共享 suspect 置末位;每档位一条 `kind=combined` 任务(dataset_id=0、dataset_ids 快照),与单物种任务同队列排队;产物基名 `combined`combined.tflite / combined_n.tflite |
| GET | `/admin/trainings` | 训练任务列表:`page/size` 分页,按下发时间倒序,含 status(queued/running/success/failed)/variant/进度/指标;`status` 过滤参数支持 queued |
| GET | `/admin/trainings/detail` | 任务详情 `{"id":1}`:参数快照 + 进度 + 指标 + 日志尾部 |
| POST | `/admin/trainings/cancel` | 取消训练 `{"id":1}`running=杀训练进程置 failedqueued=无进程直接置 failed |
| POST | `/admin/trainings/publish` | 发布为最新模型 `{"id":1,"notes":"..."}`(仅 success):读训练成功已直写的 tflite(s 档 `<文件名前缀>.tflite` / n 档 `<文件名前缀>_n.tflite`)算 sha256/大小 → 插入 `model_version`(版本号数据集内递增 m1.0.0 → m1.0.1+ 同档位旧版 `is_latest=0`s/n 互不影响) |
| GET | `/admin/label-workbench` | 标注工作台数据 `{"datasetId":1}`:数据集全部图片 + 每张标注框(`boxes`labels_json 全量)——无历史标注任务时详情页工作台的数据源 |
| POST | `/admin/annotate-tasks` | 下发标注任务 `{"datasetId":1,"name":"...","imageIds":[...]}`2026-09-07 图片粒度下发):勾选的未标注图批量占用(`annotate_task_id` 写任务 id;校验属本数据集/未标注/未占用,任一非法整单拒绝),**已下发图即从「未标注」tab 消失**;同数据集已有 published 任务时拒绝 |
| GET | `/admin/annotate-tasks` | 标注任务列表:`datasetId` 可选过滤(**无独立菜单——「标注任务」tab 内嵌数据训练详情页,任务图集网格展示勾选下发的具体图片,头部任务概要条/停用**),含数据集名/池余量/各状态记录数/状态 |
| POST | `/admin/annotate-tasks/stop` | 停用任务 `{"id":1}`:整体不可再领取(已领取未提交的可继续提交);**释放未被领取过的图**(清 `annotate_task_id`,回到「未标注」tab 可再次下发) |
| POST | `/admin/annotate-review` | 批量审核 `{"imageIds":[...],"approve":true}`(详情页「待审核」tab):通过 → review_status=2 已标注(**全部框原样保留,含疑似框——通过即人工背书**,2026-09-07 定案);拒绝 → 清 labels_json 回未标注池 + 对应 submitted 记录置 rejected(计入该用户低质统计:通过比例 <80% 且已审核样本 ≥5 → 冻结 24h,自动解冻) |
| GET | `/admin/annotate-records` | 用户标注记录分页:`datasetId/phone/taskId/status` 过滤,每用户每张图的领取/提交快照/审核结果(**「标注记录」tab 同样内嵌详情页**) |
| POST | `/admin/annotate-unfreeze` | 手动解冻 `{"phone":"..."}`:清冻结标记与统计基线 |
### 标注众包(AppBearer token
| 接口 | 说明 |
|---|---|
| GET | `/api/v1/annotate/tasks` 可领任务列表(published + 池余量),附我的统计:今日已得时长/上限、进度(已提交 x/10)、通过比例、冻结到期时间(冻结中不可领取) |
| POST | `/api/v1/annotate/claim` 领取 `{"taskId":1}`:分配 ≤10 张未处理过的**任务下发图**(池 = 任务占用的未标注图,2026-09-07 起任务为勾选图片集合;pending 锁定,超时惰性释放;返回图片 URL + 数据集物种名) |
| POST | `/api/v1/annotate/submit` 提交 `{"imageId":1,"boxes":[...]}`:写 labels_json + review_status=1 待审核 + 记录置 submitted;每累计 10 张即时发 30 分钟(到期时间顺延),当日累计超 2 小时不再发 |
| GET | `/api/v1/annotate/me` 我的标注统计:累计提交/通过/拒绝、通过比例、累计获得时长、今日已得、冻结状态 |
管理页面由 `server_admin/` 构建产物提供,访问 `http://<host>/admin/`。金额均为整数分,前端展示 ÷100 转元。
### POST /api/v1/feedback/false-target
假目标上报(需 Bearer token`multipart/form-data`):App 识别页一键上报当前画面。字段:`file`(上报瞬间的分析帧整图 jpg,与推理帧同源同尺寸,客户端重编码已剥离元数据)+ `detections`(当前全部检测框快照 JSON 数组 `[{label,class,score,model,cx,cy,w,h}]`,归一化坐标,可空)+ `sourceW`/`sourceH`(分析帧宽高)。每用户每日上限 50 条,超限报错;与已上报记录整帧 dHash 近重复(汉明 ≤8)报「相似样本已存在,无需重复上报」(2026-09-09;查重先于落盘——重复上报不产生文件、不占限额,2026-09-10)。响应 `data: {id}`
### 管理端假目标上报(`/api/v1/admin`X-Admin-Token 鉴权;页面入口 = 数据训练页第三个 tab)
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | `/admin/false-targets` | 上报分页列表,筛选 `phoneNum/status`pending/approved),条目含画面预览地址/检测框快照/来源模型名 |
| GET | `/admin/false-targets/image` | 上报画面预览(静态字节流,`id` 定位) |
| POST | `/admin/false-targets/review` | 审核 `{"ids":[1],"approve":true}`:通过 = 画面迁移入负样本库(`__negative__` 数据集当背景图训练)+ 置 approved;**拒绝 = 删除记录与图片文件**;仅 pending 可审 |
### GET /api/v1/models
模型目录(需 Bearer token,客户端模型管理页拉取):返回**全部数据集当前生效模型**。响应 `data`
```json
{
"list": [
{"datasetId": 1, "datasetName": "pheasant", "variant": "s", "version": "m1.2.0", "labels": ["pheasant", "suspect"],
"sizeBytes": 6400000, "sha256": "ab12...", "notes": "修复小目标漏检", "publishedAt": "2026-08-26T10:00:00+08:00",
"downloadUrl": "/download/trainings/pheasant.tflite"},
{"datasetId": 1, "datasetName": "pheasant", "variant": "n", "version": "m1.3.0", "labels": ["pheasant", "suspect"],
"sizeBytes": 3500000, "sha256": "cd34...", "notes": "", "publishedAt": "2026-09-03T10:00:00+08:00",
"downloadUrl": "/download/trainings/pheasant_n.tflite"}
]
}
```
- 只返回 `is_latest=1` 的模型:**每 (数据集, 档位) 至多一条**(每数据集 s/n 各一条,variant 标识档位;n 档文件名带 `_n` 后缀);无任何发布模型时 `list` 为空数组
- **综合模型条目(2026-09-09**`kind:"combined"` + `datasetIds`(覆盖的数据集 id 列表)+ `datasetId:0``datasetName:"综合"`,下载地址 `/download/trainings/combined(_n).tflite`;单物种条目 `kind:"species"`(缺省视为 species,老 App 兼容);App 激活综合模型时自动停用其覆盖物种的单物种模型(反之亦然,覆盖互斥)
- 下载地址由客户端拼 `apiBaseUrl` 访问;下载文件 sha256 校验,类别名数组 `labels` 用于多模型合并推理展示;条目带 `variant`2026-09-03),App 按识别档位(s 高识别/n 高性能)筛选加载
## 使用说明
1. 配置 `config.yml`:监听端口、数据库路径、登录 token 签名密钥 `auth.secret`(必填,换值即全员下线)、套餐 `plans` 节点、微信支付(appid/mchid/商户私钥/证书序列号/APIv3 密钥)、支付宝(appid/应用私钥/支付宝公钥)、管理端 `admin.token`;模型训练相关节点:`training`(训练通道 mode=subprocess/ssh、ssh 连接信息、训练机工作目录/venv/数据集目录、并发度 1、超时;**双档位 2026-09-03**s 档基座/分辨率 `model`/`imgsz`、n 档 `modelN`/`imgszN`——n 档未配置时发起 n 档训练报错,epochs/batch/device 双档共用)、`imageGen`AI 生成图片 provider`dashscope` 通义万相(apiKey + model qwen-image-3.0/ `localai` 本地 local-aibaseUrl + model 如 qwen-image;每张不设调用超时,失败由 provider 真实返回判定))、`localAi`(二期标注用 RF-DETR 服务地址);SQLite 库由服务启动时自动建表并迁移,无需手工初始化
2. `go build ./...` 编译验证
3. 本地运行 `go run main.go`;服务层白盒测试:`GF_GCFG_FILE=biz/service/testdata/config.yml go test ./biz/service/`(独立测试库,见 `biz/service/testdata/`
4. 后台管理端:`cd server_admin && npm run build`(构建产物输出到 `server/admin_dist/`,由后端 `/admin/` 托管);开发联调 `npm run dev`Vite 代理 `/api``:8080`)。首次访问 `/admin/` 进入登录页,输入 `config.yml admin.token` 对应的管理 token(存浏览器 localStorage,随请求携带;token 不内嵌构建产物)。**硬性要求:只要修改了 `server_admin/` 源码,必须同步重新构建 `admin_dist/` 并提交产物**(后端托管的是构建产物,不重新构建则线上/部署版本不生效;禁止只改源码不构建)
5. Docker Compose 部署见 `Dockerfile` / `docker-compose.yml``./data` 运行时数据目录挂载持久化,容器重建不丢数据)
6. 客户端对接:`flutter_app/lib/config/app_config.dart``apiBaseUrl`、微信/支付宝 AppID 与 URL Scheme 替换为真实值
## 金额与安全约定
- 金额一律**整数分**`price_cents` / `amount_cents` 为 INT64),禁止浮点元;前端展示 ÷100
- 支付回调必须**验签**(微信 APIv3 验签/解密、支付宝 RSA2),回调处理**幂等**(`wx_trade_no`/`alipay_trade_no` 唯一约束,重复回调直接忽略)
- 授权落库以**较晚到期为准**:已有授权未过期时新套餐顺延叠加,禁止覆盖缩短