feat: add superadmin assets and notifications
This commit is contained in:
63
backend/app/http/super/v1/assets.go
Normal file
63
backend/app/http/super/v1/assets.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
dto "quyun/v2/app/http/super/v1/dto"
|
||||
"quyun/v2/app/requests"
|
||||
"quyun/v2/app/services"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
// @provider
|
||||
type assets struct{}
|
||||
|
||||
// List assets
|
||||
//
|
||||
// @Router /super/v1/assets [get]
|
||||
// @Summary List assets
|
||||
// @Description List assets across tenants
|
||||
// @Tags Asset
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param page query int false "Page number"
|
||||
// @Param limit query int false "Page size"
|
||||
// @Success 200 {object} requests.Pager{items=[]dto.SuperAssetItem}
|
||||
// @Bind filter query
|
||||
func (c *assets) List(ctx fiber.Ctx, filter *dto.SuperAssetListFilter) (*requests.Pager, error) {
|
||||
return services.Super.ListAssets(ctx, filter)
|
||||
}
|
||||
|
||||
// Asset usage
|
||||
//
|
||||
// @Router /super/v1/assets/usage [get]
|
||||
// @Summary Asset usage
|
||||
// @Description Asset usage statistics
|
||||
// @Tags Asset
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} dto.SuperAssetUsageResponse
|
||||
// @Bind filter query
|
||||
func (c *assets) Usage(ctx fiber.Ctx, filter *dto.SuperAssetUsageFilter) (*dto.SuperAssetUsageResponse, error) {
|
||||
return services.Super.AssetUsage(ctx, filter)
|
||||
}
|
||||
|
||||
// Delete asset
|
||||
//
|
||||
// @Router /super/v1/assets/:id<int> [delete]
|
||||
// @Summary Delete asset
|
||||
// @Description Delete asset
|
||||
// @Tags Asset
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int64 true "Asset ID"
|
||||
// @Param force query bool false "Force delete"
|
||||
// @Success 200 {string} string "Deleted"
|
||||
// @Bind id path
|
||||
// @Bind query query
|
||||
func (c *assets) Delete(ctx fiber.Ctx, id int64, query *dto.SuperAssetDeleteQuery) error {
|
||||
force := false
|
||||
if query != nil && query.Force != nil {
|
||||
force = *query.Force
|
||||
}
|
||||
return services.Super.DeleteAsset(ctx, id, force)
|
||||
}
|
||||
119
backend/app/http/super/v1/dto/super_asset.go
Normal file
119
backend/app/http/super/v1/dto/super_asset.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"quyun/v2/app/requests"
|
||||
"quyun/v2/pkg/consts"
|
||||
)
|
||||
|
||||
// SuperAssetListFilter 超管资产列表查询条件。
|
||||
type SuperAssetListFilter struct {
|
||||
requests.Pagination
|
||||
// ID 资产ID,精确匹配。
|
||||
ID *int64 `query:"id"`
|
||||
// TenantID 租户ID,精确匹配。
|
||||
TenantID *int64 `query:"tenant_id"`
|
||||
// TenantCode 租户编码,模糊匹配。
|
||||
TenantCode *string `query:"tenant_code"`
|
||||
// TenantName 租户名称,模糊匹配。
|
||||
TenantName *string `query:"tenant_name"`
|
||||
// UserID 上传用户ID,精确匹配。
|
||||
UserID *int64 `query:"user_id"`
|
||||
// Username 上传用户名/昵称,模糊匹配。
|
||||
Username *string `query:"username"`
|
||||
// Type 媒体类型过滤。
|
||||
Type *consts.MediaAssetType `query:"type"`
|
||||
// Status 处理状态过滤。
|
||||
Status *consts.MediaAssetStatus `query:"status"`
|
||||
// Provider 存储提供方过滤。
|
||||
Provider *string `query:"provider"`
|
||||
// ObjectKey 对象Key关键字,模糊匹配。
|
||||
ObjectKey *string `query:"object_key"`
|
||||
// CreatedAtFrom 上传时间起始(RFC3339)。
|
||||
CreatedAtFrom *string `query:"created_at_from"`
|
||||
// CreatedAtTo 上传时间结束(RFC3339)。
|
||||
CreatedAtTo *string `query:"created_at_to"`
|
||||
// SizeMin 文件大小下限(字节)。
|
||||
SizeMin *int64 `query:"size_min"`
|
||||
// SizeMax 文件大小上限(字节)。
|
||||
SizeMax *int64 `query:"size_max"`
|
||||
// Asc 升序字段(id/created_at)。
|
||||
Asc *string `query:"asc"`
|
||||
// Desc 降序字段(id/created_at)。
|
||||
Desc *string `query:"desc"`
|
||||
}
|
||||
|
||||
// SuperAssetUsageFilter 超管资产用量统计查询条件。
|
||||
type SuperAssetUsageFilter struct {
|
||||
// TenantID 租户ID(不传代表全平台)。
|
||||
TenantID *int64 `query:"tenant_id"`
|
||||
}
|
||||
|
||||
// SuperAssetDeleteQuery 超管资产删除参数。
|
||||
type SuperAssetDeleteQuery struct {
|
||||
// Force 是否强制删除(忽略被内容引用的限制)。
|
||||
Force *bool `query:"force"`
|
||||
}
|
||||
|
||||
// SuperAssetItem 超管资产条目。
|
||||
type SuperAssetItem struct {
|
||||
// ID 资产ID。
|
||||
ID int64 `json:"id"`
|
||||
// TenantID 租户ID。
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
// TenantCode 租户编码。
|
||||
TenantCode string `json:"tenant_code"`
|
||||
// TenantName 租户名称。
|
||||
TenantName string `json:"tenant_name"`
|
||||
// UserID 上传用户ID。
|
||||
UserID int64 `json:"user_id"`
|
||||
// Username 上传用户名/昵称。
|
||||
Username string `json:"username"`
|
||||
// Type 媒体类型。
|
||||
Type consts.MediaAssetType `json:"type"`
|
||||
// Status 处理状态。
|
||||
Status consts.MediaAssetStatus `json:"status"`
|
||||
// Provider 存储提供方。
|
||||
Provider string `json:"provider"`
|
||||
// Bucket 存储桶名称。
|
||||
Bucket string `json:"bucket"`
|
||||
// ObjectKey 对象Key。
|
||||
ObjectKey string `json:"object_key"`
|
||||
// URL 访问URL(若可用)。
|
||||
URL string `json:"url"`
|
||||
// Filename 原始文件名。
|
||||
Filename string `json:"filename"`
|
||||
// Size 文件大小(字节)。
|
||||
Size int64 `json:"size"`
|
||||
// Hash 文件哈希(MD5)。
|
||||
Hash string `json:"hash"`
|
||||
// Variant 媒体变体(main/preview/cover 等)。
|
||||
Variant consts.MediaAssetVariant `json:"variant"`
|
||||
// SourceAssetID 源资产ID(用于变体关联)。
|
||||
SourceAssetID int64 `json:"source_asset_id"`
|
||||
// UsedCount 被内容引用次数。
|
||||
UsedCount int64 `json:"used_count"`
|
||||
// CreatedAt 创建时间(RFC3339)。
|
||||
CreatedAt string `json:"created_at"`
|
||||
// UpdatedAt 更新时间(RFC3339)。
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
// SuperAssetUsageResponse 超管资产用量统计响应。
|
||||
type SuperAssetUsageResponse struct {
|
||||
// TotalCount 资产总量。
|
||||
TotalCount int64 `json:"total_count"`
|
||||
// TotalSize 资产总大小(字节)。
|
||||
TotalSize int64 `json:"total_size"`
|
||||
// ByType 按媒体类型汇总的用量统计。
|
||||
ByType []SuperAssetUsageItem `json:"by_type"`
|
||||
}
|
||||
|
||||
// SuperAssetUsageItem 资产类型用量统计条目。
|
||||
type SuperAssetUsageItem struct {
|
||||
// Type 媒体类型。
|
||||
Type consts.MediaAssetType `json:"type"`
|
||||
// Count 该类型资产数量。
|
||||
Count int64 `json:"count"`
|
||||
// TotalSize 该类型资产大小总和(字节)。
|
||||
TotalSize int64 `json:"total_size"`
|
||||
}
|
||||
140
backend/app/http/super/v1/dto/super_notification.go
Normal file
140
backend/app/http/super/v1/dto/super_notification.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"quyun/v2/app/requests"
|
||||
"quyun/v2/pkg/consts"
|
||||
)
|
||||
|
||||
// SuperNotificationListFilter 超管通知列表查询条件。
|
||||
type SuperNotificationListFilter struct {
|
||||
requests.Pagination
|
||||
// ID 通知ID,精确匹配。
|
||||
ID *int64 `query:"id"`
|
||||
// TenantID 租户ID,精确匹配。
|
||||
TenantID *int64 `query:"tenant_id"`
|
||||
// TenantCode 租户编码,模糊匹配。
|
||||
TenantCode *string `query:"tenant_code"`
|
||||
// TenantName 租户名称,模糊匹配。
|
||||
TenantName *string `query:"tenant_name"`
|
||||
// UserID 用户ID,精确匹配。
|
||||
UserID *int64 `query:"user_id"`
|
||||
// Username 用户名/昵称,模糊匹配。
|
||||
Username *string `query:"username"`
|
||||
// Type 通知类型过滤。
|
||||
Type *consts.NotificationType `query:"type"`
|
||||
// IsRead 是否已读过滤。
|
||||
IsRead *bool `query:"is_read"`
|
||||
// Keyword 标题或内容关键词,模糊匹配。
|
||||
Keyword *string `query:"keyword"`
|
||||
// CreatedAtFrom 创建时间起始(RFC3339)。
|
||||
CreatedAtFrom *string `query:"created_at_from"`
|
||||
// CreatedAtTo 创建时间结束(RFC3339)。
|
||||
CreatedAtTo *string `query:"created_at_to"`
|
||||
// Asc 升序字段(id/created_at)。
|
||||
Asc *string `query:"asc"`
|
||||
// Desc 降序字段(id/created_at)。
|
||||
Desc *string `query:"desc"`
|
||||
}
|
||||
|
||||
// SuperNotificationBroadcastForm 超管通知群发参数。
|
||||
type SuperNotificationBroadcastForm struct {
|
||||
// TenantID 租户ID(选填,用于指定租户成员)。
|
||||
TenantID *int64 `json:"tenant_id"`
|
||||
// UserIDs 指定接收用户ID列表(优先级高于 TenantID)。
|
||||
UserIDs []int64 `json:"user_ids"`
|
||||
// Type 通知类型(system/order/audit/interaction)。
|
||||
Type consts.NotificationType `json:"type"`
|
||||
// Title 通知标题。
|
||||
Title string `json:"title"`
|
||||
// Content 通知内容。
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// SuperNotificationItem 超管通知条目。
|
||||
type SuperNotificationItem struct {
|
||||
// ID 通知ID。
|
||||
ID int64 `json:"id"`
|
||||
// TenantID 租户ID。
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
// TenantCode 租户编码。
|
||||
TenantCode string `json:"tenant_code"`
|
||||
// TenantName 租户名称。
|
||||
TenantName string `json:"tenant_name"`
|
||||
// UserID 用户ID。
|
||||
UserID int64 `json:"user_id"`
|
||||
// Username 用户名/昵称。
|
||||
Username string `json:"username"`
|
||||
// Type 通知类型。
|
||||
Type consts.NotificationType `json:"type"`
|
||||
// Title 通知标题。
|
||||
Title string `json:"title"`
|
||||
// Content 通知内容。
|
||||
Content string `json:"content"`
|
||||
// IsRead 是否已读。
|
||||
IsRead bool `json:"is_read"`
|
||||
// CreatedAt 创建时间(RFC3339)。
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// SuperNotificationTemplateListFilter 超管通知模板列表查询条件。
|
||||
type SuperNotificationTemplateListFilter struct {
|
||||
requests.Pagination
|
||||
// TenantID 租户ID(不传代表全平台模板)。
|
||||
TenantID *int64 `query:"tenant_id"`
|
||||
// Keyword 模板名称或标题关键字,模糊匹配。
|
||||
Keyword *string `query:"keyword"`
|
||||
// Type 通知类型过滤。
|
||||
Type *consts.NotificationType `query:"type"`
|
||||
// IsActive 是否启用过滤。
|
||||
IsActive *bool `query:"is_active"`
|
||||
// CreatedAtFrom 创建时间起始(RFC3339)。
|
||||
CreatedAtFrom *string `query:"created_at_from"`
|
||||
// CreatedAtTo 创建时间结束(RFC3339)。
|
||||
CreatedAtTo *string `query:"created_at_to"`
|
||||
// Asc 升序字段(id/created_at)。
|
||||
Asc *string `query:"asc"`
|
||||
// Desc 降序字段(id/created_at)。
|
||||
Desc *string `query:"desc"`
|
||||
}
|
||||
|
||||
// SuperNotificationTemplateCreateForm 超管通知模板创建参数。
|
||||
type SuperNotificationTemplateCreateForm struct {
|
||||
// TenantID 租户ID(不传代表全平台模板)。
|
||||
TenantID *int64 `json:"tenant_id"`
|
||||
// Name 模板名称(用于识别用途)。
|
||||
Name string `json:"name"`
|
||||
// Type 通知类型(system/order/audit/interaction)。
|
||||
Type consts.NotificationType `json:"type"`
|
||||
// Title 通知标题。
|
||||
Title string `json:"title"`
|
||||
// Content 通知内容。
|
||||
Content string `json:"content"`
|
||||
// IsActive 是否启用(不传默认启用)。
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
// SuperNotificationTemplateItem 超管通知模板条目。
|
||||
type SuperNotificationTemplateItem struct {
|
||||
// ID 模板ID。
|
||||
ID int64 `json:"id"`
|
||||
// TenantID 租户ID。
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
// TenantCode 租户编码。
|
||||
TenantCode string `json:"tenant_code"`
|
||||
// TenantName 租户名称。
|
||||
TenantName string `json:"tenant_name"`
|
||||
// Name 模板名称。
|
||||
Name string `json:"name"`
|
||||
// Type 通知类型。
|
||||
Type consts.NotificationType `json:"type"`
|
||||
// Title 模板标题。
|
||||
Title string `json:"title"`
|
||||
// Content 模板内容。
|
||||
Content string `json:"content"`
|
||||
// IsActive 是否启用。
|
||||
IsActive bool `json:"is_active"`
|
||||
// CreatedAt 创建时间(RFC3339)。
|
||||
CreatedAt string `json:"created_at"`
|
||||
// UpdatedAt 更新时间(RFC3339)。
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
72
backend/app/http/super/v1/notifications.go
Normal file
72
backend/app/http/super/v1/notifications.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
dto "quyun/v2/app/http/super/v1/dto"
|
||||
"quyun/v2/app/requests"
|
||||
"quyun/v2/app/services"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
// @provider
|
||||
type notifications struct{}
|
||||
|
||||
// List notifications
|
||||
//
|
||||
// @Router /super/v1/notifications [get]
|
||||
// @Summary List notifications
|
||||
// @Description List notifications across tenants
|
||||
// @Tags Notification
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param page query int false "Page number"
|
||||
// @Param limit query int false "Page size"
|
||||
// @Success 200 {object} requests.Pager{items=[]dto.SuperNotificationItem}
|
||||
// @Bind filter query
|
||||
func (c *notifications) List(ctx fiber.Ctx, filter *dto.SuperNotificationListFilter) (*requests.Pager, error) {
|
||||
return services.Super.ListNotifications(ctx, filter)
|
||||
}
|
||||
|
||||
// Broadcast notification
|
||||
//
|
||||
// @Router /super/v1/notifications/broadcast [post]
|
||||
// @Summary Broadcast notification
|
||||
// @Description Broadcast notification to users or tenant members
|
||||
// @Tags Notification
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param form body dto.SuperNotificationBroadcastForm true "Broadcast form"
|
||||
// @Success 200 {string} string "Sent"
|
||||
// @Bind form body
|
||||
func (c *notifications) Broadcast(ctx fiber.Ctx, form *dto.SuperNotificationBroadcastForm) error {
|
||||
return services.Super.BroadcastNotifications(ctx, form)
|
||||
}
|
||||
|
||||
// List notification templates
|
||||
//
|
||||
// @Router /super/v1/notifications/templates [get]
|
||||
// @Summary List notification templates
|
||||
// @Description List notification templates
|
||||
// @Tags Notification
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} requests.Pager{items=[]dto.SuperNotificationTemplateItem}
|
||||
// @Bind filter query
|
||||
func (c *notifications) ListTemplates(ctx fiber.Ctx, filter *dto.SuperNotificationTemplateListFilter) (*requests.Pager, error) {
|
||||
return services.Super.ListNotificationTemplates(ctx, filter)
|
||||
}
|
||||
|
||||
// Create notification template
|
||||
//
|
||||
// @Router /super/v1/notifications/templates [post]
|
||||
// @Summary Create notification template
|
||||
// @Description Create notification template
|
||||
// @Tags Notification
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param form body dto.SuperNotificationTemplateCreateForm true "Template form"
|
||||
// @Success 200 {object} dto.SuperNotificationTemplateItem
|
||||
// @Bind form body
|
||||
func (c *notifications) CreateTemplate(ctx fiber.Ctx, form *dto.SuperNotificationTemplateCreateForm) (*dto.SuperNotificationTemplateItem, error) {
|
||||
return services.Super.CreateNotificationTemplate(ctx, form)
|
||||
}
|
||||
@@ -10,6 +10,13 @@ import (
|
||||
)
|
||||
|
||||
func Provide(opts ...opt.Option) error {
|
||||
if err := container.Container.Provide(func() (*assets, error) {
|
||||
obj := &assets{}
|
||||
|
||||
return obj, nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := container.Container.Provide(func() (*contents, error) {
|
||||
obj := &contents{}
|
||||
|
||||
@@ -38,6 +45,13 @@ func Provide(opts ...opt.Option) error {
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := container.Container.Provide(func() (*notifications, error) {
|
||||
obj := ¬ifications{}
|
||||
|
||||
return obj, nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := container.Container.Provide(func() (*orders, error) {
|
||||
obj := &orders{}
|
||||
|
||||
@@ -60,11 +74,13 @@ func Provide(opts ...opt.Option) error {
|
||||
return err
|
||||
}
|
||||
if err := container.Container.Provide(func(
|
||||
assets *assets,
|
||||
contents *contents,
|
||||
coupons *coupons,
|
||||
creatorApplications *creatorApplications,
|
||||
creators *creators,
|
||||
middlewares *middlewares.Middlewares,
|
||||
notifications *notifications,
|
||||
orders *orders,
|
||||
payoutAccounts *payoutAccounts,
|
||||
reports *reports,
|
||||
@@ -73,11 +89,13 @@ func Provide(opts ...opt.Option) error {
|
||||
withdrawals *withdrawals,
|
||||
) (contracts.HttpRoute, error) {
|
||||
obj := &Routes{
|
||||
assets: assets,
|
||||
contents: contents,
|
||||
coupons: coupons,
|
||||
creatorApplications: creatorApplications,
|
||||
creators: creators,
|
||||
middlewares: middlewares,
|
||||
notifications: notifications,
|
||||
orders: orders,
|
||||
payoutAccounts: payoutAccounts,
|
||||
reports: reports,
|
||||
|
||||
@@ -25,10 +25,12 @@ type Routes struct {
|
||||
log *log.Entry `inject:"false"`
|
||||
middlewares *middlewares.Middlewares
|
||||
// Controller instances
|
||||
assets *assets
|
||||
contents *contents
|
||||
coupons *coupons
|
||||
creatorApplications *creatorApplications
|
||||
creators *creators
|
||||
notifications *notifications
|
||||
orders *orders
|
||||
payoutAccounts *payoutAccounts
|
||||
reports *reports
|
||||
@@ -52,6 +54,23 @@ func (r *Routes) Name() string {
|
||||
// Register registers all HTTP routes with the provided fiber router.
|
||||
// Each route is registered with its corresponding controller action and parameter bindings.
|
||||
func (r *Routes) Register(router fiber.Router) {
|
||||
// Register routes for controller: assets
|
||||
r.log.Debugf("Registering route: Delete /super/v1/assets/:id<int> -> assets.Delete")
|
||||
router.Delete("/super/v1/assets/:id<int>"[len(r.Path()):], Func2(
|
||||
r.assets.Delete,
|
||||
PathParam[int64]("id"),
|
||||
Query[dto.SuperAssetDeleteQuery]("query"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /super/v1/assets -> assets.List")
|
||||
router.Get("/super/v1/assets"[len(r.Path()):], DataFunc1(
|
||||
r.assets.List,
|
||||
Query[dto.SuperAssetListFilter]("filter"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /super/v1/assets/usage -> assets.Usage")
|
||||
router.Get("/super/v1/assets/usage"[len(r.Path()):], DataFunc1(
|
||||
r.assets.Usage,
|
||||
Query[dto.SuperAssetUsageFilter]("filter"),
|
||||
))
|
||||
// Register routes for controller: contents
|
||||
r.log.Debugf("Registering route: Get /super/v1/contents -> contents.List")
|
||||
router.Get("/super/v1/contents"[len(r.Path()):], DataFunc1(
|
||||
@@ -154,6 +173,27 @@ func (r *Routes) Register(router fiber.Router) {
|
||||
r.creators.List,
|
||||
Query[dto.TenantListFilter]("filter"),
|
||||
))
|
||||
// Register routes for controller: notifications
|
||||
r.log.Debugf("Registering route: Get /super/v1/notifications -> notifications.List")
|
||||
router.Get("/super/v1/notifications"[len(r.Path()):], DataFunc1(
|
||||
r.notifications.List,
|
||||
Query[dto.SuperNotificationListFilter]("filter"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /super/v1/notifications/templates -> notifications.ListTemplates")
|
||||
router.Get("/super/v1/notifications/templates"[len(r.Path()):], DataFunc1(
|
||||
r.notifications.ListTemplates,
|
||||
Query[dto.SuperNotificationTemplateListFilter]("filter"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /super/v1/notifications/broadcast -> notifications.Broadcast")
|
||||
router.Post("/super/v1/notifications/broadcast"[len(r.Path()):], Func1(
|
||||
r.notifications.Broadcast,
|
||||
Body[dto.SuperNotificationBroadcastForm]("form"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /super/v1/notifications/templates -> notifications.CreateTemplate")
|
||||
router.Post("/super/v1/notifications/templates"[len(r.Path()):], DataFunc1(
|
||||
r.notifications.CreateTemplate,
|
||||
Body[dto.SuperNotificationTemplateCreateForm]("form"),
|
||||
))
|
||||
// Register routes for controller: orders
|
||||
r.log.Debugf("Registering route: Get /super/v1/orders -> orders.List")
|
||||
router.Get("/super/v1/orders"[len(r.Path()):], DataFunc1(
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -2127,6 +2128,784 @@ func (s *super) ContentStatistics(ctx context.Context, filter *super_dto.SuperCo
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *super) ListAssets(ctx context.Context, filter *super_dto.SuperAssetListFilter) (*requests.Pager, error) {
|
||||
if filter == nil {
|
||||
filter = &super_dto.SuperAssetListFilter{}
|
||||
}
|
||||
|
||||
tbl, q := models.MediaAssetQuery.QueryContext(ctx)
|
||||
|
||||
if filter.ID != nil && *filter.ID > 0 {
|
||||
q = q.Where(tbl.ID.Eq(*filter.ID))
|
||||
}
|
||||
if filter.TenantID != nil && *filter.TenantID > 0 {
|
||||
q = q.Where(tbl.TenantID.Eq(*filter.TenantID))
|
||||
}
|
||||
if filter.UserID != nil && *filter.UserID > 0 {
|
||||
q = q.Where(tbl.UserID.Eq(*filter.UserID))
|
||||
}
|
||||
if filter.Type != nil && *filter.Type != "" {
|
||||
q = q.Where(tbl.Type.Eq(*filter.Type))
|
||||
}
|
||||
if filter.Status != nil && *filter.Status != "" {
|
||||
q = q.Where(tbl.Status.Eq(*filter.Status))
|
||||
}
|
||||
if filter.Provider != nil && strings.TrimSpace(*filter.Provider) != "" {
|
||||
q = q.Where(tbl.Provider.Eq(strings.TrimSpace(*filter.Provider)))
|
||||
}
|
||||
if filter.ObjectKey != nil && strings.TrimSpace(*filter.ObjectKey) != "" {
|
||||
keyword := "%" + strings.TrimSpace(*filter.ObjectKey) + "%"
|
||||
q = q.Where(tbl.ObjectKey.Like(keyword))
|
||||
}
|
||||
|
||||
tenantIDs, tenantFilter, err := s.lookupTenantIDs(ctx, filter.TenantCode, filter.TenantName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tenantFilter {
|
||||
if len(tenantIDs) == 0 {
|
||||
q = q.Where(tbl.ID.Eq(-1))
|
||||
} else {
|
||||
q = q.Where(tbl.TenantID.In(tenantIDs...))
|
||||
}
|
||||
}
|
||||
|
||||
userIDs, userFilter, err := s.lookupUserIDs(ctx, filter.Username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if userFilter {
|
||||
if len(userIDs) == 0 {
|
||||
q = q.Where(tbl.ID.Eq(-1))
|
||||
} else {
|
||||
q = q.Where(tbl.UserID.In(userIDs...))
|
||||
}
|
||||
}
|
||||
|
||||
if filter.CreatedAtFrom != nil {
|
||||
from, err := s.parseFilterTime(filter.CreatedAtFrom)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if from != nil {
|
||||
q = q.Where(tbl.CreatedAt.Gte(*from))
|
||||
}
|
||||
}
|
||||
if filter.CreatedAtTo != nil {
|
||||
to, err := s.parseFilterTime(filter.CreatedAtTo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if to != nil {
|
||||
q = q.Where(tbl.CreatedAt.Lte(*to))
|
||||
}
|
||||
}
|
||||
|
||||
if filter.SizeMin != nil {
|
||||
// JSONB 元信息内的 size 需要使用原生表达式过滤。
|
||||
q = q.Where(field.NewUnsafeFieldRaw("coalesce((meta->>'size')::bigint,0) >= ?", *filter.SizeMin))
|
||||
}
|
||||
if filter.SizeMax != nil {
|
||||
// JSONB 元信息内的 size 需要使用原生表达式过滤。
|
||||
q = q.Where(field.NewUnsafeFieldRaw("coalesce((meta->>'size')::bigint,0) <= ?", *filter.SizeMax))
|
||||
}
|
||||
|
||||
orderApplied := false
|
||||
if filter.Desc != nil && strings.TrimSpace(*filter.Desc) != "" {
|
||||
switch strings.TrimSpace(*filter.Desc) {
|
||||
case "id":
|
||||
q = q.Order(tbl.ID.Desc())
|
||||
case "created_at":
|
||||
q = q.Order(tbl.CreatedAt.Desc())
|
||||
}
|
||||
orderApplied = true
|
||||
} else if filter.Asc != nil && strings.TrimSpace(*filter.Asc) != "" {
|
||||
switch strings.TrimSpace(*filter.Asc) {
|
||||
case "id":
|
||||
q = q.Order(tbl.ID)
|
||||
case "created_at":
|
||||
q = q.Order(tbl.CreatedAt)
|
||||
}
|
||||
orderApplied = true
|
||||
}
|
||||
if !orderApplied {
|
||||
q = q.Order(tbl.CreatedAt.Desc())
|
||||
}
|
||||
|
||||
filter.Pagination.Format()
|
||||
total, err := q.Count()
|
||||
if err != nil {
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
list, err := q.Offset(int(filter.Pagination.Offset())).Limit(int(filter.Pagination.Limit)).Find()
|
||||
if err != nil {
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
if len(list) == 0 {
|
||||
return &requests.Pager{
|
||||
Pagination: filter.Pagination,
|
||||
Total: total,
|
||||
Items: []super_dto.SuperAssetItem{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
tenantSet := make(map[int64]struct{})
|
||||
userSet := make(map[int64]struct{})
|
||||
assetIDs := make([]int64, 0, len(list))
|
||||
for _, asset := range list {
|
||||
assetIDs = append(assetIDs, asset.ID)
|
||||
if asset.TenantID > 0 {
|
||||
tenantSet[asset.TenantID] = struct{}{}
|
||||
}
|
||||
if asset.UserID > 0 {
|
||||
userSet[asset.UserID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
tenantMap := make(map[int64]*models.Tenant, len(tenantSet))
|
||||
if len(tenantSet) > 0 {
|
||||
ids := make([]int64, 0, len(tenantSet))
|
||||
for id := range tenantSet {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
tenantTbl, tenantQuery := models.TenantQuery.QueryContext(ctx)
|
||||
tenants, err := tenantQuery.Where(tenantTbl.ID.In(ids...)).Find()
|
||||
if err != nil {
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
for _, tenant := range tenants {
|
||||
tenantMap[tenant.ID] = tenant
|
||||
}
|
||||
}
|
||||
|
||||
userMap := make(map[int64]*models.User, len(userSet))
|
||||
if len(userSet) > 0 {
|
||||
ids := make([]int64, 0, len(userSet))
|
||||
for id := range userSet {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
userTbl, userQuery := models.UserQuery.QueryContext(ctx)
|
||||
users, err := userQuery.Where(userTbl.ID.In(ids...)).Find()
|
||||
if err != nil {
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
for _, user := range users {
|
||||
userMap[user.ID] = user
|
||||
}
|
||||
}
|
||||
|
||||
usedCountMap := make(map[int64]int64)
|
||||
if len(assetIDs) > 0 {
|
||||
type assetUseRow struct {
|
||||
AssetID int64 `gorm:"column:asset_id"`
|
||||
Count int64 `gorm:"column:count"`
|
||||
}
|
||||
rows := make([]assetUseRow, 0)
|
||||
query := models.ContentAssetQuery.WithContext(ctx).
|
||||
UnderlyingDB().
|
||||
Model(&models.ContentAsset{}).
|
||||
Select("asset_id, count(*) as count").
|
||||
Where("asset_id in ?", assetIDs).
|
||||
Group("asset_id")
|
||||
if err := query.Scan(&rows).Error; err != nil {
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
for _, row := range rows {
|
||||
usedCountMap[row.AssetID] = row.Count
|
||||
}
|
||||
}
|
||||
|
||||
items := make([]super_dto.SuperAssetItem, 0, len(list))
|
||||
for _, asset := range list {
|
||||
meta := asset.Meta.Data()
|
||||
filename := strings.TrimSpace(meta.Filename)
|
||||
if filename == "" {
|
||||
filename = filepath.Base(asset.ObjectKey)
|
||||
}
|
||||
url := ""
|
||||
if Common != nil {
|
||||
url = Common.GetAssetURL(asset.ObjectKey)
|
||||
}
|
||||
item := super_dto.SuperAssetItem{
|
||||
ID: asset.ID,
|
||||
TenantID: asset.TenantID,
|
||||
UserID: asset.UserID,
|
||||
Type: asset.Type,
|
||||
Status: asset.Status,
|
||||
Provider: asset.Provider,
|
||||
Bucket: asset.Bucket,
|
||||
ObjectKey: asset.ObjectKey,
|
||||
URL: url,
|
||||
Filename: filename,
|
||||
Size: meta.Size,
|
||||
Hash: asset.Hash,
|
||||
Variant: asset.Variant,
|
||||
SourceAssetID: asset.SourceAssetID,
|
||||
UsedCount: usedCountMap[asset.ID],
|
||||
CreatedAt: s.formatTime(asset.CreatedAt),
|
||||
UpdatedAt: s.formatTime(asset.UpdatedAt),
|
||||
}
|
||||
if tenant := tenantMap[asset.TenantID]; tenant != nil {
|
||||
item.TenantCode = tenant.Code
|
||||
item.TenantName = tenant.Name
|
||||
}
|
||||
if user := userMap[asset.UserID]; user != nil {
|
||||
item.Username = user.Username
|
||||
} else if asset.UserID > 0 {
|
||||
item.Username = "ID:" + strconv.FormatInt(asset.UserID, 10)
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
return &requests.Pager{
|
||||
Pagination: filter.Pagination,
|
||||
Total: total,
|
||||
Items: items,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *super) AssetUsage(ctx context.Context, filter *super_dto.SuperAssetUsageFilter) (*super_dto.SuperAssetUsageResponse, error) {
|
||||
tenantID := int64(0)
|
||||
if filter != nil && filter.TenantID != nil {
|
||||
tenantID = *filter.TenantID
|
||||
}
|
||||
|
||||
tbl, q := models.MediaAssetQuery.QueryContext(ctx)
|
||||
if tenantID > 0 {
|
||||
q = q.Where(tbl.TenantID.Eq(tenantID))
|
||||
}
|
||||
total, err := q.Count()
|
||||
if err != nil {
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
|
||||
var totalSize int64
|
||||
sizeQuery := models.MediaAssetQuery.WithContext(ctx).
|
||||
UnderlyingDB().
|
||||
Model(&models.MediaAsset{}).
|
||||
Select("coalesce(sum((meta->>'size')::bigint), 0) as size")
|
||||
if tenantID > 0 {
|
||||
sizeQuery = sizeQuery.Where("tenant_id = ?", tenantID)
|
||||
}
|
||||
if err := sizeQuery.Scan(&totalSize).Error; err != nil {
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
|
||||
type usageRow struct {
|
||||
Type string `gorm:"column:type"`
|
||||
Count int64 `gorm:"column:count"`
|
||||
Size int64 `gorm:"column:size"`
|
||||
}
|
||||
rows := make([]usageRow, 0)
|
||||
typeQuery := models.MediaAssetQuery.WithContext(ctx).
|
||||
UnderlyingDB().
|
||||
Model(&models.MediaAsset{}).
|
||||
Select("type, count(*) as count, coalesce(sum((meta->>'size')::bigint), 0) as size").
|
||||
Group("type")
|
||||
if tenantID > 0 {
|
||||
typeQuery = typeQuery.Where("tenant_id = ?", tenantID)
|
||||
}
|
||||
if err := typeQuery.Scan(&rows).Error; err != nil {
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
|
||||
byType := make([]super_dto.SuperAssetUsageItem, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
byType = append(byType, super_dto.SuperAssetUsageItem{
|
||||
Type: consts.MediaAssetType(row.Type),
|
||||
Count: row.Count,
|
||||
TotalSize: row.Size,
|
||||
})
|
||||
}
|
||||
|
||||
return &super_dto.SuperAssetUsageResponse{
|
||||
TotalCount: total,
|
||||
TotalSize: totalSize,
|
||||
ByType: byType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *super) DeleteAsset(ctx context.Context, assetID int64, force bool) error {
|
||||
if assetID <= 0 {
|
||||
return errorx.ErrBadRequest.WithMsg("资产ID不能为空")
|
||||
}
|
||||
|
||||
tbl, q := models.MediaAssetQuery.QueryContext(ctx)
|
||||
asset, err := q.Where(tbl.ID.Eq(assetID)).First()
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errorx.ErrRecordNotFound.WithMsg("资产不存在")
|
||||
}
|
||||
return errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
|
||||
useTbl, useQuery := models.ContentAssetQuery.QueryContext(ctx)
|
||||
usedCount, err := useQuery.Where(useTbl.AssetID.Eq(assetID)).Count()
|
||||
if err != nil {
|
||||
return errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
if usedCount > 0 && !force {
|
||||
return errorx.ErrStatusConflict.WithMsg("资产已被内容引用,无法删除")
|
||||
}
|
||||
if usedCount > 0 && force {
|
||||
// 强制删除时先清理引用关系,避免残留无效关联。
|
||||
if _, err := useQuery.Where(useTbl.AssetID.Eq(assetID)).Delete(); err != nil {
|
||||
return errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := q.Where(tbl.ID.Eq(assetID)).Delete(); err != nil {
|
||||
return errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
|
||||
if Common != nil && asset.ObjectKey != "" {
|
||||
count, err := models.MediaAssetQuery.WithContext(ctx).
|
||||
Where(models.MediaAssetQuery.ObjectKey.Eq(asset.ObjectKey)).
|
||||
Count()
|
||||
if err != nil {
|
||||
return errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
if count == 0 && Common.storage != nil {
|
||||
_ = Common.storage.Delete(asset.ObjectKey)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *super) ListNotifications(ctx context.Context, filter *super_dto.SuperNotificationListFilter) (*requests.Pager, error) {
|
||||
if filter == nil {
|
||||
filter = &super_dto.SuperNotificationListFilter{}
|
||||
}
|
||||
|
||||
tbl, q := models.NotificationQuery.QueryContext(ctx)
|
||||
|
||||
if filter.ID != nil && *filter.ID > 0 {
|
||||
q = q.Where(tbl.ID.Eq(*filter.ID))
|
||||
}
|
||||
if filter.TenantID != nil && *filter.TenantID > 0 {
|
||||
q = q.Where(tbl.TenantID.Eq(*filter.TenantID))
|
||||
}
|
||||
if filter.UserID != nil && *filter.UserID > 0 {
|
||||
q = q.Where(tbl.UserID.Eq(*filter.UserID))
|
||||
}
|
||||
if filter.Type != nil && *filter.Type != "" {
|
||||
q = q.Where(tbl.Type.Eq(filter.Type.String()))
|
||||
}
|
||||
if filter.IsRead != nil {
|
||||
q = q.Where(tbl.IsRead.Is(*filter.IsRead))
|
||||
}
|
||||
if filter.Keyword != nil && strings.TrimSpace(*filter.Keyword) != "" {
|
||||
keyword := "%" + strings.TrimSpace(*filter.Keyword) + "%"
|
||||
q = q.Where(field.Or(tbl.Title.Like(keyword), tbl.Content.Like(keyword)))
|
||||
}
|
||||
|
||||
tenantIDs, tenantFilter, err := s.lookupTenantIDs(ctx, filter.TenantCode, filter.TenantName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tenantFilter {
|
||||
if len(tenantIDs) == 0 {
|
||||
q = q.Where(tbl.ID.Eq(-1))
|
||||
} else {
|
||||
q = q.Where(tbl.TenantID.In(tenantIDs...))
|
||||
}
|
||||
}
|
||||
|
||||
userIDs, userFilter, err := s.lookupUserIDs(ctx, filter.Username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if userFilter {
|
||||
if len(userIDs) == 0 {
|
||||
q = q.Where(tbl.ID.Eq(-1))
|
||||
} else {
|
||||
q = q.Where(tbl.UserID.In(userIDs...))
|
||||
}
|
||||
}
|
||||
|
||||
if filter.CreatedAtFrom != nil {
|
||||
from, err := s.parseFilterTime(filter.CreatedAtFrom)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if from != nil {
|
||||
q = q.Where(tbl.CreatedAt.Gte(*from))
|
||||
}
|
||||
}
|
||||
if filter.CreatedAtTo != nil {
|
||||
to, err := s.parseFilterTime(filter.CreatedAtTo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if to != nil {
|
||||
q = q.Where(tbl.CreatedAt.Lte(*to))
|
||||
}
|
||||
}
|
||||
|
||||
orderApplied := false
|
||||
if filter.Desc != nil && strings.TrimSpace(*filter.Desc) != "" {
|
||||
switch strings.TrimSpace(*filter.Desc) {
|
||||
case "id":
|
||||
q = q.Order(tbl.ID.Desc())
|
||||
case "created_at":
|
||||
q = q.Order(tbl.CreatedAt.Desc())
|
||||
}
|
||||
orderApplied = true
|
||||
} else if filter.Asc != nil && strings.TrimSpace(*filter.Asc) != "" {
|
||||
switch strings.TrimSpace(*filter.Asc) {
|
||||
case "id":
|
||||
q = q.Order(tbl.ID)
|
||||
case "created_at":
|
||||
q = q.Order(tbl.CreatedAt)
|
||||
}
|
||||
orderApplied = true
|
||||
}
|
||||
if !orderApplied {
|
||||
q = q.Order(tbl.CreatedAt.Desc())
|
||||
}
|
||||
|
||||
filter.Pagination.Format()
|
||||
total, err := q.Count()
|
||||
if err != nil {
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
list, err := q.Offset(int(filter.Pagination.Offset())).Limit(int(filter.Pagination.Limit)).Find()
|
||||
if err != nil {
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
if len(list) == 0 {
|
||||
return &requests.Pager{
|
||||
Pagination: filter.Pagination,
|
||||
Total: total,
|
||||
Items: []super_dto.SuperNotificationItem{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
tenantSet := make(map[int64]struct{})
|
||||
userSet := make(map[int64]struct{})
|
||||
for _, n := range list {
|
||||
if n.TenantID > 0 {
|
||||
tenantSet[n.TenantID] = struct{}{}
|
||||
}
|
||||
if n.UserID > 0 {
|
||||
userSet[n.UserID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
tenantMap := make(map[int64]*models.Tenant, len(tenantSet))
|
||||
if len(tenantSet) > 0 {
|
||||
ids := make([]int64, 0, len(tenantSet))
|
||||
for id := range tenantSet {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
tenantTbl, tenantQuery := models.TenantQuery.QueryContext(ctx)
|
||||
tenants, err := tenantQuery.Where(tenantTbl.ID.In(ids...)).Find()
|
||||
if err != nil {
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
for _, tenant := range tenants {
|
||||
tenantMap[tenant.ID] = tenant
|
||||
}
|
||||
}
|
||||
|
||||
userMap := make(map[int64]*models.User, len(userSet))
|
||||
if len(userSet) > 0 {
|
||||
ids := make([]int64, 0, len(userSet))
|
||||
for id := range userSet {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
userTbl, userQuery := models.UserQuery.QueryContext(ctx)
|
||||
users, err := userQuery.Where(userTbl.ID.In(ids...)).Find()
|
||||
if err != nil {
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
for _, user := range users {
|
||||
userMap[user.ID] = user
|
||||
}
|
||||
}
|
||||
|
||||
items := make([]super_dto.SuperNotificationItem, 0, len(list))
|
||||
for _, n := range list {
|
||||
item := super_dto.SuperNotificationItem{
|
||||
ID: n.ID,
|
||||
TenantID: n.TenantID,
|
||||
UserID: n.UserID,
|
||||
Type: consts.NotificationType(n.Type),
|
||||
Title: n.Title,
|
||||
Content: n.Content,
|
||||
IsRead: n.IsRead,
|
||||
CreatedAt: s.formatTime(n.CreatedAt),
|
||||
}
|
||||
if tenant := tenantMap[n.TenantID]; tenant != nil {
|
||||
item.TenantCode = tenant.Code
|
||||
item.TenantName = tenant.Name
|
||||
}
|
||||
if user := userMap[n.UserID]; user != nil {
|
||||
item.Username = user.Username
|
||||
} else if n.UserID > 0 {
|
||||
item.Username = "ID:" + strconv.FormatInt(n.UserID, 10)
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
return &requests.Pager{
|
||||
Pagination: filter.Pagination,
|
||||
Total: total,
|
||||
Items: items,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *super) BroadcastNotifications(ctx context.Context, form *super_dto.SuperNotificationBroadcastForm) error {
|
||||
if form == nil {
|
||||
return errorx.ErrBadRequest.WithMsg("群发参数不能为空")
|
||||
}
|
||||
|
||||
title := strings.TrimSpace(form.Title)
|
||||
content := strings.TrimSpace(form.Content)
|
||||
if title == "" || content == "" {
|
||||
return errorx.ErrBadRequest.WithMsg("通知标题和内容不能为空")
|
||||
}
|
||||
if !form.Type.IsValid() {
|
||||
return errorx.ErrBadRequest.WithMsg("通知类型非法")
|
||||
}
|
||||
|
||||
tenantID := int64(0)
|
||||
if form.TenantID != nil {
|
||||
tenantID = *form.TenantID
|
||||
}
|
||||
|
||||
userSet := make(map[int64]struct{})
|
||||
for _, id := range form.UserIDs {
|
||||
if id <= 0 {
|
||||
continue
|
||||
}
|
||||
userSet[id] = struct{}{}
|
||||
}
|
||||
|
||||
if len(userSet) == 0 && tenantID <= 0 {
|
||||
return errorx.ErrBadRequest.WithMsg("请指定租户或用户")
|
||||
}
|
||||
|
||||
if len(userSet) == 0 && tenantID > 0 {
|
||||
// 仅向该租户的已验证成员发送。
|
||||
tbl, q := models.TenantUserQuery.QueryContext(ctx)
|
||||
list, err := q.Where(tbl.TenantID.Eq(tenantID), tbl.Status.Eq(consts.UserStatusVerified)).Find()
|
||||
if err != nil {
|
||||
return errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
for _, tu := range list {
|
||||
if tu.UserID > 0 {
|
||||
userSet[tu.UserID] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(userSet) == 0 {
|
||||
return errorx.ErrRecordNotFound.WithMsg("未找到可通知的用户")
|
||||
}
|
||||
if Notification == nil {
|
||||
return errorx.ErrInternalError.WithMsg("通知服务不可用")
|
||||
}
|
||||
|
||||
typ := form.Type.String()
|
||||
for userID := range userSet {
|
||||
if err := Notification.Send(ctx, tenantID, userID, typ, title, content); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *super) ListNotificationTemplates(ctx context.Context, filter *super_dto.SuperNotificationTemplateListFilter) (*requests.Pager, error) {
|
||||
if filter == nil {
|
||||
filter = &super_dto.SuperNotificationTemplateListFilter{}
|
||||
}
|
||||
|
||||
tbl, q := models.NotificationTemplateQuery.QueryContext(ctx)
|
||||
|
||||
if filter.TenantID != nil && *filter.TenantID >= 0 {
|
||||
q = q.Where(tbl.TenantID.Eq(*filter.TenantID))
|
||||
}
|
||||
if filter.Type != nil && *filter.Type != "" {
|
||||
q = q.Where(tbl.Type.Eq(*filter.Type))
|
||||
}
|
||||
if filter.IsActive != nil {
|
||||
q = q.Where(tbl.IsActive.Is(*filter.IsActive))
|
||||
}
|
||||
if filter.Keyword != nil && strings.TrimSpace(*filter.Keyword) != "" {
|
||||
keyword := "%" + strings.TrimSpace(*filter.Keyword) + "%"
|
||||
q = q.Where(field.Or(tbl.Name.Like(keyword), tbl.Title.Like(keyword)))
|
||||
}
|
||||
|
||||
if filter.CreatedAtFrom != nil {
|
||||
from, err := s.parseFilterTime(filter.CreatedAtFrom)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if from != nil {
|
||||
q = q.Where(tbl.CreatedAt.Gte(*from))
|
||||
}
|
||||
}
|
||||
if filter.CreatedAtTo != nil {
|
||||
to, err := s.parseFilterTime(filter.CreatedAtTo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if to != nil {
|
||||
q = q.Where(tbl.CreatedAt.Lte(*to))
|
||||
}
|
||||
}
|
||||
|
||||
orderApplied := false
|
||||
if filter.Desc != nil && strings.TrimSpace(*filter.Desc) != "" {
|
||||
switch strings.TrimSpace(*filter.Desc) {
|
||||
case "id":
|
||||
q = q.Order(tbl.ID.Desc())
|
||||
case "created_at":
|
||||
q = q.Order(tbl.CreatedAt.Desc())
|
||||
}
|
||||
orderApplied = true
|
||||
} else if filter.Asc != nil && strings.TrimSpace(*filter.Asc) != "" {
|
||||
switch strings.TrimSpace(*filter.Asc) {
|
||||
case "id":
|
||||
q = q.Order(tbl.ID)
|
||||
case "created_at":
|
||||
q = q.Order(tbl.CreatedAt)
|
||||
}
|
||||
orderApplied = true
|
||||
}
|
||||
if !orderApplied {
|
||||
q = q.Order(tbl.CreatedAt.Desc())
|
||||
}
|
||||
|
||||
filter.Pagination.Format()
|
||||
total, err := q.Count()
|
||||
if err != nil {
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
list, err := q.Offset(int(filter.Pagination.Offset())).Limit(int(filter.Pagination.Limit)).Find()
|
||||
if err != nil {
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
if len(list) == 0 {
|
||||
return &requests.Pager{
|
||||
Pagination: filter.Pagination,
|
||||
Total: total,
|
||||
Items: []super_dto.SuperNotificationTemplateItem{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
tenantSet := make(map[int64]struct{})
|
||||
for _, tmpl := range list {
|
||||
if tmpl.TenantID > 0 {
|
||||
tenantSet[tmpl.TenantID] = struct{}{}
|
||||
}
|
||||
}
|
||||
tenantMap := make(map[int64]*models.Tenant, len(tenantSet))
|
||||
if len(tenantSet) > 0 {
|
||||
ids := make([]int64, 0, len(tenantSet))
|
||||
for id := range tenantSet {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
tenantTbl, tenantQuery := models.TenantQuery.QueryContext(ctx)
|
||||
tenants, err := tenantQuery.Where(tenantTbl.ID.In(ids...)).Find()
|
||||
if err != nil {
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
for _, tenant := range tenants {
|
||||
tenantMap[tenant.ID] = tenant
|
||||
}
|
||||
}
|
||||
|
||||
items := make([]super_dto.SuperNotificationTemplateItem, 0, len(list))
|
||||
for _, tmpl := range list {
|
||||
item := super_dto.SuperNotificationTemplateItem{
|
||||
ID: tmpl.ID,
|
||||
TenantID: tmpl.TenantID,
|
||||
Name: tmpl.Name,
|
||||
Type: tmpl.Type,
|
||||
Title: tmpl.Title,
|
||||
Content: tmpl.Content,
|
||||
IsActive: tmpl.IsActive,
|
||||
CreatedAt: s.formatTime(tmpl.CreatedAt),
|
||||
UpdatedAt: s.formatTime(tmpl.UpdatedAt),
|
||||
}
|
||||
if tenant := tenantMap[tmpl.TenantID]; tenant != nil {
|
||||
item.TenantCode = tenant.Code
|
||||
item.TenantName = tenant.Name
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
return &requests.Pager{
|
||||
Pagination: filter.Pagination,
|
||||
Total: total,
|
||||
Items: items,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *super) CreateNotificationTemplate(ctx context.Context, form *super_dto.SuperNotificationTemplateCreateForm) (*super_dto.SuperNotificationTemplateItem, error) {
|
||||
if form == nil {
|
||||
return nil, errorx.ErrBadRequest.WithMsg("模板参数不能为空")
|
||||
}
|
||||
|
||||
name := strings.TrimSpace(form.Name)
|
||||
title := strings.TrimSpace(form.Title)
|
||||
content := strings.TrimSpace(form.Content)
|
||||
if name == "" || title == "" || content == "" {
|
||||
return nil, errorx.ErrBadRequest.WithMsg("模板名称、标题和内容不能为空")
|
||||
}
|
||||
if !form.Type.IsValid() {
|
||||
return nil, errorx.ErrBadRequest.WithMsg("通知类型非法")
|
||||
}
|
||||
|
||||
tenantID := int64(0)
|
||||
if form.TenantID != nil {
|
||||
tenantID = *form.TenantID
|
||||
}
|
||||
isActive := true
|
||||
if form.IsActive != nil {
|
||||
isActive = *form.IsActive
|
||||
}
|
||||
|
||||
tmpl := &models.NotificationTemplate{
|
||||
TenantID: tenantID,
|
||||
Name: name,
|
||||
Type: form.Type,
|
||||
Title: title,
|
||||
Content: content,
|
||||
IsActive: isActive,
|
||||
}
|
||||
if err := models.NotificationTemplateQuery.WithContext(ctx).Create(tmpl); err != nil {
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
|
||||
item := &super_dto.SuperNotificationTemplateItem{
|
||||
ID: tmpl.ID,
|
||||
TenantID: tmpl.TenantID,
|
||||
Name: tmpl.Name,
|
||||
Type: tmpl.Type,
|
||||
Title: tmpl.Title,
|
||||
Content: tmpl.Content,
|
||||
IsActive: tmpl.IsActive,
|
||||
CreatedAt: s.formatTime(tmpl.CreatedAt),
|
||||
UpdatedAt: s.formatTime(tmpl.UpdatedAt),
|
||||
}
|
||||
if tmpl.TenantID > 0 {
|
||||
tenantTbl, tenantQuery := models.TenantQuery.QueryContext(ctx)
|
||||
tenant, err := tenantQuery.Where(tenantTbl.ID.Eq(tmpl.TenantID)).First()
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
if tenant != nil {
|
||||
item.TenantCode = tenant.Code
|
||||
item.TenantName = tenant.Name
|
||||
}
|
||||
}
|
||||
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (s *super) ListOrders(ctx context.Context, filter *super_dto.SuperOrderListFilter) (*requests.Pager, error) {
|
||||
tbl, q := models.OrderQuery.QueryContext(ctx)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user