Scope knowledge and image libraries by brand
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { imagesApi } from '@/lib/api'
|
||||
import { useCompanyStore } from '@/stores/company'
|
||||
import type { ScheduleCoverMode, ScheduleCoverRandomScope } from '@geo/shared-types'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { computed, watch } from 'vue'
|
||||
@@ -24,10 +25,12 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const companyStore = useCompanyStore()
|
||||
const currentBrandId = computed(() => companyStore.currentBrandId)
|
||||
|
||||
const foldersQuery = useQuery({
|
||||
queryKey: ['images', 'folders', 'cover-source'],
|
||||
enabled: computed(() => !props.disabled),
|
||||
queryKey: computed(() => ['images', 'folders', 'cover-source', currentBrandId.value]),
|
||||
enabled: computed(() => !props.disabled && Boolean(currentBrandId.value)),
|
||||
queryFn: () => imagesApi.listFolders(),
|
||||
})
|
||||
|
||||
@@ -38,6 +41,13 @@ const folderOptions = computed(() =>
|
||||
})),
|
||||
)
|
||||
|
||||
watch(
|
||||
currentBrandId,
|
||||
() => {
|
||||
emit('update:randomFolderId', null)
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.mode,
|
||||
(mode) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { imagesApi } from '@/lib/api'
|
||||
import { formatBytes, formatPercentage } from '@/lib/display'
|
||||
import { useCompanyStore } from '@/stores/company'
|
||||
import type { ImageAssetItem } from '@geo/shared-types'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
@@ -20,6 +21,8 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const companyStore = useCompanyStore()
|
||||
const currentBrandId = computed(() => companyStore.currentBrandId)
|
||||
|
||||
// State
|
||||
const selectedFolderId = ref<number | null>(null)
|
||||
@@ -31,7 +34,8 @@ const selectedId = ref<number | null>(null)
|
||||
|
||||
// Queries
|
||||
const foldersQuery = useQuery({
|
||||
queryKey: ['images', 'folders'],
|
||||
queryKey: computed(() => ['images', 'folders', currentBrandId.value]),
|
||||
enabled: computed(() => Boolean(currentBrandId.value)),
|
||||
queryFn: () => imagesApi.listFolders(),
|
||||
})
|
||||
|
||||
@@ -40,6 +44,7 @@ const imagesQuery = useQuery({
|
||||
'images',
|
||||
'list',
|
||||
'picker',
|
||||
currentBrandId.value,
|
||||
{
|
||||
folder_id: selectedFolderId.value,
|
||||
q: debouncedSearch.value,
|
||||
@@ -47,6 +52,7 @@ const imagesQuery = useQuery({
|
||||
page_size: pageSize.value,
|
||||
},
|
||||
]),
|
||||
enabled: computed(() => Boolean(currentBrandId.value)),
|
||||
queryFn: () =>
|
||||
imagesApi.list({
|
||||
folder_id: selectedFolderId.value ?? undefined,
|
||||
@@ -77,6 +83,12 @@ watch(searchKeyword, () => {
|
||||
}, 300)
|
||||
})
|
||||
|
||||
watch(currentBrandId, () => {
|
||||
selectedFolderId.value = null
|
||||
selectedId.value = null
|
||||
currentPage.value = 1
|
||||
})
|
||||
|
||||
function handleSelect(image: ImageAssetItem) {
|
||||
selectedId.value = image.id
|
||||
emit('select', image)
|
||||
|
||||
@@ -5,6 +5,7 @@ import { computed, watch } from 'vue'
|
||||
import MarkdownPreview from '@/components/MarkdownPreview.vue'
|
||||
import { knowledgeApi } from '@/lib/api'
|
||||
import { formatDateTime } from '@/lib/display'
|
||||
import { useCompanyStore } from '@/stores/company'
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
@@ -16,10 +17,12 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const enabled = computed(() => props.open && Boolean(props.itemId))
|
||||
const companyStore = useCompanyStore()
|
||||
const currentBrandId = computed(() => companyStore.currentBrandId)
|
||||
const enabled = computed(() => props.open && Boolean(props.itemId) && Boolean(currentBrandId.value))
|
||||
|
||||
const detailQuery = useQuery({
|
||||
queryKey: computed(() => ['knowledge', 'detail', props.itemId]),
|
||||
queryKey: computed(() => ['knowledge', 'detail', currentBrandId.value, props.itemId]),
|
||||
enabled,
|
||||
queryFn: () => knowledgeApi.detail(props.itemId as number),
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useQuery } from '@tanstack/vue-query'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { knowledgeApi } from '@/lib/api'
|
||||
import { useCompanyStore } from '@/stores/company'
|
||||
|
||||
interface TreeNode {
|
||||
title: string
|
||||
@@ -29,8 +30,12 @@ const emit = defineEmits<{
|
||||
'update:modelValue': [value: number[]]
|
||||
}>()
|
||||
|
||||
const companyStore = useCompanyStore()
|
||||
const currentBrandId = computed(() => companyStore.currentBrandId)
|
||||
|
||||
const groupsQuery = useQuery({
|
||||
queryKey: ['knowledge', 'groups', 'selector'],
|
||||
queryKey: computed(() => ['knowledge', 'groups', 'selector', currentBrandId.value]),
|
||||
enabled: computed(() => Boolean(currentBrandId.value)),
|
||||
queryFn: () => knowledgeApi.listGroups(),
|
||||
})
|
||||
|
||||
|
||||
@@ -355,6 +355,8 @@ const currentBrandScopedPathPatterns = [
|
||||
/^\/api\/tenant\/enterprise-sites\/[^/]+\/publish(?:\/|$)/,
|
||||
/^\/api\/tenant\/media-supply\/orders(?:\/|$)/,
|
||||
/^\/api\/tenant\/prompt-rules(?:\/|$)/,
|
||||
/^\/api\/tenant\/knowledge(?:\/|$)/,
|
||||
/^\/api\/tenant\/images(?:\/|$)/,
|
||||
]
|
||||
|
||||
function shouldAttachCurrentBrandHeader(url: unknown): boolean {
|
||||
@@ -362,6 +364,9 @@ function shouldAttachCurrentBrandHeader(url: unknown): boolean {
|
||||
return false
|
||||
}
|
||||
const pathname = normalizeRequestPath(url)
|
||||
if (pathname === '/api/tenant/images/storage-usage') {
|
||||
return false
|
||||
}
|
||||
return currentBrandScopedPathPatterns.some((pattern) => pattern.test(pathname))
|
||||
}
|
||||
|
||||
|
||||
@@ -18,11 +18,14 @@ import { imagesApi } from '@/lib/api'
|
||||
import { formatBytes, formatDateTime, formatPercentage } from '@/lib/display'
|
||||
import { formatError } from '@/lib/errors'
|
||||
import { IMAGE_UPLOAD_ACCEPT, useImageUploadProgress } from '@/lib/image-webp'
|
||||
import { useCompanyStore } from '@/stores/company'
|
||||
import type { ImageAssetItem, ImageFolder } from '@geo/shared-types'
|
||||
|
||||
const { t } = useI18n()
|
||||
const queryClient = useQueryClient()
|
||||
const imageUploadProgress = useImageUploadProgress()
|
||||
const companyStore = useCompanyStore()
|
||||
const currentBrandId = computed(() => companyStore.currentBrandId)
|
||||
|
||||
// State
|
||||
const selectedFolderId = ref<number | null>(null) // null means "All" or is handled by folders query
|
||||
@@ -51,7 +54,8 @@ const fileInput = ref<HTMLInputElement | null>(null)
|
||||
|
||||
// Queries
|
||||
const foldersQuery = useQuery({
|
||||
queryKey: ['images', 'folders'],
|
||||
queryKey: computed(() => ['images', 'folders', currentBrandId.value]),
|
||||
enabled: computed(() => Boolean(currentBrandId.value)),
|
||||
queryFn: () => imagesApi.listFolders(),
|
||||
})
|
||||
|
||||
@@ -59,6 +63,7 @@ const imagesQuery = useQuery({
|
||||
queryKey: computed(() => [
|
||||
'images',
|
||||
'list',
|
||||
currentBrandId.value,
|
||||
{
|
||||
folder_id: debouncedSearch.value ? undefined : selectedFolderId.value,
|
||||
q: debouncedSearch.value,
|
||||
@@ -66,6 +71,7 @@ const imagesQuery = useQuery({
|
||||
page_size: pageSize.value,
|
||||
},
|
||||
]),
|
||||
enabled: computed(() => Boolean(currentBrandId.value)),
|
||||
queryFn: () =>
|
||||
imagesApi.list({
|
||||
folder_id: debouncedSearch.value ? undefined : (selectedFolderId.value ?? undefined),
|
||||
@@ -76,7 +82,8 @@ const imagesQuery = useQuery({
|
||||
})
|
||||
|
||||
const allImagesCountQuery = useQuery({
|
||||
queryKey: ['images', 'list', 'all-count'],
|
||||
queryKey: computed(() => ['images', 'list', 'all-count', currentBrandId.value]),
|
||||
enabled: computed(() => Boolean(currentBrandId.value)),
|
||||
queryFn: () =>
|
||||
imagesApi.list({
|
||||
page: 1,
|
||||
@@ -181,6 +188,17 @@ watch(searchKeyword, () => {
|
||||
}, 300)
|
||||
})
|
||||
|
||||
watch(currentBrandId, () => {
|
||||
selectedFolderId.value = null
|
||||
movingImage.value = null
|
||||
previewingImage.value = null
|
||||
renamingImage.value = null
|
||||
moveModalOpen.value = false
|
||||
previewModalOpen.value = false
|
||||
renameImageModalOpen.value = false
|
||||
currentPage.value = 1
|
||||
})
|
||||
|
||||
function handleFolderSelect(id: number | null) {
|
||||
if (debouncedSearch.value) return
|
||||
selectedFolderId.value = id
|
||||
|
||||
@@ -16,6 +16,7 @@ import KnowledgeDetailDrawer from '@/components/KnowledgeDetailDrawer.vue'
|
||||
import { knowledgeApi } from '@/lib/api'
|
||||
import { formatDateTime } from '@/lib/display'
|
||||
import { formatError } from '@/lib/errors'
|
||||
import { useCompanyStore } from '@/stores/company'
|
||||
|
||||
type KnowledgeSourceType = 'document' | 'website' | 'text'
|
||||
type KnowledgeGroupLevel = 'root' | 'child'
|
||||
@@ -35,6 +36,8 @@ interface TreeNode {
|
||||
}
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const companyStore = useCompanyStore()
|
||||
const currentBrandId = computed(() => companyStore.currentBrandId)
|
||||
|
||||
const selectedGroupId = ref<number | null>(null)
|
||||
const groupModalOpen = ref(false)
|
||||
@@ -59,12 +62,14 @@ const urlsText = ref('')
|
||||
const textItems = ref([{ name: '', content: '' }])
|
||||
|
||||
const groupsQuery = useQuery({
|
||||
queryKey: ['knowledge', 'groups'],
|
||||
queryKey: computed(() => ['knowledge', 'groups', currentBrandId.value]),
|
||||
enabled: computed(() => Boolean(currentBrandId.value)),
|
||||
queryFn: () => knowledgeApi.listGroups(),
|
||||
})
|
||||
|
||||
const itemsQuery = useQuery({
|
||||
queryKey: computed(() => ['knowledge', 'items', selectedGroupId.value]),
|
||||
queryKey: computed(() => ['knowledge', 'items', currentBrandId.value, selectedGroupId.value]),
|
||||
enabled: computed(() => Boolean(currentBrandId.value)),
|
||||
queryFn: () => knowledgeApi.listItems(selectedGroupId.value ?? undefined),
|
||||
refetchInterval: (query) => {
|
||||
const items = query.state.data as KnowledgeItem[] | undefined
|
||||
@@ -267,6 +272,18 @@ const retryItemMutation = useMutation({
|
||||
onError: (error) => message.error(formatError(error)),
|
||||
})
|
||||
|
||||
watch(
|
||||
currentBrandId,
|
||||
() => {
|
||||
selectedGroupId.value = null
|
||||
groupModalOpen.value = false
|
||||
editingGroup.value = null
|
||||
itemModalOpen.value = false
|
||||
previewItemId.value = null
|
||||
previewDrawerOpen.value = false
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => groupsQuery.data.value,
|
||||
(groups) => {
|
||||
|
||||
@@ -38,6 +38,7 @@ type VectorPoint struct {
|
||||
type SearchRequest struct {
|
||||
Vector []float64
|
||||
TenantID int64
|
||||
BrandID int64
|
||||
GroupIDs []int64
|
||||
Limit int
|
||||
}
|
||||
@@ -193,6 +194,9 @@ func (s *qdrantStore) Search(ctx context.Context, req SearchRequest) ([]SearchPo
|
||||
sdk.NewMatchKeyword("status", "active"),
|
||||
},
|
||||
}
|
||||
if req.BrandID > 0 {
|
||||
filter.Must = append(filter.Must, sdk.NewMatchInt("brand_id", req.BrandID))
|
||||
}
|
||||
groupIDs := normalizeGroupIDs(req.GroupIDs)
|
||||
if len(groupIDs) > 0 {
|
||||
filter.Must = append(filter.Must, sdk.NewMatchInts("group_id", groupIDs...))
|
||||
|
||||
@@ -353,6 +353,7 @@ func (s *ArticleImitationService) executeGeneration(ctx context.Context, job art
|
||||
knowledgeContext, resolveErr := s.knowledge.ResolveContext(
|
||||
ctx,
|
||||
job.TenantID,
|
||||
int64(extractInt(job.InputParams, "brand_id")),
|
||||
knowledgeGroupIDs,
|
||||
buildImitationKnowledgeQuery(job.InputParams),
|
||||
)
|
||||
|
||||
@@ -492,6 +492,7 @@ func (s *ArticleService) loadArticleDetail(ctx context.Context, tenantID, brandI
|
||||
ctx,
|
||||
s.pool,
|
||||
tenantID,
|
||||
brandID,
|
||||
markdownValue,
|
||||
item.CoverImageAssetID,
|
||||
)
|
||||
@@ -628,6 +629,7 @@ func (s *ArticleService) Update(ctx context.Context, id int64, req UpdateArticle
|
||||
ctx,
|
||||
tx,
|
||||
actor.TenantID,
|
||||
brandID,
|
||||
markdown,
|
||||
coverImageAssetID,
|
||||
)
|
||||
@@ -1037,12 +1039,13 @@ func (s *ArticleService) resolveArticleImageMetadata(
|
||||
ctx context.Context,
|
||||
q imageAssetLookupQueryer,
|
||||
tenantID int64,
|
||||
brandID int64,
|
||||
markdown string,
|
||||
coverImageAssetID *int64,
|
||||
) (string, []int64, *int64, error) {
|
||||
referencedImageAssetIDs := extractReferencedImageAssetIDsFromMarkdown(markdown)
|
||||
imageAssetIDsToValidate := collectImageAssetIDsForValidation(referencedImageAssetIDs, coverImageAssetID)
|
||||
activeImageAssetIDs, err := s.loadActiveImageAssetIDSet(ctx, q, tenantID, imageAssetIDsToValidate)
|
||||
activeImageAssetIDs, err := s.loadActiveImageAssetIDSet(ctx, q, tenantID, brandID, imageAssetIDsToValidate)
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
@@ -1064,6 +1067,7 @@ func (s *ArticleService) loadActiveImageAssetIDSet(
|
||||
ctx context.Context,
|
||||
q imageAssetLookupQueryer,
|
||||
tenantID int64,
|
||||
brandID int64,
|
||||
imageAssetIDs []int64,
|
||||
) (map[int64]struct{}, error) {
|
||||
if len(imageAssetIDs) == 0 {
|
||||
@@ -1073,11 +1077,12 @@ func (s *ArticleService) loadActiveImageAssetIDSet(
|
||||
rows, err := q.Query(ctx, `
|
||||
SELECT id
|
||||
FROM image_assets
|
||||
WHERE tenant_id = $1
|
||||
AND id = ANY($2::BIGINT[])
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL
|
||||
`, tenantID, imageAssetIDs)
|
||||
WHERE tenant_id = $1
|
||||
AND brand_id = $2
|
||||
AND id = ANY($3::BIGINT[])
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL
|
||||
`, tenantID, brandID, imageAssetIDs)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50012, "update_failed", "failed to validate image assets")
|
||||
}
|
||||
|
||||
@@ -809,6 +809,7 @@ func (s *EnterpriseSiteService) PublishArticle(ctx context.Context, connectionID
|
||||
contentHTML, coverURL := s.prepareEnterpriseSitePublishAssets(
|
||||
ctx,
|
||||
actor.TenantID,
|
||||
brandID,
|
||||
contentHTML,
|
||||
coverURLInput,
|
||||
coverImageAssetID,
|
||||
@@ -1865,20 +1866,20 @@ func enterpriseSitePublishCoverInput(article enterpriseSitePublishArticle, req P
|
||||
return coverURL, coverImageAssetID
|
||||
}
|
||||
|
||||
func (s *EnterpriseSiteService) prepareEnterpriseSitePublishAssets(ctx context.Context, tenantID int64, contentHTML, coverURL string, coverImageAssetID *int64) (string, string) {
|
||||
func (s *EnterpriseSiteService) prepareEnterpriseSitePublishAssets(ctx context.Context, tenantID int64, brandID int64, contentHTML, coverURL string, coverImageAssetID *int64) (string, string) {
|
||||
base := s.enterpriseSitePublicBaseURL()
|
||||
preparedHTML := s.prepareEnterpriseSiteImageURLs(ctx, tenantID, contentHTML, base)
|
||||
preparedCover := s.prepareEnterpriseSiteCoverURL(ctx, tenantID, coverURL, coverImageAssetID, base)
|
||||
preparedHTML := s.prepareEnterpriseSiteImageURLs(ctx, tenantID, brandID, contentHTML, base)
|
||||
preparedCover := s.prepareEnterpriseSiteCoverURL(ctx, tenantID, brandID, coverURL, coverImageAssetID, base)
|
||||
return preparedHTML, preparedCover
|
||||
}
|
||||
|
||||
func (s *EnterpriseSiteService) prepareEnterpriseSiteCoverURL(ctx context.Context, tenantID int64, coverURL string, coverImageAssetID *int64, baseURL string) string {
|
||||
func (s *EnterpriseSiteService) prepareEnterpriseSiteCoverURL(ctx context.Context, tenantID int64, brandID int64, coverURL string, coverImageAssetID *int64, baseURL string) string {
|
||||
if coverImageAssetID != nil && *coverImageAssetID > 0 {
|
||||
if dataURI := s.enterpriseSiteDataURIFromImageAssetID(ctx, tenantID, *coverImageAssetID); dataURI != "" {
|
||||
if dataURI := s.enterpriseSiteDataURIFromImageAssetID(ctx, tenantID, brandID, *coverImageAssetID); dataURI != "" {
|
||||
return dataURI
|
||||
}
|
||||
}
|
||||
return s.prepareEnterpriseSiteAssetURL(ctx, tenantID, coverURL, baseURL)
|
||||
return s.prepareEnterpriseSiteAssetURL(ctx, tenantID, brandID, coverURL, baseURL)
|
||||
}
|
||||
|
||||
func (s *EnterpriseSiteService) enterpriseSitePublicBaseURL() string {
|
||||
@@ -1918,7 +1919,7 @@ func normalizeEnterpriseSiteAssetBaseURL(value string) string {
|
||||
return strings.TrimRight(parsed.String(), "/")
|
||||
}
|
||||
|
||||
func (s *EnterpriseSiteService) prepareEnterpriseSiteImageURLs(ctx context.Context, tenantID int64, fragment string, baseURL string) string {
|
||||
func (s *EnterpriseSiteService) prepareEnterpriseSiteImageURLs(ctx context.Context, tenantID int64, brandID int64, fragment string, baseURL string) string {
|
||||
if strings.TrimSpace(fragment) == "" || !strings.Contains(strings.ToLower(fragment), "<img") {
|
||||
return fragment
|
||||
}
|
||||
@@ -1929,37 +1930,37 @@ func (s *EnterpriseSiteService) prepareEnterpriseSiteImageURLs(ctx context.Conte
|
||||
}
|
||||
nodes, err := html.ParseFragment(strings.NewReader(fragment), contextNode)
|
||||
if err != nil {
|
||||
return s.prepareEnterpriseSiteImageURLsFallback(ctx, tenantID, fragment, baseURL)
|
||||
return s.prepareEnterpriseSiteImageURLsFallback(ctx, tenantID, brandID, fragment, baseURL)
|
||||
}
|
||||
for _, node := range nodes {
|
||||
s.prepareEnterpriseSiteImageNodeURLs(ctx, tenantID, node, baseURL)
|
||||
s.prepareEnterpriseSiteImageNodeURLs(ctx, tenantID, brandID, node, baseURL)
|
||||
}
|
||||
nodes = enterpriseSitePruneEmptyImageWrapperNodes(nodes)
|
||||
return enterpriseSiteRenderHTMLNodes(nodes, s.prepareEnterpriseSiteImageURLsFallback(ctx, tenantID, fragment, baseURL))
|
||||
return enterpriseSiteRenderHTMLNodes(nodes, s.prepareEnterpriseSiteImageURLsFallback(ctx, tenantID, brandID, fragment, baseURL))
|
||||
}
|
||||
|
||||
func (s *EnterpriseSiteService) prepareEnterpriseSiteImageNodeURLs(ctx context.Context, tenantID int64, node *html.Node, baseURL string) {
|
||||
func (s *EnterpriseSiteService) prepareEnterpriseSiteImageNodeURLs(ctx context.Context, tenantID int64, brandID int64, node *html.Node, baseURL string) {
|
||||
if node == nil {
|
||||
return
|
||||
}
|
||||
if node.Type == html.ElementNode && strings.EqualFold(node.Data, "img") {
|
||||
if !s.prepareEnterpriseSiteImageAttrs(ctx, tenantID, node, baseURL) {
|
||||
if !s.prepareEnterpriseSiteImageAttrs(ctx, tenantID, brandID, node, baseURL) {
|
||||
enterpriseSiteRemoveHTMLNode(node)
|
||||
return
|
||||
}
|
||||
}
|
||||
for child := node.FirstChild; child != nil; {
|
||||
next := child.NextSibling
|
||||
s.prepareEnterpriseSiteImageNodeURLs(ctx, tenantID, child, baseURL)
|
||||
s.prepareEnterpriseSiteImageNodeURLs(ctx, tenantID, brandID, child, baseURL)
|
||||
child = next
|
||||
}
|
||||
}
|
||||
|
||||
func (s *EnterpriseSiteService) prepareEnterpriseSiteImageAttrs(ctx context.Context, tenantID int64, node *html.Node, baseURL string) bool {
|
||||
func (s *EnterpriseSiteService) prepareEnterpriseSiteImageAttrs(ctx context.Context, tenantID int64, brandID int64, node *html.Node, baseURL string) bool {
|
||||
if node == nil {
|
||||
return true
|
||||
}
|
||||
dataURI := s.enterpriseSiteDataURIFromImageNode(ctx, tenantID, node)
|
||||
dataURI := s.enterpriseSiteDataURIFromImageNode(ctx, tenantID, brandID, node)
|
||||
hasImageURL := false
|
||||
for idx := range node.Attr {
|
||||
key := strings.ToLower(strings.TrimSpace(node.Attr[idx].Key))
|
||||
@@ -1971,7 +1972,7 @@ func (s *EnterpriseSiteService) prepareEnterpriseSiteImageAttrs(ctx context.Cont
|
||||
node.Attr[idx].Val = dataURI
|
||||
continue
|
||||
}
|
||||
node.Attr[idx].Val = s.prepareEnterpriseSiteAssetURL(ctx, tenantID, node.Attr[idx].Val, baseURL)
|
||||
node.Attr[idx].Val = s.prepareEnterpriseSiteAssetURL(ctx, tenantID, brandID, node.Attr[idx].Val, baseURL)
|
||||
if strings.TrimSpace(node.Attr[idx].Val) == "" {
|
||||
return false
|
||||
}
|
||||
@@ -2046,12 +2047,12 @@ func enterpriseSitePruneEmptyImageWrapperNodes(nodes []*html.Node) []*html.Node
|
||||
return pruned
|
||||
}
|
||||
|
||||
func (s *EnterpriseSiteService) prepareEnterpriseSiteAssetURL(ctx context.Context, tenantID int64, value string, baseURL string) string {
|
||||
func (s *EnterpriseSiteService) prepareEnterpriseSiteAssetURL(ctx context.Context, tenantID int64, brandID int64, value string, baseURL string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" || strings.HasPrefix(strings.ToLower(value), "data:") {
|
||||
return value
|
||||
}
|
||||
if dataURI := s.enterpriseSiteDataURIFromAssetURL(ctx, tenantID, value); dataURI != "" {
|
||||
if dataURI := s.enterpriseSiteDataURIFromAssetURL(ctx, tenantID, brandID, value); dataURI != "" {
|
||||
return dataURI
|
||||
}
|
||||
if enterpriseSiteLooksLikeSaaSAssetURL(value) {
|
||||
@@ -2060,34 +2061,34 @@ func (s *EnterpriseSiteService) prepareEnterpriseSiteAssetURL(ctx context.Contex
|
||||
return enterpriseSiteAbsoluteAssetURL(value, baseURL)
|
||||
}
|
||||
|
||||
func (s *EnterpriseSiteService) prepareEnterpriseSiteImageURLsFallback(ctx context.Context, tenantID int64, fragment string, baseURL string) string {
|
||||
func (s *EnterpriseSiteService) prepareEnterpriseSiteImageURLsFallback(ctx context.Context, tenantID int64, brandID int64, fragment string, baseURL string) string {
|
||||
return enterpriseSiteImageAttrPattern.ReplaceAllStringFunc(fragment, func(match string) string {
|
||||
parts := enterpriseSiteImageAttrPattern.FindStringSubmatch(match)
|
||||
if len(parts) != 4 {
|
||||
return match
|
||||
}
|
||||
return parts[1] + s.prepareEnterpriseSiteAssetURL(ctx, tenantID, parts[2], baseURL) + parts[3]
|
||||
return parts[1] + s.prepareEnterpriseSiteAssetURL(ctx, tenantID, brandID, parts[2], baseURL) + parts[3]
|
||||
})
|
||||
}
|
||||
|
||||
func (s *EnterpriseSiteService) enterpriseSiteDataURIFromAssetURL(ctx context.Context, tenantID int64, value string) string {
|
||||
objectKey, ok := s.enterpriseSiteObjectKeyFromAssetURL(ctx, tenantID, value)
|
||||
func (s *EnterpriseSiteService) enterpriseSiteDataURIFromAssetURL(ctx context.Context, tenantID int64, brandID int64, value string) string {
|
||||
objectKey, ok := s.enterpriseSiteObjectKeyFromAssetURL(ctx, tenantID, brandID, value)
|
||||
if !ok || s == nil || s.storage == nil {
|
||||
return ""
|
||||
}
|
||||
return s.enterpriseSiteDataURIFromObjectKey(ctx, objectKey)
|
||||
}
|
||||
|
||||
func (s *EnterpriseSiteService) enterpriseSiteDataURIFromImageNode(ctx context.Context, tenantID int64, node *html.Node) string {
|
||||
func (s *EnterpriseSiteService) enterpriseSiteDataURIFromImageNode(ctx context.Context, tenantID int64, brandID int64, node *html.Node) string {
|
||||
assetID := enterpriseSiteImageNodeAssetID(node)
|
||||
if assetID <= 0 {
|
||||
return ""
|
||||
}
|
||||
return s.enterpriseSiteDataURIFromImageAssetID(ctx, tenantID, assetID)
|
||||
return s.enterpriseSiteDataURIFromImageAssetID(ctx, tenantID, brandID, assetID)
|
||||
}
|
||||
|
||||
func (s *EnterpriseSiteService) enterpriseSiteDataURIFromImageAssetID(ctx context.Context, tenantID, assetID int64) string {
|
||||
objectKey, ok := s.loadEnterpriseSiteImageAssetObjectKey(ctx, tenantID, assetID)
|
||||
func (s *EnterpriseSiteService) enterpriseSiteDataURIFromImageAssetID(ctx context.Context, tenantID, brandID, assetID int64) string {
|
||||
objectKey, ok := s.loadEnterpriseSiteImageAssetObjectKey(ctx, tenantID, brandID, assetID)
|
||||
if !ok || s == nil || s.storage == nil {
|
||||
return ""
|
||||
}
|
||||
@@ -2143,27 +2144,54 @@ func enterpriseSiteImageNodeAssetID(node *html.Node) int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (s *EnterpriseSiteService) enterpriseSiteObjectKeyFromAssetURL(ctx context.Context, tenantID int64, value string) (string, bool) {
|
||||
func (s *EnterpriseSiteService) enterpriseSiteObjectKeyFromAssetURL(ctx context.Context, tenantID int64, brandID int64, value string) (string, bool) {
|
||||
if objectKey, ok := enterpriseSiteObjectKeyFromPublicAssetURL(value, tenantID, s.enterpriseSiteAssetTokenSecret()); ok {
|
||||
if !s.enterpriseSiteImageObjectKeyBelongsToBrand(ctx, tenantID, brandID, objectKey) {
|
||||
return "", false
|
||||
}
|
||||
return objectKey, true
|
||||
}
|
||||
assetID, ok := enterpriseSiteAssetIDFromURL(value)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
return s.loadEnterpriseSiteImageAssetObjectKey(ctx, tenantID, assetID)
|
||||
return s.loadEnterpriseSiteImageAssetObjectKey(ctx, tenantID, brandID, assetID)
|
||||
}
|
||||
|
||||
func (s *EnterpriseSiteService) loadEnterpriseSiteImageAssetObjectKey(ctx context.Context, tenantID, assetID int64) (string, bool) {
|
||||
if s == nil || s.pool == nil || tenantID <= 0 || assetID <= 0 {
|
||||
func (s *EnterpriseSiteService) enterpriseSiteImageObjectKeyBelongsToBrand(ctx context.Context, tenantID, brandID int64, objectKey string) bool {
|
||||
if s == nil || s.pool == nil || !enterpriseSiteObjectKeyIsImageLibraryAsset(objectKey, tenantID) {
|
||||
return true
|
||||
}
|
||||
if tenantID <= 0 || brandID <= 0 || strings.TrimSpace(objectKey) == "" {
|
||||
return false
|
||||
}
|
||||
var exists bool
|
||||
if err := s.pool.QueryRow(ctx, `
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM image_assets
|
||||
WHERE tenant_id = $1
|
||||
AND brand_id = $2
|
||||
AND object_key = $3
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL
|
||||
)
|
||||
`, tenantID, brandID, objectKey).Scan(&exists); err != nil {
|
||||
return false
|
||||
}
|
||||
return exists
|
||||
}
|
||||
|
||||
func (s *EnterpriseSiteService) loadEnterpriseSiteImageAssetObjectKey(ctx context.Context, tenantID, brandID, assetID int64) (string, bool) {
|
||||
if s == nil || s.pool == nil || tenantID <= 0 || brandID <= 0 || assetID <= 0 {
|
||||
return "", false
|
||||
}
|
||||
var objectKey string
|
||||
if err := s.pool.QueryRow(ctx, `
|
||||
SELECT object_key
|
||||
FROM image_assets
|
||||
WHERE id = $1 AND tenant_id = $2 AND status = 'active' AND deleted_at IS NULL
|
||||
`, assetID, tenantID).Scan(&objectKey); err != nil {
|
||||
SELECT object_key
|
||||
FROM image_assets
|
||||
WHERE id = $1 AND tenant_id = $2 AND brand_id = $3 AND status = 'active' AND deleted_at IS NULL
|
||||
`, assetID, tenantID, brandID).Scan(&objectKey); err != nil {
|
||||
return "", false
|
||||
}
|
||||
if !enterpriseSiteObjectKeyAllowedForTenant(objectKey, tenantID) {
|
||||
@@ -2264,6 +2292,14 @@ func enterpriseSiteObjectKeyAllowedForTenant(objectKey string, tenantID int64) b
|
||||
strings.HasPrefix(objectKey, tenantPrefix+"articles/")
|
||||
}
|
||||
|
||||
func enterpriseSiteObjectKeyIsImageLibraryAsset(objectKey string, tenantID int64) bool {
|
||||
if tenantID <= 0 {
|
||||
return false
|
||||
}
|
||||
tenantPrefix := "tenants/" + strconv.FormatInt(tenantID, 10) + "/"
|
||||
return strings.HasPrefix(strings.TrimSpace(objectKey), tenantPrefix+"images/")
|
||||
}
|
||||
|
||||
func enterpriseSiteAbsoluteImageURLs(fragment string, baseURL string) string {
|
||||
if strings.TrimSpace(fragment) == "" || !strings.Contains(strings.ToLower(fragment), "<img") {
|
||||
return fragment
|
||||
|
||||
@@ -245,6 +245,7 @@ func TestPrepareEnterpriseSiteImageURLsEmbedsPublicAssetImages(t *testing.T) {
|
||||
got := svc.prepareEnterpriseSiteImageURLs(
|
||||
context.Background(),
|
||||
7,
|
||||
11,
|
||||
`<p><img src="/api/public/assets/`+token+`" data-src="/api/public/assets/`+token+`" /></p>`,
|
||||
"http://127.0.0.1:8080",
|
||||
)
|
||||
@@ -271,6 +272,7 @@ func TestPrepareEnterpriseSiteImageURLsEmbedsPublicAssetImagesWithStaleSignature
|
||||
got := svc.prepareEnterpriseSiteImageURLs(
|
||||
context.Background(),
|
||||
7,
|
||||
11,
|
||||
`<p><img src="http://localhost:5178/api/public/assets/`+token+`" /></p>`,
|
||||
"http://127.0.0.1:8080",
|
||||
)
|
||||
@@ -297,6 +299,7 @@ func TestPrepareEnterpriseSiteImageURLsStripsSaaSImageAttrs(t *testing.T) {
|
||||
got := svc.prepareEnterpriseSiteImageURLs(
|
||||
context.Background(),
|
||||
7,
|
||||
11,
|
||||
`<p><img src="/api/public/assets/`+token+`" alt="" data-asset-id="35" asset-id="35" /></p>`,
|
||||
"http://127.0.0.1:8080",
|
||||
)
|
||||
@@ -320,6 +323,7 @@ func TestPrepareEnterpriseSiteImageURLsDropsUnavailableSaaSAssetImage(t *testing
|
||||
got := svc.prepareEnterpriseSiteImageURLs(
|
||||
context.Background(),
|
||||
7,
|
||||
11,
|
||||
`<p>前文</p><p class="article-editor-image article-editor-image--center" align="center"><img src="http://localhost:5178/api/public/assets/`+token+`" alt="" /></p><p>后文</p>`,
|
||||
"http://127.0.0.1:8080",
|
||||
)
|
||||
@@ -346,6 +350,7 @@ func TestPrepareEnterpriseSiteImageURLsEmbedsIncompletePublicAssetPaths(t *testi
|
||||
got := svc.prepareEnterpriseSiteImageURLs(
|
||||
context.Background(),
|
||||
7,
|
||||
11,
|
||||
`<p><img src="api/public/assets/`+token+`?format=png" /></p>`,
|
||||
"http://127.0.0.1:8080",
|
||||
)
|
||||
@@ -373,7 +378,7 @@ func TestMarkdownRenderedEnterpriseSiteImageEmbedsIncompletePublicAssetPath(t *t
|
||||
t.Fatalf("render markdown: %v", err)
|
||||
}
|
||||
|
||||
got := svc.prepareEnterpriseSiteImageURLs(context.Background(), 7, html, "http://127.0.0.1:8080")
|
||||
got := svc.prepareEnterpriseSiteImageURLs(context.Background(), 7, 11, html, "http://127.0.0.1:8080")
|
||||
|
||||
if storage.gotKey != objectKey {
|
||||
t.Fatalf("object key = %q, want %q", storage.gotKey, objectKey)
|
||||
@@ -398,6 +403,7 @@ func TestPrepareEnterpriseSitePublishAssetsEmbedsIncompleteCoverAssetPath(t *tes
|
||||
got := svc.prepareEnterpriseSiteCoverURL(
|
||||
context.Background(),
|
||||
7,
|
||||
11,
|
||||
"api/public/assets/"+token+"?format=png",
|
||||
nil,
|
||||
"http://127.0.0.1:8080",
|
||||
@@ -423,6 +429,7 @@ func TestPrepareEnterpriseSiteCoverURLDropsUnavailableSaaSAssetURL(t *testing.T)
|
||||
got := svc.prepareEnterpriseSiteCoverURL(
|
||||
context.Background(),
|
||||
7,
|
||||
11,
|
||||
"http://localhost:5178/api/public/assets/"+token,
|
||||
nil,
|
||||
"http://127.0.0.1:8080",
|
||||
|
||||
@@ -376,6 +376,7 @@ func (s *PromptRuleGenerationService) ProcessQueuedTask(ctx context.Context, tas
|
||||
knowledgeContext, err := s.knowledge.ResolveContext(
|
||||
ctx,
|
||||
task.TenantID,
|
||||
int64(extractInt(inputParams, "brand_id")),
|
||||
knowledgeGroupIDs,
|
||||
buildPromptRuleKnowledgeQuery(rule, inputParams),
|
||||
)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/shared/auth"
|
||||
"github.com/geo-platform/tenant-api/internal/shared/messaging/rabbitmq"
|
||||
"github.com/geo-platform/tenant-api/internal/shared/middleware"
|
||||
"github.com/geo-platform/tenant-api/internal/shared/objectstorage"
|
||||
@@ -48,9 +49,13 @@ type ImageReferenceArticleBrief struct {
|
||||
|
||||
func (s *ImageFolderService) List(ctx context.Context) ([]ImageFolderItem, error) {
|
||||
tenantID := middleware.TenantIDFromCtx(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repo := repository.NewImageFolderRepository(s.pool)
|
||||
|
||||
folders, err := repo.List(ctx, tenantID)
|
||||
folders, err := repo.List(ctx, tenantID, brandID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -77,11 +82,15 @@ func (s *ImageFolderService) Create(ctx context.Context, name string) (int64, er
|
||||
}
|
||||
|
||||
tenantID := middleware.TenantIDFromCtx(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
repo := repository.NewImageFolderRepository(s.pool)
|
||||
|
||||
id, err := repo.Create(ctx, tenantID, name)
|
||||
id, err := repo.Create(ctx, tenantID, brandID, name)
|
||||
if err != nil {
|
||||
if isConstraintError(err, "uq_image_folders_tenant_name") {
|
||||
if isConstraintError(err, "uq_image_folders_tenant_name") || isConstraintError(err, "uq_image_folders_tenant_brand_name") {
|
||||
return 0, response.ErrConflict(40901, "folder_name_exists", "a folder with this name already exists")
|
||||
}
|
||||
return 0, err
|
||||
@@ -96,13 +105,17 @@ func (s *ImageFolderService) Update(ctx context.Context, id int64, name string)
|
||||
}
|
||||
|
||||
tenantID := middleware.TenantIDFromCtx(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
repo := repository.NewImageFolderRepository(s.pool)
|
||||
if _, err := repo.Get(ctx, tenantID, id); err != nil {
|
||||
if _, err := repo.Get(ctx, tenantID, brandID, id); err != nil {
|
||||
return mapImageFolderNotFound(err)
|
||||
}
|
||||
|
||||
if err := repo.Update(ctx, tenantID, id, name); err != nil {
|
||||
if isConstraintError(err, "uq_image_folders_tenant_name") {
|
||||
if err := repo.Update(ctx, tenantID, brandID, id, name); err != nil {
|
||||
if isConstraintError(err, "uq_image_folders_tenant_name") || isConstraintError(err, "uq_image_folders_tenant_brand_name") {
|
||||
return response.ErrConflict(40901, "folder_name_exists", "a folder with this name already exists")
|
||||
}
|
||||
return err
|
||||
@@ -112,13 +125,17 @@ func (s *ImageFolderService) Update(ctx context.Context, id int64, name string)
|
||||
|
||||
func (s *ImageFolderService) DeletePreview(ctx context.Context, folderID int64) (*FolderDeletePreview, error) {
|
||||
tenantID := middleware.TenantIDFromCtx(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
folderRepo := repository.NewImageFolderRepository(s.pool)
|
||||
|
||||
if _, err := folderRepo.Get(ctx, tenantID, folderID); err != nil {
|
||||
if _, err := folderRepo.Get(ctx, tenantID, brandID, folderID); err != nil {
|
||||
return nil, mapImageFolderNotFound(err)
|
||||
}
|
||||
|
||||
imageCount, err := folderRepo.CountActiveImages(ctx, tenantID, folderID)
|
||||
imageCount, err := folderRepo.CountActiveImages(ctx, tenantID, brandID, folderID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -131,19 +148,23 @@ func (s *ImageFolderService) DeletePreview(ctx context.Context, folderID int64)
|
||||
|
||||
func (s *ImageFolderService) Delete(ctx context.Context, folderID int64, force bool) error {
|
||||
tenantID := middleware.TenantIDFromCtx(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
folderRepo := repository.NewImageFolderRepository(s.pool)
|
||||
imageRepo := repository.NewImageRepository(s.pool)
|
||||
|
||||
if _, err := folderRepo.Get(ctx, tenantID, folderID); err != nil {
|
||||
if _, err := folderRepo.Get(ctx, tenantID, brandID, folderID); err != nil {
|
||||
return mapImageFolderNotFound(err)
|
||||
}
|
||||
|
||||
imageCount, err := folderRepo.CountActiveImages(ctx, tenantID, folderID)
|
||||
imageCount, err := folderRepo.CountActiveImages(ctx, tenantID, brandID, folderID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if imageCount == 0 {
|
||||
return folderRepo.Delete(ctx, tenantID, folderID)
|
||||
return folderRepo.Delete(ctx, tenantID, brandID, folderID)
|
||||
}
|
||||
|
||||
if !force {
|
||||
@@ -159,7 +180,7 @@ func (s *ImageFolderService) Delete(ctx context.Context, folderID int64, force b
|
||||
}
|
||||
}
|
||||
|
||||
images, err := imageRepo.ListActiveByFolder(ctx, tenantID, folderID)
|
||||
images, err := imageRepo.ListActiveByFolder(ctx, tenantID, brandID, folderID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -174,10 +195,10 @@ func (s *ImageFolderService) Delete(ctx context.Context, folderID int64, force b
|
||||
|
||||
imageRepoTx := repository.NewImageRepository(tx)
|
||||
folderRepoTx := repository.NewImageFolderRepository(tx)
|
||||
if err := imageRepoTx.MarkImagesInFolderPendingDelete(ctx, tenantID, folderID); err != nil {
|
||||
if err := imageRepoTx.MarkImagesInFolderPendingDelete(ctx, tenantID, brandID, folderID); err != nil {
|
||||
return response.ErrInternal(50048, "folder_image_delete_prepare_failed", "failed to prepare folder images for deletion")
|
||||
}
|
||||
if err := folderRepoTx.Delete(ctx, tenantID, folderID); err != nil {
|
||||
if err := folderRepoTx.Delete(ctx, tenantID, brandID, folderID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
@@ -185,7 +206,8 @@ func (s *ImageFolderService) Delete(ctx context.Context, folderID int64, force b
|
||||
}
|
||||
|
||||
for _, image := range images {
|
||||
if err := s.imageSvc.finalizeDeleteNowOrEnqueue(ctx, tenantID, image.ID); err != nil {
|
||||
deleteCtx := auth.WithCurrentBrandID(ctx, brandID)
|
||||
if err := s.imageSvc.finalizeDeleteNowOrEnqueue(deleteCtx, tenantID, image.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,9 +158,13 @@ func (s *ImageService) Upload(ctx context.Context, fileName string, content []by
|
||||
}
|
||||
|
||||
tenantID := middleware.TenantIDFromCtx(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
actor := auth.MustActor(ctx)
|
||||
if folderID != nil {
|
||||
if err := s.ensureFolderExists(ctx, tenantID, *folderID); err != nil {
|
||||
if err := s.ensureFolderExists(ctx, tenantID, brandID, *folderID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -181,7 +185,7 @@ func (s *ImageService) Upload(ctx context.Context, fileName string, content []by
|
||||
|
||||
sizeBytes := int64(len(webpBytes))
|
||||
contentHash := hashImageContent(webpBytes)
|
||||
existingAsset, err := s.findActiveImageAssetByContentHash(ctx, tenantID, contentHash)
|
||||
existingAsset, err := s.findActiveImageAssetByContentHash(ctx, tenantID, brandID, contentHash)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50012, "image_asset_lookup_failed", "failed to check existing image asset")
|
||||
}
|
||||
@@ -219,9 +223,9 @@ func (s *ImageService) Upload(ctx context.Context, fileName string, content []by
|
||||
return nil, response.ErrInternal(50015, "image_quota_reserve_failed", "failed to reserve image storage quota")
|
||||
}
|
||||
|
||||
assetID, err := s.insertPendingImageAsset(ctx, tx, tenantID, folderID, objectKey, displayName, sizeBytes, contentHash, actor.UserID)
|
||||
assetID, err := s.insertPendingImageAsset(ctx, tx, tenantID, brandID, folderID, objectKey, displayName, sizeBytes, contentHash, actor.UserID)
|
||||
if err != nil {
|
||||
if isConstraintError(err, "fk_image_assets_folder") {
|
||||
if isConstraintError(err, "fk_image_assets_folder") || isConstraintError(err, "fk_image_assets_folder_brand") {
|
||||
return nil, response.ErrNotFound(40402, "image_folder_not_found", "folder not found")
|
||||
}
|
||||
return nil, response.ErrInternal(50016, "image_asset_create_failed", "failed to create image asset")
|
||||
@@ -241,7 +245,7 @@ func (s *ImageService) Upload(ctx context.Context, fileName string, content []by
|
||||
}
|
||||
|
||||
imageRepo := repository.NewImageRepository(s.pool)
|
||||
if err := imageRepo.UpdateStatus(ctx, tenantID, assetID, "active"); err != nil {
|
||||
if err := imageRepo.UpdateStatus(ctx, tenantID, brandID, assetID, "active"); err != nil {
|
||||
s.logError("failed to mark image active after upload", zap.Int64("tenant_id", tenantID), zap.Int64("asset_id", assetID), zap.Error(err))
|
||||
if queueErr := s.enqueueFinalizeUploadTask(tenantID, assetID); queueErr == nil {
|
||||
return &ImageUploadResult{
|
||||
@@ -282,9 +286,14 @@ func (s *ImageService) List(ctx context.Context, folderID *int64, query *string,
|
||||
}
|
||||
|
||||
tenantID := middleware.TenantIDFromCtx(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repo := repository.NewImageRepository(s.pool)
|
||||
assets, total, err := repo.List(ctx, repository.ImageAssetListParams{
|
||||
TenantID: tenantID,
|
||||
BrandID: brandID,
|
||||
FolderID: folderID,
|
||||
SearchQuery: searchQuery,
|
||||
PageSize: pageSize,
|
||||
@@ -323,9 +332,13 @@ func (s *ImageService) Update(ctx context.Context, id int64, req ImageUpdateRequ
|
||||
}
|
||||
|
||||
tenantID := middleware.TenantIDFromCtx(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
repo := repository.NewImageRepository(s.pool)
|
||||
|
||||
asset, err := repo.Get(ctx, tenantID, id)
|
||||
asset, err := repo.Get(ctx, tenantID, brandID, id)
|
||||
if err != nil {
|
||||
return mapImageNotFound(err)
|
||||
}
|
||||
@@ -341,7 +354,7 @@ func (s *ImageService) Update(ctx context.Context, id int64, req ImageUpdateRequ
|
||||
if len(name) > 255 {
|
||||
return response.ErrBadRequest(40002, "invalid_name", "name must be at most 255 characters")
|
||||
}
|
||||
if err := repo.UpdateName(ctx, tenantID, id, name); err != nil {
|
||||
if err := repo.UpdateName(ctx, tenantID, brandID, id, name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -352,12 +365,12 @@ func (s *ImageService) Update(ctx context.Context, id int64, req ImageUpdateRequ
|
||||
return err
|
||||
}
|
||||
if folderID != nil {
|
||||
if err := s.ensureFolderExists(ctx, tenantID, *folderID); err != nil {
|
||||
if err := s.ensureFolderExists(ctx, tenantID, brandID, *folderID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := repo.UpdateFolder(ctx, tenantID, id, folderID); err != nil {
|
||||
if isConstraintError(err, "fk_image_assets_folder") {
|
||||
if err := repo.UpdateFolder(ctx, tenantID, brandID, id, folderID); err != nil {
|
||||
if isConstraintError(err, "fk_image_assets_folder") || isConstraintError(err, "fk_image_assets_folder_brand") {
|
||||
return response.ErrNotFound(40402, "image_folder_not_found", "folder not found")
|
||||
}
|
||||
return err
|
||||
@@ -374,7 +387,7 @@ type existingImageAsset struct {
|
||||
SizeBytes int64
|
||||
}
|
||||
|
||||
func (s *ImageService) findActiveImageAssetByContentHash(ctx context.Context, tenantID int64, contentHash string) (*existingImageAsset, error) {
|
||||
func (s *ImageService) findActiveImageAssetByContentHash(ctx context.Context, tenantID, brandID int64, contentHash string) (*existingImageAsset, error) {
|
||||
if strings.TrimSpace(contentHash) == "" {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -384,12 +397,13 @@ func (s *ImageService) findActiveImageAssetByContentHash(ctx context.Context, te
|
||||
SELECT id, object_key, name, size_bytes
|
||||
FROM image_assets
|
||||
WHERE tenant_id = $1
|
||||
AND content_hash = $2
|
||||
AND brand_id = $2
|
||||
AND content_hash = $3
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
`, tenantID, contentHash).Scan(&asset.ID, &asset.ObjectKey, &asset.Name, &asset.SizeBytes)
|
||||
`, tenantID, brandID, contentHash).Scan(&asset.ID, &asset.ObjectKey, &asset.Name, &asset.SizeBytes)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
@@ -404,6 +418,7 @@ func (s *ImageService) insertPendingImageAsset(
|
||||
ctx context.Context,
|
||||
tx pgx.Tx,
|
||||
tenantID int64,
|
||||
brandID int64,
|
||||
folderID *int64,
|
||||
objectKey string,
|
||||
name string,
|
||||
@@ -412,10 +427,10 @@ func (s *ImageService) insertPendingImageAsset(
|
||||
createdBy int64,
|
||||
) (int64, error) {
|
||||
row := tx.QueryRow(ctx, `
|
||||
INSERT INTO image_assets (tenant_id, folder_id, object_key, name, size_bytes, status, content_hash, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5, 'pending_upload', $6, $7)
|
||||
INSERT INTO image_assets (tenant_id, brand_id, folder_id, object_key, name, size_bytes, status, content_hash, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, 'pending_upload', $7, $8)
|
||||
RETURNING id
|
||||
`, tenantID, nullableImageFolderValue(folderID), objectKey, name, sizeBytes, contentHash, createdBy)
|
||||
`, tenantID, brandID, nullableImageFolderValue(folderID), objectKey, name, sizeBytes, contentHash, createdBy)
|
||||
|
||||
var assetID int64
|
||||
if err := row.Scan(&assetID); err != nil {
|
||||
@@ -426,10 +441,14 @@ RETURNING id
|
||||
|
||||
func (s *ImageService) GetReferences(ctx context.Context, imageID int64) (*ImageReferenceDetail, error) {
|
||||
tenantID := middleware.TenantIDFromCtx(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
imageRepo := repository.NewImageRepository(s.pool)
|
||||
refRepo := repository.NewImageReferenceRepository(s.pool)
|
||||
|
||||
asset, err := imageRepo.Get(ctx, tenantID, imageID)
|
||||
asset, err := imageRepo.Get(ctx, tenantID, brandID, imageID)
|
||||
if err != nil {
|
||||
return nil, mapImageNotFound(err)
|
||||
}
|
||||
@@ -494,10 +513,14 @@ func (s *ImageService) Delete(ctx context.Context, id int64, force bool) error {
|
||||
}
|
||||
|
||||
tenantID := middleware.TenantIDFromCtx(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
imageRepo := repository.NewImageRepository(s.pool)
|
||||
refRepo := repository.NewImageReferenceRepository(s.pool)
|
||||
|
||||
asset, err := imageRepo.Get(ctx, tenantID, id)
|
||||
asset, err := imageRepo.Get(ctx, tenantID, brandID, id)
|
||||
if err != nil {
|
||||
return mapImageNotFound(err)
|
||||
}
|
||||
@@ -515,7 +538,7 @@ func (s *ImageService) Delete(ctx context.Context, id int64, force bool) error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := imageRepo.UpdateStatus(ctx, tenantID, id, "pending_delete"); err != nil {
|
||||
if err := imageRepo.UpdateStatus(ctx, tenantID, brandID, id, "pending_delete"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -556,7 +579,13 @@ func (s *ImageService) rollbackUpload(ctx context.Context, tenantID, assetID, si
|
||||
imageRepo := repository.NewImageRepository(s.pool)
|
||||
usageRepo := repository.NewImageUsageRepository(s.pool)
|
||||
|
||||
if err := imageRepo.UpdateStatus(ctx, tenantID, assetID, "upload_failed"); err != nil {
|
||||
brandID, err := s.lookupImageAssetBrandID(ctx, tenantID, assetID)
|
||||
if err != nil {
|
||||
s.logError("failed to resolve image brand after upload rollback", zap.Int64("tenant_id", tenantID), zap.Int64("asset_id", assetID), zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := imageRepo.UpdateStatus(ctx, tenantID, brandID, assetID, "upload_failed"); err != nil {
|
||||
s.logError("failed to mark image upload as failed", zap.Int64("tenant_id", tenantID), zap.Int64("asset_id", assetID), zap.Error(err))
|
||||
}
|
||||
if err := usageRepo.Release(ctx, tenantID, sizeBytes); err != nil {
|
||||
@@ -605,11 +634,11 @@ func (s *ImageService) ProcessFinalizeUpload(ctx context.Context, tenantID, asse
|
||||
|
||||
imageRepoTx := repository.NewImageRepository(tx)
|
||||
if exists {
|
||||
if err := imageRepoTx.UpdateStatus(ctx, tenantID, assetID, "active"); err != nil {
|
||||
if err := imageRepoTx.UpdateStatus(ctx, tenantID, lockedAsset.BrandID, assetID, "active"); err != nil {
|
||||
return response.ErrInternal(50033, "image_upload_finalize_failed", "failed to activate uploaded image")
|
||||
}
|
||||
} else {
|
||||
if err := imageRepoTx.UpdateStatus(ctx, tenantID, assetID, "upload_failed"); err != nil {
|
||||
if err := imageRepoTx.UpdateStatus(ctx, tenantID, lockedAsset.BrandID, assetID, "upload_failed"); err != nil {
|
||||
return response.ErrInternal(50034, "image_upload_finalize_failed", "failed to mark image upload as failed")
|
||||
}
|
||||
usageRepoTx := repository.NewImageUsageRepository(tx)
|
||||
@@ -680,7 +709,7 @@ WHERE tenant_id = $1 AND image_asset_id = $2
|
||||
|
||||
imageRepoTx := repository.NewImageRepository(tx)
|
||||
usageRepoTx := repository.NewImageUsageRepository(tx)
|
||||
if err := imageRepoTx.MarkDeleted(ctx, tenantID, assetID); err != nil {
|
||||
if err := imageRepoTx.MarkDeleted(ctx, tenantID, lockedAsset.BrandID, assetID); err != nil {
|
||||
return response.ErrInternal(50044, "image_delete_finalize_failed", "failed to finalize image deletion")
|
||||
}
|
||||
if err := usageRepoTx.Release(ctx, tenantID, lockedAsset.SizeBytes); err != nil {
|
||||
@@ -725,10 +754,15 @@ func (s *ImageService) enqueueFinalizeDeleteTask(tenantID, assetID int64) error
|
||||
}
|
||||
|
||||
func (s *ImageService) loadImageAsset(ctx context.Context, tenantID, assetID int64) (*repository.ImageAsset, error) {
|
||||
return repository.NewImageRepository(s.pool).Get(ctx, tenantID, assetID)
|
||||
brandID, err := s.lookupImageAssetBrandID(ctx, tenantID, assetID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return repository.NewImageRepository(s.pool).Get(ctx, tenantID, brandID, assetID)
|
||||
}
|
||||
|
||||
type lockedImageAsset struct {
|
||||
BrandID int64
|
||||
ObjectKey string
|
||||
SizeBytes int64
|
||||
Status string
|
||||
@@ -739,11 +773,11 @@ func (s *ImageService) lockImageAsset(ctx context.Context, tx pgx.Tx, tenantID,
|
||||
var asset lockedImageAsset
|
||||
var deletedAt *time.Time
|
||||
err := tx.QueryRow(ctx, `
|
||||
SELECT object_key, size_bytes, status, deleted_at
|
||||
SELECT brand_id, object_key, size_bytes, status, deleted_at
|
||||
FROM image_assets
|
||||
WHERE tenant_id = $1 AND id = $2
|
||||
FOR UPDATE
|
||||
`, tenantID, assetID).Scan(&asset.ObjectKey, &asset.SizeBytes, &asset.Status, &deletedAt)
|
||||
`, tenantID, assetID).Scan(&asset.BrandID, &asset.ObjectKey, &asset.SizeBytes, &asset.Status, &deletedAt)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, false, nil
|
||||
@@ -754,14 +788,26 @@ FOR UPDATE
|
||||
return &asset, true, nil
|
||||
}
|
||||
|
||||
func (s *ImageService) ensureFolderExists(ctx context.Context, tenantID, folderID int64) error {
|
||||
func (s *ImageService) ensureFolderExists(ctx context.Context, tenantID, brandID, folderID int64) error {
|
||||
folderRepo := repository.NewImageFolderRepository(s.pool)
|
||||
if _, err := folderRepo.Get(ctx, tenantID, folderID); err != nil {
|
||||
if _, err := folderRepo.Get(ctx, tenantID, brandID, folderID); err != nil {
|
||||
return mapImageFolderNotFound(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ImageService) lookupImageAssetBrandID(ctx context.Context, tenantID, assetID int64) (int64, error) {
|
||||
var brandID int64
|
||||
if err := s.pool.QueryRow(ctx, `
|
||||
SELECT brand_id
|
||||
FROM image_assets
|
||||
WHERE tenant_id = $1 AND id = $2
|
||||
`, tenantID, assetID).Scan(&brandID); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return brandID, nil
|
||||
}
|
||||
|
||||
func (s *ImageService) lookupImageQuotaBytes(ctx context.Context, tenantID int64) (int64, error) {
|
||||
var quotaBytes int64
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
|
||||
@@ -61,6 +61,7 @@ type knowledgeParseJob struct {
|
||||
type knowledgeItemRecord struct {
|
||||
ID int64
|
||||
TenantID int64
|
||||
BrandID int64
|
||||
GroupID int64
|
||||
GroupName string
|
||||
Name string
|
||||
@@ -207,12 +208,16 @@ func (s *KnowledgeService) runtimeConfig() knowledgeRuntimeConfig {
|
||||
|
||||
func (s *KnowledgeService) ListGroups(ctx context.Context) ([]KnowledgeGroupResponse, error) {
|
||||
actor := auth.MustActor(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, name, parent_id, sort_order, created_at
|
||||
FROM knowledge_groups
|
||||
WHERE tenant_id = $1 AND deleted_at IS NULL
|
||||
WHERE tenant_id = $1 AND brand_id = $2 AND deleted_at IS NULL
|
||||
ORDER BY sort_order, created_at, id
|
||||
`, actor.TenantID)
|
||||
`, actor.TenantID, brandID)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50050, "knowledge_groups_query_failed", "failed to list knowledge groups")
|
||||
}
|
||||
@@ -240,6 +245,7 @@ func (s *KnowledgeService) ListGroups(ctx context.Context) ([]KnowledgeGroupResp
|
||||
SELECT ki.group_id, COUNT(DISTINCT ki.id)::bigint
|
||||
FROM knowledge_items ki
|
||||
WHERE ki.tenant_id = $1
|
||||
AND ki.brand_id = $2
|
||||
AND ki.deleted_at IS NULL
|
||||
AND ki.status = 'completed'
|
||||
AND EXISTS (
|
||||
@@ -251,7 +257,7 @@ func (s *KnowledgeService) ListGroups(ctx context.Context) ([]KnowledgeGroupResp
|
||||
AND kcm.status = 'active'
|
||||
)
|
||||
GROUP BY ki.group_id
|
||||
`, actor.TenantID)
|
||||
`, actor.TenantID, brandID)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50050, "knowledge_group_counts_query_failed", "failed to list knowledge group counts")
|
||||
}
|
||||
@@ -310,6 +316,10 @@ func (s *KnowledgeService) ListGroups(ctx context.Context) ([]KnowledgeGroupResp
|
||||
|
||||
func (s *KnowledgeService) CreateGroup(ctx context.Context, req KnowledgeGroupRequest) (*KnowledgeGroupResponse, error) {
|
||||
actor := auth.MustActor(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
name := strings.TrimSpace(req.Name)
|
||||
if name == "" {
|
||||
@@ -318,7 +328,7 @@ func (s *KnowledgeService) CreateGroup(ctx context.Context, req KnowledgeGroupRe
|
||||
|
||||
var parentID *int64
|
||||
if req.ParentID != nil && *req.ParentID > 0 {
|
||||
if err := s.ensureRootGroup(ctx, actor.TenantID, *req.ParentID); err != nil {
|
||||
if err := s.ensureRootGroup(ctx, actor.TenantID, brandID, *req.ParentID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parentID = req.ParentID
|
||||
@@ -333,11 +343,11 @@ func (s *KnowledgeService) CreateGroup(ctx context.Context, req KnowledgeGroupRe
|
||||
groupID int64
|
||||
createdAt time.Time
|
||||
)
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
INSERT INTO knowledge_groups (tenant_id, name, parent_id, sort_order)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
err = s.pool.QueryRow(ctx, `
|
||||
INSERT INTO knowledge_groups (tenant_id, brand_id, name, parent_id, sort_order)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id, created_at
|
||||
`, actor.TenantID, name, parentID, sortOrder).Scan(&groupID, &createdAt)
|
||||
`, actor.TenantID, brandID, name, parentID, sortOrder).Scan(&groupID, &createdAt)
|
||||
if err != nil {
|
||||
return nil, response.ErrConflict(40950, "knowledge_group_create_failed", "knowledge group already exists or cannot be created")
|
||||
}
|
||||
@@ -355,6 +365,10 @@ func (s *KnowledgeService) CreateGroup(ctx context.Context, req KnowledgeGroupRe
|
||||
|
||||
func (s *KnowledgeService) UpdateGroup(ctx context.Context, groupID int64, req KnowledgeGroupRequest) error {
|
||||
actor := auth.MustActor(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
name := strings.TrimSpace(req.Name)
|
||||
if name == "" {
|
||||
@@ -369,8 +383,8 @@ func (s *KnowledgeService) UpdateGroup(ctx context.Context, groupID int64, req K
|
||||
tag, err := s.pool.Exec(ctx, `
|
||||
UPDATE knowledge_groups
|
||||
SET name = $1, sort_order = $2, updated_at = NOW()
|
||||
WHERE id = $3 AND tenant_id = $4 AND deleted_at IS NULL
|
||||
`, name, sortOrder, groupID, actor.TenantID)
|
||||
WHERE id = $3 AND tenant_id = $4 AND brand_id = $5 AND deleted_at IS NULL
|
||||
`, name, sortOrder, groupID, actor.TenantID, brandID)
|
||||
if err != nil {
|
||||
return response.ErrConflict(40950, "knowledge_group_update_failed", "knowledge group already exists or cannot be updated")
|
||||
}
|
||||
@@ -382,6 +396,10 @@ func (s *KnowledgeService) UpdateGroup(ctx context.Context, groupID int64, req K
|
||||
|
||||
func (s *KnowledgeService) DeleteGroup(ctx context.Context, groupID int64) error {
|
||||
actor := auth.MustActor(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
@@ -389,7 +407,7 @@ func (s *KnowledgeService) DeleteGroup(ctx context.Context, groupID int64) error
|
||||
}
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
|
||||
groupIDs, err := s.listGroupSubtreeIDs(ctx, tx, actor.TenantID, groupID)
|
||||
groupIDs, err := s.listGroupSubtreeIDs(ctx, tx, actor.TenantID, brandID, groupID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -397,7 +415,7 @@ func (s *KnowledgeService) DeleteGroup(ctx context.Context, groupID int64) error
|
||||
return response.ErrNotFound(40451, "knowledge_group_not_found", "knowledge group not found")
|
||||
}
|
||||
|
||||
itemIDs, err := s.listActiveKnowledgeItemIDs(ctx, tx, actor.TenantID, groupIDs)
|
||||
itemIDs, err := s.listActiveKnowledgeItemIDs(ctx, tx, actor.TenantID, brandID, groupIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -407,9 +425,10 @@ func (s *KnowledgeService) DeleteGroup(ctx context.Context, groupID int64) error
|
||||
UPDATE knowledge_items
|
||||
SET deleted_at = NOW(), updated_at = NOW(), status = 'deleted'
|
||||
WHERE tenant_id = $1
|
||||
AND brand_id = $2
|
||||
AND deleted_at IS NULL
|
||||
AND id = ANY($2::bigint[])
|
||||
`, actor.TenantID, itemIDs); err != nil {
|
||||
AND id = ANY($3::bigint[])
|
||||
`, actor.TenantID, brandID, itemIDs); err != nil {
|
||||
return response.ErrInternal(50054, "knowledge_group_delete_failed", "failed to delete knowledge items")
|
||||
}
|
||||
|
||||
@@ -448,9 +467,10 @@ func (s *KnowledgeService) DeleteGroup(ctx context.Context, groupID int64) error
|
||||
UPDATE knowledge_groups
|
||||
SET deleted_at = NOW(), updated_at = NOW()
|
||||
WHERE tenant_id = $1
|
||||
AND brand_id = $2
|
||||
AND deleted_at IS NULL
|
||||
AND id = ANY($2::bigint[])
|
||||
`, actor.TenantID, groupIDs); err != nil {
|
||||
AND id = ANY($3::bigint[])
|
||||
`, actor.TenantID, brandID, groupIDs); err != nil {
|
||||
return response.ErrInternal(50054, "knowledge_group_delete_failed", "failed to delete knowledge groups")
|
||||
}
|
||||
|
||||
@@ -475,8 +495,12 @@ func (s *KnowledgeService) DeleteGroup(ctx context.Context, groupID int64) error
|
||||
|
||||
func (s *KnowledgeService) ListItems(ctx context.Context, groupID *int64) ([]KnowledgeItemResponse, error) {
|
||||
actor := auth.MustActor(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if groupID != nil && *groupID > 0 {
|
||||
if err := s.ensureGroupAccessible(ctx, actor.TenantID, *groupID); err != nil {
|
||||
if err := s.ensureGroupAccessible(ctx, actor.TenantID, brandID, *groupID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -485,23 +509,24 @@ func (s *KnowledgeService) ListItems(ctx context.Context, groupID *int64) ([]Kno
|
||||
SELECT ki.id, ki.group_id, kg.name, ki.name, ki.source_type, ki.source_uri, ki.status,
|
||||
ki.size_bytes, ki.error_message, ki.created_at, ki.updated_at
|
||||
FROM knowledge_items ki
|
||||
JOIN knowledge_groups kg ON kg.id = ki.group_id AND kg.deleted_at IS NULL
|
||||
JOIN knowledge_groups kg ON kg.id = ki.group_id AND kg.brand_id = ki.brand_id AND kg.deleted_at IS NULL
|
||||
WHERE ki.tenant_id = $1
|
||||
AND ki.brand_id = $2
|
||||
AND ki.deleted_at IS NULL
|
||||
`
|
||||
args := []any{actor.TenantID}
|
||||
args := []any{actor.TenantID, brandID}
|
||||
if groupID != nil && *groupID > 0 {
|
||||
query += `
|
||||
AND ki.group_id IN (
|
||||
WITH RECURSIVE selected_groups AS (
|
||||
SELECT id
|
||||
FROM knowledge_groups
|
||||
WHERE id = $2 AND tenant_id = $1 AND deleted_at IS NULL
|
||||
WHERE id = $3 AND tenant_id = $1 AND brand_id = $2 AND deleted_at IS NULL
|
||||
UNION ALL
|
||||
SELECT child.id
|
||||
FROM knowledge_groups child
|
||||
JOIN selected_groups parent ON child.parent_id = parent.id
|
||||
WHERE child.tenant_id = $1 AND child.deleted_at IS NULL
|
||||
WHERE child.tenant_id = $1 AND child.brand_id = $2 AND child.deleted_at IS NULL
|
||||
)
|
||||
SELECT id FROM selected_groups
|
||||
)
|
||||
@@ -551,6 +576,10 @@ func (s *KnowledgeService) ListItems(ctx context.Context, groupID *int64) ([]Kno
|
||||
|
||||
func (s *KnowledgeService) GetItemDetail(ctx context.Context, itemID int64) (*KnowledgeItemDetailResponse, error) {
|
||||
actor := auth.MustActor(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var (
|
||||
item KnowledgeItemDetailResponse
|
||||
@@ -560,13 +589,13 @@ func (s *KnowledgeService) GetItemDetail(ctx context.Context, itemID int64) (*Kn
|
||||
createdAt, updatedAt time.Time
|
||||
)
|
||||
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
err = s.pool.QueryRow(ctx, `
|
||||
SELECT ki.id, ki.group_id, kg.name, ki.name, ki.source_type, ki.source_uri, ki.status,
|
||||
ki.size_bytes, ki.error_message, ki.markdown_content, ki.created_at, ki.updated_at
|
||||
FROM knowledge_items ki
|
||||
JOIN knowledge_groups kg ON kg.id = ki.group_id AND kg.deleted_at IS NULL
|
||||
WHERE ki.id = $1 AND ki.tenant_id = $2 AND ki.deleted_at IS NULL
|
||||
`, itemID, actor.TenantID).Scan(
|
||||
JOIN knowledge_groups kg ON kg.id = ki.group_id AND kg.brand_id = ki.brand_id AND kg.deleted_at IS NULL
|
||||
WHERE ki.id = $1 AND ki.tenant_id = $2 AND ki.brand_id = $3 AND ki.deleted_at IS NULL
|
||||
`, itemID, actor.TenantID, brandID).Scan(
|
||||
&item.ID,
|
||||
&item.GroupID,
|
||||
&item.GroupName,
|
||||
@@ -597,6 +626,10 @@ func (s *KnowledgeService) GetItemDetail(ctx context.Context, itemID int64) (*Kn
|
||||
|
||||
func (s *KnowledgeService) CreateTextItem(ctx context.Context, req KnowledgeTextItemRequest) (*KnowledgeItemResponse, error) {
|
||||
actor := auth.MustActor(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.validateIngestionDependencies("text"); err != nil {
|
||||
return nil, err
|
||||
@@ -612,7 +645,7 @@ func (s *KnowledgeService) CreateTextItem(ctx context.Context, req KnowledgeText
|
||||
return nil, response.ErrBadRequest(40055, "knowledge_text_name_too_long", "knowledge text name must be at most 20 characters")
|
||||
}
|
||||
|
||||
return s.queueKnowledgeItem(ctx, actor.TenantID, knowledgeIngestInput{
|
||||
return s.queueKnowledgeItem(ctx, actor.TenantID, brandID, knowledgeIngestInput{
|
||||
GroupID: req.GroupID,
|
||||
Name: name,
|
||||
SourceType: "text",
|
||||
@@ -623,6 +656,10 @@ func (s *KnowledgeService) CreateTextItem(ctx context.Context, req KnowledgeText
|
||||
|
||||
func (s *KnowledgeService) CreateURLItem(ctx context.Context, req KnowledgeURLItemRequest) (*KnowledgeItemResponse, error) {
|
||||
actor := auth.MustActor(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.validateIngestionDependencies("website"); err != nil {
|
||||
return nil, err
|
||||
@@ -633,7 +670,7 @@ func (s *KnowledgeService) CreateURLItem(ctx context.Context, req KnowledgeURLIt
|
||||
return nil, response.ErrBadRequest(40052, "knowledge_url_invalid", "knowledge url is invalid")
|
||||
}
|
||||
|
||||
return s.queueKnowledgeItem(ctx, actor.TenantID, knowledgeIngestInput{
|
||||
return s.queueKnowledgeItem(ctx, actor.TenantID, brandID, knowledgeIngestInput{
|
||||
GroupID: req.GroupID,
|
||||
Name: strings.TrimSpace(req.Name),
|
||||
SourceType: "website",
|
||||
@@ -649,6 +686,10 @@ func (s *KnowledgeService) CreateFileItem(
|
||||
content []byte,
|
||||
) (*KnowledgeItemResponse, error) {
|
||||
actor := auth.MustActor(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.validateIngestionDependencies("document"); err != nil {
|
||||
return nil, err
|
||||
@@ -664,7 +705,7 @@ func (s *KnowledgeService) CreateFileItem(
|
||||
return nil, response.ErrServiceUnavailable(50357, "knowledge_storage_unavailable", err.Error())
|
||||
}
|
||||
|
||||
return s.queueKnowledgeItem(ctx, actor.TenantID, knowledgeIngestInput{
|
||||
return s.queueKnowledgeItem(ctx, actor.TenantID, brandID, knowledgeIngestInput{
|
||||
GroupID: groupID,
|
||||
Name: displayName,
|
||||
SourceType: "document",
|
||||
@@ -675,6 +716,10 @@ func (s *KnowledgeService) CreateFileItem(
|
||||
|
||||
func (s *KnowledgeService) DeleteItem(ctx context.Context, itemID int64) error {
|
||||
actor := auth.MustActor(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
@@ -685,8 +730,8 @@ func (s *KnowledgeService) DeleteItem(ctx context.Context, itemID int64) error {
|
||||
tag, err := tx.Exec(ctx, `
|
||||
UPDATE knowledge_items
|
||||
SET deleted_at = NOW(), updated_at = NOW(), status = 'deleted'
|
||||
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
|
||||
`, itemID, actor.TenantID)
|
||||
WHERE id = $1 AND tenant_id = $2 AND brand_id = $3 AND deleted_at IS NULL
|
||||
`, itemID, actor.TenantID, brandID)
|
||||
if err != nil {
|
||||
return response.ErrInternal(50054, "knowledge_item_delete_failed", "failed to delete knowledge item")
|
||||
}
|
||||
@@ -734,14 +779,18 @@ func (s *KnowledgeService) DeleteItem(ctx context.Context, itemID int64) error {
|
||||
|
||||
func (s *KnowledgeService) RetryItem(ctx context.Context, itemID int64) (*KnowledgeItemResponse, error) {
|
||||
actor := auth.MustActor(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var sourceType string
|
||||
var status string
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
err = s.pool.QueryRow(ctx, `
|
||||
SELECT source_type, status
|
||||
FROM knowledge_items
|
||||
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
|
||||
`, itemID, actor.TenantID).Scan(&sourceType, &status)
|
||||
WHERE id = $1 AND tenant_id = $2 AND brand_id = $3 AND deleted_at IS NULL
|
||||
`, itemID, actor.TenantID, brandID).Scan(&sourceType, &status)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, response.ErrNotFound(40450, "knowledge_item_not_found", "knowledge item not found")
|
||||
@@ -793,9 +842,10 @@ func (s *KnowledgeService) RetryItem(ctx context.Context, itemID int64) (*Knowle
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
AND tenant_id = $2
|
||||
AND brand_id = $3
|
||||
AND deleted_at IS NULL
|
||||
AND status = 'failed'
|
||||
`, itemID, actor.TenantID)
|
||||
`, itemID, actor.TenantID, brandID)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50055, "knowledge_item_retry_failed", "failed to reset knowledge item")
|
||||
}
|
||||
@@ -816,7 +866,7 @@ func (s *KnowledgeService) RetryItem(ctx context.Context, itemID int64) (*Knowle
|
||||
return nil, response.ErrServiceUnavailable(50356, "knowledge_queue_unavailable", "knowledge parse queue is busy, please retry")
|
||||
}
|
||||
|
||||
items, err := s.listKnowledgeItemsByIDs(ctx, actor.TenantID, []int64{itemID})
|
||||
items, err := s.listKnowledgeItemsByIDs(ctx, actor.TenantID, brandID, []int64{itemID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -829,6 +879,7 @@ func (s *KnowledgeService) RetryItem(ctx context.Context, itemID int64) (*Knowle
|
||||
func (s *KnowledgeService) ResolveContext(
|
||||
ctx context.Context,
|
||||
tenantID int64,
|
||||
brandID int64,
|
||||
groupIDs []int64,
|
||||
query string,
|
||||
) (*KnowledgeContext, error) {
|
||||
@@ -871,7 +922,7 @@ func (s *KnowledgeService) ResolveContext(
|
||||
return nil, response.ErrServiceUnavailable(50352, "qdrant_unavailable", err.Error())
|
||||
}
|
||||
|
||||
searchGroupIDs, groupNames, err := s.resolveKnowledgeSearchScope(ctx, tenantID, requestedGroupIDs)
|
||||
searchGroupIDs, groupNames, err := s.resolveKnowledgeSearchScope(ctx, tenantID, brandID, requestedGroupIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -883,7 +934,7 @@ func (s *KnowledgeService) ResolveContext(
|
||||
GroupIDs: searchGroupIDs,
|
||||
GroupNames: groupNames,
|
||||
Snippets: []KnowledgeSnippet{},
|
||||
PreciseFacts: s.collectKnowledgeContextPreciseFacts(ctx, tenantID, searchGroupIDs, nil),
|
||||
PreciseFacts: s.collectKnowledgeContextPreciseFacts(ctx, tenantID, brandID, searchGroupIDs, nil),
|
||||
}
|
||||
|
||||
vectors, err := s.provider.Embed(ctx, queries)
|
||||
@@ -903,6 +954,7 @@ func (s *KnowledgeService) ResolveContext(
|
||||
points, err := s.store.Search(ctx, retrieval.SearchRequest{
|
||||
Vector: vector,
|
||||
TenantID: tenantID,
|
||||
BrandID: brandID,
|
||||
GroupIDs: searchGroupIDs,
|
||||
Limit: runtimeCfg.RecallLimit,
|
||||
})
|
||||
@@ -922,7 +974,7 @@ func (s *KnowledgeService) ResolveContext(
|
||||
return baseContext, nil
|
||||
}
|
||||
|
||||
candidates, err = s.filterActiveKnowledgeSnippets(ctx, tenantID, candidates)
|
||||
candidates, err = s.filterActiveKnowledgeSnippets(ctx, tenantID, brandID, candidates)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -939,17 +991,17 @@ func (s *KnowledgeService) ResolveContext(
|
||||
reranked, err := s.provider.Rerank(ctx, rerankQuery, documents, minInt(runtimeCfg.RerankTopN, len(documents)))
|
||||
if err != nil {
|
||||
selected := selectKnowledgeCandidateSnippets(candidates, runtimeCfg.RerankTopN)
|
||||
selected = s.enrichKnowledgePromptSnippets(ctx, tenantID, selected)
|
||||
selected = s.enrichKnowledgePromptSnippets(ctx, tenantID, brandID, selected)
|
||||
baseContext.Snippets = selected
|
||||
baseContext.PreciseFacts = s.collectKnowledgeContextPreciseFacts(ctx, tenantID, searchGroupIDs, selected)
|
||||
baseContext.PreciseFacts = s.collectKnowledgeContextPreciseFacts(ctx, tenantID, brandID, searchGroupIDs, selected)
|
||||
s.logKnowledgeResolve(baseContext, tenantID, rerankQuery, "rerank_failed_fallback", queryRewriteStage, queryRewriteModel, candidates, selected)
|
||||
return baseContext, nil
|
||||
}
|
||||
|
||||
selected := selectKnowledgeRerankedSnippets(candidates, reranked, runtimeCfg.RerankTopN)
|
||||
selected = s.enrichKnowledgePromptSnippets(ctx, tenantID, selected)
|
||||
selected = s.enrichKnowledgePromptSnippets(ctx, tenantID, brandID, selected)
|
||||
baseContext.Snippets = selected
|
||||
baseContext.PreciseFacts = s.collectKnowledgeContextPreciseFacts(ctx, tenantID, searchGroupIDs, selected)
|
||||
baseContext.PreciseFacts = s.collectKnowledgeContextPreciseFacts(ctx, tenantID, brandID, searchGroupIDs, selected)
|
||||
s.logKnowledgeResolve(baseContext, tenantID, rerankQuery, "resolved", queryRewriteStage, queryRewriteModel, candidates, selected)
|
||||
return baseContext, nil
|
||||
}
|
||||
@@ -1341,6 +1393,7 @@ type knowledgePromptItemRecord struct {
|
||||
func (s *KnowledgeService) enrichKnowledgePromptSnippets(
|
||||
ctx context.Context,
|
||||
tenantID int64,
|
||||
brandID int64,
|
||||
snippets []KnowledgeSnippet,
|
||||
) []KnowledgeSnippet {
|
||||
if len(snippets) == 0 {
|
||||
@@ -1358,7 +1411,7 @@ func (s *KnowledgeService) enrichKnowledgePromptSnippets(
|
||||
return snippets
|
||||
}
|
||||
|
||||
items, err := s.loadKnowledgePromptItems(ctx, tenantID, itemIDs)
|
||||
items, err := s.loadKnowledgePromptItems(ctx, tenantID, brandID, itemIDs)
|
||||
if err != nil {
|
||||
if s.logger != nil {
|
||||
s.logger.Warn("load knowledge prompt items failed", zap.Error(err))
|
||||
@@ -1395,6 +1448,7 @@ func (s *KnowledgeService) enrichKnowledgePromptSnippets(
|
||||
func (s *KnowledgeService) loadKnowledgePromptItems(
|
||||
ctx context.Context,
|
||||
tenantID int64,
|
||||
brandID int64,
|
||||
itemIDs []int64,
|
||||
) ([]knowledgePromptItemRecord, error) {
|
||||
if len(itemIDs) == 0 {
|
||||
@@ -1404,12 +1458,13 @@ func (s *KnowledgeService) loadKnowledgePromptItems(
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT ki.id, ki.group_id, kg.name, ki.name, ki.source_type, ki.source_uri, ki.content_text, ki.markdown_content
|
||||
FROM knowledge_items ki
|
||||
JOIN knowledge_groups kg ON kg.id = ki.group_id AND kg.deleted_at IS NULL
|
||||
JOIN knowledge_groups kg ON kg.id = ki.group_id AND kg.brand_id = ki.brand_id AND kg.deleted_at IS NULL
|
||||
WHERE ki.tenant_id = $1
|
||||
AND ki.id = ANY($2::bigint[])
|
||||
AND ki.brand_id = $2
|
||||
AND ki.id = ANY($3::bigint[])
|
||||
AND ki.status = 'completed'
|
||||
AND ki.deleted_at IS NULL
|
||||
`, tenantID, itemIDs)
|
||||
`, tenantID, brandID, itemIDs)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50051, "knowledge_items_query_failed", "failed to load knowledge prompt items")
|
||||
}
|
||||
@@ -1438,6 +1493,7 @@ func (s *KnowledgeService) loadKnowledgePromptItems(
|
||||
func (s *KnowledgeService) collectKnowledgeContextPreciseFacts(
|
||||
ctx context.Context,
|
||||
tenantID int64,
|
||||
brandID int64,
|
||||
groupIDs []int64,
|
||||
snippets []KnowledgeSnippet,
|
||||
) []string {
|
||||
@@ -1454,7 +1510,7 @@ func (s *KnowledgeService) collectKnowledgeContextPreciseFacts(
|
||||
}
|
||||
matchedItemIDs = normalizeInt64IDs(matchedItemIDs)
|
||||
|
||||
items, err := s.loadKnowledgePromptItemsByGroupIDs(ctx, tenantID, groupIDs, matchedItemIDs, 24)
|
||||
items, err := s.loadKnowledgePromptItemsByGroupIDs(ctx, tenantID, brandID, groupIDs, matchedItemIDs, 24)
|
||||
if err != nil {
|
||||
if s.logger != nil {
|
||||
s.logger.Warn("collect knowledge context precise facts failed", zap.Error(err))
|
||||
@@ -1506,6 +1562,7 @@ func (s *KnowledgeService) collectKnowledgeContextPreciseFacts(
|
||||
func (s *KnowledgeService) loadKnowledgePromptItemsByGroupIDs(
|
||||
ctx context.Context,
|
||||
tenantID int64,
|
||||
brandID int64,
|
||||
groupIDs []int64,
|
||||
prioritizedItemIDs []int64,
|
||||
limit int,
|
||||
@@ -1524,31 +1581,34 @@ func (s *KnowledgeService) loadKnowledgePromptItemsByGroupIDs(
|
||||
SELECT id
|
||||
FROM knowledge_groups
|
||||
WHERE tenant_id = $1
|
||||
AND id = ANY($2::bigint[])
|
||||
AND brand_id = $2
|
||||
AND id = ANY($3::bigint[])
|
||||
AND deleted_at IS NULL
|
||||
UNION ALL
|
||||
SELECT child.id
|
||||
FROM knowledge_groups child
|
||||
JOIN selected_groups parent ON child.parent_id = parent.id
|
||||
WHERE child.tenant_id = $1
|
||||
AND child.brand_id = $2
|
||||
AND child.deleted_at IS NULL
|
||||
)
|
||||
SELECT ki.id, ki.group_id, kg.name, ki.name, ki.source_type, ki.source_uri, ki.content_text, ki.markdown_content
|
||||
FROM knowledge_items ki
|
||||
JOIN knowledge_groups kg ON kg.id = ki.group_id AND kg.deleted_at IS NULL
|
||||
JOIN knowledge_groups kg ON kg.id = ki.group_id AND kg.brand_id = ki.brand_id AND kg.deleted_at IS NULL
|
||||
WHERE ki.tenant_id = $1
|
||||
AND ki.brand_id = $2
|
||||
AND ki.status = 'completed'
|
||||
AND ki.deleted_at IS NULL
|
||||
AND ki.group_id IN (SELECT id FROM selected_groups)
|
||||
ORDER BY
|
||||
CASE
|
||||
WHEN ki.id = ANY($3::bigint[]) THEN 0
|
||||
WHEN ki.id = ANY($4::bigint[]) THEN 0
|
||||
ELSE 1
|
||||
END,
|
||||
ki.updated_at DESC,
|
||||
ki.id DESC
|
||||
LIMIT $4
|
||||
`, tenantID, groupIDs, prioritizedItemIDs, limit)
|
||||
LIMIT $5
|
||||
`, tenantID, brandID, groupIDs, prioritizedItemIDs, limit)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50051, "knowledge_items_query_failed", "failed to load knowledge prompt items by groups")
|
||||
}
|
||||
@@ -1859,9 +1919,10 @@ func (s *KnowledgeService) validateIngestionDependencies(sourceType string) erro
|
||||
func (s *KnowledgeService) queueKnowledgeItem(
|
||||
ctx context.Context,
|
||||
tenantID int64,
|
||||
brandID int64,
|
||||
input knowledgeIngestInput,
|
||||
) (*KnowledgeItemResponse, error) {
|
||||
resolvedGroupID, err := s.resolveKnowledgeStorageGroupID(ctx, tenantID, input.GroupID)
|
||||
resolvedGroupID, err := s.resolveKnowledgeStorageGroupID(ctx, tenantID, brandID, input.GroupID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1880,7 +1941,7 @@ func (s *KnowledgeService) queueKnowledgeItem(
|
||||
input.ContentText = &contentText
|
||||
}
|
||||
|
||||
itemID, parseTaskID, err := s.createPendingKnowledgeItem(ctx, tenantID, input)
|
||||
itemID, parseTaskID, err := s.createPendingKnowledgeItem(ctx, tenantID, brandID, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1894,7 +1955,7 @@ func (s *KnowledgeService) queueKnowledgeItem(
|
||||
return nil, response.ErrServiceUnavailable(50356, "knowledge_queue_unavailable", "knowledge parse queue is busy, please retry")
|
||||
}
|
||||
|
||||
items, err := s.listKnowledgeItemsByIDs(ctx, tenantID, []int64{itemID})
|
||||
items, err := s.listKnowledgeItemsByIDs(ctx, tenantID, brandID, []int64{itemID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -2064,6 +2125,7 @@ func (s *KnowledgeService) executeParseJob(ctx context.Context, job knowledgePar
|
||||
Vector: embeddings[index],
|
||||
Payload: map[string]any{
|
||||
"tenant_id": job.TenantID,
|
||||
"brand_id": item.BrandID,
|
||||
"group_id": item.GroupID,
|
||||
"group_name": item.GroupName,
|
||||
"item_id": item.ID,
|
||||
@@ -2105,14 +2167,15 @@ func (s *KnowledgeService) loadKnowledgeItemForParse(
|
||||
var contentText *string
|
||||
var markdownContent *string
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
SELECT ki.id, ki.tenant_id, ki.group_id, kg.name, ki.name, ki.source_type,
|
||||
SELECT ki.id, ki.tenant_id, ki.brand_id, ki.group_id, kg.name, ki.name, ki.source_type,
|
||||
ki.source_uri, ki.storage_key, ki.content_text, ki.markdown_content, ki.item_version
|
||||
FROM knowledge_items ki
|
||||
JOIN knowledge_groups kg ON kg.id = ki.group_id
|
||||
JOIN knowledge_groups kg ON kg.id = ki.group_id AND kg.brand_id = ki.brand_id
|
||||
WHERE ki.id = $1 AND ki.tenant_id = $2 AND ki.deleted_at IS NULL
|
||||
`, itemID, tenantID).Scan(
|
||||
&record.ID,
|
||||
&record.TenantID,
|
||||
&record.BrandID,
|
||||
&record.GroupID,
|
||||
&record.GroupName,
|
||||
&record.Name,
|
||||
@@ -2166,6 +2229,7 @@ type knowledgeChunkMetaInput struct {
|
||||
func (s *KnowledgeService) createPendingKnowledgeItem(
|
||||
ctx context.Context,
|
||||
tenantID int64,
|
||||
brandID int64,
|
||||
input knowledgeIngestInput,
|
||||
) (int64, int64, error) {
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
@@ -2177,12 +2241,12 @@ func (s *KnowledgeService) createPendingKnowledgeItem(
|
||||
var itemID int64
|
||||
err = tx.QueryRow(ctx, `
|
||||
INSERT INTO knowledge_items (
|
||||
tenant_id, group_id, source_type, name, source_uri, storage_key,
|
||||
tenant_id, brand_id, group_id, source_type, name, source_uri, storage_key,
|
||||
content_text, markdown_content, status, size_bytes, item_version
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, NULL, 'pending', $8, 1)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NULL, 'pending', $9, 1)
|
||||
RETURNING id
|
||||
`, tenantID, input.GroupID, input.SourceType, input.Name, input.SourceURI, input.StorageKey, input.ContentText, input.SizeBytes).Scan(&itemID)
|
||||
`, tenantID, brandID, input.GroupID, input.SourceType, input.Name, input.SourceURI, input.StorageKey, input.ContentText, input.SizeBytes).Scan(&itemID)
|
||||
if err != nil {
|
||||
return 0, 0, response.ErrConflict(40951, "knowledge_item_create_failed", "failed to create knowledge item")
|
||||
}
|
||||
@@ -2434,15 +2498,15 @@ func buildKnowledgeSearchText(input knowledgeSearchTextInput) string {
|
||||
return strings.Join(metadata, "\n")
|
||||
}
|
||||
|
||||
func (s *KnowledgeService) ensureGroupAccessible(ctx context.Context, tenantID, groupID int64) error {
|
||||
func (s *KnowledgeService) ensureGroupAccessible(ctx context.Context, tenantID, brandID, groupID int64) error {
|
||||
var exists bool
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
SELECT EXISTS(
|
||||
SELECT 1
|
||||
FROM knowledge_groups
|
||||
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
|
||||
WHERE id = $1 AND tenant_id = $2 AND brand_id = $3 AND deleted_at IS NULL
|
||||
)
|
||||
`, groupID, tenantID).Scan(&exists)
|
||||
`, groupID, tenantID, brandID).Scan(&exists)
|
||||
if err != nil {
|
||||
return response.ErrInternal(50050, "knowledge_group_query_failed", "failed to query knowledge group")
|
||||
}
|
||||
@@ -2452,8 +2516,8 @@ func (s *KnowledgeService) ensureGroupAccessible(ctx context.Context, tenantID,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *KnowledgeService) ensureRootGroup(ctx context.Context, tenantID, groupID int64) error {
|
||||
if err := s.ensureGroupAccessible(ctx, tenantID, groupID); err != nil {
|
||||
func (s *KnowledgeService) ensureRootGroup(ctx context.Context, tenantID, brandID, groupID int64) error {
|
||||
if err := s.ensureGroupAccessible(ctx, tenantID, brandID, groupID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2464,10 +2528,11 @@ func (s *KnowledgeService) ensureRootGroup(ctx context.Context, tenantID, groupI
|
||||
FROM knowledge_groups
|
||||
WHERE id = $1
|
||||
AND tenant_id = $2
|
||||
AND brand_id = $3
|
||||
AND deleted_at IS NULL
|
||||
AND parent_id IS NULL
|
||||
)
|
||||
`, groupID, tenantID).Scan(&isRoot)
|
||||
`, groupID, tenantID, brandID).Scan(&isRoot)
|
||||
if err != nil {
|
||||
return response.ErrInternal(50050, "knowledge_group_query_failed", "failed to query knowledge group")
|
||||
}
|
||||
@@ -2477,8 +2542,8 @@ func (s *KnowledgeService) ensureRootGroup(ctx context.Context, tenantID, groupI
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *KnowledgeService) ensureStorableGroup(ctx context.Context, tenantID, groupID int64) error {
|
||||
if err := s.ensureGroupAccessible(ctx, tenantID, groupID); err != nil {
|
||||
func (s *KnowledgeService) ensureStorableGroup(ctx context.Context, tenantID, brandID, groupID int64) error {
|
||||
if err := s.ensureGroupAccessible(ctx, tenantID, brandID, groupID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2490,13 +2555,15 @@ func (s *KnowledgeService) ensureStorableGroup(ctx context.Context, tenantID, gr
|
||||
JOIN knowledge_groups parent
|
||||
ON parent.id = child.parent_id
|
||||
AND parent.tenant_id = child.tenant_id
|
||||
AND parent.brand_id = child.brand_id
|
||||
AND parent.deleted_at IS NULL
|
||||
WHERE child.id = $1
|
||||
AND child.tenant_id = $2
|
||||
AND child.brand_id = $3
|
||||
AND child.deleted_at IS NULL
|
||||
AND parent.parent_id IS NULL
|
||||
)
|
||||
`, groupID, tenantID).Scan(&storable)
|
||||
`, groupID, tenantID, brandID).Scan(&storable)
|
||||
if err != nil {
|
||||
return response.ErrInternal(50050, "knowledge_group_query_failed", "failed to query knowledge group")
|
||||
}
|
||||
@@ -2506,14 +2573,14 @@ func (s *KnowledgeService) ensureStorableGroup(ctx context.Context, tenantID, gr
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *KnowledgeService) resolveKnowledgeStorageGroupID(ctx context.Context, tenantID, groupID int64) (int64, error) {
|
||||
func (s *KnowledgeService) resolveKnowledgeStorageGroupID(ctx context.Context, tenantID, brandID, groupID int64) (int64, error) {
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return 0, response.ErrInternal(50055, "knowledge_group_resolve_failed", "failed to begin knowledge group transaction")
|
||||
}
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
|
||||
resolvedGroupID, err := s.resolveKnowledgeStorageGroupIDTx(ctx, tx, tenantID, groupID)
|
||||
resolvedGroupID, err := s.resolveKnowledgeStorageGroupIDTx(ctx, tx, tenantID, brandID, groupID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -2527,15 +2594,15 @@ func (s *KnowledgeService) resolveKnowledgeStorageGroupID(ctx context.Context, t
|
||||
func (s *KnowledgeService) resolveKnowledgeStorageGroupIDTx(
|
||||
ctx context.Context,
|
||||
tx pgx.Tx,
|
||||
tenantID, groupID int64,
|
||||
tenantID, brandID, groupID int64,
|
||||
) (int64, error) {
|
||||
var parentID *int64
|
||||
if err := tx.QueryRow(ctx, `
|
||||
SELECT parent_id
|
||||
FROM knowledge_groups
|
||||
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
|
||||
WHERE id = $1 AND tenant_id = $2 AND brand_id = $3 AND deleted_at IS NULL
|
||||
FOR UPDATE
|
||||
`, groupID, tenantID).Scan(&parentID); err != nil {
|
||||
`, groupID, tenantID, brandID).Scan(&parentID); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return 0, response.ErrNotFound(40451, "knowledge_group_not_found", "knowledge group not found")
|
||||
}
|
||||
@@ -2547,8 +2614,8 @@ func (s *KnowledgeService) resolveKnowledgeStorageGroupIDTx(
|
||||
if err := tx.QueryRow(ctx, `
|
||||
SELECT parent_id
|
||||
FROM knowledge_groups
|
||||
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
|
||||
`, *parentID, tenantID).Scan(&grandParentID); err != nil {
|
||||
WHERE id = $1 AND tenant_id = $2 AND brand_id = $3 AND deleted_at IS NULL
|
||||
`, *parentID, tenantID, brandID).Scan(&grandParentID); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return 0, response.ErrNotFound(40451, "knowledge_group_not_found", "knowledge group not found")
|
||||
}
|
||||
@@ -2565,22 +2632,23 @@ func (s *KnowledgeService) resolveKnowledgeStorageGroupIDTx(
|
||||
SELECT id
|
||||
FROM knowledge_groups
|
||||
WHERE tenant_id = $1
|
||||
AND parent_id = $2
|
||||
AND name = $3
|
||||
AND brand_id = $2
|
||||
AND parent_id = $3
|
||||
AND name = $4
|
||||
AND deleted_at IS NULL
|
||||
ORDER BY id
|
||||
LIMIT 1
|
||||
`, tenantID, groupID, defaultKnowledgeChildGroupName).Scan(&childGroupID); err == nil {
|
||||
`, tenantID, brandID, groupID, defaultKnowledgeChildGroupName).Scan(&childGroupID); err == nil {
|
||||
return childGroupID, nil
|
||||
} else if !errors.Is(err, pgx.ErrNoRows) {
|
||||
return 0, response.ErrInternal(50050, "knowledge_group_query_failed", "failed to query knowledge group")
|
||||
}
|
||||
|
||||
if err := tx.QueryRow(ctx, `
|
||||
INSERT INTO knowledge_groups (tenant_id, name, parent_id, sort_order)
|
||||
VALUES ($1, $2, $3, 0)
|
||||
INSERT INTO knowledge_groups (tenant_id, brand_id, name, parent_id, sort_order)
|
||||
VALUES ($1, $2, $3, $4, 0)
|
||||
RETURNING id
|
||||
`, tenantID, defaultKnowledgeChildGroupName, groupID).Scan(&childGroupID); err == nil {
|
||||
`, tenantID, brandID, defaultKnowledgeChildGroupName, groupID).Scan(&childGroupID); err == nil {
|
||||
return childGroupID, nil
|
||||
}
|
||||
|
||||
@@ -2588,12 +2656,13 @@ func (s *KnowledgeService) resolveKnowledgeStorageGroupIDTx(
|
||||
SELECT id
|
||||
FROM knowledge_groups
|
||||
WHERE tenant_id = $1
|
||||
AND parent_id = $2
|
||||
AND name = $3
|
||||
AND brand_id = $2
|
||||
AND parent_id = $3
|
||||
AND name = $4
|
||||
AND deleted_at IS NULL
|
||||
ORDER BY id
|
||||
LIMIT 1
|
||||
`, tenantID, groupID, defaultKnowledgeChildGroupName).Scan(&childGroupID); err != nil {
|
||||
`, tenantID, brandID, groupID, defaultKnowledgeChildGroupName).Scan(&childGroupID); err != nil {
|
||||
return 0, response.ErrConflict(40950, "knowledge_group_create_failed", "failed to create default knowledge group")
|
||||
}
|
||||
return childGroupID, nil
|
||||
@@ -2602,22 +2671,22 @@ func (s *KnowledgeService) resolveKnowledgeStorageGroupIDTx(
|
||||
func (s *KnowledgeService) listGroupSubtreeIDs(
|
||||
ctx context.Context,
|
||||
tx pgx.Tx,
|
||||
tenantID, groupID int64,
|
||||
tenantID, brandID, groupID int64,
|
||||
) ([]int64, error) {
|
||||
rows, err := tx.Query(ctx, `
|
||||
WITH RECURSIVE selected_groups AS (
|
||||
SELECT id
|
||||
FROM knowledge_groups
|
||||
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
|
||||
WHERE id = $1 AND tenant_id = $2 AND brand_id = $3 AND deleted_at IS NULL
|
||||
UNION ALL
|
||||
SELECT child.id
|
||||
FROM knowledge_groups child
|
||||
JOIN selected_groups parent ON child.parent_id = parent.id
|
||||
WHERE child.tenant_id = $2 AND child.deleted_at IS NULL
|
||||
WHERE child.tenant_id = $2 AND child.brand_id = $3 AND child.deleted_at IS NULL
|
||||
)
|
||||
SELECT id
|
||||
FROM selected_groups
|
||||
`, groupID, tenantID)
|
||||
`, groupID, tenantID, brandID)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50054, "knowledge_group_delete_failed", "failed to query knowledge groups")
|
||||
}
|
||||
@@ -2638,6 +2707,7 @@ func (s *KnowledgeService) listActiveKnowledgeItemIDs(
|
||||
ctx context.Context,
|
||||
tx pgx.Tx,
|
||||
tenantID int64,
|
||||
brandID int64,
|
||||
groupIDs []int64,
|
||||
) ([]int64, error) {
|
||||
if len(groupIDs) == 0 {
|
||||
@@ -2648,9 +2718,10 @@ func (s *KnowledgeService) listActiveKnowledgeItemIDs(
|
||||
SELECT id
|
||||
FROM knowledge_items
|
||||
WHERE tenant_id = $1
|
||||
AND brand_id = $2
|
||||
AND deleted_at IS NULL
|
||||
AND group_id = ANY($2::bigint[])
|
||||
`, tenantID, groupIDs)
|
||||
AND group_id = ANY($3::bigint[])
|
||||
`, tenantID, brandID, groupIDs)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50054, "knowledge_group_delete_failed", "failed to query knowledge items")
|
||||
}
|
||||
@@ -2714,6 +2785,7 @@ func (s *KnowledgeService) listKnowledgePointIDs(ctx context.Context, tenantID,
|
||||
func (s *KnowledgeService) filterActiveKnowledgeSnippets(
|
||||
ctx context.Context,
|
||||
tenantID int64,
|
||||
brandID int64,
|
||||
candidates []KnowledgeSnippet,
|
||||
) ([]KnowledgeSnippet, error) {
|
||||
if len(candidates) == 0 {
|
||||
@@ -2735,10 +2807,11 @@ func (s *KnowledgeService) filterActiveKnowledgeSnippets(
|
||||
SELECT id
|
||||
FROM knowledge_items
|
||||
WHERE tenant_id = $1
|
||||
AND id = ANY($2::bigint[])
|
||||
AND brand_id = $2
|
||||
AND id = ANY($3::bigint[])
|
||||
AND status = 'completed'
|
||||
AND deleted_at IS NULL
|
||||
`, tenantID, itemIDs)
|
||||
`, tenantID, brandID, itemIDs)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50051, "knowledge_items_query_failed", "failed to filter deleted knowledge items")
|
||||
}
|
||||
@@ -2852,14 +2925,15 @@ func (s *KnowledgeService) loadKnowledgeItemForCleanup(
|
||||
var contentText *string
|
||||
var markdownContent *string
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
SELECT ki.id, ki.tenant_id, ki.group_id, kg.name, ki.name, ki.source_type,
|
||||
SELECT ki.id, ki.tenant_id, ki.brand_id, ki.group_id, kg.name, ki.name, ki.source_type,
|
||||
ki.source_uri, ki.storage_key, ki.content_text, ki.markdown_content, ki.item_version
|
||||
FROM knowledge_items ki
|
||||
JOIN knowledge_groups kg ON kg.id = ki.group_id
|
||||
JOIN knowledge_groups kg ON kg.id = ki.group_id AND kg.brand_id = ki.brand_id
|
||||
WHERE ki.id = $1 AND ki.tenant_id = $2 AND ki.deleted_at IS NOT NULL
|
||||
`, itemID, tenantID).Scan(
|
||||
&record.ID,
|
||||
&record.TenantID,
|
||||
&record.BrandID,
|
||||
&record.GroupID,
|
||||
&record.GroupName,
|
||||
&record.Name,
|
||||
@@ -2925,7 +2999,7 @@ func (s *KnowledgeService) clearKnowledgeCleanupError(ctx context.Context, tenan
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *KnowledgeService) listKnowledgeItemsByIDs(ctx context.Context, tenantID int64, itemIDs []int64) ([]KnowledgeItemResponse, error) {
|
||||
func (s *KnowledgeService) listKnowledgeItemsByIDs(ctx context.Context, tenantID, brandID int64, itemIDs []int64) ([]KnowledgeItemResponse, error) {
|
||||
if len(itemIDs) == 0 {
|
||||
return []KnowledgeItemResponse{}, nil
|
||||
}
|
||||
@@ -2933,10 +3007,10 @@ func (s *KnowledgeService) listKnowledgeItemsByIDs(ctx context.Context, tenantID
|
||||
SELECT ki.id, ki.group_id, kg.name, ki.name, ki.source_type, ki.source_uri, ki.status,
|
||||
ki.size_bytes, ki.error_message, ki.created_at, ki.updated_at
|
||||
FROM knowledge_items ki
|
||||
JOIN knowledge_groups kg ON kg.id = ki.group_id
|
||||
WHERE ki.tenant_id = $1 AND ki.id = ANY($2::bigint[]) AND ki.deleted_at IS NULL
|
||||
JOIN knowledge_groups kg ON kg.id = ki.group_id AND kg.brand_id = ki.brand_id
|
||||
WHERE ki.tenant_id = $1 AND ki.brand_id = $2 AND ki.id = ANY($3::bigint[]) AND ki.deleted_at IS NULL
|
||||
ORDER BY ki.created_at DESC, ki.id DESC
|
||||
`, tenantID, itemIDs)
|
||||
`, tenantID, brandID, itemIDs)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50051, "knowledge_items_query_failed", "failed to list knowledge items")
|
||||
}
|
||||
@@ -2974,7 +3048,7 @@ func (s *KnowledgeService) listKnowledgeItemsByIDs(ctx context.Context, tenantID
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *KnowledgeService) resolveKnowledgeSearchScope(ctx context.Context, tenantID int64, groupIDs []int64) ([]int64, []string, error) {
|
||||
func (s *KnowledgeService) resolveKnowledgeSearchScope(ctx context.Context, tenantID, brandID int64, groupIDs []int64) ([]int64, []string, error) {
|
||||
groupIDs = normalizeInt64IDs(groupIDs)
|
||||
if len(groupIDs) == 0 {
|
||||
return []int64{}, []string{}, nil
|
||||
@@ -2985,19 +3059,21 @@ func (s *KnowledgeService) resolveKnowledgeSearchScope(ctx context.Context, tena
|
||||
SELECT id, name, parent_id, sort_order, created_at
|
||||
FROM knowledge_groups
|
||||
WHERE tenant_id = $1
|
||||
AND id = ANY($2::bigint[])
|
||||
AND brand_id = $2
|
||||
AND id = ANY($3::bigint[])
|
||||
AND deleted_at IS NULL
|
||||
UNION
|
||||
SELECT child.id, child.name, child.parent_id, child.sort_order, child.created_at
|
||||
FROM knowledge_groups child
|
||||
JOIN selected_groups parent ON child.parent_id = parent.id
|
||||
WHERE child.tenant_id = $1
|
||||
AND child.brand_id = $2
|
||||
AND child.deleted_at IS NULL
|
||||
)
|
||||
SELECT id, name
|
||||
FROM selected_groups
|
||||
ORDER BY sort_order, created_at, id
|
||||
`, tenantID, groupIDs)
|
||||
`, tenantID, brandID, groupIDs)
|
||||
if err != nil {
|
||||
return nil, nil, response.ErrInternal(50050, "knowledge_group_query_failed", "failed to query knowledge groups")
|
||||
}
|
||||
|
||||
@@ -32,15 +32,15 @@ type mediaSupplyArticleImageUpload struct {
|
||||
Content []byte
|
||||
}
|
||||
|
||||
func (s *MediaSupplyService) prepareMeijiequanArticleContent(ctx context.Context, tenantID int64, content string) (string, error) {
|
||||
func (s *MediaSupplyService) prepareMeijiequanArticleContent(ctx context.Context, tenantID int64, brandID int64, content string) (string, error) {
|
||||
html := meijiequanArticleHTML(content)
|
||||
if strings.TrimSpace(html) == "" {
|
||||
return "", nil
|
||||
}
|
||||
return s.uploadMeijiequanArticleImages(ctx, tenantID, html)
|
||||
return s.uploadMeijiequanArticleImages(ctx, tenantID, brandID, html)
|
||||
}
|
||||
|
||||
func (s *MediaSupplyService) uploadMeijiequanArticleImages(ctx context.Context, tenantID int64, fragment string) (string, error) {
|
||||
func (s *MediaSupplyService) uploadMeijiequanArticleImages(ctx context.Context, tenantID int64, brandID int64, fragment string) (string, error) {
|
||||
if !strings.Contains(strings.ToLower(fragment), "<img") {
|
||||
return fragment, nil
|
||||
}
|
||||
@@ -56,7 +56,7 @@ func (s *MediaSupplyService) uploadMeijiequanArticleImages(ctx context.Context,
|
||||
|
||||
cache := make(map[string]string)
|
||||
for _, node := range nodes {
|
||||
if err := s.uploadMeijiequanArticleImageNode(ctx, tenantID, node, cache); err != nil {
|
||||
if err := s.uploadMeijiequanArticleImageNode(ctx, tenantID, brandID, node, cache); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
@@ -70,31 +70,31 @@ func (s *MediaSupplyService) uploadMeijiequanArticleImages(ctx context.Context,
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
func (s *MediaSupplyService) uploadMeijiequanArticleImageNode(ctx context.Context, tenantID int64, node *nethtml.Node, cache map[string]string) error {
|
||||
func (s *MediaSupplyService) uploadMeijiequanArticleImageNode(ctx context.Context, tenantID int64, brandID int64, node *nethtml.Node, cache map[string]string) error {
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
if node.Type == nethtml.ElementNode && strings.EqualFold(node.Data, "img") {
|
||||
if err := s.uploadMeijiequanArticleImageAttrs(ctx, tenantID, node, cache); err != nil {
|
||||
if err := s.uploadMeijiequanArticleImageAttrs(ctx, tenantID, brandID, node, cache); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
||||
if err := s.uploadMeijiequanArticleImageNode(ctx, tenantID, child, cache); err != nil {
|
||||
if err := s.uploadMeijiequanArticleImageNode(ctx, tenantID, brandID, child, cache); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MediaSupplyService) uploadMeijiequanArticleImageAttrs(ctx context.Context, tenantID int64, node *nethtml.Node, cache map[string]string) error {
|
||||
func (s *MediaSupplyService) uploadMeijiequanArticleImageAttrs(ctx context.Context, tenantID int64, brandID int64, node *nethtml.Node, cache map[string]string) error {
|
||||
src := mediaSupplyHTMLAttrValue(node.Attr, "src")
|
||||
originalSrc := mediaSupplyHTMLAttrValue(node.Attr, "_src")
|
||||
dataSrc := mediaSupplyHTMLAttrValue(node.Attr, "data-src")
|
||||
assetID := mediaSupplyHTMLInt64AttrValue(node.Attr, "data-asset-id")
|
||||
currentURL := firstNonEmptyText(src, originalSrc, dataSrc)
|
||||
|
||||
upload, resolved, err := s.resolveMeijiequanArticleImageUpload(ctx, tenantID, currentURL, assetID)
|
||||
upload, resolved, err := s.resolveMeijiequanArticleImageUpload(ctx, tenantID, brandID, currentURL, assetID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -158,8 +158,8 @@ func (s *MediaSupplyService) uploadMeijiequanArticleImageAttrs(ctx context.Conte
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MediaSupplyService) resolveMeijiequanArticleImageUpload(ctx context.Context, tenantID int64, imageURL string, assetID int64) (mediaSupplyArticleImageUpload, bool, error) {
|
||||
objectKey, internalAssetURL, ok := s.mediaSupplyObjectKeyFromPublicAssetURL(imageURL, tenantID)
|
||||
func (s *MediaSupplyService) resolveMeijiequanArticleImageUpload(ctx context.Context, tenantID int64, brandID int64, imageURL string, assetID int64) (mediaSupplyArticleImageUpload, bool, error) {
|
||||
objectKey, internalAssetURL, ok := s.mediaSupplyObjectKeyFromPublicAssetURL(ctx, imageURL, tenantID, brandID)
|
||||
if internalAssetURL {
|
||||
if !ok {
|
||||
if assetID <= 0 {
|
||||
@@ -171,7 +171,7 @@ func (s *MediaSupplyService) resolveMeijiequanArticleImageUpload(ctx context.Con
|
||||
}
|
||||
}
|
||||
if assetID > 0 {
|
||||
objectKey, err := s.loadMediaSupplyImageAssetObjectKey(ctx, tenantID, assetID)
|
||||
objectKey, err := s.loadMediaSupplyImageAssetObjectKey(ctx, tenantID, brandID, assetID)
|
||||
if err != nil {
|
||||
return mediaSupplyArticleImageUpload{}, false, err
|
||||
}
|
||||
@@ -259,7 +259,7 @@ func mediaSupplyUploadFilename(objectKey, sourceURL, ext string) string {
|
||||
return name
|
||||
}
|
||||
|
||||
func (s *MediaSupplyService) mediaSupplyObjectKeyFromPublicAssetURL(raw string, tenantID int64) (string, bool, bool) {
|
||||
func (s *MediaSupplyService) mediaSupplyObjectKeyFromPublicAssetURL(ctx context.Context, raw string, tenantID int64, brandID int64) (string, bool, bool) {
|
||||
token, ok := mediaSupplyPublicAssetTokenFromURL(raw)
|
||||
if !ok {
|
||||
return "", false, false
|
||||
@@ -268,6 +268,9 @@ func (s *MediaSupplyService) mediaSupplyObjectKeyFromPublicAssetURL(raw string,
|
||||
if !valid || !mediaSupplyObjectKeyAllowedForTenant(objectKey, tenantID) {
|
||||
return "", true, false
|
||||
}
|
||||
if !s.mediaSupplyImageObjectKeyBelongsToBrand(ctx, tenantID, brandID, objectKey) {
|
||||
return "", true, false
|
||||
}
|
||||
return objectKey, true, true
|
||||
}
|
||||
|
||||
@@ -317,16 +320,48 @@ func mediaSupplyObjectKeyAllowedForTenant(objectKey string, tenantID int64) bool
|
||||
strings.HasPrefix(objectKey, tenantPrefix+"articles/")
|
||||
}
|
||||
|
||||
func (s *MediaSupplyService) loadMediaSupplyImageAssetObjectKey(ctx context.Context, tenantID, assetID int64) (string, error) {
|
||||
func mediaSupplyObjectKeyIsImageLibraryAsset(objectKey string, tenantID int64) bool {
|
||||
if tenantID <= 0 {
|
||||
return false
|
||||
}
|
||||
tenantPrefix := "tenants/" + strconv.FormatInt(tenantID, 10) + "/"
|
||||
return strings.HasPrefix(strings.TrimSpace(objectKey), tenantPrefix+"images/")
|
||||
}
|
||||
|
||||
func (s *MediaSupplyService) mediaSupplyImageObjectKeyBelongsToBrand(ctx context.Context, tenantID, brandID int64, objectKey string) bool {
|
||||
if s == nil || s.pool == nil || !mediaSupplyObjectKeyIsImageLibraryAsset(objectKey, tenantID) {
|
||||
return true
|
||||
}
|
||||
if tenantID <= 0 || brandID <= 0 || strings.TrimSpace(objectKey) == "" {
|
||||
return false
|
||||
}
|
||||
var exists bool
|
||||
if err := s.pool.QueryRow(ctx, `
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM image_assets
|
||||
WHERE tenant_id = $1
|
||||
AND brand_id = $2
|
||||
AND object_key = $3
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL
|
||||
)
|
||||
`, tenantID, brandID, objectKey).Scan(&exists); err != nil {
|
||||
return false
|
||||
}
|
||||
return exists
|
||||
}
|
||||
|
||||
func (s *MediaSupplyService) loadMediaSupplyImageAssetObjectKey(ctx context.Context, tenantID, brandID, assetID int64) (string, error) {
|
||||
if s == nil || s.pool == nil {
|
||||
return "", errMediaSupplyArticleImageUpload
|
||||
}
|
||||
var objectKey string
|
||||
if err := s.pool.QueryRow(ctx, `
|
||||
SELECT object_key
|
||||
FROM image_assets
|
||||
WHERE id = $1 AND tenant_id = $2 AND status = 'active' AND deleted_at IS NULL
|
||||
`, assetID, tenantID).Scan(&objectKey); err != nil {
|
||||
SELECT object_key
|
||||
FROM image_assets
|
||||
WHERE id = $1 AND tenant_id = $2 AND brand_id = $3 AND status = 'active' AND deleted_at IS NULL
|
||||
`, assetID, tenantID, brandID).Scan(&objectKey); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return "", errMediaSupplyArticleImageUpload
|
||||
}
|
||||
|
||||
@@ -534,7 +534,7 @@ func TestPrepareMeijiequanArticleContentUploadsSaaSImages(t *testing.T) {
|
||||
}).WithObjectStorage(storage)
|
||||
content := `<p class="article-editor-image article-editor-image--center" align="center"><img src="/api/public/assets/` + token + `" alt="" data-asset-id="5" /></p>`
|
||||
|
||||
got, err := svc.prepareMeijiequanArticleContent(context.Background(), 4, content)
|
||||
got, err := svc.prepareMeijiequanArticleContent(context.Background(), 4, 11, content)
|
||||
if err != nil {
|
||||
t.Fatalf("prepare content: %v", err)
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ type mediaSupplySyncJob struct {
|
||||
type mediaSupplySubmitOrder struct {
|
||||
ID int64
|
||||
TenantID int64
|
||||
BrandID int64
|
||||
UserID int64
|
||||
ModelID int
|
||||
Title string
|
||||
@@ -172,7 +173,7 @@ func (w *MediaSupplyWorker) processOneOrder(ctx context.Context) error {
|
||||
if err := w.refreshOrderCostsBeforeSubmit(ctx, order, items); err != nil {
|
||||
return w.failOrder(ctx, order, err)
|
||||
}
|
||||
content, err := w.service.prepareMeijiequanArticleContent(ctx, order.TenantID, order.ContentSnapshot)
|
||||
content, err := w.service.prepareMeijiequanArticleContent(ctx, order.TenantID, order.BrandID, order.ContentSnapshot)
|
||||
if err != nil {
|
||||
return w.failOrder(ctx, order, err)
|
||||
}
|
||||
@@ -1143,7 +1144,7 @@ func (w *MediaSupplyWorker) claimSubmitOrder(ctx context.Context) (*mediaSupplyS
|
||||
var order mediaSupplySubmitOrder
|
||||
var requestPayload []byte
|
||||
if err := tx.QueryRow(ctx, `
|
||||
SELECT id, tenant_id, user_id, model_id, title, content_snapshot, remark, order_brand, attempt_count, request_payload_json
|
||||
SELECT id, tenant_id, brand_id, user_id, model_id, title, content_snapshot, remark, order_brand, attempt_count, request_payload_json
|
||||
FROM media_supply_orders
|
||||
WHERE supplier = $1 AND status = $2 AND next_attempt_at <= NOW() AND deleted_at IS NULL
|
||||
ORDER BY queued_at ASC, id ASC
|
||||
@@ -1152,6 +1153,7 @@ func (w *MediaSupplyWorker) claimSubmitOrder(ctx context.Context) (*mediaSupplyS
|
||||
`, mediaSupplySupplierMeijiequan, mediaSupplyOrderStatusQueued).Scan(
|
||||
&order.ID,
|
||||
&order.TenantID,
|
||||
&order.BrandID,
|
||||
&order.UserID,
|
||||
&order.ModelID,
|
||||
&order.Title,
|
||||
|
||||
@@ -355,6 +355,7 @@ func (s *PromptRuleGenerationService) enqueuePromptRuleGeneration(ctx context.Co
|
||||
knowledgeContext, resolveErr := s.knowledge.ResolveContext(
|
||||
ctx,
|
||||
input.TenantID,
|
||||
*input.BrandID,
|
||||
rule.KnowledgeGroupIDs,
|
||||
buildPromptRuleKnowledgeQuery(rule, inputParams),
|
||||
)
|
||||
|
||||
@@ -254,7 +254,7 @@ func (s *PromptRuleService) Create(ctx context.Context, req PromptRuleRequest) (
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
knowledgeGroups, err := s.validatePromptRuleKnowledgeGroups(ctx, actor.TenantID, req.KnowledgeGroupIDs)
|
||||
knowledgeGroups, err := s.validatePromptRuleKnowledgeGroups(ctx, actor.TenantID, brandID, req.KnowledgeGroupIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -325,7 +325,7 @@ func (s *PromptRuleService) Update(ctx context.Context, id int64, req PromptRule
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
knowledgeGroups, err := s.validatePromptRuleKnowledgeGroups(ctx, actor.TenantID, req.KnowledgeGroupIDs)
|
||||
knowledgeGroups, err := s.validatePromptRuleKnowledgeGroups(ctx, actor.TenantID, brandID, req.KnowledgeGroupIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -535,7 +535,7 @@ func (s *PromptRuleService) loadPromptRules(ctx context.Context, tenantID, brand
|
||||
item.KnowledgeGroups = []PromptRuleKnowledgeGroup{}
|
||||
items = append(items, item)
|
||||
}
|
||||
groupMap, err := s.loadPromptRuleKnowledgeGroupMap(ctx, tenantID, collectPromptRuleIDs(items))
|
||||
groupMap, err := s.loadPromptRuleKnowledgeGroupMap(ctx, tenantID, brandID, collectPromptRuleIDs(items))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -572,7 +572,7 @@ func (s *PromptRuleService) loadPromptRuleDetail(ctx context.Context, tenantID,
|
||||
item.UpdatedAt = fmt.Sprintf("%v", updatedAt)
|
||||
item.ArticleCount = articleCount
|
||||
|
||||
groupMap, err := s.loadPromptRuleKnowledgeGroupMap(ctx, tenantID, []int64{ruleID})
|
||||
groupMap, err := s.loadPromptRuleKnowledgeGroupMap(ctx, tenantID, brandID, []int64{ruleID})
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
@@ -630,6 +630,7 @@ func (s *PromptRuleService) validatePromptRuleGroup(ctx context.Context, tenantI
|
||||
func (s *PromptRuleService) validatePromptRuleKnowledgeGroups(
|
||||
ctx context.Context,
|
||||
tenantID int64,
|
||||
brandID int64,
|
||||
groupIDs []int64,
|
||||
) ([]PromptRuleKnowledgeGroup, error) {
|
||||
groupIDs = normalizeInt64IDs(groupIDs)
|
||||
@@ -640,9 +641,9 @@ func (s *PromptRuleService) validatePromptRuleKnowledgeGroups(
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, name
|
||||
FROM knowledge_groups
|
||||
WHERE tenant_id = $1 AND id = ANY($2::bigint[]) AND deleted_at IS NULL
|
||||
WHERE tenant_id = $1 AND brand_id = $2 AND id = ANY($3::bigint[]) AND deleted_at IS NULL
|
||||
ORDER BY sort_order, created_at, id
|
||||
`, tenantID, groupIDs)
|
||||
`, tenantID, brandID, groupIDs)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50010, "knowledge_group_query_failed", "failed to query knowledge groups")
|
||||
}
|
||||
@@ -669,6 +670,7 @@ func (s *PromptRuleService) validatePromptRuleKnowledgeGroups(
|
||||
func (s *PromptRuleService) loadPromptRuleKnowledgeGroupMap(
|
||||
ctx context.Context,
|
||||
tenantID int64,
|
||||
brandID int64,
|
||||
ruleIDs []int64,
|
||||
) (map[int64][]PromptRuleKnowledgeGroup, error) {
|
||||
result := make(map[int64][]PromptRuleKnowledgeGroup, len(ruleIDs))
|
||||
@@ -683,9 +685,10 @@ func (s *PromptRuleService) loadPromptRuleKnowledgeGroupMap(
|
||||
JOIN knowledge_groups kg ON kg.id = prkg.knowledge_group_id
|
||||
WHERE prkg.prompt_rule_id = ANY($1::bigint[])
|
||||
AND kg.tenant_id = $2
|
||||
AND kg.brand_id = $3
|
||||
AND kg.deleted_at IS NULL
|
||||
ORDER BY kg.sort_order, kg.created_at, kg.id
|
||||
`, ruleIDs, tenantID)
|
||||
`, ruleIDs, tenantID, brandID)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50010, "knowledge_group_query_failed", "failed to query prompt rule knowledge groups")
|
||||
}
|
||||
|
||||
@@ -1169,7 +1169,11 @@ func (s *ScheduleTaskService) normalizeAutoPublishConfig(ctx context.Context, ac
|
||||
randomScope := ScheduleCoverRandomScopeAll
|
||||
var randomFolderID *int64
|
||||
if coverMode == ScheduleCoverModeRandom {
|
||||
randomScope, randomFolderID, err = s.normalizeScheduleCoverRandomSource(ctx, actor.TenantID, req.CoverRandomScope, req.CoverRandomFolderID)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return config, err
|
||||
}
|
||||
randomScope, randomFolderID, err = s.normalizeScheduleCoverRandomSource(ctx, actor.TenantID, brandID, req.CoverRandomScope, req.CoverRandomFolderID)
|
||||
if err != nil {
|
||||
return config, err
|
||||
}
|
||||
@@ -1218,7 +1222,7 @@ func normalizeScheduleCoverMode(value string) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ScheduleTaskService) normalizeScheduleCoverRandomSource(ctx context.Context, tenantID int64, scope string, folderID *int64) (string, *int64, error) {
|
||||
func (s *ScheduleTaskService) normalizeScheduleCoverRandomSource(ctx context.Context, tenantID, brandID int64, scope string, folderID *int64) (string, *int64, error) {
|
||||
normalizedScope := strings.TrimSpace(scope)
|
||||
if normalizedScope == "" {
|
||||
normalizedScope = ScheduleCoverRandomScopeAll
|
||||
@@ -1230,7 +1234,7 @@ func (s *ScheduleTaskService) normalizeScheduleCoverRandomSource(ctx context.Con
|
||||
if folderID == nil || *folderID <= 0 {
|
||||
return "", nil, response.ErrBadRequest(40077, "cover_random_folder_required", "请选择随机封面图片文件夹")
|
||||
}
|
||||
if err := s.ensureImageFolderExists(ctx, tenantID, *folderID); err != nil {
|
||||
if err := s.ensureImageFolderExists(ctx, tenantID, brandID, *folderID); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return ScheduleCoverRandomScopeFolder, folderID, nil
|
||||
@@ -1239,7 +1243,7 @@ func (s *ScheduleTaskService) normalizeScheduleCoverRandomSource(ctx context.Con
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ScheduleTaskService) ensureImageFolderExists(ctx context.Context, tenantID, folderID int64) error {
|
||||
func (s *ScheduleTaskService) ensureImageFolderExists(ctx context.Context, tenantID, brandID, folderID int64) error {
|
||||
var exists bool
|
||||
if err := s.pool.QueryRow(ctx, `
|
||||
SELECT EXISTS (
|
||||
@@ -1247,9 +1251,9 @@ func (s *ScheduleTaskService) ensureImageFolderExists(ctx context.Context, tenan
|
||||
FROM image_folders
|
||||
WHERE id = $1
|
||||
AND tenant_id = $2
|
||||
AND deleted_at IS NULL
|
||||
AND brand_id = $3
|
||||
)
|
||||
`, folderID, tenantID).Scan(&exists); err != nil {
|
||||
`, folderID, tenantID, brandID).Scan(&exists); err != nil {
|
||||
return response.ErrInternal(50010, "query_failed", "failed to validate image folder")
|
||||
}
|
||||
if !exists {
|
||||
|
||||
@@ -490,6 +490,7 @@ func (s *TemplateService) executeGeneration(ctx context.Context, job generationJ
|
||||
knowledgeContext, resolveErr := s.knowledge.ResolveContext(
|
||||
ctx,
|
||||
job.TenantID,
|
||||
int64(extractInt(job.Params, "brand_id")),
|
||||
knowledgeGroupIDs,
|
||||
buildGenerationKnowledgeQuery(job.TemplateKey, job.TemplateName, job.Params),
|
||||
)
|
||||
|
||||
@@ -15,24 +15,31 @@ const countImageAssets = `-- name: CountImageAssets :one
|
||||
SELECT COUNT(*)::INT AS total
|
||||
FROM image_assets
|
||||
WHERE tenant_id = $1
|
||||
AND brand_id = $2
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL
|
||||
AND (
|
||||
$2::BIGINT IS NULL
|
||||
OR ($2::BIGINT = 0 AND folder_id IS NULL)
|
||||
OR folder_id = $2::BIGINT
|
||||
$3::BIGINT IS NULL
|
||||
OR ($3::BIGINT = 0 AND folder_id IS NULL)
|
||||
OR folder_id = $3::BIGINT
|
||||
)
|
||||
AND ($3::TEXT IS NULL OR name ILIKE '%' || $3::TEXT || '%')
|
||||
AND ($4::TEXT IS NULL OR name ILIKE '%' || $4::TEXT || '%')
|
||||
`
|
||||
|
||||
type CountImageAssetsParams struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
FolderID pgtype.Int8 `json:"folder_id"`
|
||||
SearchQuery pgtype.Text `json:"search_query"`
|
||||
}
|
||||
|
||||
func (q *Queries) CountImageAssets(ctx context.Context, arg CountImageAssetsParams) (int32, error) {
|
||||
row := q.db.QueryRow(ctx, countImageAssets, arg.TenantID, arg.FolderID, arg.SearchQuery)
|
||||
row := q.db.QueryRow(ctx, countImageAssets,
|
||||
arg.TenantID,
|
||||
arg.BrandID,
|
||||
arg.FolderID,
|
||||
arg.SearchQuery,
|
||||
)
|
||||
var total int32
|
||||
err := row.Scan(&total)
|
||||
return total, err
|
||||
@@ -42,32 +49,42 @@ const countUncategorizedImages = `-- name: CountUncategorizedImages :one
|
||||
SELECT COUNT(*)::INT AS count
|
||||
FROM image_assets
|
||||
WHERE tenant_id = $1
|
||||
AND brand_id = $2
|
||||
AND folder_id IS NULL
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL
|
||||
`
|
||||
|
||||
func (q *Queries) CountUncategorizedImages(ctx context.Context, tenantID int64) (int32, error) {
|
||||
row := q.db.QueryRow(ctx, countUncategorizedImages, tenantID)
|
||||
type CountUncategorizedImagesParams struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) CountUncategorizedImages(ctx context.Context, arg CountUncategorizedImagesParams) (int32, error) {
|
||||
row := q.db.QueryRow(ctx, countUncategorizedImages, arg.TenantID, arg.BrandID)
|
||||
var count int32
|
||||
err := row.Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
const getImageAsset = `-- name: GetImageAsset :one
|
||||
SELECT id, tenant_id, folder_id, object_key, name, size_bytes, status, created_by, created_at, updated_at
|
||||
SELECT id, tenant_id, brand_id, folder_id, object_key, name, size_bytes, status, created_by, created_at, updated_at
|
||||
FROM image_assets
|
||||
WHERE id = $1 AND tenant_id = $2
|
||||
WHERE id = $1
|
||||
AND tenant_id = $2
|
||||
AND brand_id = $3
|
||||
`
|
||||
|
||||
type GetImageAssetParams struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
}
|
||||
|
||||
type GetImageAssetRow struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
FolderID pgtype.Int8 `json:"folder_id"`
|
||||
ObjectKey string `json:"object_key"`
|
||||
Name string `json:"name"`
|
||||
@@ -79,11 +96,12 @@ type GetImageAssetRow struct {
|
||||
}
|
||||
|
||||
func (q *Queries) GetImageAsset(ctx context.Context, arg GetImageAssetParams) (GetImageAssetRow, error) {
|
||||
row := q.db.QueryRow(ctx, getImageAsset, arg.ID, arg.TenantID)
|
||||
row := q.db.QueryRow(ctx, getImageAsset, arg.ID, arg.TenantID, arg.BrandID)
|
||||
var i GetImageAssetRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.TenantID,
|
||||
&i.BrandID,
|
||||
&i.FolderID,
|
||||
&i.ObjectKey,
|
||||
&i.Name,
|
||||
@@ -97,13 +115,14 @@ func (q *Queries) GetImageAsset(ctx context.Context, arg GetImageAssetParams) (G
|
||||
}
|
||||
|
||||
const insertImageAsset = `-- name: InsertImageAsset :one
|
||||
INSERT INTO image_assets (tenant_id, folder_id, object_key, name, size_bytes, status, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
INSERT INTO image_assets (tenant_id, brand_id, folder_id, object_key, name, size_bytes, status, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
type InsertImageAssetParams struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
FolderID pgtype.Int8 `json:"folder_id"`
|
||||
ObjectKey string `json:"object_key"`
|
||||
Name string `json:"name"`
|
||||
@@ -115,6 +134,7 @@ type InsertImageAssetParams struct {
|
||||
func (q *Queries) InsertImageAsset(ctx context.Context, arg InsertImageAssetParams) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, insertImageAsset,
|
||||
arg.TenantID,
|
||||
arg.BrandID,
|
||||
arg.FolderID,
|
||||
arg.ObjectKey,
|
||||
arg.Name,
|
||||
@@ -131,13 +151,15 @@ const listActiveImagesByFolder = `-- name: ListActiveImagesByFolder :many
|
||||
SELECT id, object_key, size_bytes
|
||||
FROM image_assets
|
||||
WHERE tenant_id = $1
|
||||
AND folder_id = $2
|
||||
AND brand_id = $2
|
||||
AND folder_id = $3
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL
|
||||
`
|
||||
|
||||
type ListActiveImagesByFolderParams struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
FolderID pgtype.Int8 `json:"folder_id"`
|
||||
}
|
||||
|
||||
@@ -148,7 +170,7 @@ type ListActiveImagesByFolderRow struct {
|
||||
}
|
||||
|
||||
func (q *Queries) ListActiveImagesByFolder(ctx context.Context, arg ListActiveImagesByFolderParams) ([]ListActiveImagesByFolderRow, error) {
|
||||
rows, err := q.db.Query(ctx, listActiveImagesByFolder, arg.TenantID, arg.FolderID)
|
||||
rows, err := q.db.Query(ctx, listActiveImagesByFolder, arg.TenantID, arg.BrandID, arg.FolderID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -168,23 +190,25 @@ func (q *Queries) ListActiveImagesByFolder(ctx context.Context, arg ListActiveIm
|
||||
}
|
||||
|
||||
const listImageAssets = `-- name: ListImageAssets :many
|
||||
SELECT id, tenant_id, folder_id, object_key, name, size_bytes, status, created_by, created_at, updated_at
|
||||
SELECT id, tenant_id, brand_id, folder_id, object_key, name, size_bytes, status, created_by, created_at, updated_at
|
||||
FROM image_assets
|
||||
WHERE tenant_id = $1
|
||||
AND brand_id = $2
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL
|
||||
AND (
|
||||
$2::BIGINT IS NULL
|
||||
OR ($2::BIGINT = 0 AND folder_id IS NULL)
|
||||
OR folder_id = $2::BIGINT
|
||||
$3::BIGINT IS NULL
|
||||
OR ($3::BIGINT = 0 AND folder_id IS NULL)
|
||||
OR folder_id = $3::BIGINT
|
||||
)
|
||||
AND ($3::TEXT IS NULL OR name ILIKE '%' || $3::TEXT || '%')
|
||||
AND ($4::TEXT IS NULL OR name ILIKE '%' || $4::TEXT || '%')
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $5 OFFSET $4
|
||||
LIMIT $6 OFFSET $5
|
||||
`
|
||||
|
||||
type ListImageAssetsParams struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
FolderID pgtype.Int8 `json:"folder_id"`
|
||||
SearchQuery pgtype.Text `json:"search_query"`
|
||||
PageOffset int32 `json:"page_offset"`
|
||||
@@ -194,6 +218,7 @@ type ListImageAssetsParams struct {
|
||||
type ListImageAssetsRow struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
FolderID pgtype.Int8 `json:"folder_id"`
|
||||
ObjectKey string `json:"object_key"`
|
||||
Name string `json:"name"`
|
||||
@@ -207,6 +232,7 @@ type ListImageAssetsRow struct {
|
||||
func (q *Queries) ListImageAssets(ctx context.Context, arg ListImageAssetsParams) ([]ListImageAssetsRow, error) {
|
||||
rows, err := q.db.Query(ctx, listImageAssets,
|
||||
arg.TenantID,
|
||||
arg.BrandID,
|
||||
arg.FolderID,
|
||||
arg.SearchQuery,
|
||||
arg.PageOffset,
|
||||
@@ -222,6 +248,7 @@ func (q *Queries) ListImageAssets(ctx context.Context, arg ListImageAssetsParams
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.TenantID,
|
||||
&i.BrandID,
|
||||
&i.FolderID,
|
||||
&i.ObjectKey,
|
||||
&i.Name,
|
||||
@@ -245,84 +272,113 @@ const markImagesInFolderPendingDelete = `-- name: MarkImagesInFolderPendingDelet
|
||||
UPDATE image_assets
|
||||
SET status = 'pending_delete', folder_id = NULL, updated_at = NOW()
|
||||
WHERE tenant_id = $1
|
||||
AND folder_id = $2
|
||||
AND brand_id = $2
|
||||
AND folder_id = $3
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL
|
||||
`
|
||||
|
||||
type MarkImagesInFolderPendingDeleteParams struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
FolderID pgtype.Int8 `json:"folder_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) MarkImagesInFolderPendingDelete(ctx context.Context, arg MarkImagesInFolderPendingDeleteParams) error {
|
||||
_, err := q.db.Exec(ctx, markImagesInFolderPendingDelete, arg.TenantID, arg.FolderID)
|
||||
_, err := q.db.Exec(ctx, markImagesInFolderPendingDelete, arg.TenantID, arg.BrandID, arg.FolderID)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateImageAssetDeleted = `-- name: UpdateImageAssetDeleted :exec
|
||||
UPDATE image_assets
|
||||
SET status = 'deleted', deleted_at = NOW(), updated_at = NOW()
|
||||
WHERE id = $1 AND tenant_id = $2
|
||||
WHERE id = $1
|
||||
AND tenant_id = $2
|
||||
AND brand_id = $3
|
||||
`
|
||||
|
||||
type UpdateImageAssetDeletedParams struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateImageAssetDeleted(ctx context.Context, arg UpdateImageAssetDeletedParams) error {
|
||||
_, err := q.db.Exec(ctx, updateImageAssetDeleted, arg.ID, arg.TenantID)
|
||||
_, err := q.db.Exec(ctx, updateImageAssetDeleted, arg.ID, arg.TenantID, arg.BrandID)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateImageAssetFolder = `-- name: UpdateImageAssetFolder :exec
|
||||
UPDATE image_assets
|
||||
SET folder_id = $1, updated_at = NOW()
|
||||
WHERE id = $2 AND tenant_id = $3
|
||||
WHERE id = $2
|
||||
AND tenant_id = $3
|
||||
AND brand_id = $4
|
||||
`
|
||||
|
||||
type UpdateImageAssetFolderParams struct {
|
||||
FolderID pgtype.Int8 `json:"folder_id"`
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateImageAssetFolder(ctx context.Context, arg UpdateImageAssetFolderParams) error {
|
||||
_, err := q.db.Exec(ctx, updateImageAssetFolder, arg.FolderID, arg.ID, arg.TenantID)
|
||||
_, err := q.db.Exec(ctx, updateImageAssetFolder,
|
||||
arg.FolderID,
|
||||
arg.ID,
|
||||
arg.TenantID,
|
||||
arg.BrandID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateImageAssetName = `-- name: UpdateImageAssetName :exec
|
||||
UPDATE image_assets
|
||||
SET name = $1, updated_at = NOW()
|
||||
WHERE id = $2 AND tenant_id = $3
|
||||
WHERE id = $2
|
||||
AND tenant_id = $3
|
||||
AND brand_id = $4
|
||||
`
|
||||
|
||||
type UpdateImageAssetNameParams struct {
|
||||
Name string `json:"name"`
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateImageAssetName(ctx context.Context, arg UpdateImageAssetNameParams) error {
|
||||
_, err := q.db.Exec(ctx, updateImageAssetName, arg.Name, arg.ID, arg.TenantID)
|
||||
_, err := q.db.Exec(ctx, updateImageAssetName,
|
||||
arg.Name,
|
||||
arg.ID,
|
||||
arg.TenantID,
|
||||
arg.BrandID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateImageAssetStatus = `-- name: UpdateImageAssetStatus :exec
|
||||
UPDATE image_assets
|
||||
SET status = $1, updated_at = NOW()
|
||||
WHERE id = $2 AND tenant_id = $3
|
||||
WHERE id = $2
|
||||
AND tenant_id = $3
|
||||
AND brand_id = $4
|
||||
`
|
||||
|
||||
type UpdateImageAssetStatusParams struct {
|
||||
Status string `json:"status"`
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateImageAssetStatus(ctx context.Context, arg UpdateImageAssetStatusParams) error {
|
||||
_, err := q.db.Exec(ctx, updateImageAssetStatus, arg.Status, arg.ID, arg.TenantID)
|
||||
_, err := q.db.Exec(ctx, updateImageAssetStatus,
|
||||
arg.Status,
|
||||
arg.ID,
|
||||
arg.TenantID,
|
||||
arg.BrandID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -15,36 +15,39 @@ const countActiveImagesInFolder = `-- name: CountActiveImagesInFolder :one
|
||||
SELECT COUNT(*)::INT AS count
|
||||
FROM image_assets
|
||||
WHERE tenant_id = $1
|
||||
AND folder_id = $2
|
||||
AND brand_id = $2
|
||||
AND folder_id = $3
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL
|
||||
`
|
||||
|
||||
type CountActiveImagesInFolderParams struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
FolderID pgtype.Int8 `json:"folder_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) CountActiveImagesInFolder(ctx context.Context, arg CountActiveImagesInFolderParams) (int32, error) {
|
||||
row := q.db.QueryRow(ctx, countActiveImagesInFolder, arg.TenantID, arg.FolderID)
|
||||
row := q.db.QueryRow(ctx, countActiveImagesInFolder, arg.TenantID, arg.BrandID, arg.FolderID)
|
||||
var count int32
|
||||
err := row.Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
const createImageFolder = `-- name: CreateImageFolder :one
|
||||
INSERT INTO image_folders (tenant_id, name)
|
||||
VALUES ($1, $2)
|
||||
INSERT INTO image_folders (tenant_id, brand_id, name)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
type CreateImageFolderParams struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateImageFolder(ctx context.Context, arg CreateImageFolderParams) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, createImageFolder, arg.TenantID, arg.Name)
|
||||
row := q.db.QueryRow(ctx, createImageFolder, arg.TenantID, arg.BrandID, arg.Name)
|
||||
var id int64
|
||||
err := row.Scan(&id)
|
||||
return id, err
|
||||
@@ -52,44 +55,52 @@ func (q *Queries) CreateImageFolder(ctx context.Context, arg CreateImageFolderPa
|
||||
|
||||
const deleteImageFolder = `-- name: DeleteImageFolder :exec
|
||||
DELETE FROM image_folders
|
||||
WHERE id = $1 AND tenant_id = $2
|
||||
WHERE id = $1
|
||||
AND tenant_id = $2
|
||||
AND brand_id = $3
|
||||
`
|
||||
|
||||
type DeleteImageFolderParams struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) DeleteImageFolder(ctx context.Context, arg DeleteImageFolderParams) error {
|
||||
_, err := q.db.Exec(ctx, deleteImageFolder, arg.ID, arg.TenantID)
|
||||
_, err := q.db.Exec(ctx, deleteImageFolder, arg.ID, arg.TenantID, arg.BrandID)
|
||||
return err
|
||||
}
|
||||
|
||||
const getImageFolder = `-- name: GetImageFolder :one
|
||||
SELECT id, tenant_id, name, created_at, updated_at
|
||||
SELECT id, tenant_id, brand_id, name, created_at, updated_at
|
||||
FROM image_folders
|
||||
WHERE id = $1 AND tenant_id = $2
|
||||
WHERE id = $1
|
||||
AND tenant_id = $2
|
||||
AND brand_id = $3
|
||||
`
|
||||
|
||||
type GetImageFolderParams struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
}
|
||||
|
||||
type GetImageFolderRow struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetImageFolder(ctx context.Context, arg GetImageFolderParams) (GetImageFolderRow, error) {
|
||||
row := q.db.QueryRow(ctx, getImageFolder, arg.ID, arg.TenantID)
|
||||
row := q.db.QueryRow(ctx, getImageFolder, arg.ID, arg.TenantID, arg.BrandID)
|
||||
var i GetImageFolderRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.TenantID,
|
||||
&i.BrandID,
|
||||
&i.Name,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
@@ -98,26 +109,33 @@ func (q *Queries) GetImageFolder(ctx context.Context, arg GetImageFolderParams)
|
||||
}
|
||||
|
||||
const listImageFolders = `-- name: ListImageFolders :many
|
||||
SELECT f.id, f.tenant_id, f.name, f.created_at, f.updated_at,
|
||||
SELECT f.id, f.tenant_id, f.brand_id, f.name, f.created_at, f.updated_at,
|
||||
COUNT(ia.id) FILTER (WHERE ia.status = 'active' AND ia.deleted_at IS NULL)::INT AS image_count
|
||||
FROM image_folders f
|
||||
LEFT JOIN image_assets ia ON ia.tenant_id = f.tenant_id AND ia.folder_id = f.id
|
||||
LEFT JOIN image_assets ia ON ia.tenant_id = f.tenant_id AND ia.brand_id = f.brand_id AND ia.folder_id = f.id
|
||||
WHERE f.tenant_id = $1
|
||||
AND f.brand_id = $2
|
||||
GROUP BY f.id
|
||||
ORDER BY f.name
|
||||
`
|
||||
|
||||
type ListImageFoldersParams struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
}
|
||||
|
||||
type ListImageFoldersRow struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
ImageCount int32 `json:"image_count"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListImageFolders(ctx context.Context, tenantID int64) ([]ListImageFoldersRow, error) {
|
||||
rows, err := q.db.Query(ctx, listImageFolders, tenantID)
|
||||
func (q *Queries) ListImageFolders(ctx context.Context, arg ListImageFoldersParams) ([]ListImageFoldersRow, error) {
|
||||
rows, err := q.db.Query(ctx, listImageFolders, arg.TenantID, arg.BrandID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -128,6 +146,7 @@ func (q *Queries) ListImageFolders(ctx context.Context, tenantID int64) ([]ListI
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.TenantID,
|
||||
&i.BrandID,
|
||||
&i.Name,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
@@ -146,16 +165,24 @@ func (q *Queries) ListImageFolders(ctx context.Context, tenantID int64) ([]ListI
|
||||
const updateImageFolder = `-- name: UpdateImageFolder :exec
|
||||
UPDATE image_folders
|
||||
SET name = $1, updated_at = NOW()
|
||||
WHERE id = $2 AND tenant_id = $3
|
||||
WHERE id = $2
|
||||
AND tenant_id = $3
|
||||
AND brand_id = $4
|
||||
`
|
||||
|
||||
type UpdateImageFolderParams struct {
|
||||
Name string `json:"name"`
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateImageFolder(ctx context.Context, arg UpdateImageFolderParams) error {
|
||||
_, err := q.db.Exec(ctx, updateImageFolder, arg.Name, arg.ID, arg.TenantID)
|
||||
_, err := q.db.Exec(ctx, updateImageFolder,
|
||||
arg.Name,
|
||||
arg.ID,
|
||||
arg.TenantID,
|
||||
arg.BrandID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -460,6 +460,7 @@ type ImageAsset struct {
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
|
||||
ContentHash pgtype.Text `json:"content_hash"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
}
|
||||
|
||||
type ImageAssetReference struct {
|
||||
@@ -478,6 +479,7 @@ type ImageFolder struct {
|
||||
Name string `json:"name"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
}
|
||||
|
||||
type KnowledgeChunksMetum struct {
|
||||
@@ -517,6 +519,7 @@ type KnowledgeGroup struct {
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
}
|
||||
|
||||
type KnowledgeItem struct {
|
||||
@@ -536,6 +539,7 @@ type KnowledgeItem struct {
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
|
||||
MarkdownContent pgtype.Text `json:"markdown_content"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
}
|
||||
|
||||
type KnowledgeParseTask struct {
|
||||
|
||||
@@ -28,7 +28,7 @@ type Querier interface {
|
||||
CountPromptRulesUngrouped(ctx context.Context, arg CountPromptRulesUngroupedParams) (int64, error)
|
||||
CountPublishedArticlesByTenantAndBrand(ctx context.Context, arg CountPublishedArticlesByTenantAndBrandParams) (int64, error)
|
||||
CountScheduleTasks(ctx context.Context, arg CountScheduleTasksParams) (int64, error)
|
||||
CountUncategorizedImages(ctx context.Context, tenantID int64) (int32, error)
|
||||
CountUncategorizedImages(ctx context.Context, arg CountUncategorizedImagesParams) (int32, error)
|
||||
CreateArticle(ctx context.Context, arg CreateArticleParams) (int64, error)
|
||||
CreateArticleVersion(ctx context.Context, arg CreateArticleVersionParams) (int64, error)
|
||||
CreateAuditLog(ctx context.Context, arg CreateAuditLogParams) error
|
||||
@@ -121,7 +121,7 @@ type Querier interface {
|
||||
ListDesktopClientsByUser(ctx context.Context, arg ListDesktopClientsByUserParams) ([]DesktopClient, error)
|
||||
ListDesktopClientsByWorkspace(ctx context.Context, workspaceID int64) ([]DesktopClient, error)
|
||||
ListImageAssets(ctx context.Context, arg ListImageAssetsParams) ([]ListImageAssetsRow, error)
|
||||
ListImageFolders(ctx context.Context, tenantID int64) ([]ListImageFoldersRow, error)
|
||||
ListImageFolders(ctx context.Context, arg ListImageFoldersParams) ([]ListImageFoldersRow, error)
|
||||
ListImageReferenceArticles(ctx context.Context, arg ListImageReferenceArticlesParams) ([]ListImageReferenceArticlesRow, error)
|
||||
ListImageReferences(ctx context.Context, arg ListImageReferencesParams) ([]ListImageReferencesRow, error)
|
||||
ListKeywords(ctx context.Context, arg ListKeywordsParams) ([]ListKeywordsRow, error)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
type ImageFolder struct {
|
||||
ID int64
|
||||
TenantID int64
|
||||
BrandID int64
|
||||
Name string
|
||||
ImageCount int
|
||||
CreatedAt time.Time
|
||||
@@ -17,12 +18,12 @@ type ImageFolder struct {
|
||||
}
|
||||
|
||||
type ImageFolderRepository interface {
|
||||
List(ctx context.Context, tenantID int64) ([]ImageFolder, error)
|
||||
Get(ctx context.Context, tenantID, id int64) (*ImageFolder, error)
|
||||
Create(ctx context.Context, tenantID int64, name string) (int64, error)
|
||||
Update(ctx context.Context, tenantID, id int64, name string) error
|
||||
Delete(ctx context.Context, tenantID, id int64) error
|
||||
CountActiveImages(ctx context.Context, tenantID, folderID int64) (int, error)
|
||||
List(ctx context.Context, tenantID, brandID int64) ([]ImageFolder, error)
|
||||
Get(ctx context.Context, tenantID, brandID, id int64) (*ImageFolder, error)
|
||||
Create(ctx context.Context, tenantID, brandID int64, name string) (int64, error)
|
||||
Update(ctx context.Context, tenantID, brandID, id int64, name string) error
|
||||
Delete(ctx context.Context, tenantID, brandID, id int64) error
|
||||
CountActiveImages(ctx context.Context, tenantID, brandID, folderID int64) (int, error)
|
||||
}
|
||||
|
||||
type imageFolderRepository struct {
|
||||
@@ -33,8 +34,11 @@ func NewImageFolderRepository(db generated.DBTX) ImageFolderRepository {
|
||||
return &imageFolderRepository{q: newQuerier(db)}
|
||||
}
|
||||
|
||||
func (r *imageFolderRepository) List(ctx context.Context, tenantID int64) ([]ImageFolder, error) {
|
||||
rows, err := r.q.ListImageFolders(ctx, tenantID)
|
||||
func (r *imageFolderRepository) List(ctx context.Context, tenantID, brandID int64) ([]ImageFolder, error) {
|
||||
rows, err := r.q.ListImageFolders(ctx, generated.ListImageFoldersParams{
|
||||
TenantID: tenantID,
|
||||
BrandID: brandID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -44,6 +48,7 @@ func (r *imageFolderRepository) List(ctx context.Context, tenantID int64) ([]Ima
|
||||
folders[i] = ImageFolder{
|
||||
ID: row.ID,
|
||||
TenantID: row.TenantID,
|
||||
BrandID: row.BrandID,
|
||||
Name: row.Name,
|
||||
ImageCount: int(row.ImageCount),
|
||||
CreatedAt: timeFromTimestamp(row.CreatedAt),
|
||||
@@ -53,10 +58,11 @@ func (r *imageFolderRepository) List(ctx context.Context, tenantID int64) ([]Ima
|
||||
return folders, nil
|
||||
}
|
||||
|
||||
func (r *imageFolderRepository) Get(ctx context.Context, tenantID, id int64) (*ImageFolder, error) {
|
||||
func (r *imageFolderRepository) Get(ctx context.Context, tenantID, brandID, id int64) (*ImageFolder, error) {
|
||||
row, err := r.q.GetImageFolder(ctx, generated.GetImageFolderParams{
|
||||
ID: id,
|
||||
TenantID: tenantID,
|
||||
BrandID: brandID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -65,37 +71,42 @@ func (r *imageFolderRepository) Get(ctx context.Context, tenantID, id int64) (*I
|
||||
return &ImageFolder{
|
||||
ID: row.ID,
|
||||
TenantID: row.TenantID,
|
||||
BrandID: row.BrandID,
|
||||
Name: row.Name,
|
||||
CreatedAt: timeFromTimestamp(row.CreatedAt),
|
||||
UpdatedAt: timeFromTimestamp(row.UpdatedAt),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *imageFolderRepository) Create(ctx context.Context, tenantID int64, name string) (int64, error) {
|
||||
func (r *imageFolderRepository) Create(ctx context.Context, tenantID, brandID int64, name string) (int64, error) {
|
||||
return r.q.CreateImageFolder(ctx, generated.CreateImageFolderParams{
|
||||
TenantID: tenantID,
|
||||
BrandID: brandID,
|
||||
Name: name,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *imageFolderRepository) Update(ctx context.Context, tenantID, id int64, name string) error {
|
||||
func (r *imageFolderRepository) Update(ctx context.Context, tenantID, brandID, id int64, name string) error {
|
||||
return r.q.UpdateImageFolder(ctx, generated.UpdateImageFolderParams{
|
||||
Name: name,
|
||||
ID: id,
|
||||
TenantID: tenantID,
|
||||
BrandID: brandID,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *imageFolderRepository) Delete(ctx context.Context, tenantID, id int64) error {
|
||||
func (r *imageFolderRepository) Delete(ctx context.Context, tenantID, brandID, id int64) error {
|
||||
return r.q.DeleteImageFolder(ctx, generated.DeleteImageFolderParams{
|
||||
ID: id,
|
||||
TenantID: tenantID,
|
||||
BrandID: brandID,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *imageFolderRepository) CountActiveImages(ctx context.Context, tenantID, folderID int64) (int, error) {
|
||||
func (r *imageFolderRepository) CountActiveImages(ctx context.Context, tenantID, brandID, folderID int64) (int, error) {
|
||||
count, err := r.q.CountActiveImagesInFolder(ctx, generated.CountActiveImagesInFolderParams{
|
||||
TenantID: tenantID,
|
||||
BrandID: brandID,
|
||||
FolderID: pgInt8(&folderID),
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
type ImageAsset struct {
|
||||
ID int64
|
||||
TenantID int64
|
||||
BrandID int64
|
||||
FolderID *int64
|
||||
ObjectKey string
|
||||
Name string
|
||||
@@ -22,6 +23,7 @@ type ImageAsset struct {
|
||||
|
||||
type ImageAssetListParams struct {
|
||||
TenantID int64
|
||||
BrandID int64
|
||||
FolderID *int64
|
||||
SearchQuery *string
|
||||
PageSize int
|
||||
@@ -30,6 +32,7 @@ type ImageAssetListParams struct {
|
||||
|
||||
type InsertImageAssetInput struct {
|
||||
TenantID int64
|
||||
BrandID int64
|
||||
FolderID *int64
|
||||
ObjectKey string
|
||||
Name string
|
||||
@@ -46,15 +49,15 @@ type FolderImageSummary struct {
|
||||
|
||||
type ImageRepository interface {
|
||||
List(ctx context.Context, params ImageAssetListParams) ([]ImageAsset, int, error)
|
||||
Get(ctx context.Context, tenantID, id int64) (*ImageAsset, error)
|
||||
Get(ctx context.Context, tenantID, brandID, id int64) (*ImageAsset, error)
|
||||
Insert(ctx context.Context, input InsertImageAssetInput) (int64, error)
|
||||
UpdateStatus(ctx context.Context, tenantID, id int64, status string) error
|
||||
MarkDeleted(ctx context.Context, tenantID, id int64) error
|
||||
UpdateName(ctx context.Context, tenantID, id int64, name string) error
|
||||
UpdateFolder(ctx context.Context, tenantID, id int64, folderID *int64) error
|
||||
ListActiveByFolder(ctx context.Context, tenantID, folderID int64) ([]FolderImageSummary, error)
|
||||
MarkImagesInFolderPendingDelete(ctx context.Context, tenantID, folderID int64) error
|
||||
CountUncategorized(ctx context.Context, tenantID int64) (int, error)
|
||||
UpdateStatus(ctx context.Context, tenantID, brandID, id int64, status string) error
|
||||
MarkDeleted(ctx context.Context, tenantID, brandID, id int64) error
|
||||
UpdateName(ctx context.Context, tenantID, brandID, id int64, name string) error
|
||||
UpdateFolder(ctx context.Context, tenantID, brandID, id int64, folderID *int64) error
|
||||
ListActiveByFolder(ctx context.Context, tenantID, brandID, folderID int64) ([]FolderImageSummary, error)
|
||||
MarkImagesInFolderPendingDelete(ctx context.Context, tenantID, brandID, folderID int64) error
|
||||
CountUncategorized(ctx context.Context, tenantID, brandID int64) (int, error)
|
||||
}
|
||||
|
||||
type imageRepository struct {
|
||||
@@ -68,6 +71,7 @@ func NewImageRepository(db generated.DBTX) ImageRepository {
|
||||
func (r *imageRepository) List(ctx context.Context, params ImageAssetListParams) ([]ImageAsset, int, error) {
|
||||
listParams := generated.ListImageAssetsParams{
|
||||
TenantID: params.TenantID,
|
||||
BrandID: params.BrandID,
|
||||
FolderID: pgInt8(params.FolderID),
|
||||
PageOffset: int32(params.PageOffset),
|
||||
PageSize: int32(params.PageSize),
|
||||
@@ -81,6 +85,7 @@ func (r *imageRepository) List(ctx context.Context, params ImageAssetListParams)
|
||||
|
||||
total, err := r.q.CountImageAssets(ctx, generated.CountImageAssetsParams{
|
||||
TenantID: params.TenantID,
|
||||
BrandID: params.BrandID,
|
||||
FolderID: listParams.FolderID,
|
||||
SearchQuery: listParams.SearchQuery,
|
||||
})
|
||||
@@ -93,6 +98,7 @@ func (r *imageRepository) List(ctx context.Context, params ImageAssetListParams)
|
||||
assets[i] = ImageAsset{
|
||||
ID: row.ID,
|
||||
TenantID: row.TenantID,
|
||||
BrandID: row.BrandID,
|
||||
FolderID: nullableInt64(row.FolderID),
|
||||
ObjectKey: row.ObjectKey,
|
||||
Name: row.Name,
|
||||
@@ -107,10 +113,11 @@ func (r *imageRepository) List(ctx context.Context, params ImageAssetListParams)
|
||||
return assets, int(total), nil
|
||||
}
|
||||
|
||||
func (r *imageRepository) Get(ctx context.Context, tenantID, id int64) (*ImageAsset, error) {
|
||||
func (r *imageRepository) Get(ctx context.Context, tenantID, brandID, id int64) (*ImageAsset, error) {
|
||||
row, err := r.q.GetImageAsset(ctx, generated.GetImageAssetParams{
|
||||
ID: id,
|
||||
TenantID: tenantID,
|
||||
BrandID: brandID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -119,6 +126,7 @@ func (r *imageRepository) Get(ctx context.Context, tenantID, id int64) (*ImageAs
|
||||
return &ImageAsset{
|
||||
ID: row.ID,
|
||||
TenantID: row.TenantID,
|
||||
BrandID: row.BrandID,
|
||||
FolderID: nullableInt64(row.FolderID),
|
||||
ObjectKey: row.ObjectKey,
|
||||
Name: row.Name,
|
||||
@@ -133,6 +141,7 @@ func (r *imageRepository) Get(ctx context.Context, tenantID, id int64) (*ImageAs
|
||||
func (r *imageRepository) Insert(ctx context.Context, input InsertImageAssetInput) (int64, error) {
|
||||
return r.q.InsertImageAsset(ctx, generated.InsertImageAssetParams{
|
||||
TenantID: input.TenantID,
|
||||
BrandID: input.BrandID,
|
||||
FolderID: pgInt8(input.FolderID),
|
||||
ObjectKey: input.ObjectKey,
|
||||
Name: input.Name,
|
||||
@@ -142,40 +151,45 @@ func (r *imageRepository) Insert(ctx context.Context, input InsertImageAssetInpu
|
||||
})
|
||||
}
|
||||
|
||||
func (r *imageRepository) UpdateStatus(ctx context.Context, tenantID, id int64, status string) error {
|
||||
func (r *imageRepository) UpdateStatus(ctx context.Context, tenantID, brandID, id int64, status string) error {
|
||||
return r.q.UpdateImageAssetStatus(ctx, generated.UpdateImageAssetStatusParams{
|
||||
Status: status,
|
||||
ID: id,
|
||||
TenantID: tenantID,
|
||||
BrandID: brandID,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *imageRepository) MarkDeleted(ctx context.Context, tenantID, id int64) error {
|
||||
func (r *imageRepository) MarkDeleted(ctx context.Context, tenantID, brandID, id int64) error {
|
||||
return r.q.UpdateImageAssetDeleted(ctx, generated.UpdateImageAssetDeletedParams{
|
||||
ID: id,
|
||||
TenantID: tenantID,
|
||||
BrandID: brandID,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *imageRepository) UpdateName(ctx context.Context, tenantID, id int64, name string) error {
|
||||
func (r *imageRepository) UpdateName(ctx context.Context, tenantID, brandID, id int64, name string) error {
|
||||
return r.q.UpdateImageAssetName(ctx, generated.UpdateImageAssetNameParams{
|
||||
Name: name,
|
||||
ID: id,
|
||||
TenantID: tenantID,
|
||||
BrandID: brandID,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *imageRepository) UpdateFolder(ctx context.Context, tenantID, id int64, folderID *int64) error {
|
||||
func (r *imageRepository) UpdateFolder(ctx context.Context, tenantID, brandID, id int64, folderID *int64) error {
|
||||
return r.q.UpdateImageAssetFolder(ctx, generated.UpdateImageAssetFolderParams{
|
||||
FolderID: pgInt8(folderID),
|
||||
ID: id,
|
||||
TenantID: tenantID,
|
||||
BrandID: brandID,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *imageRepository) ListActiveByFolder(ctx context.Context, tenantID, folderID int64) ([]FolderImageSummary, error) {
|
||||
func (r *imageRepository) ListActiveByFolder(ctx context.Context, tenantID, brandID, folderID int64) ([]FolderImageSummary, error) {
|
||||
rows, err := r.q.ListActiveImagesByFolder(ctx, generated.ListActiveImagesByFolderParams{
|
||||
TenantID: tenantID,
|
||||
BrandID: brandID,
|
||||
FolderID: pgInt8(&folderID),
|
||||
})
|
||||
if err != nil {
|
||||
@@ -193,15 +207,19 @@ func (r *imageRepository) ListActiveByFolder(ctx context.Context, tenantID, fold
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *imageRepository) MarkImagesInFolderPendingDelete(ctx context.Context, tenantID, folderID int64) error {
|
||||
func (r *imageRepository) MarkImagesInFolderPendingDelete(ctx context.Context, tenantID, brandID, folderID int64) error {
|
||||
return r.q.MarkImagesInFolderPendingDelete(ctx, generated.MarkImagesInFolderPendingDeleteParams{
|
||||
TenantID: tenantID,
|
||||
BrandID: brandID,
|
||||
FolderID: pgInt8(&folderID),
|
||||
})
|
||||
}
|
||||
|
||||
func (r *imageRepository) CountUncategorized(ctx context.Context, tenantID int64) (int, error) {
|
||||
count, err := r.q.CountUncategorizedImages(ctx, tenantID)
|
||||
func (r *imageRepository) CountUncategorized(ctx context.Context, tenantID, brandID int64) (int, error) {
|
||||
count, err := r.q.CountUncategorizedImages(ctx, generated.CountUncategorizedImagesParams{
|
||||
TenantID: tenantID,
|
||||
BrandID: brandID,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
-- name: ListImageAssets :many
|
||||
SELECT id, tenant_id, folder_id, object_key, name, size_bytes, status, created_by, created_at, updated_at
|
||||
SELECT id, tenant_id, brand_id, folder_id, object_key, name, size_bytes, status, created_by, created_at, updated_at
|
||||
FROM image_assets
|
||||
WHERE tenant_id = sqlc.arg(tenant_id)
|
||||
AND brand_id = sqlc.arg(brand_id)
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL
|
||||
AND (
|
||||
@@ -17,6 +18,7 @@ LIMIT sqlc.arg(page_size) OFFSET sqlc.arg(page_offset);
|
||||
SELECT COUNT(*)::INT AS total
|
||||
FROM image_assets
|
||||
WHERE tenant_id = sqlc.arg(tenant_id)
|
||||
AND brand_id = sqlc.arg(brand_id)
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL
|
||||
AND (
|
||||
@@ -27,39 +29,50 @@ WHERE tenant_id = sqlc.arg(tenant_id)
|
||||
AND (sqlc.narg(search_query)::TEXT IS NULL OR name ILIKE '%' || sqlc.narg(search_query)::TEXT || '%');
|
||||
|
||||
-- name: GetImageAsset :one
|
||||
SELECT id, tenant_id, folder_id, object_key, name, size_bytes, status, created_by, created_at, updated_at
|
||||
SELECT id, tenant_id, brand_id, folder_id, object_key, name, size_bytes, status, created_by, created_at, updated_at
|
||||
FROM image_assets
|
||||
WHERE id = sqlc.arg(id) AND tenant_id = sqlc.arg(tenant_id);
|
||||
WHERE id = sqlc.arg(id)
|
||||
AND tenant_id = sqlc.arg(tenant_id)
|
||||
AND brand_id = sqlc.arg(brand_id);
|
||||
|
||||
-- name: InsertImageAsset :one
|
||||
INSERT INTO image_assets (tenant_id, folder_id, object_key, name, size_bytes, status, created_by)
|
||||
VALUES (sqlc.arg(tenant_id), sqlc.narg(folder_id), sqlc.arg(object_key), sqlc.arg(name), sqlc.arg(size_bytes), sqlc.arg(status), sqlc.arg(created_by))
|
||||
INSERT INTO image_assets (tenant_id, brand_id, folder_id, object_key, name, size_bytes, status, created_by)
|
||||
VALUES (sqlc.arg(tenant_id), sqlc.arg(brand_id), sqlc.narg(folder_id), sqlc.arg(object_key), sqlc.arg(name), sqlc.arg(size_bytes), sqlc.arg(status), sqlc.arg(created_by))
|
||||
RETURNING id;
|
||||
|
||||
-- name: UpdateImageAssetStatus :exec
|
||||
UPDATE image_assets
|
||||
SET status = sqlc.arg(status), updated_at = NOW()
|
||||
WHERE id = sqlc.arg(id) AND tenant_id = sqlc.arg(tenant_id);
|
||||
WHERE id = sqlc.arg(id)
|
||||
AND tenant_id = sqlc.arg(tenant_id)
|
||||
AND brand_id = sqlc.arg(brand_id);
|
||||
|
||||
-- name: UpdateImageAssetDeleted :exec
|
||||
UPDATE image_assets
|
||||
SET status = 'deleted', deleted_at = NOW(), updated_at = NOW()
|
||||
WHERE id = sqlc.arg(id) AND tenant_id = sqlc.arg(tenant_id);
|
||||
WHERE id = sqlc.arg(id)
|
||||
AND tenant_id = sqlc.arg(tenant_id)
|
||||
AND brand_id = sqlc.arg(brand_id);
|
||||
|
||||
-- name: UpdateImageAssetName :exec
|
||||
UPDATE image_assets
|
||||
SET name = sqlc.arg(name), updated_at = NOW()
|
||||
WHERE id = sqlc.arg(id) AND tenant_id = sqlc.arg(tenant_id);
|
||||
WHERE id = sqlc.arg(id)
|
||||
AND tenant_id = sqlc.arg(tenant_id)
|
||||
AND brand_id = sqlc.arg(brand_id);
|
||||
|
||||
-- name: UpdateImageAssetFolder :exec
|
||||
UPDATE image_assets
|
||||
SET folder_id = sqlc.narg(folder_id), updated_at = NOW()
|
||||
WHERE id = sqlc.arg(id) AND tenant_id = sqlc.arg(tenant_id);
|
||||
WHERE id = sqlc.arg(id)
|
||||
AND tenant_id = sqlc.arg(tenant_id)
|
||||
AND brand_id = sqlc.arg(brand_id);
|
||||
|
||||
-- name: ListActiveImagesByFolder :many
|
||||
SELECT id, object_key, size_bytes
|
||||
FROM image_assets
|
||||
WHERE tenant_id = sqlc.arg(tenant_id)
|
||||
AND brand_id = sqlc.arg(brand_id)
|
||||
AND folder_id = sqlc.arg(folder_id)
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL;
|
||||
@@ -68,6 +81,7 @@ WHERE tenant_id = sqlc.arg(tenant_id)
|
||||
UPDATE image_assets
|
||||
SET status = 'pending_delete', folder_id = NULL, updated_at = NOW()
|
||||
WHERE tenant_id = sqlc.arg(tenant_id)
|
||||
AND brand_id = sqlc.arg(brand_id)
|
||||
AND folder_id = sqlc.arg(folder_id)
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL;
|
||||
@@ -76,6 +90,7 @@ WHERE tenant_id = sqlc.arg(tenant_id)
|
||||
SELECT COUNT(*)::INT AS count
|
||||
FROM image_assets
|
||||
WHERE tenant_id = sqlc.arg(tenant_id)
|
||||
AND brand_id = sqlc.arg(brand_id)
|
||||
AND folder_id IS NULL
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL;
|
||||
|
||||
@@ -1,35 +1,43 @@
|
||||
-- name: ListImageFolders :many
|
||||
SELECT f.id, f.tenant_id, f.name, f.created_at, f.updated_at,
|
||||
SELECT f.id, f.tenant_id, f.brand_id, f.name, f.created_at, f.updated_at,
|
||||
COUNT(ia.id) FILTER (WHERE ia.status = 'active' AND ia.deleted_at IS NULL)::INT AS image_count
|
||||
FROM image_folders f
|
||||
LEFT JOIN image_assets ia ON ia.tenant_id = f.tenant_id AND ia.folder_id = f.id
|
||||
LEFT JOIN image_assets ia ON ia.tenant_id = f.tenant_id AND ia.brand_id = f.brand_id AND ia.folder_id = f.id
|
||||
WHERE f.tenant_id = sqlc.arg(tenant_id)
|
||||
AND f.brand_id = sqlc.arg(brand_id)
|
||||
GROUP BY f.id
|
||||
ORDER BY f.name;
|
||||
|
||||
-- name: CreateImageFolder :one
|
||||
INSERT INTO image_folders (tenant_id, name)
|
||||
VALUES (sqlc.arg(tenant_id), sqlc.arg(name))
|
||||
INSERT INTO image_folders (tenant_id, brand_id, name)
|
||||
VALUES (sqlc.arg(tenant_id), sqlc.arg(brand_id), sqlc.arg(name))
|
||||
RETURNING id;
|
||||
|
||||
-- name: UpdateImageFolder :exec
|
||||
UPDATE image_folders
|
||||
SET name = sqlc.arg(name), updated_at = NOW()
|
||||
WHERE id = sqlc.arg(id) AND tenant_id = sqlc.arg(tenant_id);
|
||||
WHERE id = sqlc.arg(id)
|
||||
AND tenant_id = sqlc.arg(tenant_id)
|
||||
AND brand_id = sqlc.arg(brand_id);
|
||||
|
||||
-- name: GetImageFolder :one
|
||||
SELECT id, tenant_id, name, created_at, updated_at
|
||||
SELECT id, tenant_id, brand_id, name, created_at, updated_at
|
||||
FROM image_folders
|
||||
WHERE id = sqlc.arg(id) AND tenant_id = sqlc.arg(tenant_id);
|
||||
WHERE id = sqlc.arg(id)
|
||||
AND tenant_id = sqlc.arg(tenant_id)
|
||||
AND brand_id = sqlc.arg(brand_id);
|
||||
|
||||
-- name: DeleteImageFolder :exec
|
||||
DELETE FROM image_folders
|
||||
WHERE id = sqlc.arg(id) AND tenant_id = sqlc.arg(tenant_id);
|
||||
WHERE id = sqlc.arg(id)
|
||||
AND tenant_id = sqlc.arg(tenant_id)
|
||||
AND brand_id = sqlc.arg(brand_id);
|
||||
|
||||
-- name: CountActiveImagesInFolder :one
|
||||
SELECT COUNT(*)::INT AS count
|
||||
FROM image_assets
|
||||
WHERE tenant_id = sqlc.arg(tenant_id)
|
||||
AND brand_id = sqlc.arg(brand_id)
|
||||
AND folder_id = sqlc.arg(folder_id)
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL;
|
||||
|
||||
@@ -268,6 +268,7 @@ func RegisterRoutes(app *bootstrap.App) {
|
||||
monitoring.POST("/brands/:brand_id/collect-now", monitoringHandler.CollectNow)
|
||||
|
||||
knowledge := tenantProtected.Group("/knowledge")
|
||||
knowledge.Use(middleware.RequireCurrentBrand())
|
||||
knowledgeHandler := NewKnowledgeHandler(app)
|
||||
knowledge.GET("/groups", knowledgeHandler.ListGroups)
|
||||
knowledge.POST("/groups", knowledgeHandler.CreateGroup)
|
||||
@@ -315,14 +316,16 @@ func RegisterRoutes(app *bootstrap.App) {
|
||||
|
||||
images := tenantProtected.Group("/images")
|
||||
imgHandler := NewImageHandler(app)
|
||||
images.GET("/storage-usage", imgHandler.StorageUsage)
|
||||
images.Use(middleware.RequireCurrentBrand())
|
||||
images.GET("", imgHandler.ListImages)
|
||||
images.POST("", imgHandler.UploadImage)
|
||||
images.PUT("/:id", imgHandler.UpdateImage)
|
||||
images.GET("/:id/references", imgHandler.GetImageReferences)
|
||||
images.DELETE("/:id", imgHandler.DeleteImage)
|
||||
images.GET("/storage-usage", imgHandler.StorageUsage)
|
||||
|
||||
imageFolders := tenantProtected.Group("/images/folders")
|
||||
imageFolders.Use(middleware.RequireCurrentBrand())
|
||||
imageFolders.GET("", imgHandler.ListFolders)
|
||||
imageFolders.POST("", imgHandler.CreateFolder)
|
||||
imageFolders.PUT("/:id", imgHandler.UpdateFolder)
|
||||
|
||||
@@ -125,6 +125,7 @@ func (w *KolGenerationWorker) Process(ctx context.Context, task tenantapp.Claime
|
||||
knowledgeContext, err := w.knowledge.ResolveContext(
|
||||
ctx,
|
||||
task.TenantID,
|
||||
int64(kolIntInput(task.InputParams, "brand_id")),
|
||||
knowledgeGroupIDs,
|
||||
buildKolKnowledgeQuery(task.InputParams),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
ALTER TABLE image_assets
|
||||
DROP CONSTRAINT IF EXISTS fk_image_assets_folder_brand;
|
||||
|
||||
ALTER TABLE image_folders
|
||||
DROP CONSTRAINT IF EXISTS uq_image_folders_tenant_brand_name;
|
||||
|
||||
ALTER TABLE image_folders
|
||||
DROP CONSTRAINT IF EXISTS uq_image_folders_tenant_brand_id;
|
||||
|
||||
DROP INDEX IF EXISTS idx_image_folders_tenant_brand;
|
||||
DROP INDEX IF EXISTS idx_image_assets_tenant_brand_folder;
|
||||
DROP INDEX IF EXISTS idx_image_assets_tenant_brand_created;
|
||||
DROP INDEX IF EXISTS idx_image_assets_tenant_brand_status;
|
||||
|
||||
ALTER TABLE image_assets
|
||||
DROP COLUMN IF EXISTS brand_id;
|
||||
|
||||
ALTER TABLE image_folders
|
||||
DROP COLUMN IF EXISTS brand_id;
|
||||
|
||||
ALTER TABLE image_folders
|
||||
ADD CONSTRAINT uq_image_folders_tenant_name UNIQUE (tenant_id, name);
|
||||
|
||||
ALTER TABLE image_folders
|
||||
ADD CONSTRAINT uq_image_folders_tenant_id UNIQUE (tenant_id, id);
|
||||
|
||||
ALTER TABLE image_assets
|
||||
ADD CONSTRAINT fk_image_assets_folder
|
||||
FOREIGN KEY (tenant_id, folder_id)
|
||||
REFERENCES image_folders(tenant_id, id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_image_folders_tenant ON image_folders (tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_image_assets_tenant_folder
|
||||
ON image_assets (tenant_id, folder_id)
|
||||
WHERE status = 'active' AND deleted_at IS NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_image_assets_tenant_created
|
||||
ON image_assets (tenant_id, created_at DESC)
|
||||
WHERE status = 'active' AND deleted_at IS NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_image_assets_tenant_status
|
||||
ON image_assets (tenant_id, status, created_at DESC);
|
||||
|
||||
ALTER TABLE knowledge_groups
|
||||
DROP CONSTRAINT IF EXISTS fk_knowledge_groups_parent_brand;
|
||||
|
||||
ALTER TABLE knowledge_items
|
||||
DROP CONSTRAINT IF EXISTS fk_knowledge_items_group_brand;
|
||||
|
||||
ALTER TABLE knowledge_groups
|
||||
DROP CONSTRAINT IF EXISTS uq_knowledge_groups_tenant_brand_id;
|
||||
|
||||
DROP INDEX IF EXISTS uk_knowledge_group_brand_name;
|
||||
DROP INDEX IF EXISTS idx_knowledge_group_tenant_brand_parent;
|
||||
DROP INDEX IF EXISTS idx_knowledge_item_tenant_brand_group_status;
|
||||
|
||||
ALTER TABLE knowledge_items
|
||||
DROP COLUMN IF EXISTS brand_id;
|
||||
|
||||
ALTER TABLE knowledge_groups
|
||||
DROP COLUMN IF EXISTS brand_id;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uk_knowledge_group_name
|
||||
ON knowledge_groups(tenant_id, COALESCE(parent_id, 0), name)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_knowledge_group_tenant_parent
|
||||
ON knowledge_groups(tenant_id, parent_id, sort_order)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_knowledge_item_group_status
|
||||
ON knowledge_items(tenant_id, group_id, status)
|
||||
WHERE deleted_at IS NULL;
|
||||
@@ -0,0 +1,282 @@
|
||||
ALTER TABLE knowledge_groups
|
||||
ADD COLUMN IF NOT EXISTS brand_id BIGINT REFERENCES brands(id);
|
||||
|
||||
ALTER TABLE knowledge_items
|
||||
ADD COLUMN IF NOT EXISTS brand_id BIGINT REFERENCES brands(id);
|
||||
|
||||
ALTER TABLE image_folders
|
||||
ADD COLUMN IF NOT EXISTS brand_id BIGINT REFERENCES brands(id);
|
||||
|
||||
ALTER TABLE image_assets
|
||||
ADD COLUMN IF NOT EXISTS brand_id BIGINT REFERENCES brands(id);
|
||||
|
||||
INSERT INTO brands (tenant_id, name, description, status)
|
||||
SELECT tenant_id, '未归属', '系统迁移生成,用于存放无法自动推断品牌的历史知识库和图片库资料。', 'active'
|
||||
FROM (
|
||||
SELECT tenant_id FROM knowledge_groups WHERE brand_id IS NULL
|
||||
UNION
|
||||
SELECT tenant_id FROM knowledge_items WHERE brand_id IS NULL
|
||||
UNION
|
||||
SELECT tenant_id FROM image_folders WHERE brand_id IS NULL
|
||||
UNION
|
||||
SELECT tenant_id FROM image_assets WHERE brand_id IS NULL
|
||||
) tenants_with_assets
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM brands b
|
||||
WHERE b.tenant_id = tenants_with_assets.tenant_id
|
||||
AND b.deleted_at IS NULL
|
||||
);
|
||||
|
||||
WITH inferred_group_brand AS (
|
||||
SELECT DISTINCT ON (prkg.knowledge_group_id)
|
||||
prkg.knowledge_group_id,
|
||||
pr.brand_id
|
||||
FROM prompt_rule_knowledge_groups prkg
|
||||
JOIN prompt_rules pr
|
||||
ON pr.id = prkg.prompt_rule_id
|
||||
AND pr.deleted_at IS NULL
|
||||
JOIN knowledge_groups kg
|
||||
ON kg.id = prkg.knowledge_group_id
|
||||
AND kg.tenant_id = pr.tenant_id
|
||||
AND kg.deleted_at IS NULL
|
||||
WHERE pr.brand_id IS NOT NULL
|
||||
ORDER BY prkg.knowledge_group_id, pr.updated_at DESC, pr.id DESC
|
||||
)
|
||||
UPDATE knowledge_groups kg
|
||||
SET brand_id = igb.brand_id,
|
||||
updated_at = NOW()
|
||||
FROM inferred_group_brand igb
|
||||
WHERE kg.id = igb.knowledge_group_id
|
||||
AND kg.brand_id IS NULL;
|
||||
|
||||
UPDATE knowledge_groups child
|
||||
SET brand_id = parent.brand_id,
|
||||
updated_at = NOW()
|
||||
FROM knowledge_groups parent
|
||||
WHERE child.parent_id = parent.id
|
||||
AND child.tenant_id = parent.tenant_id
|
||||
AND parent.brand_id IS NOT NULL;
|
||||
|
||||
UPDATE knowledge_groups parent
|
||||
SET brand_id = child.brand_id,
|
||||
updated_at = NOW()
|
||||
FROM knowledge_groups child
|
||||
WHERE child.parent_id = parent.id
|
||||
AND child.tenant_id = parent.tenant_id
|
||||
AND parent.brand_id IS NULL
|
||||
AND child.brand_id IS NOT NULL;
|
||||
|
||||
WITH fallback_brand AS (
|
||||
SELECT DISTINCT ON (tenant_id) tenant_id, id AS brand_id
|
||||
FROM brands
|
||||
WHERE deleted_at IS NULL
|
||||
ORDER BY tenant_id, created_at, id
|
||||
)
|
||||
UPDATE knowledge_groups kg
|
||||
SET brand_id = fb.brand_id,
|
||||
updated_at = NOW()
|
||||
FROM fallback_brand fb
|
||||
WHERE kg.tenant_id = fb.tenant_id
|
||||
AND kg.brand_id IS NULL;
|
||||
|
||||
UPDATE knowledge_items ki
|
||||
SET brand_id = kg.brand_id,
|
||||
updated_at = NOW()
|
||||
FROM knowledge_groups kg
|
||||
WHERE ki.group_id = kg.id
|
||||
AND ki.tenant_id = kg.tenant_id
|
||||
AND kg.brand_id IS NOT NULL;
|
||||
|
||||
WITH inferred_asset_brand AS (
|
||||
SELECT DISTINCT ON (r.image_asset_id)
|
||||
r.image_asset_id,
|
||||
a.brand_id
|
||||
FROM image_asset_references r
|
||||
JOIN articles a
|
||||
ON a.id = r.article_id
|
||||
AND a.tenant_id = r.tenant_id
|
||||
AND a.deleted_at IS NULL
|
||||
WHERE a.brand_id IS NOT NULL
|
||||
ORDER BY r.image_asset_id, a.updated_at DESC, a.id DESC
|
||||
)
|
||||
UPDATE image_assets ia
|
||||
SET brand_id = iab.brand_id,
|
||||
updated_at = NOW()
|
||||
FROM inferred_asset_brand iab
|
||||
WHERE ia.id = iab.image_asset_id
|
||||
AND ia.brand_id IS NULL;
|
||||
|
||||
WITH schedule_cover_asset_brand AS (
|
||||
SELECT DISTINCT ON (cover_image_asset_id)
|
||||
cover_image_asset_id AS image_asset_id,
|
||||
brand_id
|
||||
FROM schedule_tasks
|
||||
WHERE cover_image_asset_id IS NOT NULL
|
||||
AND brand_id IS NOT NULL
|
||||
AND deleted_at IS NULL
|
||||
ORDER BY cover_image_asset_id, updated_at DESC, id DESC
|
||||
)
|
||||
UPDATE image_assets ia
|
||||
SET brand_id = scab.brand_id,
|
||||
updated_at = NOW()
|
||||
FROM schedule_cover_asset_brand scab
|
||||
WHERE ia.id = scab.image_asset_id
|
||||
AND ia.tenant_id = (SELECT tenant_id FROM brands WHERE id = scab.brand_id)
|
||||
AND ia.brand_id IS NULL;
|
||||
|
||||
WITH schedule_folder_brand AS (
|
||||
SELECT DISTINCT ON (cover_random_folder_id)
|
||||
cover_random_folder_id AS folder_id,
|
||||
brand_id
|
||||
FROM schedule_tasks
|
||||
WHERE cover_random_folder_id IS NOT NULL
|
||||
AND brand_id IS NOT NULL
|
||||
AND deleted_at IS NULL
|
||||
ORDER BY cover_random_folder_id, updated_at DESC, id DESC
|
||||
)
|
||||
UPDATE image_folders f
|
||||
SET brand_id = sfb.brand_id,
|
||||
updated_at = NOW()
|
||||
FROM schedule_folder_brand sfb
|
||||
WHERE f.id = sfb.folder_id
|
||||
AND f.tenant_id = (SELECT tenant_id FROM brands WHERE id = sfb.brand_id)
|
||||
AND f.brand_id IS NULL;
|
||||
|
||||
WITH folder_brand_from_assets AS (
|
||||
SELECT DISTINCT ON (folder_id)
|
||||
folder_id,
|
||||
brand_id
|
||||
FROM image_assets
|
||||
WHERE folder_id IS NOT NULL
|
||||
AND brand_id IS NOT NULL
|
||||
ORDER BY folder_id, updated_at DESC, id DESC
|
||||
)
|
||||
UPDATE image_folders f
|
||||
SET brand_id = fbfa.brand_id,
|
||||
updated_at = NOW()
|
||||
FROM folder_brand_from_assets fbfa
|
||||
WHERE f.id = fbfa.folder_id
|
||||
AND f.brand_id IS NULL;
|
||||
|
||||
UPDATE image_assets ia
|
||||
SET brand_id = f.brand_id,
|
||||
updated_at = NOW()
|
||||
FROM image_folders f
|
||||
WHERE ia.folder_id = f.id
|
||||
AND ia.tenant_id = f.tenant_id
|
||||
AND f.brand_id IS NOT NULL;
|
||||
|
||||
WITH fallback_brand AS (
|
||||
SELECT DISTINCT ON (tenant_id) tenant_id, id AS brand_id
|
||||
FROM brands
|
||||
WHERE deleted_at IS NULL
|
||||
ORDER BY tenant_id, created_at, id
|
||||
)
|
||||
UPDATE image_folders f
|
||||
SET brand_id = fb.brand_id,
|
||||
updated_at = NOW()
|
||||
FROM fallback_brand fb
|
||||
WHERE f.tenant_id = fb.tenant_id
|
||||
AND f.brand_id IS NULL;
|
||||
|
||||
UPDATE image_assets ia
|
||||
SET brand_id = f.brand_id,
|
||||
updated_at = NOW()
|
||||
FROM image_folders f
|
||||
WHERE ia.folder_id = f.id
|
||||
AND ia.tenant_id = f.tenant_id
|
||||
AND f.brand_id IS NOT NULL;
|
||||
|
||||
WITH fallback_brand AS (
|
||||
SELECT DISTINCT ON (tenant_id) tenant_id, id AS brand_id
|
||||
FROM brands
|
||||
WHERE deleted_at IS NULL
|
||||
ORDER BY tenant_id, created_at, id
|
||||
)
|
||||
UPDATE image_assets ia
|
||||
SET brand_id = fb.brand_id,
|
||||
updated_at = NOW()
|
||||
FROM fallback_brand fb
|
||||
WHERE ia.tenant_id = fb.tenant_id
|
||||
AND ia.brand_id IS NULL;
|
||||
|
||||
ALTER TABLE knowledge_groups
|
||||
ALTER COLUMN brand_id SET NOT NULL;
|
||||
|
||||
ALTER TABLE knowledge_items
|
||||
ALTER COLUMN brand_id SET NOT NULL;
|
||||
|
||||
ALTER TABLE image_folders
|
||||
ALTER COLUMN brand_id SET NOT NULL;
|
||||
|
||||
ALTER TABLE image_assets
|
||||
ALTER COLUMN brand_id SET NOT NULL;
|
||||
|
||||
DROP INDEX IF EXISTS uk_knowledge_group_name;
|
||||
DROP INDEX IF EXISTS idx_knowledge_group_tenant_parent;
|
||||
DROP INDEX IF EXISTS idx_knowledge_item_group_status;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uk_knowledge_group_brand_name
|
||||
ON knowledge_groups(tenant_id, brand_id, COALESCE(parent_id, 0), name)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_knowledge_group_tenant_brand_parent
|
||||
ON knowledge_groups(tenant_id, brand_id, parent_id, sort_order)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_knowledge_item_tenant_brand_group_status
|
||||
ON knowledge_items(tenant_id, brand_id, group_id, status)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
ALTER TABLE knowledge_groups
|
||||
ADD CONSTRAINT uq_knowledge_groups_tenant_brand_id UNIQUE (tenant_id, brand_id, id);
|
||||
|
||||
ALTER TABLE knowledge_items
|
||||
ADD CONSTRAINT fk_knowledge_items_group_brand
|
||||
FOREIGN KEY (tenant_id, brand_id, group_id)
|
||||
REFERENCES knowledge_groups(tenant_id, brand_id, id);
|
||||
|
||||
ALTER TABLE knowledge_groups
|
||||
ADD CONSTRAINT fk_knowledge_groups_parent_brand
|
||||
FOREIGN KEY (tenant_id, brand_id, parent_id)
|
||||
REFERENCES knowledge_groups(tenant_id, brand_id, id);
|
||||
|
||||
ALTER TABLE image_assets
|
||||
DROP CONSTRAINT IF EXISTS fk_image_assets_folder;
|
||||
|
||||
ALTER TABLE image_folders
|
||||
DROP CONSTRAINT IF EXISTS uq_image_folders_tenant_name;
|
||||
|
||||
ALTER TABLE image_folders
|
||||
DROP CONSTRAINT IF EXISTS uq_image_folders_tenant_id;
|
||||
|
||||
ALTER TABLE image_folders
|
||||
ADD CONSTRAINT uq_image_folders_tenant_brand_name UNIQUE (tenant_id, brand_id, name);
|
||||
|
||||
ALTER TABLE image_folders
|
||||
ADD CONSTRAINT uq_image_folders_tenant_brand_id UNIQUE (tenant_id, brand_id, id);
|
||||
|
||||
ALTER TABLE image_assets
|
||||
ADD CONSTRAINT fk_image_assets_folder_brand
|
||||
FOREIGN KEY (tenant_id, brand_id, folder_id)
|
||||
REFERENCES image_folders(tenant_id, brand_id, id);
|
||||
|
||||
DROP INDEX IF EXISTS idx_image_folders_tenant;
|
||||
DROP INDEX IF EXISTS idx_image_assets_tenant_folder;
|
||||
DROP INDEX IF EXISTS idx_image_assets_tenant_created;
|
||||
DROP INDEX IF EXISTS idx_image_assets_tenant_status;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_image_folders_tenant_brand
|
||||
ON image_folders (tenant_id, brand_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_image_assets_tenant_brand_folder
|
||||
ON image_assets (tenant_id, brand_id, folder_id)
|
||||
WHERE status = 'active' AND deleted_at IS NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_image_assets_tenant_brand_created
|
||||
ON image_assets (tenant_id, brand_id, created_at DESC)
|
||||
WHERE status = 'active' AND deleted_at IS NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_image_assets_tenant_brand_status
|
||||
ON image_assets (tenant_id, brand_id, status, created_at DESC);
|
||||
Reference in New Issue
Block a user