实现余额购买接口 & 实现全局 id 生成器

This commit is contained in:
2025-04-08 17:15:23 +08:00
parent c02d843dbc
commit 4c47a71f30
10 changed files with 506 additions and 116 deletions

View File

@@ -11,7 +11,7 @@
- [ ] 充值余额
- [ ] 选择套餐
- [X] 提取 IP
- [ ] 长效提取
- [ ] 长效提取
- [ ] 连接
中间件:
@@ -25,9 +25,10 @@
业务代码和测试代码共用的控制变量可以优化为环境变量
channel 优化:
- 重新梳理逻辑流程,简化循环
- 端口分配时加锁
- 数据存入顺序,数据库 > 缓存 > 外部接口
- 重新梳理逻辑流程,简化循环
- 端口分配时加锁
- 数据存入顺序,数据库 > 缓存 > 外部接口
remote 令牌问题
@@ -52,6 +53,7 @@ oauth token 验证授权范围
在 init/env 中有定义和默认值
开发环境数据库迁移:
```powershell
pg-schema-diff apply --schema-dir .\scripts\sql --dsn "host=localhost user=test password=test dbname=app port=5432 sslmode=disable TimeZone=Asia/Shanghai"
```
@@ -66,6 +68,13 @@ pg-schema-diff apply --schema-dir .\scripts\sql --dsn "host=localhost user=test
| proxy/shared-rotate | psr | 隧道代理 |
| proxy/private-static | pps | 独享代理 |
### 订单类型
| 枚举 | 说明 |
|----|------|
| 1 | 充值余额 |
| 2 | 直接购买 |
### 外部服务
服务器ip 110.40.82.248

View File

@@ -1678,10 +1678,10 @@
{
"id": "UFwaLWIsE6V1u0wBoIki2",
"type": "arrow",
"x": 1550.476799787434,
"y": 700.4507453599007,
"width": 199.2013303179067,
"height": 199.09850928019864,
"x": 1580,
"y": 700,
"width": 140,
"height": 200,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
@@ -1697,11 +1697,11 @@
"type": 2
},
"seed": 700454335,
"version": 1345,
"versionNonce": 1843048196,
"version": 1367,
"versionNonce": 321661628,
"isDeleted": false,
"boundElements": [],
"updated": 1742973056826,
"updated": 1742973231871,
"link": null,
"locked": false,
"points": [
@@ -1710,19 +1710,19 @@
0
],
[
199.2013303179067,
199.09850928019864
140,
200
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "U3ry809DLSs3z2kp6IH9m",
"focus": 0,
"focus": -0.3333333333333334,
"gap": 1
},
"endBinding": {
"elementId": "02tdc6VRdsQxJlfDZOFSa",
"focus": 4.547473508864641e-15,
"focus": -0.3333333333333326,
"gap": 1
},
"startArrowhead": "crowfoot_one",
@@ -1786,8 +1786,8 @@
{
"id": "qFfFxWmqoOyrikrAZnpf6",
"type": "arrow",
"x": 1300.0990604598142,
"y": 399.5491556175755,
"x": 1300.0990604598144,
"y": 399.09841025767486,
"width": 0.19812091962853629,
"height": 99.09831123515096,
"angle": 0.0019992334198262185,
@@ -1805,11 +1805,11 @@
"type": 2
},
"seed": 557170239,
"version": 500,
"versionNonce": 1905164420,
"version": 504,
"versionNonce": 703426753,
"isDeleted": false,
"boundElements": [],
"updated": 1742973044311,
"updated": 1744102055508,
"link": null,
"locked": false,
"points": [
@@ -1825,15 +1825,15 @@
"lastCommittedPoint": null,
"startBinding": {
"elementId": "Bto4ODTDgZzQWKJot-An0",
"focus": 0,
"focus": 4.547473508864641e-15,
"gap": 1
},
"endBinding": {
"elementId": "1jrJPZuuyVEe_3htPZakP",
"focus": 4.547473508864641e-15,
"focus": 2.2737367544323206e-15,
"gap": 1
},
"startArrowhead": "crowfoot_one",
"startArrowhead": null,
"endArrowhead": "crowfoot_many",
"elbowed": false
},
@@ -2528,11 +2528,11 @@
"index": "b2G",
"roundness": null,
"seed": 220752260,
"version": 71,
"versionNonce": 1899879740,
"version": 84,
"versionNonce": 1844124804,
"isDeleted": false,
"boundElements": null,
"updated": 1742973149462,
"boundElements": [],
"updated": 1742973179481,
"link": null,
"locked": false,
"text": "分配\nassign",
@@ -2548,10 +2548,10 @@
{
"id": "R_-u_XvFXVoOYZdQkVqEP",
"type": "arrow",
"x": 1500.8926525754896,
"y": 973.3353179908356,
"width": 201.09954604136533,
"height": 137.1139381444691,
"x": 1500,
"y": 980,
"width": 201.99219861685492,
"height": 130.44925613530472,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
@@ -2567,11 +2567,11 @@
"type": 2
},
"seed": 1509277188,
"version": 74,
"versionNonce": 1128401540,
"version": 79,
"versionNonce": 1522980412,
"isDeleted": false,
"boundElements": null,
"updated": 1742973114690,
"boundElements": [],
"updated": 1742973247275,
"link": null,
"locked": false,
"points": [
@@ -2580,20 +2580,20 @@
0
],
[
201.09954604136533,
137.1139381444691
201.99219861685492,
130.44925613530472
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "sWVhyKNTaf6c4MTpx5iEK",
"focus": -0.38461538461538525,
"focus": -0.3018059982941868,
"gap": 1
},
"endBinding": {
"elementId": "P5uYFnh0fSXvOjdD6id2Z",
"focus": -0.23076923076923048,
"gap": 1
"focus": -0.20722399317674445,
"gap": 1.0001133441418582
},
"startArrowhead": "crowfoot_one",
"endArrowhead": "crowfoot_many",
@@ -2624,7 +2624,7 @@
"version": 152,
"versionNonce": 257905468,
"isDeleted": false,
"boundElements": null,
"boundElements": [],
"updated": 1742973125284,
"link": null,
"locked": false,

View File

@@ -496,27 +496,27 @@ comment on column whitelist.deleted_at is '删除时间';
drop table if exists channel cascade;
create table channel (
id serial primary key,
user_id int not null references "user" (id)
user_id int not null references "user" (id)
on update cascade
on delete cascade,
proxy_id int not null references proxy (id) --
on update cascade --
proxy_id int not null references proxy (id) --
on update cascade --
on delete set null,
node_id int references node (id) --
on update cascade --
on delete set null,
node_id int references node (id) --
on update cascade --
on delete set null,
proxy_host varchar(255) not null default '',
proxy_port int not null,
proxy_port int not null,
node_host varchar(255),
protocol varchar(255),
auth_ip bool not null default false,
auth_ip bool not null default false,
user_host varchar(255),
auth_pass bool not null default false,
auth_pass bool not null default false,
username varchar(255) unique,
password varchar(255),
expiration timestamp not null,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
expiration timestamp not null,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
deleted_at timestamp
);
create index channel_user_id_index on channel (user_id);
@@ -718,7 +718,7 @@ create table trade (
remark varchar(255),
amount decimal(12, 2) not null default 0,
payment decimal(12, 2) not null default 0,
method int not null default 0,
method int not null,
status int not null default 0,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
@@ -747,27 +747,29 @@ comment on column trade.deleted_at is '删除时间';
-- bill
drop table if exists bill cascade;
create table bill (
id serial primary key,
trade_id int not null references trade (id)
id serial primary key,
user_id int not null references "user" (id)
on update cascade
on delete cascade,
user_id int not null references "user" (id)
on update cascade
on delete cascade,
product_id int references product (id) --
on update cascade --
on delete set null,
info varchar(255),
count int default 0,
price decimal(12, 2) not null default 0,
amount decimal(12, 2) not null default 0,
payment decimal(12, 2) not null default 0,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
deleted_at timestamp
trade_id int references trade (id) --
on update cascade --
on delete set null,
resource_id int references resource (id) --
on update cascade --
on delete set null,
bill_no varchar(255) not null unique,
type int not null,
info varchar(255),
amount decimal(12, 2) not null default 0,
payment decimal(12, 2) not null default 0,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
deleted_at timestamp
);
create index bill_user_id_index on bill (user_id);
create index bill_trade_id_index on bill (trade_id);
create index bill_product_id_index on bill (product_id);
create index bill_resource_id_index on bill (resource_id);
create index bill_type_index on bill (type);
create index bill_deleted_at_index on bill (deleted_at);
-- bill表字段注释
@@ -775,10 +777,10 @@ comment on table bill is '账单表';
comment on column bill.id is '账单ID';
comment on column bill.trade_id is '订单ID';
comment on column bill.user_id is '用户ID';
comment on column bill.product_id is '产品ID';
comment on column bill.resource_id is '套餐ID';
comment on column bill.bill_no is '易读账单号';
comment on column bill.info is '产品可读信息';
comment on column bill.count is '购买数量';
comment on column bill.price is '单价';
comment on column bill.type is '账单类型0-充值1-消费2-退款';
comment on column bill.amount is '总金额';
comment on column bill.payment is '支付金额';
comment on column bill.created_at is '创建时间';

140
web/handlers/resource.go Normal file
View File

@@ -0,0 +1,140 @@
package handlers
import (
"errors"
"platform/web/auth"
m "platform/web/models"
q "platform/web/queries"
"platform/web/services"
"time"
"github.com/gofiber/fiber/v2"
)
// region CreateResourceByBalance
type CreateResourceByBalanceReq struct {
Type int32 `json:"type" validate:"required"`
Live int32 `json:"live" validate:"required"`
Expire int32 `json:"expire" validate:"required"`
Quota int32 `json:"quota" validate:"required"`
DailyLimit int32 `json:"daily_limit" validate:"required"`
}
// CreateResourceByBalance 通过余额创建资源
func CreateResourceByBalance(c *fiber.Ctx) error {
// 检查权限
authContext, err := auth.Protect(c, []services.PayloadType{services.PayloadUser}, []string{})
if err != nil {
return err
}
// 解析请求参数
req := new(CreateResourceByBalanceReq)
if err := c.BodyParser(req); err != nil {
return err
}
err = q.Q.Transaction(func(q *q.Query) error {
// 检查用户
user, err := q.User.Where(q.User.ID.Eq(authContext.Payload.Id)).Take()
if err != nil {
return err
}
// 计算价格
var amount = 0
var payment = 0
// 检查余额
if user.Balance < float64(req.Quota)/100 {
return errors.New("余额不足")
}
// 创建资源
resource := m.Resource{
UserID: authContext.Payload.Id,
}
err = q.Resource.Save(&resource)
if err != nil {
return err
}
resourcePss := m.ResourcePss{
ResourceID: resource.ID,
Type: req.Type,
Live: req.Live,
Quota: req.Quota,
Expire: time.Now().Add(time.Duration(req.Expire) * time.Second),
DailyLimit: req.DailyLimit,
}
err = q.ResourcePss.Save(&resourcePss)
if err != nil {
return err
}
// 更新用户余额
user.Balance -= float64(payment)
_, err = q.User.
Where(q.User.ID.Eq(authContext.Payload.Id)).
Update(q.User.Balance, user.Balance)
if err != nil {
return err
}
// 生成账单
bill := m.Bill{
UserID: authContext.Payload.Id,
ResourceID: resource.ID,
BillNo: services.ID.GenReadable("bil"),
Type: 1,
Info: "购买套餐",
Amount: float64(amount),
Payment: float64(payment),
}
err = q.Bill.Save(&bill)
if err != nil {
return err
}
return nil
})
if err != nil {
return err
}
return errors.New("not implemented")
}
// endregion
// region CreateResourceByAlipayCallback
type CreateResourceByAlipayCallbackReq struct {
}
// CreateResourceByAlipayCallback 支付宝支付回调
func CreateResourceByAlipayCallback(c *fiber.Ctx) error {
// 根据支付类型执行不同流程:
// 1. 支付宝或微信(即时支付)
// - 更新订单状态
// - 生成账单
// - 生成资源
return errors.New("not implemented")
}
// endregion
// region CreateResourceByWechatCallback
type CreateResourceByWechatCallbackReq struct {
}
// CreateResourceByWechatCallback 微信支付回调
func CreateResourceByWechatCallback(c *fiber.Ctx) error {
return errors.New("not implemented")
}
// endregion

61
web/handlers/trade.go Normal file
View File

@@ -0,0 +1,61 @@
package handlers
import (
"platform/web/auth"
m "platform/web/models"
q "platform/web/queries"
"platform/web/services"
"strconv"
"github.com/gofiber/fiber/v2"
)
// region CreateTrade
type CreateTradeReq struct {
Subject string `json:"subject" validate:"required"`
Remark string `json:"remark"`
Amount int `json:"amount" validate:"required"`
Method int `json:"method" validate:"required"` // 支付方式1.支付宝2.微信
}
func CreateTrade(c *fiber.Ctx) error {
// 检查权限
authContext, err := auth.Protect(c, []services.PayloadType{services.PayloadUser}, []string{})
if err != nil {
return err
}
// 解析请求参数
req := new(CreateTradeReq)
if err := c.BodyParser(req); err != nil {
return err
}
// 创建交易订单
num, err := services.ID.GenSerial(c.Context())
if err != nil {
return err
}
var trade = m.Trade{
UserID: authContext.Payload.Id,
InnerNo: strconv.FormatUint(num, 10),
Subject: req.Subject,
Remark: req.Remark,
Amount: float64(req.Amount) / 100,
Method: int32(req.Method),
}
// 调用外部接口
// 保存交易订单
err = q.Trade.Create(&trade)
if err != nil {
return err
}
// 返回结果,外部支付链接
return nil
}
// endregion

View File

@@ -14,18 +14,18 @@ const TableNameBill = "bill"
// Bill mapped from table <bill>
type Bill struct {
ID int32 `gorm:"column:id;primaryKey;autoIncrement:true;comment:账单ID" json:"id"` // 账单ID
OrderID int32 `gorm:"column:order_id;not null;comment:订单ID" json:"order_id"` // 订单ID
UserID int32 `gorm:"column:user_id;not null;comment:用户ID" json:"user_id"` // 用户ID
ProductID int32 `gorm:"column:product_id;comment:产品ID" json:"product_id"` // 产品ID
Info string `gorm:"column:info;comment:产品可读信息" json:"info"` // 产品可读信息
Count_ int32 `gorm:"column:count;comment:购买数量" json:"count"` // 购买数量
Price float64 `gorm:"column:price;not null;comment:单价" json:"price"` // 单价
Amount float64 `gorm:"column:amount;not null;comment:总金额" json:"amount"` // 总金额
Payment float64 `gorm:"column:payment;not null;comment:支付金额" json:"payment"` // 支付金额
CreatedAt time.Time `gorm:"column:created_at;default:CURRENT_TIMESTAMP;comment:创建时间" json:"created_at"` // 创建时间
UpdatedAt time.Time `gorm:"column:updated_at;default:CURRENT_TIMESTAMP;comment:更新时间" json:"updated_at"` // 更新时间
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;comment:删除时间" json:"deleted_at"` // 删除时间
ID int32 `gorm:"column:id;primaryKey;autoIncrement:true;comment:账单ID" json:"id"` // 账单ID
UserID int32 `gorm:"column:user_id;not null;comment:用户ID" json:"user_id"` // 用户ID
Info string `gorm:"column:info;comment:产品可读信息" json:"info"` // 产品可读信息
Amount float64 `gorm:"column:amount;not null;comment:总金额" json:"amount"` // 总金额
Payment float64 `gorm:"column:payment;not null;comment:支付金额" json:"payment"` // 支付金额
CreatedAt time.Time `gorm:"column:created_at;default:CURRENT_TIMESTAMP;comment:创建时间" json:"created_at"` // 创建时间
UpdatedAt time.Time `gorm:"column:updated_at;default:CURRENT_TIMESTAMP;comment:更新时间" json:"updated_at"` // 更新时间
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;comment:删除时间" json:"deleted_at"` // 删除时间
TradeID int32 `gorm:"column:trade_id" json:"trade_id"`
ResourceID int32 `gorm:"column:resource_id" json:"resource_id"`
Type int32 `gorm:"column:type;not null" json:"type"`
BillNo string `gorm:"column:bill_no;not null" json:"bill_no"`
}
// TableName Bill's table name

View File

@@ -15,12 +15,12 @@ const TableNameRefund = "refund"
// Refund mapped from table <refund>
type Refund struct {
ID int32 `gorm:"column:id;primaryKey;autoIncrement:true;comment:退款ID" json:"id"` // 退款ID
OrderID int32 `gorm:"column:order_id;not null;comment:订单ID" json:"order_id"` // 订单ID
ProductID int32 `gorm:"column:product_id;comment:产品ID" json:"product_id"` // 产品ID
Amount float64 `gorm:"column:amount;not null;comment:退款金额" json:"amount"` // 退款金额
CreatedAt time.Time `gorm:"column:created_at;default:CURRENT_TIMESTAMP;comment:创建时间" json:"created_at"` // 创建时间
UpdatedAt time.Time `gorm:"column:updated_at;default:CURRENT_TIMESTAMP;comment:更新时间" json:"updated_at"` // 更新时间
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;comment:删除时间" json:"deleted_at"` // 删除时间
TradeID int32 `gorm:"column:trade_id;not null" json:"trade_id"`
}
// TableName Refund's table name

View File

@@ -28,17 +28,17 @@ func newBill(db *gorm.DB, opts ...gen.DOOption) bill {
tableName := _bill.billDo.TableName()
_bill.ALL = field.NewAsterisk(tableName)
_bill.ID = field.NewInt32(tableName, "id")
_bill.OrderID = field.NewInt32(tableName, "order_id")
_bill.UserID = field.NewInt32(tableName, "user_id")
_bill.ProductID = field.NewInt32(tableName, "product_id")
_bill.Info = field.NewString(tableName, "info")
_bill.Count_ = field.NewInt32(tableName, "count")
_bill.Price = field.NewFloat64(tableName, "price")
_bill.Amount = field.NewFloat64(tableName, "amount")
_bill.Payment = field.NewFloat64(tableName, "payment")
_bill.CreatedAt = field.NewTime(tableName, "created_at")
_bill.UpdatedAt = field.NewTime(tableName, "updated_at")
_bill.DeletedAt = field.NewField(tableName, "deleted_at")
_bill.TradeID = field.NewInt32(tableName, "trade_id")
_bill.ResourceID = field.NewInt32(tableName, "resource_id")
_bill.Type = field.NewInt32(tableName, "type")
_bill.BillNo = field.NewString(tableName, "bill_no")
_bill.fillFieldMap()
@@ -48,19 +48,19 @@ func newBill(db *gorm.DB, opts ...gen.DOOption) bill {
type bill struct {
billDo
ALL field.Asterisk
ID field.Int32 // 账单ID
OrderID field.Int32 // 订单ID
UserID field.Int32 // 用户ID
ProductID field.Int32 // 产品ID
Info field.String // 产品可读信息
Count_ field.Int32 // 购买数量
Price field.Float64 // 单价
Amount field.Float64 // 总金额
Payment field.Float64 // 支付金额
CreatedAt field.Time // 创建时间
UpdatedAt field.Time // 更新时间
DeletedAt field.Field // 删除时间
ALL field.Asterisk
ID field.Int32 // 账单ID
UserID field.Int32 // 用户ID
Info field.String // 产品可读信息
Amount field.Float64 // 总金额
Payment field.Float64 // 支付金额
CreatedAt field.Time // 创建时间
UpdatedAt field.Time // 更新时间
DeletedAt field.Field // 删除时间
TradeID field.Int32
ResourceID field.Int32
Type field.Int32
BillNo field.String
fieldMap map[string]field.Expr
}
@@ -78,17 +78,17 @@ func (b bill) As(alias string) *bill {
func (b *bill) updateTableName(table string) *bill {
b.ALL = field.NewAsterisk(table)
b.ID = field.NewInt32(table, "id")
b.OrderID = field.NewInt32(table, "order_id")
b.UserID = field.NewInt32(table, "user_id")
b.ProductID = field.NewInt32(table, "product_id")
b.Info = field.NewString(table, "info")
b.Count_ = field.NewInt32(table, "count")
b.Price = field.NewFloat64(table, "price")
b.Amount = field.NewFloat64(table, "amount")
b.Payment = field.NewFloat64(table, "payment")
b.CreatedAt = field.NewTime(table, "created_at")
b.UpdatedAt = field.NewTime(table, "updated_at")
b.DeletedAt = field.NewField(table, "deleted_at")
b.TradeID = field.NewInt32(table, "trade_id")
b.ResourceID = field.NewInt32(table, "resource_id")
b.Type = field.NewInt32(table, "type")
b.BillNo = field.NewString(table, "bill_no")
b.fillFieldMap()
@@ -107,17 +107,17 @@ func (b *bill) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
func (b *bill) fillFieldMap() {
b.fieldMap = make(map[string]field.Expr, 12)
b.fieldMap["id"] = b.ID
b.fieldMap["order_id"] = b.OrderID
b.fieldMap["user_id"] = b.UserID
b.fieldMap["product_id"] = b.ProductID
b.fieldMap["info"] = b.Info
b.fieldMap["count"] = b.Count_
b.fieldMap["price"] = b.Price
b.fieldMap["amount"] = b.Amount
b.fieldMap["payment"] = b.Payment
b.fieldMap["created_at"] = b.CreatedAt
b.fieldMap["updated_at"] = b.UpdatedAt
b.fieldMap["deleted_at"] = b.DeletedAt
b.fieldMap["trade_id"] = b.TradeID
b.fieldMap["resource_id"] = b.ResourceID
b.fieldMap["type"] = b.Type
b.fieldMap["bill_no"] = b.BillNo
}
func (b bill) clone(db *gorm.DB) bill {

View File

@@ -28,12 +28,12 @@ func newRefund(db *gorm.DB, opts ...gen.DOOption) refund {
tableName := _refund.refundDo.TableName()
_refund.ALL = field.NewAsterisk(tableName)
_refund.ID = field.NewInt32(tableName, "id")
_refund.OrderID = field.NewInt32(tableName, "order_id")
_refund.ProductID = field.NewInt32(tableName, "product_id")
_refund.Amount = field.NewFloat64(tableName, "amount")
_refund.CreatedAt = field.NewTime(tableName, "created_at")
_refund.UpdatedAt = field.NewTime(tableName, "updated_at")
_refund.DeletedAt = field.NewField(tableName, "deleted_at")
_refund.TradeID = field.NewInt32(tableName, "trade_id")
_refund.fillFieldMap()
@@ -45,12 +45,12 @@ type refund struct {
ALL field.Asterisk
ID field.Int32 // 退款ID
OrderID field.Int32 // 订单ID
ProductID field.Int32 // 产品ID
Amount field.Float64 // 退款金额
CreatedAt field.Time // 创建时间
UpdatedAt field.Time // 更新时间
DeletedAt field.Field // 删除时间
TradeID field.Int32
fieldMap map[string]field.Expr
}
@@ -68,12 +68,12 @@ func (r refund) As(alias string) *refund {
func (r *refund) updateTableName(table string) *refund {
r.ALL = field.NewAsterisk(table)
r.ID = field.NewInt32(table, "id")
r.OrderID = field.NewInt32(table, "order_id")
r.ProductID = field.NewInt32(table, "product_id")
r.Amount = field.NewFloat64(table, "amount")
r.CreatedAt = field.NewTime(table, "created_at")
r.UpdatedAt = field.NewTime(table, "updated_at")
r.DeletedAt = field.NewField(table, "deleted_at")
r.TradeID = field.NewInt32(table, "trade_id")
r.fillFieldMap()
@@ -92,12 +92,12 @@ func (r *refund) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
func (r *refund) fillFieldMap() {
r.fieldMap = make(map[string]field.Expr, 7)
r.fieldMap["id"] = r.ID
r.fieldMap["order_id"] = r.OrderID
r.fieldMap["product_id"] = r.ProductID
r.fieldMap["amount"] = r.Amount
r.fieldMap["created_at"] = r.CreatedAt
r.fieldMap["updated_at"] = r.UpdatedAt
r.fieldMap["deleted_at"] = r.DeletedAt
r.fieldMap["trade_id"] = r.TradeID
}
func (r refund) clone(db *gorm.DB) refund {

178
web/services/id.go Normal file
View File

@@ -0,0 +1,178 @@
package services
import (
"context"
"errors"
"fmt"
"platform/pkg/rds"
"strings"
"time"
"github.com/google/uuid"
"github.com/jxskiss/base62"
"github.com/redis/go-redis/v9"
)
var ID IdService = IdService{}
type IdService struct {
}
// region SerialID
const (
// 保留位确保最高位为0防止产生负值
reservedBits = 1
// 时间戳位数
timestampBits = 41
// 序列号位数
sequenceBits = 22
// 最大序列号掩码2^22 - 1
maxSequence = (1 << sequenceBits) - 1
// 位移计算常量
timestampShift = sequenceBits
// Redis 缓存过期时间(秒)
redisTTL = 5
)
var (
ErrSequenceOverflow = errors.New("sequence overflow")
)
func (s *IdService) GenSerial(ctx context.Context) (uint64, error) {
// 构造Redis键
now := time.Now().Unix()
key := idSerialKey(now)
// 使用Redis事务确保原子操作
var sequence int64
err := rds.Client.Watch(ctx, func(tx *redis.Tx) error {
// 获取当前序列号
currentVal, err := tx.Get(ctx, key).Int64()
if err != nil && !errors.Is(err, redis.Nil) {
return err
}
if errors.Is(err, redis.Nil) {
currentVal = 0
}
sequence = currentVal + 1
// 检查序列号是否溢出
if sequence > maxSequence {
return ErrSequenceOverflow
}
// 将更新后的序列号保存回Redis设置5秒过期时间
pipe := tx.Pipeline()
pipe.Set(ctx, key, sequence, redisTTL*time.Second)
_, err = pipe.Exec(ctx)
return err
}, key)
if err != nil {
return 0, err
}
// 组装最终ID
id := uint64((now << timestampShift) | sequence)
return id, nil
}
// ParseSerial 解析ID返回其组成部分
func (s *IdService) ParseSerial(id uint64) (timestamp int64, sequence int64) {
// 通过位运算和掩码提取各部分
timestamp = int64(id >> timestampShift)
sequence = int64(id & maxSequence)
return
}
// idSerialKey 根据时间戳生成Redis键
func idSerialKey(timestamp int64) string {
return fmt.Sprintf("global:id:serial:%d", timestamp)
}
// endregion
// region ReadableID
// GenReadable 根据给定的标签生成易读的全局唯一标识符
// tag 参数用于标识 ID 的用途,如 "usr" 表示用户ID"ord" 表示订单ID等
// 生成的 ID 格式为:<tag>_<encoded-uuid>例如usr_7NLmVLeHwqS73enFZ1i8tB
func (s *IdService) GenReadable(tag string) string {
// 生成 UUID
id := uuid.New()
// 将 UUID 编码为 Base62 字符串(更短,更易读)
encoded := base62.EncodeToString(id[:])
// 如标签为空,则直接返回编码后的字符串
if tag == "" {
return encoded
}
// 标准化标签:转换为小写并移除特殊字符
tag = normalizeTag(tag)
// 组合最终 ID
return fmt.Sprintf("%s_%s", tag, encoded)
}
// ParseReadableID 解析易读ID返回其标签和编码部分
func (s *IdService) ParseReadableID(id string) (tag string, encoded string) {
parts := strings.SplitN(id, "_", 2)
if len(parts) != 2 {
return "", id
}
return parts[0], parts[1]
}
// TryDecodeID 尝试将编码部分解码回 UUID
// 如果解码失败,返回错误
func (s *IdService) TryDecodeID(encoded string) (uuid.UUID, error) {
// 尝试解码 Base62 编码
bytes, err := base62.DecodeString(encoded)
if err != nil {
return uuid.UUID{}, err
}
// 确保长度正确
if len(bytes) != 16 {
return uuid.UUID{}, fmt.Errorf("invalid UUID length after decoding: %d", len(bytes))
}
// 转换为 UUID
var result uuid.UUID
copy(result[:], bytes)
return result, nil
}
// normalizeTag 标准化标签
// 转换为小写,移除特殊字符,最多保留 5 个字符
func normalizeTag(tag string) string {
// 转换为小写
tag = strings.ToLower(tag)
// 移除特殊字符
var sb strings.Builder
for _, c := range tag {
if (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') {
sb.WriteRune(c)
}
}
// 截取最多 5 个字符
result := sb.String()
if len(result) > 5 {
result = result[:5]
}
return result
}
// endregion