feat: add image management functionality

- Implemented image folder repository with CRUD operations.
- Added image reference repository for managing image references to articles.
- Created image repository for handling image assets, including listing, inserting, updating, and deleting images.
- Introduced image usage repository to track storage usage and quotas for tenants.
- Added SQL queries for image assets, folders, references, and usage.
- Developed image handler for HTTP endpoints to manage images and folders.
- Created database migration scripts for image-related tables and structures.
This commit is contained in:
2026-04-16 20:40:41 +08:00
parent 0a3558fc51
commit 27389164b0
57 changed files with 8063 additions and 153 deletions
@@ -0,0 +1,8 @@
DROP TABLE IF EXISTS image_asset_references;
DROP TABLE IF EXISTS image_assets;
DROP TABLE IF EXISTS image_folders;
DROP TABLE IF EXISTS tenant_image_storage_usage;
UPDATE plans
SET quota_policy_json = quota_policy_json - 'image_storage_bytes'
WHERE deleted_at IS NULL;
@@ -0,0 +1,70 @@
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE TABLE image_folders (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
name VARCHAR(100) NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uq_image_folders_tenant_name UNIQUE (tenant_id, name),
CONSTRAINT uq_image_folders_tenant_id UNIQUE (tenant_id, id)
);
CREATE INDEX idx_image_folders_tenant ON image_folders (tenant_id);
CREATE TABLE image_assets (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
folder_id BIGINT,
object_key TEXT NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL,
size_bytes BIGINT NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'active',
created_by BIGINT NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
CONSTRAINT fk_image_assets_folder
FOREIGN KEY (tenant_id, folder_id)
REFERENCES image_folders(tenant_id, id),
CONSTRAINT uq_image_assets_tenant_id UNIQUE (tenant_id, id)
);
CREATE INDEX idx_image_assets_tenant_folder ON image_assets (tenant_id, folder_id) WHERE status = 'active' AND deleted_at IS NULL;
CREATE INDEX idx_image_assets_tenant_created ON image_assets (tenant_id, created_at DESC) WHERE status = 'active' AND deleted_at IS NULL;
CREATE INDEX idx_image_assets_tenant_status ON image_assets (tenant_id, status, created_at DESC);
CREATE INDEX idx_image_assets_name_trgm ON image_assets USING GIN (name gin_trgm_ops) WHERE status = 'active' AND deleted_at IS NULL;
CREATE TABLE image_asset_references (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
image_asset_id BIGINT NOT NULL,
article_id BIGINT NOT NULL,
ref_scope VARCHAR(20) NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT fk_image_asset_references_asset
FOREIGN KEY (tenant_id, image_asset_id)
REFERENCES image_assets(tenant_id, id)
ON DELETE CASCADE,
CONSTRAINT uq_image_asset_reference UNIQUE (image_asset_id, article_id, ref_scope)
);
CREATE INDEX idx_image_asset_references_asset ON image_asset_references (tenant_id, image_asset_id);
CREATE INDEX idx_image_asset_references_article ON image_asset_references (tenant_id, article_id);
CREATE TABLE tenant_image_storage_usage (
tenant_id BIGINT PRIMARY KEY,
used_bytes BIGINT NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
UPDATE plans
SET quota_policy_json = quota_policy_json ||
CASE plan_code
WHEN 'free' THEN '{"image_storage_bytes":104857600}'::jsonb
WHEN 'regular' THEN '{"image_storage_bytes":1073741824}'::jsonb
WHEN 'premium' THEN '{"image_storage_bytes":2147483648}'::jsonb
ELSE '{}'::jsonb
END
WHERE deleted_at IS NULL;
@@ -0,0 +1,4 @@
DROP INDEX IF EXISTS idx_image_assets_tenant_content_hash_active;
ALTER TABLE image_assets
DROP COLUMN IF EXISTS content_hash;
@@ -0,0 +1,8 @@
ALTER TABLE image_assets
ADD COLUMN content_hash VARCHAR(64);
CREATE INDEX idx_image_assets_tenant_content_hash_active
ON image_assets (tenant_id, content_hash)
WHERE content_hash IS NOT NULL
AND status = 'active'
AND deleted_at IS NULL;