Files
observer/server/biz/dao/dataset_image.go
T
2026-09-07 10:11:36 +08:00

322 lines
12 KiB
Go
Raw 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.
package dao
import (
"context"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv"
"observer-server/biz/consts"
"observer-server/biz/model/entity"
"observer-server/common"
)
// DatasetImage 数据集图片表 DAO:文件名唯一(防重名),按数据集查询;表小不做缓存。
type datasetImageDao struct{}
var DatasetImage = &datasetImageDao{}
func init() {
ctx := context.Background()
_, err := g.DB().Exec(ctx, `CREATE TABLE IF NOT EXISTS dataset_image (
id INTEGER PRIMARY KEY AUTOINCREMENT,
dataset_id INTEGER NOT NULL,
filename TEXT NOT NULL,
source TEXT NOT NULL DEFAULT 'manual',
animal_count INTEGER NOT NULL DEFAULT 0,
labels_json TEXT,
created_at TEXT NOT NULL,
UNIQUE (dataset_id, filename)
)`)
if err != nil {
panic(err)
}
// 存量库迁移:标注列缺失时追加(新库建表已含列,幂等跳过);candidates_json 已随 v10 删除
// 注意 EnsureColumn 的 ddl 参数须自带列名(拼接为 ALTER TABLE ADD COLUMN <ddl>
common.EnsureColumn(ctx, consts.TableDatasetImage, "labels_json", "labels_json TEXT")
common.EnsureColumn(ctx, consts.TableDatasetImage, "animal_count", "animal_count INTEGER NOT NULL DEFAULT 0")
common.EnsureColumn(ctx, consts.TableDatasetImage, "clean_excluded", "clean_excluded INTEGER NOT NULL DEFAULT 0")
common.EnsureColumn(ctx, consts.TableDatasetImage, "review_status", "review_status INTEGER NOT NULL DEFAULT 0")
// 存量库迁移:众包任务图片粒度下发占用标记(2026-09-07,0=未下发)
common.EnsureColumn(ctx, consts.TableDatasetImage, "annotate_task_id", "annotate_task_id INTEGER NOT NULL DEFAULT 0")
// 存量迁移(幂等):加列后既有标注统一视为已审定稿(非空框→2);上线后不变式
// review_status=0 ⟹ labels_json 无框 恒成立(拒绝清框、提交/预标置 1、审核置 2),
// 故本 UPDATE 只会命中存量行,不会误改新数据
if _, err := g.DB().Exec(ctx, `UPDATE dataset_image SET review_status = 2
WHERE review_status = 0 AND labels_json IS NOT NULL AND labels_json != '' AND labels_json != '[]'`); err != nil {
panic(err)
}
// 存量库迁移:删除 prompt 列(生成提示词不再存储,2026-09-02 决策)
cols, err := g.DB().GetAll(ctx, "PRAGMA table_info(dataset_image)")
if err != nil {
panic(err)
}
for _, col := range cols {
if gconv.String(col["name"]) == "prompt" {
if _, err := g.DB().Exec(ctx, "ALTER TABLE dataset_image DROP COLUMN prompt"); err != nil {
panic(err)
}
g.Log().Warningf(ctx, "存量表 dataset_image 已迁移:删除 prompt 列")
break
}
}
}
// Insert 插入图片记录
func (d *datasetImageDao) Insert(ctx context.Context, m *entity.DatasetImage) (int64, error) {
res, err := g.DB().Model(consts.TableDatasetImage).Ctx(ctx).Data(g.Map{
"dataset_id": m.DatasetId,
"filename": m.Filename,
"source": m.Source,
"animal_count": m.AnimalCount,
"created_at": m.CreatedAt,
}).Insert()
if err != nil {
return 0, err
}
return res.LastInsertId()
}
// GetById 按主键取图片记录
func (d *datasetImageDao) GetById(ctx context.Context, id int64) (*entity.DatasetImage, error) {
var one *entity.DatasetImage
err := g.DB().Model(consts.TableDatasetImage).Ctx(ctx).Where("id", id).Scan(&one)
if err != nil {
return nil, err
}
return one, nil
}
// ListByDataset 某数据集全部图片(按文件名编号倒序,详情页逐行展示用)。
// 文件名形如 <前缀>_<编号>.jpg(无前缀为时间戳数字名),须提取下划线后的数字按数值倒序:
// 字符串序会把 pheasant_101.jpg 排在 pheasant_2.jpg 之前('1'<'9'),导致最大编号落到底部
func (d *datasetImageDao) ListByDataset(ctx context.Context, datasetId int64) ([]*entity.DatasetImage, error) {
var list []*entity.DatasetImage
err := g.DB().Model(consts.TableDatasetImage).Ctx(ctx).
Where("dataset_id", datasetId).
Order("CAST(substr(filename, instr(filename, '_') + 1) AS INTEGER) DESC").Scan(&list)
if err != nil {
if common.IsNoRows(err) {
return []*entity.DatasetImage{}, nil
}
return nil, err
}
return list, nil
}
// ListPoolByTask 众包任务池(技术设计.md「App 标注众包与时长激励」;2026-09-07 图片粒度下发):
// 本任务占用(annotate_task_id=taskId)且未审核未清洗排除的图;待审核(1)/已审核(2)不进池——
// 分别等管理端审核与已定稿
func (d *datasetImageDao) ListPoolByTask(ctx context.Context, taskId int64) ([]*entity.DatasetImage, error) {
var list []*entity.DatasetImage
err := g.DB().Model(consts.TableDatasetImage).Ctx(ctx).
Where("annotate_task_id", taskId).
Where("review_status", consts.ReviewImageNone).
Where("clean_excluded", 0).
OrderAsc("id").Scan(&list)
if err != nil {
if common.IsNoRows(err) {
return []*entity.DatasetImage{}, nil
}
return nil, err
}
return list, nil
}
// CountPoolByTaskIds 多任务池余量(一次 GROUP BY;众包任务列表展示用)
func (d *datasetImageDao) CountPoolByTaskIds(ctx context.Context, taskIds []int64) (map[int64]int64, error) {
out := make(map[int64]int64)
if len(taskIds) == 0 {
return out, nil
}
var rows []struct {
AnnotateTaskId int64 `orm:"annotate_task_id"`
Cnt int64 `orm:"cnt"`
}
err := g.DB().Model(consts.TableDatasetImage).Ctx(ctx).
WhereIn("annotate_task_id", taskIds).
Where("review_status", consts.ReviewImageNone).
Where("clean_excluded", 0).
Fields("annotate_task_id, COUNT(*) AS cnt").
Group("annotate_task_id").
Scan(&rows)
if err != nil {
return nil, err
}
for _, r := range rows {
out[r.AnnotateTaskId] = r.Cnt
}
return out, nil
}
// OccupyByTask 下发占用:批量写 annotate_task_idSerial 内调用,防与并发下发竞态;IN ≤100 分批)
func (d *datasetImageDao) OccupyByTask(ctx context.Context, ids []int64, taskId int64) error {
for start := 0; start < len(ids); start += 100 {
end := start + 100
if end > len(ids) {
end = len(ids)
}
if _, err := g.DB().Model(consts.TableDatasetImage).Ctx(ctx).
WhereIn("id", ids[start:end]).
Where("annotate_task_id", 0).
Data(g.Map{"annotate_task_id": taskId}).Update(); err != nil {
return err
}
}
return nil
}
// ReleaseByTask 停用释放:清指定图的任务占用标记回未标注(仅当仍占用本任务,防误清新任务标记;IN ≤100 分批)
func (d *datasetImageDao) ReleaseByTask(ctx context.Context, ids []int64, taskId int64) error {
for start := 0; start < len(ids); start += 100 {
end := start + 100
if end > len(ids) {
end = len(ids)
}
if _, err := g.DB().Model(consts.TableDatasetImage).Ctx(ctx).
WhereIn("id", ids[start:end]).
Where("annotate_task_id", taskId).
Data(g.Map{"annotate_task_id": 0}).Update(); err != nil {
return err
}
}
return nil
}
// GetByIds 按 id 批量取(删除图片定位文件用)
func (d *datasetImageDao) GetByIds(ctx context.Context, ids []int64) ([]*entity.DatasetImage, error) {
var list []*entity.DatasetImage
err := g.DB().Model(consts.TableDatasetImage).Ctx(ctx).WhereIn("id", ids).Scan(&list)
if err != nil {
if common.IsNoRows(err) {
return []*entity.DatasetImage{}, nil
}
return nil, err
}
return list, nil
}
// GetByFilename 按文件名取(图片访问校验归属)
func (d *datasetImageDao) GetByFilename(ctx context.Context, datasetId int64, filename string) (*entity.DatasetImage, error) {
var e entity.DatasetImage
err := g.DB().Model(consts.TableDatasetImage).Ctx(ctx).
Where("dataset_id", datasetId).Where("filename", filename).Scan(&e)
if err != nil {
if common.IsNoRows(err) {
return nil, nil
}
return nil, err
}
return &e, nil
}
// CountByDataset 某数据集图片数
func (d *datasetImageDao) CountByDataset(ctx context.Context, datasetId int64) (int64, error) {
n, err := g.DB().Model(consts.TableDatasetImage).Ctx(ctx).Where("dataset_id", datasetId).Count()
return int64(n), err
}
// UpdateLabels 覆写标注 JSON(整图粒度;空串=清空标注;AI 自动标注与人工保存共用)
func (d *datasetImageDao) UpdateLabels(ctx context.Context, id int64, labelsJson string) error {
_, err := g.DB().Model(consts.TableDatasetImage).Ctx(ctx).Where("id", id).
Data(g.Map{"labels_json": labelsJson}).Update()
return err
}
// UpdateLabelsAndReview 覆写标注 JSON 并同步审核状态(预标/AdminLabelSave/App 提交共用):
// 预标检出/人工保存/App 提交 → 1;人工清空保存 → 0(回到未标注池)
func (d *datasetImageDao) UpdateLabelsAndReview(ctx context.Context, id int64, labelsJson string, reviewStatus int) error {
_, err := g.DB().Model(consts.TableDatasetImage).Ctx(ctx).Where("id", id).
Data(g.Map{"labels_json": labelsJson, "review_status": reviewStatus}).Update()
return err
}
// SetReviewStatus 只改审核状态(审核通过=2 / 拒绝先清标注再置 0,两步由 service 编排)
func (d *datasetImageDao) SetReviewStatus(ctx context.Context, ids []int64, reviewStatus int) error {
if len(ids) == 0 {
return nil
}
_, err := g.DB().Model(consts.TableDatasetImage).Ctx(ctx).WhereIn("id", ids).
Data(g.Map{"review_status": reviewStatus}).Update()
return err
}
// CountLabeledByDataset 某数据集已审核图片数(review_status=22026-09-04 起已标注口径
// = 人工审核通过,labels_json 非空只是必要条件)
func (d *datasetImageDao) CountLabeledByDataset(ctx context.Context, datasetId int64) (int64, error) {
n, err := g.DB().Model(consts.TableDatasetImage).Ctx(ctx).
Where("dataset_id", datasetId).
Where("review_status", consts.ReviewImageApproved).
Count()
return int64(n), err
}
// CountStat 某数据集图片数/已标注数(列表实时统计:冗余计数列会漂移,展示一律以此为准)
type CountStat struct {
Count int64
Labeled int64
}
// CountStatByDatasets 多数据集图片数/已标注数(一次 GROUP BY 查询;IN 按 ≤100 分批)
func (d *datasetImageDao) CountStatByDatasets(ctx context.Context, datasetIds []int64) (map[int64]CountStat, error) {
stat := make(map[int64]CountStat)
for start := 0; start < len(datasetIds); start += 100 {
end := start + 100
if end > len(datasetIds) {
end = len(datasetIds)
}
var rows []struct {
DatasetId int64 `orm:"dataset_id"`
Cnt int64 `orm:"cnt"`
Labeled int64 `orm:"labeled_cnt"`
}
err := g.DB().Model(consts.TableDatasetImage).Ctx(ctx).
WhereIn("dataset_id", datasetIds[start:end]).
Fields("dataset_id, COUNT(*) AS cnt, SUM(CASE WHEN review_status = 2 THEN 1 ELSE 0 END) AS labeled_cnt").
Group("dataset_id").
Scan(&rows)
if err != nil {
return nil, err
}
for _, r := range rows {
stat[r.DatasetId] = CountStat{Count: r.Cnt, Labeled: r.Labeled}
}
}
return stat, nil
}
// DeleteByIds 按 id 批量删除(标注随行删除)
func (d *datasetImageDao) DeleteByIds(ctx context.Context, ids []int64) error {
_, err := g.DB().Model(consts.TableDatasetImage).Ctx(ctx).WhereIn("id", ids).Delete()
return err
}
// DeleteByDataset 删除某数据集全部图片记录(删数据集联动)
func (d *datasetImageDao) DeleteByDataset(ctx context.Context, datasetId int64) error {
_, err := g.DB().Model(consts.TableDatasetImage).Ctx(ctx).Where("dataset_id", datasetId).Delete()
return err
}
// UpdateCleanExcluded 批量置/清数据清洗排除标记(excluded: 1=出训练集, 0=恢复;IN ≤100 分批)
func (d *datasetImageDao) UpdateCleanExcluded(ctx context.Context, ids []int64, excluded int) error {
for start := 0; start < len(ids); start += 100 {
end := start + 100
if end > len(ids) {
end = len(ids)
}
if _, err := g.DB().Model(consts.TableDatasetImage).Ctx(ctx).
WhereIn("id", ids[start:end]).
Data(g.Map{"clean_excluded": excluded}).Update(); err != nil {
return err
}
}
return nil
}
// CountExcludedByDataset 某数据集已排除(clean_excluded=1)图数(数据清洗后角标/计数刷新)
func (d *datasetImageDao) CountExcludedByDataset(ctx context.Context, datasetId int64) (int64, error) {
n, err := g.DB().Model(consts.TableDatasetImage).Ctx(ctx).
Where("dataset_id", datasetId).Where("clean_excluded", 1).Count()
return int64(n), err
}