Merge pull request #187 from Dvorinka/feat/sku-shop

feat: SKU entity, admin product management, and /shop purchase flow
pull/3587/head
Tomáš Dvořák 2 weeks ago committed by GitHub
commit c5daf0e675
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -183,7 +183,7 @@ Order = user-visible value first; each ships with backend + UI + tests.
- [ ] Multi-account switcher UI — N-token session store, avatar-menu switch + signed-out badges (frontend-only, backend already supports)
- [ ] QQ Connect (non-OIDC protocol, separate integration), account linking UI for existing local accounts, group/role claim mapping
4. **VAS/monetization-free** — credits + redemption codes as *free* features (gift codes for admin use), storage/membership SKU definitions; payment processors stay out of scope (fixes #3231). See §1.3a for the SKU/credits/gift-code spec.
- [ ] `sku` entity (storage-capacity + group-upgrade types, duration, cash+points price, label, bullets); Shop page (Memberships/Storage/Redeem tabs)
- [x] `sku` entity (storage-capacity + group-upgrade types, duration, cash+points price, label, bullets); points purchase → atomic debit+grant; admin SKU tables; `/shop` page (Memberships/Storage/Redeem tabs) + nav entry
- [x] `user.credits` + `credit_txn` ledger (guarded atomic adjust); Finance settings tab (balance + grants + redeem + ledger); admin manual adjust (#183)
- [x] `gift_code` entity (points/storage/group × amount × duration) + `user_grant` expiring grants + `grant_expire` cron; admin generate/list/revoke + user redeem (#183)
- [ ] Paid-share `price_points` wired to the ledger + purchase/resume-ticket flow

@ -34,6 +34,7 @@ import (
"github.com/cloudreve/Cloudreve/v4/ent/passkey"
"github.com/cloudreve/Cloudreve/v4/ent/setting"
"github.com/cloudreve/Cloudreve/v4/ent/share"
"github.com/cloudreve/Cloudreve/v4/ent/sku"
"github.com/cloudreve/Cloudreve/v4/ent/storagepolicy"
"github.com/cloudreve/Cloudreve/v4/ent/task"
"github.com/cloudreve/Cloudreve/v4/ent/user"
@ -85,6 +86,8 @@ type Client struct {
Setting *SettingClient
// Share is the client for interacting with the Share builders.
Share *ShareClient
// Sku is the client for interacting with the Sku builders.
Sku *SkuClient
// StoragePolicy is the client for interacting with the StoragePolicy builders.
StoragePolicy *StoragePolicyClient
// Task is the client for interacting with the Task builders.
@ -123,6 +126,7 @@ func (c *Client) init() {
c.Passkey = NewPasskeyClient(c.config)
c.Setting = NewSettingClient(c.config)
c.Share = NewShareClient(c.config)
c.Sku = NewSkuClient(c.config)
c.StoragePolicy = NewStoragePolicyClient(c.config)
c.Task = NewTaskClient(c.config)
c.User = NewUserClient(c.config)
@ -238,6 +242,7 @@ func (c *Client) Tx(ctx context.Context) (*Tx, error) {
Passkey: NewPasskeyClient(cfg),
Setting: NewSettingClient(cfg),
Share: NewShareClient(cfg),
Sku: NewSkuClient(cfg),
StoragePolicy: NewStoragePolicyClient(cfg),
Task: NewTaskClient(cfg),
User: NewUserClient(cfg),
@ -280,6 +285,7 @@ func (c *Client) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error)
Passkey: NewPasskeyClient(cfg),
Setting: NewSettingClient(cfg),
Share: NewShareClient(cfg),
Sku: NewSkuClient(cfg),
StoragePolicy: NewStoragePolicyClient(cfg),
Task: NewTaskClient(cfg),
User: NewUserClient(cfg),
@ -316,7 +322,7 @@ func (c *Client) Use(hooks ...Hook) {
c.AbuseReport, c.AclEntry, c.ActivityEvent, c.CreditTxn, c.DavAccount,
c.DirectLink, c.Entity, c.File, c.FsEvent, c.GiftCode, c.Group,
c.InvitationCode, c.Metadata, c.Node, c.OAuthClient, c.OAuthGrant, c.Passkey,
c.Setting, c.Share, c.StoragePolicy, c.Task, c.User, c.UserGrant,
c.Setting, c.Share, c.Sku, c.StoragePolicy, c.Task, c.User, c.UserGrant,
} {
n.Use(hooks...)
}
@ -329,7 +335,7 @@ func (c *Client) Intercept(interceptors ...Interceptor) {
c.AbuseReport, c.AclEntry, c.ActivityEvent, c.CreditTxn, c.DavAccount,
c.DirectLink, c.Entity, c.File, c.FsEvent, c.GiftCode, c.Group,
c.InvitationCode, c.Metadata, c.Node, c.OAuthClient, c.OAuthGrant, c.Passkey,
c.Setting, c.Share, c.StoragePolicy, c.Task, c.User, c.UserGrant,
c.Setting, c.Share, c.Sku, c.StoragePolicy, c.Task, c.User, c.UserGrant,
} {
n.Intercept(interceptors...)
}
@ -376,6 +382,8 @@ func (c *Client) Mutate(ctx context.Context, m Mutation) (Value, error) {
return c.Setting.mutate(ctx, m)
case *ShareMutation:
return c.Share.mutate(ctx, m)
case *SkuMutation:
return c.Sku.mutate(ctx, m)
case *StoragePolicyMutation:
return c.StoragePolicy.mutate(ctx, m)
case *TaskMutation:
@ -3417,6 +3425,141 @@ func (c *ShareClient) mutate(ctx context.Context, m *ShareMutation) (Value, erro
}
}
// SkuClient is a client for the Sku schema.
type SkuClient struct {
config
}
// NewSkuClient returns a client for the Sku from the given config.
func NewSkuClient(c config) *SkuClient {
return &SkuClient{config: c}
}
// Use adds a list of mutation hooks to the hooks stack.
// A call to `Use(f, g, h)` equals to `sku.Hooks(f(g(h())))`.
func (c *SkuClient) Use(hooks ...Hook) {
c.hooks.Sku = append(c.hooks.Sku, hooks...)
}
// Intercept adds a list of query interceptors to the interceptors stack.
// A call to `Intercept(f, g, h)` equals to `sku.Intercept(f(g(h())))`.
func (c *SkuClient) Intercept(interceptors ...Interceptor) {
c.inters.Sku = append(c.inters.Sku, interceptors...)
}
// Create returns a builder for creating a Sku entity.
func (c *SkuClient) Create() *SkuCreate {
mutation := newSkuMutation(c.config, OpCreate)
return &SkuCreate{config: c.config, hooks: c.Hooks(), mutation: mutation}
}
// CreateBulk returns a builder for creating a bulk of Sku entities.
func (c *SkuClient) CreateBulk(builders ...*SkuCreate) *SkuCreateBulk {
return &SkuCreateBulk{config: c.config, builders: builders}
}
// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates
// a builder and applies setFunc on it.
func (c *SkuClient) MapCreateBulk(slice any, setFunc func(*SkuCreate, int)) *SkuCreateBulk {
rv := reflect.ValueOf(slice)
if rv.Kind() != reflect.Slice {
return &SkuCreateBulk{err: fmt.Errorf("calling to SkuClient.MapCreateBulk with wrong type %T, need slice", slice)}
}
builders := make([]*SkuCreate, rv.Len())
for i := 0; i < rv.Len(); i++ {
builders[i] = c.Create()
setFunc(builders[i], i)
}
return &SkuCreateBulk{config: c.config, builders: builders}
}
// Update returns an update builder for Sku.
func (c *SkuClient) Update() *SkuUpdate {
mutation := newSkuMutation(c.config, OpUpdate)
return &SkuUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation}
}
// UpdateOne returns an update builder for the given entity.
func (c *SkuClient) UpdateOne(s *Sku) *SkuUpdateOne {
mutation := newSkuMutation(c.config, OpUpdateOne, withSku(s))
return &SkuUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation}
}
// UpdateOneID returns an update builder for the given id.
func (c *SkuClient) UpdateOneID(id int) *SkuUpdateOne {
mutation := newSkuMutation(c.config, OpUpdateOne, withSkuID(id))
return &SkuUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation}
}
// Delete returns a delete builder for Sku.
func (c *SkuClient) Delete() *SkuDelete {
mutation := newSkuMutation(c.config, OpDelete)
return &SkuDelete{config: c.config, hooks: c.Hooks(), mutation: mutation}
}
// DeleteOne returns a builder for deleting the given entity.
func (c *SkuClient) DeleteOne(s *Sku) *SkuDeleteOne {
return c.DeleteOneID(s.ID)
}
// DeleteOneID returns a builder for deleting the given entity by its id.
func (c *SkuClient) DeleteOneID(id int) *SkuDeleteOne {
builder := c.Delete().Where(sku.ID(id))
builder.mutation.id = &id
builder.mutation.op = OpDeleteOne
return &SkuDeleteOne{builder}
}
// Query returns a query builder for Sku.
func (c *SkuClient) Query() *SkuQuery {
return &SkuQuery{
config: c.config,
ctx: &QueryContext{Type: TypeSku},
inters: c.Interceptors(),
}
}
// Get returns a Sku entity by its id.
func (c *SkuClient) Get(ctx context.Context, id int) (*Sku, error) {
return c.Query().Where(sku.ID(id)).Only(ctx)
}
// GetX is like Get, but panics if an error occurs.
func (c *SkuClient) GetX(ctx context.Context, id int) *Sku {
obj, err := c.Get(ctx, id)
if err != nil {
panic(err)
}
return obj
}
// Hooks returns the client hooks.
func (c *SkuClient) Hooks() []Hook {
hooks := c.hooks.Sku
return append(hooks[:len(hooks):len(hooks)], sku.Hooks[:]...)
}
// Interceptors returns the client interceptors.
func (c *SkuClient) Interceptors() []Interceptor {
inters := c.inters.Sku
return append(inters[:len(inters):len(inters)], sku.Interceptors[:]...)
}
func (c *SkuClient) mutate(ctx context.Context, m *SkuMutation) (Value, error) {
switch m.Op() {
case OpCreate:
return (&SkuCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
case OpUpdate:
return (&SkuUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
case OpUpdateOne:
return (&SkuUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
case OpDelete, OpDeleteOne:
return (&SkuDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx)
default:
return nil, fmt.Errorf("ent: unknown Sku mutation op: %q", m.Op())
}
}
// StoragePolicyClient is a client for the StoragePolicy schema.
type StoragePolicyClient struct {
config
@ -4266,13 +4409,13 @@ type (
hooks struct {
AbuseReport, AclEntry, ActivityEvent, CreditTxn, DavAccount, DirectLink, Entity,
File, FsEvent, GiftCode, Group, InvitationCode, Metadata, Node, OAuthClient,
OAuthGrant, Passkey, Setting, Share, StoragePolicy, Task, User,
OAuthGrant, Passkey, Setting, Share, Sku, StoragePolicy, Task, User,
UserGrant []ent.Hook
}
inters struct {
AbuseReport, AclEntry, ActivityEvent, CreditTxn, DavAccount, DirectLink, Entity,
File, FsEvent, GiftCode, Group, InvitationCode, Metadata, Node, OAuthClient,
OAuthGrant, Passkey, Setting, Share, StoragePolicy, Task, User,
OAuthGrant, Passkey, Setting, Share, Sku, StoragePolicy, Task, User,
UserGrant []ent.Interceptor
}
)

@ -31,6 +31,7 @@ import (
"github.com/cloudreve/Cloudreve/v4/ent/passkey"
"github.com/cloudreve/Cloudreve/v4/ent/setting"
"github.com/cloudreve/Cloudreve/v4/ent/share"
"github.com/cloudreve/Cloudreve/v4/ent/sku"
"github.com/cloudreve/Cloudreve/v4/ent/storagepolicy"
"github.com/cloudreve/Cloudreve/v4/ent/task"
"github.com/cloudreve/Cloudreve/v4/ent/user"
@ -114,6 +115,7 @@ func checkColumn(table, column string) error {
passkey.Table: passkey.ValidColumn,
setting.Table: setting.ValidColumn,
share.Table: share.ValidColumn,
sku.Table: sku.ValidColumn,
storagepolicy.Table: storagepolicy.ValidColumn,
task.Table: task.ValidColumn,
user.Table: user.ValidColumn,

@ -237,6 +237,18 @@ func (f ShareFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error
return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.ShareMutation", m)
}
// The SkuFunc type is an adapter to allow the use of ordinary
// function as Sku mutator.
type SkuFunc func(context.Context, *ent.SkuMutation) (ent.Value, error)
// Mutate calls f(ctx, m).
func (f SkuFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) {
if mv, ok := m.(*ent.SkuMutation); ok {
return f(ctx, mv)
}
return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.SkuMutation", m)
}
// The StoragePolicyFunc type is an adapter to allow the use of ordinary
// function as StoragePolicy mutator.
type StoragePolicyFunc func(context.Context, *ent.StoragePolicyMutation) (ent.Value, error)

@ -28,6 +28,7 @@ import (
"github.com/cloudreve/Cloudreve/v4/ent/predicate"
"github.com/cloudreve/Cloudreve/v4/ent/setting"
"github.com/cloudreve/Cloudreve/v4/ent/share"
"github.com/cloudreve/Cloudreve/v4/ent/sku"
"github.com/cloudreve/Cloudreve/v4/ent/storagepolicy"
"github.com/cloudreve/Cloudreve/v4/ent/task"
"github.com/cloudreve/Cloudreve/v4/ent/user"
@ -603,6 +604,33 @@ func (f TraverseShare) Traverse(ctx context.Context, q ent.Query) error {
return fmt.Errorf("unexpected query type %T. expect *ent.ShareQuery", q)
}
// The SkuFunc type is an adapter to allow the use of ordinary function as a Querier.
type SkuFunc func(context.Context, *ent.SkuQuery) (ent.Value, error)
// Query calls f(ctx, q).
func (f SkuFunc) Query(ctx context.Context, q ent.Query) (ent.Value, error) {
if q, ok := q.(*ent.SkuQuery); ok {
return f(ctx, q)
}
return nil, fmt.Errorf("unexpected query type %T. expect *ent.SkuQuery", q)
}
// The TraverseSku type is an adapter to allow the use of ordinary function as Traverser.
type TraverseSku func(context.Context, *ent.SkuQuery) error
// Intercept is a dummy implementation of Intercept that returns the next Querier in the pipeline.
func (f TraverseSku) Intercept(next ent.Querier) ent.Querier {
return next
}
// Traverse calls f(ctx, q).
func (f TraverseSku) Traverse(ctx context.Context, q ent.Query) error {
if q, ok := q.(*ent.SkuQuery); ok {
return f(ctx, q)
}
return fmt.Errorf("unexpected query type %T. expect *ent.SkuQuery", q)
}
// The StoragePolicyFunc type is an adapter to allow the use of ordinary function as a Querier.
type StoragePolicyFunc func(context.Context, *ent.StoragePolicyQuery) (ent.Value, error)
@ -752,6 +780,8 @@ func NewQuery(q ent.Query) (Query, error) {
return &query[*ent.SettingQuery, predicate.Setting, setting.OrderOption]{typ: ent.TypeSetting, tq: q}, nil
case *ent.ShareQuery:
return &query[*ent.ShareQuery, predicate.Share, share.OrderOption]{typ: ent.TypeShare, tq: q}, nil
case *ent.SkuQuery:
return &query[*ent.SkuQuery, predicate.Sku, sku.OrderOption]{typ: ent.TypeSku, tq: q}, nil
case *ent.StoragePolicyQuery:
return &query[*ent.StoragePolicyQuery, predicate.StoragePolicy, storagepolicy.OrderOption]{typ: ent.TypeStoragePolicy, tq: q}, nil
case *ent.TaskQuery:

File diff suppressed because one or more lines are too long

@ -602,6 +602,29 @@ var (
},
},
}
// SkusColumns holds the columns for the "skus" table.
SkusColumns = []*schema.Column{
{Name: "id", Type: field.TypeInt, Increment: true},
{Name: "created_at", Type: field.TypeTime, SchemaType: map[string]string{"mysql": "datetime"}},
{Name: "updated_at", Type: field.TypeTime, SchemaType: map[string]string{"mysql": "datetime"}},
{Name: "deleted_at", Type: field.TypeTime, Nullable: true, SchemaType: map[string]string{"mysql": "datetime"}},
{Name: "name", Type: field.TypeString},
{Name: "type", Type: field.TypeEnum, Enums: []string{"storage", "group"}},
{Name: "amount", Type: field.TypeInt64},
{Name: "duration", Type: field.TypeInt64, Nullable: true},
{Name: "price", Type: field.TypeInt64, Nullable: true},
{Name: "points", Type: field.TypeInt64, Nullable: true},
{Name: "label", Type: field.TypeString, Nullable: true},
{Name: "des", Type: field.TypeString, Nullable: true},
{Name: "enabled", Type: field.TypeBool, Default: true},
{Name: "weight", Type: field.TypeInt, Default: 0},
}
// SkusTable holds the schema information for the "skus" table.
SkusTable = &schema.Table{
Name: "skus",
Columns: SkusColumns,
PrimaryKey: []*schema.Column{SkusColumns[0]},
}
// StoragePoliciesColumns holds the columns for the "storage_policies" table.
StoragePoliciesColumns = []*schema.Column{
{Name: "id", Type: field.TypeInt, Increment: true},
@ -808,6 +831,7 @@ var (
PasskeysTable,
SettingsTable,
SharesTable,
SkusTable,
StoragePoliciesTable,
TasksTable,
UsersTable,

File diff suppressed because it is too large Load Diff

@ -118,6 +118,12 @@ func (m *ShareMutation) SetRawID(t int) {
// SetUpdatedAt sets the "updated_at" field.
func (m *SkuMutation) SetRawID(t int) {
m.id = &t
}
// SetUpdatedAt sets the "updated_at" field.
func (m *StoragePolicyMutation) SetRawID(t int) {
m.id = &t
}

@ -63,6 +63,9 @@ type Setting func(*sql.Selector)
// Share is the predicate function for share builders.
type Share func(*sql.Selector)
// Sku is the predicate function for sku builders.
type Sku func(*sql.Selector)
// StoragePolicy is the predicate function for storagepolicy builders.
type StoragePolicy func(*sql.Selector)

@ -25,6 +25,7 @@ import (
"github.com/cloudreve/Cloudreve/v4/ent/schema"
"github.com/cloudreve/Cloudreve/v4/ent/setting"
"github.com/cloudreve/Cloudreve/v4/ent/share"
"github.com/cloudreve/Cloudreve/v4/ent/sku"
"github.com/cloudreve/Cloudreve/v4/ent/storagepolicy"
"github.com/cloudreve/Cloudreve/v4/ent/task"
"github.com/cloudreve/Cloudreve/v4/ent/user"
@ -482,6 +483,33 @@ func init() {
shareDescDownloads := shareFields[2].Descriptor()
// share.DefaultDownloads holds the default value on creation for the downloads field.
share.DefaultDownloads = shareDescDownloads.Default.(int)
skuMixin := schema.Sku{}.Mixin()
skuMixinHooks0 := skuMixin[0].Hooks()
sku.Hooks[0] = skuMixinHooks0[0]
skuMixinInters0 := skuMixin[0].Interceptors()
sku.Interceptors[0] = skuMixinInters0[0]
skuMixinFields0 := skuMixin[0].Fields()
_ = skuMixinFields0
skuFields := schema.Sku{}.Fields()
_ = skuFields
// skuDescCreatedAt is the schema descriptor for created_at field.
skuDescCreatedAt := skuMixinFields0[0].Descriptor()
// sku.DefaultCreatedAt holds the default value on creation for the created_at field.
sku.DefaultCreatedAt = skuDescCreatedAt.Default.(func() time.Time)
// skuDescUpdatedAt is the schema descriptor for updated_at field.
skuDescUpdatedAt := skuMixinFields0[1].Descriptor()
// sku.DefaultUpdatedAt holds the default value on creation for the updated_at field.
sku.DefaultUpdatedAt = skuDescUpdatedAt.Default.(func() time.Time)
// sku.UpdateDefaultUpdatedAt holds the default value on update for the updated_at field.
sku.UpdateDefaultUpdatedAt = skuDescUpdatedAt.UpdateDefault.(func() time.Time)
// skuDescEnabled is the schema descriptor for enabled field.
skuDescEnabled := skuFields[8].Descriptor()
// sku.DefaultEnabled holds the default value on creation for the enabled field.
sku.DefaultEnabled = skuDescEnabled.Default.(bool)
// skuDescWeight is the schema descriptor for weight field.
skuDescWeight := skuFields[9].Descriptor()
// sku.DefaultWeight holds the default value on creation for the weight field.
sku.DefaultWeight = skuDescWeight.Default.(int)
storagepolicyMixin := schema.StoragePolicy{}.Mixin()
storagepolicyMixinHooks0 := storagepolicyMixin[0].Hooks()
storagepolicy.Hooks[0] = storagepolicyMixinHooks0[0]

@ -0,0 +1,52 @@
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/field"
)
// Sku holds the schema definition for purchasable products: a storage
// capacity pack (amount = bytes) or a membership upgrade (amount = target
// group id). duration is seconds; 0 means the grant never expires. points
// is the credit price; NULL means the product cannot be bought with
// points. price is the display cash price in the smallest currency unit —
// cash payment processors are intentionally out of scope.
type Sku struct {
ent.Schema
}
// Fields of the Sku.
func (Sku) Fields() []ent.Field {
return []ent.Field{
field.String("name"),
field.Enum("type").
Values("storage", "group"),
field.Int64("amount"),
field.Int64("duration").
Optional(),
field.Int64("price").
Optional(),
field.Int64("points").
Optional().
Nillable(),
field.String("label").
Optional(),
field.String("des").
Optional(),
field.Bool("enabled").
Default(true),
field.Int("weight").
Default(0),
}
}
// Edges of the Sku.
func (Sku) Edges() []ent.Edge {
return nil
}
func (Sku) Mixin() []ent.Mixin {
return []ent.Mixin{
CommonMixin{},
}
}

@ -0,0 +1,246 @@
// Code generated by ent, DO NOT EDIT.
package ent
import (
"fmt"
"strings"
"time"
"entgo.io/ent"
"entgo.io/ent/dialect/sql"
"github.com/cloudreve/Cloudreve/v4/ent/sku"
)
// Sku is the model entity for the Sku schema.
type Sku struct {
config `json:"-"`
// ID of the ent.
ID int `json:"id,omitempty"`
// CreatedAt holds the value of the "created_at" field.
CreatedAt time.Time `json:"created_at,omitempty"`
// UpdatedAt holds the value of the "updated_at" field.
UpdatedAt time.Time `json:"updated_at,omitempty"`
// DeletedAt holds the value of the "deleted_at" field.
DeletedAt *time.Time `json:"deleted_at,omitempty"`
// Name holds the value of the "name" field.
Name string `json:"name,omitempty"`
// Type holds the value of the "type" field.
Type sku.Type `json:"type,omitempty"`
// Amount holds the value of the "amount" field.
Amount int64 `json:"amount,omitempty"`
// Duration holds the value of the "duration" field.
Duration int64 `json:"duration,omitempty"`
// Price holds the value of the "price" field.
Price int64 `json:"price,omitempty"`
// Points holds the value of the "points" field.
Points *int64 `json:"points,omitempty"`
// Label holds the value of the "label" field.
Label string `json:"label,omitempty"`
// Des holds the value of the "des" field.
Des string `json:"des,omitempty"`
// Enabled holds the value of the "enabled" field.
Enabled bool `json:"enabled,omitempty"`
// Weight holds the value of the "weight" field.
Weight int `json:"weight,omitempty"`
selectValues sql.SelectValues
}
// scanValues returns the types for scanning values from sql.Rows.
func (*Sku) scanValues(columns []string) ([]any, error) {
values := make([]any, len(columns))
for i := range columns {
switch columns[i] {
case sku.FieldEnabled:
values[i] = new(sql.NullBool)
case sku.FieldID, sku.FieldAmount, sku.FieldDuration, sku.FieldPrice, sku.FieldPoints, sku.FieldWeight:
values[i] = new(sql.NullInt64)
case sku.FieldName, sku.FieldType, sku.FieldLabel, sku.FieldDes:
values[i] = new(sql.NullString)
case sku.FieldCreatedAt, sku.FieldUpdatedAt, sku.FieldDeletedAt:
values[i] = new(sql.NullTime)
default:
values[i] = new(sql.UnknownType)
}
}
return values, nil
}
// assignValues assigns the values that were returned from sql.Rows (after scanning)
// to the Sku fields.
func (s *Sku) assignValues(columns []string, values []any) error {
if m, n := len(values), len(columns); m < n {
return fmt.Errorf("mismatch number of scan values: %d != %d", m, n)
}
for i := range columns {
switch columns[i] {
case sku.FieldID:
value, ok := values[i].(*sql.NullInt64)
if !ok {
return fmt.Errorf("unexpected type %T for field id", value)
}
s.ID = int(value.Int64)
case sku.FieldCreatedAt:
if value, ok := values[i].(*sql.NullTime); !ok {
return fmt.Errorf("unexpected type %T for field created_at", values[i])
} else if value.Valid {
s.CreatedAt = value.Time
}
case sku.FieldUpdatedAt:
if value, ok := values[i].(*sql.NullTime); !ok {
return fmt.Errorf("unexpected type %T for field updated_at", values[i])
} else if value.Valid {
s.UpdatedAt = value.Time
}
case sku.FieldDeletedAt:
if value, ok := values[i].(*sql.NullTime); !ok {
return fmt.Errorf("unexpected type %T for field deleted_at", values[i])
} else if value.Valid {
s.DeletedAt = new(time.Time)
*s.DeletedAt = value.Time
}
case sku.FieldName:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field name", values[i])
} else if value.Valid {
s.Name = value.String
}
case sku.FieldType:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field type", values[i])
} else if value.Valid {
s.Type = sku.Type(value.String)
}
case sku.FieldAmount:
if value, ok := values[i].(*sql.NullInt64); !ok {
return fmt.Errorf("unexpected type %T for field amount", values[i])
} else if value.Valid {
s.Amount = value.Int64
}
case sku.FieldDuration:
if value, ok := values[i].(*sql.NullInt64); !ok {
return fmt.Errorf("unexpected type %T for field duration", values[i])
} else if value.Valid {
s.Duration = value.Int64
}
case sku.FieldPrice:
if value, ok := values[i].(*sql.NullInt64); !ok {
return fmt.Errorf("unexpected type %T for field price", values[i])
} else if value.Valid {
s.Price = value.Int64
}
case sku.FieldPoints:
if value, ok := values[i].(*sql.NullInt64); !ok {
return fmt.Errorf("unexpected type %T for field points", values[i])
} else if value.Valid {
s.Points = new(int64)
*s.Points = value.Int64
}
case sku.FieldLabel:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field label", values[i])
} else if value.Valid {
s.Label = value.String
}
case sku.FieldDes:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field des", values[i])
} else if value.Valid {
s.Des = value.String
}
case sku.FieldEnabled:
if value, ok := values[i].(*sql.NullBool); !ok {
return fmt.Errorf("unexpected type %T for field enabled", values[i])
} else if value.Valid {
s.Enabled = value.Bool
}
case sku.FieldWeight:
if value, ok := values[i].(*sql.NullInt64); !ok {
return fmt.Errorf("unexpected type %T for field weight", values[i])
} else if value.Valid {
s.Weight = int(value.Int64)
}
default:
s.selectValues.Set(columns[i], values[i])
}
}
return nil
}
// Value returns the ent.Value that was dynamically selected and assigned to the Sku.
// This includes values selected through modifiers, order, etc.
func (s *Sku) Value(name string) (ent.Value, error) {
return s.selectValues.Get(name)
}
// Update returns a builder for updating this Sku.
// Note that you need to call Sku.Unwrap() before calling this method if this Sku
// was returned from a transaction, and the transaction was committed or rolled back.
func (s *Sku) Update() *SkuUpdateOne {
return NewSkuClient(s.config).UpdateOne(s)
}
// Unwrap unwraps the Sku entity that was returned from a transaction after it was closed,
// so that all future queries will be executed through the driver which created the transaction.
func (s *Sku) Unwrap() *Sku {
_tx, ok := s.config.driver.(*txDriver)
if !ok {
panic("ent: Sku is not a transactional entity")
}
s.config.driver = _tx.drv
return s
}
// String implements the fmt.Stringer.
func (s *Sku) String() string {
var builder strings.Builder
builder.WriteString("Sku(")
builder.WriteString(fmt.Sprintf("id=%v, ", s.ID))
builder.WriteString("created_at=")
builder.WriteString(s.CreatedAt.Format(time.ANSIC))
builder.WriteString(", ")
builder.WriteString("updated_at=")
builder.WriteString(s.UpdatedAt.Format(time.ANSIC))
builder.WriteString(", ")
if v := s.DeletedAt; v != nil {
builder.WriteString("deleted_at=")
builder.WriteString(v.Format(time.ANSIC))
}
builder.WriteString(", ")
builder.WriteString("name=")
builder.WriteString(s.Name)
builder.WriteString(", ")
builder.WriteString("type=")
builder.WriteString(fmt.Sprintf("%v", s.Type))
builder.WriteString(", ")
builder.WriteString("amount=")
builder.WriteString(fmt.Sprintf("%v", s.Amount))
builder.WriteString(", ")
builder.WriteString("duration=")
builder.WriteString(fmt.Sprintf("%v", s.Duration))
builder.WriteString(", ")
builder.WriteString("price=")
builder.WriteString(fmt.Sprintf("%v", s.Price))
builder.WriteString(", ")
if v := s.Points; v != nil {
builder.WriteString("points=")
builder.WriteString(fmt.Sprintf("%v", *v))
}
builder.WriteString(", ")
builder.WriteString("label=")
builder.WriteString(s.Label)
builder.WriteString(", ")
builder.WriteString("des=")
builder.WriteString(s.Des)
builder.WriteString(", ")
builder.WriteString("enabled=")
builder.WriteString(fmt.Sprintf("%v", s.Enabled))
builder.WriteString(", ")
builder.WriteString("weight=")
builder.WriteString(fmt.Sprintf("%v", s.Weight))
builder.WriteByte(')')
return builder.String()
}
// Skus is a parsable slice of Sku.
type Skus []*Sku

@ -0,0 +1,190 @@
// Code generated by ent, DO NOT EDIT.
package sku
import (
"fmt"
"time"
"entgo.io/ent"
"entgo.io/ent/dialect/sql"
)
const (
// Label holds the string label denoting the sku type in the database.
Label = "sku"
// FieldID holds the string denoting the id field in the database.
FieldID = "id"
// FieldCreatedAt holds the string denoting the created_at field in the database.
FieldCreatedAt = "created_at"
// FieldUpdatedAt holds the string denoting the updated_at field in the database.
FieldUpdatedAt = "updated_at"
// FieldDeletedAt holds the string denoting the deleted_at field in the database.
FieldDeletedAt = "deleted_at"
// FieldName holds the string denoting the name field in the database.
FieldName = "name"
// FieldType holds the string denoting the type field in the database.
FieldType = "type"
// FieldAmount holds the string denoting the amount field in the database.
FieldAmount = "amount"
// FieldDuration holds the string denoting the duration field in the database.
FieldDuration = "duration"
// FieldPrice holds the string denoting the price field in the database.
FieldPrice = "price"
// FieldPoints holds the string denoting the points field in the database.
FieldPoints = "points"
// FieldLabel holds the string denoting the label field in the database.
FieldLabel = "label"
// FieldDes holds the string denoting the des field in the database.
FieldDes = "des"
// FieldEnabled holds the string denoting the enabled field in the database.
FieldEnabled = "enabled"
// FieldWeight holds the string denoting the weight field in the database.
FieldWeight = "weight"
// Table holds the table name of the sku in the database.
Table = "skus"
)
// Columns holds all SQL columns for sku fields.
var Columns = []string{
FieldID,
FieldCreatedAt,
FieldUpdatedAt,
FieldDeletedAt,
FieldName,
FieldType,
FieldAmount,
FieldDuration,
FieldPrice,
FieldPoints,
FieldLabel,
FieldDes,
FieldEnabled,
FieldWeight,
}
// ValidColumn reports if the column name is valid (part of the table columns).
func ValidColumn(column string) bool {
for i := range Columns {
if column == Columns[i] {
return true
}
}
return false
}
// Note that the variables below are initialized by the runtime
// package on the initialization of the application. Therefore,
// it should be imported in the main as follows:
//
// import _ "github.com/cloudreve/Cloudreve/v4/ent/runtime"
var (
Hooks [1]ent.Hook
Interceptors [1]ent.Interceptor
// DefaultCreatedAt holds the default value on creation for the "created_at" field.
DefaultCreatedAt func() time.Time
// DefaultUpdatedAt holds the default value on creation for the "updated_at" field.
DefaultUpdatedAt func() time.Time
// UpdateDefaultUpdatedAt holds the default value on update for the "updated_at" field.
UpdateDefaultUpdatedAt func() time.Time
// DefaultEnabled holds the default value on creation for the "enabled" field.
DefaultEnabled bool
// DefaultWeight holds the default value on creation for the "weight" field.
DefaultWeight int
)
// Type defines the type for the "type" enum field.
type Type string
// Type values.
const (
TypeStorage Type = "storage"
TypeGroup Type = "group"
)
func (_type Type) String() string {
return string(_type)
}
// TypeValidator is a validator for the "type" field enum values. It is called by the builders before save.
func TypeValidator(_type Type) error {
switch _type {
case TypeStorage, TypeGroup:
return nil
default:
return fmt.Errorf("sku: invalid enum value for type field: %q", _type)
}
}
// OrderOption defines the ordering options for the Sku queries.
type OrderOption func(*sql.Selector)
// ByID orders the results by the id field.
func ByID(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldID, opts...).ToFunc()
}
// ByCreatedAt orders the results by the created_at field.
func ByCreatedAt(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldCreatedAt, opts...).ToFunc()
}
// ByUpdatedAt orders the results by the updated_at field.
func ByUpdatedAt(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldUpdatedAt, opts...).ToFunc()
}
// ByDeletedAt orders the results by the deleted_at field.
func ByDeletedAt(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldDeletedAt, opts...).ToFunc()
}
// ByName orders the results by the name field.
func ByName(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldName, opts...).ToFunc()
}
// ByType orders the results by the type field.
func ByType(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldType, opts...).ToFunc()
}
// ByAmount orders the results by the amount field.
func ByAmount(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldAmount, opts...).ToFunc()
}
// ByDuration orders the results by the duration field.
func ByDuration(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldDuration, opts...).ToFunc()
}
// ByPrice orders the results by the price field.
func ByPrice(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldPrice, opts...).ToFunc()
}
// ByPoints orders the results by the points field.
func ByPoints(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldPoints, opts...).ToFunc()
}
// ByLabel orders the results by the label field.
func ByLabel(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldLabel, opts...).ToFunc()
}
// ByDes orders the results by the des field.
func ByDes(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldDes, opts...).ToFunc()
}
// ByEnabled orders the results by the enabled field.
func ByEnabled(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldEnabled, opts...).ToFunc()
}
// ByWeight orders the results by the weight field.
func ByWeight(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldWeight, opts...).ToFunc()
}

@ -0,0 +1,730 @@
// Code generated by ent, DO NOT EDIT.
package sku
import (
"time"
"entgo.io/ent/dialect/sql"
"github.com/cloudreve/Cloudreve/v4/ent/predicate"
)
// ID filters vertices based on their ID field.
func ID(id int) predicate.Sku {
return predicate.Sku(sql.FieldEQ(FieldID, id))
}
// IDEQ applies the EQ predicate on the ID field.
func IDEQ(id int) predicate.Sku {
return predicate.Sku(sql.FieldEQ(FieldID, id))
}
// IDNEQ applies the NEQ predicate on the ID field.
func IDNEQ(id int) predicate.Sku {
return predicate.Sku(sql.FieldNEQ(FieldID, id))
}
// IDIn applies the In predicate on the ID field.
func IDIn(ids ...int) predicate.Sku {
return predicate.Sku(sql.FieldIn(FieldID, ids...))
}
// IDNotIn applies the NotIn predicate on the ID field.
func IDNotIn(ids ...int) predicate.Sku {
return predicate.Sku(sql.FieldNotIn(FieldID, ids...))
}
// IDGT applies the GT predicate on the ID field.
func IDGT(id int) predicate.Sku {
return predicate.Sku(sql.FieldGT(FieldID, id))
}
// IDGTE applies the GTE predicate on the ID field.
func IDGTE(id int) predicate.Sku {
return predicate.Sku(sql.FieldGTE(FieldID, id))
}
// IDLT applies the LT predicate on the ID field.
func IDLT(id int) predicate.Sku {
return predicate.Sku(sql.FieldLT(FieldID, id))
}
// IDLTE applies the LTE predicate on the ID field.
func IDLTE(id int) predicate.Sku {
return predicate.Sku(sql.FieldLTE(FieldID, id))
}
// CreatedAt applies equality check predicate on the "created_at" field. It's identical to CreatedAtEQ.
func CreatedAt(v time.Time) predicate.Sku {
return predicate.Sku(sql.FieldEQ(FieldCreatedAt, v))
}
// UpdatedAt applies equality check predicate on the "updated_at" field. It's identical to UpdatedAtEQ.
func UpdatedAt(v time.Time) predicate.Sku {
return predicate.Sku(sql.FieldEQ(FieldUpdatedAt, v))
}
// DeletedAt applies equality check predicate on the "deleted_at" field. It's identical to DeletedAtEQ.
func DeletedAt(v time.Time) predicate.Sku {
return predicate.Sku(sql.FieldEQ(FieldDeletedAt, v))
}
// Name applies equality check predicate on the "name" field. It's identical to NameEQ.
func Name(v string) predicate.Sku {
return predicate.Sku(sql.FieldEQ(FieldName, v))
}
// Amount applies equality check predicate on the "amount" field. It's identical to AmountEQ.
func Amount(v int64) predicate.Sku {
return predicate.Sku(sql.FieldEQ(FieldAmount, v))
}
// Duration applies equality check predicate on the "duration" field. It's identical to DurationEQ.
func Duration(v int64) predicate.Sku {
return predicate.Sku(sql.FieldEQ(FieldDuration, v))
}
// Price applies equality check predicate on the "price" field. It's identical to PriceEQ.
func Price(v int64) predicate.Sku {
return predicate.Sku(sql.FieldEQ(FieldPrice, v))
}
// Points applies equality check predicate on the "points" field. It's identical to PointsEQ.
func Points(v int64) predicate.Sku {
return predicate.Sku(sql.FieldEQ(FieldPoints, v))
}
// Des applies equality check predicate on the "des" field. It's identical to DesEQ.
func Des(v string) predicate.Sku {
return predicate.Sku(sql.FieldEQ(FieldDes, v))
}
// Enabled applies equality check predicate on the "enabled" field. It's identical to EnabledEQ.
func Enabled(v bool) predicate.Sku {
return predicate.Sku(sql.FieldEQ(FieldEnabled, v))
}
// Weight applies equality check predicate on the "weight" field. It's identical to WeightEQ.
func Weight(v int) predicate.Sku {
return predicate.Sku(sql.FieldEQ(FieldWeight, v))
}
// CreatedAtEQ applies the EQ predicate on the "created_at" field.
func CreatedAtEQ(v time.Time) predicate.Sku {
return predicate.Sku(sql.FieldEQ(FieldCreatedAt, v))
}
// CreatedAtNEQ applies the NEQ predicate on the "created_at" field.
func CreatedAtNEQ(v time.Time) predicate.Sku {
return predicate.Sku(sql.FieldNEQ(FieldCreatedAt, v))
}
// CreatedAtIn applies the In predicate on the "created_at" field.
func CreatedAtIn(vs ...time.Time) predicate.Sku {
return predicate.Sku(sql.FieldIn(FieldCreatedAt, vs...))
}
// CreatedAtNotIn applies the NotIn predicate on the "created_at" field.
func CreatedAtNotIn(vs ...time.Time) predicate.Sku {
return predicate.Sku(sql.FieldNotIn(FieldCreatedAt, vs...))
}
// CreatedAtGT applies the GT predicate on the "created_at" field.
func CreatedAtGT(v time.Time) predicate.Sku {
return predicate.Sku(sql.FieldGT(FieldCreatedAt, v))
}
// CreatedAtGTE applies the GTE predicate on the "created_at" field.
func CreatedAtGTE(v time.Time) predicate.Sku {
return predicate.Sku(sql.FieldGTE(FieldCreatedAt, v))
}
// CreatedAtLT applies the LT predicate on the "created_at" field.
func CreatedAtLT(v time.Time) predicate.Sku {
return predicate.Sku(sql.FieldLT(FieldCreatedAt, v))
}
// CreatedAtLTE applies the LTE predicate on the "created_at" field.
func CreatedAtLTE(v time.Time) predicate.Sku {
return predicate.Sku(sql.FieldLTE(FieldCreatedAt, v))
}
// UpdatedAtEQ applies the EQ predicate on the "updated_at" field.
func UpdatedAtEQ(v time.Time) predicate.Sku {
return predicate.Sku(sql.FieldEQ(FieldUpdatedAt, v))
}
// UpdatedAtNEQ applies the NEQ predicate on the "updated_at" field.
func UpdatedAtNEQ(v time.Time) predicate.Sku {
return predicate.Sku(sql.FieldNEQ(FieldUpdatedAt, v))
}
// UpdatedAtIn applies the In predicate on the "updated_at" field.
func UpdatedAtIn(vs ...time.Time) predicate.Sku {
return predicate.Sku(sql.FieldIn(FieldUpdatedAt, vs...))
}
// UpdatedAtNotIn applies the NotIn predicate on the "updated_at" field.
func UpdatedAtNotIn(vs ...time.Time) predicate.Sku {
return predicate.Sku(sql.FieldNotIn(FieldUpdatedAt, vs...))
}
// UpdatedAtGT applies the GT predicate on the "updated_at" field.
func UpdatedAtGT(v time.Time) predicate.Sku {
return predicate.Sku(sql.FieldGT(FieldUpdatedAt, v))
}
// UpdatedAtGTE applies the GTE predicate on the "updated_at" field.
func UpdatedAtGTE(v time.Time) predicate.Sku {
return predicate.Sku(sql.FieldGTE(FieldUpdatedAt, v))
}
// UpdatedAtLT applies the LT predicate on the "updated_at" field.
func UpdatedAtLT(v time.Time) predicate.Sku {
return predicate.Sku(sql.FieldLT(FieldUpdatedAt, v))
}
// UpdatedAtLTE applies the LTE predicate on the "updated_at" field.
func UpdatedAtLTE(v time.Time) predicate.Sku {
return predicate.Sku(sql.FieldLTE(FieldUpdatedAt, v))
}
// DeletedAtEQ applies the EQ predicate on the "deleted_at" field.
func DeletedAtEQ(v time.Time) predicate.Sku {
return predicate.Sku(sql.FieldEQ(FieldDeletedAt, v))
}
// DeletedAtNEQ applies the NEQ predicate on the "deleted_at" field.
func DeletedAtNEQ(v time.Time) predicate.Sku {
return predicate.Sku(sql.FieldNEQ(FieldDeletedAt, v))
}
// DeletedAtIn applies the In predicate on the "deleted_at" field.
func DeletedAtIn(vs ...time.Time) predicate.Sku {
return predicate.Sku(sql.FieldIn(FieldDeletedAt, vs...))
}
// DeletedAtNotIn applies the NotIn predicate on the "deleted_at" field.
func DeletedAtNotIn(vs ...time.Time) predicate.Sku {
return predicate.Sku(sql.FieldNotIn(FieldDeletedAt, vs...))
}
// DeletedAtGT applies the GT predicate on the "deleted_at" field.
func DeletedAtGT(v time.Time) predicate.Sku {
return predicate.Sku(sql.FieldGT(FieldDeletedAt, v))
}
// DeletedAtGTE applies the GTE predicate on the "deleted_at" field.
func DeletedAtGTE(v time.Time) predicate.Sku {
return predicate.Sku(sql.FieldGTE(FieldDeletedAt, v))
}
// DeletedAtLT applies the LT predicate on the "deleted_at" field.
func DeletedAtLT(v time.Time) predicate.Sku {
return predicate.Sku(sql.FieldLT(FieldDeletedAt, v))
}
// DeletedAtLTE applies the LTE predicate on the "deleted_at" field.
func DeletedAtLTE(v time.Time) predicate.Sku {
return predicate.Sku(sql.FieldLTE(FieldDeletedAt, v))
}
// DeletedAtIsNil applies the IsNil predicate on the "deleted_at" field.
func DeletedAtIsNil() predicate.Sku {
return predicate.Sku(sql.FieldIsNull(FieldDeletedAt))
}
// DeletedAtNotNil applies the NotNil predicate on the "deleted_at" field.
func DeletedAtNotNil() predicate.Sku {
return predicate.Sku(sql.FieldNotNull(FieldDeletedAt))
}
// NameEQ applies the EQ predicate on the "name" field.
func NameEQ(v string) predicate.Sku {
return predicate.Sku(sql.FieldEQ(FieldName, v))
}
// NameNEQ applies the NEQ predicate on the "name" field.
func NameNEQ(v string) predicate.Sku {
return predicate.Sku(sql.FieldNEQ(FieldName, v))
}
// NameIn applies the In predicate on the "name" field.
func NameIn(vs ...string) predicate.Sku {
return predicate.Sku(sql.FieldIn(FieldName, vs...))
}
// NameNotIn applies the NotIn predicate on the "name" field.
func NameNotIn(vs ...string) predicate.Sku {
return predicate.Sku(sql.FieldNotIn(FieldName, vs...))
}
// NameGT applies the GT predicate on the "name" field.
func NameGT(v string) predicate.Sku {
return predicate.Sku(sql.FieldGT(FieldName, v))
}
// NameGTE applies the GTE predicate on the "name" field.
func NameGTE(v string) predicate.Sku {
return predicate.Sku(sql.FieldGTE(FieldName, v))
}
// NameLT applies the LT predicate on the "name" field.
func NameLT(v string) predicate.Sku {
return predicate.Sku(sql.FieldLT(FieldName, v))
}
// NameLTE applies the LTE predicate on the "name" field.
func NameLTE(v string) predicate.Sku {
return predicate.Sku(sql.FieldLTE(FieldName, v))
}
// NameContains applies the Contains predicate on the "name" field.
func NameContains(v string) predicate.Sku {
return predicate.Sku(sql.FieldContains(FieldName, v))
}
// NameHasPrefix applies the HasPrefix predicate on the "name" field.
func NameHasPrefix(v string) predicate.Sku {
return predicate.Sku(sql.FieldHasPrefix(FieldName, v))
}
// NameHasSuffix applies the HasSuffix predicate on the "name" field.
func NameHasSuffix(v string) predicate.Sku {
return predicate.Sku(sql.FieldHasSuffix(FieldName, v))
}
// NameEqualFold applies the EqualFold predicate on the "name" field.
func NameEqualFold(v string) predicate.Sku {
return predicate.Sku(sql.FieldEqualFold(FieldName, v))
}
// NameContainsFold applies the ContainsFold predicate on the "name" field.
func NameContainsFold(v string) predicate.Sku {
return predicate.Sku(sql.FieldContainsFold(FieldName, v))
}
// TypeEQ applies the EQ predicate on the "type" field.
func TypeEQ(v Type) predicate.Sku {
return predicate.Sku(sql.FieldEQ(FieldType, v))
}
// TypeNEQ applies the NEQ predicate on the "type" field.
func TypeNEQ(v Type) predicate.Sku {
return predicate.Sku(sql.FieldNEQ(FieldType, v))
}
// TypeIn applies the In predicate on the "type" field.
func TypeIn(vs ...Type) predicate.Sku {
return predicate.Sku(sql.FieldIn(FieldType, vs...))
}
// TypeNotIn applies the NotIn predicate on the "type" field.
func TypeNotIn(vs ...Type) predicate.Sku {
return predicate.Sku(sql.FieldNotIn(FieldType, vs...))
}
// AmountEQ applies the EQ predicate on the "amount" field.
func AmountEQ(v int64) predicate.Sku {
return predicate.Sku(sql.FieldEQ(FieldAmount, v))
}
// AmountNEQ applies the NEQ predicate on the "amount" field.
func AmountNEQ(v int64) predicate.Sku {
return predicate.Sku(sql.FieldNEQ(FieldAmount, v))
}
// AmountIn applies the In predicate on the "amount" field.
func AmountIn(vs ...int64) predicate.Sku {
return predicate.Sku(sql.FieldIn(FieldAmount, vs...))
}
// AmountNotIn applies the NotIn predicate on the "amount" field.
func AmountNotIn(vs ...int64) predicate.Sku {
return predicate.Sku(sql.FieldNotIn(FieldAmount, vs...))
}
// AmountGT applies the GT predicate on the "amount" field.
func AmountGT(v int64) predicate.Sku {
return predicate.Sku(sql.FieldGT(FieldAmount, v))
}
// AmountGTE applies the GTE predicate on the "amount" field.
func AmountGTE(v int64) predicate.Sku {
return predicate.Sku(sql.FieldGTE(FieldAmount, v))
}
// AmountLT applies the LT predicate on the "amount" field.
func AmountLT(v int64) predicate.Sku {
return predicate.Sku(sql.FieldLT(FieldAmount, v))
}
// AmountLTE applies the LTE predicate on the "amount" field.
func AmountLTE(v int64) predicate.Sku {
return predicate.Sku(sql.FieldLTE(FieldAmount, v))
}
// DurationEQ applies the EQ predicate on the "duration" field.
func DurationEQ(v int64) predicate.Sku {
return predicate.Sku(sql.FieldEQ(FieldDuration, v))
}
// DurationNEQ applies the NEQ predicate on the "duration" field.
func DurationNEQ(v int64) predicate.Sku {
return predicate.Sku(sql.FieldNEQ(FieldDuration, v))
}
// DurationIn applies the In predicate on the "duration" field.
func DurationIn(vs ...int64) predicate.Sku {
return predicate.Sku(sql.FieldIn(FieldDuration, vs...))
}
// DurationNotIn applies the NotIn predicate on the "duration" field.
func DurationNotIn(vs ...int64) predicate.Sku {
return predicate.Sku(sql.FieldNotIn(FieldDuration, vs...))
}
// DurationGT applies the GT predicate on the "duration" field.
func DurationGT(v int64) predicate.Sku {
return predicate.Sku(sql.FieldGT(FieldDuration, v))
}
// DurationGTE applies the GTE predicate on the "duration" field.
func DurationGTE(v int64) predicate.Sku {
return predicate.Sku(sql.FieldGTE(FieldDuration, v))
}
// DurationLT applies the LT predicate on the "duration" field.
func DurationLT(v int64) predicate.Sku {
return predicate.Sku(sql.FieldLT(FieldDuration, v))
}
// DurationLTE applies the LTE predicate on the "duration" field.
func DurationLTE(v int64) predicate.Sku {
return predicate.Sku(sql.FieldLTE(FieldDuration, v))
}
// DurationIsNil applies the IsNil predicate on the "duration" field.
func DurationIsNil() predicate.Sku {
return predicate.Sku(sql.FieldIsNull(FieldDuration))
}
// DurationNotNil applies the NotNil predicate on the "duration" field.
func DurationNotNil() predicate.Sku {
return predicate.Sku(sql.FieldNotNull(FieldDuration))
}
// PriceEQ applies the EQ predicate on the "price" field.
func PriceEQ(v int64) predicate.Sku {
return predicate.Sku(sql.FieldEQ(FieldPrice, v))
}
// PriceNEQ applies the NEQ predicate on the "price" field.
func PriceNEQ(v int64) predicate.Sku {
return predicate.Sku(sql.FieldNEQ(FieldPrice, v))
}
// PriceIn applies the In predicate on the "price" field.
func PriceIn(vs ...int64) predicate.Sku {
return predicate.Sku(sql.FieldIn(FieldPrice, vs...))
}
// PriceNotIn applies the NotIn predicate on the "price" field.
func PriceNotIn(vs ...int64) predicate.Sku {
return predicate.Sku(sql.FieldNotIn(FieldPrice, vs...))
}
// PriceGT applies the GT predicate on the "price" field.
func PriceGT(v int64) predicate.Sku {
return predicate.Sku(sql.FieldGT(FieldPrice, v))
}
// PriceGTE applies the GTE predicate on the "price" field.
func PriceGTE(v int64) predicate.Sku {
return predicate.Sku(sql.FieldGTE(FieldPrice, v))
}
// PriceLT applies the LT predicate on the "price" field.
func PriceLT(v int64) predicate.Sku {
return predicate.Sku(sql.FieldLT(FieldPrice, v))
}
// PriceLTE applies the LTE predicate on the "price" field.
func PriceLTE(v int64) predicate.Sku {
return predicate.Sku(sql.FieldLTE(FieldPrice, v))
}
// PriceIsNil applies the IsNil predicate on the "price" field.
func PriceIsNil() predicate.Sku {
return predicate.Sku(sql.FieldIsNull(FieldPrice))
}
// PriceNotNil applies the NotNil predicate on the "price" field.
func PriceNotNil() predicate.Sku {
return predicate.Sku(sql.FieldNotNull(FieldPrice))
}
// PointsEQ applies the EQ predicate on the "points" field.
func PointsEQ(v int64) predicate.Sku {
return predicate.Sku(sql.FieldEQ(FieldPoints, v))
}
// PointsNEQ applies the NEQ predicate on the "points" field.
func PointsNEQ(v int64) predicate.Sku {
return predicate.Sku(sql.FieldNEQ(FieldPoints, v))
}
// PointsIn applies the In predicate on the "points" field.
func PointsIn(vs ...int64) predicate.Sku {
return predicate.Sku(sql.FieldIn(FieldPoints, vs...))
}
// PointsNotIn applies the NotIn predicate on the "points" field.
func PointsNotIn(vs ...int64) predicate.Sku {
return predicate.Sku(sql.FieldNotIn(FieldPoints, vs...))
}
// PointsGT applies the GT predicate on the "points" field.
func PointsGT(v int64) predicate.Sku {
return predicate.Sku(sql.FieldGT(FieldPoints, v))
}
// PointsGTE applies the GTE predicate on the "points" field.
func PointsGTE(v int64) predicate.Sku {
return predicate.Sku(sql.FieldGTE(FieldPoints, v))
}
// PointsLT applies the LT predicate on the "points" field.
func PointsLT(v int64) predicate.Sku {
return predicate.Sku(sql.FieldLT(FieldPoints, v))
}
// PointsLTE applies the LTE predicate on the "points" field.
func PointsLTE(v int64) predicate.Sku {
return predicate.Sku(sql.FieldLTE(FieldPoints, v))
}
// PointsIsNil applies the IsNil predicate on the "points" field.
func PointsIsNil() predicate.Sku {
return predicate.Sku(sql.FieldIsNull(FieldPoints))
}
// PointsNotNil applies the NotNil predicate on the "points" field.
func PointsNotNil() predicate.Sku {
return predicate.Sku(sql.FieldNotNull(FieldPoints))
}
// LabelEQ applies the EQ predicate on the "label" field.
func LabelEQ(v string) predicate.Sku {
return predicate.Sku(sql.FieldEQ(FieldLabel, v))
}
// LabelNEQ applies the NEQ predicate on the "label" field.
func LabelNEQ(v string) predicate.Sku {
return predicate.Sku(sql.FieldNEQ(FieldLabel, v))
}
// LabelIn applies the In predicate on the "label" field.
func LabelIn(vs ...string) predicate.Sku {
return predicate.Sku(sql.FieldIn(FieldLabel, vs...))
}
// LabelNotIn applies the NotIn predicate on the "label" field.
func LabelNotIn(vs ...string) predicate.Sku {
return predicate.Sku(sql.FieldNotIn(FieldLabel, vs...))
}
// LabelGT applies the GT predicate on the "label" field.
func LabelGT(v string) predicate.Sku {
return predicate.Sku(sql.FieldGT(FieldLabel, v))
}
// LabelGTE applies the GTE predicate on the "label" field.
func LabelGTE(v string) predicate.Sku {
return predicate.Sku(sql.FieldGTE(FieldLabel, v))
}
// LabelLT applies the LT predicate on the "label" field.
func LabelLT(v string) predicate.Sku {
return predicate.Sku(sql.FieldLT(FieldLabel, v))
}
// LabelLTE applies the LTE predicate on the "label" field.
func LabelLTE(v string) predicate.Sku {
return predicate.Sku(sql.FieldLTE(FieldLabel, v))
}
// LabelContains applies the Contains predicate on the "label" field.
func LabelContains(v string) predicate.Sku {
return predicate.Sku(sql.FieldContains(FieldLabel, v))
}
// LabelHasPrefix applies the HasPrefix predicate on the "label" field.
func LabelHasPrefix(v string) predicate.Sku {
return predicate.Sku(sql.FieldHasPrefix(FieldLabel, v))
}
// LabelHasSuffix applies the HasSuffix predicate on the "label" field.
func LabelHasSuffix(v string) predicate.Sku {
return predicate.Sku(sql.FieldHasSuffix(FieldLabel, v))
}
// LabelIsNil applies the IsNil predicate on the "label" field.
func LabelIsNil() predicate.Sku {
return predicate.Sku(sql.FieldIsNull(FieldLabel))
}
// LabelNotNil applies the NotNil predicate on the "label" field.
func LabelNotNil() predicate.Sku {
return predicate.Sku(sql.FieldNotNull(FieldLabel))
}
// LabelEqualFold applies the EqualFold predicate on the "label" field.
func LabelEqualFold(v string) predicate.Sku {
return predicate.Sku(sql.FieldEqualFold(FieldLabel, v))
}
// LabelContainsFold applies the ContainsFold predicate on the "label" field.
func LabelContainsFold(v string) predicate.Sku {
return predicate.Sku(sql.FieldContainsFold(FieldLabel, v))
}
// DesEQ applies the EQ predicate on the "des" field.
func DesEQ(v string) predicate.Sku {
return predicate.Sku(sql.FieldEQ(FieldDes, v))
}
// DesNEQ applies the NEQ predicate on the "des" field.
func DesNEQ(v string) predicate.Sku {
return predicate.Sku(sql.FieldNEQ(FieldDes, v))
}
// DesIn applies the In predicate on the "des" field.
func DesIn(vs ...string) predicate.Sku {
return predicate.Sku(sql.FieldIn(FieldDes, vs...))
}
// DesNotIn applies the NotIn predicate on the "des" field.
func DesNotIn(vs ...string) predicate.Sku {
return predicate.Sku(sql.FieldNotIn(FieldDes, vs...))
}
// DesGT applies the GT predicate on the "des" field.
func DesGT(v string) predicate.Sku {
return predicate.Sku(sql.FieldGT(FieldDes, v))
}
// DesGTE applies the GTE predicate on the "des" field.
func DesGTE(v string) predicate.Sku {
return predicate.Sku(sql.FieldGTE(FieldDes, v))
}
// DesLT applies the LT predicate on the "des" field.
func DesLT(v string) predicate.Sku {
return predicate.Sku(sql.FieldLT(FieldDes, v))
}
// DesLTE applies the LTE predicate on the "des" field.
func DesLTE(v string) predicate.Sku {
return predicate.Sku(sql.FieldLTE(FieldDes, v))
}
// DesContains applies the Contains predicate on the "des" field.
func DesContains(v string) predicate.Sku {
return predicate.Sku(sql.FieldContains(FieldDes, v))
}
// DesHasPrefix applies the HasPrefix predicate on the "des" field.
func DesHasPrefix(v string) predicate.Sku {
return predicate.Sku(sql.FieldHasPrefix(FieldDes, v))
}
// DesHasSuffix applies the HasSuffix predicate on the "des" field.
func DesHasSuffix(v string) predicate.Sku {
return predicate.Sku(sql.FieldHasSuffix(FieldDes, v))
}
// DesIsNil applies the IsNil predicate on the "des" field.
func DesIsNil() predicate.Sku {
return predicate.Sku(sql.FieldIsNull(FieldDes))
}
// DesNotNil applies the NotNil predicate on the "des" field.
func DesNotNil() predicate.Sku {
return predicate.Sku(sql.FieldNotNull(FieldDes))
}
// DesEqualFold applies the EqualFold predicate on the "des" field.
func DesEqualFold(v string) predicate.Sku {
return predicate.Sku(sql.FieldEqualFold(FieldDes, v))
}
// DesContainsFold applies the ContainsFold predicate on the "des" field.
func DesContainsFold(v string) predicate.Sku {
return predicate.Sku(sql.FieldContainsFold(FieldDes, v))
}
// EnabledEQ applies the EQ predicate on the "enabled" field.
func EnabledEQ(v bool) predicate.Sku {
return predicate.Sku(sql.FieldEQ(FieldEnabled, v))
}
// EnabledNEQ applies the NEQ predicate on the "enabled" field.
func EnabledNEQ(v bool) predicate.Sku {
return predicate.Sku(sql.FieldNEQ(FieldEnabled, v))
}
// WeightEQ applies the EQ predicate on the "weight" field.
func WeightEQ(v int) predicate.Sku {
return predicate.Sku(sql.FieldEQ(FieldWeight, v))
}
// WeightNEQ applies the NEQ predicate on the "weight" field.
func WeightNEQ(v int) predicate.Sku {
return predicate.Sku(sql.FieldNEQ(FieldWeight, v))
}
// WeightIn applies the In predicate on the "weight" field.
func WeightIn(vs ...int) predicate.Sku {
return predicate.Sku(sql.FieldIn(FieldWeight, vs...))
}
// WeightNotIn applies the NotIn predicate on the "weight" field.
func WeightNotIn(vs ...int) predicate.Sku {
return predicate.Sku(sql.FieldNotIn(FieldWeight, vs...))
}
// WeightGT applies the GT predicate on the "weight" field.
func WeightGT(v int) predicate.Sku {
return predicate.Sku(sql.FieldGT(FieldWeight, v))
}
// WeightGTE applies the GTE predicate on the "weight" field.
func WeightGTE(v int) predicate.Sku {
return predicate.Sku(sql.FieldGTE(FieldWeight, v))
}
// WeightLT applies the LT predicate on the "weight" field.
func WeightLT(v int) predicate.Sku {
return predicate.Sku(sql.FieldLT(FieldWeight, v))
}
// WeightLTE applies the LTE predicate on the "weight" field.
func WeightLTE(v int) predicate.Sku {
return predicate.Sku(sql.FieldLTE(FieldWeight, v))
}
// And groups predicates with the AND operator between them.
func And(predicates ...predicate.Sku) predicate.Sku {
return predicate.Sku(sql.AndPredicates(predicates...))
}
// Or groups predicates with the OR operator between them.
func Or(predicates ...predicate.Sku) predicate.Sku {
return predicate.Sku(sql.OrPredicates(predicates...))
}
// Not applies the not operator on the given predicate.
func Not(p predicate.Sku) predicate.Sku {
return predicate.Sku(sql.NotPredicates(p))
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,88 @@
// Code generated by ent, DO NOT EDIT.
package ent
import (
"context"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
"github.com/cloudreve/Cloudreve/v4/ent/predicate"
"github.com/cloudreve/Cloudreve/v4/ent/sku"
)
// SkuDelete is the builder for deleting a Sku entity.
type SkuDelete struct {
config
hooks []Hook
mutation *SkuMutation
}
// Where appends a list predicates to the SkuDelete builder.
func (sd *SkuDelete) Where(ps ...predicate.Sku) *SkuDelete {
sd.mutation.Where(ps...)
return sd
}
// Exec executes the deletion query and returns how many vertices were deleted.
func (sd *SkuDelete) Exec(ctx context.Context) (int, error) {
return withHooks(ctx, sd.sqlExec, sd.mutation, sd.hooks)
}
// ExecX is like Exec, but panics if an error occurs.
func (sd *SkuDelete) ExecX(ctx context.Context) int {
n, err := sd.Exec(ctx)
if err != nil {
panic(err)
}
return n
}
func (sd *SkuDelete) sqlExec(ctx context.Context) (int, error) {
_spec := sqlgraph.NewDeleteSpec(sku.Table, sqlgraph.NewFieldSpec(sku.FieldID, field.TypeInt))
if ps := sd.mutation.predicates; len(ps) > 0 {
_spec.Predicate = func(selector *sql.Selector) {
for i := range ps {
ps[i](selector)
}
}
}
affected, err := sqlgraph.DeleteNodes(ctx, sd.driver, _spec)
if err != nil && sqlgraph.IsConstraintError(err) {
err = &ConstraintError{msg: err.Error(), wrap: err}
}
sd.mutation.done = true
return affected, err
}
// SkuDeleteOne is the builder for deleting a single Sku entity.
type SkuDeleteOne struct {
sd *SkuDelete
}
// Where appends a list predicates to the SkuDelete builder.
func (sdo *SkuDeleteOne) Where(ps ...predicate.Sku) *SkuDeleteOne {
sdo.sd.mutation.Where(ps...)
return sdo
}
// Exec executes the deletion query.
func (sdo *SkuDeleteOne) Exec(ctx context.Context) error {
n, err := sdo.sd.Exec(ctx)
switch {
case err != nil:
return err
case n == 0:
return &NotFoundError{sku.Label}
default:
return nil
}
}
// ExecX is like Exec, but panics if an error occurs.
func (sdo *SkuDeleteOne) ExecX(ctx context.Context) {
if err := sdo.Exec(ctx); err != nil {
panic(err)
}
}

@ -0,0 +1,526 @@
// Code generated by ent, DO NOT EDIT.
package ent
import (
"context"
"fmt"
"math"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
"github.com/cloudreve/Cloudreve/v4/ent/predicate"
"github.com/cloudreve/Cloudreve/v4/ent/sku"
)
// SkuQuery is the builder for querying Sku entities.
type SkuQuery struct {
config
ctx *QueryContext
order []sku.OrderOption
inters []Interceptor
predicates []predicate.Sku
// intermediate query (i.e. traversal path).
sql *sql.Selector
path func(context.Context) (*sql.Selector, error)
}
// Where adds a new predicate for the SkuQuery builder.
func (sq *SkuQuery) Where(ps ...predicate.Sku) *SkuQuery {
sq.predicates = append(sq.predicates, ps...)
return sq
}
// Limit the number of records to be returned by this query.
func (sq *SkuQuery) Limit(limit int) *SkuQuery {
sq.ctx.Limit = &limit
return sq
}
// Offset to start from.
func (sq *SkuQuery) Offset(offset int) *SkuQuery {
sq.ctx.Offset = &offset
return sq
}
// Unique configures the query builder to filter duplicate records on query.
// By default, unique is set to true, and can be disabled using this method.
func (sq *SkuQuery) Unique(unique bool) *SkuQuery {
sq.ctx.Unique = &unique
return sq
}
// Order specifies how the records should be ordered.
func (sq *SkuQuery) Order(o ...sku.OrderOption) *SkuQuery {
sq.order = append(sq.order, o...)
return sq
}
// First returns the first Sku entity from the query.
// Returns a *NotFoundError when no Sku was found.
func (sq *SkuQuery) First(ctx context.Context) (*Sku, error) {
nodes, err := sq.Limit(1).All(setContextOp(ctx, sq.ctx, "First"))
if err != nil {
return nil, err
}
if len(nodes) == 0 {
return nil, &NotFoundError{sku.Label}
}
return nodes[0], nil
}
// FirstX is like First, but panics if an error occurs.
func (sq *SkuQuery) FirstX(ctx context.Context) *Sku {
node, err := sq.First(ctx)
if err != nil && !IsNotFound(err) {
panic(err)
}
return node
}
// FirstID returns the first Sku ID from the query.
// Returns a *NotFoundError when no Sku ID was found.
func (sq *SkuQuery) FirstID(ctx context.Context) (id int, err error) {
var ids []int
if ids, err = sq.Limit(1).IDs(setContextOp(ctx, sq.ctx, "FirstID")); err != nil {
return
}
if len(ids) == 0 {
err = &NotFoundError{sku.Label}
return
}
return ids[0], nil
}
// FirstIDX is like FirstID, but panics if an error occurs.
func (sq *SkuQuery) FirstIDX(ctx context.Context) int {
id, err := sq.FirstID(ctx)
if err != nil && !IsNotFound(err) {
panic(err)
}
return id
}
// Only returns a single Sku entity found by the query, ensuring it only returns one.
// Returns a *NotSingularError when more than one Sku entity is found.
// Returns a *NotFoundError when no Sku entities are found.
func (sq *SkuQuery) Only(ctx context.Context) (*Sku, error) {
nodes, err := sq.Limit(2).All(setContextOp(ctx, sq.ctx, "Only"))
if err != nil {
return nil, err
}
switch len(nodes) {
case 1:
return nodes[0], nil
case 0:
return nil, &NotFoundError{sku.Label}
default:
return nil, &NotSingularError{sku.Label}
}
}
// OnlyX is like Only, but panics if an error occurs.
func (sq *SkuQuery) OnlyX(ctx context.Context) *Sku {
node, err := sq.Only(ctx)
if err != nil {
panic(err)
}
return node
}
// OnlyID is like Only, but returns the only Sku ID in the query.
// Returns a *NotSingularError when more than one Sku ID is found.
// Returns a *NotFoundError when no entities are found.
func (sq *SkuQuery) OnlyID(ctx context.Context) (id int, err error) {
var ids []int
if ids, err = sq.Limit(2).IDs(setContextOp(ctx, sq.ctx, "OnlyID")); err != nil {
return
}
switch len(ids) {
case 1:
id = ids[0]
case 0:
err = &NotFoundError{sku.Label}
default:
err = &NotSingularError{sku.Label}
}
return
}
// OnlyIDX is like OnlyID, but panics if an error occurs.
func (sq *SkuQuery) OnlyIDX(ctx context.Context) int {
id, err := sq.OnlyID(ctx)
if err != nil {
panic(err)
}
return id
}
// All executes the query and returns a list of Skus.
func (sq *SkuQuery) All(ctx context.Context) ([]*Sku, error) {
ctx = setContextOp(ctx, sq.ctx, "All")
if err := sq.prepareQuery(ctx); err != nil {
return nil, err
}
qr := querierAll[[]*Sku, *SkuQuery]()
return withInterceptors[[]*Sku](ctx, sq, qr, sq.inters)
}
// AllX is like All, but panics if an error occurs.
func (sq *SkuQuery) AllX(ctx context.Context) []*Sku {
nodes, err := sq.All(ctx)
if err != nil {
panic(err)
}
return nodes
}
// IDs executes the query and returns a list of Sku IDs.
func (sq *SkuQuery) IDs(ctx context.Context) (ids []int, err error) {
if sq.ctx.Unique == nil && sq.path != nil {
sq.Unique(true)
}
ctx = setContextOp(ctx, sq.ctx, "IDs")
if err = sq.Select(sku.FieldID).Scan(ctx, &ids); err != nil {
return nil, err
}
return ids, nil
}
// IDsX is like IDs, but panics if an error occurs.
func (sq *SkuQuery) IDsX(ctx context.Context) []int {
ids, err := sq.IDs(ctx)
if err != nil {
panic(err)
}
return ids
}
// Count returns the count of the given query.
func (sq *SkuQuery) Count(ctx context.Context) (int, error) {
ctx = setContextOp(ctx, sq.ctx, "Count")
if err := sq.prepareQuery(ctx); err != nil {
return 0, err
}
return withInterceptors[int](ctx, sq, querierCount[*SkuQuery](), sq.inters)
}
// CountX is like Count, but panics if an error occurs.
func (sq *SkuQuery) CountX(ctx context.Context) int {
count, err := sq.Count(ctx)
if err != nil {
panic(err)
}
return count
}
// Exist returns true if the query has elements in the graph.
func (sq *SkuQuery) Exist(ctx context.Context) (bool, error) {
ctx = setContextOp(ctx, sq.ctx, "Exist")
switch _, err := sq.FirstID(ctx); {
case IsNotFound(err):
return false, nil
case err != nil:
return false, fmt.Errorf("ent: check existence: %w", err)
default:
return true, nil
}
}
// ExistX is like Exist, but panics if an error occurs.
func (sq *SkuQuery) ExistX(ctx context.Context) bool {
exist, err := sq.Exist(ctx)
if err != nil {
panic(err)
}
return exist
}
// Clone returns a duplicate of the SkuQuery builder, including all associated steps. It can be
// used to prepare common query builders and use them differently after the clone is made.
func (sq *SkuQuery) Clone() *SkuQuery {
if sq == nil {
return nil
}
return &SkuQuery{
config: sq.config,
ctx: sq.ctx.Clone(),
order: append([]sku.OrderOption{}, sq.order...),
inters: append([]Interceptor{}, sq.inters...),
predicates: append([]predicate.Sku{}, sq.predicates...),
// clone intermediate query.
sql: sq.sql.Clone(),
path: sq.path,
}
}
// GroupBy is used to group vertices by one or more fields/columns.
// It is often used with aggregate functions, like: count, max, mean, min, sum.
//
// Example:
//
// var v []struct {
// CreatedAt time.Time `json:"created_at,omitempty"`
// Count int `json:"count,omitempty"`
// }
//
// client.Sku.Query().
// GroupBy(sku.FieldCreatedAt).
// Aggregate(ent.Count()).
// Scan(ctx, &v)
func (sq *SkuQuery) GroupBy(field string, fields ...string) *SkuGroupBy {
sq.ctx.Fields = append([]string{field}, fields...)
grbuild := &SkuGroupBy{build: sq}
grbuild.flds = &sq.ctx.Fields
grbuild.label = sku.Label
grbuild.scan = grbuild.Scan
return grbuild
}
// Select allows the selection one or more fields/columns for the given query,
// instead of selecting all fields in the entity.
//
// Example:
//
// var v []struct {
// CreatedAt time.Time `json:"created_at,omitempty"`
// }
//
// client.Sku.Query().
// Select(sku.FieldCreatedAt).
// Scan(ctx, &v)
func (sq *SkuQuery) Select(fields ...string) *SkuSelect {
sq.ctx.Fields = append(sq.ctx.Fields, fields...)
sbuild := &SkuSelect{SkuQuery: sq}
sbuild.label = sku.Label
sbuild.flds, sbuild.scan = &sq.ctx.Fields, sbuild.Scan
return sbuild
}
// Aggregate returns a SkuSelect configured with the given aggregations.
func (sq *SkuQuery) Aggregate(fns ...AggregateFunc) *SkuSelect {
return sq.Select().Aggregate(fns...)
}
func (sq *SkuQuery) prepareQuery(ctx context.Context) error {
for _, inter := range sq.inters {
if inter == nil {
return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)")
}
if trv, ok := inter.(Traverser); ok {
if err := trv.Traverse(ctx, sq); err != nil {
return err
}
}
}
for _, f := range sq.ctx.Fields {
if !sku.ValidColumn(f) {
return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)}
}
}
if sq.path != nil {
prev, err := sq.path(ctx)
if err != nil {
return err
}
sq.sql = prev
}
return nil
}
func (sq *SkuQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Sku, error) {
var (
nodes = []*Sku{}
_spec = sq.querySpec()
)
_spec.ScanValues = func(columns []string) ([]any, error) {
return (*Sku).scanValues(nil, columns)
}
_spec.Assign = func(columns []string, values []any) error {
node := &Sku{config: sq.config}
nodes = append(nodes, node)
return node.assignValues(columns, values)
}
for i := range hooks {
hooks[i](ctx, _spec)
}
if err := sqlgraph.QueryNodes(ctx, sq.driver, _spec); err != nil {
return nil, err
}
if len(nodes) == 0 {
return nodes, nil
}
return nodes, nil
}
func (sq *SkuQuery) sqlCount(ctx context.Context) (int, error) {
_spec := sq.querySpec()
_spec.Node.Columns = sq.ctx.Fields
if len(sq.ctx.Fields) > 0 {
_spec.Unique = sq.ctx.Unique != nil && *sq.ctx.Unique
}
return sqlgraph.CountNodes(ctx, sq.driver, _spec)
}
func (sq *SkuQuery) querySpec() *sqlgraph.QuerySpec {
_spec := sqlgraph.NewQuerySpec(sku.Table, sku.Columns, sqlgraph.NewFieldSpec(sku.FieldID, field.TypeInt))
_spec.From = sq.sql
if unique := sq.ctx.Unique; unique != nil {
_spec.Unique = *unique
} else if sq.path != nil {
_spec.Unique = true
}
if fields := sq.ctx.Fields; len(fields) > 0 {
_spec.Node.Columns = make([]string, 0, len(fields))
_spec.Node.Columns = append(_spec.Node.Columns, sku.FieldID)
for i := range fields {
if fields[i] != sku.FieldID {
_spec.Node.Columns = append(_spec.Node.Columns, fields[i])
}
}
}
if ps := sq.predicates; len(ps) > 0 {
_spec.Predicate = func(selector *sql.Selector) {
for i := range ps {
ps[i](selector)
}
}
}
if limit := sq.ctx.Limit; limit != nil {
_spec.Limit = *limit
}
if offset := sq.ctx.Offset; offset != nil {
_spec.Offset = *offset
}
if ps := sq.order; len(ps) > 0 {
_spec.Order = func(selector *sql.Selector) {
for i := range ps {
ps[i](selector)
}
}
}
return _spec
}
func (sq *SkuQuery) sqlQuery(ctx context.Context) *sql.Selector {
builder := sql.Dialect(sq.driver.Dialect())
t1 := builder.Table(sku.Table)
columns := sq.ctx.Fields
if len(columns) == 0 {
columns = sku.Columns
}
selector := builder.Select(t1.Columns(columns...)...).From(t1)
if sq.sql != nil {
selector = sq.sql
selector.Select(selector.Columns(columns...)...)
}
if sq.ctx.Unique != nil && *sq.ctx.Unique {
selector.Distinct()
}
for _, p := range sq.predicates {
p(selector)
}
for _, p := range sq.order {
p(selector)
}
if offset := sq.ctx.Offset; offset != nil {
// limit is mandatory for offset clause. We start
// with default value, and override it below if needed.
selector.Offset(*offset).Limit(math.MaxInt32)
}
if limit := sq.ctx.Limit; limit != nil {
selector.Limit(*limit)
}
return selector
}
// SkuGroupBy is the group-by builder for Sku entities.
type SkuGroupBy struct {
selector
build *SkuQuery
}
// Aggregate adds the given aggregation functions to the group-by query.
func (sgb *SkuGroupBy) Aggregate(fns ...AggregateFunc) *SkuGroupBy {
sgb.fns = append(sgb.fns, fns...)
return sgb
}
// Scan applies the selector query and scans the result into the given value.
func (sgb *SkuGroupBy) Scan(ctx context.Context, v any) error {
ctx = setContextOp(ctx, sgb.build.ctx, "GroupBy")
if err := sgb.build.prepareQuery(ctx); err != nil {
return err
}
return scanWithInterceptors[*SkuQuery, *SkuGroupBy](ctx, sgb.build, sgb, sgb.build.inters, v)
}
func (sgb *SkuGroupBy) sqlScan(ctx context.Context, root *SkuQuery, v any) error {
selector := root.sqlQuery(ctx).Select()
aggregation := make([]string, 0, len(sgb.fns))
for _, fn := range sgb.fns {
aggregation = append(aggregation, fn(selector))
}
if len(selector.SelectedColumns()) == 0 {
columns := make([]string, 0, len(*sgb.flds)+len(sgb.fns))
for _, f := range *sgb.flds {
columns = append(columns, selector.C(f))
}
columns = append(columns, aggregation...)
selector.Select(columns...)
}
selector.GroupBy(selector.Columns(*sgb.flds...)...)
if err := selector.Err(); err != nil {
return err
}
rows := &sql.Rows{}
query, args := selector.Query()
if err := sgb.build.driver.Query(ctx, query, args, rows); err != nil {
return err
}
defer rows.Close()
return sql.ScanSlice(rows, v)
}
// SkuSelect is the builder for selecting fields of Sku entities.
type SkuSelect struct {
*SkuQuery
selector
}
// Aggregate adds the given aggregation functions to the selector query.
func (ss *SkuSelect) Aggregate(fns ...AggregateFunc) *SkuSelect {
ss.fns = append(ss.fns, fns...)
return ss
}
// Scan applies the selector query and scans the result into the given value.
func (ss *SkuSelect) Scan(ctx context.Context, v any) error {
ctx = setContextOp(ctx, ss.ctx, "Select")
if err := ss.prepareQuery(ctx); err != nil {
return err
}
return scanWithInterceptors[*SkuQuery, *SkuSelect](ctx, ss.SkuQuery, ss, ss.inters, v)
}
func (ss *SkuSelect) sqlScan(ctx context.Context, root *SkuQuery, v any) error {
selector := root.sqlQuery(ctx)
aggregation := make([]string, 0, len(ss.fns))
for _, fn := range ss.fns {
aggregation = append(aggregation, fn(selector))
}
switch n := len(*ss.selector.flds); {
case n == 0 && len(aggregation) > 0:
selector.Select(aggregation...)
case n != 0 && len(aggregation) > 0:
selector.AppendSelect(aggregation...)
}
rows := &sql.Rows{}
query, args := selector.Query()
if err := ss.driver.Query(ctx, query, args, rows); err != nil {
return err
}
defer rows.Close()
return sql.ScanSlice(rows, v)
}

@ -0,0 +1,832 @@
// Code generated by ent, DO NOT EDIT.
package ent
import (
"context"
"errors"
"fmt"
"time"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
"github.com/cloudreve/Cloudreve/v4/ent/predicate"
"github.com/cloudreve/Cloudreve/v4/ent/sku"
)
// SkuUpdate is the builder for updating Sku entities.
type SkuUpdate struct {
config
hooks []Hook
mutation *SkuMutation
}
// Where appends a list predicates to the SkuUpdate builder.
func (su *SkuUpdate) Where(ps ...predicate.Sku) *SkuUpdate {
su.mutation.Where(ps...)
return su
}
// SetUpdatedAt sets the "updated_at" field.
func (su *SkuUpdate) SetUpdatedAt(t time.Time) *SkuUpdate {
su.mutation.SetUpdatedAt(t)
return su
}
// SetDeletedAt sets the "deleted_at" field.
func (su *SkuUpdate) SetDeletedAt(t time.Time) *SkuUpdate {
su.mutation.SetDeletedAt(t)
return su
}
// SetNillableDeletedAt sets the "deleted_at" field if the given value is not nil.
func (su *SkuUpdate) SetNillableDeletedAt(t *time.Time) *SkuUpdate {
if t != nil {
su.SetDeletedAt(*t)
}
return su
}
// ClearDeletedAt clears the value of the "deleted_at" field.
func (su *SkuUpdate) ClearDeletedAt() *SkuUpdate {
su.mutation.ClearDeletedAt()
return su
}
// SetName sets the "name" field.
func (su *SkuUpdate) SetName(s string) *SkuUpdate {
su.mutation.SetName(s)
return su
}
// SetNillableName sets the "name" field if the given value is not nil.
func (su *SkuUpdate) SetNillableName(s *string) *SkuUpdate {
if s != nil {
su.SetName(*s)
}
return su
}
// SetType sets the "type" field.
func (su *SkuUpdate) SetType(s sku.Type) *SkuUpdate {
su.mutation.SetType(s)
return su
}
// SetNillableType sets the "type" field if the given value is not nil.
func (su *SkuUpdate) SetNillableType(s *sku.Type) *SkuUpdate {
if s != nil {
su.SetType(*s)
}
return su
}
// SetAmount sets the "amount" field.
func (su *SkuUpdate) SetAmount(i int64) *SkuUpdate {
su.mutation.ResetAmount()
su.mutation.SetAmount(i)
return su
}
// SetNillableAmount sets the "amount" field if the given value is not nil.
func (su *SkuUpdate) SetNillableAmount(i *int64) *SkuUpdate {
if i != nil {
su.SetAmount(*i)
}
return su
}
// AddAmount adds i to the "amount" field.
func (su *SkuUpdate) AddAmount(i int64) *SkuUpdate {
su.mutation.AddAmount(i)
return su
}
// SetDuration sets the "duration" field.
func (su *SkuUpdate) SetDuration(i int64) *SkuUpdate {
su.mutation.ResetDuration()
su.mutation.SetDuration(i)
return su
}
// SetNillableDuration sets the "duration" field if the given value is not nil.
func (su *SkuUpdate) SetNillableDuration(i *int64) *SkuUpdate {
if i != nil {
su.SetDuration(*i)
}
return su
}
// AddDuration adds i to the "duration" field.
func (su *SkuUpdate) AddDuration(i int64) *SkuUpdate {
su.mutation.AddDuration(i)
return su
}
// ClearDuration clears the value of the "duration" field.
func (su *SkuUpdate) ClearDuration() *SkuUpdate {
su.mutation.ClearDuration()
return su
}
// SetPrice sets the "price" field.
func (su *SkuUpdate) SetPrice(i int64) *SkuUpdate {
su.mutation.ResetPrice()
su.mutation.SetPrice(i)
return su
}
// SetNillablePrice sets the "price" field if the given value is not nil.
func (su *SkuUpdate) SetNillablePrice(i *int64) *SkuUpdate {
if i != nil {
su.SetPrice(*i)
}
return su
}
// AddPrice adds i to the "price" field.
func (su *SkuUpdate) AddPrice(i int64) *SkuUpdate {
su.mutation.AddPrice(i)
return su
}
// ClearPrice clears the value of the "price" field.
func (su *SkuUpdate) ClearPrice() *SkuUpdate {
su.mutation.ClearPrice()
return su
}
// SetPoints sets the "points" field.
func (su *SkuUpdate) SetPoints(i int64) *SkuUpdate {
su.mutation.ResetPoints()
su.mutation.SetPoints(i)
return su
}
// SetNillablePoints sets the "points" field if the given value is not nil.
func (su *SkuUpdate) SetNillablePoints(i *int64) *SkuUpdate {
if i != nil {
su.SetPoints(*i)
}
return su
}
// AddPoints adds i to the "points" field.
func (su *SkuUpdate) AddPoints(i int64) *SkuUpdate {
su.mutation.AddPoints(i)
return su
}
// ClearPoints clears the value of the "points" field.
func (su *SkuUpdate) ClearPoints() *SkuUpdate {
su.mutation.ClearPoints()
return su
}
// SetLabel sets the "label" field.
func (su *SkuUpdate) SetLabel(s string) *SkuUpdate {
su.mutation.SetLabel(s)
return su
}
// SetNillableLabel sets the "label" field if the given value is not nil.
func (su *SkuUpdate) SetNillableLabel(s *string) *SkuUpdate {
if s != nil {
su.SetLabel(*s)
}
return su
}
// ClearLabel clears the value of the "label" field.
func (su *SkuUpdate) ClearLabel() *SkuUpdate {
su.mutation.ClearLabel()
return su
}
// SetDes sets the "des" field.
func (su *SkuUpdate) SetDes(s string) *SkuUpdate {
su.mutation.SetDes(s)
return su
}
// SetNillableDes sets the "des" field if the given value is not nil.
func (su *SkuUpdate) SetNillableDes(s *string) *SkuUpdate {
if s != nil {
su.SetDes(*s)
}
return su
}
// ClearDes clears the value of the "des" field.
func (su *SkuUpdate) ClearDes() *SkuUpdate {
su.mutation.ClearDes()
return su
}
// SetEnabled sets the "enabled" field.
func (su *SkuUpdate) SetEnabled(b bool) *SkuUpdate {
su.mutation.SetEnabled(b)
return su
}
// SetNillableEnabled sets the "enabled" field if the given value is not nil.
func (su *SkuUpdate) SetNillableEnabled(b *bool) *SkuUpdate {
if b != nil {
su.SetEnabled(*b)
}
return su
}
// SetWeight sets the "weight" field.
func (su *SkuUpdate) SetWeight(i int) *SkuUpdate {
su.mutation.ResetWeight()
su.mutation.SetWeight(i)
return su
}
// SetNillableWeight sets the "weight" field if the given value is not nil.
func (su *SkuUpdate) SetNillableWeight(i *int) *SkuUpdate {
if i != nil {
su.SetWeight(*i)
}
return su
}
// AddWeight adds i to the "weight" field.
func (su *SkuUpdate) AddWeight(i int) *SkuUpdate {
su.mutation.AddWeight(i)
return su
}
// Mutation returns the SkuMutation object of the builder.
func (su *SkuUpdate) Mutation() *SkuMutation {
return su.mutation
}
// Save executes the query and returns the number of nodes affected by the update operation.
func (su *SkuUpdate) Save(ctx context.Context) (int, error) {
if err := su.defaults(); err != nil {
return 0, err
}
return withHooks(ctx, su.sqlSave, su.mutation, su.hooks)
}
// SaveX is like Save, but panics if an error occurs.
func (su *SkuUpdate) SaveX(ctx context.Context) int {
affected, err := su.Save(ctx)
if err != nil {
panic(err)
}
return affected
}
// Exec executes the query.
func (su *SkuUpdate) Exec(ctx context.Context) error {
_, err := su.Save(ctx)
return err
}
// ExecX is like Exec, but panics if an error occurs.
func (su *SkuUpdate) ExecX(ctx context.Context) {
if err := su.Exec(ctx); err != nil {
panic(err)
}
}
// defaults sets the default values of the builder before save.
func (su *SkuUpdate) defaults() error {
if _, ok := su.mutation.UpdatedAt(); !ok {
if sku.UpdateDefaultUpdatedAt == nil {
return fmt.Errorf("ent: uninitialized sku.UpdateDefaultUpdatedAt (forgotten import ent/runtime?)")
}
v := sku.UpdateDefaultUpdatedAt()
su.mutation.SetUpdatedAt(v)
}
return nil
}
// check runs all checks and user-defined validators on the builder.
func (su *SkuUpdate) check() error {
if v, ok := su.mutation.GetType(); ok {
if err := sku.TypeValidator(v); err != nil {
return &ValidationError{Name: "type", err: fmt.Errorf(`ent: validator failed for field "Sku.type": %w`, err)}
}
}
return nil
}
func (su *SkuUpdate) sqlSave(ctx context.Context) (n int, err error) {
if err := su.check(); err != nil {
return n, err
}
_spec := sqlgraph.NewUpdateSpec(sku.Table, sku.Columns, sqlgraph.NewFieldSpec(sku.FieldID, field.TypeInt))
if ps := su.mutation.predicates; len(ps) > 0 {
_spec.Predicate = func(selector *sql.Selector) {
for i := range ps {
ps[i](selector)
}
}
}
if value, ok := su.mutation.UpdatedAt(); ok {
_spec.SetField(sku.FieldUpdatedAt, field.TypeTime, value)
}
if value, ok := su.mutation.DeletedAt(); ok {
_spec.SetField(sku.FieldDeletedAt, field.TypeTime, value)
}
if su.mutation.DeletedAtCleared() {
_spec.ClearField(sku.FieldDeletedAt, field.TypeTime)
}
if value, ok := su.mutation.Name(); ok {
_spec.SetField(sku.FieldName, field.TypeString, value)
}
if value, ok := su.mutation.GetType(); ok {
_spec.SetField(sku.FieldType, field.TypeEnum, value)
}
if value, ok := su.mutation.Amount(); ok {
_spec.SetField(sku.FieldAmount, field.TypeInt64, value)
}
if value, ok := su.mutation.AddedAmount(); ok {
_spec.AddField(sku.FieldAmount, field.TypeInt64, value)
}
if value, ok := su.mutation.Duration(); ok {
_spec.SetField(sku.FieldDuration, field.TypeInt64, value)
}
if value, ok := su.mutation.AddedDuration(); ok {
_spec.AddField(sku.FieldDuration, field.TypeInt64, value)
}
if su.mutation.DurationCleared() {
_spec.ClearField(sku.FieldDuration, field.TypeInt64)
}
if value, ok := su.mutation.Price(); ok {
_spec.SetField(sku.FieldPrice, field.TypeInt64, value)
}
if value, ok := su.mutation.AddedPrice(); ok {
_spec.AddField(sku.FieldPrice, field.TypeInt64, value)
}
if su.mutation.PriceCleared() {
_spec.ClearField(sku.FieldPrice, field.TypeInt64)
}
if value, ok := su.mutation.Points(); ok {
_spec.SetField(sku.FieldPoints, field.TypeInt64, value)
}
if value, ok := su.mutation.AddedPoints(); ok {
_spec.AddField(sku.FieldPoints, field.TypeInt64, value)
}
if su.mutation.PointsCleared() {
_spec.ClearField(sku.FieldPoints, field.TypeInt64)
}
if value, ok := su.mutation.Label(); ok {
_spec.SetField(sku.FieldLabel, field.TypeString, value)
}
if su.mutation.LabelCleared() {
_spec.ClearField(sku.FieldLabel, field.TypeString)
}
if value, ok := su.mutation.Des(); ok {
_spec.SetField(sku.FieldDes, field.TypeString, value)
}
if su.mutation.DesCleared() {
_spec.ClearField(sku.FieldDes, field.TypeString)
}
if value, ok := su.mutation.Enabled(); ok {
_spec.SetField(sku.FieldEnabled, field.TypeBool, value)
}
if value, ok := su.mutation.Weight(); ok {
_spec.SetField(sku.FieldWeight, field.TypeInt, value)
}
if value, ok := su.mutation.AddedWeight(); ok {
_spec.AddField(sku.FieldWeight, field.TypeInt, value)
}
if n, err = sqlgraph.UpdateNodes(ctx, su.driver, _spec); err != nil {
if _, ok := err.(*sqlgraph.NotFoundError); ok {
err = &NotFoundError{sku.Label}
} else if sqlgraph.IsConstraintError(err) {
err = &ConstraintError{msg: err.Error(), wrap: err}
}
return 0, err
}
su.mutation.done = true
return n, nil
}
// SkuUpdateOne is the builder for updating a single Sku entity.
type SkuUpdateOne struct {
config
fields []string
hooks []Hook
mutation *SkuMutation
}
// SetUpdatedAt sets the "updated_at" field.
func (suo *SkuUpdateOne) SetUpdatedAt(t time.Time) *SkuUpdateOne {
suo.mutation.SetUpdatedAt(t)
return suo
}
// SetDeletedAt sets the "deleted_at" field.
func (suo *SkuUpdateOne) SetDeletedAt(t time.Time) *SkuUpdateOne {
suo.mutation.SetDeletedAt(t)
return suo
}
// SetNillableDeletedAt sets the "deleted_at" field if the given value is not nil.
func (suo *SkuUpdateOne) SetNillableDeletedAt(t *time.Time) *SkuUpdateOne {
if t != nil {
suo.SetDeletedAt(*t)
}
return suo
}
// ClearDeletedAt clears the value of the "deleted_at" field.
func (suo *SkuUpdateOne) ClearDeletedAt() *SkuUpdateOne {
suo.mutation.ClearDeletedAt()
return suo
}
// SetName sets the "name" field.
func (suo *SkuUpdateOne) SetName(s string) *SkuUpdateOne {
suo.mutation.SetName(s)
return suo
}
// SetNillableName sets the "name" field if the given value is not nil.
func (suo *SkuUpdateOne) SetNillableName(s *string) *SkuUpdateOne {
if s != nil {
suo.SetName(*s)
}
return suo
}
// SetType sets the "type" field.
func (suo *SkuUpdateOne) SetType(s sku.Type) *SkuUpdateOne {
suo.mutation.SetType(s)
return suo
}
// SetNillableType sets the "type" field if the given value is not nil.
func (suo *SkuUpdateOne) SetNillableType(s *sku.Type) *SkuUpdateOne {
if s != nil {
suo.SetType(*s)
}
return suo
}
// SetAmount sets the "amount" field.
func (suo *SkuUpdateOne) SetAmount(i int64) *SkuUpdateOne {
suo.mutation.ResetAmount()
suo.mutation.SetAmount(i)
return suo
}
// SetNillableAmount sets the "amount" field if the given value is not nil.
func (suo *SkuUpdateOne) SetNillableAmount(i *int64) *SkuUpdateOne {
if i != nil {
suo.SetAmount(*i)
}
return suo
}
// AddAmount adds i to the "amount" field.
func (suo *SkuUpdateOne) AddAmount(i int64) *SkuUpdateOne {
suo.mutation.AddAmount(i)
return suo
}
// SetDuration sets the "duration" field.
func (suo *SkuUpdateOne) SetDuration(i int64) *SkuUpdateOne {
suo.mutation.ResetDuration()
suo.mutation.SetDuration(i)
return suo
}
// SetNillableDuration sets the "duration" field if the given value is not nil.
func (suo *SkuUpdateOne) SetNillableDuration(i *int64) *SkuUpdateOne {
if i != nil {
suo.SetDuration(*i)
}
return suo
}
// AddDuration adds i to the "duration" field.
func (suo *SkuUpdateOne) AddDuration(i int64) *SkuUpdateOne {
suo.mutation.AddDuration(i)
return suo
}
// ClearDuration clears the value of the "duration" field.
func (suo *SkuUpdateOne) ClearDuration() *SkuUpdateOne {
suo.mutation.ClearDuration()
return suo
}
// SetPrice sets the "price" field.
func (suo *SkuUpdateOne) SetPrice(i int64) *SkuUpdateOne {
suo.mutation.ResetPrice()
suo.mutation.SetPrice(i)
return suo
}
// SetNillablePrice sets the "price" field if the given value is not nil.
func (suo *SkuUpdateOne) SetNillablePrice(i *int64) *SkuUpdateOne {
if i != nil {
suo.SetPrice(*i)
}
return suo
}
// AddPrice adds i to the "price" field.
func (suo *SkuUpdateOne) AddPrice(i int64) *SkuUpdateOne {
suo.mutation.AddPrice(i)
return suo
}
// ClearPrice clears the value of the "price" field.
func (suo *SkuUpdateOne) ClearPrice() *SkuUpdateOne {
suo.mutation.ClearPrice()
return suo
}
// SetPoints sets the "points" field.
func (suo *SkuUpdateOne) SetPoints(i int64) *SkuUpdateOne {
suo.mutation.ResetPoints()
suo.mutation.SetPoints(i)
return suo
}
// SetNillablePoints sets the "points" field if the given value is not nil.
func (suo *SkuUpdateOne) SetNillablePoints(i *int64) *SkuUpdateOne {
if i != nil {
suo.SetPoints(*i)
}
return suo
}
// AddPoints adds i to the "points" field.
func (suo *SkuUpdateOne) AddPoints(i int64) *SkuUpdateOne {
suo.mutation.AddPoints(i)
return suo
}
// ClearPoints clears the value of the "points" field.
func (suo *SkuUpdateOne) ClearPoints() *SkuUpdateOne {
suo.mutation.ClearPoints()
return suo
}
// SetLabel sets the "label" field.
func (suo *SkuUpdateOne) SetLabel(s string) *SkuUpdateOne {
suo.mutation.SetLabel(s)
return suo
}
// SetNillableLabel sets the "label" field if the given value is not nil.
func (suo *SkuUpdateOne) SetNillableLabel(s *string) *SkuUpdateOne {
if s != nil {
suo.SetLabel(*s)
}
return suo
}
// ClearLabel clears the value of the "label" field.
func (suo *SkuUpdateOne) ClearLabel() *SkuUpdateOne {
suo.mutation.ClearLabel()
return suo
}
// SetDes sets the "des" field.
func (suo *SkuUpdateOne) SetDes(s string) *SkuUpdateOne {
suo.mutation.SetDes(s)
return suo
}
// SetNillableDes sets the "des" field if the given value is not nil.
func (suo *SkuUpdateOne) SetNillableDes(s *string) *SkuUpdateOne {
if s != nil {
suo.SetDes(*s)
}
return suo
}
// ClearDes clears the value of the "des" field.
func (suo *SkuUpdateOne) ClearDes() *SkuUpdateOne {
suo.mutation.ClearDes()
return suo
}
// SetEnabled sets the "enabled" field.
func (suo *SkuUpdateOne) SetEnabled(b bool) *SkuUpdateOne {
suo.mutation.SetEnabled(b)
return suo
}
// SetNillableEnabled sets the "enabled" field if the given value is not nil.
func (suo *SkuUpdateOne) SetNillableEnabled(b *bool) *SkuUpdateOne {
if b != nil {
suo.SetEnabled(*b)
}
return suo
}
// SetWeight sets the "weight" field.
func (suo *SkuUpdateOne) SetWeight(i int) *SkuUpdateOne {
suo.mutation.ResetWeight()
suo.mutation.SetWeight(i)
return suo
}
// SetNillableWeight sets the "weight" field if the given value is not nil.
func (suo *SkuUpdateOne) SetNillableWeight(i *int) *SkuUpdateOne {
if i != nil {
suo.SetWeight(*i)
}
return suo
}
// AddWeight adds i to the "weight" field.
func (suo *SkuUpdateOne) AddWeight(i int) *SkuUpdateOne {
suo.mutation.AddWeight(i)
return suo
}
// Mutation returns the SkuMutation object of the builder.
func (suo *SkuUpdateOne) Mutation() *SkuMutation {
return suo.mutation
}
// Where appends a list predicates to the SkuUpdate builder.
func (suo *SkuUpdateOne) Where(ps ...predicate.Sku) *SkuUpdateOne {
suo.mutation.Where(ps...)
return suo
}
// Select allows selecting one or more fields (columns) of the returned entity.
// The default is selecting all fields defined in the entity schema.
func (suo *SkuUpdateOne) Select(field string, fields ...string) *SkuUpdateOne {
suo.fields = append([]string{field}, fields...)
return suo
}
// Save executes the query and returns the updated Sku entity.
func (suo *SkuUpdateOne) Save(ctx context.Context) (*Sku, error) {
if err := suo.defaults(); err != nil {
return nil, err
}
return withHooks(ctx, suo.sqlSave, suo.mutation, suo.hooks)
}
// SaveX is like Save, but panics if an error occurs.
func (suo *SkuUpdateOne) SaveX(ctx context.Context) *Sku {
node, err := suo.Save(ctx)
if err != nil {
panic(err)
}
return node
}
// Exec executes the query on the entity.
func (suo *SkuUpdateOne) Exec(ctx context.Context) error {
_, err := suo.Save(ctx)
return err
}
// ExecX is like Exec, but panics if an error occurs.
func (suo *SkuUpdateOne) ExecX(ctx context.Context) {
if err := suo.Exec(ctx); err != nil {
panic(err)
}
}
// defaults sets the default values of the builder before save.
func (suo *SkuUpdateOne) defaults() error {
if _, ok := suo.mutation.UpdatedAt(); !ok {
if sku.UpdateDefaultUpdatedAt == nil {
return fmt.Errorf("ent: uninitialized sku.UpdateDefaultUpdatedAt (forgotten import ent/runtime?)")
}
v := sku.UpdateDefaultUpdatedAt()
suo.mutation.SetUpdatedAt(v)
}
return nil
}
// check runs all checks and user-defined validators on the builder.
func (suo *SkuUpdateOne) check() error {
if v, ok := suo.mutation.GetType(); ok {
if err := sku.TypeValidator(v); err != nil {
return &ValidationError{Name: "type", err: fmt.Errorf(`ent: validator failed for field "Sku.type": %w`, err)}
}
}
return nil
}
func (suo *SkuUpdateOne) sqlSave(ctx context.Context) (_node *Sku, err error) {
if err := suo.check(); err != nil {
return _node, err
}
_spec := sqlgraph.NewUpdateSpec(sku.Table, sku.Columns, sqlgraph.NewFieldSpec(sku.FieldID, field.TypeInt))
id, ok := suo.mutation.ID()
if !ok {
return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "Sku.id" for update`)}
}
_spec.Node.ID.Value = id
if fields := suo.fields; len(fields) > 0 {
_spec.Node.Columns = make([]string, 0, len(fields))
_spec.Node.Columns = append(_spec.Node.Columns, sku.FieldID)
for _, f := range fields {
if !sku.ValidColumn(f) {
return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)}
}
if f != sku.FieldID {
_spec.Node.Columns = append(_spec.Node.Columns, f)
}
}
}
if ps := suo.mutation.predicates; len(ps) > 0 {
_spec.Predicate = func(selector *sql.Selector) {
for i := range ps {
ps[i](selector)
}
}
}
if value, ok := suo.mutation.UpdatedAt(); ok {
_spec.SetField(sku.FieldUpdatedAt, field.TypeTime, value)
}
if value, ok := suo.mutation.DeletedAt(); ok {
_spec.SetField(sku.FieldDeletedAt, field.TypeTime, value)
}
if suo.mutation.DeletedAtCleared() {
_spec.ClearField(sku.FieldDeletedAt, field.TypeTime)
}
if value, ok := suo.mutation.Name(); ok {
_spec.SetField(sku.FieldName, field.TypeString, value)
}
if value, ok := suo.mutation.GetType(); ok {
_spec.SetField(sku.FieldType, field.TypeEnum, value)
}
if value, ok := suo.mutation.Amount(); ok {
_spec.SetField(sku.FieldAmount, field.TypeInt64, value)
}
if value, ok := suo.mutation.AddedAmount(); ok {
_spec.AddField(sku.FieldAmount, field.TypeInt64, value)
}
if value, ok := suo.mutation.Duration(); ok {
_spec.SetField(sku.FieldDuration, field.TypeInt64, value)
}
if value, ok := suo.mutation.AddedDuration(); ok {
_spec.AddField(sku.FieldDuration, field.TypeInt64, value)
}
if suo.mutation.DurationCleared() {
_spec.ClearField(sku.FieldDuration, field.TypeInt64)
}
if value, ok := suo.mutation.Price(); ok {
_spec.SetField(sku.FieldPrice, field.TypeInt64, value)
}
if value, ok := suo.mutation.AddedPrice(); ok {
_spec.AddField(sku.FieldPrice, field.TypeInt64, value)
}
if suo.mutation.PriceCleared() {
_spec.ClearField(sku.FieldPrice, field.TypeInt64)
}
if value, ok := suo.mutation.Points(); ok {
_spec.SetField(sku.FieldPoints, field.TypeInt64, value)
}
if value, ok := suo.mutation.AddedPoints(); ok {
_spec.AddField(sku.FieldPoints, field.TypeInt64, value)
}
if suo.mutation.PointsCleared() {
_spec.ClearField(sku.FieldPoints, field.TypeInt64)
}
if value, ok := suo.mutation.Label(); ok {
_spec.SetField(sku.FieldLabel, field.TypeString, value)
}
if suo.mutation.LabelCleared() {
_spec.ClearField(sku.FieldLabel, field.TypeString)
}
if value, ok := suo.mutation.Des(); ok {
_spec.SetField(sku.FieldDes, field.TypeString, value)
}
if suo.mutation.DesCleared() {
_spec.ClearField(sku.FieldDes, field.TypeString)
}
if value, ok := suo.mutation.Enabled(); ok {
_spec.SetField(sku.FieldEnabled, field.TypeBool, value)
}
if value, ok := suo.mutation.Weight(); ok {
_spec.SetField(sku.FieldWeight, field.TypeInt, value)
}
if value, ok := suo.mutation.AddedWeight(); ok {
_spec.AddField(sku.FieldWeight, field.TypeInt, value)
}
_node = &Sku{config: suo.config}
_spec.Assign = _node.assignValues
_spec.ScanValues = _node.scanValues
if err = sqlgraph.UpdateNode(ctx, suo.driver, _spec); err != nil {
if _, ok := err.(*sqlgraph.NotFoundError); ok {
err = &NotFoundError{sku.Label}
} else if sqlgraph.IsConstraintError(err) {
err = &ConstraintError{msg: err.Error(), wrap: err}
}
return nil, err
}
suo.mutation.done = true
return _node, nil
}

@ -52,6 +52,8 @@ type Tx struct {
Setting *SettingClient
// Share is the client for interacting with the Share builders.
Share *ShareClient
// Sku is the client for interacting with the Sku builders.
Sku *SkuClient
// StoragePolicy is the client for interacting with the StoragePolicy builders.
StoragePolicy *StoragePolicyClient
// Task is the client for interacting with the Task builders.
@ -210,6 +212,7 @@ func (tx *Tx) init() {
tx.Passkey = NewPasskeyClient(tx.config)
tx.Setting = NewSettingClient(tx.config)
tx.Share = NewShareClient(tx.config)
tx.Sku = NewSkuClient(tx.config)
tx.StoragePolicy = NewStoragePolicyClient(tx.config)
tx.Task = NewTaskClient(tx.config)
tx.User = NewUserClient(tx.config)

@ -134,6 +134,7 @@
"remoteDownload": "Remote Download",
"connect": "Connect & Mount",
"taskQueue": "Background Tasks",
"shop": "Shop",
"setting": "Settings",
"videos": "Videos",
"photos": "Photos",
@ -1113,6 +1114,17 @@
"reportDescription": "Additional description",
"reportAbuseSuccess": "Report submitted."
},
"shop": {
"memberships": "Memberships",
"storagePacks": "Storage packs",
"redeem": "Redeem",
"balance": "Balance: {{credits}} pts",
"purchased": "Purchased {{name}}",
"permanent": "Permanent",
"noProducts": "No products available",
"buyWithPoints": "Buy for {{points}} pts",
"pointsUnavailable": "Not purchasable with points"
},
"announcement": {
"title": "Announcement",
"dontShowAgain": "Don't show this again"

@ -134,6 +134,7 @@
"remoteDownload": "离线下载",
"connect": "连接与挂载",
"taskQueue": "后台任务",
"shop": "商城",
"setting": "设置",
"videos": "视频",
"photos": "图片",
@ -1113,6 +1114,17 @@
"reportDescription": "补充说明",
"reportAbuseSuccess": "举报已提交"
},
"shop": {
"memberships": "会员",
"storagePacks": "容量包",
"redeem": "兑换",
"balance": "余额:{{credits}} 积分",
"purchased": "已购买 {{name}}",
"permanent": "永久",
"noProducts": "暂无商品",
"buyWithPoints": "{{points}} 积分购买",
"pointsUnavailable": "不支持积分购买"
},
"announcement": {
"title": "公告",
"dontShowAgain": "不再显示"

@ -38,6 +38,7 @@ import {
QueueMetric,
SetSettingService,
Share as ShareEnt,
Sku,
Task,
TestNodeDownloaderService,
TestNodeService,
@ -112,6 +113,7 @@ import {
RefreshTokenRequest,
ResetPasswordService,
SendResetEmailService,
ShopSku,
SignUpService,
Token,
TwoFALoginRequest,
@ -2681,6 +2683,76 @@ export function adminDeleteGiftCode(id: number): ThunkResponse<void> {
};
}
export function getShopSkus(): ThunkResponse<ShopSku[]> {
return async (dispatch, _getState) => {
return await dispatch(
send(
"/user/shop/skus",
{ method: "GET" },
{
...defaultOpts,
},
),
);
};
}
export function purchaseSku(sku: string): ThunkResponse<CreditInfo> {
return async (dispatch, _getState) => {
return await dispatch(
send(
"/user/shop/purchase",
{ method: "POST", data: { sku } },
{
...defaultOpts,
},
),
);
};
}
export function adminListSkus(): ThunkResponse<Sku[]> {
return async (dispatch, _getState) => {
return await dispatch(
send(
"/admin/vas/sku",
{ method: "GET" },
{
...defaultOpts,
},
),
);
};
}
export function adminUpsertSku(sku: Sku): ThunkResponse<Sku> {
return async (dispatch, _getState) => {
return await dispatch(
send(
sku.id > 0 ? `/admin/vas/sku/${sku.id}` : "/admin/vas/sku",
{ method: "PUT", data: { sku } },
{
...defaultOpts,
},
),
);
};
}
export function adminDeleteSku(id: number): ThunkResponse<void> {
return async (dispatch, _getState) => {
return await dispatch(
send(
`/admin/vas/sku/${id}`,
{ method: "DELETE" },
{
...defaultOpts,
},
),
);
};
}
export function adminListEvents(args: {
page: number;
pageSize: number;

@ -656,6 +656,26 @@ export interface GiftCodeListResponse {
total: number;
}
export interface Sku {
id: number;
name: string;
type: "storage" | "group";
amount: number;
duration?: number;
price?: number;
points?: number;
label?: string;
des?: string;
enabled: boolean;
weight: number;
created_at?: string;
updated_at?: string;
}
export interface SkuUpsertService {
sku: Sku;
}
export interface ActivityEvent {
id: string;
type: number;

@ -314,3 +314,17 @@ export interface CreditTxnList {
txns: CreditTxn[];
total: number;
}
export interface ShopSku {
id: string;
name: string;
type: "storage" | "group";
amount: number;
group?: string;
group_id?: string;
duration: number;
price: number;
points?: number;
label?: string;
des?: string;
}

@ -1,43 +0,0 @@
import { Box, Table, TableBody, TableContainer, TableHead, TableRow, Typography } from "@mui/material";
import { useTranslation } from "react-i18next";
import { NoWrapCell, SecondaryButton, StyledTableContainerPaper } from "../../../Common/StyledComponents.tsx";
import Add from "../../../Icons/Add.tsx";
const GroupProducts = () => {
const { t } = useTranslation("dashboard");
return (
<Box>
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 1 }}>
<SecondaryButton variant="contained" startIcon={<Add />}>
{t("settings.addGroupProduct")}
</SecondaryButton>
</Box>
<TableContainer component={StyledTableContainerPaper}>
<Table sx={{ width: "100%" }} size="small">
<TableHead>
<TableRow>
<NoWrapCell>{t("settings.displayName")}</NoWrapCell>
<NoWrapCell>{t("settings.price")}</NoWrapCell>
<NoWrapCell>{t("settings.duration")}</NoWrapCell>
<NoWrapCell>{t("settings.description")}</NoWrapCell>
<NoWrapCell>{t("settings.actions")}</NoWrapCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<NoWrapCell colSpan={5} align="center">
<Typography variant="caption" color="text.secondary">
{t("application:setting.listEmpty")}
</Typography>
</NoWrapCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</Box>
);
};
export default GroupProducts;

@ -0,0 +1,349 @@
import {
Box,
Chip,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
FormControl,
FormControlLabel,
IconButton,
InputLabel,
MenuItem,
Select,
Stack,
Switch,
Table,
TableBody,
TableContainer,
TableHead,
TableRow,
Typography,
} from "@mui/material";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { adminDeleteSku, adminListSkus, adminUpsertSku, getGroupList } from "../../../../api/api.ts";
import { GroupEnt, Sku } from "../../../../api/dashboard.ts";
import { useAppDispatch } from "../../../../redux/hooks.ts";
import { sizeToString } from "../../../../util/index.ts";
import {
DenseFilledTextField,
NoWrapCell,
SecondaryButton,
StyledTableContainerPaper,
} from "../../../Common/StyledComponents.tsx";
import Add from "../../../Icons/Add.tsx";
import Dismiss from "../../../Icons/Dismiss.tsx";
import Edit from "../../../Icons/Edit.tsx";
import SettingForm from "../../../Pages/Setting/SettingForm.tsx";
import { NoMarginHelperText } from "../Settings.tsx";
const DAY_SECONDS = 86400;
export interface SkuTableProps {
type: "storage" | "group";
}
interface SkuForm {
id: number;
name: string;
amount: number;
durationDays: number;
price: number;
allowPoints: boolean;
points: number;
label: string;
des: string;
enabled: boolean;
}
const emptyForm = (type: string): SkuForm => ({
id: 0,
name: "",
amount: 0,
durationDays: type === "storage" ? 365 : 30,
price: 0,
allowPoints: false,
points: 0,
label: "",
des: "",
enabled: true,
});
const SkuTable = ({ type }: SkuTableProps) => {
const { t } = useTranslation("dashboard");
const dispatch = useAppDispatch();
const [skus, setSkus] = useState<Sku[] | undefined>(undefined);
const [groups, setGroups] = useState<GroupEnt[]>([]);
const [form, setForm] = useState<SkuForm | undefined>(undefined);
const [saving, setSaving] = useState(false);
const load = useCallback(() => {
dispatch(adminListSkus()).then((res) => setSkus(res.filter((s) => s.type === type)));
}, [type]);
useEffect(() => {
load();
dispatch(getGroupList({ page: 1, page_size: 200, order_by: "id", order_direction: "asc" })).then((res) =>
setGroups(res.groups),
);
}, [load]);
const groupName = useCallback(
(id: number) => groups.find((g) => g.id === id)?.name ?? `#${id}`,
[groups],
);
const openEdit = (s?: Sku) => {
if (!s) {
setForm(emptyForm(type));
return;
}
setForm({
id: s.id,
name: s.name,
amount: s.amount,
durationDays: Math.round((s.duration ?? 0) / DAY_SECONDS),
price: s.price ?? 0,
allowPoints: s.points != null,
points: s.points ?? 0,
label: s.label ?? "",
des: s.des ?? "",
enabled: s.enabled,
});
};
const onSave = () => {
if (!form) {
return;
}
setSaving(true);
dispatch(
adminUpsertSku({
id: form.id,
name: form.name,
type,
amount: form.amount,
duration: Math.max(0, form.durationDays) * DAY_SECONDS,
price: Math.max(0, form.price),
points: form.allowPoints ? Math.max(1, form.points) : undefined,
label: form.label || undefined,
des: form.des || undefined,
enabled: form.enabled,
weight: 0,
}),
)
.then(() => {
setForm(undefined);
load();
})
.finally(() => setSaving(false));
};
const onDelete = (id: number) => {
dispatch(adminDeleteSku(id)).then(load);
};
const amountLabel = (s: Sku) => (type === "storage" ? sizeToString(s.amount) : groupName(s.amount));
return (
<Stack spacing={2}>
<Box>
<SecondaryButton variant="contained" startIcon={<Add />} onClick={() => openEdit()}>
{type === "storage" ? t("vas.addStoragePack") : t("vas.addMembership")}
</SecondaryButton>
</Box>
<StyledTableContainerPaper>
<TableContainer>
<Table size="small">
<TableHead>
<TableRow>
<NoWrapCell>{t("vas.name")}</NoWrapCell>
<NoWrapCell>{type === "storage" ? t("vas.size") : t("vas.group")}</NoWrapCell>
<NoWrapCell>{t("vas.duration")}</NoWrapCell>
<NoWrapCell>{t("vas.price")}</NoWrapCell>
<NoWrapCell>{t("vas.priceCredits")}</NoWrapCell>
<NoWrapCell>{t("vas.status")}</NoWrapCell>
<NoWrapCell align="right">{t("vas.actions")}</NoWrapCell>
</TableRow>
</TableHead>
<TableBody>
{skus?.map((s) => (
<TableRow key={s.id}>
<NoWrapCell>
{s.name}
{s.label && <Chip size="small" color="primary" label={s.label} sx={{ ml: 1 }} />}
</NoWrapCell>
<NoWrapCell>{amountLabel(s)}</NoWrapCell>
<NoWrapCell>
{(s.duration ?? 0) > 0
? t("application:vas.validDurationDays", { num: Math.round((s.duration ?? 0) / DAY_SECONDS) })
: t("application:shop.permanent")}
</NoWrapCell>
<NoWrapCell>{s.price ?? 0}</NoWrapCell>
<NoWrapCell>{s.points != null ? s.points : "-"}</NoWrapCell>
<NoWrapCell>
<Chip
size="small"
color={s.enabled ? "success" : "default"}
label={s.enabled ? t("vas.enable") : t("vas.no")}
/>
</NoWrapCell>
<NoWrapCell align="right">
<IconButton size="small" onClick={() => openEdit(s)}>
<Edit fontSize="small" />
</IconButton>
<IconButton size="small" onClick={() => onDelete(s.id)}>
<Dismiss fontSize="small" />
</IconButton>
</NoWrapCell>
</TableRow>
))}
{(!skus || skus.length === 0) && (
<TableRow>
<NoWrapCell colSpan={7} align="center">
<Typography variant="caption" color="text.secondary">
{t("application:setting.listEmpty")}
</Typography>
</NoWrapCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
</StyledTableContainerPaper>
<Dialog open={!!form} onClose={() => setForm(undefined)} maxWidth="sm" fullWidth>
<DialogTitle>{type === "storage" ? t("vas.editStoragePack") : t("vas.editMembership")}</DialogTitle>
<DialogContent>
{form && (
<Stack spacing={2} sx={{ mt: 1 }}>
<SettingForm title={t("vas.productName")}>
<DenseFilledTextField
fullWidth
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
/>
<NoMarginHelperText>{t("vas.productNameDes")}</NoMarginHelperText>
</SettingForm>
{type === "storage" ? (
<SettingForm title={t("vas.size")}>
<DenseFilledTextField
fullWidth
type="number"
value={form.amount}
onChange={(e) => setForm({ ...form, amount: parseInt(e.target.value) || 0 })}
/>
<NoMarginHelperText>{t("vas.packSizeDes")}</NoMarginHelperText>
</SettingForm>
) : (
<SettingForm title={t("vas.purchasableGroups")}>
<FormControl fullWidth size="small">
<InputLabel>{t("vas.purchasableGroups")}</InputLabel>
<Select
value={form.amount}
label={t("vas.purchasableGroups")}
onChange={(e) => setForm({ ...form, amount: e.target.value as number })}
>
{groups.map((g) => (
<MenuItem key={g.id} value={g.id}>
{g.name}
</MenuItem>
))}
</Select>
</FormControl>
<NoMarginHelperText>{t("vas.groupDes")}</NoMarginHelperText>
</SettingForm>
)}
<SettingForm title={t("vas.durationDay")}>
<DenseFilledTextField
fullWidth
type="number"
value={form.durationDays}
onChange={(e) => setForm({ ...form, durationDays: parseInt(e.target.value) || 0 })}
/>
<NoMarginHelperText>
{type === "storage" ? t("vas.durationDayDes") : t("vas.durationGroupDes")}
</NoMarginHelperText>
</SettingForm>
<SettingForm title={t("vas.priceYuan")}>
<DenseFilledTextField
fullWidth
type="number"
value={form.price}
onChange={(e) => setForm({ ...form, price: parseInt(e.target.value) || 0 })}
/>
<NoMarginHelperText>{t("vas.packPriceDes")}</NoMarginHelperText>
</SettingForm>
<SettingForm lgWidth={5}>
<FormControlLabel
control={
<Switch
checked={form.allowPoints}
onChange={(e) => setForm({ ...form, allowPoints: e.target.checked })}
/>
}
label={t("vas.priceCredits")}
/>
<NoMarginHelperText>{t("vas.priceCreditsDes")}</NoMarginHelperText>
</SettingForm>
{form.allowPoints && (
<SettingForm title={t("vas.priceCredits")}>
<DenseFilledTextField
fullWidth
type="number"
value={form.points}
onChange={(e) => setForm({ ...form, points: parseInt(e.target.value) || 0 })}
/>
</SettingForm>
)}
<SettingForm title={t("vas.highlight")}>
<DenseFilledTextField
fullWidth
value={form.label}
onChange={(e) => setForm({ ...form, label: e.target.value })}
/>
<NoMarginHelperText>{t("vas.highlightDes")}</NoMarginHelperText>
</SettingForm>
<SettingForm title={t("vas.productDescription")}>
<DenseFilledTextField
fullWidth
multiline
minRows={3}
value={form.des}
onChange={(e) => setForm({ ...form, des: e.target.value })}
/>
<NoMarginHelperText>{t("vas.productDescriptionDes")}</NoMarginHelperText>
</SettingForm>
<SettingForm lgWidth={5}>
<FormControlLabel
control={
<Switch checked={form.enabled} onChange={(e) => setForm({ ...form, enabled: e.target.checked })} />
}
label={t("vas.enable")}
/>
</SettingForm>
</Stack>
)}
</DialogContent>
<DialogActions>
<SecondaryButton onClick={() => setForm(undefined)}>{t("common:cancel")}</SecondaryButton>
<SecondaryButton variant="contained" onClick={onSave} disabled={saving || !form?.name || !form?.amount}>
{t("common:ok")}
</SecondaryButton>
</DialogActions>
</Dialog>
</Stack>
);
};
export default SkuTable;

@ -1,43 +0,0 @@
import { Box, Table, TableBody, TableContainer, TableHead, TableRow, Typography } from "@mui/material";
import { useTranslation } from "react-i18next";
import { NoWrapCell, SecondaryButton, StyledTableContainerPaper } from "../../../Common/StyledComponents.tsx";
import Add from "../../../Icons/Add.tsx";
const StorageProducts = () => {
const { t } = useTranslation("dashboard");
return (
<Box>
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 1 }}>
<SecondaryButton variant="contained" startIcon={<Add />}>
{t("settings.addStorageProduct")}
</SecondaryButton>
</Box>
<TableContainer component={StyledTableContainerPaper}>
<Table sx={{ width: "100%" }} size="small">
<TableHead>
<TableRow>
<NoWrapCell>{t("settings.displayName")}</NoWrapCell>
<NoWrapCell>{t("settings.price")}</NoWrapCell>
<NoWrapCell>{t("settings.duration")}</NoWrapCell>
<NoWrapCell>{t("settings.storageSize")}</NoWrapCell>
<NoWrapCell>{t("settings.actions")}</NoWrapCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<NoWrapCell colSpan={5} align="center">
<Typography variant="caption" color="text.secondary">
{t("application:setting.listEmpty")}
</Typography>
</NoWrapCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</Box>
);
};
export default StorageProducts;

@ -17,10 +17,9 @@ import SettingForm from "../../../Pages/Setting/SettingForm.tsx";
import { NoMarginHelperText, SettingSection, SettingSectionContent } from "../Settings.tsx";
import { SettingContext } from "../SettingWrapper.tsx";
import GiftCodes from "./GiftCodes.tsx";
import GroupProducts from "./GroupProducts.tsx";
import ManualCreditAdjust from "./ManualCreditAdjust.tsx";
import PaymentProviders from "./PaymentProviders.tsx";
import StorageProducts from "./StorageProducts.tsx";
import SkuTable from "./SkuTable.tsx";
const VAS = () => {
const { t } = useTranslation("dashboard");
const { formRef } = useContext(SettingContext);
@ -170,7 +169,7 @@ const VAS = () => {
<SettingSectionContent>
<SettingForm lgWidth={12}>
<FormControl fullWidth>
<StorageProducts />
<SkuTable type="storage" />
<NoMarginHelperText>{t("settings.storageProductsDes")}</NoMarginHelperText>
</FormControl>
</SettingForm>
@ -184,7 +183,7 @@ const VAS = () => {
<SettingSectionContent>
<SettingForm lgWidth={12}>
<FormControl fullWidth>
<GroupProducts />
<SkuTable type="group" />
<NoMarginHelperText>{t("settings.groupProductsDes")}</NoMarginHelperText>
</FormControl>
</SettingForm>

@ -0,0 +1,55 @@
import { Button, InputAdornment } from "@mui/material";
import { useSnackbar } from "notistack";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { redeemGiftCode } from "../../../api/api.ts";
import { useAppDispatch } from "../../../redux/hooks.ts";
import { DenseFilledTextField } from "../StyledComponents.tsx";
export interface RedeemCodeInputProps {
onRedeemed?: () => void;
}
const RedeemCodeInput = ({ onRedeemed }: RedeemCodeInputProps) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const { enqueueSnackbar } = useSnackbar();
const [code, setCode] = useState("");
const [redeeming, setRedeeming] = useState(false);
const onRedeem = () => {
if (!code.trim()) {
return;
}
setRedeeming(true);
dispatch(redeemGiftCode(code.trim()))
.then(() => {
enqueueSnackbar(t("setting.giftCodeRedeemed"), { variant: "success" });
setCode("");
onRedeemed?.();
})
.finally(() => setRedeeming(false));
};
return (
<DenseFilledTextField
fullWidth
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder={t("setting.giftCodePlaceholder")}
slotProps={{
input: {
endAdornment: (
<InputAdornment position="end">
<Button variant="contained" onClick={onRedeem} disabled={redeeming || !code.trim()}>
{t("setting.redeem")}
</Button>
</InputAdornment>
),
},
}}
/>
);
};
export default RedeemCodeInput;

@ -10,6 +10,8 @@ import SessionManager from "../../../session";
import { GroupBS } from "../../../session/utils.ts";
import BoxMultiple from "../../Icons/BoxMultiple.tsx";
import BoxMultipleFilled from "../../Icons/BoxMultipleFilled.tsx";
import BuildingShop from "../../Icons/BuildingShop.tsx";
import BuildingShopFilled from "../../Icons/BuildingShopFilled.tsx";
import CloudDownload from "../../Icons/CloudDownload.tsx";
import CloudDownloadOutlined from "../../Icons/CloudDownloadOutlined.tsx";
import CubeSync from "../../Icons/CubeSync.tsx";
@ -84,6 +86,12 @@ const RemoteDownloadNavigationItem: NavigationItem = {
path: "/downloads",
};
const ShopNavigationItem: NavigationItem = {
label: "navbar.shop",
icon: [BuildingShopFilled, BuildingShop],
path: "/shop",
};
export const SideNavItemComponent = ({ item }: { item: NavigationItem }) => {
const { t } = useTranslation("application");
const navigate = useNavigate();
@ -274,6 +282,7 @@ const PageNavigation = () => {
{connectEnabled && <SideNavItemComponent item={ConnectNavigationItem} />}
<SideNavItemComponent item={TaskNavigationItem} />
{remoteDownloadEnabled && <SideNavItemComponent item={RemoteDownloadNavigationItem} />}
<SideNavItemComponent item={ShopNavigationItem} />
</>
</Box>
)}

@ -4,5 +4,6 @@ import DownloadList from "./Tasks/DownloadList.tsx";
import Devices from "./Devices/Devices.tsx";
import Setting from "./Setting/Setting.tsx";
import Profile from "./Profile/Profile.tsx";
import Shop from "./Shop/Shop.tsx";
export { Setting, TaskList, ShareList, DownloadList, Devices, Profile };
export { Setting, TaskList, ShareList, DownloadList, Devices, Profile, Shop };

@ -1,8 +1,6 @@
import {
Box,
Button,
Chip,
InputAdornment,
Paper,
Stack,
Table,
@ -13,15 +11,14 @@ import {
TableRow,
Typography,
} from "@mui/material";
import { useSnackbar } from "notistack";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { getCredit, getCreditTxns, redeemGiftCode } from "../../../api/api.ts";
import { getCredit, getCreditTxns } from "../../../api/api.ts";
import { CreditInfo, CreditTxnList } from "../../../api/user.ts";
import { useAppDispatch } from "../../../redux/hooks.ts";
import { sizeToString } from "../../../util/index.ts";
import FacebookCircularProgress from "../../Common/CircularProgress.tsx";
import { DenseFilledTextField } from "../../Common/StyledComponents.tsx";
import RedeemCodeInput from "../../Common/Form/RedeemCodeInput.tsx";
import TablePagination from "../../Admin/Common/TablePagination.tsx";
import { NoMarginHelperText, SettingSection, SettingSectionContent } from "../../Admin/Settings/Settings.tsx";
import SettingForm from "./SettingForm.tsx";
@ -29,13 +26,10 @@ import SettingForm from "./SettingForm.tsx";
const FinanceSetting = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const { enqueueSnackbar } = useSnackbar();
const [info, setInfo] = useState<CreditInfo | undefined>(undefined);
const [txns, setTxns] = useState<CreditTxnList | undefined>(undefined);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [code, setCode] = useState("");
const [redeeming, setRedeeming] = useState(false);
const loadInfo = () => {
dispatch(getCredit()).then((res) => setInfo(res));
@ -49,21 +43,6 @@ const FinanceSetting = () => {
dispatch(getCreditTxns(page, pageSize)).then((res) => setTxns(res));
}, [page, pageSize]);
const onRedeem = () => {
if (!code.trim()) {
return;
}
setRedeeming(true);
dispatch(redeemGiftCode(code.trim()))
.then(() => {
enqueueSnackbar(t("setting.giftCodeRedeemed"), { variant: "success" });
setCode("");
loadInfo();
dispatch(getCreditTxns(page, pageSize)).then((res) => setTxns(res));
})
.finally(() => setRedeeming(false));
};
const txnReason = (type: string) => t(`setting.txnType.${type}`, { defaultValue: type });
if (!info) {
@ -120,21 +99,10 @@ const FinanceSetting = () => {
)}
<SettingForm title={t("setting.redeemGiftCode")} lgWidth={5}>
<DenseFilledTextField
fullWidth
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder={t("setting.giftCodePlaceholder")}
slotProps={{
input: {
endAdornment: (
<InputAdornment position="end">
<Button variant="contained" onClick={onRedeem} disabled={redeeming || !code.trim()}>
{t("setting.redeem")}
</Button>
</InputAdornment>
),
},
<RedeemCodeInput
onRedeemed={() => {
loadInfo();
dispatch(getCreditTxns(page, pageSize)).then((res) => setTxns(res));
}}
/>
<NoMarginHelperText>{t("setting.redeemGiftCodeDes")}</NoMarginHelperText>

@ -0,0 +1,152 @@
import { Button, Chip, Container, Grid2, Paper, Stack, Typography } from "@mui/material";
import dayjs from "dayjs";
import { useSnackbar } from "notistack";
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useSearchParams } from "react-router-dom";
import { getCredit, getShopSkus, purchaseSku } from "../../../api/api.ts";
import { CreditInfo, ShopSku } from "../../../api/user.ts";
import { useAppDispatch } from "../../../redux/hooks.ts";
import { sizeToString } from "../../../util/index.ts";
import { formatDuration } from "../../../util/datetime.ts";
import FacebookCircularProgress from "../../Common/CircularProgress.tsx";
import RedeemCodeInput from "../../Common/Form/RedeemCodeInput.tsx";
import Nothing from "../../Common/Nothing.tsx";
import ResponsiveTabs from "../../Common/ResponsiveTabs.tsx";
import PageContainer from "../PageContainer.tsx";
import PageHeader, { PageTabQuery } from "../PageHeader.tsx";
export enum ShopPageTab {
Membership = "membership",
Storage = "storage",
Redeem = "redeem",
}
const Shop = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const { enqueueSnackbar } = useSnackbar();
const [searchParams] = useSearchParams();
const [tab, setTab] = useState(searchParams.get(PageTabQuery) ?? ShopPageTab.Membership);
const [skus, setSkus] = useState<ShopSku[] | undefined>(undefined);
const [info, setInfo] = useState<CreditInfo | undefined>(undefined);
const [buying, setBuying] = useState<string | undefined>(undefined);
const loadInfo = () => {
dispatch(getCredit()).then((res) => setInfo(res));
};
useEffect(() => {
dispatch(getShopSkus()).then((res) => setSkus(res));
loadInfo();
}, []);
const tabs = useMemo(
() => [
{ label: t("application:shop.memberships"), value: ShopPageTab.Membership },
{ label: t("application:shop.storagePacks"), value: ShopPageTab.Storage },
{ label: t("application:shop.redeem"), value: ShopPageTab.Redeem },
],
[],
);
const filtered = useMemo(
() =>
(skus ?? []).filter((s) =>
tab === ShopPageTab.Membership ? s.type === "group" : s.type === "storage",
),
[skus, tab],
);
const onPurchase = (s: ShopSku) => {
setBuying(s.id);
dispatch(purchaseSku(s.id))
.then((res) => {
setInfo(res);
enqueueSnackbar(t("shop.purchased", { name: s.name }), { variant: "success" });
})
.finally(() => setBuying(undefined));
};
const skuSubtitle = (s: ShopSku) => {
const parts = [
s.type === "storage" ? sizeToString(s.amount) : s.group,
s.duration > 0 ? formatDuration(dayjs.duration(s.duration, "seconds")) : t("shop.permanent"),
];
return parts.filter(Boolean).join(" · ");
};
return (
<PageContainer>
<Container maxWidth="lg">
<PageHeader
title={t("application:navbar.shop")}
secondaryAction={
info ? <Chip variant="outlined" label={t("shop.balance", { credits: info.credits })} /> : undefined
}
/>
<ResponsiveTabs value={tab} onChange={(_e, v) => setTab(v)} tabs={tabs} />
{tab === ShopPageTab.Redeem && (
<Paper variant="outlined" sx={{ p: 2, maxWidth: 480 }}>
<Typography variant="subtitle2" gutterBottom>
{t("setting.redeemGiftCode")}
</Typography>
<RedeemCodeInput onRedeemed={loadInfo} />
</Paper>
)}
{tab !== ShopPageTab.Redeem && skus === undefined && (
<Grid2 container sx={{ pt: 10, justifyContent: "center" }}>
<FacebookCircularProgress />
</Grid2>
)}
{tab !== ShopPageTab.Redeem && skus !== undefined && filtered.length === 0 && (
<Nothing primary={t("shop.noProducts")} />
)}
{tab !== ShopPageTab.Redeem && skus !== undefined && filtered.length > 0 && (
<Grid2 container spacing={2} sx={{ pt: 2 }}>
{filtered.map((s) => (
<Grid2 key={s.id} size={{ xs: 12, sm: 6, md: 4 }}>
<Paper variant="outlined" sx={{ p: 2, height: "100%" }}>
<Stack spacing={1.5} sx={{ height: "100%" }}>
<Stack direction="row" spacing={1} alignItems="center">
<Typography variant="subtitle1" sx={{ flexGrow: 1 }}>
{s.name}
</Typography>
{s.label && <Chip size="small" color="primary" label={s.label} />}
</Stack>
<Typography variant="body2" color="text.secondary">
{skuSubtitle(s)}
</Typography>
{s.des && (
<Stack spacing={0.5} sx={{ flexGrow: 1 }}>
{s.des
.split("\n")
.filter((l) => l.trim())
.map((l, i) => (
<Typography key={i} variant="body2" color="text.secondary">
{l}
</Typography>
))}
</Stack>
)}
<Button
variant="contained"
disabled={s.points == null || buying === s.id || (info != null && info.credits < s.points)}
onClick={() => onPurchase(s)}
>
{s.points != null
? t("shop.buyWithPoints", { points: s.points })
: t("shop.pointsUnavailable")}
</Button>
</Stack>
</Paper>
</Grid2>
))}
</Grid2>
)}
</Container>
</PageContainer>
);
};
export default Shop;

@ -272,6 +272,13 @@ export const router = createBrowserRouter([
return { Component: Devices };
},
},
{
path: "/shop",
async lazy() {
let { Shop } = await import("../component/Pages/Pages");
return { Component: Shop };
},
},
{
path: "/settings",
async lazy() {

@ -3,12 +3,14 @@ package inventory
import (
"context"
"errors"
"strconv"
"time"
"github.com/cloudreve/Cloudreve/v4/ent"
"github.com/cloudreve/Cloudreve/v4/ent/credittxn"
"github.com/cloudreve/Cloudreve/v4/ent/giftcode"
"github.com/cloudreve/Cloudreve/v4/ent/schema"
"github.com/cloudreve/Cloudreve/v4/ent/sku"
"github.com/cloudreve/Cloudreve/v4/ent/user"
"github.com/cloudreve/Cloudreve/v4/ent/usergrant"
"github.com/cloudreve/Cloudreve/v4/pkg/conf"
@ -18,6 +20,7 @@ import (
var (
ErrGiftCodeNotFound = errors.New("gift code not found")
ErrGiftCodeUsed = errors.New("gift code already used")
ErrSkuNotPurchasable = errors.New("sku not purchasable with points")
)
type (
@ -49,6 +52,20 @@ type (
// upgrades whose users still sit on the granted group. Users without a
// recorded previous group fall back to defaultGroupID.
ExpireGrants(ctx context.Context, defaultGroupID int) error
// ListSkus returns products ordered by weight desc then id. When
// onlyEnabled is set, disabled products are excluded.
ListSkus(ctx context.Context, onlyEnabled bool) ([]*ent.Sku, error)
// GetSku returns one product by id.
GetSku(ctx context.Context, id int) (*ent.Sku, error)
// UpsertSku creates the product when its ID is zero, otherwise
// updates the mutable product fields.
UpsertSku(ctx context.Context, sku *ent.Sku) (*ent.Sku, error)
// DeleteSkus removes products by id.
DeleteSkus(ctx context.Context, ids []int) error
// PurchaseSku atomically debits the sku points price and applies its
// grant. Fails with ErrInsufficientPoints when the balance cannot
// cover the price, leaving the grant unapplied.
PurchaseSku(ctx context.Context, userID int, sku *ent.Sku) error
}
CreateGiftCodeParams struct {
@ -228,15 +245,18 @@ func (c *vasClient) RedeemGiftCode(ctx context.Context, userID int, code string)
}
func (c *vasClient) applyGroupCode(ctx context.Context, userID int, gc *ent.GiftCode) error {
return c.applyGroupGrant(ctx, userID, int(gc.Amount), gc.Duration)
}
func (c *vasClient) applyGroupGrant(ctx context.Context, userID, targetGroup int, durationSeconds int64) error {
u, err := c.client.User.Get(ctx, userID)
if err != nil {
return err
}
targetGroup := int(gc.Amount)
if _, err := c.client.Group.Get(ctx, targetGroup); err != nil {
return err
}
if err := c.createGrant(ctx, userID, usergrant.TypeGroup, gc.Amount, gc.Duration, u.GroupUsers); err != nil {
if err := c.createGrant(ctx, userID, usergrant.TypeGroup, int64(targetGroup), durationSeconds, u.GroupUsers); err != nil {
return err
}
return c.client.User.Update().Where(user.ID(userID)).SetGroupUsers(targetGroup).Exec(ctx)
@ -320,6 +340,83 @@ func (c *vasClient) ExpireGrants(ctx context.Context, defaultGroupID int) error
return nil
}
func (c *vasClient) ListSkus(ctx context.Context, onlyEnabled bool) ([]*ent.Sku, error) {
q := c.client.Sku.Query()
if onlyEnabled {
q = q.Where(sku.Enabled(true))
}
return q.Order(ent.Desc(sku.FieldWeight), ent.Asc(sku.FieldID)).All(ctx)
}
func (c *vasClient) GetSku(ctx context.Context, id int) (*ent.Sku, error) {
return c.client.Sku.Get(ctx, id)
}
func (c *vasClient) UpsertSku(ctx context.Context, s *ent.Sku) (*ent.Sku, error) {
if s.ID == 0 {
return c.client.Sku.Create().
SetName(s.Name).
SetType(s.Type).
SetAmount(s.Amount).
SetDuration(s.Duration).
SetPrice(s.Price).
SetNillablePoints(s.Points).
SetLabel(s.Label).
SetDes(s.Des).
SetEnabled(s.Enabled).
SetWeight(s.Weight).
Save(ctx)
}
return c.client.Sku.UpdateOneID(s.ID).
SetName(s.Name).
SetType(s.Type).
SetAmount(s.Amount).
SetDuration(s.Duration).
SetPrice(s.Price).
SetNillablePoints(s.Points).
SetLabel(s.Label).
SetDes(s.Des).
SetEnabled(s.Enabled).
SetWeight(s.Weight).
Save(ctx)
}
func (c *vasClient) DeleteSkus(ctx context.Context, ids []int) error {
_, err := c.client.Sku.Delete().
Where(sku.IDIn(ids...)).
Exec(schema.SkipSoftDelete(ctx))
return err
}
func (c *vasClient) PurchaseSku(ctx context.Context, userID int, s *ent.Sku) error {
if s.Points == nil {
return ErrSkuNotPurchasable
}
txVc, tx, ctx, err := WithTx(ctx, c)
if err != nil {
return err
}
if err := txVc.CreditAdjust(ctx, userID, -*s.Points, credittxn.TypePurchase,
strconv.Itoa(s.ID), s.Name); err != nil {
_ = Rollback(tx)
return err
}
switch s.Type {
case sku.TypeStorage:
err = txVc.createGrant(ctx, userID, usergrant.TypeStorage, s.Amount, s.Duration, 0)
case sku.TypeGroup:
err = txVc.applyGroupGrant(ctx, userID, int(s.Amount), s.Duration)
}
if err != nil {
return Rollback(tx)
}
return Commit(tx)
}
func newGiftCodeString() string {
return uuid.Must(uuid.NewV4()).String()
}

@ -10,6 +10,7 @@ import (
"github.com/cloudreve/Cloudreve/v4/ent/credittxn"
"github.com/cloudreve/Cloudreve/v4/ent/enttest"
"github.com/cloudreve/Cloudreve/v4/ent/giftcode"
"github.com/cloudreve/Cloudreve/v4/ent/sku"
"github.com/cloudreve/Cloudreve/v4/ent/usergrant"
"github.com/cloudreve/Cloudreve/v4/pkg/boolset"
"github.com/stretchr/testify/require"
@ -185,3 +186,56 @@ func TestDeleteGiftCodes(t *testing.T) {
// Used code survives revocation; unused is gone.
require.Equal(t, 1, client.GiftCode.Query().CountX(ctx))
}
func TestPurchaseSku(t *testing.T) {
ctx := context.Background()
client, c := newVasClient(t)
group, u := vasFixture(t, client)
vip := client.Group.Create().SetName("vip").SetPermissions(&boolset.BooleanSet{}).SaveX(ctx)
require.NoError(t, c.CreditAdjust(ctx, u.ID, 500, credittxn.TypeAdjust, "", "seed"))
points := int64(200)
storageSku, err := c.UpsertSku(ctx, &ent.Sku{
Name: "1GB pack", Type: sku.TypeStorage, Amount: 1024,
Duration: int64(time.Hour.Seconds()), Points: &points, Enabled: true,
})
require.NoError(t, err)
require.NoError(t, c.PurchaseSku(ctx, u.ID, storageSku))
u = client.User.GetX(ctx, u.ID)
require.Equal(t, int64(300), u.Credits)
bonus, err := c.StorageBonus(ctx, u.ID)
require.NoError(t, err)
require.Equal(t, int64(1024), bonus)
txns, _, _ := c.ListCreditTxns(ctx, u.ID, 1, 10)
require.Equal(t, credittxn.TypePurchase, txns[0].Type)
require.Equal(t, int64(-200), txns[0].Amount)
// Group sku → user moved, prev group recorded.
groupSku, err := c.UpsertSku(ctx, &ent.Sku{
Name: "vip month", Type: sku.TypeGroup, Amount: int64(vip.ID),
Duration: int64(time.Hour.Seconds()), Points: &points, Enabled: true,
})
require.NoError(t, err)
require.NoError(t, c.PurchaseSku(ctx, u.ID, groupSku))
require.Equal(t, vip.ID, client.User.GetX(ctx, u.ID).GroupUsers)
grant := client.UserGrant.Query().Where(usergrant.TypeEQ(usergrant.TypeGroup)).OnlyX(ctx)
require.Equal(t, group.ID, grant.PrevGroupID)
// No points price → not purchasable.
cashOnly, err := c.UpsertSku(ctx, &ent.Sku{
Name: "cash only", Type: sku.TypeStorage, Amount: 1, Price: 700, Enabled: true,
})
require.NoError(t, err)
require.ErrorIs(t, c.PurchaseSku(ctx, u.ID, cashOnly), ErrSkuNotPurchasable)
// Insufficient balance → no grant applied.
balance := client.User.GetX(ctx, u.ID).Credits
require.NoError(t, c.CreditAdjust(ctx, u.ID, -balance, credittxn.TypeAdjust, "", "drain"))
require.ErrorIs(t, c.PurchaseSku(ctx, u.ID, storageSku), ErrInsufficientPoints)
bonus, err = c.StorageBonus(ctx, u.ID)
require.NoError(t, err)
require.Equal(t, int64(1024), bonus)
}

@ -23,6 +23,7 @@ const (
DavAccountID
PaymentID
InvitationCodeID
SkuID
)
var (
@ -146,6 +147,11 @@ func EncodeInvitationCodeID(encoder Encoder, id int) string {
return encodeID(encoder, id, InvitationCodeID)
}
// EncodeSkuID encode sku id to hash id
func EncodeSkuID(encoder Encoder, id int) string {
return encodeID(encoder, id, SkuID)
}
func FromContext(c context.Context) int {
return c.Value(ObjectIDCtx{}).(int)
}

@ -333,6 +333,42 @@ func AdminAdjustCredit(c *gin.Context) {
c.JSON(200, serializer.Response{})
}
func AdminListSkus(c *gin.Context) {
service := ParametersFromContext[*admin.SkuListService](c, admin.SkuListParamCtx{})
res, err := service.List(c)
if respondErr(c, err) {
return
}
c.JSON(200, serializer.Response{Data: res})
}
func AdminCreateSku(c *gin.Context) {
service := ParametersFromContext[*admin.SkuUpsertService](c, admin.SkuUpsertParamCtx{})
res, err := service.Create(c)
if respondErr(c, err) {
return
}
c.JSON(200, serializer.Response{Data: res})
}
func AdminUpdateSku(c *gin.Context) {
service := ParametersFromContext[*admin.SkuUpsertService](c, admin.SkuUpsertParamCtx{})
res, err := service.Update(c)
if respondErr(c, err) {
return
}
c.JSON(200, serializer.Response{Data: res})
}
func AdminDeleteSku(c *gin.Context) {
service := ParametersFromContext[*admin.SingleSkuService](c, admin.SingleSkuParamCtx{})
err := service.Delete(c)
if respondErr(c, err) {
return
}
c.JSON(200, serializer.Response{})
}
func AdminListEvents(c *gin.Context) {
service := ParametersFromContext[*admin.EventListService](c, admin.EventListParamCtx{})
res, err := service.Get(c)

@ -291,6 +291,26 @@ func UserRedeemGiftCode(c *gin.Context) {
c.JSON(200, serializer.Response{Data: res})
}
// UserListSkus lists enabled products for the shop page.
func UserListSkus(c *gin.Context) {
service := ParametersFromContext[*user.SkuListService](c, user.SkuListParamCtx{})
res, err := service.List(c)
if respondErr(c, err) {
return
}
c.JSON(200, serializer.Response{Data: res})
}
// UserPurchaseSku buys a product with credit points.
func UserPurchaseSku(c *gin.Context) {
service := ParametersFromContext[*user.PurchaseSkuService](c, user.PurchaseSkuParamCtx{})
res, err := service.Create(c)
if respondErr(c, err) {
return
}
c.JSON(200, serializer.Response{Data: res})
}
// UserInit2FA 初始化二步验证
func UserInit2FA(c *gin.Context) {
secret, err := user.Init2FA(c)

@ -1322,6 +1322,29 @@ func initMasterRouter(dep dependency.Dep) *gin.Engine {
controllers.FromJSON[adminsvc.AdjustCreditService](adminsvc.AdjustCreditParamCtx{}),
controllers.AdminAdjustCredit,
)
// 列出商品
vas.GET("sku",
controllers.FromQuery[adminsvc.SkuListService](adminsvc.SkuListParamCtx{}),
controllers.AdminListSkus,
)
// 创建商品
vas.PUT("sku",
middleware.RequiredScopes(types.ScopeAdminWrite),
controllers.FromJSON[adminsvc.SkuUpsertService](adminsvc.SkuUpsertParamCtx{}),
controllers.AdminCreateSku,
)
// 更新商品
vas.PUT("sku/:id",
middleware.RequiredScopes(types.ScopeAdminWrite),
controllers.FromJSON[adminsvc.SkuUpsertService](adminsvc.SkuUpsertParamCtx{}),
controllers.AdminUpdateSku,
)
// 删除商品
vas.DELETE("sku/:id",
middleware.RequiredScopes(types.ScopeAdminWrite),
controllers.FromUri[adminsvc.SingleSkuService](adminsvc.SingleSkuParamCtx{}),
controllers.AdminDeleteSku,
)
}
event := admin.Group("event", middleware.AdminSection(types.GroupPermissionAdminEvents))
@ -1516,6 +1539,23 @@ func initMasterRouter(dep dependency.Dep) *gin.Engine {
controllers.UserRedeemGiftCode,
)
}
// 积分商城
shop := user.Group("shop")
{
// 列出商品
shop.GET("skus",
middleware.RequiredScopes(types.ScopeUserInfoRead),
controllers.FromQuery[usersvc.SkuListService](usersvc.SkuListParamCtx{}),
controllers.UserListSkus,
)
// 积分购买商品
shop.POST("purchase",
middleware.RequiredScopes(types.ScopeUserInfoWrite),
controllers.FromJSON[usersvc.PurchaseSkuService](usersvc.PurchaseSkuParamCtx{}),
controllers.UserPurchaseSku,
)
}
}
// WebDAV and devices

@ -8,6 +8,7 @@ import (
"github.com/cloudreve/Cloudreve/v4/ent"
"github.com/cloudreve/Cloudreve/v4/ent/credittxn"
"github.com/cloudreve/Cloudreve/v4/ent/giftcode"
"github.com/cloudreve/Cloudreve/v4/ent/sku"
"github.com/cloudreve/Cloudreve/v4/inventory"
"github.com/cloudreve/Cloudreve/v4/inventory/types"
"github.com/cloudreve/Cloudreve/v4/pkg/activity"
@ -16,6 +17,22 @@ import (
)
type (
// SkuListService lists all products for admins.
SkuListService struct{}
SkuListParamCtx struct{}
// SkuUpsertService creates or updates one product. ID zero creates.
SkuUpsertService struct {
Sku *ent.Sku `json:"sku" binding:"required"`
}
SkuUpsertParamCtx struct{}
// SingleSkuService targets one product by ID.
SingleSkuService struct {
ID int `uri:"id" json:"id" binding:"required"`
}
SingleSkuParamCtx struct{}
// GiftCodeListService lists gift codes for admins.
GiftCodeListService struct {
Page int `form:"page" json:"page" binding:"required,min=1"`
@ -94,6 +111,51 @@ func (service *SingleGiftCodeService) Delete(c *gin.Context) error {
return nil
}
func (service *SkuListService) List(c *gin.Context) ([]*ent.Sku, error) {
skus, err := dependency.FromContext(c).VasClient().ListSkus(c, false)
if err != nil {
return nil, serializer.NewError(serializer.CodeDBError, "Failed to list products", err)
}
return skus, nil
}
func (service *SkuUpsertService) Update(c *gin.Context) (*ent.Sku, error) {
return upsertSku(c, service.Sku)
}
func (service *SkuUpsertService) Create(c *gin.Context) (*ent.Sku, error) {
return upsertSku(c, service.Sku)
}
func upsertSku(c *gin.Context, s *ent.Sku) (*ent.Sku, error) {
dep := dependency.FromContext(c)
if s.Name == "" || s.Amount <= 0 || s.Duration < 0 || s.Price < 0 || s.Weight < 0 {
return nil, serializer.NewError(serializer.CodeParamErr, "Invalid product fields", nil)
}
if s.Points != nil && *s.Points <= 0 {
return nil, serializer.NewError(serializer.CodeParamErr, "Invalid points price", nil)
}
if s.Type == sku.TypeGroup {
if _, err := dep.GroupClient().GetByID(c, int(s.Amount)); err != nil {
return nil, serializer.NewError(serializer.CodeParamErr, "Invalid target group", err)
}
}
res, err := dep.VasClient().UpsertSku(c, s)
if err != nil {
return nil, serializer.NewError(serializer.CodeDBError, "Failed to save product", err)
}
return res, nil
}
func (service *SingleSkuService) Delete(c *gin.Context) error {
if err := dependency.FromContext(c).VasClient().DeleteSkus(c, []int{service.ID}); err != nil {
return serializer.NewError(serializer.CodeDBError, "Failed to delete product", err)
}
return nil
}
func (service *AdjustCreditService) Create(c *gin.Context) error {
dep := dependency.FromContext(c)

@ -6,9 +6,11 @@ import (
"github.com/cloudreve/Cloudreve/v4/application/dependency"
"github.com/cloudreve/Cloudreve/v4/ent"
"github.com/cloudreve/Cloudreve/v4/ent/credittxn"
"github.com/cloudreve/Cloudreve/v4/ent/sku"
"github.com/cloudreve/Cloudreve/v4/inventory"
"github.com/cloudreve/Cloudreve/v4/inventory/types"
"github.com/cloudreve/Cloudreve/v4/pkg/activity"
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
"github.com/gin-gonic/gin"
)
@ -31,6 +33,32 @@ type (
}
RedeemGiftCodeParamCtx struct{}
// SkuListService lists enabled products for the shop page.
SkuListService struct{}
SkuListParamCtx struct{}
// PurchaseSkuService buys a product with credit points.
PurchaseSkuService struct {
Sku string `json:"sku" form:"sku" binding:"required,max=64"`
}
PurchaseSkuParamCtx struct{}
// SkuResponse is the public product view; amount carries the target
// group hashid for membership products, bytes for storage packs.
SkuResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Amount int64 `json:"amount"`
Group string `json:"group,omitempty"`
GroupID string `json:"group_id,omitempty"`
Duration int64 `json:"duration"`
Price int64 `json:"price"`
Points *int64 `json:"points,omitempty"`
Label string `json:"label,omitempty"`
Des string `json:"des,omitempty"`
}
CreditResponse struct {
Credits int64 `json:"credits"`
StorageBonus int64 `json:"storage_bonus"`
@ -97,6 +125,68 @@ func (service *RedeemGiftCodeService) Create(c *gin.Context) (*ent.GiftCode, err
return gc, nil
}
func (service *SkuListService) List(c *gin.Context) ([]*SkuResponse, error) {
dep := dependency.FromContext(c)
skus, err := dep.VasClient().ListSkus(c, true)
if err != nil {
return nil, serializer.NewError(serializer.CodeDBError, "Failed to list products", err)
}
res := make([]*SkuResponse, 0, len(skus))
for _, s := range skus {
r := &SkuResponse{
ID: hashid.EncodeSkuID(dep.HashIDEncoder(), s.ID),
Name: s.Name,
Type: string(s.Type),
Amount: s.Amount,
Duration: s.Duration,
Price: s.Price,
Points: s.Points,
Label: s.Label,
Des: s.Des,
}
if s.Type == sku.TypeGroup {
if g, err := dep.GroupClient().GetByID(c, int(s.Amount)); err == nil {
r.Group = g.Name
r.GroupID = hashid.EncodeGroupID(dep.HashIDEncoder(), g.ID)
}
}
res = append(res, r)
}
return res, nil
}
func (service *PurchaseSkuService) Create(c *gin.Context) (*CreditResponse, error) {
dep := dependency.FromContext(c)
u := inventory.UserFromContext(c)
skuID, err := dep.HashIDEncoder().Decode(service.Sku, hashid.SkuID)
if err != nil {
return nil, serializer.NewError(serializer.CodeParamErr, "Invalid product", err)
}
s, err := dep.VasClient().GetSku(c, skuID)
if err != nil || !s.Enabled {
return nil, serializer.NewError(serializer.CodeNotFound, "Product not found", err)
}
if err := dep.VasClient().PurchaseSku(c, u.ID, s); err != nil {
switch {
case errors.Is(err, inventory.ErrSkuNotPurchasable):
return nil, serializer.NewError(serializer.CodeParamErr, "Product cannot be purchased with points", err)
case errors.Is(err, inventory.ErrInsufficientPoints):
return nil, serializer.NewError(serializer.CodeParamErr, "Insufficient credit balance", err)
default:
return nil, serializer.NewError(serializer.CodeDBError, "Failed to purchase product", err)
}
}
activity.Record(c, dep.SettingProvider(), dep.ActivityClient(), types.EventPointsChange,
activity.Extra(map[string]any{"sku": s.ID, "name": s.Name, "delta": -*s.Points}))
return (&CreditService{}).Get(c)
}
// TxnDescription maps ledger types to stable English descriptors consumed by
// the Finance settings tab.
func TxnDescription(t *ent.CreditTxn) string {

Loading…
Cancel
Save