实现用户咨询数据收集接口

This commit is contained in:
2025-12-18 14:22:56 +08:00
parent 0207720943
commit 8f2e71849f
14 changed files with 606 additions and 121 deletions

16
.vscode/launch.json vendored
View File

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

13
.zed/debug.json Normal file
View File

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

View File

@@ -1,19 +1,15 @@
## TODO ## TODO
价格与优惠
优化中间件,配置通用限速 优化中间件,配置通用限速
trade/create 性能问题,缩短事务时间,考虑其他方式实现可靠分布式事务 observe 部署,蓝狐部署
jsonb 类型转换问题,考虑一个高效的 any 到 struct 转换工具
端口资源池的 gc 实现
channel 服务代码结构,用 provider 代替整个 service 的复用
用反射实现环境变量解析,以简化函数签名
--- ---
用反射实现环境变量解析,以简化函数签名
分离 task 的客户端支持多进程prefork 必要!) 分离 task 的客户端支持多进程prefork 必要!)
调整目录结构: 调整目录结构:
@@ -39,6 +35,10 @@ http 调用 clients 的初始化函数
--- ---
数据库转模型文件
jsonb 类型转换问题,考虑一个高效的 any 到 struct 转换工具
慢速请求底层调用埋点监控 慢速请求底层调用埋点监控
- redis - redis
- gorm - gorm

View File

@@ -61,6 +61,7 @@ func main() {
m.User{}, m.User{},
m.UserRole{}, m.UserRole{},
m.Whitelist{}, m.Whitelist{},
m.Inquiry{},
) )
g.Execute() g.Execute()
} }

View File

@@ -108,6 +108,76 @@ comment on column logs_user_bandwidth.time is '记录时间';
-- endregion -- 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 管理员信息 -- 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.updated_at is '更新时间';
comment on column admin_role.deleted_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 -- endregion
-- ==================== -- ====================

48
web/handlers/inquiry.go Normal file
View File

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

View File

@@ -439,7 +439,7 @@ func ResourcePrice(c *fiber.Ctx) error {
// 解析请求参数 // 解析请求参数
var req = new(CreateResourceReq) var req = new(CreateResourceReq)
if err := g.Validator.ParseBody(c, req); err != nil { if err := g.Validator.ParseBody(c, req); err != nil {
return err return core.NewBizErr("接口参数解析异常", err)
} }
// 获取套餐价格 // 获取套餐价格

25
web/models/inquiry.go Normal file
View File

@@ -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 // 已处理
)

View File

@@ -25,6 +25,7 @@ var (
Client *client Client *client
Coupon *coupon Coupon *coupon
Edge *edge Edge *edge
Inquiry *inquiry
LinkAdminRole *linkAdminRole LinkAdminRole *linkAdminRole
LinkAdminRolePermission *linkAdminRolePermission LinkAdminRolePermission *linkAdminRolePermission
LinkClientPermission *linkClientPermission LinkClientPermission *linkClientPermission
@@ -58,6 +59,7 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
Client = &Q.Client Client = &Q.Client
Coupon = &Q.Coupon Coupon = &Q.Coupon
Edge = &Q.Edge Edge = &Q.Edge
Inquiry = &Q.Inquiry
LinkAdminRole = &Q.LinkAdminRole LinkAdminRole = &Q.LinkAdminRole
LinkAdminRolePermission = &Q.LinkAdminRolePermission LinkAdminRolePermission = &Q.LinkAdminRolePermission
LinkClientPermission = &Q.LinkClientPermission LinkClientPermission = &Q.LinkClientPermission
@@ -92,6 +94,7 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
Client: newClient(db, opts...), Client: newClient(db, opts...),
Coupon: newCoupon(db, opts...), Coupon: newCoupon(db, opts...),
Edge: newEdge(db, opts...), Edge: newEdge(db, opts...),
Inquiry: newInquiry(db, opts...),
LinkAdminRole: newLinkAdminRole(db, opts...), LinkAdminRole: newLinkAdminRole(db, opts...),
LinkAdminRolePermission: newLinkAdminRolePermission(db, opts...), LinkAdminRolePermission: newLinkAdminRolePermission(db, opts...),
LinkClientPermission: newLinkClientPermission(db, opts...), LinkClientPermission: newLinkClientPermission(db, opts...),
@@ -127,6 +130,7 @@ type Query struct {
Client client Client client
Coupon coupon Coupon coupon
Edge edge Edge edge
Inquiry inquiry
LinkAdminRole linkAdminRole LinkAdminRole linkAdminRole
LinkAdminRolePermission linkAdminRolePermission LinkAdminRolePermission linkAdminRolePermission
LinkClientPermission linkClientPermission LinkClientPermission linkClientPermission
@@ -163,6 +167,7 @@ func (q *Query) clone(db *gorm.DB) *Query {
Client: q.Client.clone(db), Client: q.Client.clone(db),
Coupon: q.Coupon.clone(db), Coupon: q.Coupon.clone(db),
Edge: q.Edge.clone(db), Edge: q.Edge.clone(db),
Inquiry: q.Inquiry.clone(db),
LinkAdminRole: q.LinkAdminRole.clone(db), LinkAdminRole: q.LinkAdminRole.clone(db),
LinkAdminRolePermission: q.LinkAdminRolePermission.clone(db), LinkAdminRolePermission: q.LinkAdminRolePermission.clone(db),
LinkClientPermission: q.LinkClientPermission.clone(db), LinkClientPermission: q.LinkClientPermission.clone(db),
@@ -206,6 +211,7 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
Client: q.Client.replaceDB(db), Client: q.Client.replaceDB(db),
Coupon: q.Coupon.replaceDB(db), Coupon: q.Coupon.replaceDB(db),
Edge: q.Edge.replaceDB(db), Edge: q.Edge.replaceDB(db),
Inquiry: q.Inquiry.replaceDB(db),
LinkAdminRole: q.LinkAdminRole.replaceDB(db), LinkAdminRole: q.LinkAdminRole.replaceDB(db),
LinkAdminRolePermission: q.LinkAdminRolePermission.replaceDB(db), LinkAdminRolePermission: q.LinkAdminRolePermission.replaceDB(db),
LinkClientPermission: q.LinkClientPermission.replaceDB(db), LinkClientPermission: q.LinkClientPermission.replaceDB(db),
@@ -239,6 +245,7 @@ type queryCtx struct {
Client *clientDo Client *clientDo
Coupon *couponDo Coupon *couponDo
Edge *edgeDo Edge *edgeDo
Inquiry *inquiryDo
LinkAdminRole *linkAdminRoleDo LinkAdminRole *linkAdminRoleDo
LinkAdminRolePermission *linkAdminRolePermissionDo LinkAdminRolePermission *linkAdminRolePermissionDo
LinkClientPermission *linkClientPermissionDo LinkClientPermission *linkClientPermissionDo
@@ -272,6 +279,7 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
Client: q.Client.WithContext(ctx), Client: q.Client.WithContext(ctx),
Coupon: q.Coupon.WithContext(ctx), Coupon: q.Coupon.WithContext(ctx),
Edge: q.Edge.WithContext(ctx), Edge: q.Edge.WithContext(ctx),
Inquiry: q.Inquiry.WithContext(ctx),
LinkAdminRole: q.LinkAdminRole.WithContext(ctx), LinkAdminRole: q.LinkAdminRole.WithContext(ctx),
LinkAdminRolePermission: q.LinkAdminRolePermission.WithContext(ctx), LinkAdminRolePermission: q.LinkAdminRolePermission.WithContext(ctx),
LinkClientPermission: q.LinkClientPermission.WithContext(ctx), LinkClientPermission: q.LinkClientPermission.WithContext(ctx),

359
web/queries/inquiry.gen.go Normal file
View File

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

View File

@@ -73,6 +73,10 @@ func ApplyRouters(app *fiber.App) {
edge.Post("/assign", handlers.AssignEdge) edge.Post("/assign", handlers.AssignEdge)
edge.Post("/all", handlers.AllEdgesAvailable) edge.Post("/all", handlers.AllEdgesAvailable)
// 其他系统接口
inquiry := api.Group("/inquiry")
inquiry.Post("/create", handlers.CreateInquiry)
// 回调 // 回调
callbacks := app.Group("/callback") callbacks := app.Group("/callback")
callbacks.Get("/identify", handlers.IdentifyCallbackNew) callbacks.Get("/identify", handlers.IdentifyCallbackNew)

View File

@@ -20,16 +20,16 @@ import (
// 通道服务 // 通道服务
var Channel = &channelServer{ 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) CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter ...EdgeFilter) ([]*m.Channel, error)
RemoveChannels(batch string) error RemoveChannels(batch string) error
} }
type channelServer struct { 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) { func (s *channelServer) CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter ...EdgeFilter) ([]*m.Channel, error) {

View File

@@ -21,9 +21,9 @@ import (
"gorm.io/gen/field" "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 var filter *EdgeFilter = nil
if len(edgeFilter) > 0 { if len(edgeFilter) > 0 {
filter = &edgeFilter[0] filter = &edgeFilter[0]
@@ -256,7 +256,7 @@ func (s *channelBaiyinService) CreateChannels(source netip.Addr, resourceId int3
return channels, nil return channels, nil
} }
func (s *channelBaiyinService) RemoveChannels(batch string) error { func (s *channelBaiyinProvider) RemoveChannels(batch string) error {
start := time.Now() start := time.Now()
// 获取连接数据 // 获取连接数据

View File

@@ -6,7 +6,6 @@ import (
"fmt" "fmt"
"platform/pkg/u" "platform/pkg/u"
"platform/web/core" "platform/web/core"
g "platform/web/globals"
m "platform/web/models" m "platform/web/models"
q "platform/web/queries" q "platform/web/queries"
"time" "time"
@@ -19,64 +18,67 @@ var Resource = &resourceService{}
type resourceService struct{} type resourceService struct{}
func (s *resourceService) CreateResourceByBalance(uid int32, now time.Time, data *CreateResourceData) error { 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() user, err := q.User.
if err != nil { Where(q.User.ID.Eq(uid)).
return err Take()
} if err != nil {
balance := user.Balance.Sub(amount) return err
if balance.IsNegative() { }
return ErrBalanceNotEnough
}
// 更新用户余额 // 检查余额
_, err = q.User. amount, err := data.GetAmount()
Where(q.User.ID.Eq(uid), q.User.Balance.Eq(user.Balance)). if err != nil {
UpdateSimple(q.User.Balance.Value(balance)) return err
if err != nil { }
return core.NewServErr("更新用户余额失败", err) newBalance := user.Balance.Sub(amount)
} if newBalance.IsNegative() {
return ErrBalanceNotEnough
}
// 保存套餐 return q.Q.Transaction(func(q *q.Query) error {
resource, err := createResource(q, uid, now, data)
if err != nil {
return core.NewServErr("创建套餐失败", err)
}
// 生成账单 // 更新用户余额
subject, err := data.GetSubject() _, err = q.User.
if err != nil { Where(
return err q.User.ID.Eq(uid),
} q.User.Balance.Eq(user.Balance),
err = q.Bill.Create(newForConsume(uid, Bill.GenNo(), subject, amount, resource)) ).
if err != nil { UpdateSimple(q.User.Balance.Value(newBalance))
return core.NewServErr("生成账单失败", err) 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 { 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) resource, err := createResource(q, uid, now, data)
@@ -171,7 +173,7 @@ type CreateResourceData struct {
type CreateShortResourceData struct { type CreateShortResourceData struct {
Live int32 `json:"live" validate:"required,min=180"` Live int32 `json:"live" validate:"required,min=180"`
Mode m.ResourceMode `json:"mode" validate:"required"` Mode m.ResourceMode `json:"mode" validate:"required"`
Quota int32 `json:"quota"` Quota int32 `json:"quota" validate:"required"`
Expire *int32 `json:"expire"` Expire *int32 `json:"expire"`
name string name string
@@ -182,7 +184,7 @@ type CreateLongResourceData struct {
Live int32 `json:"live" validate:"required"` Live int32 `json:"live" validate:"required"`
Mode m.ResourceMode `json:"mode" validate:"required"` Mode m.ResourceMode `json:"mode" validate:"required"`
Quota int32 `json:"quota" validate:"required"` Quota int32 `json:"quota" validate:"required"`
Expire *int32 `json:"expire" validate:"required"` Expire *int32 `json:"expire"`
name string name string
price *decimal.Decimal price *decimal.Decimal
@@ -193,25 +195,25 @@ func (c *CreateResourceData) GetType() m.TradeType {
} }
func (c *CreateResourceData) GetSubject() (string, error) { func (c *CreateResourceData) GetSubject() (string, error) {
switch c.Type { switch {
default: default:
return "", errors.New("无效的套餐类型") return "", errors.New("无效的套餐类型")
case m.ResourceTypeShort: case c.Type == m.ResourceTypeShort && c.Short != nil:
return c.Short.GetSubject() return c.Short.GetSubject()
case m.ResourceTypeLong: case c.Type == m.ResourceTypeLong && c.Long != nil:
return c.Long.GetSubject() return c.Long.GetSubject()
} }
} }
func (c *CreateResourceData) GetAmount() (decimal.Decimal, error) { func (c *CreateResourceData) GetAmount() (decimal.Decimal, error) {
switch c.Type { switch {
default: default:
return decimal.Zero, errors.New("无效的套餐类型") return decimal.Zero, errors.New("无效的套餐类型")
case m.ResourceTypeShort: case c.Type == m.ResourceTypeShort && c.Short != nil:
return c.Short.GetAmount() return c.Short.GetAmount()
case m.ResourceTypeLong: case c.Type == m.ResourceTypeLong && c.Long != nil:
return c.Long.GetAmount() return c.Long.GetAmount()
} }
} }
@@ -339,6 +341,7 @@ func (data *CreateLongResourceData) GetAmount() (decimal.Decimal, error) {
return *data.price, nil return *data.price, nil
} }
// 交易后创建套餐
type ResourceOnTradeComplete struct{} type ResourceOnTradeComplete struct{}
func (r ResourceOnTradeComplete) Check(t m.TradeType) (ProductInfo, bool) { 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) return Resource.CreateResourceByTrade(trade.UserID, time.Time(*trade.CompletedAt), info.(*CreateResourceData), trade)
} }
// 服务错误
type ResourceServiceErr string type ResourceServiceErr string
func (e ResourceServiceErr) Error() string { func (e ResourceServiceErr) Error() string {