优化交易创建流程,客户管理新增折扣与来源字段及功能

This commit is contained in:
2026-03-27 16:16:55 +08:00
parent 75ad12efb3
commit 7e8d824ba6
26 changed files with 523 additions and 140 deletions

View File

@@ -258,10 +258,12 @@ drop table if exists "user" cascade;
create table "user" ( create table "user" (
id int generated by default as identity primary key, id int generated by default as identity primary key,
admin_id int, admin_id int,
discount_id int,
phone text not null unique, phone text not null unique,
username text, username text,
email text, email text,
password text, password text,
source int default 0,
name text, name text,
avatar text, avatar text,
status int not null default 1, status int not null default 1,
@@ -279,6 +281,7 @@ create table "user" (
deleted_at timestamptz deleted_at timestamptz
); );
create index idx_user_admin_id on "user" (admin_id) where deleted_at is null; 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_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_username on "user" (username) where deleted_at is null;
create unique index udx_user_email on "user" (email) 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 table "user" is '用户表';
comment on column "user".id is '用户ID'; comment on column "user".id is '用户ID';
comment on column "user".admin_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".password is '用户密码';
comment on column "user".username is '用户名'; comment on column "user".username is '用户名';
comment on column "user".phone 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".name is '真实姓名';
comment on column "user".avatar is '头像URL'; comment on column "user".avatar is '头像URL';
comment on column "user".status is '用户状态0-禁用1-正常'; comment on column "user".status is '用户状态0-禁用1-正常';
@@ -1053,6 +1058,8 @@ comment on column coupon.deleted_at is '删除时间';
-- user表外键 -- user表外键
alter table "user" alter table "user"
add constraint fk_user_admin_id foreign key (admin_id) references admin (id) on delete set null; 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表外键 -- session表外键
alter table session alter table session

View File

@@ -21,4 +21,7 @@ const (
ScopeResourceRead = string("resource:read") ScopeResourceRead = string("resource:read")
ScopeResourceWrite = string("resource:write") ScopeResourceWrite = string("resource:write")
ScopeCouponRead = string("coupon:read")
ScopeCouponWrite = string("coupon:write")
) )

View File

@@ -8,6 +8,7 @@ import (
"platform/web/auth" "platform/web/auth"
"platform/web/core" "platform/web/core"
"reflect" "reflect"
"time"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
@@ -22,6 +23,7 @@ func ErrorHandler(c *fiber.Ctx, err error) error {
var bizErr *core.BizErr var bizErr *core.BizErr
var servErr *core.ServErr var servErr *core.ServErr
var jsonErr *json.UnmarshalTypeError var jsonErr *json.UnmarshalTypeError
var timeErr *time.ParseError
switch { switch {
@@ -55,9 +57,21 @@ func ErrorHandler(c *fiber.Ctx, err error) error {
code = fiber.StatusBadRequest code = fiber.StatusBadRequest
message = fmt.Sprintf("参数 %s 类型不正确,传入类型为 %s正确类型应该为 %s", jsonErr.Field, jsonErr.Value, jsonErr.Type.Name()) 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: 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) c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)

105
web/handlers/coupon.go Normal file
View File

@@ -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
}

View File

@@ -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 { if err != nil {
return err return err
} }
@@ -670,28 +670,28 @@ func ResourcePrice(c *fiber.Ctx) error {
} }
// 获取套餐价格 // 获取套餐价格
sku, err := s.Resource.GetSku(req.CreateResourceData.Code()) // sku, err := s.Resource.GetSku(req.CreateResourceData.Code())
if err != nil { // if err != nil {
return err // 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 { if err != nil {
return err return err
} }
// 计算折扣 // 计算折扣
return c.JSON(ResourcePriceResp{ return c.JSON(ResourcePriceResp{
Discount: float32(sku.Discount.Discount) / 100, Price: detail.Amount.StringFixed(2),
Price: amount.StringFixed(2), Discounted: detail.Actual.StringFixed(2),
Discounted: discounted.StringFixed(2),
CouponApplied: couponApplied.StringFixed(2),
}) })
} }
type ResourcePriceResp struct { type ResourcePriceResp struct {
Price string `json:"price"` Price string `json:"price"`
Discount float32 `json:"discounted"` Discounted string `json:"discounted_price"`
Discounted string `json:"discounted_price"`
CouponApplied string `json:"coupon_applied"`
} }

View File

@@ -121,8 +121,7 @@ func TradeCreate(c *fiber.Ctx) error {
} }
// 处理订单 // 处理订单
uid := authCtx.User.ID result, err := s.Trade.Create(authCtx.User, req.CreateTradeData, req.Resource)
result, err := s.Trade.Create(uid, req.CreateTradeData, req.Resource)
if err != nil { if err != nil {
return core.NewServErr("处理购买产品信息失败", err) return core.NewServErr("处理购买产品信息失败", err)
} }

View File

@@ -28,8 +28,14 @@ func PageUserByAdmin(c *fiber.Ctx) error {
// 构建查询条件 // 构建查询条件
do := q.User.Where() do := q.User.Where()
if req.Phone != nil { if req.Account != nil {
do = do.Where(q.User.Phone.Eq(*req.Phone)) 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 { if req.Name != nil {
do = do.Where(q.User.Name.Eq(*req.Name)) 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). Preload(q.User.Admin).
Omit(q.User.Password). Omit(q.User.Password).
Where(do). Where(do).
@@ -85,7 +91,7 @@ func PageUserByAdmin(c *fiber.Ctx) error {
type PageUserByAdminReq struct { type PageUserByAdminReq struct {
core.PageReq core.PageReq
Phone *string `json:"phone,omitempty" validate:"omitempty,number"` Account *string `json:"account,omitempty"`
Name *string `json:"name,omitempty"` Name *string `json:"name,omitempty"`
Identified *bool `json:"identified,omitempty"` Identified *bool `json:"identified,omitempty"`
Enabled *bool `json:"enabled,omitempty"` Enabled *bool `json:"enabled,omitempty"`

View File

@@ -13,7 +13,7 @@ type ProductDiscount struct {
Discount int32 `json:"discount" gorm:"column:discount"` // 产品折扣 Discount int32 `json:"discount" gorm:"column:discount"` // 产品折扣
} }
func (pd ProductDiscount) Decimal() decimal.Decimal { func (pd ProductDiscount) Rate() decimal.Decimal {
return decimal.NewFromInt32(pd.Discount). return decimal.NewFromInt32(pd.Discount).
Div(decimal.NewFromInt32(100)) Div(decimal.NewFromInt32(100))
} }

View File

@@ -12,10 +12,12 @@ import (
type User struct { type User struct {
core.Model core.Model
AdminID *int32 `json:"admin_id,omitempty" gorm:"column:admin_id"` // 管理员ID 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"` // 手机号码 Phone string `json:"phone" gorm:"column:phone"` // 手机号码
Username *string `json:"username,omitempty" gorm:"column:username"` // 用户名 Username *string `json:"username,omitempty" gorm:"column:username"` // 用户名
Email *string `json:"email,omitempty" gorm:"column:email"` // 邮箱 Email *string `json:"email,omitempty" gorm:"column:email"` // 邮箱
Password *string `json:"password,omitempty" gorm:"column:password"` // 用户密码 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"` // 真实姓名 Name *string `json:"name,omitempty" gorm:"column:name"` // 真实姓名
Avatar *string `json:"avatar,omitempty" gorm:"column:avatar"` // 头像URL Avatar *string `json:"avatar,omitempty" gorm:"column:avatar"` // 头像URL
Status UserStatus `json:"status" gorm:"column:status"` // 用户状态0-禁用1-正常 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"` // 最后登录地址 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"` // 最后登录代理 LastLoginUA *string `json:"last_login_ua,omitempty" gorm:"column:last_login_ua"` // 最后登录代理
Admin *Admin `json:"admin,omitempty" gorm:"foreignKey:AdminID"` Admin *Admin `json:"admin,omitempty" gorm:"foreignKey:AdminID"`
Roles []*UserRole `json:"roles" gorm:"many2many:link_user_role"` Roles []*UserRole `json:"roles" gorm:"many2many:link_user_role"`
Discount *ProductDiscount `json:"discount,omitempty" gorm:"foreignKey:DiscountID"`
} }
// UserStatus 用户状态枚举 // UserStatus 用户状态枚举
@@ -49,3 +52,13 @@ const (
UserIDTypePersonal UserIDType = 1 // 个人认证 UserIDTypePersonal UserIDType = 1 // 个人认证
UserIDTypeEnterprise UserIDType = 2 // 企业认证 UserIDTypeEnterprise UserIDType = 2 // 企业认证
) )
// UserSource 用户来源枚举
type UserSource int
const (
UserSourceReg UserSource = 0 // 官网注册
UserSourceAdd UserSource = 1 // 管理员添加
UserSourceAgentReg UserSource = 2 // 代理商注册
UserSourceAgentAdd UserSource = 3 // 代理商添加
)

View File

@@ -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 { Roles: struct {
field.RelationField field.RelationField
Permissions struct { Permissions struct {
@@ -329,6 +334,9 @@ type billBelongsToUser struct {
} }
} }
} }
Discount struct {
field.RelationField
}
Roles struct { Roles struct {
field.RelationField field.RelationField
Permissions struct { Permissions struct {

View File

@@ -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 { Roles: struct {
field.RelationField field.RelationField
Permissions struct { Permissions struct {
@@ -385,6 +390,9 @@ type channelBelongsToUser struct {
} }
} }
} }
Discount struct {
field.RelationField
}
Roles struct { Roles struct {
field.RelationField field.RelationField
Permissions struct { Permissions struct {

View File

@@ -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 { Roles: struct {
field.RelationField field.RelationField
Permissions struct { Permissions struct {
@@ -209,6 +214,9 @@ type logsLoginBelongsToUser struct {
} }
} }
} }
Discount struct {
field.RelationField
}
Roles struct { Roles struct {
field.RelationField field.RelationField
Permissions struct { Permissions struct {

View File

@@ -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 { Roles: struct {
field.RelationField field.RelationField
Permissions struct { Permissions struct {
@@ -310,6 +315,9 @@ type logsRequestBelongsToUser struct {
} }
} }
} }
Discount struct {
field.RelationField
}
Roles struct { Roles struct {
field.RelationField field.RelationField
Permissions struct { Permissions struct {

View File

@@ -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 { Roles: struct {
field.RelationField field.RelationField
Permissions struct { Permissions struct {
@@ -286,6 +291,9 @@ type logsUserUsageBelongsToUser struct {
} }
} }
} }
Discount struct {
field.RelationField
}
Roles struct { Roles struct {
field.RelationField field.RelationField
Permissions struct { Permissions struct {

View File

@@ -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 { Roles: struct {
field.RelationField field.RelationField
Permissions struct { Permissions struct {
@@ -233,6 +238,9 @@ type productSkuUserBelongsToUser struct {
} }
} }
} }
Discount struct {
field.RelationField
}
Roles struct { Roles struct {
field.RelationField field.RelationField
Permissions struct { Permissions struct {

View File

@@ -60,6 +60,9 @@ func newProxy(db *gorm.DB, opts ...gen.DOOption) proxy {
} }
} }
} }
Discount struct {
field.RelationField
}
Roles struct { Roles struct {
field.RelationField field.RelationField
Permissions struct { 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 { Roles: struct {
field.RelationField field.RelationField
Permissions struct { Permissions struct {
@@ -358,6 +366,9 @@ type proxyHasManyChannels struct {
} }
} }
} }
Discount struct {
field.RelationField
}
Roles struct { Roles struct {
field.RelationField field.RelationField
Permissions struct { Permissions struct {

View File

@@ -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 { Roles: struct {
field.RelationField field.RelationField
Permissions struct { Permissions struct {
@@ -529,6 +534,9 @@ type resourceBelongsToUser struct {
} }
} }
} }
Discount struct {
field.RelationField
}
Roles struct { Roles struct {
field.RelationField field.RelationField
Permissions struct { Permissions struct {

View File

@@ -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 { Roles: struct {
field.RelationField field.RelationField
Permissions struct { Permissions struct {
@@ -260,6 +265,9 @@ type sessionBelongsToUser struct {
} }
} }
} }
Discount struct {
field.RelationField
}
Roles struct { Roles struct {
field.RelationField field.RelationField
Permissions struct { Permissions struct {

View File

@@ -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 { Roles: struct {
field.RelationField field.RelationField
Permissions struct { Permissions struct {
@@ -257,6 +262,9 @@ type tradeBelongsToUser struct {
} }
} }
} }
Discount struct {
field.RelationField
}
Roles struct { Roles struct {
field.RelationField field.RelationField
Permissions struct { Permissions struct {

View File

@@ -32,10 +32,12 @@ func newUser(db *gorm.DB, opts ...gen.DOOption) user {
_user.UpdatedAt = field.NewTime(tableName, "updated_at") _user.UpdatedAt = field.NewTime(tableName, "updated_at")
_user.DeletedAt = field.NewField(tableName, "deleted_at") _user.DeletedAt = field.NewField(tableName, "deleted_at")
_user.AdminID = field.NewInt32(tableName, "admin_id") _user.AdminID = field.NewInt32(tableName, "admin_id")
_user.DiscountID = field.NewInt32(tableName, "discount_id")
_user.Phone = field.NewString(tableName, "phone") _user.Phone = field.NewString(tableName, "phone")
_user.Username = field.NewString(tableName, "username") _user.Username = field.NewString(tableName, "username")
_user.Email = field.NewString(tableName, "email") _user.Email = field.NewString(tableName, "email")
_user.Password = field.NewString(tableName, "password") _user.Password = field.NewString(tableName, "password")
_user.Source = field.NewInt(tableName, "source")
_user.Name = field.NewString(tableName, "name") _user.Name = field.NewString(tableName, "name")
_user.Avatar = field.NewString(tableName, "avatar") _user.Avatar = field.NewString(tableName, "avatar")
_user.Status = field.NewInt(tableName, "status") _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{ _user.Roles = userManyToManyRoles{
db: db.Session(&gorm.Session{}), db: db.Session(&gorm.Session{}),
@@ -114,10 +122,12 @@ type user struct {
UpdatedAt field.Time UpdatedAt field.Time
DeletedAt field.Field DeletedAt field.Field
AdminID field.Int32 AdminID field.Int32
DiscountID field.Int32
Phone field.String Phone field.String
Username field.String Username field.String
Email field.String Email field.String
Password field.String Password field.String
Source field.Int
Name field.String Name field.String
Avatar field.String Avatar field.String
Status field.Int Status field.Int
@@ -132,6 +142,8 @@ type user struct {
LastLoginUA field.String LastLoginUA field.String
Admin userBelongsToAdmin Admin userBelongsToAdmin
Discount userBelongsToDiscount
Roles userManyToManyRoles Roles userManyToManyRoles
fieldMap map[string]field.Expr fieldMap map[string]field.Expr
@@ -154,10 +166,12 @@ func (u *user) updateTableName(table string) *user {
u.UpdatedAt = field.NewTime(table, "updated_at") u.UpdatedAt = field.NewTime(table, "updated_at")
u.DeletedAt = field.NewField(table, "deleted_at") u.DeletedAt = field.NewField(table, "deleted_at")
u.AdminID = field.NewInt32(table, "admin_id") u.AdminID = field.NewInt32(table, "admin_id")
u.DiscountID = field.NewInt32(table, "discount_id")
u.Phone = field.NewString(table, "phone") u.Phone = field.NewString(table, "phone")
u.Username = field.NewString(table, "username") u.Username = field.NewString(table, "username")
u.Email = field.NewString(table, "email") u.Email = field.NewString(table, "email")
u.Password = field.NewString(table, "password") u.Password = field.NewString(table, "password")
u.Source = field.NewInt(table, "source")
u.Name = field.NewString(table, "name") u.Name = field.NewString(table, "name")
u.Avatar = field.NewString(table, "avatar") u.Avatar = field.NewString(table, "avatar")
u.Status = field.NewInt(table, "status") u.Status = field.NewInt(table, "status")
@@ -186,16 +200,18 @@ func (u *user) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
} }
func (u *user) fillFieldMap() { 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["id"] = u.ID
u.fieldMap["created_at"] = u.CreatedAt u.fieldMap["created_at"] = u.CreatedAt
u.fieldMap["updated_at"] = u.UpdatedAt u.fieldMap["updated_at"] = u.UpdatedAt
u.fieldMap["deleted_at"] = u.DeletedAt u.fieldMap["deleted_at"] = u.DeletedAt
u.fieldMap["admin_id"] = u.AdminID u.fieldMap["admin_id"] = u.AdminID
u.fieldMap["discount_id"] = u.DiscountID
u.fieldMap["phone"] = u.Phone u.fieldMap["phone"] = u.Phone
u.fieldMap["username"] = u.Username u.fieldMap["username"] = u.Username
u.fieldMap["email"] = u.Email u.fieldMap["email"] = u.Email
u.fieldMap["password"] = u.Password u.fieldMap["password"] = u.Password
u.fieldMap["source"] = u.Source
u.fieldMap["name"] = u.Name u.fieldMap["name"] = u.Name
u.fieldMap["avatar"] = u.Avatar u.fieldMap["avatar"] = u.Avatar
u.fieldMap["status"] = u.Status u.fieldMap["status"] = u.Status
@@ -215,6 +231,8 @@ func (u user) clone(db *gorm.DB) user {
u.userDo.ReplaceConnPool(db.Statement.ConnPool) u.userDo.ReplaceConnPool(db.Statement.ConnPool)
u.Admin.db = db.Session(&gorm.Session{Initialized: true}) u.Admin.db = db.Session(&gorm.Session{Initialized: true})
u.Admin.db.Statement.ConnPool = db.Statement.ConnPool 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 = db.Session(&gorm.Session{Initialized: true})
u.Roles.db.Statement.ConnPool = db.Statement.ConnPool u.Roles.db.Statement.ConnPool = db.Statement.ConnPool
return u return u
@@ -223,6 +241,7 @@ func (u user) clone(db *gorm.DB) user {
func (u user) replaceDB(db *gorm.DB) user { func (u user) replaceDB(db *gorm.DB) user {
u.userDo.ReplaceDB(db) u.userDo.ReplaceDB(db)
u.Admin.db = db.Session(&gorm.Session{}) u.Admin.db = db.Session(&gorm.Session{})
u.Discount.db = db.Session(&gorm.Session{})
u.Roles.db = db.Session(&gorm.Session{}) u.Roles.db = db.Session(&gorm.Session{})
return u return u
} }
@@ -321,6 +340,87 @@ func (a userBelongsToAdminTx) Unscoped() *userBelongsToAdminTx {
return &a 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 { type userManyToManyRoles struct {
db *gorm.DB db *gorm.DB

View File

@@ -176,9 +176,20 @@ func adminRouter(api fiber.Router) {
product.Post("/sku/update", handlers.UpdateProductSku) product.Post("/sku/update", handlers.UpdateProductSku)
product.Post("/sku/update/discount/batch", handlers.BatchUpdateProductSkuDiscount) product.Post("/sku/update/discount/batch", handlers.BatchUpdateProductSkuDiscount)
product.Post("/sku/remove", handlers.DeleteProductSku) product.Post("/sku/remove", handlers.DeleteProductSku)
product.Post("/discount/page", handlers.PageProductDiscountByAdmin)
product.Post("/discount/all", handlers.AllProductDiscountsByAdmin) // discount 折扣
product.Post("/discount/create", handlers.CreateProductDiscount) var discount = api.Group("/discount")
product.Post("/discount/update", handlers.UpdateProductDiscount) discount.Post("/page", handlers.PageProductDiscountByAdmin)
product.Post("/discount/remove", handlers.DeleteProductDiscount) 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)
} }

View File

@@ -3,8 +3,6 @@ package services
import ( import (
m "platform/web/models" m "platform/web/models"
q "platform/web/queries" q "platform/web/queries"
"github.com/shopspring/decimal"
) )
var Bill = &billService{} 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{ return q.Bill.Create(&m.Bill{
UserID: uid, UserID: uid,
BillNo: ID.GenReadable("bil"), BillNo: ID.GenReadable("bil"),
ResourceID: &resourceId, ResourceID: &resourceId,
TradeID: &tradeId, TradeID: tradeId,
CouponID: detail.CouponId, CouponID: detail.CouponId,
Type: m.BillTypeConsume, Type: m.BillTypeConsume,
Info: &detail.Subject, Info: &detail.Subject,
@@ -36,16 +34,3 @@ func (s *billService) CreateForResourceByTrade(q *q.Query, uid, tradeId, resourc
Actual: detail.Actual, 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,
})
}

View File

@@ -9,6 +9,7 @@ import (
"time" "time"
"github.com/shopspring/decimal" "github.com/shopspring/decimal"
"gorm.io/gen/field"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -16,6 +17,80 @@ var Coupon = &couponService{}
type couponService struct{} 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) { func (s *couponService) GetCouponAvailableByCode(code string, amount decimal.Decimal, uid *int32) (*m.Coupon, error) {
// 获取优惠券 // 获取优惠券
coupon, err := q.Coupon.Where( coupon, err := q.Coupon.Where(

View File

@@ -1,7 +1,6 @@
package services package services
import ( import (
"errors"
"fmt" "fmt"
"platform/pkg/u" "platform/pkg/u"
"platform/web/core" "platform/web/core"
@@ -11,7 +10,6 @@ import (
"github.com/shopspring/decimal" "github.com/shopspring/decimal"
"gorm.io/gen/field" "gorm.io/gen/field"
"gorm.io/gorm"
) )
var Resource = &resourceService{} var Resource = &resourceService{}
@@ -19,33 +17,16 @@ var Resource = &resourceService{}
type resourceService struct{} type resourceService struct{}
// CreateResourceByBalance 通过余额购买套餐 // CreateResourceByBalance 通过余额购买套餐
func (s *resourceService) CreateResourceByBalance(uid int32, now time.Time, data *CreateResourceData) error { func (s *resourceService) CreateResourceByBalance(user *m.User, data *CreateResourceData) error {
now := time.Now()
// 找到用户
user, err := q.User.
Where(q.User.ID.Eq(uid)).
Take()
if err != nil {
return err
}
// 获取 sku // 获取 sku
sku, err := s.GetSku(data.Code()) detail, err := data.TradeDetail(user)
if err != nil { if err != nil {
return err return core.NewServErr("获取产品支付信息失败", err)
} }
// 检查余额 newBalance := user.Balance.Sub(detail.Actual)
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)
if newBalance.IsNegative() { if newBalance.IsNegative() {
return ErrBalanceNotEnough return ErrBalanceNotEnough
} }
@@ -55,7 +36,7 @@ func (s *resourceService) CreateResourceByBalance(uid int32, now time.Time, data
// 更新用户余额 // 更新用户余额
_, err = q.User. _, err = q.User.
Where( Where(
q.User.ID.Eq(uid), q.User.ID.Eq(user.ID),
q.User.Balance.Eq(user.Balance), q.User.Balance.Eq(user.Balance),
). ).
UpdateSimple(q.User.Balance.Value(newBalance)) 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 { if err != nil {
return core.NewServErr("创建套餐失败", err) 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 { if err != nil {
return core.NewServErr("生成账单失败", err) return core.NewServErr("生成账单失败", err)
} }
// 核销优惠券 // 核销优惠券
if coupon != nil { if detail.CouponId != nil {
err = Coupon.UseCoupon(q, coupon.ID) err = Coupon.UseCoupon(q, *detail.CouponId)
if err != nil { if err != nil {
return core.NewServErr("核销优惠券失败", err) return core.NewServErr("核销优惠券失败", err)
} }
@@ -174,64 +155,59 @@ type UpdateResourceData struct {
Active *bool `json:"active"` 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. sku, err := q.ProductSku.
Joins(q.ProductSku.Discount). Joins(q.ProductSku.Discount).
Where(q.ProductSku.Code.Eq(code)). Where(q.ProductSku.Code.Eq(skuCode)).
Take() Take()
if err != nil { 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 price := sku.Price
amount := price.Mul(decimal.NewFromInt32(count)) amount := price.Mul(decimal.NewFromInt32(count))
// 折扣价 // 折扣价
discount := sku.Discount.Decimal() discount := sku.Discount
if uid != nil { // 用户特殊优惠 if discount == nil {
var err error return nil, nil, nil, decimal.Zero, decimal.Zero, core.NewServErr("价格查询失败", err)
uSku, err := q.ProductSkuUser. }
Joins(q.ProductSkuUser.Discount).
Where( discountRate := discount.Rate()
q.ProductSkuUser.UserID.Eq(*uid), if user != nil && user.DiscountID != nil { // 用户特殊优惠
q.ProductSkuUser.ProductSkuID.Eq(sku.ID)). uDiscount, err := q.ProductDiscount.Where(q.ProductDiscount.ID.Eq(*user.DiscountID)).Take()
Take() if err != nil {
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil, nil, decimal.Zero, decimal.Zero, core.NewServErr("客户特殊价查询失败", err)
return nil, decimal.Zero, decimal.Zero, decimal.Zero, core.NewServErr("客户特殊价查询失败", err)
} }
if uSku.Discount == nil {
return nil, decimal.Zero, decimal.Zero, decimal.Zero, core.NewServErr("价格获取失败") uDiscountRate := uDiscount.Rate()
} if uDiscountRate.Cmp(discountRate) > 0 {
uDiscount := uSku.Discount.Decimal() discountRate = uDiscountRate
if uDiscount.Cmp(discount) > 0 {
discount = uDiscount discount = uDiscount
} }
} }
discounted := amount.Mul(discount) discounted := amount.Mul(discountRate)
// 优惠价 // 优惠价
uid := (*int32)(nil)
if user != nil {
uid = &user.ID
}
coupon := (*m.Coupon)(nil) coupon := (*m.Coupon)(nil)
couponApplied := discounted.Copy() couponApplied := discounted.Copy()
if couponCode != nil { if couponCode != nil {
var err error var err error
coupon, err = Coupon.GetCouponAvailableByCode(*couponCode, discounted, uid) coupon, err = Coupon.GetCouponAvailableByCode(*couponCode, discounted, uid)
if err != nil { 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) couponApplied = discounted.Sub(coupon.Amount)
} }
return coupon, amount, discounted, couponApplied, nil return sku, discount, coupon, discounted, couponApplied, nil
} }
type CreateResourceData struct { type CreateResourceData struct {
@@ -261,52 +237,49 @@ type CreateLongResourceData struct {
price *decimal.Decimal price *decimal.Decimal
} }
func (c *CreateResourceData) Count() int32 { func (data *CreateResourceData) Count() int32 {
switch { switch {
default: default:
return 0 return 0
case c.Type == m.ResourceTypeShort && c.Short != nil: case data.Type == m.ResourceTypeShort && data.Short != nil:
return c.Short.Quota return data.Short.Quota
case c.Type == m.ResourceTypeLong && c.Long != nil: case data.Type == m.ResourceTypeLong && data.Long != nil:
return c.Long.Quota return data.Long.Quota
} }
} }
func (c *CreateResourceData) Code() string { func (data *CreateResourceData) Code() string {
switch { switch {
default: default:
return "" return ""
case c.Type == m.ResourceTypeShort && c.Short != nil: case data.Type == m.ResourceTypeShort && data.Short != nil:
return fmt.Sprintf( return fmt.Sprintf(
"mode=%s,live=%d,expire=%d", "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( return fmt.Sprintf(
"mode=%s,live=%d,expire=%d", "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) { func (data *CreateResourceData) TradeDetail(user *m.User) (*TradeDetail, error) {
sku, err := Resource.GetSku(c.Code()) sku, discount, coupon, amount, actual, err := Resource.CalcPrice(data.Code(), data.Count(), user, data.CouponCode)
if err != nil {
return nil, err
}
coupon, _, amount, actual, err := Resource.GetPrice(sku, c.Count(), nil, c.CouponCode)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &TradeDetail{ return &TradeDetail{
data,
m.TradeTypePurchase, m.TradeTypePurchase,
sku.Name, sku.Name,
amount, actual, amount, actual,
&coupon.ID, c, &discount.ID, discount,
&coupon.ID, coupon,
}, nil }, nil
} }

View File

@@ -32,8 +32,12 @@ type tradeService struct {
} }
// 创建交易 // 创建交易
func (s *tradeService) Create(uid int32, tradeData *CreateTradeData, productData *CreateResourceData) (*CreateTradeResult, error) { func (s *tradeService) Create(user *m.User, tradeData *CreateTradeData, productData *CreateResourceData) (*CreateTradeResult, error) {
detail, err := productData.TradeDetail() if user == nil {
return nil, core.NewBizErr("用户未登录")
}
detail, err := productData.TradeDetail(user)
if err != nil { if err != nil {
return nil, core.NewServErr("获取产品支付信息失败", err) return nil, core.NewServErr("获取产品支付信息失败", err)
} }
@@ -178,7 +182,7 @@ func (s *tradeService) Create(uid int32, tradeData *CreateTradeData, productData
// 保存订单 // 保存订单
err = q.Trade.Create(&m.Trade{ err = q.Trade.Create(&m.Trade{
UserID: uid, UserID: user.ID,
InnerNo: tradeNo, InnerNo: tradeNo,
Type: detail.Type, Type: detail.Type,
Subject: detail.Subject, 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 { if err != nil {
return nil, core.NewServErr("提交异步关闭事件失败", err) 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 { if err != nil {
return core.NewServErr("生成账单失败", err) return core.NewServErr("生成账单失败", err)
} }
@@ -602,16 +606,19 @@ type OnTradeCompletedData struct {
} }
type ProductInfo interface { type ProductInfo interface {
TradeDetail() (*TradeDetail, error) TradeDetail(user *m.User) (*TradeDetail, error)
} }
type TradeDetail struct { type TradeDetail struct {
Type m.TradeType `json:"type"` Product ProductInfo `json:"product"`
Subject string `json:"subject"` Type m.TradeType `json:"type"`
Amount decimal.Decimal `json:"amount"` Subject string `json:"subject"`
Actual decimal.Decimal `json:"actual"` Amount decimal.Decimal `json:"amount"`
CouponId *int32 `json:"coupon_id,omitempty"` Actual decimal.Decimal `json:"actual"`
Product ProductInfo `json:"product"` DiscountId *int32 `json:"discount_id,omitempty"`
Discount *m.ProductDiscount `json:"-"` // 不应缓存
CouponId *int32 `json:"coupon_id,omitempty"`
Coupon *m.Coupon `json:"-"` // 不应缓存
} }
type CompleteEvent interface { type CompleteEvent interface {

View File

@@ -51,12 +51,14 @@ type UpdateBalanceData struct {
Amount int `json:"amount"` Amount int `json:"amount"`
} }
func (c *UpdateBalanceData) TradeDetail() (*TradeDetail, error) { func (data *UpdateBalanceData) TradeDetail(user *m.User) (*TradeDetail, error) {
amount := decimal.NewFromInt(int64(c.Amount)).Div(decimal.NewFromInt(100)) amount := decimal.NewFromInt(int64(data.Amount)).Div(decimal.NewFromInt(100))
return &TradeDetail{ return &TradeDetail{
data,
m.TradeTypeRecharge, m.TradeTypeRecharge,
fmt.Sprintf("账户充值 - %s元", amount.StringFixed(2)), fmt.Sprintf("账户充值 - %s元", amount.StringFixed(2)),
amount, amount, amount, amount,
nil, c, nil, nil,
nil, nil,
}, nil }, nil
} }