From 8f2e71849f7d09b320c99bea7635e867d10bbb1f Mon Sep 17 00:00:00 2001 From: luorijun Date: Thu, 18 Dec 2025 14:22:56 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E7=94=A8=E6=88=B7=E5=92=A8?= =?UTF-8?q?=E8=AF=A2=E6=95=B0=E6=8D=AE=E6=94=B6=E9=9B=86=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/launch.json | 16 -- .zed/debug.json | 13 ++ README.md | 18 +- cmd/gen/main.go | 1 + scripts/sql/init.sql | 101 +++++++--- web/handlers/inquiry.go | 48 +++++ web/handlers/resource.go | 2 +- web/models/inquiry.go | 25 +++ web/queries/gen.go | 8 + web/queries/inquiry.gen.go | 359 +++++++++++++++++++++++++++++++++ web/routes.go | 4 + web/services/channel.go | 6 +- web/services/channel_baiyin.go | 6 +- web/services/resource.go | 120 +++++------ 14 files changed, 606 insertions(+), 121 deletions(-) delete mode 100644 .vscode/launch.json create mode 100644 .zed/debug.json create mode 100644 web/handlers/inquiry.go create mode 100644 web/models/inquiry.go create mode 100644 web/queries/inquiry.gen.go diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index bbd6612..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - // 使用 IntelliSense 了解相关属性。 - // 悬停以查看现有属性的描述。 - // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "main", - "type": "go", - "request": "launch", - "mode": "debug", - "program": "${workspaceFolder}/cmd/main", - "cwd": "${workspaceFolder}" - } - ] -} diff --git a/.zed/debug.json b/.zed/debug.json new file mode 100644 index 0000000..5368a71 --- /dev/null +++ b/.zed/debug.json @@ -0,0 +1,13 @@ +// Project-local debug tasks +// +// For more documentation on how to configure debug tasks, +// see: https://zed.dev/docs/debugger +[ + { + "label": "debug main", + "adapter": "Delve", + "request": "launch", + "mode": "debug", + "program": "./cmd/main" + } +] diff --git a/README.md b/README.md index 4bfbc50..522a253 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,15 @@ ## TODO +价格与优惠 + 优化中间件,配置通用限速 -trade/create 性能问题,缩短事务时间,考虑其他方式实现可靠分布式事务 - -jsonb 类型转换问题,考虑一个高效的 any 到 struct 转换工具 - -端口资源池的 gc 实现 - -channel 服务代码结构,用 provider 代替整个 service 的复用 - -用反射实现环境变量解析,以简化函数签名 +observe 部署,蓝狐部署 --- +用反射实现环境变量解析,以简化函数签名 + 分离 task 的客户端,支持多进程(prefork 必要!) 调整目录结构: @@ -39,6 +35,10 @@ http 调用 clients 的初始化函数 --- +数据库转模型文件 + +jsonb 类型转换问题,考虑一个高效的 any 到 struct 转换工具 + 慢速请求底层调用埋点监控 - redis - gorm diff --git a/cmd/gen/main.go b/cmd/gen/main.go index 2ec3a95..7aad34e 100644 --- a/cmd/gen/main.go +++ b/cmd/gen/main.go @@ -61,6 +61,7 @@ func main() { m.User{}, m.UserRole{}, m.Whitelist{}, + m.Inquiry{}, ) g.Execute() } diff --git a/scripts/sql/init.sql b/scripts/sql/init.sql index 2e7aefc..4700451 100644 --- a/scripts/sql/init.sql +++ b/scripts/sql/init.sql @@ -108,6 +108,76 @@ comment on column logs_user_bandwidth.time is '记录时间'; -- endregion +-- ==================== +-- region 系统信息 +-- ==================== + +-- announcement +drop table if exists announcement cascade; +create table announcement ( + id int generated by default as identity primary key, + title text not null, + content text, + type int not null default 1, + pin bool not null default false, + status int not null default 1, + sort int not null default 0, + created_at timestamptz default current_timestamp, + updated_at timestamptz default current_timestamp, + deleted_at timestamptz +); +create index idx_announcement_type on announcement (type) where deleted_at is null; +create index idx_announcement_pin on announcement (pin) where deleted_at is null; +create index idx_announcement_created_at on announcement (created_at) where deleted_at is null; + +-- announcement表字段注释 +comment on table announcement is '公告表'; +comment on column announcement.id is '公告ID'; +comment on column announcement.title is '公告标题'; +comment on column announcement.content is '公告内容'; +comment on column announcement.type is '公告类型:1-普通公告'; +comment on column announcement.status is '公告状态:0-禁用,1-正常'; +comment on column announcement.pin is '是否置顶'; +comment on column announcement.sort is '公告排序'; +comment on column announcement.created_at is '创建时间'; +comment on column announcement.updated_at is '更新时间'; +comment on column announcement.deleted_at is '删除时间'; + +-- inquiry +drop table if exists inquiry cascade; +create table inquiry ( + id int generated by default as identity primary key, + company text, + name text, + phone text, + email text, + content text, + status int not null default 0, + remark text, + created_at timestamptz default current_timestamp, + updated_at timestamptz default current_timestamp, + deleted_at timestamptz +); +create index idx_inquiry_phone on inquiry (phone) where deleted_at is null; +create index idx_inquiry_status on inquiry (status) where deleted_at is null; +create index idx_inquiry_created_at on inquiry (created_at) where deleted_at is null; + +-- inquiry表字段注释 +comment on table inquiry is '用户咨询表'; +comment on column inquiry.id is '咨询ID'; +comment on column inquiry.name is '联系人姓名'; +comment on column inquiry.phone is '联系电话'; +comment on column inquiry.email is '联系邮箱'; +comment on column inquiry.company is '公司名称'; +comment on column inquiry.content is '咨询内容'; +comment on column inquiry.status is '处理状态:0-待处理,1-已处理'; +comment on column inquiry.remark is '备注'; +comment on column inquiry.created_at is '创建时间'; +comment on column inquiry.updated_at is '更新时间'; +comment on column inquiry.deleted_at is '删除时间'; + +-- endregion + -- ==================== -- region 管理员信息 -- ==================== @@ -177,37 +247,6 @@ comment on column admin_role.created_at is '创建时间'; comment on column admin_role.updated_at is '更新时间'; comment on column admin_role.deleted_at is '删除时间'; --- announcement -drop table if exists announcement cascade; -create table announcement ( - id int generated by default as identity primary key, - title text not null, - content text, - type int not null default 1, - pin bool not null default false, - status int not null default 1, - sort int not null default 0, - created_at timestamptz default current_timestamp, - updated_at timestamptz default current_timestamp, - deleted_at timestamptz -); -create index idx_announcement_type on announcement (type) where deleted_at is null; -create index idx_announcement_pin on announcement (pin) where deleted_at is null; -create index idx_announcement_created_at on announcement (created_at) where deleted_at is null; - --- announcement表字段注释 -comment on table announcement is '公告表'; -comment on column announcement.id is '公告ID'; -comment on column announcement.title is '公告标题'; -comment on column announcement.content is '公告内容'; -comment on column announcement.type is '公告类型:1-普通公告'; -comment on column announcement.status is '公告状态:0-禁用,1-正常'; -comment on column announcement.pin is '是否置顶'; -comment on column announcement.sort is '公告排序'; -comment on column announcement.created_at is '创建时间'; -comment on column announcement.updated_at is '更新时间'; -comment on column announcement.deleted_at is '删除时间'; - -- endregion -- ==================== diff --git a/web/handlers/inquiry.go b/web/handlers/inquiry.go new file mode 100644 index 0000000..0526b53 --- /dev/null +++ b/web/handlers/inquiry.go @@ -0,0 +1,48 @@ +package handlers + +import ( + "platform/pkg/u" + "platform/web/core" + g "platform/web/globals" + m "platform/web/models" + q "platform/web/queries" + + "github.com/gofiber/fiber/v2" +) + +// region CreateInquiry + +type CreateInquiryRequest struct { + Company string `json:"company" validate:"omitempty,max=200"` + Name string `json:"name" validate:"required,max=100"` + Phone string `json:"phone" validate:"required,max=20"` + Email string `json:"email" validate:"omitempty,email,max=100"` + Content string `json:"content" validate:"required,max=1000"` +} + +func CreateInquiry(c *fiber.Ctx) error { + + // 解析请求参数 + req := new(CreateInquiryRequest) + err := g.Validator.ParseBody(c, req) + if err != nil { + return err + } + + // 创建咨询记录 + err = q.Inquiry.Create(&m.Inquiry{ + Company: u.X(req.Company), + Name: u.X(req.Name), + Phone: u.X(req.Phone), + Email: u.X(req.Email), + Content: u.X(req.Content), + Status: m.InquiryStatusPending, + }) + if err != nil { + return core.NewServErr("提交咨询失败", err) + } + + return c.SendStatus(fiber.StatusNoContent) +} + +// endregion diff --git a/web/handlers/resource.go b/web/handlers/resource.go index 1abb6b9..5194f2c 100644 --- a/web/handlers/resource.go +++ b/web/handlers/resource.go @@ -439,7 +439,7 @@ func ResourcePrice(c *fiber.Ctx) error { // 解析请求参数 var req = new(CreateResourceReq) if err := g.Validator.ParseBody(c, req); err != nil { - return err + return core.NewBizErr("接口参数解析异常", err) } // 获取套餐价格 diff --git a/web/models/inquiry.go b/web/models/inquiry.go new file mode 100644 index 0000000..e6616e8 --- /dev/null +++ b/web/models/inquiry.go @@ -0,0 +1,25 @@ +package models + +import ( + "platform/web/core" +) + +// Inquiry 用户咨询表 +type Inquiry struct { + core.Model + Company *string `json:"company,omitempty" gorm:"column:company"` // 公司名称 + Name *string `json:"name,omitempty" gorm:"column:name"` // 联系人姓名 + Phone *string `json:"phone,omitempty" gorm:"column:phone"` // 联系电话 + Email *string `json:"email,omitempty" gorm:"column:email"` // 联系邮箱 + Content *string `json:"content,omitempty" gorm:"column:content"` // 咨询内容 + Status InquiryStatus `json:"status" gorm:"column:status"` // 处理状态:0-待处理,1-已处理 + Remark *string `json:"remark,omitempty" gorm:"column:remark"` // 备注 +} + +// InquiryStatus 咨询处理状态枚举 +type InquiryStatus int + +const ( + InquiryStatusPending InquiryStatus = 0 // 待处理 + InquiryStatusProcessed InquiryStatus = 1 // 已处理 +) diff --git a/web/queries/gen.go b/web/queries/gen.go index fbbd2af..d0b6964 100644 --- a/web/queries/gen.go +++ b/web/queries/gen.go @@ -25,6 +25,7 @@ var ( Client *client Coupon *coupon Edge *edge + Inquiry *inquiry LinkAdminRole *linkAdminRole LinkAdminRolePermission *linkAdminRolePermission LinkClientPermission *linkClientPermission @@ -58,6 +59,7 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) { Client = &Q.Client Coupon = &Q.Coupon Edge = &Q.Edge + Inquiry = &Q.Inquiry LinkAdminRole = &Q.LinkAdminRole LinkAdminRolePermission = &Q.LinkAdminRolePermission LinkClientPermission = &Q.LinkClientPermission @@ -92,6 +94,7 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query { Client: newClient(db, opts...), Coupon: newCoupon(db, opts...), Edge: newEdge(db, opts...), + Inquiry: newInquiry(db, opts...), LinkAdminRole: newLinkAdminRole(db, opts...), LinkAdminRolePermission: newLinkAdminRolePermission(db, opts...), LinkClientPermission: newLinkClientPermission(db, opts...), @@ -127,6 +130,7 @@ type Query struct { Client client Coupon coupon Edge edge + Inquiry inquiry LinkAdminRole linkAdminRole LinkAdminRolePermission linkAdminRolePermission LinkClientPermission linkClientPermission @@ -163,6 +167,7 @@ func (q *Query) clone(db *gorm.DB) *Query { Client: q.Client.clone(db), Coupon: q.Coupon.clone(db), Edge: q.Edge.clone(db), + Inquiry: q.Inquiry.clone(db), LinkAdminRole: q.LinkAdminRole.clone(db), LinkAdminRolePermission: q.LinkAdminRolePermission.clone(db), LinkClientPermission: q.LinkClientPermission.clone(db), @@ -206,6 +211,7 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query { Client: q.Client.replaceDB(db), Coupon: q.Coupon.replaceDB(db), Edge: q.Edge.replaceDB(db), + Inquiry: q.Inquiry.replaceDB(db), LinkAdminRole: q.LinkAdminRole.replaceDB(db), LinkAdminRolePermission: q.LinkAdminRolePermission.replaceDB(db), LinkClientPermission: q.LinkClientPermission.replaceDB(db), @@ -239,6 +245,7 @@ type queryCtx struct { Client *clientDo Coupon *couponDo Edge *edgeDo + Inquiry *inquiryDo LinkAdminRole *linkAdminRoleDo LinkAdminRolePermission *linkAdminRolePermissionDo LinkClientPermission *linkClientPermissionDo @@ -272,6 +279,7 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx { Client: q.Client.WithContext(ctx), Coupon: q.Coupon.WithContext(ctx), Edge: q.Edge.WithContext(ctx), + Inquiry: q.Inquiry.WithContext(ctx), LinkAdminRole: q.LinkAdminRole.WithContext(ctx), LinkAdminRolePermission: q.LinkAdminRolePermission.WithContext(ctx), LinkClientPermission: q.LinkClientPermission.WithContext(ctx), diff --git a/web/queries/inquiry.gen.go b/web/queries/inquiry.gen.go new file mode 100644 index 0000000..82b048c --- /dev/null +++ b/web/queries/inquiry.gen.go @@ -0,0 +1,359 @@ +// Code generated by gorm.io/gen. DO NOT EDIT. +// Code generated by gorm.io/gen. DO NOT EDIT. +// Code generated by gorm.io/gen. DO NOT EDIT. + +package queries + +import ( + "context" + + "gorm.io/gorm" + "gorm.io/gorm/clause" + "gorm.io/gorm/schema" + + "gorm.io/gen" + "gorm.io/gen/field" + + "gorm.io/plugin/dbresolver" + + "platform/web/models" +) + +func newInquiry(db *gorm.DB, opts ...gen.DOOption) inquiry { + _inquiry := inquiry{} + + _inquiry.inquiryDo.UseDB(db, opts...) + _inquiry.inquiryDo.UseModel(&models.Inquiry{}) + + tableName := _inquiry.inquiryDo.TableName() + _inquiry.ALL = field.NewAsterisk(tableName) + _inquiry.ID = field.NewInt32(tableName, "id") + _inquiry.CreatedAt = field.NewTime(tableName, "created_at") + _inquiry.UpdatedAt = field.NewTime(tableName, "updated_at") + _inquiry.DeletedAt = field.NewField(tableName, "deleted_at") + _inquiry.Company = field.NewString(tableName, "company") + _inquiry.Name = field.NewString(tableName, "name") + _inquiry.Phone = field.NewString(tableName, "phone") + _inquiry.Email = field.NewString(tableName, "email") + _inquiry.Content = field.NewString(tableName, "content") + _inquiry.Status = field.NewInt(tableName, "status") + _inquiry.Remark = field.NewString(tableName, "remark") + + _inquiry.fillFieldMap() + + return _inquiry +} + +type inquiry struct { + inquiryDo + + ALL field.Asterisk + ID field.Int32 + CreatedAt field.Time + UpdatedAt field.Time + DeletedAt field.Field + Company field.String + Name field.String + Phone field.String + Email field.String + Content field.String + Status field.Int + Remark field.String + + fieldMap map[string]field.Expr +} + +func (i inquiry) Table(newTableName string) *inquiry { + i.inquiryDo.UseTable(newTableName) + return i.updateTableName(newTableName) +} + +func (i inquiry) As(alias string) *inquiry { + i.inquiryDo.DO = *(i.inquiryDo.As(alias).(*gen.DO)) + return i.updateTableName(alias) +} + +func (i *inquiry) updateTableName(table string) *inquiry { + i.ALL = field.NewAsterisk(table) + i.ID = field.NewInt32(table, "id") + i.CreatedAt = field.NewTime(table, "created_at") + i.UpdatedAt = field.NewTime(table, "updated_at") + i.DeletedAt = field.NewField(table, "deleted_at") + i.Company = field.NewString(table, "company") + i.Name = field.NewString(table, "name") + i.Phone = field.NewString(table, "phone") + i.Email = field.NewString(table, "email") + i.Content = field.NewString(table, "content") + i.Status = field.NewInt(table, "status") + i.Remark = field.NewString(table, "remark") + + i.fillFieldMap() + + return i +} + +func (i *inquiry) GetFieldByName(fieldName string) (field.OrderExpr, bool) { + _f, ok := i.fieldMap[fieldName] + if !ok || _f == nil { + return nil, false + } + _oe, ok := _f.(field.OrderExpr) + return _oe, ok +} + +func (i *inquiry) fillFieldMap() { + i.fieldMap = make(map[string]field.Expr, 11) + i.fieldMap["id"] = i.ID + i.fieldMap["created_at"] = i.CreatedAt + i.fieldMap["updated_at"] = i.UpdatedAt + i.fieldMap["deleted_at"] = i.DeletedAt + i.fieldMap["company"] = i.Company + i.fieldMap["name"] = i.Name + i.fieldMap["phone"] = i.Phone + i.fieldMap["email"] = i.Email + i.fieldMap["content"] = i.Content + i.fieldMap["status"] = i.Status + i.fieldMap["remark"] = i.Remark +} + +func (i inquiry) clone(db *gorm.DB) inquiry { + i.inquiryDo.ReplaceConnPool(db.Statement.ConnPool) + return i +} + +func (i inquiry) replaceDB(db *gorm.DB) inquiry { + i.inquiryDo.ReplaceDB(db) + return i +} + +type inquiryDo struct{ gen.DO } + +func (i inquiryDo) Debug() *inquiryDo { + return i.withDO(i.DO.Debug()) +} + +func (i inquiryDo) WithContext(ctx context.Context) *inquiryDo { + return i.withDO(i.DO.WithContext(ctx)) +} + +func (i inquiryDo) ReadDB() *inquiryDo { + return i.Clauses(dbresolver.Read) +} + +func (i inquiryDo) WriteDB() *inquiryDo { + return i.Clauses(dbresolver.Write) +} + +func (i inquiryDo) Session(config *gorm.Session) *inquiryDo { + return i.withDO(i.DO.Session(config)) +} + +func (i inquiryDo) Clauses(conds ...clause.Expression) *inquiryDo { + return i.withDO(i.DO.Clauses(conds...)) +} + +func (i inquiryDo) Returning(value interface{}, columns ...string) *inquiryDo { + return i.withDO(i.DO.Returning(value, columns...)) +} + +func (i inquiryDo) Not(conds ...gen.Condition) *inquiryDo { + return i.withDO(i.DO.Not(conds...)) +} + +func (i inquiryDo) Or(conds ...gen.Condition) *inquiryDo { + return i.withDO(i.DO.Or(conds...)) +} + +func (i inquiryDo) Select(conds ...field.Expr) *inquiryDo { + return i.withDO(i.DO.Select(conds...)) +} + +func (i inquiryDo) Where(conds ...gen.Condition) *inquiryDo { + return i.withDO(i.DO.Where(conds...)) +} + +func (i inquiryDo) Order(conds ...field.Expr) *inquiryDo { + return i.withDO(i.DO.Order(conds...)) +} + +func (i inquiryDo) Distinct(cols ...field.Expr) *inquiryDo { + return i.withDO(i.DO.Distinct(cols...)) +} + +func (i inquiryDo) Omit(cols ...field.Expr) *inquiryDo { + return i.withDO(i.DO.Omit(cols...)) +} + +func (i inquiryDo) Join(table schema.Tabler, on ...field.Expr) *inquiryDo { + return i.withDO(i.DO.Join(table, on...)) +} + +func (i inquiryDo) LeftJoin(table schema.Tabler, on ...field.Expr) *inquiryDo { + return i.withDO(i.DO.LeftJoin(table, on...)) +} + +func (i inquiryDo) RightJoin(table schema.Tabler, on ...field.Expr) *inquiryDo { + return i.withDO(i.DO.RightJoin(table, on...)) +} + +func (i inquiryDo) Group(cols ...field.Expr) *inquiryDo { + return i.withDO(i.DO.Group(cols...)) +} + +func (i inquiryDo) Having(conds ...gen.Condition) *inquiryDo { + return i.withDO(i.DO.Having(conds...)) +} + +func (i inquiryDo) Limit(limit int) *inquiryDo { + return i.withDO(i.DO.Limit(limit)) +} + +func (i inquiryDo) Offset(offset int) *inquiryDo { + return i.withDO(i.DO.Offset(offset)) +} + +func (i inquiryDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *inquiryDo { + return i.withDO(i.DO.Scopes(funcs...)) +} + +func (i inquiryDo) Unscoped() *inquiryDo { + return i.withDO(i.DO.Unscoped()) +} + +func (i inquiryDo) Create(values ...*models.Inquiry) error { + if len(values) == 0 { + return nil + } + return i.DO.Create(values) +} + +func (i inquiryDo) CreateInBatches(values []*models.Inquiry, batchSize int) error { + return i.DO.CreateInBatches(values, batchSize) +} + +// Save : !!! underlying implementation is different with GORM +// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values) +func (i inquiryDo) Save(values ...*models.Inquiry) error { + if len(values) == 0 { + return nil + } + return i.DO.Save(values) +} + +func (i inquiryDo) First() (*models.Inquiry, error) { + if result, err := i.DO.First(); err != nil { + return nil, err + } else { + return result.(*models.Inquiry), nil + } +} + +func (i inquiryDo) Take() (*models.Inquiry, error) { + if result, err := i.DO.Take(); err != nil { + return nil, err + } else { + return result.(*models.Inquiry), nil + } +} + +func (i inquiryDo) Last() (*models.Inquiry, error) { + if result, err := i.DO.Last(); err != nil { + return nil, err + } else { + return result.(*models.Inquiry), nil + } +} + +func (i inquiryDo) Find() ([]*models.Inquiry, error) { + result, err := i.DO.Find() + return result.([]*models.Inquiry), err +} + +func (i inquiryDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*models.Inquiry, err error) { + buf := make([]*models.Inquiry, 0, batchSize) + err = i.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error { + defer func() { results = append(results, buf...) }() + return fc(tx, batch) + }) + return results, err +} + +func (i inquiryDo) FindInBatches(result *[]*models.Inquiry, batchSize int, fc func(tx gen.Dao, batch int) error) error { + return i.DO.FindInBatches(result, batchSize, fc) +} + +func (i inquiryDo) Attrs(attrs ...field.AssignExpr) *inquiryDo { + return i.withDO(i.DO.Attrs(attrs...)) +} + +func (i inquiryDo) Assign(attrs ...field.AssignExpr) *inquiryDo { + return i.withDO(i.DO.Assign(attrs...)) +} + +func (i inquiryDo) Joins(fields ...field.RelationField) *inquiryDo { + for _, _f := range fields { + i = *i.withDO(i.DO.Joins(_f)) + } + return &i +} + +func (i inquiryDo) Preload(fields ...field.RelationField) *inquiryDo { + for _, _f := range fields { + i = *i.withDO(i.DO.Preload(_f)) + } + return &i +} + +func (i inquiryDo) FirstOrInit() (*models.Inquiry, error) { + if result, err := i.DO.FirstOrInit(); err != nil { + return nil, err + } else { + return result.(*models.Inquiry), nil + } +} + +func (i inquiryDo) FirstOrCreate() (*models.Inquiry, error) { + if result, err := i.DO.FirstOrCreate(); err != nil { + return nil, err + } else { + return result.(*models.Inquiry), nil + } +} + +func (i inquiryDo) FindByPage(offset int, limit int) (result []*models.Inquiry, count int64, err error) { + result, err = i.Offset(offset).Limit(limit).Find() + if err != nil { + return + } + + if size := len(result); 0 < limit && 0 < size && size < limit { + count = int64(size + offset) + return + } + + count, err = i.Offset(-1).Limit(-1).Count() + return +} + +func (i inquiryDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) { + count, err = i.Count() + if err != nil { + return + } + + err = i.Offset(offset).Limit(limit).Scan(result) + return +} + +func (i inquiryDo) Scan(result interface{}) (err error) { + return i.DO.Scan(result) +} + +func (i inquiryDo) Delete(models ...*models.Inquiry) (result gen.ResultInfo, err error) { + return i.DO.Delete(models) +} + +func (i *inquiryDo) withDO(do gen.Dao) *inquiryDo { + i.DO = *do.(*gen.DO) + return i +} diff --git a/web/routes.go b/web/routes.go index a46e452..ee8bed4 100644 --- a/web/routes.go +++ b/web/routes.go @@ -73,6 +73,10 @@ func ApplyRouters(app *fiber.App) { edge.Post("/assign", handlers.AssignEdge) edge.Post("/all", handlers.AllEdgesAvailable) + // 其他系统接口 + inquiry := api.Group("/inquiry") + inquiry.Post("/create", handlers.CreateInquiry) + // 回调 callbacks := app.Group("/callback") callbacks.Get("/identify", handlers.IdentifyCallbackNew) diff --git a/web/services/channel.go b/web/services/channel.go index 58208f7..2862abb 100644 --- a/web/services/channel.go +++ b/web/services/channel.go @@ -20,16 +20,16 @@ import ( // 通道服务 var Channel = &channelServer{ - provider: &channelBaiyinService{}, + provider: &channelBaiyinProvider{}, } -type ChanProviderAdapter interface { +type ChannelServiceProvider interface { CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter ...EdgeFilter) ([]*m.Channel, error) RemoveChannels(batch string) error } type channelServer struct { - provider ChanProviderAdapter + provider ChannelServiceProvider } func (s *channelServer) CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter ...EdgeFilter) ([]*m.Channel, error) { diff --git a/web/services/channel_baiyin.go b/web/services/channel_baiyin.go index beec589..af5a67d 100644 --- a/web/services/channel_baiyin.go +++ b/web/services/channel_baiyin.go @@ -21,9 +21,9 @@ import ( "gorm.io/gen/field" ) -type channelBaiyinService struct{} +type channelBaiyinProvider struct{} -func (s *channelBaiyinService) CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter ...EdgeFilter) ([]*m.Channel, error) { +func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter ...EdgeFilter) ([]*m.Channel, error) { var filter *EdgeFilter = nil if len(edgeFilter) > 0 { filter = &edgeFilter[0] @@ -256,7 +256,7 @@ func (s *channelBaiyinService) CreateChannels(source netip.Addr, resourceId int3 return channels, nil } -func (s *channelBaiyinService) RemoveChannels(batch string) error { +func (s *channelBaiyinProvider) RemoveChannels(batch string) error { start := time.Now() // 获取连接数据 diff --git a/web/services/resource.go b/web/services/resource.go index bf98ed0..2de231f 100644 --- a/web/services/resource.go +++ b/web/services/resource.go @@ -6,7 +6,6 @@ import ( "fmt" "platform/pkg/u" "platform/web/core" - g "platform/web/globals" m "platform/web/models" q "platform/web/queries" "time" @@ -19,64 +18,67 @@ var Resource = &resourceService{} type resourceService struct{} func (s *resourceService) CreateResourceByBalance(uid int32, now time.Time, data *CreateResourceData) error { - return g.Redsync.WithLock(userBalanceKey(uid), func() error { - return q.Q.Transaction(func(q *q.Query) error { - // 找到用户 - user, err := q.User. - Where(q.User.ID.Eq(uid)). - Take() - if err != nil { - return err - } - // 检查余额 - amount, err := data.GetAmount() - if err != nil { - return err - } - balance := user.Balance.Sub(amount) - if balance.IsNegative() { - return ErrBalanceNotEnough - } + // 找到用户 + user, err := q.User. + Where(q.User.ID.Eq(uid)). + Take() + if err != nil { + return err + } - // 更新用户余额 - _, err = q.User. - Where(q.User.ID.Eq(uid), q.User.Balance.Eq(user.Balance)). - UpdateSimple(q.User.Balance.Value(balance)) - if err != nil { - return core.NewServErr("更新用户余额失败", err) - } + // 检查余额 + amount, err := data.GetAmount() + if err != nil { + return err + } + newBalance := user.Balance.Sub(amount) + if newBalance.IsNegative() { + return ErrBalanceNotEnough + } - // 保存套餐 - resource, err := createResource(q, uid, now, data) - if err != nil { - return core.NewServErr("创建套餐失败", err) - } + return q.Q.Transaction(func(q *q.Query) error { - // 生成账单 - subject, err := data.GetSubject() - if err != nil { - return err - } - err = q.Bill.Create(newForConsume(uid, Bill.GenNo(), subject, amount, resource)) - if err != nil { - return core.NewServErr("生成账单失败", err) - } + // 更新用户余额 + _, err = q.User. + Where( + q.User.ID.Eq(uid), + q.User.Balance.Eq(user.Balance), + ). + UpdateSimple(q.User.Balance.Value(newBalance)) + if err != nil { + return core.NewServErr("更新用户余额失败", err) + } - return nil - }) + // 保存套餐 + resource, err := createResource(q, uid, now, data) + if err != nil { + return core.NewServErr("创建套餐失败", err) + } + + // 生成账单 + subject, err := data.GetSubject() + if err != nil { + return err + } + err = q.Bill.Create(newForConsume(uid, Bill.GenNo(), subject, amount, resource)) + if err != nil { + return core.NewServErr("生成账单失败", err) + } + + return nil }) } -func (s *resourceService) CreateResourceByTrade(uid int32, now time.Time, data *CreateResourceData, trade *m.Trade) error { +func (s *resourceService) CreateResourceByTrade(uid int32, now time.Time, data *CreateResourceData, trade *m.Trade) error { // 检查交易 + if trade == nil { + return core.NewBizErr("交易数据不能为空") + } + if trade.Status != m.TradeStatusSuccess { + return core.NewBizErr("交易状态不正确") + } + return q.Q.Transaction(func(q *q.Query) error { - // 检查交易 - if trade == nil { - return core.NewBizErr("交易数据不能为空") - } - if trade.Status != m.TradeStatusSuccess { - return core.NewBizErr("交易状态不正确") - } // 保存套餐 resource, err := createResource(q, uid, now, data) @@ -171,7 +173,7 @@ type CreateResourceData struct { type CreateShortResourceData struct { Live int32 `json:"live" validate:"required,min=180"` Mode m.ResourceMode `json:"mode" validate:"required"` - Quota int32 `json:"quota"` + Quota int32 `json:"quota" validate:"required"` Expire *int32 `json:"expire"` name string @@ -182,7 +184,7 @@ type CreateLongResourceData struct { Live int32 `json:"live" validate:"required"` Mode m.ResourceMode `json:"mode" validate:"required"` Quota int32 `json:"quota" validate:"required"` - Expire *int32 `json:"expire" validate:"required"` + Expire *int32 `json:"expire"` name string price *decimal.Decimal @@ -193,25 +195,25 @@ func (c *CreateResourceData) GetType() m.TradeType { } func (c *CreateResourceData) GetSubject() (string, error) { - switch c.Type { + switch { default: return "", errors.New("无效的套餐类型") - case m.ResourceTypeShort: + case c.Type == m.ResourceTypeShort && c.Short != nil: return c.Short.GetSubject() - case m.ResourceTypeLong: + case c.Type == m.ResourceTypeLong && c.Long != nil: return c.Long.GetSubject() } } func (c *CreateResourceData) GetAmount() (decimal.Decimal, error) { - switch c.Type { + switch { default: return decimal.Zero, errors.New("无效的套餐类型") - case m.ResourceTypeShort: + case c.Type == m.ResourceTypeShort && c.Short != nil: return c.Short.GetAmount() - case m.ResourceTypeLong: + case c.Type == m.ResourceTypeLong && c.Long != nil: return c.Long.GetAmount() } } @@ -339,6 +341,7 @@ func (data *CreateLongResourceData) GetAmount() (decimal.Decimal, error) { return *data.price, nil } +// 交易后创建套餐 type ResourceOnTradeComplete struct{} func (r ResourceOnTradeComplete) Check(t m.TradeType) (ProductInfo, bool) { @@ -352,6 +355,7 @@ func (r ResourceOnTradeComplete) OnTradeComplete(info ProductInfo, trade *m.Trad return Resource.CreateResourceByTrade(trade.UserID, time.Time(*trade.CompletedAt), info.(*CreateResourceData), trade) } +// 服务错误 type ResourceServiceErr string func (e ResourceServiceErr) Error() string {