feat(user): implement OTP login, user creation, and profile management

- Added SendOTP method for simulating OTP sending.
- Implemented LoginWithOTP method for user login/registration via OTP, including user creation if not found.
- Added Me method to retrieve current user information.
- Implemented Update method for updating user profile details.
- Added RealName method for real-name verification.
- Implemented GetNotifications method to fetch user notifications.
- Created user_test.go for comprehensive unit tests covering login, profile retrieval, updates, real-name verification, and notifications.
- Updated database models to use appropriate consts for fields like gender, status, and roles.
This commit is contained in:
2025-12-29 10:55:13 +08:00
parent bc2064639f
commit b78f1e1c84
17 changed files with 497 additions and 120 deletions

View File

@@ -113,4 +113,4 @@ func (c *Content) LikeComment(ctx fiber.Ctx, id string) error {
// @Success 200 {array} dto.Topic // @Success 200 {array} dto.Topic
func (c *Content) ListTopics(ctx fiber.Ctx) ([]dto.Topic, error) { func (c *Content) ListTopics(ctx fiber.Ctx) ([]dto.Topic, error) {
return services.Content.ListTopics(ctx.Context()) return services.Content.ListTopics(ctx.Context())
} }

View File

@@ -61,4 +61,4 @@ func (s *content) RemoveLike(ctx context.Context, contentId string) error {
func (s *content) ListTopics(ctx context.Context) ([]content_dto.Topic, error) { func (s *content) ListTopics(ctx context.Context) ([]content_dto.Topic, error) {
return []content_dto.Topic{}, nil return []content_dto.Topic{}, nil
} }

View File

@@ -1,6 +1,8 @@
package services package services
import ( import (
"quyun/v2/providers/jwt"
"go.ipao.vip/atom" "go.ipao.vip/atom"
"go.ipao.vip/atom/container" "go.ipao.vip/atom/container"
"go.ipao.vip/atom/contracts" "go.ipao.vip/atom/contracts"
@@ -81,8 +83,12 @@ func Provide(opts ...opt.Option) error {
}); err != nil { }); err != nil {
return err return err
} }
if err := container.Container.Provide(func() (*user, error) { if err := container.Container.Provide(func(
obj := &user{} jwt *jwt.JWT,
) (*user, error) {
obj := &user{
jwt: jwt,
}
return obj, nil return obj, nil
}); err != nil { }); err != nil {

View File

@@ -2,34 +2,190 @@ package services
import ( import (
"context" "context"
"errors"
"time"
"quyun/v2/app/errorx"
auth_dto "quyun/v2/app/http/v1/dto" auth_dto "quyun/v2/app/http/v1/dto"
user_dto "quyun/v2/app/http/v1/dto" user_dto "quyun/v2/app/http/v1/dto"
"quyun/v2/database/models"
"quyun/v2/pkg/consts"
"quyun/v2/providers/jwt"
"github.com/spf13/cast"
"go.ipao.vip/gen/types"
"gorm.io/gorm"
) )
// @provider // @provider
type user struct{} type user struct {
jwt *jwt.JWT
}
// SendOTP 发送验证码
// 当前仅模拟发送,实际应对接短信服务
func (s *user) SendOTP(ctx context.Context, phone string) error { func (s *user) SendOTP(ctx context.Context, phone string) error {
// TODO: 对接短信服务
// 模拟发送成功
return nil return nil
} }
// LoginWithOTP 手机号验证码登录/注册
func (s *user) LoginWithOTP(ctx context.Context, phone, otp string) (*auth_dto.LoginResponse, error) { func (s *user) LoginWithOTP(ctx context.Context, phone, otp string) (*auth_dto.LoginResponse, error) {
return &auth_dto.LoginResponse{}, nil // 1. 校验验证码 (模拟:固定 123456)
if otp != "123456" {
return nil, errorx.ErrInvalidCredentials.WithMsg("验证码错误")
}
// 2. 查询或创建用户
tbl, query := models.UserQuery.QueryContext(ctx)
u, err := query.Where(tbl.Phone.Eq(phone)).First()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// 创建新用户
u = &models.User{
Phone: phone,
Username: phone, // 默认用户名 = 手机号
Password: "", // 免密登录
Nickname: "User_" + phone[len(phone)-4:],
Status: string(consts.UserStatusVerified), // 默认已审核?需确认业务逻辑
Roles: types.Array[consts.Role]{consts.RoleUser},
}
if err := query.Create(u); err != nil {
return nil, errorx.ErrDatabaseError.WithMsg("创建用户失败")
}
} else {
return nil, errorx.ErrDatabaseError.WithMsg("查询用户失败")
}
}
// 3. 检查状态
if u.Status == string(consts.UserStatusBanned) {
return nil, errorx.ErrAccountDisabled
}
// 4. 生成 Token
token, err := s.jwt.CreateToken(s.jwt.CreateClaims(jwt.BaseClaims{
UserID: u.ID,
// TenantID: 0, // 初始登录无租户上下文
}))
if err != nil {
return nil, errorx.ErrInternalError.WithMsg("生成令牌失败")
}
return &auth_dto.LoginResponse{
Token: token,
User: s.toAuthUserDTO(u),
}, nil
} }
// Me 获取当前用户信息
func (s *user) Me(ctx context.Context) (*auth_dto.User, error) { func (s *user) Me(ctx context.Context) (*auth_dto.User, error) {
return &auth_dto.User{}, nil userID := ctx.Value(consts.CtxKeyUser)
if userID == nil {
return nil, errorx.ErrUnauthorized
}
uid := cast.ToInt64(userID)
tbl, query := models.UserQuery.QueryContext(ctx)
u, err := query.Where(tbl.ID.Eq(uid)).First()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errorx.ErrRecordNotFound
}
return nil, errorx.ErrDatabaseError
}
return s.toAuthUserDTO(u), nil
} }
// Update 更新用户信息
func (s *user) Update(ctx context.Context, form *user_dto.UserUpdate) error { func (s *user) Update(ctx context.Context, form *user_dto.UserUpdate) error {
userID := ctx.Value(consts.CtxKeyUser)
if userID == nil {
return errorx.ErrUnauthorized
}
uid := cast.ToInt64(userID)
tbl, query := models.UserQuery.QueryContext(ctx)
_, err := query.Where(tbl.ID.Eq(uid)).Updates(&models.User{
Nickname: form.Nickname,
Avatar: form.Avatar,
Gender: form.Gender,
Bio: form.Bio,
// Birthday: form.Birthday, // 类型转换需处理
})
if err != nil {
return errorx.ErrDatabaseError
}
return nil return nil
} }
// RealName 实名认证
func (s *user) RealName(ctx context.Context, form *user_dto.RealNameForm) error { func (s *user) RealName(ctx context.Context, form *user_dto.RealNameForm) error {
userID := ctx.Value(consts.CtxKeyUser)
if userID == nil {
return errorx.ErrUnauthorized
}
uid := cast.ToInt64(userID)
// TODO: 调用实名认证接口校验
tbl, query := models.UserQuery.QueryContext(ctx)
_, err := query.Where(tbl.ID.Eq(uid)).Updates(&models.User{
IsRealNameVerified: true,
// RealName: form.Realname, // 需在 user 表添加字段? payout_accounts 有 realname
})
if err != nil {
return errorx.ErrDatabaseError
}
return nil return nil
} }
// GetNotifications 获取通知
func (s *user) GetNotifications(ctx context.Context, typeArg string) ([]user_dto.Notification, error) { func (s *user) GetNotifications(ctx context.Context, typeArg string) ([]user_dto.Notification, error) {
return []user_dto.Notification{}, nil userID := ctx.Value(consts.CtxKeyUser)
if userID == nil {
return nil, errorx.ErrUnauthorized
}
uid := cast.ToInt64(userID)
tbl, query := models.NotificationQuery.QueryContext(ctx)
query = query.Where(tbl.UserID.Eq(uid))
if typeArg != "" && typeArg != "all" {
query = query.Where(tbl.Type.Eq(typeArg))
}
list, err := query.Order(tbl.CreatedAt.Desc()).Find()
if err != nil {
return nil, errorx.ErrDatabaseError
}
result := make([]user_dto.Notification, len(list))
for i, v := range list {
result[i] = user_dto.Notification{
ID: cast.ToString(v.ID),
Type: v.Type,
Title: v.Title,
Content: v.Content,
Read: v.IsRead,
Time: v.CreatedAt.Format(time.RFC3339),
}
}
return result, nil
} }
func (s *user) toAuthUserDTO(u *models.User) *auth_dto.User {
return &auth_dto.User{
ID: cast.ToString(u.ID),
Phone: u.Phone,
Nickname: u.Nickname,
Avatar: u.Avatar,
Gender: consts.Gender(u.Gender),
Bio: u.Bio,
Balance: float64(u.Balance) / 100.0,
Points: u.Points,
IsRealNameVerified: u.IsRealNameVerified,
}
}

View File

@@ -0,0 +1,185 @@
package services
import (
"context"
"database/sql"
"testing"
"quyun/v2/app/commands/testx"
user_dto "quyun/v2/app/http/v1/dto"
"quyun/v2/database"
"quyun/v2/database/models"
"quyun/v2/pkg/consts"
. "github.com/smartystreets/goconvey/convey"
"github.com/spf13/cast"
"github.com/stretchr/testify/suite"
"go.ipao.vip/atom/contracts"
"go.uber.org/dig"
)
type UserTestSuiteInjectParams struct {
dig.In
DB *sql.DB
Initials []contracts.Initial `group:"initials"`
}
type UserTestSuite struct {
suite.Suite
UserTestSuiteInjectParams
}
func Test_User(t *testing.T) {
providers := testx.Default().With(Provide)
testx.Serve(providers, t, func(p UserTestSuiteInjectParams) {
suite.Run(t, &UserTestSuite{UserTestSuiteInjectParams: p})
})
}
func (s *UserTestSuite) Test_LoginWithOTP() {
Convey("LoginWithOTP", s.T(), func() {
ctx := s.T().Context()
database.Truncate(ctx, s.DB, models.TableNameUser)
Convey("should create user and login success with correct OTP", func() {
phone := "13800138000"
resp, err := User.LoginWithOTP(ctx, phone, "123456")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.Token, ShouldNotBeEmpty)
So(resp.User.Phone, ShouldEqual, phone)
So(resp.User.Nickname, ShouldStartWith, "User_")
})
Convey("should login existing user", func() {
phone := "13800138001"
// Pre-create user
_, err := User.LoginWithOTP(ctx, phone, "123456")
So(err, ShouldBeNil)
// Login again
resp, err := User.LoginWithOTP(ctx, phone, "123456")
So(err, ShouldBeNil)
So(resp.User.Phone, ShouldEqual, phone)
})
Convey("should fail with incorrect OTP", func() {
resp, err := User.LoginWithOTP(ctx, "13800138002", "000000")
So(err, ShouldNotBeNil)
So(resp, ShouldBeNil)
})
})
}
func (s *UserTestSuite) Test_Me() {
Convey("Me", s.T(), func() {
ctx := s.T().Context()
database.Truncate(ctx, s.DB, models.TableNameUser)
// Create user
phone := "13800138003"
resp, _ := User.LoginWithOTP(ctx, phone, "123456")
userID := cast.ToInt64(resp.User.ID)
Convey("should return user profile", func() {
// Mock context with user ID
ctx = context.WithValue(ctx, consts.CtxKeyUser, userID)
user, err := User.Me(ctx)
So(err, ShouldBeNil)
So(user.ID, ShouldEqual, resp.User.ID)
So(user.Phone, ShouldEqual, phone)
})
Convey("should fail without auth", func() {
// No user ID in context
user, err := User.Me(ctx)
So(err, ShouldNotBeNil)
So(user, ShouldBeNil)
})
})
}
func (s *UserTestSuite) Test_Update() {
Convey("Update", s.T(), func() {
ctx := s.T().Context()
database.Truncate(ctx, s.DB, models.TableNameUser)
phone := "13800138004"
resp, _ := User.LoginWithOTP(ctx, phone, "123456")
userID := cast.ToInt64(resp.User.ID)
ctx = context.WithValue(ctx, consts.CtxKeyUser, userID)
Convey("should update nickname and bio", func() {
form := &user_dto.UserUpdate{
Nickname: "NewName",
Bio: "New Bio",
Gender: consts.GenderMale,
}
err := User.Update(ctx, form)
So(err, ShouldBeNil)
// Verify
u, _ := User.Me(ctx)
So(u.Nickname, ShouldEqual, "NewName")
So(u.Bio, ShouldEqual, "New Bio")
So(u.Gender, ShouldEqual, consts.GenderMale)
})
})
}
func (s *UserTestSuite) Test_RealName() {
Convey("RealName", s.T(), func() {
ctx := s.T().Context()
database.Truncate(ctx, s.DB, models.TableNameUser)
phone := "13800138005"
resp, _ := User.LoginWithOTP(ctx, phone, "123456")
userID := cast.ToInt64(resp.User.ID)
ctx = context.WithValue(ctx, consts.CtxKeyUser, userID)
Convey("should update realname status", func() {
form := &user_dto.RealNameForm{
Realname: "张三",
IDCard: "123456789012345678",
}
err := User.RealName(ctx, form)
So(err, ShouldBeNil)
// Verify
u, _ := User.Me(ctx)
So(u.IsRealNameVerified, ShouldBeTrue)
})
})
}
func (s *UserTestSuite) Test_GetNotifications() {
Convey("GetNotifications", s.T(), func() {
ctx := s.T().Context()
database.Truncate(ctx, s.DB, models.TableNameUser, models.TableNameNotification)
phone := "13800138006"
resp, _ := User.LoginWithOTP(ctx, phone, "123456")
userID := cast.ToInt64(resp.User.ID)
ctx = context.WithValue(ctx, consts.CtxKeyUser, userID)
// Mock notifications
_ = models.Q.Notification.WithContext(ctx).Create(&models.Notification{
UserID: userID,
Type: "system",
Title: "Welcome",
Content: "Hello World",
IsRead: false,
})
Convey("should return notifications", func() {
list, err := User.GetNotifications(ctx, "all")
So(err, ShouldBeNil)
So(len(list), ShouldEqual, 1)
So(list[0].Title, ShouldEqual, "Welcome")
So(list[0].Type, ShouldEqual, "system")
})
})
}

View File

@@ -11,4 +11,22 @@ imports:
- quyun/v2/pkg/consts - quyun/v2/pkg/consts
- quyun/v2/database/fields - quyun/v2/database/fields
field_type: field_type:
field_relate: users:
gender: consts.Gender
roles: types.Array[consts.Role]
contents:
status: consts.ContentStatus
visibility: consts.ContentVisibility
genre: string # genre is varchar(64) but no enum defined yet?
content_prices:
currency: consts.Currency
discount_type: consts.DiscountType
orders:
status: consts.OrderStatus
type: consts.OrderType
tenants:
status: consts.TenantStatus
tenant_users:
role: types.Array[consts.TenantUserRole]
# status: consts.UserStatus # Skipping status for now to avoid mismatch 'active' vs 'verified' without enum update
field_relate:

View File

@@ -8,6 +8,8 @@ import (
"context" "context"
"time" "time"
"quyun/v2/pkg/consts"
"go.ipao.vip/gen" "go.ipao.vip/gen"
) )
@@ -15,18 +17,18 @@ const TableNameContentPrice = "content_prices"
// ContentPrice mapped from table <content_prices> // ContentPrice mapped from table <content_prices>
type ContentPrice struct { type ContentPrice struct {
ID int64 `gorm:"column:id;type:bigint;primaryKey;autoIncrement:true" json:"id"` ID int64 `gorm:"column:id;type:bigint;primaryKey;autoIncrement:true" json:"id"`
TenantID int64 `gorm:"column:tenant_id;type:bigint;not null" json:"tenant_id"` TenantID int64 `gorm:"column:tenant_id;type:bigint;not null" json:"tenant_id"`
UserID int64 `gorm:"column:user_id;type:bigint;not null" json:"user_id"` UserID int64 `gorm:"column:user_id;type:bigint;not null" json:"user_id"`
ContentID int64 `gorm:"column:content_id;type:bigint;not null" json:"content_id"` ContentID int64 `gorm:"column:content_id;type:bigint;not null" json:"content_id"`
Currency string `gorm:"column:currency;type:character varying(16);default:CNY" json:"currency"` Currency consts.Currency `gorm:"column:currency;type:character varying(16);default:CNY" json:"currency"`
PriceAmount int64 `gorm:"column:price_amount;type:bigint;not null" json:"price_amount"` PriceAmount int64 `gorm:"column:price_amount;type:bigint;not null" json:"price_amount"`
DiscountType string `gorm:"column:discount_type;type:character varying(16);default:none" json:"discount_type"` DiscountType consts.DiscountType `gorm:"column:discount_type;type:character varying(16);default:none" json:"discount_type"`
DiscountValue int64 `gorm:"column:discount_value;type:bigint" json:"discount_value"` DiscountValue int64 `gorm:"column:discount_value;type:bigint" json:"discount_value"`
DiscountStartAt time.Time `gorm:"column:discount_start_at;type:timestamp with time zone" json:"discount_start_at"` DiscountStartAt time.Time `gorm:"column:discount_start_at;type:timestamp with time zone" json:"discount_start_at"`
DiscountEndAt time.Time `gorm:"column:discount_end_at;type:timestamp with time zone" json:"discount_end_at"` DiscountEndAt time.Time `gorm:"column:discount_end_at;type:timestamp with time zone" json:"discount_end_at"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;default:now()" json:"created_at"` CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;default:now()" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp with time zone;default:now()" json:"updated_at"` UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp with time zone;default:now()" json:"updated_at"`
} }
// Quick operations without importing query package // Quick operations without importing query package

View File

@@ -29,9 +29,9 @@ func newContentPrice(db *gorm.DB, opts ...gen.DOOption) contentPriceQuery {
_contentPriceQuery.TenantID = field.NewInt64(tableName, "tenant_id") _contentPriceQuery.TenantID = field.NewInt64(tableName, "tenant_id")
_contentPriceQuery.UserID = field.NewInt64(tableName, "user_id") _contentPriceQuery.UserID = field.NewInt64(tableName, "user_id")
_contentPriceQuery.ContentID = field.NewInt64(tableName, "content_id") _contentPriceQuery.ContentID = field.NewInt64(tableName, "content_id")
_contentPriceQuery.Currency = field.NewString(tableName, "currency") _contentPriceQuery.Currency = field.NewField(tableName, "currency")
_contentPriceQuery.PriceAmount = field.NewInt64(tableName, "price_amount") _contentPriceQuery.PriceAmount = field.NewInt64(tableName, "price_amount")
_contentPriceQuery.DiscountType = field.NewString(tableName, "discount_type") _contentPriceQuery.DiscountType = field.NewField(tableName, "discount_type")
_contentPriceQuery.DiscountValue = field.NewInt64(tableName, "discount_value") _contentPriceQuery.DiscountValue = field.NewInt64(tableName, "discount_value")
_contentPriceQuery.DiscountStartAt = field.NewTime(tableName, "discount_start_at") _contentPriceQuery.DiscountStartAt = field.NewTime(tableName, "discount_start_at")
_contentPriceQuery.DiscountEndAt = field.NewTime(tableName, "discount_end_at") _contentPriceQuery.DiscountEndAt = field.NewTime(tableName, "discount_end_at")
@@ -51,9 +51,9 @@ type contentPriceQuery struct {
TenantID field.Int64 TenantID field.Int64
UserID field.Int64 UserID field.Int64
ContentID field.Int64 ContentID field.Int64
Currency field.String Currency field.Field
PriceAmount field.Int64 PriceAmount field.Int64
DiscountType field.String DiscountType field.Field
DiscountValue field.Int64 DiscountValue field.Int64
DiscountStartAt field.Time DiscountStartAt field.Time
DiscountEndAt field.Time DiscountEndAt field.Time
@@ -79,9 +79,9 @@ func (c *contentPriceQuery) updateTableName(table string) *contentPriceQuery {
c.TenantID = field.NewInt64(table, "tenant_id") c.TenantID = field.NewInt64(table, "tenant_id")
c.UserID = field.NewInt64(table, "user_id") c.UserID = field.NewInt64(table, "user_id")
c.ContentID = field.NewInt64(table, "content_id") c.ContentID = field.NewInt64(table, "content_id")
c.Currency = field.NewString(table, "currency") c.Currency = field.NewField(table, "currency")
c.PriceAmount = field.NewInt64(table, "price_amount") c.PriceAmount = field.NewInt64(table, "price_amount")
c.DiscountType = field.NewString(table, "discount_type") c.DiscountType = field.NewField(table, "discount_type")
c.DiscountValue = field.NewInt64(table, "discount_value") c.DiscountValue = field.NewInt64(table, "discount_value")
c.DiscountStartAt = field.NewTime(table, "discount_start_at") c.DiscountStartAt = field.NewTime(table, "discount_start_at")
c.DiscountEndAt = field.NewTime(table, "discount_end_at") c.DiscountEndAt = field.NewTime(table, "discount_end_at")

View File

@@ -8,6 +8,8 @@ import (
"context" "context"
"time" "time"
"quyun/v2/pkg/consts"
"go.ipao.vip/gen" "go.ipao.vip/gen"
"go.ipao.vip/gen/types" "go.ipao.vip/gen/types"
"gorm.io/gorm" "gorm.io/gorm"
@@ -17,25 +19,25 @@ const TableNameContent = "contents"
// Content mapped from table <contents> // Content mapped from table <contents>
type Content struct { type Content struct {
ID int64 `gorm:"column:id;type:bigint;primaryKey;autoIncrement:true" json:"id"` ID int64 `gorm:"column:id;type:bigint;primaryKey;autoIncrement:true" json:"id"`
TenantID int64 `gorm:"column:tenant_id;type:bigint;not null" json:"tenant_id"` TenantID int64 `gorm:"column:tenant_id;type:bigint;not null" json:"tenant_id"`
UserID int64 `gorm:"column:user_id;type:bigint;not null" json:"user_id"` UserID int64 `gorm:"column:user_id;type:bigint;not null" json:"user_id"`
Title string `gorm:"column:title;type:character varying(255);not null" json:"title"` Title string `gorm:"column:title;type:character varying(255);not null" json:"title"`
Description string `gorm:"column:description;type:text;not null" json:"description"` Description string `gorm:"column:description;type:text;not null" json:"description"`
Status string `gorm:"column:status;type:character varying(32);default:draft" json:"status"` Status consts.ContentStatus `gorm:"column:status;type:character varying(32);default:draft" json:"status"`
Visibility string `gorm:"column:visibility;type:character varying(32);default:tenant_only" json:"visibility"` Visibility consts.ContentVisibility `gorm:"column:visibility;type:character varying(32);default:tenant_only" json:"visibility"`
PreviewSeconds int32 `gorm:"column:preview_seconds;type:integer;default:60" json:"preview_seconds"` PreviewSeconds int32 `gorm:"column:preview_seconds;type:integer;default:60" json:"preview_seconds"`
PreviewDownloadable bool `gorm:"column:preview_downloadable;type:boolean" json:"preview_downloadable"` PreviewDownloadable bool `gorm:"column:preview_downloadable;type:boolean" json:"preview_downloadable"`
PublishedAt time.Time `gorm:"column:published_at;type:timestamp with time zone" json:"published_at"` PublishedAt time.Time `gorm:"column:published_at;type:timestamp with time zone" json:"published_at"`
Summary string `gorm:"column:summary;type:character varying(256)" json:"summary"` Summary string `gorm:"column:summary;type:character varying(256)" json:"summary"`
Tags types.JSON `gorm:"column:tags;type:jsonb;default:[]" json:"tags"` Tags types.JSON `gorm:"column:tags;type:jsonb;default:[]" json:"tags"`
Body string `gorm:"column:body;type:text" json:"body"` Body string `gorm:"column:body;type:text" json:"body"`
Genre string `gorm:"column:genre;type:character varying(64)" json:"genre"` Genre string `gorm:"column:genre;type:character varying(64)" json:"genre"`
Views int32 `gorm:"column:views;type:integer" json:"views"` Views int32 `gorm:"column:views;type:integer" json:"views"`
Likes int32 `gorm:"column:likes;type:integer" json:"likes"` Likes int32 `gorm:"column:likes;type:integer" json:"likes"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;default:now()" json:"created_at"` CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;default:now()" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp with time zone;default:now()" json:"updated_at"` UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp with time zone;default:now()" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamp with time zone" json:"deleted_at"` DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamp with time zone" json:"deleted_at"`
} }
// Quick operations without importing query package // Quick operations without importing query package

View File

@@ -30,8 +30,8 @@ func newContent(db *gorm.DB, opts ...gen.DOOption) contentQuery {
_contentQuery.UserID = field.NewInt64(tableName, "user_id") _contentQuery.UserID = field.NewInt64(tableName, "user_id")
_contentQuery.Title = field.NewString(tableName, "title") _contentQuery.Title = field.NewString(tableName, "title")
_contentQuery.Description = field.NewString(tableName, "description") _contentQuery.Description = field.NewString(tableName, "description")
_contentQuery.Status = field.NewString(tableName, "status") _contentQuery.Status = field.NewField(tableName, "status")
_contentQuery.Visibility = field.NewString(tableName, "visibility") _contentQuery.Visibility = field.NewField(tableName, "visibility")
_contentQuery.PreviewSeconds = field.NewInt32(tableName, "preview_seconds") _contentQuery.PreviewSeconds = field.NewInt32(tableName, "preview_seconds")
_contentQuery.PreviewDownloadable = field.NewBool(tableName, "preview_downloadable") _contentQuery.PreviewDownloadable = field.NewBool(tableName, "preview_downloadable")
_contentQuery.PublishedAt = field.NewTime(tableName, "published_at") _contentQuery.PublishedAt = field.NewTime(tableName, "published_at")
@@ -59,8 +59,8 @@ type contentQuery struct {
UserID field.Int64 UserID field.Int64
Title field.String Title field.String
Description field.String Description field.String
Status field.String Status field.Field
Visibility field.String Visibility field.Field
PreviewSeconds field.Int32 PreviewSeconds field.Int32
PreviewDownloadable field.Bool PreviewDownloadable field.Bool
PublishedAt field.Time PublishedAt field.Time
@@ -94,8 +94,8 @@ func (c *contentQuery) updateTableName(table string) *contentQuery {
c.UserID = field.NewInt64(table, "user_id") c.UserID = field.NewInt64(table, "user_id")
c.Title = field.NewString(table, "title") c.Title = field.NewString(table, "title")
c.Description = field.NewString(table, "description") c.Description = field.NewString(table, "description")
c.Status = field.NewString(table, "status") c.Status = field.NewField(table, "status")
c.Visibility = field.NewString(table, "visibility") c.Visibility = field.NewField(table, "visibility")
c.PreviewSeconds = field.NewInt32(table, "preview_seconds") c.PreviewSeconds = field.NewInt32(table, "preview_seconds")
c.PreviewDownloadable = field.NewBool(table, "preview_downloadable") c.PreviewDownloadable = field.NewBool(table, "preview_downloadable")
c.PublishedAt = field.NewTime(table, "published_at") c.PublishedAt = field.NewTime(table, "published_at")

View File

@@ -8,6 +8,8 @@ import (
"context" "context"
"time" "time"
"quyun/v2/pkg/consts"
"go.ipao.vip/gen" "go.ipao.vip/gen"
"go.ipao.vip/gen/types" "go.ipao.vip/gen/types"
) )
@@ -16,24 +18,24 @@ const TableNameOrder = "orders"
// Order mapped from table <orders> // Order mapped from table <orders>
type Order struct { type Order struct {
ID int64 `gorm:"column:id;type:bigint;primaryKey;autoIncrement:true" json:"id"` ID int64 `gorm:"column:id;type:bigint;primaryKey;autoIncrement:true" json:"id"`
TenantID int64 `gorm:"column:tenant_id;type:bigint;not null" json:"tenant_id"` TenantID int64 `gorm:"column:tenant_id;type:bigint;not null" json:"tenant_id"`
UserID int64 `gorm:"column:user_id;type:bigint;not null" json:"user_id"` UserID int64 `gorm:"column:user_id;type:bigint;not null" json:"user_id"`
Type string `gorm:"column:type;type:character varying(32);default:content_purchase" json:"type"` Type consts.OrderType `gorm:"column:type;type:character varying(32);default:content_purchase" json:"type"`
Status string `gorm:"column:status;type:character varying(32);default:created" json:"status"` Status consts.OrderStatus `gorm:"column:status;type:character varying(32);default:created" json:"status"`
Currency string `gorm:"column:currency;type:character varying(16);default:CNY" json:"currency"` Currency string `gorm:"column:currency;type:character varying(16);default:CNY" json:"currency"`
AmountOriginal int64 `gorm:"column:amount_original;type:bigint;not null" json:"amount_original"` AmountOriginal int64 `gorm:"column:amount_original;type:bigint;not null" json:"amount_original"`
AmountDiscount int64 `gorm:"column:amount_discount;type:bigint;not null" json:"amount_discount"` AmountDiscount int64 `gorm:"column:amount_discount;type:bigint;not null" json:"amount_discount"`
AmountPaid int64 `gorm:"column:amount_paid;type:bigint;not null" json:"amount_paid"` AmountPaid int64 `gorm:"column:amount_paid;type:bigint;not null" json:"amount_paid"`
Snapshot types.JSON `gorm:"column:snapshot;type:jsonb;default:{}" json:"snapshot"` Snapshot types.JSON `gorm:"column:snapshot;type:jsonb;default:{}" json:"snapshot"`
IdempotencyKey string `gorm:"column:idempotency_key;type:character varying(128);not null" json:"idempotency_key"` IdempotencyKey string `gorm:"column:idempotency_key;type:character varying(128);not null" json:"idempotency_key"`
PaidAt time.Time `gorm:"column:paid_at;type:timestamp with time zone" json:"paid_at"` PaidAt time.Time `gorm:"column:paid_at;type:timestamp with time zone" json:"paid_at"`
RefundedAt time.Time `gorm:"column:refunded_at;type:timestamp with time zone" json:"refunded_at"` RefundedAt time.Time `gorm:"column:refunded_at;type:timestamp with time zone" json:"refunded_at"`
RefundForced bool `gorm:"column:refund_forced;type:boolean" json:"refund_forced"` RefundForced bool `gorm:"column:refund_forced;type:boolean" json:"refund_forced"`
RefundOperatorUserID int64 `gorm:"column:refund_operator_user_id;type:bigint" json:"refund_operator_user_id"` RefundOperatorUserID int64 `gorm:"column:refund_operator_user_id;type:bigint" json:"refund_operator_user_id"`
RefundReason string `gorm:"column:refund_reason;type:character varying(255)" json:"refund_reason"` RefundReason string `gorm:"column:refund_reason;type:character varying(255)" json:"refund_reason"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;default:now()" json:"created_at"` CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;default:now()" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp with time zone;default:now()" json:"updated_at"` UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp with time zone;default:now()" json:"updated_at"`
} }
// Quick operations without importing query package // Quick operations without importing query package

View File

@@ -28,8 +28,8 @@ func newOrder(db *gorm.DB, opts ...gen.DOOption) orderQuery {
_orderQuery.ID = field.NewInt64(tableName, "id") _orderQuery.ID = field.NewInt64(tableName, "id")
_orderQuery.TenantID = field.NewInt64(tableName, "tenant_id") _orderQuery.TenantID = field.NewInt64(tableName, "tenant_id")
_orderQuery.UserID = field.NewInt64(tableName, "user_id") _orderQuery.UserID = field.NewInt64(tableName, "user_id")
_orderQuery.Type = field.NewString(tableName, "type") _orderQuery.Type = field.NewField(tableName, "type")
_orderQuery.Status = field.NewString(tableName, "status") _orderQuery.Status = field.NewField(tableName, "status")
_orderQuery.Currency = field.NewString(tableName, "currency") _orderQuery.Currency = field.NewString(tableName, "currency")
_orderQuery.AmountOriginal = field.NewInt64(tableName, "amount_original") _orderQuery.AmountOriginal = field.NewInt64(tableName, "amount_original")
_orderQuery.AmountDiscount = field.NewInt64(tableName, "amount_discount") _orderQuery.AmountDiscount = field.NewInt64(tableName, "amount_discount")
@@ -56,8 +56,8 @@ type orderQuery struct {
ID field.Int64 ID field.Int64
TenantID field.Int64 TenantID field.Int64
UserID field.Int64 UserID field.Int64
Type field.String Type field.Field
Status field.String Status field.Field
Currency field.String Currency field.String
AmountOriginal field.Int64 AmountOriginal field.Int64
AmountDiscount field.Int64 AmountDiscount field.Int64
@@ -90,8 +90,8 @@ func (o *orderQuery) updateTableName(table string) *orderQuery {
o.ID = field.NewInt64(table, "id") o.ID = field.NewInt64(table, "id")
o.TenantID = field.NewInt64(table, "tenant_id") o.TenantID = field.NewInt64(table, "tenant_id")
o.UserID = field.NewInt64(table, "user_id") o.UserID = field.NewInt64(table, "user_id")
o.Type = field.NewString(table, "type") o.Type = field.NewField(table, "type")
o.Status = field.NewString(table, "status") o.Status = field.NewField(table, "status")
o.Currency = field.NewString(table, "currency") o.Currency = field.NewString(table, "currency")
o.AmountOriginal = field.NewInt64(table, "amount_original") o.AmountOriginal = field.NewInt64(table, "amount_original")
o.AmountDiscount = field.NewInt64(table, "amount_discount") o.AmountDiscount = field.NewInt64(table, "amount_discount")

View File

@@ -8,6 +8,8 @@ import (
"context" "context"
"time" "time"
"quyun/v2/pkg/consts"
"go.ipao.vip/gen" "go.ipao.vip/gen"
"go.ipao.vip/gen/types" "go.ipao.vip/gen/types"
) )
@@ -16,13 +18,13 @@ const TableNameTenantUser = "tenant_users"
// TenantUser mapped from table <tenant_users> // TenantUser mapped from table <tenant_users>
type TenantUser struct { type TenantUser struct {
ID int64 `gorm:"column:id;type:bigint;primaryKey;autoIncrement:true" json:"id"` ID int64 `gorm:"column:id;type:bigint;primaryKey;autoIncrement:true" json:"id"`
TenantID int64 `gorm:"column:tenant_id;type:bigint;not null" json:"tenant_id"` TenantID int64 `gorm:"column:tenant_id;type:bigint;not null" json:"tenant_id"`
UserID int64 `gorm:"column:user_id;type:bigint;not null" json:"user_id"` UserID int64 `gorm:"column:user_id;type:bigint;not null" json:"user_id"`
Role types.Array[string] `gorm:"column:role;type:text[];default:{member}" json:"role"` Role types.Array[consts.TenantUserRole] `gorm:"column:role;type:text[];default:{member}" json:"role"`
Status string `gorm:"column:status;type:character varying(50);default:verified" json:"status"` Status string `gorm:"column:status;type:character varying(50);default:verified" json:"status"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;default:now()" json:"created_at"` CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;default:now()" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp with time zone;default:now()" json:"updated_at"` UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp with time zone;default:now()" json:"updated_at"`
} }
// Quick operations without importing query package // Quick operations without importing query package

View File

@@ -8,6 +8,8 @@ import (
"context" "context"
"time" "time"
"quyun/v2/pkg/consts"
"go.ipao.vip/gen" "go.ipao.vip/gen"
"go.ipao.vip/gen/types" "go.ipao.vip/gen/types"
) )
@@ -16,16 +18,16 @@ const TableNameTenant = "tenants"
// Tenant mapped from table <tenants> // Tenant mapped from table <tenants>
type Tenant struct { type Tenant struct {
ID int64 `gorm:"column:id;type:bigint;primaryKey;autoIncrement:true" json:"id"` ID int64 `gorm:"column:id;type:bigint;primaryKey;autoIncrement:true" json:"id"`
UserID int64 `gorm:"column:user_id;type:bigint;not null" json:"user_id"` UserID int64 `gorm:"column:user_id;type:bigint;not null" json:"user_id"`
Code string `gorm:"column:code;type:character varying(64);not null" json:"code"` Code string `gorm:"column:code;type:character varying(64);not null" json:"code"`
UUID types.UUID `gorm:"column:uuid;type:uuid;not null" json:"uuid"` UUID types.UUID `gorm:"column:uuid;type:uuid;not null" json:"uuid"`
Name string `gorm:"column:name;type:character varying(128);not null" json:"name"` Name string `gorm:"column:name;type:character varying(128);not null" json:"name"`
Status string `gorm:"column:status;type:character varying(64);not null" json:"status"` Status consts.TenantStatus `gorm:"column:status;type:character varying(64);not null" json:"status"`
Config types.JSON `gorm:"column:config;type:jsonb;default:{}" json:"config"` Config types.JSON `gorm:"column:config;type:jsonb;default:{}" json:"config"`
ExpiredAt time.Time `gorm:"column:expired_at;type:timestamp with time zone" json:"expired_at"` ExpiredAt time.Time `gorm:"column:expired_at;type:timestamp with time zone" json:"expired_at"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;default:now()" json:"created_at"` CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;default:now()" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp with time zone;default:now()" json:"updated_at"` UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp with time zone;default:now()" json:"updated_at"`
} }
// Quick operations without importing query package // Quick operations without importing query package

View File

@@ -30,7 +30,7 @@ func newTenant(db *gorm.DB, opts ...gen.DOOption) tenantQuery {
_tenantQuery.Code = field.NewString(tableName, "code") _tenantQuery.Code = field.NewString(tableName, "code")
_tenantQuery.UUID = field.NewField(tableName, "uuid") _tenantQuery.UUID = field.NewField(tableName, "uuid")
_tenantQuery.Name = field.NewString(tableName, "name") _tenantQuery.Name = field.NewString(tableName, "name")
_tenantQuery.Status = field.NewString(tableName, "status") _tenantQuery.Status = field.NewField(tableName, "status")
_tenantQuery.Config = field.NewJSONB(tableName, "config") _tenantQuery.Config = field.NewJSONB(tableName, "config")
_tenantQuery.ExpiredAt = field.NewTime(tableName, "expired_at") _tenantQuery.ExpiredAt = field.NewTime(tableName, "expired_at")
_tenantQuery.CreatedAt = field.NewTime(tableName, "created_at") _tenantQuery.CreatedAt = field.NewTime(tableName, "created_at")
@@ -50,7 +50,7 @@ type tenantQuery struct {
Code field.String Code field.String
UUID field.Field UUID field.Field
Name field.String Name field.String
Status field.String Status field.Field
Config field.JSONB Config field.JSONB
ExpiredAt field.Time ExpiredAt field.Time
CreatedAt field.Time CreatedAt field.Time
@@ -76,7 +76,7 @@ func (t *tenantQuery) updateTableName(table string) *tenantQuery {
t.Code = field.NewString(table, "code") t.Code = field.NewString(table, "code")
t.UUID = field.NewField(table, "uuid") t.UUID = field.NewField(table, "uuid")
t.Name = field.NewString(table, "name") t.Name = field.NewString(table, "name")
t.Status = field.NewString(table, "status") t.Status = field.NewField(table, "status")
t.Config = field.NewJSONB(table, "config") t.Config = field.NewJSONB(table, "config")
t.ExpiredAt = field.NewTime(table, "expired_at") t.ExpiredAt = field.NewTime(table, "expired_at")
t.CreatedAt = field.NewTime(table, "created_at") t.CreatedAt = field.NewTime(table, "created_at")

View File

@@ -8,6 +8,8 @@ import (
"context" "context"
"time" "time"
"quyun/v2/pkg/consts"
"go.ipao.vip/gen" "go.ipao.vip/gen"
"go.ipao.vip/gen/types" "go.ipao.vip/gen/types"
"gorm.io/gorm" "gorm.io/gorm"
@@ -17,27 +19,27 @@ const TableNameUser = "users"
// User mapped from table <users> // User mapped from table <users>
type User struct { type User struct {
ID int64 `gorm:"column:id;type:bigint;primaryKey;autoIncrement:true" json:"id"` ID int64 `gorm:"column:id;type:bigint;primaryKey;autoIncrement:true" json:"id"`
Username string `gorm:"column:username;type:character varying(255);not null" json:"username"` Username string `gorm:"column:username;type:character varying(255);not null" json:"username"`
Password string `gorm:"column:password;type:character varying(255);not null" json:"password"` Password string `gorm:"column:password;type:character varying(255);not null" json:"password"`
Roles types.Array[string] `gorm:"column:roles;type:text[];default:{user}" json:"roles"` Roles types.Array[consts.Role] `gorm:"column:roles;type:text[];default:{user}" json:"roles"`
Status string `gorm:"column:status;type:character varying(50);default:active" json:"status"` Status string `gorm:"column:status;type:character varying(50);default:active" json:"status"`
Metas types.JSON `gorm:"column:metas;type:jsonb;default:{}" json:"metas"` Metas types.JSON `gorm:"column:metas;type:jsonb;default:{}" json:"metas"`
Balance int64 `gorm:"column:balance;type:bigint" json:"balance"` Balance int64 `gorm:"column:balance;type:bigint" json:"balance"`
BalanceFrozen int64 `gorm:"column:balance_frozen;type:bigint" json:"balance_frozen"` BalanceFrozen int64 `gorm:"column:balance_frozen;type:bigint" json:"balance_frozen"`
VerifiedAt time.Time `gorm:"column:verified_at;type:timestamp with time zone" json:"verified_at"` VerifiedAt time.Time `gorm:"column:verified_at;type:timestamp with time zone" json:"verified_at"`
Nickname string `gorm:"column:nickname;type:character varying(255)" json:"nickname"` Nickname string `gorm:"column:nickname;type:character varying(255)" json:"nickname"`
Avatar string `gorm:"column:avatar;type:character varying(512)" json:"avatar"` Avatar string `gorm:"column:avatar;type:character varying(512)" json:"avatar"`
Gender string `gorm:"column:gender;type:character varying(32);default:secret" json:"gender"` Gender consts.Gender `gorm:"column:gender;type:character varying(32);default:secret" json:"gender"`
Bio string `gorm:"column:bio;type:character varying(512)" json:"bio"` Bio string `gorm:"column:bio;type:character varying(512)" json:"bio"`
Birthday types.Date `gorm:"column:birthday;type:date" json:"birthday"` Birthday types.Date `gorm:"column:birthday;type:date" json:"birthday"`
Location types.JSON `gorm:"column:location;type:jsonb;default:{}" json:"location"` Location types.JSON `gorm:"column:location;type:jsonb;default:{}" json:"location"`
Points int64 `gorm:"column:points;type:bigint" json:"points"` Points int64 `gorm:"column:points;type:bigint" json:"points"`
Phone string `gorm:"column:phone;type:character varying(32)" json:"phone"` Phone string `gorm:"column:phone;type:character varying(32)" json:"phone"`
IsRealNameVerified bool `gorm:"column:is_real_name_verified;type:boolean" json:"is_real_name_verified"` IsRealNameVerified bool `gorm:"column:is_real_name_verified;type:boolean" json:"is_real_name_verified"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;default:now()" json:"created_at"` CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;default:now()" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp with time zone;default:now()" json:"updated_at"` UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp with time zone;default:now()" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamp with time zone" json:"deleted_at"` DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamp with time zone" json:"deleted_at"`
} }
// Quick operations without importing query package // Quick operations without importing query package

View File

@@ -36,7 +36,7 @@ func newUser(db *gorm.DB, opts ...gen.DOOption) userQuery {
_userQuery.VerifiedAt = field.NewTime(tableName, "verified_at") _userQuery.VerifiedAt = field.NewTime(tableName, "verified_at")
_userQuery.Nickname = field.NewString(tableName, "nickname") _userQuery.Nickname = field.NewString(tableName, "nickname")
_userQuery.Avatar = field.NewString(tableName, "avatar") _userQuery.Avatar = field.NewString(tableName, "avatar")
_userQuery.Gender = field.NewString(tableName, "gender") _userQuery.Gender = field.NewField(tableName, "gender")
_userQuery.Bio = field.NewString(tableName, "bio") _userQuery.Bio = field.NewString(tableName, "bio")
_userQuery.Birthday = field.NewField(tableName, "birthday") _userQuery.Birthday = field.NewField(tableName, "birthday")
_userQuery.Location = field.NewJSONB(tableName, "location") _userQuery.Location = field.NewJSONB(tableName, "location")
@@ -67,7 +67,7 @@ type userQuery struct {
VerifiedAt field.Time VerifiedAt field.Time
Nickname field.String Nickname field.String
Avatar field.String Avatar field.String
Gender field.String Gender field.Field
Bio field.String Bio field.String
Birthday field.Field Birthday field.Field
Location field.JSONB Location field.JSONB
@@ -104,7 +104,7 @@ func (u *userQuery) updateTableName(table string) *userQuery {
u.VerifiedAt = field.NewTime(table, "verified_at") u.VerifiedAt = field.NewTime(table, "verified_at")
u.Nickname = field.NewString(table, "nickname") u.Nickname = field.NewString(table, "nickname")
u.Avatar = field.NewString(table, "avatar") u.Avatar = field.NewString(table, "avatar")
u.Gender = field.NewString(table, "gender") u.Gender = field.NewField(table, "gender")
u.Bio = field.NewString(table, "bio") u.Bio = field.NewString(table, "bio")
u.Birthday = field.NewField(table, "birthday") u.Birthday = field.NewField(table, "birthday")
u.Location = field.NewJSONB(table, "location") u.Location = field.NewJSONB(table, "location")