248 lines
33 KiB
Markdown
248 lines
33 KiB
Markdown
# 视野 后端服务
|
||
|
||
野生动物实时识别 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 识别入口强制服务端校验 |
|
||
| 套餐 | `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 文件,删历史版本仅删记录 |
|
||
| 数据训练(唯一入口) | 后台管理端「数据训练」一个菜单承载数据集全流程:**数据集卡片列表**(封面图/描述/图片数/已标注数/**训练状态徽标**),**卡片下方直接展示训练任务进度条与状态**(无独立训练页);详情页为**图片与标注一体视图**:分页(每页 20 条)逐行「原图 ‖ 标注图」对照展示;**图片入库(手动上传/AI 生成)自动触发 RF-DETR 四级漏斗标注(全图扫描→空检切片扫描→VLM 提议候选区精修,见技术设计.md「预标注四级漏斗」)**,进度条展示在页顶;页顶另有「全量标注」按钮可手动重标全部图片(覆盖各图已有标注);点击原图/标注图弹窗放大,弹窗为**审核视图(不做手动画框)**:点击框选中,列表可确认疑似框/删除误检框/清空并保存——AI 自动标注结果直接作为标注,人工仅审核确认;封面(上传/生成统一 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` 快照;**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` 逻辑内嵌脚本),自检失败任务置失败并带出原因;`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 模型兜底 |
|
||
| 标注 | **图片入库自动触发**:手动上传/AI 生成成功后,新增图自动调 `config.yml` `localAi` 节点配置的 AI 端点做 RF-DETR 检测(**四级漏斗(2026-09-04)**:全图扫描→空检自动升级切片扫描→仍空 VLM 提议候选区+RF-DETR 精修→全空写空标注等人工,切片块长边/重叠/切片阈值走 `localAi.tileSize`/`tileOverlap`/`tileThreshold`,见技术设计.md「预标注四级漏斗」;`label_task` 记录进度,页顶进度条展示;**localAi 未配置 → 上传/生成接口直接报错;已有标注任务在跑(忙)→ 不报错**,当前任务成功完成后自动补标未标注图)→ 扫描结果(**重叠去重**:NMS 风格按置信度降序保留,重叠比 > `localAi.overlapThreshold` 默认 0.3 的框剔除——重叠比 = 交叠面积/两框较小面积,RF-DETR 同目标常输出一大一小两框,此判据能命中,同目标只留置信度最高者)**直接写 `dataset_image.labels_json`**(覆盖该图已有标注,即重标语义);点击弹窗放大进入**审核视图(不做手动画框)**:点击框选中,列表确认疑似框/删除误检框/清空 → 保存即整体覆写 `dataset_image.labels_json`(YOLO 归一化 JSON 数组,AI 标注直写、人工仅审核确认);`POST /admin/label-tasks` 详情页「全量标注」按钮入口(另有自动触发),可发起全量/指定图重标;自动/手动/混合并存,训练前自动整理(prepare_yolo 逻辑在服务端) |
|
||
|
||
## 架构与数据流
|
||
|
||
```
|
||
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)、`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)、`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/''/'[]'=未标注)、`clean_excluded`(0/1,2026-09-02 数据清洗排除出训练集标记,prepare_yolo 打包跳过,可恢复)、`created_at` |
|
||
| `model_training` | 训练任务 | `id`(PK)、`name`、`status`(queued/running/success/failed;queued=GPU 忙排队中,2026-09-03)、`dataset_id`、`variant`(s/n 档位,default s,2026-09-03)、`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/n,default s;存量行迁移为 s)、`version`(m1.0.0 递增, 同数据集 UNIQUE,s/n 交替发布共用序列)、`training_id`、`metrics`(JSON)、`labels`(JSON 类别名数组)、`sha256`、`size_bytes`、`is_latest`(按 (数据集,档位) 各记一条)、`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` |
|
||
|
||
建表与迁移见 `技术设计.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) |
|
||
| 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:9(1248x704)、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` provider(dashscope / localai,config 切换)逐张生成落盘 + 入库(记录 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"]}`(详情页「全量标注」按钮入口,图片入库亦自动触发):**filenames 缺省=全量**,指定图则只扫描选中图(全量重标/单张修补);调 `config.yml` `localAi` 配置的 AI 端点 RF-DETR 逐张推理(common 池并行,未配置报错),扫描结果做**重叠去重**(NMS 风格按置信度降序保留,重叠比 > `localAi.overlapThreshold` 默认 0.3 的框剔除,同目标只留置信度最高者)后**直写 `dataset_image.labels_json`(重跑覆盖该图标注)**,`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)时拒绝(防重复提交) |
|
||
| 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=杀训练进程置 failed;queued=无进程直接置 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 全量)——无历史标注任务时详情页工作台的数据源 |
|
||
|
||
管理页面由 `server_admin/` 构建产物提供,访问 `http://<host>/admin/`。金额均为整数分,前端展示 ÷100 转元。
|
||
|
||
### 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` 为空数组
|
||
- 下载地址由客户端拼 `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-ai(baseUrl + 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` 唯一约束,重复回调直接忽略)
|
||
- 授权落库以**较晚到期为准**:已有授权未过期时新套餐顺延叠加,禁止覆盖缩短
|