From 7e8d824ba617d6ce3f490b8ab1ad179c6f28ce60 Mon Sep 17 00:00:00 2001 From: luorijun Date: Fri, 27 Mar 2026 16:16:55 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BA=A4=E6=98=93=E5=88=9B?= =?UTF-8?q?=E5=BB=BA=E6=B5=81=E7=A8=8B=EF=BC=8C=E5=AE=A2=E6=88=B7=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E6=96=B0=E5=A2=9E=E6=8A=98=E6=89=A3=E4=B8=8E=E6=9D=A5?= =?UTF-8?q?=E6=BA=90=E5=AD=97=E6=AE=B5=E5=8F=8A=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/sql/init.sql | 7 ++ web/core/scopes.go | 3 + web/error.go | 16 +++- web/handlers/coupon.go | 105 ++++++++++++++++++++++ web/handlers/resource.go | 28 +++--- web/handlers/trade.go | 3 +- web/handlers/user.go | 14 ++- web/models/product_discount.go | 2 +- web/models/user.go | 17 +++- web/queries/bill.gen.go | 8 ++ web/queries/channel.gen.go | 8 ++ web/queries/logs_login.gen.go | 8 ++ web/queries/logs_request.gen.go | 8 ++ web/queries/logs_user_usage.gen.go | 8 ++ web/queries/product_sku_user.gen.go | 8 ++ web/queries/proxy.gen.go | 11 +++ web/queries/resource.gen.go | 8 ++ web/queries/session.gen.go | 8 ++ web/queries/trade.gen.go | 8 ++ web/queries/user.gen.go | 102 +++++++++++++++++++++- web/routes.go | 21 +++-- web/services/bill.go | 19 +--- web/services/coupon.go | 75 ++++++++++++++++ web/services/resource.go | 129 +++++++++++----------------- web/services/trade.go | 31 ++++--- web/services/user.go | 8 +- 26 files changed, 523 insertions(+), 140 deletions(-) create mode 100644 web/handlers/coupon.go diff --git a/scripts/sql/init.sql b/scripts/sql/init.sql index cf7a6eb..97cb6bd 100644 --- a/scripts/sql/init.sql +++ b/scripts/sql/init.sql @@ -258,10 +258,12 @@ drop table if exists "user" cascade; create table "user" ( id int generated by default as identity primary key, admin_id int, + discount_id int, phone text not null unique, username text, email text, password text, + source int default 0, name text, avatar text, status int not null default 1, @@ -279,6 +281,7 @@ create table "user" ( deleted_at timestamptz ); create index idx_user_admin_id on "user" (admin_id) where deleted_at is null; +create index idx_user_discount_id on "user" (discount_id) where deleted_at is null; create unique index udx_user_phone on "user" (phone) where deleted_at is null; create unique index udx_user_username on "user" (username) where deleted_at is null; create unique index udx_user_email on "user" (email) where deleted_at is null; @@ -288,9 +291,11 @@ create index idx_user_created_at on "user" (created_at) where deleted_at is null comment on table "user" is '用户表'; comment on column "user".id is '用户ID'; comment on column "user".admin_id is '管理员ID'; +comment on column "user".discount_id is '折扣ID'; comment on column "user".password is '用户密码'; comment on column "user".username is '用户名'; comment on column "user".phone is '手机号码'; +comment on column "user".source is '用户来源:0-官网注册,1-管理员添加,2-代理商注册,3-代理商添加'; comment on column "user".name is '真实姓名'; comment on column "user".avatar is '头像URL'; comment on column "user".status is '用户状态:0-禁用,1-正常'; @@ -1053,6 +1058,8 @@ comment on column coupon.deleted_at is '删除时间'; -- user表外键 alter table "user" add constraint fk_user_admin_id foreign key (admin_id) references admin (id) on delete set null; +alter table "user" + add constraint fk_user_discount_id foreign key (discount_id) references product_discount (id) on delete set null; -- session表外键 alter table session diff --git a/web/core/scopes.go b/web/core/scopes.go index 35b7cef..c9dc896 100644 --- a/web/core/scopes.go +++ b/web/core/scopes.go @@ -21,4 +21,7 @@ const ( ScopeResourceRead = string("resource:read") ScopeResourceWrite = string("resource:write") + + ScopeCouponRead = string("coupon:read") + ScopeCouponWrite = string("coupon:write") ) diff --git a/web/error.go b/web/error.go index f946a6f..27b02cb 100644 --- a/web/error.go +++ b/web/error.go @@ -8,6 +8,7 @@ import ( "platform/web/auth" "platform/web/core" "reflect" + "time" "github.com/gofiber/fiber/v2" ) @@ -22,6 +23,7 @@ func ErrorHandler(c *fiber.Ctx, err error) error { var bizErr *core.BizErr var servErr *core.ServErr var jsonErr *json.UnmarshalTypeError + var timeErr *time.ParseError switch { @@ -55,9 +57,21 @@ func ErrorHandler(c *fiber.Ctx, err error) error { code = fiber.StatusBadRequest message = fmt.Sprintf("参数 %s 类型不正确,传入类型为 %s,正确类型应该为 %s", jsonErr.Field, jsonErr.Value, jsonErr.Type.Name()) + case errors.As(err, &timeErr): + code = fiber.StatusBadRequest + message = fmt.Sprintf("时间格式不正确,传入值为 %s,检查传参是否为时间类型", timeErr.Value) + // 所有未手动声明的错误类型 default: - slog.Warn("未处理的异常", slog.String("type", reflect.TypeOf(err).Name()), slog.String("error", err.Error())) + t := reflect.TypeOf(err) + for { + if t.Kind() == reflect.Pointer { + t = t.Elem() + continue + } + break + } + slog.Warn("未处理的异常", slog.String("type", t.String()), slog.String("error", err.Error())) } c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) diff --git a/web/handlers/coupon.go b/web/handlers/coupon.go new file mode 100644 index 0000000..acaa8ab --- /dev/null +++ b/web/handlers/coupon.go @@ -0,0 +1,105 @@ +package handlers + +import ( + "platform/web/auth" + "platform/web/core" + g "platform/web/globals" + s "platform/web/services" + + "github.com/gofiber/fiber/v2" +) + +func PageCouponByAdmin(c *fiber.Ctx) error { + _, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeCouponRead) + if err != nil { + return err + } + + var req core.PageReq + if err := g.Validator.ParseBody(c, &req); err != nil { + return err + } + + list, total, err := s.Coupon.Page(&req) + if err != nil { + return err + } + + return c.JSON(core.PageResp{ + Total: int(total), + Page: req.GetPage(), + Size: req.GetSize(), + List: list, + }) +} + +func AllCouponsByAdmin(c *fiber.Ctx) error { + _, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeCouponRead) + if err != nil { + return err + } + + list, err := s.Coupon.All() + if err != nil { + return err + } + + return c.JSON(list) +} + +func CreateCoupon(c *fiber.Ctx) error { + _, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeCouponWrite) + if err != nil { + return err + } + + var req s.CreateCouponData + if err := g.Validator.ParseBody(c, &req); err != nil { + return err + } + + err = s.Coupon.Create(req) + if err != nil { + return err + } + + return nil +} + +func UpdateCoupon(c *fiber.Ctx) error { + _, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeCouponWrite) + if err != nil { + return err + } + + var req s.UpdateCouponData + if err := g.Validator.ParseBody(c, &req); err != nil { + return err + } + + err = s.Coupon.Update(req) + if err != nil { + return err + } + + return nil +} + +func DeleteCoupon(c *fiber.Ctx) error { + _, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeCouponWrite) + if err != nil { + return err + } + + var req core.IdReq + if err := g.Validator.ParseBody(c, &req); err != nil { + return err + } + + err = s.Coupon.Delete(req.Id) + if err != nil { + return err + } + + return nil +} diff --git a/web/handlers/resource.go b/web/handlers/resource.go index 8b04ccd..e5cd6c6 100644 --- a/web/handlers/resource.go +++ b/web/handlers/resource.go @@ -643,7 +643,7 @@ func CreateResource(c *fiber.Ctx) error { } // 创建套餐 - err = s.Resource.CreateResourceByBalance(authCtx.User.ID, time.Now(), req.CreateResourceData) + err = s.Resource.CreateResourceByBalance(authCtx.User, req.CreateResourceData) if err != nil { return err } @@ -670,28 +670,28 @@ func ResourcePrice(c *fiber.Ctx) error { } // 获取套餐价格 - sku, err := s.Resource.GetSku(req.CreateResourceData.Code()) - if err != nil { - return err - } + // sku, err := s.Resource.GetSku(req.CreateResourceData.Code()) + // if err != nil { + // return err + // } - _, amount, discounted, couponApplied, err := s.Resource.GetPrice(sku, req.Count(), nil, nil) + // _, amount, discounted, couponApplied, err := s.Resource.GetPrice(sku, req.Count(), nil, nil) + // if err != nil { + // return err + // } + detail, err := req.TradeDetail(nil) if err != nil { return err } // 计算折扣 return c.JSON(ResourcePriceResp{ - Discount: float32(sku.Discount.Discount) / 100, - Price: amount.StringFixed(2), - Discounted: discounted.StringFixed(2), - CouponApplied: couponApplied.StringFixed(2), + Price: detail.Amount.StringFixed(2), + Discounted: detail.Actual.StringFixed(2), }) } type ResourcePriceResp struct { - Price string `json:"price"` - Discount float32 `json:"discounted"` - Discounted string `json:"discounted_price"` - CouponApplied string `json:"coupon_applied"` + Price string `json:"price"` + Discounted string `json:"discounted_price"` } diff --git a/web/handlers/trade.go b/web/handlers/trade.go index a6f474f..bff309f 100644 --- a/web/handlers/trade.go +++ b/web/handlers/trade.go @@ -121,8 +121,7 @@ func TradeCreate(c *fiber.Ctx) error { } // 处理订单 - uid := authCtx.User.ID - result, err := s.Trade.Create(uid, req.CreateTradeData, req.Resource) + result, err := s.Trade.Create(authCtx.User, req.CreateTradeData, req.Resource) if err != nil { return core.NewServErr("处理购买产品信息失败", err) } diff --git a/web/handlers/user.go b/web/handlers/user.go index 8b2d6d1..2c8e053 100644 --- a/web/handlers/user.go +++ b/web/handlers/user.go @@ -28,8 +28,14 @@ func PageUserByAdmin(c *fiber.Ctx) error { // 构建查询条件 do := q.User.Where() - if req.Phone != nil { - do = do.Where(q.User.Phone.Eq(*req.Phone)) + if req.Account != nil { + do = do.Where(q.User.Where( + q.User.Username.Like("%" + *req.Account + "%"), + ).Or( + q.User.Phone.Like("%" + *req.Account + "%"), + ).Or( + q.User.Email.Like("%" + *req.Account + "%"), + )) } if req.Name != nil { do = do.Where(q.User.Name.Eq(*req.Name)) @@ -57,7 +63,7 @@ func PageUserByAdmin(c *fiber.Ctx) error { } // 查询用户列表 - users, total, err := q.User. + users, total, err := q.User.Debug(). Preload(q.User.Admin). Omit(q.User.Password). Where(do). @@ -85,7 +91,7 @@ func PageUserByAdmin(c *fiber.Ctx) error { type PageUserByAdminReq struct { core.PageReq - Phone *string `json:"phone,omitempty" validate:"omitempty,number"` + Account *string `json:"account,omitempty"` Name *string `json:"name,omitempty"` Identified *bool `json:"identified,omitempty"` Enabled *bool `json:"enabled,omitempty"` diff --git a/web/models/product_discount.go b/web/models/product_discount.go index 72c677e..b1f3298 100644 --- a/web/models/product_discount.go +++ b/web/models/product_discount.go @@ -13,7 +13,7 @@ type ProductDiscount struct { Discount int32 `json:"discount" gorm:"column:discount"` // 产品折扣 } -func (pd ProductDiscount) Decimal() decimal.Decimal { +func (pd ProductDiscount) Rate() decimal.Decimal { return decimal.NewFromInt32(pd.Discount). Div(decimal.NewFromInt32(100)) } diff --git a/web/models/user.go b/web/models/user.go index d3291f8..bfba99f 100644 --- a/web/models/user.go +++ b/web/models/user.go @@ -12,10 +12,12 @@ import ( type User struct { core.Model AdminID *int32 `json:"admin_id,omitempty" gorm:"column:admin_id"` // 管理员ID + DiscountID *int32 `json:"discount_id,omitempty" gorm:"column:discount_id"` // 折扣ID Phone string `json:"phone" gorm:"column:phone"` // 手机号码 Username *string `json:"username,omitempty" gorm:"column:username"` // 用户名 Email *string `json:"email,omitempty" gorm:"column:email"` // 邮箱 Password *string `json:"password,omitempty" gorm:"column:password"` // 用户密码 + Source *UserSource `json:"source,omitempty" gorm:"column:source"` // 用户来源:0-官网注册,1-管理员添加,2-代理商注册,3-代理商添加 Name *string `json:"name,omitempty" gorm:"column:name"` // 真实姓名 Avatar *string `json:"avatar,omitempty" gorm:"column:avatar"` // 头像URL Status UserStatus `json:"status" gorm:"column:status"` // 用户状态:0-禁用,1-正常 @@ -29,8 +31,9 @@ type User struct { LastLoginIP *orm.Inet `json:"last_login_ip,omitempty" gorm:"column:last_login_ip"` // 最后登录地址 LastLoginUA *string `json:"last_login_ua,omitempty" gorm:"column:last_login_ua"` // 最后登录代理 - Admin *Admin `json:"admin,omitempty" gorm:"foreignKey:AdminID"` - Roles []*UserRole `json:"roles" gorm:"many2many:link_user_role"` + Admin *Admin `json:"admin,omitempty" gorm:"foreignKey:AdminID"` + Roles []*UserRole `json:"roles" gorm:"many2many:link_user_role"` + Discount *ProductDiscount `json:"discount,omitempty" gorm:"foreignKey:DiscountID"` } // UserStatus 用户状态枚举 @@ -49,3 +52,13 @@ const ( UserIDTypePersonal UserIDType = 1 // 个人认证 UserIDTypeEnterprise UserIDType = 2 // 企业认证 ) + +// UserSource 用户来源枚举 +type UserSource int + +const ( + UserSourceReg UserSource = 0 // 官网注册 + UserSourceAdd UserSource = 1 // 管理员添加 + UserSourceAgentReg UserSource = 2 // 代理商注册 + UserSourceAgentAdd UserSource = 3 // 代理商添加 +) diff --git a/web/queries/bill.gen.go b/web/queries/bill.gen.go index c17e4e0..266a94c 100644 --- a/web/queries/bill.gen.go +++ b/web/queries/bill.gen.go @@ -97,6 +97,11 @@ func newBill(db *gorm.DB, opts ...gen.DOOption) bill { }, }, }, + Discount: struct { + field.RelationField + }{ + RelationField: field.NewRelation("User.Discount", "models.ProductDiscount"), + }, Roles: struct { field.RelationField Permissions struct { @@ -329,6 +334,9 @@ type billBelongsToUser struct { } } } + Discount struct { + field.RelationField + } Roles struct { field.RelationField Permissions struct { diff --git a/web/queries/channel.gen.go b/web/queries/channel.gen.go index e3e6669..3810da2 100644 --- a/web/queries/channel.gen.go +++ b/web/queries/channel.gen.go @@ -103,6 +103,11 @@ func newChannel(db *gorm.DB, opts ...gen.DOOption) channel { }, }, }, + Discount: struct { + field.RelationField + }{ + RelationField: field.NewRelation("User.Discount", "models.ProductDiscount"), + }, Roles: struct { field.RelationField Permissions struct { @@ -385,6 +390,9 @@ type channelBelongsToUser struct { } } } + Discount struct { + field.RelationField + } Roles struct { field.RelationField Permissions struct { diff --git a/web/queries/logs_login.gen.go b/web/queries/logs_login.gen.go index 1df7645..1a4aca0 100644 --- a/web/queries/logs_login.gen.go +++ b/web/queries/logs_login.gen.go @@ -91,6 +91,11 @@ func newLogsLogin(db *gorm.DB, opts ...gen.DOOption) logsLogin { }, }, }, + Discount: struct { + field.RelationField + }{ + RelationField: field.NewRelation("User.Discount", "models.ProductDiscount"), + }, Roles: struct { field.RelationField Permissions struct { @@ -209,6 +214,9 @@ type logsLoginBelongsToUser struct { } } } + Discount struct { + field.RelationField + } Roles struct { field.RelationField Permissions struct { diff --git a/web/queries/logs_request.gen.go b/web/queries/logs_request.gen.go index 9fb6566..45d3d5f 100644 --- a/web/queries/logs_request.gen.go +++ b/web/queries/logs_request.gen.go @@ -93,6 +93,11 @@ func newLogsRequest(db *gorm.DB, opts ...gen.DOOption) logsRequest { }, }, }, + Discount: struct { + field.RelationField + }{ + RelationField: field.NewRelation("User.Discount", "models.ProductDiscount"), + }, Roles: struct { field.RelationField Permissions struct { @@ -310,6 +315,9 @@ type logsRequestBelongsToUser struct { } } } + Discount struct { + field.RelationField + } Roles struct { field.RelationField Permissions struct { diff --git a/web/queries/logs_user_usage.gen.go b/web/queries/logs_user_usage.gen.go index 3ba0da6..883b032 100644 --- a/web/queries/logs_user_usage.gen.go +++ b/web/queries/logs_user_usage.gen.go @@ -93,6 +93,11 @@ func newLogsUserUsage(db *gorm.DB, opts ...gen.DOOption) logsUserUsage { }, }, }, + Discount: struct { + field.RelationField + }{ + RelationField: field.NewRelation("User.Discount", "models.ProductDiscount"), + }, Roles: struct { field.RelationField Permissions struct { @@ -286,6 +291,9 @@ type logsUserUsageBelongsToUser struct { } } } + Discount struct { + field.RelationField + } Roles struct { field.RelationField Permissions struct { diff --git a/web/queries/product_sku_user.gen.go b/web/queries/product_sku_user.gen.go index 08fde46..d6289fb 100644 --- a/web/queries/product_sku_user.gen.go +++ b/web/queries/product_sku_user.gen.go @@ -89,6 +89,11 @@ func newProductSkuUser(db *gorm.DB, opts ...gen.DOOption) productSkuUser { }, }, }, + Discount: struct { + field.RelationField + }{ + RelationField: field.NewRelation("User.Discount", "models.ProductDiscount"), + }, Roles: struct { field.RelationField Permissions struct { @@ -233,6 +238,9 @@ type productSkuUserBelongsToUser struct { } } } + Discount struct { + field.RelationField + } Roles struct { field.RelationField Permissions struct { diff --git a/web/queries/proxy.gen.go b/web/queries/proxy.gen.go index 98d5b2e..4e3ecfd 100644 --- a/web/queries/proxy.gen.go +++ b/web/queries/proxy.gen.go @@ -60,6 +60,9 @@ func newProxy(db *gorm.DB, opts ...gen.DOOption) proxy { } } } + Discount struct { + field.RelationField + } Roles struct { field.RelationField Permissions struct { @@ -120,6 +123,11 @@ func newProxy(db *gorm.DB, opts ...gen.DOOption) proxy { }, }, }, + Discount: struct { + field.RelationField + }{ + RelationField: field.NewRelation("Channels.User.Discount", "models.ProductDiscount"), + }, Roles: struct { field.RelationField Permissions struct { @@ -358,6 +366,9 @@ type proxyHasManyChannels struct { } } } + Discount struct { + field.RelationField + } Roles struct { field.RelationField Permissions struct { diff --git a/web/queries/resource.gen.go b/web/queries/resource.gen.go index d372367..daba968 100644 --- a/web/queries/resource.gen.go +++ b/web/queries/resource.gen.go @@ -136,6 +136,11 @@ func newResource(db *gorm.DB, opts ...gen.DOOption) resource { }, }, }, + Discount: struct { + field.RelationField + }{ + RelationField: field.NewRelation("User.Discount", "models.ProductDiscount"), + }, Roles: struct { field.RelationField Permissions struct { @@ -529,6 +534,9 @@ type resourceBelongsToUser struct { } } } + Discount struct { + field.RelationField + } Roles struct { field.RelationField Permissions struct { diff --git a/web/queries/session.gen.go b/web/queries/session.gen.go index 2d30bb8..c3a435e 100644 --- a/web/queries/session.gen.go +++ b/web/queries/session.gen.go @@ -97,6 +97,11 @@ func newSession(db *gorm.DB, opts ...gen.DOOption) session { }, }, }, + Discount: struct { + field.RelationField + }{ + RelationField: field.NewRelation("User.Discount", "models.ProductDiscount"), + }, Roles: struct { field.RelationField Permissions struct { @@ -260,6 +265,9 @@ type sessionBelongsToUser struct { } } } + Discount struct { + field.RelationField + } Roles struct { field.RelationField Permissions struct { diff --git a/web/queries/trade.gen.go b/web/queries/trade.gen.go index 31b7bb6..9ba2f8f 100644 --- a/web/queries/trade.gen.go +++ b/web/queries/trade.gen.go @@ -103,6 +103,11 @@ func newTrade(db *gorm.DB, opts ...gen.DOOption) trade { }, }, }, + Discount: struct { + field.RelationField + }{ + RelationField: field.NewRelation("User.Discount", "models.ProductDiscount"), + }, Roles: struct { field.RelationField Permissions struct { @@ -257,6 +262,9 @@ type tradeBelongsToUser struct { } } } + Discount struct { + field.RelationField + } Roles struct { field.RelationField Permissions struct { diff --git a/web/queries/user.gen.go b/web/queries/user.gen.go index 72cf349..6fe4e67 100644 --- a/web/queries/user.gen.go +++ b/web/queries/user.gen.go @@ -32,10 +32,12 @@ func newUser(db *gorm.DB, opts ...gen.DOOption) user { _user.UpdatedAt = field.NewTime(tableName, "updated_at") _user.DeletedAt = field.NewField(tableName, "deleted_at") _user.AdminID = field.NewInt32(tableName, "admin_id") + _user.DiscountID = field.NewInt32(tableName, "discount_id") _user.Phone = field.NewString(tableName, "phone") _user.Username = field.NewString(tableName, "username") _user.Email = field.NewString(tableName, "email") _user.Password = field.NewString(tableName, "password") + _user.Source = field.NewInt(tableName, "source") _user.Name = field.NewString(tableName, "name") _user.Avatar = field.NewString(tableName, "avatar") _user.Status = field.NewInt(tableName, "status") @@ -89,6 +91,12 @@ func newUser(db *gorm.DB, opts ...gen.DOOption) user { }, } + _user.Discount = userBelongsToDiscount{ + db: db.Session(&gorm.Session{}), + + RelationField: field.NewRelation("Discount", "models.ProductDiscount"), + } + _user.Roles = userManyToManyRoles{ db: db.Session(&gorm.Session{}), @@ -114,10 +122,12 @@ type user struct { UpdatedAt field.Time DeletedAt field.Field AdminID field.Int32 + DiscountID field.Int32 Phone field.String Username field.String Email field.String Password field.String + Source field.Int Name field.String Avatar field.String Status field.Int @@ -132,6 +142,8 @@ type user struct { LastLoginUA field.String Admin userBelongsToAdmin + Discount userBelongsToDiscount + Roles userManyToManyRoles fieldMap map[string]field.Expr @@ -154,10 +166,12 @@ func (u *user) updateTableName(table string) *user { u.UpdatedAt = field.NewTime(table, "updated_at") u.DeletedAt = field.NewField(table, "deleted_at") u.AdminID = field.NewInt32(table, "admin_id") + u.DiscountID = field.NewInt32(table, "discount_id") u.Phone = field.NewString(table, "phone") u.Username = field.NewString(table, "username") u.Email = field.NewString(table, "email") u.Password = field.NewString(table, "password") + u.Source = field.NewInt(table, "source") u.Name = field.NewString(table, "name") u.Avatar = field.NewString(table, "avatar") u.Status = field.NewInt(table, "status") @@ -186,16 +200,18 @@ func (u *user) GetFieldByName(fieldName string) (field.OrderExpr, bool) { } func (u *user) fillFieldMap() { - u.fieldMap = make(map[string]field.Expr, 23) + u.fieldMap = make(map[string]field.Expr, 26) u.fieldMap["id"] = u.ID u.fieldMap["created_at"] = u.CreatedAt u.fieldMap["updated_at"] = u.UpdatedAt u.fieldMap["deleted_at"] = u.DeletedAt u.fieldMap["admin_id"] = u.AdminID + u.fieldMap["discount_id"] = u.DiscountID u.fieldMap["phone"] = u.Phone u.fieldMap["username"] = u.Username u.fieldMap["email"] = u.Email u.fieldMap["password"] = u.Password + u.fieldMap["source"] = u.Source u.fieldMap["name"] = u.Name u.fieldMap["avatar"] = u.Avatar u.fieldMap["status"] = u.Status @@ -215,6 +231,8 @@ func (u user) clone(db *gorm.DB) user { u.userDo.ReplaceConnPool(db.Statement.ConnPool) u.Admin.db = db.Session(&gorm.Session{Initialized: true}) u.Admin.db.Statement.ConnPool = db.Statement.ConnPool + u.Discount.db = db.Session(&gorm.Session{Initialized: true}) + u.Discount.db.Statement.ConnPool = db.Statement.ConnPool u.Roles.db = db.Session(&gorm.Session{Initialized: true}) u.Roles.db.Statement.ConnPool = db.Statement.ConnPool return u @@ -223,6 +241,7 @@ func (u user) clone(db *gorm.DB) user { func (u user) replaceDB(db *gorm.DB) user { u.userDo.ReplaceDB(db) u.Admin.db = db.Session(&gorm.Session{}) + u.Discount.db = db.Session(&gorm.Session{}) u.Roles.db = db.Session(&gorm.Session{}) return u } @@ -321,6 +340,87 @@ func (a userBelongsToAdminTx) Unscoped() *userBelongsToAdminTx { return &a } +type userBelongsToDiscount struct { + db *gorm.DB + + field.RelationField +} + +func (a userBelongsToDiscount) Where(conds ...field.Expr) *userBelongsToDiscount { + if len(conds) == 0 { + return &a + } + + exprs := make([]clause.Expression, 0, len(conds)) + for _, cond := range conds { + exprs = append(exprs, cond.BeCond().(clause.Expression)) + } + a.db = a.db.Clauses(clause.Where{Exprs: exprs}) + return &a +} + +func (a userBelongsToDiscount) WithContext(ctx context.Context) *userBelongsToDiscount { + a.db = a.db.WithContext(ctx) + return &a +} + +func (a userBelongsToDiscount) Session(session *gorm.Session) *userBelongsToDiscount { + a.db = a.db.Session(session) + return &a +} + +func (a userBelongsToDiscount) Model(m *models.User) *userBelongsToDiscountTx { + return &userBelongsToDiscountTx{a.db.Model(m).Association(a.Name())} +} + +func (a userBelongsToDiscount) Unscoped() *userBelongsToDiscount { + a.db = a.db.Unscoped() + return &a +} + +type userBelongsToDiscountTx struct{ tx *gorm.Association } + +func (a userBelongsToDiscountTx) Find() (result *models.ProductDiscount, err error) { + return result, a.tx.Find(&result) +} + +func (a userBelongsToDiscountTx) Append(values ...*models.ProductDiscount) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Append(targetValues...) +} + +func (a userBelongsToDiscountTx) Replace(values ...*models.ProductDiscount) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Replace(targetValues...) +} + +func (a userBelongsToDiscountTx) Delete(values ...*models.ProductDiscount) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Delete(targetValues...) +} + +func (a userBelongsToDiscountTx) Clear() error { + return a.tx.Clear() +} + +func (a userBelongsToDiscountTx) Count() int64 { + return a.tx.Count() +} + +func (a userBelongsToDiscountTx) Unscoped() *userBelongsToDiscountTx { + a.tx = a.tx.Unscoped() + return &a +} + type userManyToManyRoles struct { db *gorm.DB diff --git a/web/routes.go b/web/routes.go index 1f6782d..03c543f 100644 --- a/web/routes.go +++ b/web/routes.go @@ -176,9 +176,20 @@ func adminRouter(api fiber.Router) { product.Post("/sku/update", handlers.UpdateProductSku) product.Post("/sku/update/discount/batch", handlers.BatchUpdateProductSkuDiscount) product.Post("/sku/remove", handlers.DeleteProductSku) - product.Post("/discount/page", handlers.PageProductDiscountByAdmin) - product.Post("/discount/all", handlers.AllProductDiscountsByAdmin) - product.Post("/discount/create", handlers.CreateProductDiscount) - product.Post("/discount/update", handlers.UpdateProductDiscount) - product.Post("/discount/remove", handlers.DeleteProductDiscount) + + // discount 折扣 + var discount = api.Group("/discount") + discount.Post("/page", handlers.PageProductDiscountByAdmin) + discount.Post("/all", handlers.AllProductDiscountsByAdmin) + discount.Post("/create", handlers.CreateProductDiscount) + discount.Post("/update", handlers.UpdateProductDiscount) + discount.Post("/remove", handlers.DeleteProductDiscount) + + // coupon 优惠券 + var coupon = api.Group("/coupon") + coupon.Post("/page", handlers.PageCouponByAdmin) + coupon.Post("/all", handlers.AllCouponsByAdmin) + coupon.Post("/create", handlers.CreateCoupon) + coupon.Post("/update", handlers.UpdateCoupon) + coupon.Post("/remove", handlers.DeleteCoupon) } diff --git a/web/services/bill.go b/web/services/bill.go index 0646ec7..f75c3ed 100644 --- a/web/services/bill.go +++ b/web/services/bill.go @@ -3,8 +3,6 @@ package services import ( m "platform/web/models" q "platform/web/queries" - - "github.com/shopspring/decimal" ) var Bill = &billService{} @@ -23,12 +21,12 @@ func (s *billService) CreateForBalance(q *q.Query, uid, tradeId int32, detail *T }) } -func (s *billService) CreateForResourceByTrade(q *q.Query, uid, tradeId, resourceId int32, detail *TradeDetail) error { +func (s *billService) CreateForResource(q *q.Query, uid, resourceId int32, tradeId *int32, detail *TradeDetail) error { return q.Bill.Create(&m.Bill{ UserID: uid, BillNo: ID.GenReadable("bil"), ResourceID: &resourceId, - TradeID: &tradeId, + TradeID: tradeId, CouponID: detail.CouponId, Type: m.BillTypeConsume, Info: &detail.Subject, @@ -36,16 +34,3 @@ func (s *billService) CreateForResourceByTrade(q *q.Query, uid, tradeId, resourc Actual: detail.Actual, }) } - -func (s *billService) CreateForResourceByBalance(q *q.Query, uid, resourceId int32, couponId *int32, subject string, amount, actual decimal.Decimal) error { - return q.Bill.Create(&m.Bill{ - UserID: uid, - BillNo: ID.GenReadable("bil"), - ResourceID: &resourceId, - CouponID: couponId, - Type: m.BillTypeConsume, - Info: &subject, - Amount: amount, - Actual: actual, - }) -} diff --git a/web/services/coupon.go b/web/services/coupon.go index 2bbd804..f7c041d 100644 --- a/web/services/coupon.go +++ b/web/services/coupon.go @@ -9,6 +9,7 @@ import ( "time" "github.com/shopspring/decimal" + "gorm.io/gen/field" "gorm.io/gorm" ) @@ -16,6 +17,80 @@ var Coupon = &couponService{} type couponService struct{} +func (s *couponService) All() (result []*m.Coupon, err error) { + return q.Coupon.Find() +} + +func (s *couponService) Page(req *core.PageReq) (result []*m.Coupon, count int64, err error) { + return q.Coupon.FindByPage(req.GetOffset(), req.GetLimit()) +} + +func (s *couponService) Create(data CreateCouponData) error { + return q.Coupon.Create(&m.Coupon{ + UserID: data.UserID, + Code: data.Code, + Remark: data.Remark, + Amount: data.Amount, + MinAmount: data.MinAmount, + Status: m.CouponStatusUnused, + ExpireAt: data.ExpireAt, + }) +} + +type CreateCouponData struct { + UserID *int32 `json:"user_id"` + Code string `json:"code" validate:"required"` + Remark *string `json:"remark"` + Amount decimal.Decimal `json:"amount" validate:"required"` + MinAmount decimal.Decimal `json:"min_amount"` + ExpireAt *time.Time `json:"expire_at"` +} + +func (s *couponService) Update(data UpdateCouponData) error { + do := make([]field.AssignExpr, 0) + + if data.UserID != nil { + do = append(do, q.Coupon.UserID.Value(*data.UserID)) + } + if data.Code != nil { + do = append(do, q.Coupon.Code.Value(*data.Code)) + } + if data.Remark != nil { + do = append(do, q.Coupon.Remark.Value(*data.Remark)) + } + if data.Amount != nil { + do = append(do, q.Coupon.Amount.Value(*data.Amount)) + } + if data.MinAmount != nil { + do = append(do, q.Coupon.MinAmount.Value(*data.MinAmount)) + } + if data.Status != nil { + do = append(do, q.Coupon.Status.Value(int(*data.Status))) + } + if data.ExpireAt != nil { + do = append(do, q.Coupon.ExpireAt.Value(*data.ExpireAt)) + } + + _, err := q.Coupon.Where(q.Coupon.ID.Eq(data.ID)).UpdateSimple(do...) + return err +} + +type UpdateCouponData struct { + ID int32 `json:"id" validate:"required"` + UserID *int32 `json:"user_id"` + Code *string `json:"code"` + Remark *string `json:"remark"` + Amount *decimal.Decimal `json:"amount"` + MinAmount *decimal.Decimal `json:"min_amount"` + Status *m.CouponStatus `json:"status"` + ExpireAt *time.Time `json:"expire_at"` +} + +func (s *couponService) Delete(id int32) error { + _, err := q.Coupon.Where(q.Coupon.ID.Eq(id)).UpdateColumn(q.Coupon.DeletedAt, time.Now()) + return err +} + func (s *couponService) GetCouponAvailableByCode(code string, amount decimal.Decimal, uid *int32) (*m.Coupon, error) { // 获取优惠券 coupon, err := q.Coupon.Where( diff --git a/web/services/resource.go b/web/services/resource.go index cd5fd21..39f22f2 100644 --- a/web/services/resource.go +++ b/web/services/resource.go @@ -1,7 +1,6 @@ package services import ( - "errors" "fmt" "platform/pkg/u" "platform/web/core" @@ -11,7 +10,6 @@ import ( "github.com/shopspring/decimal" "gorm.io/gen/field" - "gorm.io/gorm" ) var Resource = &resourceService{} @@ -19,33 +17,16 @@ var Resource = &resourceService{} type resourceService struct{} // CreateResourceByBalance 通过余额购买套餐 -func (s *resourceService) CreateResourceByBalance(uid int32, now time.Time, data *CreateResourceData) error { - - // 找到用户 - user, err := q.User. - Where(q.User.ID.Eq(uid)). - Take() - if err != nil { - return err - } +func (s *resourceService) CreateResourceByBalance(user *m.User, data *CreateResourceData) error { + now := time.Now() // 获取 sku - sku, err := s.GetSku(data.Code()) + detail, err := data.TradeDetail(user) if err != nil { - return err + return core.NewServErr("获取产品支付信息失败", err) } - // 检查余额 - coupon, _, amount, actual, err := s.GetPrice(sku, data.Count(), &uid, data.CouponCode) - if err != nil { - return err - } - couponId := (*int32)(nil) - if coupon != nil { - couponId = &coupon.ID - } - - newBalance := user.Balance.Sub(amount) + newBalance := user.Balance.Sub(detail.Actual) if newBalance.IsNegative() { return ErrBalanceNotEnough } @@ -55,7 +36,7 @@ func (s *resourceService) CreateResourceByBalance(uid int32, now time.Time, data // 更新用户余额 _, err = q.User. Where( - q.User.ID.Eq(uid), + q.User.ID.Eq(user.ID), q.User.Balance.Eq(user.Balance), ). UpdateSimple(q.User.Balance.Value(newBalance)) @@ -64,20 +45,20 @@ func (s *resourceService) CreateResourceByBalance(uid int32, now time.Time, data } // 保存套餐 - resource, err := s.Create(q, uid, now, data) + resource, err := s.Create(q, user.ID, now, data) if err != nil { return core.NewServErr("创建套餐失败", err) } // 生成账单 - err = Bill.CreateForResourceByBalance(q, uid, resource.ID, couponId, sku.Name, amount, actual) + err = Bill.CreateForResource(q, user.ID, resource.ID, nil, detail) if err != nil { return core.NewServErr("生成账单失败", err) } // 核销优惠券 - if coupon != nil { - err = Coupon.UseCoupon(q, coupon.ID) + if detail.CouponId != nil { + err = Coupon.UseCoupon(q, *detail.CouponId) if err != nil { return core.NewServErr("核销优惠券失败", err) } @@ -174,64 +155,59 @@ type UpdateResourceData struct { Active *bool `json:"active"` } -func (s *resourceService) GetSku(code string) (*m.ProductSku, error) { +func (s *resourceService) CalcPrice(skuCode string, count int32, user *m.User, couponCode *string) (*m.ProductSku, *m.ProductDiscount, *m.Coupon, decimal.Decimal, decimal.Decimal, error) { + sku, err := q.ProductSku. Joins(q.ProductSku.Discount). - Where(q.ProductSku.Code.Eq(code)). + Where(q.ProductSku.Code.Eq(skuCode)). Take() if err != nil { - return nil, core.NewServErr("产品不可用", err) + return nil, nil, nil, decimal.Zero, decimal.Zero, core.NewServErr("产品不可用", err) } - if sku.Discount == nil { - return nil, core.NewServErr("价格查询失败", err) - } - - return sku, nil -} - -func (s *resourceService) GetPrice(sku *m.ProductSku, count int32, uid *int32, couponCode *string) (*m.Coupon, decimal.Decimal, decimal.Decimal, decimal.Decimal, error) { - // 原价 price := sku.Price amount := price.Mul(decimal.NewFromInt32(count)) // 折扣价 - discount := sku.Discount.Decimal() - if uid != nil { // 用户特殊优惠 - var err error - uSku, err := q.ProductSkuUser. - Joins(q.ProductSkuUser.Discount). - Where( - q.ProductSkuUser.UserID.Eq(*uid), - q.ProductSkuUser.ProductSkuID.Eq(sku.ID)). - Take() - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return nil, decimal.Zero, decimal.Zero, decimal.Zero, core.NewServErr("客户特殊价查询失败", err) + discount := sku.Discount + if discount == nil { + return nil, nil, nil, decimal.Zero, decimal.Zero, core.NewServErr("价格查询失败", err) + } + + discountRate := discount.Rate() + if user != nil && user.DiscountID != nil { // 用户特殊优惠 + uDiscount, err := q.ProductDiscount.Where(q.ProductDiscount.ID.Eq(*user.DiscountID)).Take() + if err != nil { + return nil, nil, nil, decimal.Zero, decimal.Zero, core.NewServErr("客户特殊价查询失败", err) } - if uSku.Discount == nil { - return nil, decimal.Zero, decimal.Zero, decimal.Zero, core.NewServErr("价格获取失败") - } - uDiscount := uSku.Discount.Decimal() - if uDiscount.Cmp(discount) > 0 { + + uDiscountRate := uDiscount.Rate() + if uDiscountRate.Cmp(discountRate) > 0 { + discountRate = uDiscountRate discount = uDiscount } } - discounted := amount.Mul(discount) + discounted := amount.Mul(discountRate) // 优惠价 + uid := (*int32)(nil) + if user != nil { + uid = &user.ID + } + coupon := (*m.Coupon)(nil) couponApplied := discounted.Copy() if couponCode != nil { var err error coupon, err = Coupon.GetCouponAvailableByCode(*couponCode, discounted, uid) if err != nil { - return nil, decimal.Zero, decimal.Zero, decimal.Zero, err + return nil, nil, nil, decimal.Zero, decimal.Zero, err } couponApplied = discounted.Sub(coupon.Amount) } - return coupon, amount, discounted, couponApplied, nil + return sku, discount, coupon, discounted, couponApplied, nil } type CreateResourceData struct { @@ -261,52 +237,49 @@ type CreateLongResourceData struct { price *decimal.Decimal } -func (c *CreateResourceData) Count() int32 { +func (data *CreateResourceData) Count() int32 { switch { default: return 0 - case c.Type == m.ResourceTypeShort && c.Short != nil: - return c.Short.Quota - case c.Type == m.ResourceTypeLong && c.Long != nil: - return c.Long.Quota + case data.Type == m.ResourceTypeShort && data.Short != nil: + return data.Short.Quota + case data.Type == m.ResourceTypeLong && data.Long != nil: + return data.Long.Quota } } -func (c *CreateResourceData) Code() string { +func (data *CreateResourceData) Code() string { switch { default: return "" - case c.Type == m.ResourceTypeShort && c.Short != nil: + case data.Type == m.ResourceTypeShort && data.Short != nil: return fmt.Sprintf( "mode=%s,live=%d,expire=%d", - c.Short.Mode.Code(), c.Short.Live, u.Else(c.Short.Expire, 0), + data.Short.Mode.Code(), data.Short.Live, u.Else(data.Short.Expire, 0), ) - case c.Type == m.ResourceTypeLong && c.Long != nil: + case data.Type == m.ResourceTypeLong && data.Long != nil: return fmt.Sprintf( "mode=%s,live=%d,expire=%d", - c.Long.Mode.Code(), c.Long.Live, u.Else(c.Long.Expire, 0), + data.Long.Mode.Code(), data.Long.Live, u.Else(data.Long.Expire, 0), ) } } -func (c *CreateResourceData) TradeDetail() (*TradeDetail, error) { - sku, err := Resource.GetSku(c.Code()) - if err != nil { - return nil, err - } - - coupon, _, amount, actual, err := Resource.GetPrice(sku, c.Count(), nil, c.CouponCode) +func (data *CreateResourceData) TradeDetail(user *m.User) (*TradeDetail, error) { + sku, discount, coupon, amount, actual, err := Resource.CalcPrice(data.Code(), data.Count(), user, data.CouponCode) if err != nil { return nil, err } return &TradeDetail{ + data, m.TradeTypePurchase, sku.Name, amount, actual, - &coupon.ID, c, + &discount.ID, discount, + &coupon.ID, coupon, }, nil } diff --git a/web/services/trade.go b/web/services/trade.go index 156fd26..b79cf4c 100644 --- a/web/services/trade.go +++ b/web/services/trade.go @@ -32,8 +32,12 @@ type tradeService struct { } // 创建交易 -func (s *tradeService) Create(uid int32, tradeData *CreateTradeData, productData *CreateResourceData) (*CreateTradeResult, error) { - detail, err := productData.TradeDetail() +func (s *tradeService) Create(user *m.User, tradeData *CreateTradeData, productData *CreateResourceData) (*CreateTradeResult, error) { + if user == nil { + return nil, core.NewBizErr("用户未登录") + } + + detail, err := productData.TradeDetail(user) if err != nil { return nil, core.NewServErr("获取产品支付信息失败", err) } @@ -178,7 +182,7 @@ func (s *tradeService) Create(uid int32, tradeData *CreateTradeData, productData // 保存订单 err = q.Trade.Create(&m.Trade{ - UserID: uid, + UserID: user.ID, InnerNo: tradeNo, Type: detail.Type, Subject: detail.Subject, @@ -208,7 +212,7 @@ func (s *tradeService) Create(uid int32, tradeData *CreateTradeData, productData } // 提交异步关闭事件 - _, err = g.Asynq.Enqueue(e.NewCloseTradeTask(uid, tradeNo, method), asynq.ProcessAt(expireAt)) + _, err = g.Asynq.Enqueue(e.NewCloseTradeTask(user.ID, tradeNo, method), asynq.ProcessAt(expireAt)) if err != nil { return nil, core.NewServErr("提交异步关闭事件失败", err) } @@ -318,7 +322,7 @@ func (s *tradeService) OnCompleteTrade(user *m.User, interNo string, outerNo str } // 生成账单 - err = Bill.CreateForResourceByTrade(q, user.ID, resource.ID, trade.ID, &detail) + err = Bill.CreateForResource(q, user.ID, resource.ID, &trade.ID, &detail) if err != nil { return core.NewServErr("生成账单失败", err) } @@ -602,16 +606,19 @@ type OnTradeCompletedData struct { } type ProductInfo interface { - TradeDetail() (*TradeDetail, error) + TradeDetail(user *m.User) (*TradeDetail, error) } type TradeDetail struct { - Type m.TradeType `json:"type"` - Subject string `json:"subject"` - Amount decimal.Decimal `json:"amount"` - Actual decimal.Decimal `json:"actual"` - CouponId *int32 `json:"coupon_id,omitempty"` - Product ProductInfo `json:"product"` + Product ProductInfo `json:"product"` + Type m.TradeType `json:"type"` + Subject string `json:"subject"` + Amount decimal.Decimal `json:"amount"` + Actual decimal.Decimal `json:"actual"` + DiscountId *int32 `json:"discount_id,omitempty"` + Discount *m.ProductDiscount `json:"-"` // 不应缓存 + CouponId *int32 `json:"coupon_id,omitempty"` + Coupon *m.Coupon `json:"-"` // 不应缓存 } type CompleteEvent interface { diff --git a/web/services/user.go b/web/services/user.go index 111c872..e17d5d1 100644 --- a/web/services/user.go +++ b/web/services/user.go @@ -51,12 +51,14 @@ type UpdateBalanceData struct { Amount int `json:"amount"` } -func (c *UpdateBalanceData) TradeDetail() (*TradeDetail, error) { - amount := decimal.NewFromInt(int64(c.Amount)).Div(decimal.NewFromInt(100)) +func (data *UpdateBalanceData) TradeDetail(user *m.User) (*TradeDetail, error) { + amount := decimal.NewFromInt(int64(data.Amount)).Div(decimal.NewFromInt(100)) return &TradeDetail{ + data, m.TradeTypeRecharge, fmt.Sprintf("账户充值 - %s元", amount.StringFixed(2)), amount, amount, - nil, c, + nil, nil, + nil, nil, }, nil }