@ -13,8 +13,8 @@ import (
"fashionapi/internal/repository"
)
// Draft Card 审核列表统一卡片(所有模块共用),供后台列表页渲染。
type Draft Card struct {
// Record Card 审核列表统一卡片(所有模块共用),供后台列表页渲染。
type Record Card struct {
Kind string ` json:"kind" ` // runway | street | 未来其它模块
ID uint32 ` json:"id" `
Title string ` json:"title" ` // 展示标题
@ -26,11 +26,11 @@ type DraftCard struct {
UpdatedAt uint32 ` json:"updated_at" `
}
// Draft ImageRef 草稿 图片引用(统一结构)。
// Record ImageRef 记录 图片引用(统一结构)。
//
// IsDetail / ParentImageID 只有街拍模块会填(走秀的主/细节图走 LookIndex 那套,语义不同,不复用);
// DetailCount 让模板不用自己数副图。
type Draft ImageRef struct {
type Record ImageRef struct {
ID uint32 ` json:"id" `
Image string ` json:"image" `
Name string ` json:"name" `
@ -49,39 +49,39 @@ type EditField struct {
Type string ` json:"type" ` // text | textarea | number
}
// Draft DetailView 审核详情统一视图(所有模块共用)。
type Draft DetailView struct {
Kind string ` json:"kind" `
ID uint32 ` json:"id" `
BrandID uint32 ` json:"brand_id" ` // 仅 runway 等有品牌的模块有,其余为 0
Title string ` json:"title" ` // 展示标题( runway=title_en, street=title)
TitleEn string ` json:"title_en" `
TitleCn string ` json:"title_cn" `
DescriptionEn string ` json:"description_en" `
DescriptionCn string ` json:"description_cn" `
Year uint16 ` json:"year" `
Season string ` json:"season" `
CollectionType string ` json:"collection_type" `
SeasonCode string ` json:"season_code" `
City string ` json:"city" `
Cover string ` json:"cover" `
ImageCount uint16 ` json:"image_count" `
Status string ` json:"status" `
Images [ ] Draft ImageRef ` json:"images" `
// Record DetailView 审核详情统一视图(所有模块共用)。
type Record DetailView struct {
Kind string ` json:"kind" `
ID uint32 ` json:"id" `
BrandID uint32 ` json:"brand_id" ` // 仅 runway 等有品牌的模块有,其余为 0
Title string ` json:"title" ` // 展示标题( runway=title_en, street=title)
TitleEn string ` json:"title_en" `
TitleCn string ` json:"title_cn" `
DescriptionEn string ` json:"description_en" `
DescriptionCn string ` json:"description_cn" `
Year uint16 ` json:"year" `
Season string ` json:"season" `
CollectionType string ` json:"collection_type" `
SeasonCode string ` json:"season_code" `
City string ` json:"city" `
Cover string ` json:"cover" `
ImageCount uint16 ` json:"image_count" `
Status string ` json:"status" `
Images [ ] Record ImageRef ` json:"images" `
// Fields 是审核前可微调的字段列表(按模块声明顺序渲染),替代模板里的 IsStreet 分支。
Fields [ ] EditField ` json:"fields" `
// Groups 街拍详情的「主图 → 其副图」分组视图(按 sort_order 顺序)。走秀模块留空。
// 与 Images 同源同序: Images 保持扁平(既有渲染 / runway 仍用) , Groups 供按组折叠。
Groups [ ] Draft ImageGroup ` json:"groups,omitempty" `
Groups [ ] ImageGroup ` json:"groups,omitempty" `
}
// Draft ImageGroup 一张主图及其副图,供审核页按组折叠渲染。
type Draft ImageGroup struct {
Image Draft ImageRef ` json:"image" ` // 主图
Details [ ] Draft ImageRef ` json:"details" ` // 该主图名下的副图(可为空)
// ImageGroup 一张主图及其副图,供审核页按组折叠渲染。
type ImageGroup struct {
Image Record ImageRef ` json:"image" ` // 主图
Details [ ] Record ImageRef ` json:"details" ` // 该主图名下的副图(可为空)
}
// ErrInvalidImageGroup 主副图分组操作不合法:主图不存在 / 不属于本草稿 / 自身不是主图 / 空集合。
// ErrInvalidImageGroup 主副图分组操作不合法:主图不存在 / 不属于该记录 / 自身不是主图 / 空集合。
// 这里是给用户看的文案, handler 直接渲染 err.Error()。
var ErrInvalidImageGroup = errors . New ( "主图无效:它必须属于本草稿、未被删除,且自身是主图" )
@ -92,40 +92,41 @@ type KindTab struct {
Pending int64 ` json:"pending" `
}
// ReviewModule 一种待审草稿 模块( runway / street / 未来任意爬虫数据类型)的统一契约。
// ReviewModule 一种待审记录 模块( runway / street / 未来任意爬虫数据类型)的统一契约。
//
// 新增一种爬虫数据类型需要审核时,只需:
// 1. 建草稿 表 + 迁移;
// 2. 在仓储层实现 List/Detail/Images/Update/SetStatus/SaveFromDraft/CountPending ;
// 1. 建正式 表 + 迁移(记录自带 status 列) ;
// 2. 在仓储层实现 List/Detail/Images/Update/SetStatus/CountByStatus ;
// 3. 在此文件实现本接口并注册到 NewReviewService。
//
// 列表、详情、通过、驳回、待审计数、类型标签都会自动覆盖,无需改动 handler / 模板分支。
type ReviewModule interface {
Kind ( ) string
Label ( ) string
ListDraft s ( ctx context . Context , status string , page , size int ) ( [ ] Draft Card, int64 , error )
Draft Detail( ctx context . Context , id uint32 ) ( * Draft DetailView, error )
UpdateDraft ( ctx context . Context , id uint32 , fields map [ string ] any ) error
// Approve 晋升到正式表并把草稿置 approv ed; 返回正式表 主键。
ListRecord s ( ctx context . Context , status string , page , size int ) ( [ ] Record Card, int64 , error )
Record Detail( ctx context . Context , id uint32 ) ( * Record DetailView, error )
UpdateRecord ( ctx context . Context , id uint32 , fields map [ string ] any ) error
// Approve 把正式记录置为 publish ed; 返回记录 主键。
// 单表发布模型:通过只是改状态,不再晋升重建(图片在入库时已落到正式表)。
Approve ( ctx context . Context , id uint32 , reviewer string ) ( uint32 , error )
Reject ( ctx context . Context , id uint32 , reviewer , reason string ) error
CountPending ( ctx context . Context ) ( int64 , error )
}
// ReviewService 审核草稿 的业务逻辑:列出 / 查看 / 微调 / 通过(晋升 ) / 驳回。
// ReviewService 审核的业务逻辑:列出 / 查看 / 微调 / 通过(置 published ) / 驳回。
// 全部按 kind 委派到对应 ReviewModule; kind 为空表示合并所有模块。
type ReviewService interface {
ListDraft s ( ctx context . Context , kind , status string , page , size int ) ( [ ] Draft Card, int64 , error )
Draft Detail( ctx context . Context , kind string , id uint32 ) ( * Draft DetailView, error )
UpdateDraft ( ctx context . Context , kind string , id uint32 , fields map [ string ] any ) error
ListRecord s ( ctx context . Context , kind , status string , page , size int ) ( [ ] Record Card, int64 , error )
Record Detail( ctx context . Context , kind string , id uint32 ) ( * Record DetailView, error )
UpdateRecord ( ctx context . Context , kind string , id uint32 , fields map [ string ] any ) error
Approve ( ctx context . Context , kind string , id uint32 , reviewer string ) ( uint32 , error )
Reject ( ctx context . Context , kind string , id uint32 , reviewer , reason string ) error
// DeleteDraft Image 软删审核草稿 里的单张图片( 审核阶段剔除坏图, 不删S4文件) 。
DeleteDraft Image ( ctx context . Context , kind string , draft ID, imgID uint32 ) error
// AttachDraft Images 把若干张草稿 图并入指定主图(街拍专用;走秀模块返回 ErrInvalidImageGroup) 。
AttachDraft Images ( ctx context . Context , kind string , draft ID, mainImgID uint32 , imgIDs [ ] uint32 ) error
// DetachDraft Image 把副图拆出恢复为主图(街拍专用)。
DetachDraft Image ( ctx context . Context , kind string , draft ID, imgID uint32 ) error
// DeleteImage 软删审核记录 里的单张图片( 审核阶段剔除坏图, 不删S4文件) 。
DeleteImage ( ctx context . Context , kind string , record ID, imgID uint32 ) error
// AttachImages 把若干张图并入指定主图(街拍专用;走秀模块返回 ErrInvalidImageGroup) 。
AttachImages ( ctx context . Context , kind string , record ID, mainImgID uint32 , imgIDs [ ] uint32 ) error
// DetachImage 把副图拆出恢复为主图(街拍专用)。
DetachImage ( ctx context . Context , kind string , record ID, imgID uint32 ) error
// KindTabs 返回类型标签(含「全部」与每个模块的待审数),驱动大审核列表概览。
KindTabs ( ctx context . Context ) [ ] KindTab
}
@ -165,22 +166,22 @@ func (s *reviewService) module(kind string) (ReviewModule, bool) {
return m , ok
}
func ( s * reviewService ) ListDraft s ( ctx context . Context , kind , status string , page , size int ) ( [ ] Draft Card, int64 , error ) {
func ( s * reviewService ) ListRecord s ( ctx context . Context , kind , status string , page , size int ) ( [ ] Record Card, int64 , error ) {
if kind == "all" {
kind = ""
}
if kind != "" {
m , ok := s . module ( kind )
if ! ok {
return [ ] Draft Card{ } , 0 , nil
return [ ] Record Card{ } , 0 , nil
}
return m . ListDraft s ( ctx , status , page , size )
return m . ListRecord s ( ctx , status , page , size )
}
// 合并所有模块:各取较多行后在内存按创建时间倒序,再分页(后台量小,足够准确)。
var all [ ] Draft Card
var all [ ] Record Card
var total int64
for _ , k := range s . order {
cards , t , err := s . modules [ k ] . ListDraft s ( ctx , status , 1 , 1000 )
cards , t , err := s . modules [ k ] . ListRecord s ( ctx , status , 1 , 1000 )
if err != nil {
return nil , 0 , err
}
@ -208,20 +209,20 @@ func (s *reviewService) ListDrafts(ctx context.Context, kind, status string, pag
return all [ start : end ] , total , nil
}
func ( s * reviewService ) Draft Detail( ctx context . Context , kind string , id uint32 ) ( * Draft DetailView, error ) {
func ( s * reviewService ) Record Detail( ctx context . Context , kind string , id uint32 ) ( * Record DetailView, error ) {
m , ok := s . module ( kind )
if ! ok {
return nil , repository . ErrNotFound
}
return m . Draft Detail( ctx , id )
return m . Record Detail( ctx , id )
}
func ( s * reviewService ) UpdateDraft ( ctx context . Context , kind string , id uint32 , fields map [ string ] any ) error {
func ( s * reviewService ) UpdateRecord ( ctx context . Context , kind string , id uint32 , fields map [ string ] any ) error {
m , ok := s . module ( kind )
if ! ok {
return repository . ErrNotFound
}
return m . UpdateDraft ( ctx , id , fields )
return m . UpdateRecord ( ctx , id , fields )
}
func ( s * reviewService ) Approve ( ctx context . Context , kind string , id uint32 , reviewer string ) ( uint32 , error ) {
@ -256,22 +257,22 @@ func (s *reviewService) KindTabs(ctx context.Context) []KindTab {
return tabs
}
// DeleteDraft Image 软删审核草稿 里的单张图片(审核阶段剔除坏图)。
// 按 kind 委派到对应草稿 图片表的软删,并同步草稿 image_count;
// DeleteImage 软删审核记录 里的单张图片(审核阶段剔除坏图)。
// 按 kind 委派到对应图片表的软删,并同步记录 image_count;
// 若该 key 在全库已无其他引用( 引用计数归零) , 则一并清理S4孤儿文件。
func ( s * reviewService ) DeleteDraft Image ( ctx context . Context , kind string , draft ID, imgID uint32 ) error {
func ( s * reviewService ) DeleteImage ( ctx context . Context , kind string , record ID, imgID uint32 ) error {
if _ , ok := s . module ( kind ) ; ! ok {
return repository . ErrNotFound
}
key , err := s . repo . GetDraft ImageKey ( ctx , kind , draft ID, imgID )
key , err := s . repo . GetImageKey ( ctx , kind , record ID, imgID )
if err != nil {
return err
}
switch kind {
case dto . IngestKindRunway :
err = s . repo . SoftDeleteRunwayDraft Image ( ctx , draft ID, imgID )
err = s . repo . SoftDeleteRunwayImage ( ctx , record ID, imgID )
case dto . IngestKindStreet :
err = s . repo . SoftDeleteStreetDraft Image ( ctx , draft ID, imgID )
err = s . repo . SoftDeleteStreetImage ( ctx , record ID, imgID )
default :
return repository . ErrNotFound
}
@ -284,21 +285,21 @@ func (s *reviewService) DeleteDraftImage(ctx context.Context, kind string, draft
return nil
}
func ( s * reviewService ) AttachDraft Images ( ctx context . Context , kind string , draft ID, mainImgID uint32 , imgIDs [ ] uint32 ) error {
func ( s * reviewService ) AttachImages ( ctx context . Context , kind string , record ID, mainImgID uint32 , imgIDs [ ] uint32 ) error {
if kind != dto . IngestKindStreet {
return ErrInvalidImageGroup
}
if len ( imgIDs ) == 0 {
return ErrInvalidImageGroup
}
return translateGroupErr ( s . repo . AttachStreetDraft Images ( ctx , draft ID, mainImgID , imgIDs ) )
return translateGroupErr ( s . repo . AttachStreetSnap Images ( ctx , record ID, mainImgID , imgIDs ) )
}
func ( s * reviewService ) DetachDraft Image ( ctx context . Context , kind string , draft ID, imgID uint32 ) error {
func ( s * reviewService ) DetachImage ( ctx context . Context , kind string , record ID, imgID uint32 ) error {
if kind != dto . IngestKindStreet {
return ErrInvalidImageGroup
}
return translateGroupErr ( s . repo . DetachStreetDraft Image ( ctx , draft ID, imgID ) )
return translateGroupErr ( s . repo . DetachStreetSnap Image ( ctx , record ID, imgID ) )
}
// translateGroupErr 把仓储层的哨兵错误翻成给用户看的中文文案;其余错误原样透传。
@ -322,24 +323,24 @@ type runwayModule struct {
func ( m * runwayModule ) Kind ( ) string { return dto . IngestKindRunway }
func ( m * runwayModule ) Label ( ) string { return "走秀" }
func ( m * runwayModule ) ListDraft s ( ctx context . Context , status string , page , size int ) ( [ ] Draft Card, int64 , error ) {
rows , total , err := m . repo . ListDraft s ( ctx , status , page , size )
func ( m * runwayModule ) ListRecord s ( ctx context . Context , status string , page , size int ) ( [ ] Record Card, int64 , error ) {
rows , total , err := m . repo . ListRecord s ( ctx , status , page , size )
if err != nil {
return nil , 0 , err
}
return m . toRunwayCards ( rows ) , total , nil
}
func ( m * runwayModule ) Draft Detail( ctx context . Context , id uint32 ) ( * Draft DetailView, error ) {
d , err := m . repo . Draft Detail( ctx , id )
func ( m * runwayModule ) Record Detail( ctx context . Context , id uint32 ) ( * Record DetailView, error ) {
d , err := m . repo . Record Detail( ctx , id )
if err != nil {
return nil , err
}
imgs , err := m . repo . Draft Images( ctx , id )
imgs , err := m . repo . Record Images( ctx , id )
if err != nil {
return nil , err
}
v := & Draft DetailView{
v := & Record DetailView{
Kind : dto . IngestKindRunway ,
ID : d . ID ,
BrandID : d . BrandID ,
@ -367,32 +368,28 @@ func (m *runwayModule) DraftDetail(ctx context.Context, id uint32) (*DraftDetail
} ,
}
for _ , im := range imgs {
v . Images = append ( v . Images , Draft ImageRef{ ID : im . ID , Image : m . img . Compose ( im . Image ) , Name : im . Name , SortOrder : im . SortOrder } )
v . Images = append ( v . Images , Record ImageRef{ ID : im . ID , Image : m . img . Compose ( im . Image ) , Name : im . Name , SortOrder : im . SortOrder } )
}
return v , nil
}
func ( m * runwayModule ) UpdateDraft ( ctx context . Context , id uint32 , fields map [ string ] any ) error {
return m . repo . UpdateDraft ( ctx , id , fields )
func ( m * runwayModule ) UpdateRecord ( ctx context . Context , id uint32 , fields map [ string ] any ) error {
return m . repo . UpdateRecord ( ctx , id , fields )
}
func ( m * runwayModule ) Approve ( ctx context . Context , id uint32 , reviewer string ) ( uint32 , error ) {
officialID , err := m . repo . SaveRunwayFromDraft ( ctx , id )
if err != nil {
if err := m . repo . SetRecordStatus ( ctx , id , model . StatusPublished , reviewer , "" ) ; err != nil {
return 0 , err
}
if err := m . repo . SetDraftStatus ( ctx , id , model . DraftStatusApproved , reviewer , "" ) ; err != nil {
return 0 , err
}
return officialID , nil
return id , nil
}
func ( m * runwayModule ) Reject ( ctx context . Context , id uint32 , reviewer , reason string ) error {
return m . repo . SetDraft Status ( ctx , id , model . Draft StatusRejected, reviewer , reason )
return m . repo . SetRecord Status ( ctx , id , model . StatusRejected , reviewer , reason )
}
func ( m * runwayModule ) CountPending ( ctx context . Context ) ( int64 , error ) {
return m . repo . CountDraftPending ( ctx , model . Draft StatusPending, & model . BrandRunwayDraft { } )
return m . repo . CountByStatus ( ctx , model . StatusPending , & model . BrandRunway { } )
}
// ── street 模块 ────────────────────────────────────────────────────────────────
@ -405,24 +402,24 @@ type streetModule struct {
func ( m * streetModule ) Kind ( ) string { return dto . IngestKindStreet }
func ( m * streetModule ) Label ( ) string { return "街拍" }
func ( m * streetModule ) ListDraft s ( ctx context . Context , status string , page , size int ) ( [ ] Draft Card, int64 , error ) {
rows , total , err := m . repo . ListStreetDraft s ( ctx , status , page , size )
func ( m * streetModule ) ListRecord s ( ctx context . Context , status string , page , size int ) ( [ ] Record Card, int64 , error ) {
rows , total , err := m . repo . ListStreetRecord s ( ctx , status , page , size )
if err != nil {
return nil , 0 , err
}
return m . toStreetCards ( rows ) , total , nil
}
func ( m * streetModule ) Draft Detail( ctx context . Context , id uint32 ) ( * Draft DetailView, error ) {
d , err := m . repo . StreetDraft Detail ( ctx , id )
func ( m * streetModule ) Record Detail( ctx context . Context , id uint32 ) ( * Record DetailView, error ) {
d , err := m . repo . StreetRecord Detail ( ctx , id )
if err != nil {
return nil , err
}
imgs , err := m . repo . StreetDraft Images ( ctx , id )
imgs , err := m . repo . StreetRecord Images ( ctx , id )
if err != nil {
return nil , err
}
v := & Draft DetailView{
v := & Record DetailView{
Kind : dto . IngestKindStreet ,
ID : d . ID ,
Title : d . Title ,
@ -440,7 +437,7 @@ func (m *streetModule) DraftDetail(ctx context.Context, id uint32) (*DraftDetail
// 按 sort_order 顺序成组:主图各成一组;副图归入其(存活)主图组,父引用失效则自成一主图组。
// 父引用指向「副图」时沿链向上归并到该副图所在组的主图(保证与公开 API 同源同序)。
byID := make ( map [ uint32 ] model . StreetSnapDraft Image , len ( imgs ) )
byID := make ( map [ uint32 ] model . StreetSnapImage , len ( imgs ) )
for _ , im := range imgs {
byID [ im . ID ] = im
}
@ -472,7 +469,7 @@ func (m *streetModule) DraftDetail(ctx context.Context, id uint32) (*DraftDetail
}
}
groups := make ( [ ] Draft ImageGroup, 0 , len ( imgs ) )
groups := make ( [ ] ImageGroup , 0 , len ( imgs ) )
groupIndex := make ( map [ uint32 ] int , len ( imgs ) )
for _ , im := range imgs {
root := resolveRoot ( im . ID )
@ -483,7 +480,7 @@ func (m *streetModule) DraftDetail(ctx context.Context, id uint32) (*DraftDetail
isDetailVal = 1
parent = root
}
ref := Draft ImageRef{
ref := Record ImageRef{
ID : im . ID ,
Image : m . img . Compose ( im . Image ) ,
Name : im . Name ,
@ -494,14 +491,14 @@ func (m *streetModule) DraftDetail(ctx context.Context, id uint32) (*DraftDetail
}
v . Images = append ( v . Images , ref )
if ! isDetail {
groups = append ( groups , Draft ImageGroup{ Image : ref } )
groups = append ( groups , ImageGroup { Image : ref } )
groupIndex [ im . ID ] = len ( groups ) - 1
continue
}
if gi , ok := groupIndex [ root ] ; ok {
groups [ gi ] . Details = append ( groups [ gi ] . Details , ref )
} else { // 兜底(理论不发生):主图未先出现,自成一组
groups = append ( groups , Draft ImageGroup{ Image : ref } )
groups = append ( groups , ImageGroup { Image : ref } )
groupIndex [ im . ID ] = len ( groups ) - 1
}
}
@ -509,33 +506,29 @@ func (m *streetModule) DraftDetail(ctx context.Context, id uint32) (*DraftDetail
return v , nil
}
func ( m * streetModule ) UpdateDraft ( ctx context . Context , id uint32 , fields map [ string ] any ) error {
return m . repo . UpdateStreetDraft ( ctx , id , fields )
func ( m * streetModule ) UpdateRecord ( ctx context . Context , id uint32 , fields map [ string ] any ) error {
return m . repo . UpdateStreetRecord ( ctx , id , fields )
}
func ( m * streetModule ) Approve ( ctx context . Context , id uint32 , reviewer string ) ( uint32 , error ) {
officialID , err := m . repo . Sav eStreetSnapFromDraft ( ctx , id )
if err != nil {
if err := m . repo . Set StreetRecordStatus ( ctx , id , model . StatusPublished , reviewer , "" ) ; err != nil {
return 0 , err
}
if err := m . repo . SetStreetDraftStatus ( ctx , id , model . DraftStatusApproved , reviewer , "" ) ; err != nil {
return 0 , err
}
return officialID , nil
return id , nil
}
func ( m * streetModule ) Reject ( ctx context . Context , id uint32 , reviewer , reason string ) error {
return m . repo . SetStreetDraft Status ( ctx , id , model . Draft StatusRejected, reviewer , reason )
return m . repo . SetStreetRecord Status ( ctx , id , model . StatusRejected , reviewer , reason )
}
func ( m * streetModule ) CountPending ( ctx context . Context ) ( int64 , error ) {
return m . repo . CountDraftPending ( ctx , model . Draft StatusPending, & model . StreetSnapDraft { } )
return m . repo . CountByStatus ( ctx , model . StatusPending , & model . StreetSnap { } )
}
// ── 卡片转换(保留原逻辑)──────────────────────────────────────────────────────
func ( m * runwayModule ) toRunwayCards ( rows [ ] model . BrandRunwayDraft ) [ ] Draft Card {
cards := make ( [ ] Draft Card, 0 , len ( rows ) )
func ( m * runwayModule ) toRunwayCards ( rows [ ] model . BrandRunway ) [ ] Record Card {
cards := make ( [ ] Record Card, 0 , len ( rows ) )
for _ , d := range rows {
sub := d . SeasonCode
if d . CollectionType != "" {
@ -544,7 +537,7 @@ func (m *runwayModule) toRunwayCards(rows []model.BrandRunwayDraft) []DraftCard
if sub == "" {
sub = "—"
}
cards = append ( cards , Draft Card{
cards = append ( cards , Record Card{
Kind : dto . IngestKindRunway ,
ID : d . ID ,
Title : d . TitleEn ,
@ -559,14 +552,14 @@ func (m *runwayModule) toRunwayCards(rows []model.BrandRunwayDraft) []DraftCard
return cards
}
func ( m * streetModule ) toStreetCards ( rows [ ] model . StreetSnapDraft ) [ ] Draft Card {
cards := make ( [ ] Draft Card, 0 , len ( rows ) )
func ( m * streetModule ) toStreetCards ( rows [ ] model . StreetSnap ) [ ] Record Card {
cards := make ( [ ] Record Card, 0 , len ( rows ) )
for _ , d := range rows {
sub := d . City
if sub == "" {
sub = "—"
}
cards = append ( cards , Draft Card{
cards = append ( cards , Record Card{
Kind : dto . IngestKindStreet ,
ID : d . ID ,
Title : d . Title ,