feat(announcements): add admin/user announcement system
Implements announcements end-to-end (admin CRUD + read status, user list + mark read) with OR-of-AND targeting. Also breaks the ent<->service import cycle by moving schema-facing constants/targeting into a new domain package.
This commit is contained in:
@@ -4,7 +4,7 @@ package schema
|
||||
|
||||
import (
|
||||
"github.com/Wei-Shaw/sub2api/ent/schema/mixins"
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
"github.com/Wei-Shaw/sub2api/internal/domain"
|
||||
|
||||
"entgo.io/ent"
|
||||
"entgo.io/ent/dialect"
|
||||
@@ -111,7 +111,7 @@ func (Account) Fields() []ent.Field {
|
||||
// status: 账户状态,如 "active", "error", "disabled"
|
||||
field.String("status").
|
||||
MaxLen(20).
|
||||
Default(service.StatusActive),
|
||||
Default(domain.StatusActive),
|
||||
|
||||
// error_message: 错误信息,记录账户异常时的详细信息
|
||||
field.String("error_message").
|
||||
|
||||
91
backend/ent/schema/announcement.go
Normal file
91
backend/ent/schema/announcement.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/domain"
|
||||
|
||||
"entgo.io/ent"
|
||||
"entgo.io/ent/dialect"
|
||||
"entgo.io/ent/dialect/entsql"
|
||||
"entgo.io/ent/schema"
|
||||
"entgo.io/ent/schema/edge"
|
||||
"entgo.io/ent/schema/field"
|
||||
"entgo.io/ent/schema/index"
|
||||
)
|
||||
|
||||
// Announcement holds the schema definition for the Announcement entity.
|
||||
//
|
||||
// 删除策略:硬删除(已读记录通过外键级联删除)
|
||||
type Announcement struct {
|
||||
ent.Schema
|
||||
}
|
||||
|
||||
func (Announcement) Annotations() []schema.Annotation {
|
||||
return []schema.Annotation{
|
||||
entsql.Annotation{Table: "announcements"},
|
||||
}
|
||||
}
|
||||
|
||||
func (Announcement) Fields() []ent.Field {
|
||||
return []ent.Field{
|
||||
field.String("title").
|
||||
MaxLen(200).
|
||||
NotEmpty().
|
||||
Comment("公告标题"),
|
||||
field.String("content").
|
||||
SchemaType(map[string]string{dialect.Postgres: "text"}).
|
||||
NotEmpty().
|
||||
Comment("公告内容(支持 Markdown)"),
|
||||
field.String("status").
|
||||
MaxLen(20).
|
||||
Default(domain.AnnouncementStatusDraft).
|
||||
Comment("状态: draft, active, archived"),
|
||||
field.JSON("targeting", domain.AnnouncementTargeting{}).
|
||||
Optional().
|
||||
SchemaType(map[string]string{dialect.Postgres: "jsonb"}).
|
||||
Comment("展示条件(JSON 规则)"),
|
||||
field.Time("starts_at").
|
||||
Optional().
|
||||
Nillable().
|
||||
SchemaType(map[string]string{dialect.Postgres: "timestamptz"}).
|
||||
Comment("开始展示时间(为空表示立即生效)"),
|
||||
field.Time("ends_at").
|
||||
Optional().
|
||||
Nillable().
|
||||
SchemaType(map[string]string{dialect.Postgres: "timestamptz"}).
|
||||
Comment("结束展示时间(为空表示永久生效)"),
|
||||
field.Int64("created_by").
|
||||
Optional().
|
||||
Nillable().
|
||||
Comment("创建人用户ID(管理员)"),
|
||||
field.Int64("updated_by").
|
||||
Optional().
|
||||
Nillable().
|
||||
Comment("更新人用户ID(管理员)"),
|
||||
field.Time("created_at").
|
||||
Immutable().
|
||||
Default(time.Now).
|
||||
SchemaType(map[string]string{dialect.Postgres: "timestamptz"}),
|
||||
field.Time("updated_at").
|
||||
Default(time.Now).
|
||||
UpdateDefault(time.Now).
|
||||
SchemaType(map[string]string{dialect.Postgres: "timestamptz"}),
|
||||
}
|
||||
}
|
||||
|
||||
func (Announcement) Edges() []ent.Edge {
|
||||
return []ent.Edge{
|
||||
edge.To("reads", AnnouncementRead.Type),
|
||||
}
|
||||
}
|
||||
|
||||
func (Announcement) Indexes() []ent.Index {
|
||||
return []ent.Index{
|
||||
index.Fields("status"),
|
||||
index.Fields("created_at"),
|
||||
index.Fields("starts_at"),
|
||||
index.Fields("ends_at"),
|
||||
}
|
||||
}
|
||||
|
||||
66
backend/ent/schema/announcement_read.go
Normal file
66
backend/ent/schema/announcement_read.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"entgo.io/ent"
|
||||
"entgo.io/ent/dialect"
|
||||
"entgo.io/ent/dialect/entsql"
|
||||
"entgo.io/ent/schema"
|
||||
"entgo.io/ent/schema/edge"
|
||||
"entgo.io/ent/schema/field"
|
||||
"entgo.io/ent/schema/index"
|
||||
)
|
||||
|
||||
// AnnouncementRead holds the schema definition for the AnnouncementRead entity.
|
||||
//
|
||||
// 记录用户对公告的已读状态(首次已读时间)。
|
||||
type AnnouncementRead struct {
|
||||
ent.Schema
|
||||
}
|
||||
|
||||
func (AnnouncementRead) Annotations() []schema.Annotation {
|
||||
return []schema.Annotation{
|
||||
entsql.Annotation{Table: "announcement_reads"},
|
||||
}
|
||||
}
|
||||
|
||||
func (AnnouncementRead) Fields() []ent.Field {
|
||||
return []ent.Field{
|
||||
field.Int64("announcement_id"),
|
||||
field.Int64("user_id"),
|
||||
field.Time("read_at").
|
||||
Default(time.Now).
|
||||
SchemaType(map[string]string{dialect.Postgres: "timestamptz"}).
|
||||
Comment("用户首次已读时间"),
|
||||
field.Time("created_at").
|
||||
Immutable().
|
||||
Default(time.Now).
|
||||
SchemaType(map[string]string{dialect.Postgres: "timestamptz"}),
|
||||
}
|
||||
}
|
||||
|
||||
func (AnnouncementRead) Edges() []ent.Edge {
|
||||
return []ent.Edge{
|
||||
edge.From("announcement", Announcement.Type).
|
||||
Ref("reads").
|
||||
Field("announcement_id").
|
||||
Unique().
|
||||
Required(),
|
||||
edge.From("user", User.Type).
|
||||
Ref("announcement_reads").
|
||||
Field("user_id").
|
||||
Unique().
|
||||
Required(),
|
||||
}
|
||||
}
|
||||
|
||||
func (AnnouncementRead) Indexes() []ent.Index {
|
||||
return []ent.Index{
|
||||
index.Fields("announcement_id"),
|
||||
index.Fields("user_id"),
|
||||
index.Fields("read_at"),
|
||||
index.Fields("announcement_id", "user_id").Unique(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package schema
|
||||
|
||||
import (
|
||||
"github.com/Wei-Shaw/sub2api/ent/schema/mixins"
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
"github.com/Wei-Shaw/sub2api/internal/domain"
|
||||
|
||||
"entgo.io/ent"
|
||||
"entgo.io/ent/dialect/entsql"
|
||||
@@ -45,7 +45,7 @@ func (APIKey) Fields() []ent.Field {
|
||||
Nillable(),
|
||||
field.String("status").
|
||||
MaxLen(20).
|
||||
Default(service.StatusActive),
|
||||
Default(domain.StatusActive),
|
||||
field.JSON("ip_whitelist", []string{}).
|
||||
Optional().
|
||||
Comment("Allowed IPs/CIDRs, e.g. [\"192.168.1.100\", \"10.0.0.0/8\"]"),
|
||||
|
||||
@@ -2,7 +2,7 @@ package schema
|
||||
|
||||
import (
|
||||
"github.com/Wei-Shaw/sub2api/ent/schema/mixins"
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
"github.com/Wei-Shaw/sub2api/internal/domain"
|
||||
|
||||
"entgo.io/ent"
|
||||
"entgo.io/ent/dialect"
|
||||
@@ -49,15 +49,15 @@ func (Group) Fields() []ent.Field {
|
||||
Default(false),
|
||||
field.String("status").
|
||||
MaxLen(20).
|
||||
Default(service.StatusActive),
|
||||
Default(domain.StatusActive),
|
||||
|
||||
// Subscription-related fields (added by migration 003)
|
||||
field.String("platform").
|
||||
MaxLen(50).
|
||||
Default(service.PlatformAnthropic),
|
||||
Default(domain.PlatformAnthropic),
|
||||
field.String("subscription_type").
|
||||
MaxLen(20).
|
||||
Default(service.SubscriptionTypeStandard),
|
||||
Default(domain.SubscriptionTypeStandard),
|
||||
field.Float("daily_limit_usd").
|
||||
Optional().
|
||||
Nillable().
|
||||
|
||||
@@ -3,7 +3,7 @@ package schema
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
"github.com/Wei-Shaw/sub2api/internal/domain"
|
||||
|
||||
"entgo.io/ent"
|
||||
"entgo.io/ent/dialect"
|
||||
@@ -49,7 +49,7 @@ func (PromoCode) Fields() []ent.Field {
|
||||
Comment("已使用次数"),
|
||||
field.String("status").
|
||||
MaxLen(20).
|
||||
Default(service.PromoCodeStatusActive).
|
||||
Default(domain.PromoCodeStatusActive).
|
||||
Comment("状态: active, disabled"),
|
||||
field.Time("expires_at").
|
||||
Optional().
|
||||
|
||||
@@ -3,7 +3,7 @@ package schema
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
"github.com/Wei-Shaw/sub2api/internal/domain"
|
||||
|
||||
"entgo.io/ent"
|
||||
"entgo.io/ent/dialect"
|
||||
@@ -41,13 +41,13 @@ func (RedeemCode) Fields() []ent.Field {
|
||||
Unique(),
|
||||
field.String("type").
|
||||
MaxLen(20).
|
||||
Default(service.RedeemTypeBalance),
|
||||
Default(domain.RedeemTypeBalance),
|
||||
field.Float("value").
|
||||
SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}).
|
||||
Default(0),
|
||||
field.String("status").
|
||||
MaxLen(20).
|
||||
Default(service.StatusUnused),
|
||||
Default(domain.StatusUnused),
|
||||
field.Int64("used_by").
|
||||
Optional().
|
||||
Nillable(),
|
||||
|
||||
@@ -2,7 +2,7 @@ package schema
|
||||
|
||||
import (
|
||||
"github.com/Wei-Shaw/sub2api/ent/schema/mixins"
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
"github.com/Wei-Shaw/sub2api/internal/domain"
|
||||
|
||||
"entgo.io/ent"
|
||||
"entgo.io/ent/dialect"
|
||||
@@ -43,7 +43,7 @@ func (User) Fields() []ent.Field {
|
||||
NotEmpty(),
|
||||
field.String("role").
|
||||
MaxLen(20).
|
||||
Default(service.RoleUser),
|
||||
Default(domain.RoleUser),
|
||||
field.Float("balance").
|
||||
SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}).
|
||||
Default(0),
|
||||
@@ -51,7 +51,7 @@ func (User) Fields() []ent.Field {
|
||||
Default(5),
|
||||
field.String("status").
|
||||
MaxLen(20).
|
||||
Default(service.StatusActive),
|
||||
Default(domain.StatusActive),
|
||||
|
||||
// Optional profile fields (added later; default '' in DB migration)
|
||||
field.String("username").
|
||||
@@ -81,6 +81,7 @@ func (User) Edges() []ent.Edge {
|
||||
edge.To("redeem_codes", RedeemCode.Type),
|
||||
edge.To("subscriptions", UserSubscription.Type),
|
||||
edge.To("assigned_subscriptions", UserSubscription.Type),
|
||||
edge.To("announcement_reads", AnnouncementRead.Type),
|
||||
edge.To("allowed_groups", Group.Type).
|
||||
Through("user_allowed_groups", UserAllowedGroup.Type),
|
||||
edge.To("usage_logs", UsageLog.Type),
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/ent/schema/mixins"
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
"github.com/Wei-Shaw/sub2api/internal/domain"
|
||||
|
||||
"entgo.io/ent"
|
||||
"entgo.io/ent/dialect"
|
||||
@@ -44,7 +44,7 @@ func (UserSubscription) Fields() []ent.Field {
|
||||
SchemaType(map[string]string{dialect.Postgres: "timestamptz"}),
|
||||
field.String("status").
|
||||
MaxLen(20).
|
||||
Default(service.SubscriptionStatusActive),
|
||||
Default(domain.SubscriptionStatusActive),
|
||||
|
||||
field.Time("daily_window_start").
|
||||
Optional().
|
||||
|
||||
Reference in New Issue
Block a user