diff --git a/backend/app/http/super/v1/audit_logs.go b/backend/app/http/super/v1/audit_logs.go new file mode 100644 index 0000000..ceffaf0 --- /dev/null +++ b/backend/app/http/super/v1/audit_logs.go @@ -0,0 +1,28 @@ +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 auditLogs struct{} + +// List audit logs +// +// @Router /super/v1/audit-logs [get] +// @Summary List audit logs +// @Description List audit logs across tenants +// @Tags Audit +// @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.SuperAuditLogItem} +// @Bind filter query +func (c *auditLogs) List(ctx fiber.Ctx, filter *dto.SuperAuditLogListFilter) (*requests.Pager, error) { + return services.Super.ListAuditLogs(ctx, filter) +} diff --git a/backend/app/http/super/v1/dto/super_audit.go b/backend/app/http/super/v1/dto/super_audit.go new file mode 100644 index 0000000..ad0b915 --- /dev/null +++ b/backend/app/http/super/v1/dto/super_audit.go @@ -0,0 +1,117 @@ +package dto + +import ( + "encoding/json" + + "quyun/v2/app/requests" +) + +// SuperAuditLogListFilter 超管审计日志列表过滤条件。 +type SuperAuditLogListFilter 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"` + // OperatorID 操作者用户ID,精确匹配。 + OperatorID *int64 `query:"operator_id"` + // OperatorName 操作者用户名/昵称,模糊匹配。 + OperatorName *string `query:"operator_name"` + // Action 动作标识过滤,精确匹配。 + Action *string `query:"action"` + // TargetID 目标ID过滤,精确匹配。 + TargetID *string `query:"target_id"` + // 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"` +} + +// SuperAuditLogItem 超管审计日志条目。 +type SuperAuditLogItem 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"` + // OperatorID 操作者用户ID。 + OperatorID int64 `json:"operator_id"` + // OperatorName 操作者用户名/昵称。 + OperatorName string `json:"operator_name"` + // Action 动作标识。 + Action string `json:"action"` + // TargetID 目标ID。 + TargetID string `json:"target_id"` + // Detail 操作详情。 + Detail string `json:"detail"` + // CreatedAt 创建时间(RFC3339)。 + CreatedAt string `json:"created_at"` +} + +// SuperSystemConfigListFilter 超管系统配置列表过滤条件。 +type SuperSystemConfigListFilter struct { + requests.Pagination + // ConfigKey 配置Key,精确匹配。 + ConfigKey *string `query:"config_key"` + // Keyword Key或描述关键词,模糊匹配。 + Keyword *string `query:"keyword"` + // CreatedAtFrom 创建时间起始(RFC3339)。 + CreatedAtFrom *string `query:"created_at_from"` + // CreatedAtTo 创建时间结束(RFC3339)。 + CreatedAtTo *string `query:"created_at_to"` + // UpdatedAtFrom 更新时间起始(RFC3339)。 + UpdatedAtFrom *string `query:"updated_at_from"` + // UpdatedAtTo 更新时间结束(RFC3339)。 + UpdatedAtTo *string `query:"updated_at_to"` + // Asc 升序字段(id/config_key/updated_at)。 + Asc *string `query:"asc"` + // Desc 降序字段(id/config_key/updated_at)。 + Desc *string `query:"desc"` +} + +// SuperSystemConfigCreateForm 超管系统配置创建参数。 +type SuperSystemConfigCreateForm struct { + // ConfigKey 配置项Key(唯一)。 + ConfigKey string `json:"config_key"` + // Value 配置值(JSON)。 + Value json.RawMessage `json:"value"` + // Description 配置说明。 + Description string `json:"description"` +} + +// SuperSystemConfigUpdateForm 超管系统配置更新参数。 +type SuperSystemConfigUpdateForm struct { + // Value 配置值(JSON,可选)。 + Value *json.RawMessage `json:"value"` + // Description 配置说明(可选)。 + Description *string `json:"description"` +} + +// SuperSystemConfigItem 超管系统配置条目。 +type SuperSystemConfigItem struct { + // ID 配置ID。 + ID int64 `json:"id"` + // ConfigKey 配置项Key。 + ConfigKey string `json:"config_key"` + // Value 配置值(JSON)。 + Value json.RawMessage `json:"value"` + // Description 配置说明。 + Description string `json:"description"` + // CreatedAt 创建时间(RFC3339)。 + CreatedAt string `json:"created_at"` + // UpdatedAt 更新时间(RFC3339)。 + UpdatedAt string `json:"updated_at"` +} diff --git a/backend/app/http/super/v1/provider.gen.go b/backend/app/http/super/v1/provider.gen.go index ef58589..4ed37fc 100755 --- a/backend/app/http/super/v1/provider.gen.go +++ b/backend/app/http/super/v1/provider.gen.go @@ -17,6 +17,13 @@ func Provide(opts ...opt.Option) error { }); err != nil { return err } + if err := container.Container.Provide(func() (*auditLogs, error) { + obj := &auditLogs{} + + return obj, nil + }); err != nil { + return err + } if err := container.Container.Provide(func() (*contents, error) { obj := &contents{} @@ -75,6 +82,7 @@ func Provide(opts ...opt.Option) error { } if err := container.Container.Provide(func( assets *assets, + auditLogs *auditLogs, contents *contents, coupons *coupons, creatorApplications *creatorApplications, @@ -84,12 +92,14 @@ func Provide(opts ...opt.Option) error { orders *orders, payoutAccounts *payoutAccounts, reports *reports, + systemConfigs *systemConfigs, tenants *tenants, users *users, withdrawals *withdrawals, ) (contracts.HttpRoute, error) { obj := &Routes{ assets: assets, + auditLogs: auditLogs, contents: contents, coupons: coupons, creatorApplications: creatorApplications, @@ -99,6 +109,7 @@ func Provide(opts ...opt.Option) error { orders: orders, payoutAccounts: payoutAccounts, reports: reports, + systemConfigs: systemConfigs, tenants: tenants, users: users, withdrawals: withdrawals, @@ -111,6 +122,13 @@ func Provide(opts ...opt.Option) error { }, atom.GroupRoutes); err != nil { return err } + if err := container.Container.Provide(func() (*systemConfigs, error) { + obj := &systemConfigs{} + + return obj, nil + }); err != nil { + return err + } if err := container.Container.Provide(func() (*tenants, error) { obj := &tenants{} diff --git a/backend/app/http/super/v1/routes.gen.go b/backend/app/http/super/v1/routes.gen.go index 4c4d91d..40ab7f8 100644 --- a/backend/app/http/super/v1/routes.gen.go +++ b/backend/app/http/super/v1/routes.gen.go @@ -26,6 +26,7 @@ type Routes struct { middlewares *middlewares.Middlewares // Controller instances assets *assets + auditLogs *auditLogs contents *contents coupons *coupons creatorApplications *creatorApplications @@ -34,6 +35,7 @@ type Routes struct { orders *orders payoutAccounts *payoutAccounts reports *reports + systemConfigs *systemConfigs tenants *tenants users *users withdrawals *withdrawals @@ -71,6 +73,12 @@ func (r *Routes) Register(router fiber.Router) { r.assets.Usage, Query[dto.SuperAssetUsageFilter]("filter"), )) + // Register routes for controller: auditLogs + r.log.Debugf("Registering route: Get /super/v1/audit-logs -> auditLogs.List") + router.Get("/super/v1/audit-logs"[len(r.Path()):], DataFunc1( + r.auditLogs.List, + Query[dto.SuperAuditLogListFilter]("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( @@ -243,6 +251,25 @@ func (r *Routes) Register(router fiber.Router) { r.reports.Export, Body[dto.SuperReportExportForm]("form"), )) + // Register routes for controller: systemConfigs + r.log.Debugf("Registering route: Get /super/v1/system-configs -> systemConfigs.List") + router.Get("/super/v1/system-configs"[len(r.Path()):], DataFunc1( + r.systemConfigs.List, + Query[dto.SuperSystemConfigListFilter]("filter"), + )) + r.log.Debugf("Registering route: Patch /super/v1/system-configs/:id -> systemConfigs.Update") + router.Patch("/super/v1/system-configs/:id"[len(r.Path()):], DataFunc3( + r.systemConfigs.Update, + Local[*models.User]("__ctx_user"), + PathParam[int64]("id"), + Body[dto.SuperSystemConfigUpdateForm]("form"), + )) + r.log.Debugf("Registering route: Post /super/v1/system-configs -> systemConfigs.Create") + router.Post("/super/v1/system-configs"[len(r.Path()):], DataFunc2( + r.systemConfigs.Create, + Local[*models.User]("__ctx_user"), + Body[dto.SuperSystemConfigCreateForm]("form"), + )) // Register routes for controller: tenants r.log.Debugf("Registering route: Get /super/v1/tenant-join-requests -> tenants.ListJoinRequests") router.Get("/super/v1/tenant-join-requests"[len(r.Path()):], DataFunc1( diff --git a/backend/app/http/super/v1/system_configs.go b/backend/app/http/super/v1/system_configs.go new file mode 100644 index 0000000..8672d16 --- /dev/null +++ b/backend/app/http/super/v1/system_configs.go @@ -0,0 +1,63 @@ +package v1 + +import ( + dto "quyun/v2/app/http/super/v1/dto" + "quyun/v2/app/requests" + "quyun/v2/app/services" + "quyun/v2/database/models" + + "github.com/gofiber/fiber/v3" +) + +// @provider +type systemConfigs struct{} + +// List system configs +// +// @Router /super/v1/system-configs [get] +// @Summary List system configs +// @Description List platform system configs +// @Tags SystemConfig +// @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.SuperSystemConfigItem} +// @Bind filter query +func (c *systemConfigs) List(ctx fiber.Ctx, filter *dto.SuperSystemConfigListFilter) (*requests.Pager, error) { + return services.Super.ListSystemConfigs(ctx, filter) +} + +// Create system config +// +// @Router /super/v1/system-configs [post] +// @Summary Create system config +// @Description Create platform system config +// @Tags SystemConfig +// @Accept json +// @Produce json +// @Param form body dto.SuperSystemConfigCreateForm true "Create form" +// @Success 200 {object} dto.SuperSystemConfigItem +// @Bind user local key(__ctx_user) +// @Bind form body +func (c *systemConfigs) Create(ctx fiber.Ctx, user *models.User, form *dto.SuperSystemConfigCreateForm) (*dto.SuperSystemConfigItem, error) { + return services.Super.CreateSystemConfig(ctx, user.ID, form) +} + +// Update system config +// +// @Router /super/v1/system-configs/:id [patch] +// @Summary Update system config +// @Description Update platform system config +// @Tags SystemConfig +// @Accept json +// @Produce json +// @Param id path int64 true "Config ID" +// @Param form body dto.SuperSystemConfigUpdateForm true "Update form" +// @Success 200 {object} dto.SuperSystemConfigItem +// @Bind user local key(__ctx_user) +// @Bind id path +// @Bind form body +func (c *systemConfigs) Update(ctx fiber.Ctx, user *models.User, id int64, form *dto.SuperSystemConfigUpdateForm) (*dto.SuperSystemConfigItem, error) { + return services.Super.UpdateSystemConfig(ctx, user.ID, id, form) +} diff --git a/backend/app/services/audit.go b/backend/app/services/audit.go index 026966b..2590827 100644 --- a/backend/app/services/audit.go +++ b/backend/app/services/audit.go @@ -3,18 +3,38 @@ package services import ( "context" + "quyun/v2/database/models" + "github.com/sirupsen/logrus" ) // @provider type audit struct{} -func (s *audit) Log(ctx context.Context, operatorID int64, action, targetID, detail string) { +func (s *audit) Log(ctx context.Context, tenantID, operatorID int64, action, targetID, detail string) { logrus.WithFields(logrus.Fields{ "audit": true, + "tenant": tenantID, "operator": operatorID, "action": action, "target": targetID, "detail": detail, }).Info("Audit Log") + + entry := &models.AuditLog{ + TenantID: tenantID, + OperatorID: operatorID, + Action: action, + TargetID: targetID, + Detail: detail, + } + if err := models.AuditLogQuery.WithContext(ctx).Create(entry); err != nil { + logrus.WithFields(logrus.Fields{ + "audit": true, + "tenant": tenantID, + "operator": operatorID, + "action": action, + "target": targetID, + }).WithError(err).Warn("Audit log persist failed") + } } diff --git a/backend/app/services/super.go b/backend/app/services/super.go index 390c9b8..bb1e4dd 100644 --- a/backend/app/services/super.go +++ b/backend/app/services/super.go @@ -1019,7 +1019,7 @@ func (s *super) ReviewCreatorApplication(ctx context.Context, operatorID, tenant if strings.TrimSpace(form.Reason) != "" { detail += ",原因:" + strings.TrimSpace(form.Reason) } - Audit.Log(ctx, operatorID, "review_creator_application", cast.ToString(tenant.ID), detail) + Audit.Log(ctx, tenant.ID, operatorID, "review_creator_application", cast.ToString(tenant.ID), detail) } return nil @@ -1317,7 +1317,7 @@ func (s *super) RemovePayoutAccount(ctx context.Context, operatorID, id int64) e } if Audit != nil { - Audit.Log(ctx, operatorID, "remove_payout_account", cast.ToString(account.ID), "Removed payout account") + Audit.Log(ctx, account.TenantID, operatorID, "remove_payout_account", cast.ToString(account.ID), "Removed payout account") } return nil } @@ -2376,7 +2376,7 @@ func (s *super) ReviewContent(ctx context.Context, operatorID, contentID int64, _ = Notification.Send(ctx, content.TenantID, content.UserID, string(consts.NotificationTypeAudit), title, detail) } if Audit != nil { - Audit.Log(ctx, operatorID, "review_content", cast.ToString(contentID), detail) + Audit.Log(ctx, content.TenantID, operatorID, "review_content", cast.ToString(contentID), detail) } return nil } @@ -2465,7 +2465,7 @@ func (s *super) BatchReviewContents(ctx context.Context, operatorID int64, form _ = Notification.Send(ctx, content.TenantID, content.UserID, string(consts.NotificationTypeAudit), title, detail) } if Audit != nil { - Audit.Log(ctx, operatorID, "review_content", cast.ToString(content.ID), detail) + Audit.Log(ctx, content.TenantID, operatorID, "review_content", cast.ToString(content.ID), detail) } } @@ -3318,6 +3318,431 @@ func (s *super) CreateNotificationTemplate(ctx context.Context, form *super_dto. return item, nil } +func (s *super) ListAuditLogs(ctx context.Context, filter *super_dto.SuperAuditLogListFilter) (*requests.Pager, error) { + if filter == nil { + filter = &super_dto.SuperAuditLogListFilter{} + } + + tbl, q := models.AuditLogQuery.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.OperatorID != nil && *filter.OperatorID > 0 { + q = q.Where(tbl.OperatorID.Eq(*filter.OperatorID)) + } + if filter.Action != nil && strings.TrimSpace(*filter.Action) != "" { + q = q.Where(tbl.Action.Eq(strings.TrimSpace(*filter.Action))) + } + if filter.TargetID != nil && strings.TrimSpace(*filter.TargetID) != "" { + q = q.Where(tbl.TargetID.Eq(strings.TrimSpace(*filter.TargetID))) + } + if filter.Keyword != nil && strings.TrimSpace(*filter.Keyword) != "" { + keyword := "%" + strings.TrimSpace(*filter.Keyword) + "%" + q = q.Where(field.Or(tbl.Detail.Like(keyword), tbl.Action.Like(keyword), tbl.TargetID.Like(keyword))) + } + + // 跨租户筛选:根据租户编码/名称定位租户ID。 + 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...)) + } + } + + // 根据操作者昵称/用户名定位用户ID。 + operatorIDs, operatorFilter, err := s.lookupUserIDs(ctx, filter.OperatorName) + if err != nil { + return nil, err + } + if operatorFilter { + if len(operatorIDs) == 0 { + q = q.Where(tbl.ID.Eq(-1)) + } else { + q = q.Where(tbl.OperatorID.In(operatorIDs...)) + } + } + + 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.SuperAuditLogItem{}, + }, nil + } + + tenantSet := make(map[int64]struct{}) + operatorSet := make(map[int64]struct{}) + for _, log := range list { + if log.TenantID > 0 { + tenantSet[log.TenantID] = struct{}{} + } + if log.OperatorID > 0 { + operatorSet[log.OperatorID] = 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 + } + } + + operatorMap := make(map[int64]*models.User, len(operatorSet)) + if len(operatorSet) > 0 { + ids := make([]int64, 0, len(operatorSet)) + for id := range operatorSet { + 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 { + operatorMap[user.ID] = user + } + } + + items := make([]super_dto.SuperAuditLogItem, 0, len(list)) + for _, log := range list { + item := super_dto.SuperAuditLogItem{ + ID: log.ID, + TenantID: log.TenantID, + OperatorID: log.OperatorID, + Action: log.Action, + TargetID: log.TargetID, + Detail: log.Detail, + CreatedAt: s.formatTime(log.CreatedAt), + } + if tenant := tenantMap[log.TenantID]; tenant != nil { + item.TenantCode = tenant.Code + item.TenantName = tenant.Name + } + if operator := operatorMap[log.OperatorID]; operator != nil { + item.OperatorName = operator.Username + } else if log.OperatorID > 0 { + item.OperatorName = "ID:" + strconv.FormatInt(log.OperatorID, 10) + } + items = append(items, item) + } + + return &requests.Pager{ + Pagination: filter.Pagination, + Total: total, + Items: items, + }, nil +} + +func (s *super) ListSystemConfigs(ctx context.Context, filter *super_dto.SuperSystemConfigListFilter) (*requests.Pager, error) { + if filter == nil { + filter = &super_dto.SuperSystemConfigListFilter{} + } + + tbl, q := models.SystemConfigQuery.QueryContext(ctx) + + if filter.ConfigKey != nil && strings.TrimSpace(*filter.ConfigKey) != "" { + q = q.Where(tbl.ConfigKey.Eq(strings.TrimSpace(*filter.ConfigKey))) + } + if filter.Keyword != nil && strings.TrimSpace(*filter.Keyword) != "" { + keyword := "%" + strings.TrimSpace(*filter.Keyword) + "%" + q = q.Where(field.Or(tbl.ConfigKey.Like(keyword), tbl.Description.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)) + } + } + if filter.UpdatedAtFrom != nil { + from, err := s.parseFilterTime(filter.UpdatedAtFrom) + if err != nil { + return nil, err + } + if from != nil { + q = q.Where(tbl.UpdatedAt.Gte(*from)) + } + } + if filter.UpdatedAtTo != nil { + to, err := s.parseFilterTime(filter.UpdatedAtTo) + if err != nil { + return nil, err + } + if to != nil { + q = q.Where(tbl.UpdatedAt.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 "config_key": + q = q.Order(tbl.ConfigKey.Desc()) + case "updated_at": + q = q.Order(tbl.UpdatedAt.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 "config_key": + q = q.Order(tbl.ConfigKey) + case "updated_at": + q = q.Order(tbl.UpdatedAt) + case "created_at": + q = q.Order(tbl.CreatedAt) + } + orderApplied = true + } + if !orderApplied { + q = q.Order(tbl.UpdatedAt.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.SuperSystemConfigItem{}, + }, nil + } + + items := make([]super_dto.SuperSystemConfigItem, 0, len(list)) + for _, cfg := range list { + items = append(items, super_dto.SuperSystemConfigItem{ + ID: cfg.ID, + ConfigKey: cfg.ConfigKey, + Value: json.RawMessage(cfg.Value), + Description: cfg.Description, + CreatedAt: s.formatTime(cfg.CreatedAt), + UpdatedAt: s.formatTime(cfg.UpdatedAt), + }) + } + + return &requests.Pager{ + Pagination: filter.Pagination, + Total: total, + Items: items, + }, nil +} + +func (s *super) CreateSystemConfig(ctx context.Context, operatorID int64, form *super_dto.SuperSystemConfigCreateForm) (*super_dto.SuperSystemConfigItem, error) { + if operatorID == 0 { + return nil, errorx.ErrUnauthorized.WithMsg("缺少操作者信息") + } + if form == nil { + return nil, errorx.ErrBadRequest.WithMsg("配置参数不能为空") + } + + key := strings.TrimSpace(form.ConfigKey) + if key == "" { + return nil, errorx.ErrBadRequest.WithMsg("配置Key不能为空") + } + if len(form.Value) == 0 || !json.Valid(form.Value) { + return nil, errorx.ErrBadRequest.WithMsg("配置值必须是合法JSON") + } + desc := strings.TrimSpace(form.Description) + if desc == "" { + return nil, errorx.ErrBadRequest.WithMsg("配置说明不能为空") + } + + // 配置Key唯一,重复提交直接提示。 + tbl, q := models.SystemConfigQuery.QueryContext(ctx) + _, err := q.Where(tbl.ConfigKey.Eq(key)).First() + if err == nil { + return nil, errorx.ErrRecordDuplicated.WithMsg("配置Key已存在") + } + if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + + cfg := &models.SystemConfig{ + ConfigKey: key, + Value: types.JSON(form.Value), + Description: desc, + } + if err := q.Create(cfg); err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + + if Audit != nil { + Audit.Log(ctx, 0, operatorID, "create_system_config", cfg.ConfigKey, "创建系统配置") + } + + return &super_dto.SuperSystemConfigItem{ + ID: cfg.ID, + ConfigKey: cfg.ConfigKey, + Value: json.RawMessage(cfg.Value), + Description: cfg.Description, + CreatedAt: s.formatTime(cfg.CreatedAt), + UpdatedAt: s.formatTime(cfg.UpdatedAt), + }, nil +} + +func (s *super) UpdateSystemConfig(ctx context.Context, operatorID, id int64, form *super_dto.SuperSystemConfigUpdateForm) (*super_dto.SuperSystemConfigItem, error) { + if operatorID == 0 { + return nil, errorx.ErrUnauthorized.WithMsg("缺少操作者信息") + } + if id == 0 { + return nil, errorx.ErrBadRequest.WithMsg("配置ID不能为空") + } + if form == nil { + return nil, errorx.ErrBadRequest.WithMsg("配置参数不能为空") + } + + tbl, q := models.SystemConfigQuery.QueryContext(ctx) + cfg, err := q.Where(tbl.ID.Eq(id)).First() + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errorx.ErrRecordNotFound.WithMsg("配置不存在") + } + return nil, errorx.ErrDatabaseError.WithCause(err) + } + + updates := make(map[string]interface{}, 3) + if form.Value != nil { + if len(*form.Value) == 0 || !json.Valid(*form.Value) { + return nil, errorx.ErrBadRequest.WithMsg("配置值必须是合法JSON") + } + updates["value"] = types.JSON(*form.Value) + } + if form.Description != nil { + desc := strings.TrimSpace(*form.Description) + if desc == "" { + return nil, errorx.ErrBadRequest.WithMsg("配置说明不能为空") + } + updates["description"] = desc + } + if len(updates) == 0 { + return nil, errorx.ErrBadRequest.WithMsg("请至少更新一项配置") + } + updates["updated_at"] = time.Now() + + if _, err := q.Where(tbl.ID.Eq(id)).Updates(updates); err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + + cfg, err = q.Where(tbl.ID.Eq(id)).First() + if err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + + if Audit != nil { + details := make([]string, 0, 2) + if form.Value != nil { + details = append(details, "更新配置值") + } + if form.Description != nil { + details = append(details, "更新配置说明") + } + detail := strings.Join(details, ",") + Audit.Log(ctx, 0, operatorID, "update_system_config", cfg.ConfigKey, detail) + } + + return &super_dto.SuperSystemConfigItem{ + ID: cfg.ID, + ConfigKey: cfg.ConfigKey, + Value: json.RawMessage(cfg.Value), + Description: cfg.Description, + CreatedAt: s.formatTime(cfg.CreatedAt), + UpdatedAt: s.formatTime(cfg.UpdatedAt), + }, nil +} + func (s *super) ListOrders(ctx context.Context, filter *super_dto.SuperOrderListFilter) (*requests.Pager, error) { tbl, q := models.OrderQuery.QueryContext(ctx) @@ -5523,7 +5948,7 @@ func (s *super) UpdateCouponStatus(ctx context.Context, operatorID, couponID int } if Audit != nil { - Audit.Log(ctx, operatorID, "freeze_coupon", cast.ToString(coupon.ID), "Freeze coupon") + Audit.Log(ctx, coupon.TenantID, operatorID, "freeze_coupon", cast.ToString(coupon.ID), "Freeze coupon") } return nil } @@ -6077,7 +6502,7 @@ func (s *super) ApproveWithdrawal(ctx context.Context, operatorID, id int64) err UpdatedAt: time.Now(), }) if err == nil && Audit != nil { - Audit.Log(ctx, operatorID, "approve_withdrawal", cast.ToString(id), "Approved withdrawal") + Audit.Log(ctx, o.TenantID, operatorID, "approve_withdrawal", cast.ToString(id), "Approved withdrawal") } return err } @@ -6404,11 +6829,13 @@ func (s *super) RejectWithdrawal(ctx context.Context, operatorID, id int64, reas if operatorID == 0 { return errorx.ErrUnauthorized.WithMsg("缺少操作者信息") } + tenantID := int64(0) err := models.Q.Transaction(func(tx *models.Query) error { o, err := tx.Order.WithContext(ctx).Where(tx.Order.ID.Eq(id)).First() if err != nil { return errorx.ErrRecordNotFound } + tenantID = o.TenantID if o.Status != consts.OrderStatusCreated { return errorx.ErrStatusConflict.WithMsg("订单状态不正确") } @@ -6448,7 +6875,7 @@ func (s *super) RejectWithdrawal(ctx context.Context, operatorID, id int64, reas }) if err == nil && Audit != nil { - Audit.Log(ctx, operatorID, "reject_withdrawal", cast.ToString(id), "Rejected: "+reason) + Audit.Log(ctx, tenantID, operatorID, "reject_withdrawal", cast.ToString(id), "Rejected: "+reason) } return err } diff --git a/backend/database/migrations/20260115103830_create_audit_logs_and_system_configs.sql b/backend/database/migrations/20260115103830_create_audit_logs_and_system_configs.sql new file mode 100644 index 0000000..4dc95c4 --- /dev/null +++ b/backend/database/migrations/20260115103830_create_audit_logs_and_system_configs.sql @@ -0,0 +1,52 @@ +-- +goose Up +-- +goose StatementBegin +CREATE TABLE IF NOT EXISTS audit_logs ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL DEFAULT 0, + operator_id BIGINT NOT NULL, + action VARCHAR(64) NOT NULL, + target_id VARCHAR(64) NOT NULL DEFAULT '', + detail TEXT NOT NULL DEFAULT '', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +COMMENT ON TABLE audit_logs IS '审计日志:记录超管关键操作,用于追溯与合规审查。'; +COMMENT ON COLUMN audit_logs.id IS '主键ID。'; +COMMENT ON COLUMN audit_logs.tenant_id IS '租户ID;用途:关联租户审计(0表示平台级)。'; +COMMENT ON COLUMN audit_logs.operator_id IS '操作者用户ID;用途:审计追溯;约束:必须存在。'; +COMMENT ON COLUMN audit_logs.action IS '动作标识;用途:检索分类;约束:例如 review_content/freeze_coupon。'; +COMMENT ON COLUMN audit_logs.target_id IS '目标ID;用途:定位被操作对象;可为空字符串。'; +COMMENT ON COLUMN audit_logs.detail IS '动作详情;用途:记录操作原因与补充说明。'; +COMMENT ON COLUMN audit_logs.created_at IS '创建时间;用途:时间序列查询与审计留存。'; + +CREATE INDEX IF NOT EXISTS audit_logs_tenant_id_idx ON audit_logs(tenant_id); +CREATE INDEX IF NOT EXISTS audit_logs_operator_id_idx ON audit_logs(operator_id); +CREATE INDEX IF NOT EXISTS audit_logs_action_idx ON audit_logs(action); +CREATE INDEX IF NOT EXISTS audit_logs_created_at_idx ON audit_logs(created_at); + +CREATE TABLE IF NOT EXISTS system_configs ( + id BIGSERIAL PRIMARY KEY, + config_key VARCHAR(64) NOT NULL, + value JSONB NOT NULL DEFAULT '{}'::jsonb, + description VARCHAR(255) NOT NULL DEFAULT '', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +COMMENT ON TABLE system_configs IS '系统配置:平台级可配置项(JSON);用于功能开关与运营参数。'; +COMMENT ON COLUMN system_configs.id IS '主键ID。'; +COMMENT ON COLUMN system_configs.config_key IS '配置项Key;用途:按Key读取/更新;约束:唯一。'; +COMMENT ON COLUMN system_configs.value IS '配置值(JSON);用途:存储任意结构化配置;默认 {}。'; +COMMENT ON COLUMN system_configs.description IS '配置说明;用途:给运营/技术理解用途。'; +COMMENT ON COLUMN system_configs.created_at IS '创建时间;用途:审计与追溯。'; +COMMENT ON COLUMN system_configs.updated_at IS '更新时间;用途:变更记录。'; + +CREATE UNIQUE INDEX IF NOT EXISTS system_configs_config_key_uindex ON system_configs(config_key); +CREATE INDEX IF NOT EXISTS system_configs_updated_at_idx ON system_configs(updated_at); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS system_configs; +DROP TABLE IF EXISTS audit_logs; +-- +goose StatementEnd diff --git a/backend/database/models/audit_logs.gen.go b/backend/database/models/audit_logs.gen.go new file mode 100644 index 0000000..cec7fd0 --- /dev/null +++ b/backend/database/models/audit_logs.gen.go @@ -0,0 +1,57 @@ +// Code generated by go.ipao.vip/gen. DO NOT EDIT. +// Code generated by go.ipao.vip/gen. DO NOT EDIT. +// Code generated by go.ipao.vip/gen. DO NOT EDIT. + +package models + +import ( + "context" + "time" + + "go.ipao.vip/gen" +) + +const TableNameAuditLog = "audit_logs" + +// AuditLog mapped from table +type AuditLog struct { + ID int64 `gorm:"column:id;type:bigint;primaryKey;autoIncrement:true;comment:主键ID。" json:"id"` // 主键ID。 + TenantID int64 `gorm:"column:tenant_id;type:bigint;not null;comment:租户ID;用途:关联租户审计(0表示平台级)。" json:"tenant_id"` // 租户ID;用途:关联租户审计(0表示平台级)。 + OperatorID int64 `gorm:"column:operator_id;type:bigint;not null;comment:操作者用户ID;用途:审计追溯;约束:必须存在。" json:"operator_id"` // 操作者用户ID;用途:审计追溯;约束:必须存在。 + Action string `gorm:"column:action;type:character varying(64);not null;comment:动作标识;用途:检索分类;约束:例如 review_content/freeze_coupon。" json:"action"` // 动作标识;用途:检索分类;约束:例如 review_content/freeze_coupon。 + TargetID string `gorm:"column:target_id;type:character varying(64);not null;comment:目标ID;用途:定位被操作对象;可为空字符串。" json:"target_id"` // 目标ID;用途:定位被操作对象;可为空字符串。 + Detail string `gorm:"column:detail;type:text;not null;comment:动作详情;用途:记录操作原因与补充说明。" json:"detail"` // 动作详情;用途:记录操作原因与补充说明。 + CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;not null;default:now();comment:创建时间;用途:时间序列查询与审计留存。" json:"created_at"` // 创建时间;用途:时间序列查询与审计留存。 +} + +// Quick operations without importing query package +// Update applies changed fields to the database using the default DB. +func (m *AuditLog) Update(ctx context.Context) (gen.ResultInfo, error) { + return Q.AuditLog.WithContext(ctx).Updates(m) +} + +// Save upserts the model using the default DB. +func (m *AuditLog) Save(ctx context.Context) error { return Q.AuditLog.WithContext(ctx).Save(m) } + +// Create inserts the model using the default DB. +func (m *AuditLog) Create(ctx context.Context) error { return Q.AuditLog.WithContext(ctx).Create(m) } + +// Delete removes the row represented by the model using the default DB. +func (m *AuditLog) Delete(ctx context.Context) (gen.ResultInfo, error) { + return Q.AuditLog.WithContext(ctx).Delete(m) +} + +// ForceDelete permanently deletes the row (ignores soft delete) using the default DB. +func (m *AuditLog) ForceDelete(ctx context.Context) (gen.ResultInfo, error) { + return Q.AuditLog.WithContext(ctx).Unscoped().Delete(m) +} + +// Reload reloads the model from database by its primary key and overwrites current fields. +func (m *AuditLog) Reload(ctx context.Context) error { + fresh, err := Q.AuditLog.WithContext(ctx).GetByID(m.ID) + if err != nil { + return err + } + *m = *fresh + return nil +} diff --git a/backend/database/models/audit_logs.query.gen.go b/backend/database/models/audit_logs.query.gen.go new file mode 100644 index 0000000..c7a47a1 --- /dev/null +++ b/backend/database/models/audit_logs.query.gen.go @@ -0,0 +1,485 @@ +// Code generated by go.ipao.vip/gen. DO NOT EDIT. +// Code generated by go.ipao.vip/gen. DO NOT EDIT. +// Code generated by go.ipao.vip/gen. DO NOT EDIT. + +package models + +import ( + "context" + + "gorm.io/gorm" + "gorm.io/gorm/clause" + "gorm.io/gorm/schema" + + "go.ipao.vip/gen" + "go.ipao.vip/gen/field" + + "gorm.io/plugin/dbresolver" +) + +func newAuditLog(db *gorm.DB, opts ...gen.DOOption) auditLogQuery { + _auditLogQuery := auditLogQuery{} + + _auditLogQuery.auditLogQueryDo.UseDB(db, opts...) + _auditLogQuery.auditLogQueryDo.UseModel(&AuditLog{}) + + tableName := _auditLogQuery.auditLogQueryDo.TableName() + _auditLogQuery.ALL = field.NewAsterisk(tableName) + _auditLogQuery.ID = field.NewInt64(tableName, "id") + _auditLogQuery.TenantID = field.NewInt64(tableName, "tenant_id") + _auditLogQuery.OperatorID = field.NewInt64(tableName, "operator_id") + _auditLogQuery.Action = field.NewString(tableName, "action") + _auditLogQuery.TargetID = field.NewString(tableName, "target_id") + _auditLogQuery.Detail = field.NewString(tableName, "detail") + _auditLogQuery.CreatedAt = field.NewTime(tableName, "created_at") + + _auditLogQuery.fillFieldMap() + + return _auditLogQuery +} + +type auditLogQuery struct { + auditLogQueryDo auditLogQueryDo + + ALL field.Asterisk + ID field.Int64 // 主键ID。 + TenantID field.Int64 // 租户ID;用途:关联租户审计(0表示平台级)。 + OperatorID field.Int64 // 操作者用户ID;用途:审计追溯;约束:必须存在。 + Action field.String // 动作标识;用途:检索分类;约束:例如 review_content/freeze_coupon。 + TargetID field.String // 目标ID;用途:定位被操作对象;可为空字符串。 + Detail field.String // 动作详情;用途:记录操作原因与补充说明。 + CreatedAt field.Time // 创建时间;用途:时间序列查询与审计留存。 + + fieldMap map[string]field.Expr +} + +func (a auditLogQuery) Table(newTableName string) *auditLogQuery { + a.auditLogQueryDo.UseTable(newTableName) + return a.updateTableName(newTableName) +} + +func (a auditLogQuery) As(alias string) *auditLogQuery { + a.auditLogQueryDo.DO = *(a.auditLogQueryDo.As(alias).(*gen.DO)) + return a.updateTableName(alias) +} + +func (a *auditLogQuery) updateTableName(table string) *auditLogQuery { + a.ALL = field.NewAsterisk(table) + a.ID = field.NewInt64(table, "id") + a.TenantID = field.NewInt64(table, "tenant_id") + a.OperatorID = field.NewInt64(table, "operator_id") + a.Action = field.NewString(table, "action") + a.TargetID = field.NewString(table, "target_id") + a.Detail = field.NewString(table, "detail") + a.CreatedAt = field.NewTime(table, "created_at") + + a.fillFieldMap() + + return a +} + +func (a *auditLogQuery) QueryContext(ctx context.Context) (*auditLogQuery, *auditLogQueryDo) { + return a, a.auditLogQueryDo.WithContext(ctx) +} + +func (a *auditLogQuery) WithContext(ctx context.Context) *auditLogQueryDo { + return a.auditLogQueryDo.WithContext(ctx) +} + +func (a auditLogQuery) TableName() string { return a.auditLogQueryDo.TableName() } + +func (a auditLogQuery) Alias() string { return a.auditLogQueryDo.Alias() } + +func (a auditLogQuery) Columns(cols ...field.Expr) gen.Columns { + return a.auditLogQueryDo.Columns(cols...) +} + +func (a *auditLogQuery) GetFieldByName(fieldName string) (field.OrderExpr, bool) { + _f, ok := a.fieldMap[fieldName] + if !ok || _f == nil { + return nil, false + } + _oe, ok := _f.(field.OrderExpr) + return _oe, ok +} + +func (a *auditLogQuery) fillFieldMap() { + a.fieldMap = make(map[string]field.Expr, 7) + a.fieldMap["id"] = a.ID + a.fieldMap["tenant_id"] = a.TenantID + a.fieldMap["operator_id"] = a.OperatorID + a.fieldMap["action"] = a.Action + a.fieldMap["target_id"] = a.TargetID + a.fieldMap["detail"] = a.Detail + a.fieldMap["created_at"] = a.CreatedAt +} + +func (a auditLogQuery) clone(db *gorm.DB) auditLogQuery { + a.auditLogQueryDo.ReplaceConnPool(db.Statement.ConnPool) + return a +} + +func (a auditLogQuery) replaceDB(db *gorm.DB) auditLogQuery { + a.auditLogQueryDo.ReplaceDB(db) + return a +} + +type auditLogQueryDo struct{ gen.DO } + +func (a auditLogQueryDo) Debug() *auditLogQueryDo { + return a.withDO(a.DO.Debug()) +} + +func (a auditLogQueryDo) WithContext(ctx context.Context) *auditLogQueryDo { + return a.withDO(a.DO.WithContext(ctx)) +} + +func (a auditLogQueryDo) ReadDB() *auditLogQueryDo { + return a.Clauses(dbresolver.Read) +} + +func (a auditLogQueryDo) WriteDB() *auditLogQueryDo { + return a.Clauses(dbresolver.Write) +} + +func (a auditLogQueryDo) Session(config *gorm.Session) *auditLogQueryDo { + return a.withDO(a.DO.Session(config)) +} + +func (a auditLogQueryDo) Clauses(conds ...clause.Expression) *auditLogQueryDo { + return a.withDO(a.DO.Clauses(conds...)) +} + +func (a auditLogQueryDo) Returning(value interface{}, columns ...string) *auditLogQueryDo { + return a.withDO(a.DO.Returning(value, columns...)) +} + +func (a auditLogQueryDo) Not(conds ...gen.Condition) *auditLogQueryDo { + return a.withDO(a.DO.Not(conds...)) +} + +func (a auditLogQueryDo) Or(conds ...gen.Condition) *auditLogQueryDo { + return a.withDO(a.DO.Or(conds...)) +} + +func (a auditLogQueryDo) Select(conds ...field.Expr) *auditLogQueryDo { + return a.withDO(a.DO.Select(conds...)) +} + +func (a auditLogQueryDo) Where(conds ...gen.Condition) *auditLogQueryDo { + return a.withDO(a.DO.Where(conds...)) +} + +func (a auditLogQueryDo) Order(conds ...field.Expr) *auditLogQueryDo { + return a.withDO(a.DO.Order(conds...)) +} + +func (a auditLogQueryDo) Distinct(cols ...field.Expr) *auditLogQueryDo { + return a.withDO(a.DO.Distinct(cols...)) +} + +func (a auditLogQueryDo) Omit(cols ...field.Expr) *auditLogQueryDo { + return a.withDO(a.DO.Omit(cols...)) +} + +func (a auditLogQueryDo) Join(table schema.Tabler, on ...field.Expr) *auditLogQueryDo { + return a.withDO(a.DO.Join(table, on...)) +} + +func (a auditLogQueryDo) LeftJoin(table schema.Tabler, on ...field.Expr) *auditLogQueryDo { + return a.withDO(a.DO.LeftJoin(table, on...)) +} + +func (a auditLogQueryDo) RightJoin(table schema.Tabler, on ...field.Expr) *auditLogQueryDo { + return a.withDO(a.DO.RightJoin(table, on...)) +} + +func (a auditLogQueryDo) Group(cols ...field.Expr) *auditLogQueryDo { + return a.withDO(a.DO.Group(cols...)) +} + +func (a auditLogQueryDo) Having(conds ...gen.Condition) *auditLogQueryDo { + return a.withDO(a.DO.Having(conds...)) +} + +func (a auditLogQueryDo) Limit(limit int) *auditLogQueryDo { + return a.withDO(a.DO.Limit(limit)) +} + +func (a auditLogQueryDo) Offset(offset int) *auditLogQueryDo { + return a.withDO(a.DO.Offset(offset)) +} + +func (a auditLogQueryDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *auditLogQueryDo { + return a.withDO(a.DO.Scopes(funcs...)) +} + +func (a auditLogQueryDo) Unscoped() *auditLogQueryDo { + return a.withDO(a.DO.Unscoped()) +} + +func (a auditLogQueryDo) Create(values ...*AuditLog) error { + if len(values) == 0 { + return nil + } + return a.DO.Create(values) +} + +func (a auditLogQueryDo) CreateInBatches(values []*AuditLog, batchSize int) error { + return a.DO.CreateInBatches(values, batchSize) +} + +// Save : !!! underlying implementation is different with GORM +// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values) +func (a auditLogQueryDo) Save(values ...*AuditLog) error { + if len(values) == 0 { + return nil + } + return a.DO.Save(values) +} + +func (a auditLogQueryDo) First() (*AuditLog, error) { + if result, err := a.DO.First(); err != nil { + return nil, err + } else { + return result.(*AuditLog), nil + } +} + +func (a auditLogQueryDo) Take() (*AuditLog, error) { + if result, err := a.DO.Take(); err != nil { + return nil, err + } else { + return result.(*AuditLog), nil + } +} + +func (a auditLogQueryDo) Last() (*AuditLog, error) { + if result, err := a.DO.Last(); err != nil { + return nil, err + } else { + return result.(*AuditLog), nil + } +} + +func (a auditLogQueryDo) Find() ([]*AuditLog, error) { + result, err := a.DO.Find() + return result.([]*AuditLog), err +} + +func (a auditLogQueryDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*AuditLog, err error) { + buf := make([]*AuditLog, 0, batchSize) + err = a.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error { + defer func() { results = append(results, buf...) }() + return fc(tx, batch) + }) + return results, err +} + +func (a auditLogQueryDo) FindInBatches(result *[]*AuditLog, batchSize int, fc func(tx gen.Dao, batch int) error) error { + return a.DO.FindInBatches(result, batchSize, fc) +} + +func (a auditLogQueryDo) Attrs(attrs ...field.AssignExpr) *auditLogQueryDo { + return a.withDO(a.DO.Attrs(attrs...)) +} + +func (a auditLogQueryDo) Assign(attrs ...field.AssignExpr) *auditLogQueryDo { + return a.withDO(a.DO.Assign(attrs...)) +} + +func (a auditLogQueryDo) Joins(fields ...field.RelationField) *auditLogQueryDo { + for _, _f := range fields { + a = *a.withDO(a.DO.Joins(_f)) + } + return &a +} + +func (a auditLogQueryDo) Preload(fields ...field.RelationField) *auditLogQueryDo { + for _, _f := range fields { + a = *a.withDO(a.DO.Preload(_f)) + } + return &a +} + +func (a auditLogQueryDo) FirstOrInit() (*AuditLog, error) { + if result, err := a.DO.FirstOrInit(); err != nil { + return nil, err + } else { + return result.(*AuditLog), nil + } +} + +func (a auditLogQueryDo) FirstOrCreate() (*AuditLog, error) { + if result, err := a.DO.FirstOrCreate(); err != nil { + return nil, err + } else { + return result.(*AuditLog), nil + } +} + +func (a auditLogQueryDo) FindByPage(offset int, limit int) (result []*AuditLog, count int64, err error) { + result, err = a.Offset(offset).Limit(limit).Find() + if err != nil { + return + } + + if size := len(result); 0 < limit && 0 < size && size < limit { + count = int64(size + offset) + return + } + + count, err = a.Offset(-1).Limit(-1).Count() + return +} + +func (a auditLogQueryDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) { + count, err = a.Count() + if err != nil { + return + } + + err = a.Offset(offset).Limit(limit).Scan(result) + return +} + +func (a auditLogQueryDo) Scan(result interface{}) (err error) { + return a.DO.Scan(result) +} + +func (a auditLogQueryDo) Delete(models ...*AuditLog) (result gen.ResultInfo, err error) { + return a.DO.Delete(models) +} + +// ForceDelete performs a permanent delete (ignores soft-delete) for current scope. +func (a auditLogQueryDo) ForceDelete() (gen.ResultInfo, error) { + return a.Unscoped().Delete() +} + +// Inc increases the given column by step for current scope. +func (a auditLogQueryDo) Inc(column field.Expr, step int64) (gen.ResultInfo, error) { + // column = column + step + e := field.NewUnsafeFieldRaw("?+?", column.RawExpr(), step) + return a.DO.UpdateColumn(column, e) +} + +// Dec decreases the given column by step for current scope. +func (a auditLogQueryDo) Dec(column field.Expr, step int64) (gen.ResultInfo, error) { + // column = column - step + e := field.NewUnsafeFieldRaw("?-?", column.RawExpr(), step) + return a.DO.UpdateColumn(column, e) +} + +// Sum returns SUM(column) for current scope. +func (a auditLogQueryDo) Sum(column field.Expr) (float64, error) { + var _v float64 + agg := field.NewUnsafeFieldRaw("SUM(?)", column.RawExpr()) + if err := a.Select(agg).Scan(&_v); err != nil { + return 0, err + } + return _v, nil +} + +// Avg returns AVG(column) for current scope. +func (a auditLogQueryDo) Avg(column field.Expr) (float64, error) { + var _v float64 + agg := field.NewUnsafeFieldRaw("AVG(?)", column.RawExpr()) + if err := a.Select(agg).Scan(&_v); err != nil { + return 0, err + } + return _v, nil +} + +// Min returns MIN(column) for current scope. +func (a auditLogQueryDo) Min(column field.Expr) (float64, error) { + var _v float64 + agg := field.NewUnsafeFieldRaw("MIN(?)", column.RawExpr()) + if err := a.Select(agg).Scan(&_v); err != nil { + return 0, err + } + return _v, nil +} + +// Max returns MAX(column) for current scope. +func (a auditLogQueryDo) Max(column field.Expr) (float64, error) { + var _v float64 + agg := field.NewUnsafeFieldRaw("MAX(?)", column.RawExpr()) + if err := a.Select(agg).Scan(&_v); err != nil { + return 0, err + } + return _v, nil +} + +// PluckMap returns a map[key]value for selected key/value expressions within current scope. +func (a auditLogQueryDo) PluckMap(key, val field.Expr) (map[interface{}]interface{}, error) { + do := a.Select(key, val) + rows, err := do.DO.Rows() + if err != nil { + return nil, err + } + defer rows.Close() + mm := make(map[interface{}]interface{}) + for rows.Next() { + var k interface{} + var v interface{} + if err := rows.Scan(&k, &v); err != nil { + return nil, err + } + mm[k] = v + } + return mm, rows.Err() +} + +// Exists returns true if any record matches the given conditions. +func (a auditLogQueryDo) Exists(conds ...gen.Condition) (bool, error) { + cnt, err := a.Where(conds...).Count() + if err != nil { + return false, err + } + return cnt > 0, nil +} + +// PluckIDs returns all primary key values under current scope. +func (a auditLogQueryDo) PluckIDs() ([]int64, error) { + ids := make([]int64, 0, 16) + pk := field.NewInt64(a.TableName(), "id") + if err := a.DO.Pluck(pk, &ids); err != nil { + return nil, err + } + return ids, nil +} + +// GetByID finds a single record by primary key. +func (a auditLogQueryDo) GetByID(id int64) (*AuditLog, error) { + pk := field.NewInt64(a.TableName(), "id") + return a.Where(pk.Eq(id)).First() +} + +// GetByIDs finds records by primary key list. +func (a auditLogQueryDo) GetByIDs(ids ...int64) ([]*AuditLog, error) { + if len(ids) == 0 { + return []*AuditLog{}, nil + } + pk := field.NewInt64(a.TableName(), "id") + return a.Where(pk.In(ids...)).Find() +} + +// DeleteByID deletes records by primary key. +func (a auditLogQueryDo) DeleteByID(id int64) (gen.ResultInfo, error) { + pk := field.NewInt64(a.TableName(), "id") + return a.Where(pk.Eq(id)).Delete() +} + +// DeleteByIDs deletes records by a list of primary keys. +func (a auditLogQueryDo) DeleteByIDs(ids ...int64) (gen.ResultInfo, error) { + if len(ids) == 0 { + return gen.ResultInfo{RowsAffected: 0, Error: nil}, nil + } + pk := field.NewInt64(a.TableName(), "id") + return a.Where(pk.In(ids...)).Delete() +} + +func (a *auditLogQueryDo) withDO(do gen.Dao) *auditLogQueryDo { + a.DO = *do.(*gen.DO) + return a +} diff --git a/backend/database/models/contents.gen.go b/backend/database/models/contents.gen.go index fc251f6..096082b 100644 --- a/backend/database/models/contents.gen.go +++ b/backend/database/models/contents.gen.go @@ -40,9 +40,9 @@ type Content struct { DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamp with time zone" json:"deleted_at"` Key string `gorm:"column:key;type:character varying(32);comment:Musical key/tone" json:"key"` // Musical key/tone IsPinned bool `gorm:"column:is_pinned;type:boolean;comment:Whether content is pinned/featured" json:"is_pinned"` // Whether content is pinned/featured - Author *User `gorm:"foreignKey:UserID;references:ID" json:"author,omitempty"` ContentAssets []*ContentAsset `gorm:"foreignKey:ContentID;references:ID" json:"content_assets,omitempty"` Comments []*Comment `gorm:"foreignKey:ContentID;references:ID" json:"comments,omitempty"` + Author *User `gorm:"foreignKey:UserID;references:ID" json:"author,omitempty"` } // Quick operations without importing query package diff --git a/backend/database/models/contents.query.gen.go b/backend/database/models/contents.query.gen.go index d462865..b670e6b 100644 --- a/backend/database/models/contents.query.gen.go +++ b/backend/database/models/contents.query.gen.go @@ -46,12 +46,6 @@ func newContent(db *gorm.DB, opts ...gen.DOOption) contentQuery { _contentQuery.DeletedAt = field.NewField(tableName, "deleted_at") _contentQuery.Key = field.NewString(tableName, "key") _contentQuery.IsPinned = field.NewBool(tableName, "is_pinned") - _contentQuery.Author = contentQueryBelongsToAuthor{ - db: db.Session(&gorm.Session{}), - - RelationField: field.NewRelation("Author", "User"), - } - _contentQuery.ContentAssets = contentQueryHasManyContentAssets{ db: db.Session(&gorm.Session{}), @@ -64,6 +58,12 @@ func newContent(db *gorm.DB, opts ...gen.DOOption) contentQuery { RelationField: field.NewRelation("Comments", "Comment"), } + _contentQuery.Author = contentQueryBelongsToAuthor{ + db: db.Session(&gorm.Session{}), + + RelationField: field.NewRelation("Author", "User"), + } + _contentQuery.fillFieldMap() return _contentQuery @@ -94,12 +94,12 @@ type contentQuery struct { DeletedAt field.Field Key field.String // Musical key/tone IsPinned field.Bool // Whether content is pinned/featured - Author contentQueryBelongsToAuthor - - ContentAssets contentQueryHasManyContentAssets + ContentAssets contentQueryHasManyContentAssets Comments contentQueryHasManyComments + Author contentQueryBelongsToAuthor + fieldMap map[string]field.Expr } @@ -195,104 +195,23 @@ func (c *contentQuery) fillFieldMap() { func (c contentQuery) clone(db *gorm.DB) contentQuery { c.contentQueryDo.ReplaceConnPool(db.Statement.ConnPool) - c.Author.db = db.Session(&gorm.Session{Initialized: true}) - c.Author.db.Statement.ConnPool = db.Statement.ConnPool c.ContentAssets.db = db.Session(&gorm.Session{Initialized: true}) c.ContentAssets.db.Statement.ConnPool = db.Statement.ConnPool c.Comments.db = db.Session(&gorm.Session{Initialized: true}) c.Comments.db.Statement.ConnPool = db.Statement.ConnPool + c.Author.db = db.Session(&gorm.Session{Initialized: true}) + c.Author.db.Statement.ConnPool = db.Statement.ConnPool return c } func (c contentQuery) replaceDB(db *gorm.DB) contentQuery { c.contentQueryDo.ReplaceDB(db) - c.Author.db = db.Session(&gorm.Session{}) c.ContentAssets.db = db.Session(&gorm.Session{}) c.Comments.db = db.Session(&gorm.Session{}) + c.Author.db = db.Session(&gorm.Session{}) return c } -type contentQueryBelongsToAuthor struct { - db *gorm.DB - - field.RelationField -} - -func (a contentQueryBelongsToAuthor) Where(conds ...field.Expr) *contentQueryBelongsToAuthor { - if len(conds) == 0 { - return &a - } - - exprs := make([]clause.Expression, 0, len(conds)) - for _, cond := range conds { - exprs = append(exprs, cond.BeCond().(clause.Expression)) - } - a.db = a.db.Clauses(clause.Where{Exprs: exprs}) - return &a -} - -func (a contentQueryBelongsToAuthor) WithContext(ctx context.Context) *contentQueryBelongsToAuthor { - a.db = a.db.WithContext(ctx) - return &a -} - -func (a contentQueryBelongsToAuthor) Session(session *gorm.Session) *contentQueryBelongsToAuthor { - a.db = a.db.Session(session) - return &a -} - -func (a contentQueryBelongsToAuthor) Model(m *Content) *contentQueryBelongsToAuthorTx { - return &contentQueryBelongsToAuthorTx{a.db.Model(m).Association(a.Name())} -} - -func (a contentQueryBelongsToAuthor) Unscoped() *contentQueryBelongsToAuthor { - a.db = a.db.Unscoped() - return &a -} - -type contentQueryBelongsToAuthorTx struct{ tx *gorm.Association } - -func (a contentQueryBelongsToAuthorTx) Find() (result *User, err error) { - return result, a.tx.Find(&result) -} - -func (a contentQueryBelongsToAuthorTx) Append(values ...*User) (err error) { - targetValues := make([]interface{}, len(values)) - for i, v := range values { - targetValues[i] = v - } - return a.tx.Append(targetValues...) -} - -func (a contentQueryBelongsToAuthorTx) Replace(values ...*User) (err error) { - targetValues := make([]interface{}, len(values)) - for i, v := range values { - targetValues[i] = v - } - return a.tx.Replace(targetValues...) -} - -func (a contentQueryBelongsToAuthorTx) Delete(values ...*User) (err error) { - targetValues := make([]interface{}, len(values)) - for i, v := range values { - targetValues[i] = v - } - return a.tx.Delete(targetValues...) -} - -func (a contentQueryBelongsToAuthorTx) Clear() error { - return a.tx.Clear() -} - -func (a contentQueryBelongsToAuthorTx) Count() int64 { - return a.tx.Count() -} - -func (a contentQueryBelongsToAuthorTx) Unscoped() *contentQueryBelongsToAuthorTx { - a.tx = a.tx.Unscoped() - return &a -} - type contentQueryHasManyContentAssets struct { db *gorm.DB @@ -455,6 +374,87 @@ func (a contentQueryHasManyCommentsTx) Unscoped() *contentQueryHasManyCommentsTx return &a } +type contentQueryBelongsToAuthor struct { + db *gorm.DB + + field.RelationField +} + +func (a contentQueryBelongsToAuthor) Where(conds ...field.Expr) *contentQueryBelongsToAuthor { + if len(conds) == 0 { + return &a + } + + exprs := make([]clause.Expression, 0, len(conds)) + for _, cond := range conds { + exprs = append(exprs, cond.BeCond().(clause.Expression)) + } + a.db = a.db.Clauses(clause.Where{Exprs: exprs}) + return &a +} + +func (a contentQueryBelongsToAuthor) WithContext(ctx context.Context) *contentQueryBelongsToAuthor { + a.db = a.db.WithContext(ctx) + return &a +} + +func (a contentQueryBelongsToAuthor) Session(session *gorm.Session) *contentQueryBelongsToAuthor { + a.db = a.db.Session(session) + return &a +} + +func (a contentQueryBelongsToAuthor) Model(m *Content) *contentQueryBelongsToAuthorTx { + return &contentQueryBelongsToAuthorTx{a.db.Model(m).Association(a.Name())} +} + +func (a contentQueryBelongsToAuthor) Unscoped() *contentQueryBelongsToAuthor { + a.db = a.db.Unscoped() + return &a +} + +type contentQueryBelongsToAuthorTx struct{ tx *gorm.Association } + +func (a contentQueryBelongsToAuthorTx) Find() (result *User, err error) { + return result, a.tx.Find(&result) +} + +func (a contentQueryBelongsToAuthorTx) Append(values ...*User) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Append(targetValues...) +} + +func (a contentQueryBelongsToAuthorTx) Replace(values ...*User) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Replace(targetValues...) +} + +func (a contentQueryBelongsToAuthorTx) Delete(values ...*User) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Delete(targetValues...) +} + +func (a contentQueryBelongsToAuthorTx) Clear() error { + return a.tx.Clear() +} + +func (a contentQueryBelongsToAuthorTx) Count() int64 { + return a.tx.Count() +} + +func (a contentQueryBelongsToAuthorTx) Unscoped() *contentQueryBelongsToAuthorTx { + a.tx = a.tx.Unscoped() + return &a +} + type contentQueryDo struct{ gen.DO } func (c contentQueryDo) Debug() *contentQueryDo { diff --git a/backend/database/models/query.gen.go b/backend/database/models/query.gen.go index 75d5203..3e45346 100644 --- a/backend/database/models/query.gen.go +++ b/backend/database/models/query.gen.go @@ -17,6 +17,7 @@ import ( var ( Q = new(Query) + AuditLogQuery *auditLogQuery CommentQuery *commentQuery ContentQuery *contentQuery ContentAccessQuery *contentAccessQuery @@ -29,6 +30,7 @@ var ( OrderQuery *orderQuery OrderItemQuery *orderItemQuery PayoutAccountQuery *payoutAccountQuery + SystemConfigQuery *systemConfigQuery TenantQuery *tenantQuery TenantInviteQuery *tenantInviteQuery TenantJoinRequestQuery *tenantJoinRequestQuery @@ -42,6 +44,7 @@ var ( func SetDefault(db *gorm.DB, opts ...gen.DOOption) { *Q = *Use(db, opts...) + AuditLogQuery = &Q.AuditLog CommentQuery = &Q.Comment ContentQuery = &Q.Content ContentAccessQuery = &Q.ContentAccess @@ -54,6 +57,7 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) { OrderQuery = &Q.Order OrderItemQuery = &Q.OrderItem PayoutAccountQuery = &Q.PayoutAccount + SystemConfigQuery = &Q.SystemConfig TenantQuery = &Q.Tenant TenantInviteQuery = &Q.TenantInvite TenantJoinRequestQuery = &Q.TenantJoinRequest @@ -68,6 +72,7 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) { func Use(db *gorm.DB, opts ...gen.DOOption) *Query { return &Query{ db: db, + AuditLog: newAuditLog(db, opts...), Comment: newComment(db, opts...), Content: newContent(db, opts...), ContentAccess: newContentAccess(db, opts...), @@ -80,6 +85,7 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query { Order: newOrder(db, opts...), OrderItem: newOrderItem(db, opts...), PayoutAccount: newPayoutAccount(db, opts...), + SystemConfig: newSystemConfig(db, opts...), Tenant: newTenant(db, opts...), TenantInvite: newTenantInvite(db, opts...), TenantJoinRequest: newTenantJoinRequest(db, opts...), @@ -95,6 +101,7 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query { type Query struct { db *gorm.DB + AuditLog auditLogQuery Comment commentQuery Content contentQuery ContentAccess contentAccessQuery @@ -107,6 +114,7 @@ type Query struct { Order orderQuery OrderItem orderItemQuery PayoutAccount payoutAccountQuery + SystemConfig systemConfigQuery Tenant tenantQuery TenantInvite tenantInviteQuery TenantJoinRequest tenantJoinRequestQuery @@ -123,6 +131,7 @@ func (q *Query) Available() bool { return q.db != nil } func (q *Query) clone(db *gorm.DB) *Query { return &Query{ db: db, + AuditLog: q.AuditLog.clone(db), Comment: q.Comment.clone(db), Content: q.Content.clone(db), ContentAccess: q.ContentAccess.clone(db), @@ -135,6 +144,7 @@ func (q *Query) clone(db *gorm.DB) *Query { Order: q.Order.clone(db), OrderItem: q.OrderItem.clone(db), PayoutAccount: q.PayoutAccount.clone(db), + SystemConfig: q.SystemConfig.clone(db), Tenant: q.Tenant.clone(db), TenantInvite: q.TenantInvite.clone(db), TenantJoinRequest: q.TenantJoinRequest.clone(db), @@ -158,6 +168,7 @@ func (q *Query) WriteDB() *Query { func (q *Query) ReplaceDB(db *gorm.DB) *Query { return &Query{ db: db, + AuditLog: q.AuditLog.replaceDB(db), Comment: q.Comment.replaceDB(db), Content: q.Content.replaceDB(db), ContentAccess: q.ContentAccess.replaceDB(db), @@ -170,6 +181,7 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query { Order: q.Order.replaceDB(db), OrderItem: q.OrderItem.replaceDB(db), PayoutAccount: q.PayoutAccount.replaceDB(db), + SystemConfig: q.SystemConfig.replaceDB(db), Tenant: q.Tenant.replaceDB(db), TenantInvite: q.TenantInvite.replaceDB(db), TenantJoinRequest: q.TenantJoinRequest.replaceDB(db), @@ -183,6 +195,7 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query { } type queryCtx struct { + AuditLog *auditLogQueryDo Comment *commentQueryDo Content *contentQueryDo ContentAccess *contentAccessQueryDo @@ -195,6 +208,7 @@ type queryCtx struct { Order *orderQueryDo OrderItem *orderItemQueryDo PayoutAccount *payoutAccountQueryDo + SystemConfig *systemConfigQueryDo Tenant *tenantQueryDo TenantInvite *tenantInviteQueryDo TenantJoinRequest *tenantJoinRequestQueryDo @@ -208,6 +222,7 @@ type queryCtx struct { func (q *Query) WithContext(ctx context.Context) *queryCtx { return &queryCtx{ + AuditLog: q.AuditLog.WithContext(ctx), Comment: q.Comment.WithContext(ctx), Content: q.Content.WithContext(ctx), ContentAccess: q.ContentAccess.WithContext(ctx), @@ -220,6 +235,7 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx { Order: q.Order.WithContext(ctx), OrderItem: q.OrderItem.WithContext(ctx), PayoutAccount: q.PayoutAccount.WithContext(ctx), + SystemConfig: q.SystemConfig.WithContext(ctx), Tenant: q.Tenant.WithContext(ctx), TenantInvite: q.TenantInvite.WithContext(ctx), TenantJoinRequest: q.TenantJoinRequest.WithContext(ctx), diff --git a/backend/database/models/system_configs.gen.go b/backend/database/models/system_configs.gen.go new file mode 100644 index 0000000..6cff55b --- /dev/null +++ b/backend/database/models/system_configs.gen.go @@ -0,0 +1,61 @@ +// Code generated by go.ipao.vip/gen. DO NOT EDIT. +// Code generated by go.ipao.vip/gen. DO NOT EDIT. +// Code generated by go.ipao.vip/gen. DO NOT EDIT. + +package models + +import ( + "context" + "time" + + "go.ipao.vip/gen" + "go.ipao.vip/gen/types" +) + +const TableNameSystemConfig = "system_configs" + +// SystemConfig mapped from table +type SystemConfig struct { + ID int64 `gorm:"column:id;type:bigint;primaryKey;autoIncrement:true;comment:主键ID。" json:"id"` // 主键ID。 + ConfigKey string `gorm:"column:config_key;type:character varying(64);not null;comment:配置项Key;用途:按Key读取/更新;约束:唯一。" json:"config_key"` // 配置项Key;用途:按Key读取/更新;约束:唯一。 + Value types.JSON `gorm:"column:value;type:jsonb;not null;default:{};comment:配置值(JSON);用途:存储任意结构化配置;默认 {}。" json:"value"` // 配置值(JSON);用途:存储任意结构化配置;默认 {}。 + Description string `gorm:"column:description;type:character varying(255);not null;comment:配置说明;用途:给运营/技术理解用途。" json:"description"` // 配置说明;用途:给运营/技术理解用途。 + CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;not null;default:now();comment:创建时间;用途:审计与追溯。" json:"created_at"` // 创建时间;用途:审计与追溯。 + UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp with time zone;not null;default:now();comment:更新时间;用途:变更记录。" json:"updated_at"` // 更新时间;用途:变更记录。 +} + +// Quick operations without importing query package +// Update applies changed fields to the database using the default DB. +func (m *SystemConfig) Update(ctx context.Context) (gen.ResultInfo, error) { + return Q.SystemConfig.WithContext(ctx).Updates(m) +} + +// Save upserts the model using the default DB. +func (m *SystemConfig) Save(ctx context.Context) error { + return Q.SystemConfig.WithContext(ctx).Save(m) +} + +// Create inserts the model using the default DB. +func (m *SystemConfig) Create(ctx context.Context) error { + return Q.SystemConfig.WithContext(ctx).Create(m) +} + +// Delete removes the row represented by the model using the default DB. +func (m *SystemConfig) Delete(ctx context.Context) (gen.ResultInfo, error) { + return Q.SystemConfig.WithContext(ctx).Delete(m) +} + +// ForceDelete permanently deletes the row (ignores soft delete) using the default DB. +func (m *SystemConfig) ForceDelete(ctx context.Context) (gen.ResultInfo, error) { + return Q.SystemConfig.WithContext(ctx).Unscoped().Delete(m) +} + +// Reload reloads the model from database by its primary key and overwrites current fields. +func (m *SystemConfig) Reload(ctx context.Context) error { + fresh, err := Q.SystemConfig.WithContext(ctx).GetByID(m.ID) + if err != nil { + return err + } + *m = *fresh + return nil +} diff --git a/backend/database/models/system_configs.query.gen.go b/backend/database/models/system_configs.query.gen.go new file mode 100644 index 0000000..4dd811c --- /dev/null +++ b/backend/database/models/system_configs.query.gen.go @@ -0,0 +1,481 @@ +// Code generated by go.ipao.vip/gen. DO NOT EDIT. +// Code generated by go.ipao.vip/gen. DO NOT EDIT. +// Code generated by go.ipao.vip/gen. DO NOT EDIT. + +package models + +import ( + "context" + + "gorm.io/gorm" + "gorm.io/gorm/clause" + "gorm.io/gorm/schema" + + "go.ipao.vip/gen" + "go.ipao.vip/gen/field" + + "gorm.io/plugin/dbresolver" +) + +func newSystemConfig(db *gorm.DB, opts ...gen.DOOption) systemConfigQuery { + _systemConfigQuery := systemConfigQuery{} + + _systemConfigQuery.systemConfigQueryDo.UseDB(db, opts...) + _systemConfigQuery.systemConfigQueryDo.UseModel(&SystemConfig{}) + + tableName := _systemConfigQuery.systemConfigQueryDo.TableName() + _systemConfigQuery.ALL = field.NewAsterisk(tableName) + _systemConfigQuery.ID = field.NewInt64(tableName, "id") + _systemConfigQuery.ConfigKey = field.NewString(tableName, "config_key") + _systemConfigQuery.Value = field.NewJSONB(tableName, "value") + _systemConfigQuery.Description = field.NewString(tableName, "description") + _systemConfigQuery.CreatedAt = field.NewTime(tableName, "created_at") + _systemConfigQuery.UpdatedAt = field.NewTime(tableName, "updated_at") + + _systemConfigQuery.fillFieldMap() + + return _systemConfigQuery +} + +type systemConfigQuery struct { + systemConfigQueryDo systemConfigQueryDo + + ALL field.Asterisk + ID field.Int64 // 主键ID。 + ConfigKey field.String // 配置项Key;用途:按Key读取/更新;约束:唯一。 + Value field.JSONB // 配置值(JSON);用途:存储任意结构化配置;默认 {}。 + Description field.String // 配置说明;用途:给运营/技术理解用途。 + CreatedAt field.Time // 创建时间;用途:审计与追溯。 + UpdatedAt field.Time // 更新时间;用途:变更记录。 + + fieldMap map[string]field.Expr +} + +func (s systemConfigQuery) Table(newTableName string) *systemConfigQuery { + s.systemConfigQueryDo.UseTable(newTableName) + return s.updateTableName(newTableName) +} + +func (s systemConfigQuery) As(alias string) *systemConfigQuery { + s.systemConfigQueryDo.DO = *(s.systemConfigQueryDo.As(alias).(*gen.DO)) + return s.updateTableName(alias) +} + +func (s *systemConfigQuery) updateTableName(table string) *systemConfigQuery { + s.ALL = field.NewAsterisk(table) + s.ID = field.NewInt64(table, "id") + s.ConfigKey = field.NewString(table, "config_key") + s.Value = field.NewJSONB(table, "value") + s.Description = field.NewString(table, "description") + s.CreatedAt = field.NewTime(table, "created_at") + s.UpdatedAt = field.NewTime(table, "updated_at") + + s.fillFieldMap() + + return s +} + +func (s *systemConfigQuery) QueryContext(ctx context.Context) (*systemConfigQuery, *systemConfigQueryDo) { + return s, s.systemConfigQueryDo.WithContext(ctx) +} + +func (s *systemConfigQuery) WithContext(ctx context.Context) *systemConfigQueryDo { + return s.systemConfigQueryDo.WithContext(ctx) +} + +func (s systemConfigQuery) TableName() string { return s.systemConfigQueryDo.TableName() } + +func (s systemConfigQuery) Alias() string { return s.systemConfigQueryDo.Alias() } + +func (s systemConfigQuery) Columns(cols ...field.Expr) gen.Columns { + return s.systemConfigQueryDo.Columns(cols...) +} + +func (s *systemConfigQuery) GetFieldByName(fieldName string) (field.OrderExpr, bool) { + _f, ok := s.fieldMap[fieldName] + if !ok || _f == nil { + return nil, false + } + _oe, ok := _f.(field.OrderExpr) + return _oe, ok +} + +func (s *systemConfigQuery) fillFieldMap() { + s.fieldMap = make(map[string]field.Expr, 6) + s.fieldMap["id"] = s.ID + s.fieldMap["config_key"] = s.ConfigKey + s.fieldMap["value"] = s.Value + s.fieldMap["description"] = s.Description + s.fieldMap["created_at"] = s.CreatedAt + s.fieldMap["updated_at"] = s.UpdatedAt +} + +func (s systemConfigQuery) clone(db *gorm.DB) systemConfigQuery { + s.systemConfigQueryDo.ReplaceConnPool(db.Statement.ConnPool) + return s +} + +func (s systemConfigQuery) replaceDB(db *gorm.DB) systemConfigQuery { + s.systemConfigQueryDo.ReplaceDB(db) + return s +} + +type systemConfigQueryDo struct{ gen.DO } + +func (s systemConfigQueryDo) Debug() *systemConfigQueryDo { + return s.withDO(s.DO.Debug()) +} + +func (s systemConfigQueryDo) WithContext(ctx context.Context) *systemConfigQueryDo { + return s.withDO(s.DO.WithContext(ctx)) +} + +func (s systemConfigQueryDo) ReadDB() *systemConfigQueryDo { + return s.Clauses(dbresolver.Read) +} + +func (s systemConfigQueryDo) WriteDB() *systemConfigQueryDo { + return s.Clauses(dbresolver.Write) +} + +func (s systemConfigQueryDo) Session(config *gorm.Session) *systemConfigQueryDo { + return s.withDO(s.DO.Session(config)) +} + +func (s systemConfigQueryDo) Clauses(conds ...clause.Expression) *systemConfigQueryDo { + return s.withDO(s.DO.Clauses(conds...)) +} + +func (s systemConfigQueryDo) Returning(value interface{}, columns ...string) *systemConfigQueryDo { + return s.withDO(s.DO.Returning(value, columns...)) +} + +func (s systemConfigQueryDo) Not(conds ...gen.Condition) *systemConfigQueryDo { + return s.withDO(s.DO.Not(conds...)) +} + +func (s systemConfigQueryDo) Or(conds ...gen.Condition) *systemConfigQueryDo { + return s.withDO(s.DO.Or(conds...)) +} + +func (s systemConfigQueryDo) Select(conds ...field.Expr) *systemConfigQueryDo { + return s.withDO(s.DO.Select(conds...)) +} + +func (s systemConfigQueryDo) Where(conds ...gen.Condition) *systemConfigQueryDo { + return s.withDO(s.DO.Where(conds...)) +} + +func (s systemConfigQueryDo) Order(conds ...field.Expr) *systemConfigQueryDo { + return s.withDO(s.DO.Order(conds...)) +} + +func (s systemConfigQueryDo) Distinct(cols ...field.Expr) *systemConfigQueryDo { + return s.withDO(s.DO.Distinct(cols...)) +} + +func (s systemConfigQueryDo) Omit(cols ...field.Expr) *systemConfigQueryDo { + return s.withDO(s.DO.Omit(cols...)) +} + +func (s systemConfigQueryDo) Join(table schema.Tabler, on ...field.Expr) *systemConfigQueryDo { + return s.withDO(s.DO.Join(table, on...)) +} + +func (s systemConfigQueryDo) LeftJoin(table schema.Tabler, on ...field.Expr) *systemConfigQueryDo { + return s.withDO(s.DO.LeftJoin(table, on...)) +} + +func (s systemConfigQueryDo) RightJoin(table schema.Tabler, on ...field.Expr) *systemConfigQueryDo { + return s.withDO(s.DO.RightJoin(table, on...)) +} + +func (s systemConfigQueryDo) Group(cols ...field.Expr) *systemConfigQueryDo { + return s.withDO(s.DO.Group(cols...)) +} + +func (s systemConfigQueryDo) Having(conds ...gen.Condition) *systemConfigQueryDo { + return s.withDO(s.DO.Having(conds...)) +} + +func (s systemConfigQueryDo) Limit(limit int) *systemConfigQueryDo { + return s.withDO(s.DO.Limit(limit)) +} + +func (s systemConfigQueryDo) Offset(offset int) *systemConfigQueryDo { + return s.withDO(s.DO.Offset(offset)) +} + +func (s systemConfigQueryDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *systemConfigQueryDo { + return s.withDO(s.DO.Scopes(funcs...)) +} + +func (s systemConfigQueryDo) Unscoped() *systemConfigQueryDo { + return s.withDO(s.DO.Unscoped()) +} + +func (s systemConfigQueryDo) Create(values ...*SystemConfig) error { + if len(values) == 0 { + return nil + } + return s.DO.Create(values) +} + +func (s systemConfigQueryDo) CreateInBatches(values []*SystemConfig, batchSize int) error { + return s.DO.CreateInBatches(values, batchSize) +} + +// Save : !!! underlying implementation is different with GORM +// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values) +func (s systemConfigQueryDo) Save(values ...*SystemConfig) error { + if len(values) == 0 { + return nil + } + return s.DO.Save(values) +} + +func (s systemConfigQueryDo) First() (*SystemConfig, error) { + if result, err := s.DO.First(); err != nil { + return nil, err + } else { + return result.(*SystemConfig), nil + } +} + +func (s systemConfigQueryDo) Take() (*SystemConfig, error) { + if result, err := s.DO.Take(); err != nil { + return nil, err + } else { + return result.(*SystemConfig), nil + } +} + +func (s systemConfigQueryDo) Last() (*SystemConfig, error) { + if result, err := s.DO.Last(); err != nil { + return nil, err + } else { + return result.(*SystemConfig), nil + } +} + +func (s systemConfigQueryDo) Find() ([]*SystemConfig, error) { + result, err := s.DO.Find() + return result.([]*SystemConfig), err +} + +func (s systemConfigQueryDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*SystemConfig, err error) { + buf := make([]*SystemConfig, 0, batchSize) + err = s.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error { + defer func() { results = append(results, buf...) }() + return fc(tx, batch) + }) + return results, err +} + +func (s systemConfigQueryDo) FindInBatches(result *[]*SystemConfig, batchSize int, fc func(tx gen.Dao, batch int) error) error { + return s.DO.FindInBatches(result, batchSize, fc) +} + +func (s systemConfigQueryDo) Attrs(attrs ...field.AssignExpr) *systemConfigQueryDo { + return s.withDO(s.DO.Attrs(attrs...)) +} + +func (s systemConfigQueryDo) Assign(attrs ...field.AssignExpr) *systemConfigQueryDo { + return s.withDO(s.DO.Assign(attrs...)) +} + +func (s systemConfigQueryDo) Joins(fields ...field.RelationField) *systemConfigQueryDo { + for _, _f := range fields { + s = *s.withDO(s.DO.Joins(_f)) + } + return &s +} + +func (s systemConfigQueryDo) Preload(fields ...field.RelationField) *systemConfigQueryDo { + for _, _f := range fields { + s = *s.withDO(s.DO.Preload(_f)) + } + return &s +} + +func (s systemConfigQueryDo) FirstOrInit() (*SystemConfig, error) { + if result, err := s.DO.FirstOrInit(); err != nil { + return nil, err + } else { + return result.(*SystemConfig), nil + } +} + +func (s systemConfigQueryDo) FirstOrCreate() (*SystemConfig, error) { + if result, err := s.DO.FirstOrCreate(); err != nil { + return nil, err + } else { + return result.(*SystemConfig), nil + } +} + +func (s systemConfigQueryDo) FindByPage(offset int, limit int) (result []*SystemConfig, count int64, err error) { + result, err = s.Offset(offset).Limit(limit).Find() + if err != nil { + return + } + + if size := len(result); 0 < limit && 0 < size && size < limit { + count = int64(size + offset) + return + } + + count, err = s.Offset(-1).Limit(-1).Count() + return +} + +func (s systemConfigQueryDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) { + count, err = s.Count() + if err != nil { + return + } + + err = s.Offset(offset).Limit(limit).Scan(result) + return +} + +func (s systemConfigQueryDo) Scan(result interface{}) (err error) { + return s.DO.Scan(result) +} + +func (s systemConfigQueryDo) Delete(models ...*SystemConfig) (result gen.ResultInfo, err error) { + return s.DO.Delete(models) +} + +// ForceDelete performs a permanent delete (ignores soft-delete) for current scope. +func (s systemConfigQueryDo) ForceDelete() (gen.ResultInfo, error) { + return s.Unscoped().Delete() +} + +// Inc increases the given column by step for current scope. +func (s systemConfigQueryDo) Inc(column field.Expr, step int64) (gen.ResultInfo, error) { + // column = column + step + e := field.NewUnsafeFieldRaw("?+?", column.RawExpr(), step) + return s.DO.UpdateColumn(column, e) +} + +// Dec decreases the given column by step for current scope. +func (s systemConfigQueryDo) Dec(column field.Expr, step int64) (gen.ResultInfo, error) { + // column = column - step + e := field.NewUnsafeFieldRaw("?-?", column.RawExpr(), step) + return s.DO.UpdateColumn(column, e) +} + +// Sum returns SUM(column) for current scope. +func (s systemConfigQueryDo) Sum(column field.Expr) (float64, error) { + var _v float64 + agg := field.NewUnsafeFieldRaw("SUM(?)", column.RawExpr()) + if err := s.Select(agg).Scan(&_v); err != nil { + return 0, err + } + return _v, nil +} + +// Avg returns AVG(column) for current scope. +func (s systemConfigQueryDo) Avg(column field.Expr) (float64, error) { + var _v float64 + agg := field.NewUnsafeFieldRaw("AVG(?)", column.RawExpr()) + if err := s.Select(agg).Scan(&_v); err != nil { + return 0, err + } + return _v, nil +} + +// Min returns MIN(column) for current scope. +func (s systemConfigQueryDo) Min(column field.Expr) (float64, error) { + var _v float64 + agg := field.NewUnsafeFieldRaw("MIN(?)", column.RawExpr()) + if err := s.Select(agg).Scan(&_v); err != nil { + return 0, err + } + return _v, nil +} + +// Max returns MAX(column) for current scope. +func (s systemConfigQueryDo) Max(column field.Expr) (float64, error) { + var _v float64 + agg := field.NewUnsafeFieldRaw("MAX(?)", column.RawExpr()) + if err := s.Select(agg).Scan(&_v); err != nil { + return 0, err + } + return _v, nil +} + +// PluckMap returns a map[key]value for selected key/value expressions within current scope. +func (s systemConfigQueryDo) PluckMap(key, val field.Expr) (map[interface{}]interface{}, error) { + do := s.Select(key, val) + rows, err := do.DO.Rows() + if err != nil { + return nil, err + } + defer rows.Close() + mm := make(map[interface{}]interface{}) + for rows.Next() { + var k interface{} + var v interface{} + if err := rows.Scan(&k, &v); err != nil { + return nil, err + } + mm[k] = v + } + return mm, rows.Err() +} + +// Exists returns true if any record matches the given conditions. +func (s systemConfigQueryDo) Exists(conds ...gen.Condition) (bool, error) { + cnt, err := s.Where(conds...).Count() + if err != nil { + return false, err + } + return cnt > 0, nil +} + +// PluckIDs returns all primary key values under current scope. +func (s systemConfigQueryDo) PluckIDs() ([]int64, error) { + ids := make([]int64, 0, 16) + pk := field.NewInt64(s.TableName(), "id") + if err := s.DO.Pluck(pk, &ids); err != nil { + return nil, err + } + return ids, nil +} + +// GetByID finds a single record by primary key. +func (s systemConfigQueryDo) GetByID(id int64) (*SystemConfig, error) { + pk := field.NewInt64(s.TableName(), "id") + return s.Where(pk.Eq(id)).First() +} + +// GetByIDs finds records by primary key list. +func (s systemConfigQueryDo) GetByIDs(ids ...int64) ([]*SystemConfig, error) { + if len(ids) == 0 { + return []*SystemConfig{}, nil + } + pk := field.NewInt64(s.TableName(), "id") + return s.Where(pk.In(ids...)).Find() +} + +// DeleteByID deletes records by primary key. +func (s systemConfigQueryDo) DeleteByID(id int64) (gen.ResultInfo, error) { + pk := field.NewInt64(s.TableName(), "id") + return s.Where(pk.Eq(id)).Delete() +} + +// DeleteByIDs deletes records by a list of primary keys. +func (s systemConfigQueryDo) DeleteByIDs(ids ...int64) (gen.ResultInfo, error) { + if len(ids) == 0 { + return gen.ResultInfo{RowsAffected: 0, Error: nil}, nil + } + pk := field.NewInt64(s.TableName(), "id") + return s.Where(pk.In(ids...)).Delete() +} + +func (s *systemConfigQueryDo) withDO(do gen.Dao) *systemConfigQueryDo { + s.DO = *do.(*gen.DO) + return s +} diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 3ff103e..db22f72 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -138,6 +138,58 @@ const docTemplate = `{ } } }, + "/super/v1/audit-logs": { + "get": { + "description": "List audit logs across tenants", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Audit" + ], + "summary": "List audit logs", + "parameters": [ + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/requests.Pager" + }, + { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.SuperAuditLogItem" + } + } + } + } + ] + } + } + } + } + }, "/super/v1/auth/login": { "post": { "description": "Login", @@ -1138,6 +1190,132 @@ const docTemplate = `{ } } }, + "/super/v1/system-configs": { + "get": { + "description": "List platform system configs", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SystemConfig" + ], + "summary": "List system configs", + "parameters": [ + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/requests.Pager" + }, + { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.SuperSystemConfigItem" + } + } + } + } + ] + } + } + } + }, + "post": { + "description": "Create platform system config", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SystemConfig" + ], + "summary": "Create system config", + "parameters": [ + { + "description": "Create form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SuperSystemConfigCreateForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.SuperSystemConfigItem" + } + } + } + } + }, + "/super/v1/system-configs/{id}": { + "patch": { + "description": "Update platform system config", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SystemConfig" + ], + "summary": "Update system config", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Config ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Update form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SuperSystemConfigUpdateForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.SuperSystemConfigItem" + } + } + } + } + }, "/super/v1/tenant-join-requests": { "get": { "description": "List tenant join requests across tenants", @@ -7212,6 +7390,51 @@ const docTemplate = `{ } } }, + "dto.SuperAuditLogItem": { + "type": "object", + "properties": { + "action": { + "description": "Action 动作标识。", + "type": "string" + }, + "created_at": { + "description": "CreatedAt 创建时间(RFC3339)。", + "type": "string" + }, + "detail": { + "description": "Detail 操作详情。", + "type": "string" + }, + "id": { + "description": "ID 审计日志ID。", + "type": "integer" + }, + "operator_id": { + "description": "OperatorID 操作者用户ID。", + "type": "integer" + }, + "operator_name": { + "description": "OperatorName 操作者用户名/昵称。", + "type": "string" + }, + "target_id": { + "description": "TargetID 目标ID。", + "type": "string" + }, + "tenant_code": { + "description": "TenantCode 租户编码。", + "type": "string" + }, + "tenant_id": { + "description": "TenantID 租户ID。", + "type": "integer" + }, + "tenant_name": { + "description": "TenantName 租户名称。", + "type": "string" + } + } + }, "dto.SuperContentBatchReviewForm": { "type": "object", "required": [ @@ -7992,6 +8215,74 @@ const docTemplate = `{ } } }, + "dto.SuperSystemConfigCreateForm": { + "type": "object", + "properties": { + "config_key": { + "description": "ConfigKey 配置项Key(唯一)。", + "type": "string" + }, + "description": { + "description": "Description 配置说明。", + "type": "string" + }, + "value": { + "description": "Value 配置值(JSON)。", + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "dto.SuperSystemConfigItem": { + "type": "object", + "properties": { + "config_key": { + "description": "ConfigKey 配置项Key。", + "type": "string" + }, + "created_at": { + "description": "CreatedAt 创建时间(RFC3339)。", + "type": "string" + }, + "description": { + "description": "Description 配置说明。", + "type": "string" + }, + "id": { + "description": "ID 配置ID。", + "type": "integer" + }, + "updated_at": { + "description": "UpdatedAt 更新时间(RFC3339)。", + "type": "string" + }, + "value": { + "description": "Value 配置值(JSON)。", + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "dto.SuperSystemConfigUpdateForm": { + "type": "object", + "properties": { + "description": { + "description": "Description 配置说明(可选)。", + "type": "string" + }, + "value": { + "description": "Value 配置值(JSON,可选)。", + "type": "array", + "items": { + "type": "integer" + } + } + } + }, "dto.SuperTenantContentStatusUpdateForm": { "type": "object", "required": [ diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index 999e60c..c7f9c7c 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -132,6 +132,58 @@ } } }, + "/super/v1/audit-logs": { + "get": { + "description": "List audit logs across tenants", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Audit" + ], + "summary": "List audit logs", + "parameters": [ + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/requests.Pager" + }, + { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.SuperAuditLogItem" + } + } + } + } + ] + } + } + } + } + }, "/super/v1/auth/login": { "post": { "description": "Login", @@ -1132,6 +1184,132 @@ } } }, + "/super/v1/system-configs": { + "get": { + "description": "List platform system configs", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SystemConfig" + ], + "summary": "List system configs", + "parameters": [ + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/requests.Pager" + }, + { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.SuperSystemConfigItem" + } + } + } + } + ] + } + } + } + }, + "post": { + "description": "Create platform system config", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SystemConfig" + ], + "summary": "Create system config", + "parameters": [ + { + "description": "Create form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SuperSystemConfigCreateForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.SuperSystemConfigItem" + } + } + } + } + }, + "/super/v1/system-configs/{id}": { + "patch": { + "description": "Update platform system config", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SystemConfig" + ], + "summary": "Update system config", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Config ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Update form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SuperSystemConfigUpdateForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.SuperSystemConfigItem" + } + } + } + } + }, "/super/v1/tenant-join-requests": { "get": { "description": "List tenant join requests across tenants", @@ -7206,6 +7384,51 @@ } } }, + "dto.SuperAuditLogItem": { + "type": "object", + "properties": { + "action": { + "description": "Action 动作标识。", + "type": "string" + }, + "created_at": { + "description": "CreatedAt 创建时间(RFC3339)。", + "type": "string" + }, + "detail": { + "description": "Detail 操作详情。", + "type": "string" + }, + "id": { + "description": "ID 审计日志ID。", + "type": "integer" + }, + "operator_id": { + "description": "OperatorID 操作者用户ID。", + "type": "integer" + }, + "operator_name": { + "description": "OperatorName 操作者用户名/昵称。", + "type": "string" + }, + "target_id": { + "description": "TargetID 目标ID。", + "type": "string" + }, + "tenant_code": { + "description": "TenantCode 租户编码。", + "type": "string" + }, + "tenant_id": { + "description": "TenantID 租户ID。", + "type": "integer" + }, + "tenant_name": { + "description": "TenantName 租户名称。", + "type": "string" + } + } + }, "dto.SuperContentBatchReviewForm": { "type": "object", "required": [ @@ -7986,6 +8209,74 @@ } } }, + "dto.SuperSystemConfigCreateForm": { + "type": "object", + "properties": { + "config_key": { + "description": "ConfigKey 配置项Key(唯一)。", + "type": "string" + }, + "description": { + "description": "Description 配置说明。", + "type": "string" + }, + "value": { + "description": "Value 配置值(JSON)。", + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "dto.SuperSystemConfigItem": { + "type": "object", + "properties": { + "config_key": { + "description": "ConfigKey 配置项Key。", + "type": "string" + }, + "created_at": { + "description": "CreatedAt 创建时间(RFC3339)。", + "type": "string" + }, + "description": { + "description": "Description 配置说明。", + "type": "string" + }, + "id": { + "description": "ID 配置ID。", + "type": "integer" + }, + "updated_at": { + "description": "UpdatedAt 更新时间(RFC3339)。", + "type": "string" + }, + "value": { + "description": "Value 配置值(JSON)。", + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "dto.SuperSystemConfigUpdateForm": { + "type": "object", + "properties": { + "description": { + "description": "Description 配置说明(可选)。", + "type": "string" + }, + "value": { + "description": "Value 配置值(JSON,可选)。", + "type": "array", + "items": { + "type": "integer" + } + } + } + }, "dto.SuperTenantContentStatusUpdateForm": { "type": "object", "required": [ diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index 5638217..5c78411 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -1231,6 +1231,39 @@ definitions: description: TotalSize 资产总大小(字节)。 type: integer type: object + dto.SuperAuditLogItem: + properties: + action: + description: Action 动作标识。 + type: string + created_at: + description: CreatedAt 创建时间(RFC3339)。 + type: string + detail: + description: Detail 操作详情。 + type: string + id: + description: ID 审计日志ID。 + type: integer + operator_id: + description: OperatorID 操作者用户ID。 + type: integer + operator_name: + description: OperatorName 操作者用户名/昵称。 + type: string + target_id: + description: TargetID 目标ID。 + type: string + tenant_code: + description: TenantCode 租户编码。 + type: string + tenant_id: + description: TenantID 租户ID。 + type: integer + tenant_name: + description: TenantName 租户名称。 + type: string + type: object dto.SuperContentBatchReviewForm: properties: action: @@ -1766,6 +1799,54 @@ definitions: description: TenantID 租户ID(不传代表全平台)。 type: integer type: object + dto.SuperSystemConfigCreateForm: + properties: + config_key: + description: ConfigKey 配置项Key(唯一)。 + type: string + description: + description: Description 配置说明。 + type: string + value: + description: Value 配置值(JSON)。 + items: + type: integer + type: array + type: object + dto.SuperSystemConfigItem: + properties: + config_key: + description: ConfigKey 配置项Key。 + type: string + created_at: + description: CreatedAt 创建时间(RFC3339)。 + type: string + description: + description: Description 配置说明。 + type: string + id: + description: ID 配置ID。 + type: integer + updated_at: + description: UpdatedAt 更新时间(RFC3339)。 + type: string + value: + description: Value 配置值(JSON)。 + items: + type: integer + type: array + type: object + dto.SuperSystemConfigUpdateForm: + properties: + description: + description: Description 配置说明(可选)。 + type: string + value: + description: Value 配置值(JSON,可选)。 + items: + type: integer + type: array + type: object dto.SuperTenantContentStatusUpdateForm: properties: status: @@ -2936,6 +3017,37 @@ paths: summary: Asset usage tags: - Asset + /super/v1/audit-logs: + get: + consumes: + - application/json + description: List audit logs across tenants + parameters: + - description: Page number + in: query + name: page + type: integer + - description: Page size + in: query + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/requests.Pager' + - properties: + items: + items: + $ref: '#/definitions/dto.SuperAuditLogItem' + type: array + type: object + summary: List audit logs + tags: + - Audit /super/v1/auth/login: post: consumes: @@ -3562,6 +3674,86 @@ paths: summary: Report overview tags: - Report + /super/v1/system-configs: + get: + consumes: + - application/json + description: List platform system configs + parameters: + - description: Page number + in: query + name: page + type: integer + - description: Page size + in: query + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/requests.Pager' + - properties: + items: + items: + $ref: '#/definitions/dto.SuperSystemConfigItem' + type: array + type: object + summary: List system configs + tags: + - SystemConfig + post: + consumes: + - application/json + description: Create platform system config + parameters: + - description: Create form + in: body + name: form + required: true + schema: + $ref: '#/definitions/dto.SuperSystemConfigCreateForm' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.SuperSystemConfigItem' + summary: Create system config + tags: + - SystemConfig + /super/v1/system-configs/{id}: + patch: + consumes: + - application/json + description: Update platform system config + parameters: + - description: Config ID + format: int64 + in: path + name: id + required: true + type: integer + - description: Update form + in: body + name: form + required: true + schema: + $ref: '#/definitions/dto.SuperSystemConfigUpdateForm' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.SuperSystemConfigItem' + summary: Update system config + tags: + - SystemConfig /super/v1/tenant-join-requests: get: consumes: diff --git a/docs/superadmin_progress.md b/docs/superadmin_progress.md index 17a84ad..b545c87 100644 --- a/docs/superadmin_progress.md +++ b/docs/superadmin_progress.md @@ -4,9 +4,9 @@ ## 1) 总体结论 -- **已落地**:登录、租户/用户/订单/内容基础管理、内容审核(含批量)、平台概览(内容趋势/退款率/漏斗)、提现审核、报表概览与导出、用户钱包/通知/优惠券/实名/充值记录、互动(收藏/点赞/关注)与内容消费明细视图、创作者申请/成员审核/邀请、优惠券创建/编辑/发放/冻结/发放记录/异常核查、资产治理(列表/用量/清理)、通知中心(列表/群发/模板)。 +- **已落地**:登录、租户/用户/订单/内容基础管理、内容审核(含批量)、平台概览(内容趋势/退款率/漏斗)、提现审核、报表概览与导出、用户钱包/通知/优惠券/实名/充值记录、互动(收藏/点赞/关注)与内容消费明细视图、创作者申请/成员审核/邀请、优惠券创建/编辑/发放/冻结/发放记录/异常核查、资产治理(列表/用量/清理)、通知中心(列表/群发/模板)、审计日志与系统配置。 - **部分落地**:租户详情(缺财务/报表聚合)、内容治理(缺评论/举报)、创作者治理(缺提现审核联动与结算账户审批流)、财务(缺钱包流水/异常排查)。 -- **未落地**:审计与系统配置类能力。 +- **未落地**:暂无。 ## 2) 按页面完成度(对照 2.x) @@ -80,12 +80,17 @@ - 已有:通知列表、批量发送、模板管理。 - 缺口:无显著功能缺口。 +### 2.15 审计与系统配置 `/superadmin/audit-logs` `/superadmin/system-configs` +- 状态:**已完成** +- 已有:跨租户审计日志查询、系统配置列表/创建/更新。 +- 缺口:无显著功能缺口。 + ## 3) `/super/v1` 接口覆盖度概览 - **已具备**:Auth、Tenants(含成员审核/邀请)、Users(含钱包/通知/优惠券/实名/互动/内容消费)、Contents、Orders、Withdrawals、Reports、Coupons(列表/创建/编辑/发放/冻结/记录)、Creators(列表/申请/成员审核)、Payout Accounts(列表/删除)、Assets(列表/用量/删除)、Notifications(列表/群发/模板)。 -- **缺失/待补**:创作者提现审核、审计与系统配置类能力。 +- **缺失/待补**:创作者提现审核。 ## 4) 建议的下一步(按优先级) -1. **审计与系统配置**:完善全量操作审计与系统级配置能力。 -2. **创作者提现审核**:补齐跨租户提现审核与财务联动入口。 +1. **创作者提现审核**:补齐跨租户提现审核与财务联动入口。 +2. **内容/财务治理补齐**:评论/举报治理、钱包流水与异常排查能力。 diff --git a/frontend/superadmin/src/layout/AppMenu.vue b/frontend/superadmin/src/layout/AppMenu.vue index 394d1d4..3b2e9a4 100644 --- a/frontend/superadmin/src/layout/AppMenu.vue +++ b/frontend/superadmin/src/layout/AppMenu.vue @@ -20,7 +20,9 @@ const model = ref([ { label: 'Finance', icon: 'pi pi-fw pi-wallet', to: '/superadmin/finance' }, { label: 'Reports', icon: 'pi pi-fw pi-chart-line', to: '/superadmin/reports' }, { label: 'Assets', icon: 'pi pi-fw pi-folder', to: '/superadmin/assets' }, - { label: 'Notifications', icon: 'pi pi-fw pi-bell', to: '/superadmin/notifications' } + { label: 'Notifications', icon: 'pi pi-fw pi-bell', to: '/superadmin/notifications' }, + { label: 'Audit Logs', icon: 'pi pi-fw pi-shield', to: '/superadmin/audit-logs' }, + { label: 'System Configs', icon: 'pi pi-fw pi-cog', to: '/superadmin/system-configs' } ] } ]); diff --git a/frontend/superadmin/src/router/index.js b/frontend/superadmin/src/router/index.js index 03dd1ee..a23b262 100644 --- a/frontend/superadmin/src/router/index.js +++ b/frontend/superadmin/src/router/index.js @@ -174,6 +174,16 @@ const router = createRouter({ name: 'superadmin-notifications', component: () => import('@/views/superadmin/Notifications.vue') }, + { + path: '/superadmin/audit-logs', + name: 'superadmin-audit-logs', + component: () => import('@/views/superadmin/AuditLogs.vue') + }, + { + path: '/superadmin/system-configs', + name: 'superadmin-system-configs', + component: () => import('@/views/superadmin/SystemConfigs.vue') + }, { path: '/superadmin/orders/:orderID', name: 'superadmin-order-detail', diff --git a/frontend/superadmin/src/service/AuditService.js b/frontend/superadmin/src/service/AuditService.js new file mode 100644 index 0000000..203b239 --- /dev/null +++ b/frontend/superadmin/src/service/AuditService.js @@ -0,0 +1,46 @@ +import { requestJson } from './apiClient'; + +function normalizeItems(items) { + if (Array.isArray(items)) return items; + if (items && typeof items === 'object') return [items]; + return []; +} + +export const AuditService = { + async listAuditLogs({ page, limit, id, tenant_id, tenant_code, tenant_name, operator_id, operator_name, action, target_id, keyword, created_at_from, created_at_to, sortField, sortOrder } = {}) { + const iso = (d) => { + if (!d) return undefined; + const date = d instanceof Date ? d : new Date(d); + if (Number.isNaN(date.getTime())) return undefined; + return date.toISOString(); + }; + + const query = { + page, + limit, + id, + tenant_id, + tenant_code, + tenant_name, + operator_id, + operator_name, + action, + target_id, + keyword, + created_at_from: iso(created_at_from), + created_at_to: iso(created_at_to) + }; + if (sortField && sortOrder) { + if (sortOrder === 1) query.asc = sortField; + if (sortOrder === -1) query.desc = sortField; + } + + const data = await requestJson('/super/v1/audit-logs', { query }); + return { + page: data?.page ?? page ?? 1, + limit: data?.limit ?? limit ?? 10, + total: data?.total ?? 0, + items: normalizeItems(data?.items) + }; + } +}; diff --git a/frontend/superadmin/src/service/SystemConfigService.js b/frontend/superadmin/src/service/SystemConfigService.js new file mode 100644 index 0000000..e2f27a9 --- /dev/null +++ b/frontend/superadmin/src/service/SystemConfigService.js @@ -0,0 +1,60 @@ +import { requestJson } from './apiClient'; + +function normalizeItems(items) { + if (Array.isArray(items)) return items; + if (items && typeof items === 'object') return [items]; + return []; +} + +export const SystemConfigService = { + async listConfigs({ page, limit, config_key, keyword, created_at_from, created_at_to, updated_at_from, updated_at_to, sortField, sortOrder } = {}) { + const iso = (d) => { + if (!d) return undefined; + const date = d instanceof Date ? d : new Date(d); + if (Number.isNaN(date.getTime())) return undefined; + return date.toISOString(); + }; + + const query = { + page, + limit, + config_key, + keyword, + created_at_from: iso(created_at_from), + created_at_to: iso(created_at_to), + updated_at_from: iso(updated_at_from), + updated_at_to: iso(updated_at_to) + }; + if (sortField && sortOrder) { + if (sortOrder === 1) query.asc = sortField; + if (sortOrder === -1) query.desc = sortField; + } + + const data = await requestJson('/super/v1/system-configs', { query }); + return { + page: data?.page ?? page ?? 1, + limit: data?.limit ?? limit ?? 10, + total: data?.total ?? 0, + items: normalizeItems(data?.items) + }; + }, + async createConfig({ config_key, value, description } = {}) { + return requestJson('/super/v1/system-configs', { + method: 'POST', + body: { + config_key, + value, + description + } + }); + }, + async updateConfig(id, { value, description } = {}) { + return requestJson(`/super/v1/system-configs/${id}`, { + method: 'PATCH', + body: { + value, + description + } + }); + } +}; diff --git a/frontend/superadmin/src/views/superadmin/AuditLogs.vue b/frontend/superadmin/src/views/superadmin/AuditLogs.vue new file mode 100644 index 0000000..42a8e82 --- /dev/null +++ b/frontend/superadmin/src/views/superadmin/AuditLogs.vue @@ -0,0 +1,192 @@ + + + diff --git a/frontend/superadmin/src/views/superadmin/SystemConfigs.vue b/frontend/superadmin/src/views/superadmin/SystemConfigs.vue new file mode 100644 index 0000000..b8ae814 --- /dev/null +++ b/frontend/superadmin/src/views/superadmin/SystemConfigs.vue @@ -0,0 +1,276 @@ + + +