17 Commits

Author SHA1 Message Date
25cacf0bca 完善节点筛选机制 2026-06-12 16:51:08 +08:00
513fe78815 实现手动 proxy 同步接口 2026-06-11 17:52:21 +08:00
ebac8042ea 重构提取逻辑,新增 area 表 2026-06-11 10:10:07 +08:00
dd482dd6b0 优化通道处理 2026-06-10 10:24:05 +08:00
c5453557ae 实现 gost 网关 2026-06-09 15:44:09 +08:00
b00782b3f6 实现文件上传 2026-06-06 17:22:01 +08:00
1b39b2d411 新增 api 文档 2026-06-01 18:37:13 +08:00
7f30b6be4e 补全权限数据 & 优化 router 代码结构 2026-06-01 16:23:16 +08:00
0dfbbe5939 实现文章与分组管理 2026-06-01 15:46:43 +08:00
32e56b1a0f 新增提取函数,实现通过套餐编号提取 2026-05-23 13:50:52 +08:00
b436a6cade 套餐查询返回类型信息 2026-05-21 16:31:59 +08:00
dd08655e2c 查询所有用户套餐时返回名称 2026-05-20 13:31:45 +08:00
9fe6cb4bf5 清理 debug 输出 2026-05-19 14:58:04 +08:00
cf4bc4932a 查询使用 utc 时间 2026-05-19 14:56:47 +08:00
dbc909c736 修复长效时间问题 2026-05-19 13:37:33 +08:00
71554da541 修复提取并发问题 & 修复接口时区问题 2026-05-18 13:54:01 +08:00
8f89503c88 收紧数据保存检查 2026-05-14 14:23:01 +08:00
68 changed files with 17517 additions and 1705 deletions

View File

@@ -1,6 +1,9 @@
# 应用配置 # 应用配置
RUN_MODE=development RUN_MODE=development
DEBUG_HTTP_DUMP=false DEBUG_HTTP_DUMP=false
UPLOAD_DIR=./data/uploads
UPLOAD_PUBLIC_BASE_URL=
ARTICLE_UPLOAD_MAX_BYTES=5242880
# 数据库配置 # 数据库配置
DB_HOST=127.0.0.1 DB_HOST=127.0.0.1

2
.gitignore vendored
View File

@@ -19,3 +19,5 @@ scripts/*
!scripts/env/dev/ !scripts/env/dev/
!scripts/pre/ !scripts/pre/
!scripts/sql/ !scripts/sql/
*/uploads/

View File

@@ -1,29 +1,18 @@
## TODO ## TODO
提取代理: - edge.area_id 可为空,代表节点无固定地区
- 网关修改问题 - 后台展示 mac, ip:port实际地区
- 在提取流程中如果网关被修改,有可能导致数据不一致
- 提供读写锁,在提取流程中获取读锁,在修改网关时获取写锁
- 端口悬空问题
- 在提取流程中如果发生异常,可能导致端口被占用但通道信息没有被写入数据库,配置以及端口将无法解除
- 提交删除任务时,带上配置信息,无需再查数据库且接口总是会被正确清理
- 异常情况下,将只清理网关端口,而无法解除节点连接,这个问题需要额外处理
--- 上传文件平铺到 uploads不分子文件夹
- otel 没有正确记录接口失败信息
筛选和关联展示功能扩展
错误提示增强,展示整链路信息 错误提示增强,展示整链路信息
ip 提取频率限制,在 ensure 函数加逻辑,通过 redis 或者 pg 计算分钟内提取次数,只允许每分钟提取 30 次
proxy 的删除和更新,锁粒度应该有问题
交易信息持久化 交易信息持久化
用反射实现环境变量解析,以简化函数签名 订单关闭问题,在前端关闭窗口后直接调用了全部订单接口,应改成先确认再关闭
- 取消订单接口改成只允许管理员调用
- 新增关闭订单接口,关闭订单的逻辑是先尝试完成,如果订单未支付则取消订单
--- ---
@@ -33,27 +22,33 @@ proxy 的删除和更新,锁粒度应该有问题
冷数据迁移方案 冷数据迁移方案
## 开发环境 ## 开发流程
### 更新表结构的流程 ### 新建数据表流程
1. 编辑 `scripts/sql/init.sql` 文件,参照原有格式,需要注意: 1. 创建 model 文件
2. 将 model 按照格式添加声明到 `cmd/gen/main.go`
3. 编辑 `scripts/sql/init.sql` 文件,参照原有格式,需要注意:
- 先写 drop table if exists 语句,确保脚本可以幂等执行 - 先写 drop table if exists 语句,确保脚本可以幂等执行
- 编写 create table 语句,按需添加审计字段和软删除字段 (created_at, updated_at, deleted_at)
- 为有必要的字段添加索引 - 为有必要的字段添加索引
- 为数据表及其字段添加注释 - 为数据表及其字段添加注释
- 在文件末尾创建数据表流程全部结束后,为字段添加外键 - 在文件末尾创建数据表流程全部结束后,为字段添加外键
2. 建议用数据库工具检查差异并增量更新,或者手动增量更新 4. 调用 `go run ./cmd/gen/main.go` 生成查询文件
3. 创建 model 文件,并将其添加到 gen 代码中
4. 生成查询文件
### 权限管理 ### 更新数据表流程
`web/core/scopes.go` 下定义了系统所有静态权限 1. 更新 model 文件
2. 编辑 `scripts/sql/init.sql` 文件,参照原有格式,需要注意:
- 先写 drop table if exists 语句,确保脚本可以幂等执行
- 为有必要的字段添加索引
- 为数据表及其字段添加注释
- 在文件末尾创建数据表流程全部结束后,为字段添加外键
3. 调用 `go run ./cmd/gen/main.go` 更新查询文件
新增系统权限需要在数据库中配套添加权限 ### 新增接口或修改接口权限
前端也需要新增配套权限定义 1.`web/core/scopes.go` 下声明权限常量。通常格式为 `Model:Action:SubAction`,例如 `User:Create``User:Delete``User:Update:Password`
2.`scripts/sql/fill.sql` 文件的权限区域添加或修改权限条目
## 业务逻辑 ## 业务逻辑

View File

@@ -35,6 +35,9 @@ func main() {
m.Admin{}, m.Admin{},
m.AdminRole{}, m.AdminRole{},
m.Announcement{}, m.Announcement{},
m.Area{},
m.Article{},
m.ArticleGroup{},
m.Bill{}, m.Bill{},
m.Channel{}, m.Channel{},
m.Client{}, m.Client{},

View File

@@ -23,7 +23,7 @@ services:
restart: unless-stopped restart: unless-stopped
postgres-migrate: postgres-migrate:
image: postgres:17 image: postgres:17.7
environment: environment:
POSTGRES_USER: ${DB_USERNAME} POSTGRES_USER: ${DB_USERNAME}
POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_PASSWORD: ${DB_PASSWORD}
@@ -40,6 +40,14 @@ services:
depends_on: depends_on:
- redis - redis
gost:
image: gogost/gost
command: >
-api test:test@:9700
ports:
- "9700:9700"
restart: unless-stopped
volumes: volumes:
postgres_data: postgres_data:
redis_data: redis_data:

File diff suppressed because it is too large Load Diff

2
go.mod
View File

@@ -26,6 +26,7 @@ require (
go.opentelemetry.io/otel v1.43.0 go.opentelemetry.io/otel v1.43.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0
go.opentelemetry.io/otel/sdk v1.43.0 go.opentelemetry.io/otel/sdk v1.43.0
go.opentelemetry.io/otel/trace v1.43.0
golang.org/x/crypto v0.49.0 golang.org/x/crypto v0.49.0
golang.org/x/sync v0.20.0 golang.org/x/sync v0.20.0
gorm.io/datatypes v1.2.7 gorm.io/datatypes v1.2.7
@@ -88,7 +89,6 @@ require (
go.opentelemetry.io/contrib v1.38.0 // indirect go.opentelemetry.io/contrib v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect
golang.org/x/mod v0.33.0 // indirect golang.org/x/mod v0.33.0 // indirect
golang.org/x/net v0.52.0 // indirect golang.org/x/net v0.52.0 // indirect

11
pkg/env/env.go vendored
View File

@@ -24,6 +24,9 @@ var (
SessionAccessExpire = 60 * 60 * 2 // 访问令牌过期时间,单位秒。默认 2 小时 SessionAccessExpire = 60 * 60 * 2 // 访问令牌过期时间,单位秒。默认 2 小时
SessionRefreshExpire = 60 * 60 * 24 * 7 // 刷新令牌过期时间,单位秒。默认 7 天 SessionRefreshExpire = 60 * 60 * 24 * 7 // 刷新令牌过期时间,单位秒。默认 7 天
DebugHttpDump = false // 是否打印请求和响应的原始数据 DebugHttpDump = false // 是否打印请求和响应的原始数据
UploadDir = "./data/uploads"
UploadPublicBaseURL = ""
ArticleUploadMaxBytes = 5 * 1024 * 1024
DbHost = "localhost" DbHost = "localhost"
DbPort = "5432" DbPort = "5432"
@@ -42,6 +45,9 @@ var (
BaiyinCloudUrl string BaiyinCloudUrl string
BaiyinTokenUrl string BaiyinTokenUrl string
GostApiPort = 9700
GostApiPathPrefix = ""
IdenCallbackUrl string IdenCallbackUrl string
IdenAccessKey string IdenAccessKey string
IdenSecretKey string IdenSecretKey string
@@ -106,6 +112,9 @@ func Init() {
errs = append(errs, parse(&SessionAccessExpire, "SESSION_ACCESS_EXPIRE", true, nil)) errs = append(errs, parse(&SessionAccessExpire, "SESSION_ACCESS_EXPIRE", true, nil))
errs = append(errs, parse(&SessionRefreshExpire, "SESSION_REFRESH_EXPIRE", true, nil)) errs = append(errs, parse(&SessionRefreshExpire, "SESSION_REFRESH_EXPIRE", true, nil))
errs = append(errs, parse(&DebugHttpDump, "DEBUG_HTTP_DUMP", true, nil)) errs = append(errs, parse(&DebugHttpDump, "DEBUG_HTTP_DUMP", true, nil))
errs = append(errs, parse(&UploadDir, "UPLOAD_DIR", true, nil))
errs = append(errs, parse(&UploadPublicBaseURL, "UPLOAD_PUBLIC_BASE_URL", true, nil))
errs = append(errs, parse(&ArticleUploadMaxBytes, "ARTICLE_UPLOAD_MAX_BYTES", true, nil))
errs = append(errs, parse(&DbHost, "DB_HOST", true, nil)) errs = append(errs, parse(&DbHost, "DB_HOST", true, nil))
errs = append(errs, parse(&DbPort, "DB_PORT", true, nil)) errs = append(errs, parse(&DbPort, "DB_PORT", true, nil))
@@ -123,6 +132,8 @@ func Init() {
errs = append(errs, parse(&BaiyinCloudUrl, "BAIYIN_CLOUD_URL", false, nil)) errs = append(errs, parse(&BaiyinCloudUrl, "BAIYIN_CLOUD_URL", false, nil))
errs = append(errs, parse(&BaiyinTokenUrl, "BAIYIN_TOKEN_URL", false, nil)) errs = append(errs, parse(&BaiyinTokenUrl, "BAIYIN_TOKEN_URL", false, nil))
errs = append(errs, parse(&GostApiPort, "GOST_API_PORT", true, nil))
errs = append(errs, parse(&GostApiPathPrefix, "GOST_API_PATH_PREFIX", true, nil))
errs = append(errs, parse(&IdenCallbackUrl, "IDEN_CALLBACK_URL", false, nil)) errs = append(errs, parse(&IdenCallbackUrl, "IDEN_CALLBACK_URL", false, nil))
errs = append(errs, parse(&IdenAccessKey, "IDEN_ACCESS_KEY", false, nil)) errs = append(errs, parse(&IdenAccessKey, "IDEN_ACCESS_KEY", false, nil))

View File

@@ -81,24 +81,15 @@ func Map[T any, R any](src []T, convert func(T) R) []R {
// 时间 // 时间
// ==================== // ====================
func DateHead(date time.Time) time.Time { func IsSameDate(date1, date2 time.Time) bool {
var y, m, d = date.Date() var y1, m1, d1 = date1.Local().Date()
return time.Date(y, m, d, 0, 0, 0, 0, date.Location()) var y2, m2, d2 = date2.Local().Date()
} return y1 == y2 && m1 == m2 && d1 == d2
func DateTail(date time.Time) time.Time {
var y, m, d = date.Date()
return time.Date(y, m, d, 23, 59, 59, 999999999, date.Location())
} }
func Today() time.Time { func Today() time.Time {
return DateHead(time.Now()) var y, m, d = time.Now().Date()
} return time.Date(y, m, d, 0, 0, 0, 0, time.Local)
func IsSameDate(date1, date2 time.Time) bool {
var y1, m1, d1 = date1.Date()
var y2, m2, d2 = date2.Date()
return y1 == y2 && m1 == m2 && d1 == d2
} }
func IsToday(date time.Time) bool { func IsToday(date time.Time) bool {

258
scripts/sql/fill-area.sql Normal file
View File

@@ -0,0 +1,258 @@
insert into area
(name, level)
values
('上海',1),
('云南',1),
('内蒙古',1),
('北京',1),
('吉林',1),
('四川',1),
('天津',1),
('宁夏',1),
('安徽',1),
('山东',1),
('山西',1),
('广东',1),
('广西',1),
('新疆',1),
('江苏',1),
('江西',1),
('河北',1),
('河南',1),
('浙江',1),
('海南',1),
('湖北',1),
('湖南',1),
('甘肃',1),
('福建',1),
('贵州',1),
('辽宁',1),
('重庆',1),
('陕西',1),
('黑龙江',1)
;
insert into area
(name, level, parent_id)
values
('上海', 2, (select id from area where name = '上海')),
('昆明', 2, (select id from area where name = '云南')),
('包头', 2, (select id from area where name = '内蒙古')),
('呼伦贝尔', 2, (select id from area where name = '内蒙古')),
('呼和浩特', 2, (select id from area where name = '内蒙古')),
('赤峰', 2, (select id from area where name = '内蒙古')),
('通辽', 2, (select id from area where name = '内蒙古')),
('鄂尔多斯', 2, (select id from area where name = '内蒙古')),
('北京', 2, (select id from area where name = '北京')),
('四平', 2, (select id from area where name = '吉林')),
('延边朝鲜族自治州', 2, (select id from area where name = '吉林')),
('松原', 2, (select id from area where name = '吉林')),
('白山', 2, (select id from area where name = '吉林')),
('通化', 2, (select id from area where name = '吉林')),
('长春', 2, (select id from area where name = '吉林')),
('乐山', 2, (select id from area where name = '四川')),
('内江', 2, (select id from area where name = '四川')),
('南充', 2, (select id from area where name = '四川')),
('宜宾', 2, (select id from area where name = '四川')),
('广元', 2, (select id from area where name = '四川')),
('德阳', 2, (select id from area where name = '四川')),
('成都', 2, (select id from area where name = '四川')),
('攀枝花', 2, (select id from area where name = '四川')),
('泸州', 2, (select id from area where name = '四川')),
('绵阳', 2, (select id from area where name = '四川')),
('自贡', 2, (select id from area where name = '四川')),
('达州', 2, (select id from area where name = '四川')),
('天津', 2, (select id from area where name = '天津')),
('银川', 2, (select id from area where name = '宁夏')),
('亳州', 2, (select id from area where name = '安徽')),
('六安', 2, (select id from area where name = '安徽')),
('合肥', 2, (select id from area where name = '安徽')),
('安庆', 2, (select id from area where name = '安徽')),
('宣城', 2, (select id from area where name = '安徽')),
('宿州', 2, (select id from area where name = '安徽')),
('池州', 2, (select id from area where name = '安徽')),
('淮北', 2, (select id from area where name = '安徽')),
('淮南', 2, (select id from area where name = '安徽')),
('滁州', 2, (select id from area where name = '安徽')),
('芜湖', 2, (select id from area where name = '安徽')),
('蚌埠', 2, (select id from area where name = '安徽')),
('铜陵', 2, (select id from area where name = '安徽')),
('阜阳', 2, (select id from area where name = '安徽')),
('马鞍山', 2, (select id from area where name = '安徽')),
('黄山', 2, (select id from area where name = '安徽')),
('东营', 2, (select id from area where name = '山东')),
('临沂', 2, (select id from area where name = '山东')),
('威海', 2, (select id from area where name = '山东')),
('德州', 2, (select id from area where name = '山东')),
('日照', 2, (select id from area where name = '山东')),
('枣庄', 2, (select id from area where name = '山东')),
('泰安', 2, (select id from area where name = '山东')),
('济南', 2, (select id from area where name = '山东')),
('济宁', 2, (select id from area where name = '山东')),
('淄博', 2, (select id from area where name = '山东')),
('滨州', 2, (select id from area where name = '山东')),
('潍坊', 2, (select id from area where name = '山东')),
('烟台', 2, (select id from area where name = '山东')),
('聊城', 2, (select id from area where name = '山东')),
('菏泽', 2, (select id from area where name = '山东')),
('青岛', 2, (select id from area where name = '山东')),
('临汾', 2, (select id from area where name = '山西')),
('吕梁', 2, (select id from area where name = '山西')),
('大同', 2, (select id from area where name = '山西')),
('太原', 2, (select id from area where name = '山西')),
('忻州', 2, (select id from area where name = '山西')),
('晋城', 2, (select id from area where name = '山西')),
('朔州', 2, (select id from area where name = '山西')),
('运城', 2, (select id from area where name = '山西')),
('长治', 2, (select id from area where name = '山西')),
('阳泉', 2, (select id from area where name = '山西')),
('东莞', 2, (select id from area where name = '广东')),
('中山', 2, (select id from area where name = '广东')),
('云浮', 2, (select id from area where name = '广东')),
('佛山', 2, (select id from area where name = '广东')),
('广州', 2, (select id from area where name = '广东')),
('惠州', 2, (select id from area where name = '广东')),
('揭阳', 2, (select id from area where name = '广东')),
('梅州', 2, (select id from area where name = '广东')),
('汕头', 2, (select id from area where name = '广东')),
('汕尾', 2, (select id from area where name = '广东')),
('江门', 2, (select id from area where name = '广东')),
('河源', 2, (select id from area where name = '广东')),
('深圳', 2, (select id from area where name = '广东')),
('清远', 2, (select id from area where name = '广东')),
('湛江', 2, (select id from area where name = '广东')),
('潮州', 2, (select id from area where name = '广东')),
('珠海', 2, (select id from area where name = '广东')),
('肇庆', 2, (select id from area where name = '广东')),
('茂名', 2, (select id from area where name = '广东')),
('阳江', 2, (select id from area where name = '广东')),
('韶关', 2, (select id from area where name = '广东')),
('北海', 2, (select id from area where name = '广西')),
('南宁', 2, (select id from area where name = '广西')),
('柳州', 2, (select id from area where name = '广西')),
('桂林', 2, (select id from area where name = '广西')),
('玉林', 2, (select id from area where name = '广西')),
('贵港', 2, (select id from area where name = '广西')),
('钦州', 2, (select id from area where name = '广西')),
('乌鲁木齐', 2, (select id from area where name = '新疆')),
('南京', 2, (select id from area where name = '江苏')),
('南通', 2, (select id from area where name = '江苏')),
('宿迁', 2, (select id from area where name = '江苏')),
('常州', 2, (select id from area where name = '江苏')),
('徐州', 2, (select id from area where name = '江苏')),
('扬州', 2, (select id from area where name = '江苏')),
('无锡', 2, (select id from area where name = '江苏')),
('泰州', 2, (select id from area where name = '江苏')),
('淮安', 2, (select id from area where name = '江苏')),
('盐城', 2, (select id from area where name = '江苏')),
('苏州', 2, (select id from area where name = '江苏')),
('连云港', 2, (select id from area where name = '江苏')),
('镇江', 2, (select id from area where name = '江苏')),
('上饶', 2, (select id from area where name = '江西')),
('九江', 2, (select id from area where name = '江西')),
('南昌', 2, (select id from area where name = '江西')),
('吉安', 2, (select id from area where name = '江西')),
('宜春', 2, (select id from area where name = '江西')),
('抚州', 2, (select id from area where name = '江西')),
('新余', 2, (select id from area where name = '江西')),
('景德镇', 2, (select id from area where name = '江西')),
('萍乡', 2, (select id from area where name = '江西')),
('赣州', 2, (select id from area where name = '江西')),
('鹰潭', 2, (select id from area where name = '江西')),
('保定', 2, (select id from area where name = '河北')),
('唐山', 2, (select id from area where name = '河北')),
('廊坊', 2, (select id from area where name = '河北')),
('张家口', 2, (select id from area where name = '河北')),
('承德', 2, (select id from area where name = '河北')),
('沧州', 2, (select id from area where name = '河北')),
('石家庄', 2, (select id from area where name = '河北')),
('秦皇岛', 2, (select id from area where name = '河北')),
('衡水', 2, (select id from area where name = '河北')),
('邢台', 2, (select id from area where name = '河北')),
('邯郸', 2, (select id from area where name = '河北')),
('信阳', 2, (select id from area where name = '河南')),
('南阳', 2, (select id from area where name = '河南')),
('周口', 2, (select id from area where name = '河南')),
('商丘', 2, (select id from area where name = '河南')),
('安阳', 2, (select id from area where name = '河南')),
('开封', 2, (select id from area where name = '河南')),
('新乡', 2, (select id from area where name = '河南')),
('洛阳', 2, (select id from area where name = '河南')),
('漯河', 2, (select id from area where name = '河南')),
('焦作', 2, (select id from area where name = '河南')),
('许昌', 2, (select id from area where name = '河南')),
('郑州', 2, (select id from area where name = '河南')),
('驻马店', 2, (select id from area where name = '河南')),
('鹤壁', 2, (select id from area where name = '河南')),
('丽水', 2, (select id from area where name = '浙江')),
('台州', 2, (select id from area where name = '浙江')),
('嘉兴', 2, (select id from area where name = '浙江')),
('宁波', 2, (select id from area where name = '浙江')),
('杭州', 2, (select id from area where name = '浙江')),
('温州', 2, (select id from area where name = '浙江')),
('湖州', 2, (select id from area where name = '浙江')),
('绍兴', 2, (select id from area where name = '浙江')),
('舟山', 2, (select id from area where name = '浙江')),
('衢州', 2, (select id from area where name = '浙江')),
('金华', 2, (select id from area where name = '浙江')),
('三亚', 2, (select id from area where name = '海南')),
('文昌', 2, (select id from area where name = '海南')),
('海口', 2, (select id from area where name = '海南')),
('咸宁', 2, (select id from area where name = '湖北')),
('孝感', 2, (select id from area where name = '湖北')),
('宜昌', 2, (select id from area where name = '湖北')),
('武汉', 2, (select id from area where name = '湖北')),
('荆州', 2, (select id from area where name = '湖北')),
('荆门', 2, (select id from area where name = '湖北')),
('襄阳', 2, (select id from area where name = '湖北')),
('黄冈', 2, (select id from area where name = '湖北')),
('黄石', 2, (select id from area where name = '湖北')),
('岳阳', 2, (select id from area where name = '湖南')),
('株洲', 2, (select id from area where name = '湖南')),
('湘潭', 2, (select id from area where name = '湖南')),
('衡阳', 2, (select id from area where name = '湖南')),
('邵阳', 2, (select id from area where name = '湖南')),
('郴州', 2, (select id from area where name = '湖南')),
('长沙', 2, (select id from area where name = '湖南')),
('兰州', 2, (select id from area where name = '甘肃')),
('三明', 2, (select id from area where name = '福建')),
('南平', 2, (select id from area where name = '福建')),
('厦门', 2, (select id from area where name = '福建')),
('宁德', 2, (select id from area where name = '福建')),
('泉州', 2, (select id from area where name = '福建')),
('福州', 2, (select id from area where name = '福建')),
('莆田', 2, (select id from area where name = '福建')),
('龙岩', 2, (select id from area where name = '福建')),
('六盘水', 2, (select id from area where name = '贵州')),
('贵阳', 2, (select id from area where name = '贵州')),
('遵义', 2, (select id from area where name = '贵州')),
('铜仁', 2, (select id from area where name = '贵州')),
('黔东南苗族侗族自治州', 2, (select id from area where name = '贵州')),
('大连', 2, (select id from area where name = '辽宁')),
('抚顺', 2, (select id from area where name = '辽宁')),
('朝阳', 2, (select id from area where name = '辽宁')),
('沈阳', 2, (select id from area where name = '辽宁')),
('盘锦', 2, (select id from area where name = '辽宁')),
('营口', 2, (select id from area where name = '辽宁')),
('葫芦岛', 2, (select id from area where name = '辽宁')),
('铁岭', 2, (select id from area where name = '辽宁')),
('鞍山', 2, (select id from area where name = '辽宁')),
('重庆', 2, (select id from area where name = '重庆')),
('咸阳', 2, (select id from area where name = '陕西')),
('宝鸡', 2, (select id from area where name = '陕西')),
('渭南', 2, (select id from area where name = '陕西')),
('西安', 2, (select id from area where name = '陕西')),
('铜川', 2, (select id from area where name = '陕西')),
('七台河', 2, (select id from area where name = '黑龙江')),
('伊春', 2, (select id from area where name = '黑龙江')),
('佳木斯', 2, (select id from area where name = '黑龙江')),
('双鸭山', 2, (select id from area where name = '黑龙江')),
('哈尔滨', 2, (select id from area where name = '黑龙江')),
('大庆', 2, (select id from area where name = '黑龙江')),
('牡丹江', 2, (select id from area where name = '黑龙江')),
('绥化', 2, (select id from area where name = '黑龙江')),
('鸡西', 2, (select id from area where name = '黑龙江')),
('鹤岗', 2, (select id from area where name = '黑龙江')),
('黑河', 2, (select id from area where name = '黑龙江')),
('齐齐哈尔', 2, (select id from area where name = '黑龙江'))

6601
scripts/sql/fill-edge.sql Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -128,7 +128,9 @@ insert into permission (name, description, sort) values
('bill', '账单', 13), ('bill', '账单', 13),
('balance_activity', '余额变动', 14), ('balance_activity', '余额变动', 14),
('proxy', '代理', 15), ('proxy', '代理', 15),
('coupon_user', '已发放优惠券', 16); ('coupon_user', '已发放优惠券', 16),
('article', '文档', 17),
('article_group', '文档分组', 18);
-- -------------------------- -- --------------------------
-- level 2 -- level 2
@@ -215,6 +217,16 @@ insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'coupon_user' and deleted_at is null), 'coupon_user:read', '读取已发放优惠券列表', 1), ((select id from permission where name = 'coupon_user' and deleted_at is null), 'coupon_user:read', '读取已发放优惠券列表', 1),
((select id from permission where name = 'coupon_user' and deleted_at is null), 'coupon_user:write', '编辑已发放优惠券', 2); ((select id from permission where name = 'coupon_user' and deleted_at is null), 'coupon_user:write', '编辑已发放优惠券', 2);
-- article 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'article' and deleted_at is null), 'article:read', '读取文档列表', 1),
((select id from permission where name = 'article' and deleted_at is null), 'article:write', '编辑文档', 2);
-- article_group 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'article_group' and deleted_at is null), 'article_group:read', '读取文档分组列表', 1),
((select id from permission where name = 'article_group' and deleted_at is null), 'article_group:write', '编辑文档分组', 2);
-- -------------------------- -- --------------------------
-- level 3 -- level 3
-- -------------------------- -- --------------------------
@@ -227,6 +239,10 @@ insert into permission (parent_id, name, description, sort) values
insert into permission (parent_id, name, description, sort) values insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'proxy:write' and deleted_at is null), 'proxy:write:status', '更改代理状态', 1); ((select id from permission where name = 'proxy:write' and deleted_at is null), 'proxy:write:status', '更改代理状态', 1);
-- channel:write 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'channel:write' and deleted_at is null), 'channel:write:clear_expired', '清理过期 IP', 1);
-- resource:short 子权限 -- resource:short 子权限
insert into permission (parent_id, name, description, sort) values insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'resource:short' and deleted_at is null), 'resource:short:read', '读取用户短效动态套餐列表', 1); ((select id from permission where name = 'resource:short' and deleted_at is null), 'resource:short:read', '读取用户短效动态套餐列表', 1);
@@ -273,6 +289,10 @@ insert into permission (parent_id, name, description, sort) values
insert into permission (parent_id, name, description, sort) values insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'coupon_user:read' and deleted_at is null), 'coupon_user:read:of_user', '读取指定用户的已发放优惠券列表', 1); ((select id from permission where name = 'coupon_user:read' and deleted_at is null), 'coupon_user:read:of_user', '读取指定用户的已发放优惠券列表', 1);
-- trade:write 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'trade:write' and deleted_at is null), 'trade:write:complete', '完成交易', 1);
-- -------------------------- -- --------------------------
-- level 4 -- level 4
-- -------------------------- -- --------------------------

View File

@@ -143,6 +143,62 @@ comment on column announcement.created_at is '创建时间';
comment on column announcement.updated_at is '更新时间'; comment on column announcement.updated_at is '更新时间';
comment on column announcement.deleted_at is '删除时间'; comment on column announcement.deleted_at is '删除时间';
-- article_group
drop table if exists article_group cascade;
create table article_group (
id int generated by default as identity primary key,
name text not null,
code text not null,
sort int not null default 0,
status int not null default 1,
created_at timestamptz default current_timestamp,
updated_at timestamptz default current_timestamp,
deleted_at timestamptz
);
create unique index udx_article_group_code on article_group (code) where deleted_at is null;
create index idx_article_group_status on article_group (status) where deleted_at is null;
create index idx_article_group_sort on article_group (sort) where deleted_at is null;
-- article_group表字段注释
comment on table article_group is '文章分组表';
comment on column article_group.id is '分组ID';
comment on column article_group.name is '分组名称';
comment on column article_group.code is '分组编码';
comment on column article_group.sort is '分组排序';
comment on column article_group.status is '分组状态0-禁用1-正常';
comment on column article_group.created_at is '创建时间';
comment on column article_group.updated_at is '更新时间';
comment on column article_group.deleted_at is '删除时间';
-- article
drop table if exists article cascade;
create table article (
id int generated by default as identity primary key,
group_id int not null,
title text not null,
content text,
sort int not null default 0,
status int not null default 1,
created_at timestamptz default current_timestamp,
updated_at timestamptz default current_timestamp,
deleted_at timestamptz
);
create index idx_article_group_id on article (group_id) where deleted_at is null;
create index idx_article_status on article (status) where deleted_at is null;
create index idx_article_sort on article (sort) where deleted_at is null;
-- article表字段注释
comment on table article is '文章表';
comment on column article.id is '文章ID';
comment on column article.group_id is '分组ID';
comment on column article.title is '文章标题';
comment on column article.content is '文章内容';
comment on column article.sort is '文章排序';
comment on column article.status is '文章状态0-禁用1-正常';
comment on column article.created_at is '创建时间';
comment on column article.updated_at is '更新时间';
comment on column article.deleted_at is '删除时间';
-- inquiry -- inquiry
drop table if exists inquiry cascade; drop table if exists inquiry cascade;
create table inquiry ( create table inquiry (
@@ -549,6 +605,7 @@ create table proxy (
mac text not null, mac text not null,
ip inet not null, ip inet not null,
host text, host text,
port int,
secret text, secret text,
type int not null, type int not null,
status int not null, status int not null,
@@ -565,17 +622,43 @@ create index idx_proxy_created_at on proxy (created_at) where deleted_at is null
comment on table proxy is '代理服务表'; comment on table proxy is '代理服务表';
comment on column proxy.id is '代理服务ID'; comment on column proxy.id is '代理服务ID';
comment on column proxy.version is '代理服务版本'; comment on column proxy.version is '代理服务版本';
comment on column proxy.port is '代理服务端口';
comment on column proxy.mac is '代理服务名称'; comment on column proxy.mac is '代理服务名称';
comment on column proxy.ip is '代理服务地址'; comment on column proxy.ip is '代理服务地址';
comment on column proxy.host is '代理服务域名'; comment on column proxy.host is '代理服务域名';
comment on column proxy.secret is '代理服务密钥'; comment on column proxy.secret is '代理服务密钥';
comment on column proxy.type is '代理服务类型1-自有2-白银'; comment on column proxy.type is '代理服务类型1-自有2-白银3-GOST';
comment on column proxy.status is '代理服务状态0-离线1-在线'; comment on column proxy.status is '代理服务状态0-离线1-在线';
comment on column proxy.meta is '代理服务元信息'; comment on column proxy.meta is '代理服务元信息';
comment on column proxy.created_at is '创建时间'; comment on column proxy.created_at is '创建时间';
comment on column proxy.updated_at is '更新时间'; comment on column proxy.updated_at is '更新时间';
comment on column proxy.deleted_at is '删除时间'; comment on column proxy.deleted_at is '删除时间';
-- area
drop table if exists area cascade;
create table area (
id int generated by default as identity primary key,
name text not null,
level int not null,
parent_id int,
created_at timestamptz default current_timestamp,
updated_at timestamptz default current_timestamp,
deleted_at timestamptz
);
create index idx_area_level on area (level) where deleted_at is null;
create index idx_area_parent_id on area (parent_id) where deleted_at is null;
create index idx_area_created_at on area (created_at) where deleted_at is null;
-- area表字段注释
comment on table area is '地区表';
comment on column area.id is '地区ID';
comment on column area.name is '地区名称';
comment on column area.level is '地区层级1-省2-市';
comment on column area.parent_id is '父级地区ID';
comment on column area.created_at is '创建时间';
comment on column area.updated_at is '更新时间';
comment on column area.deleted_at is '删除时间';
-- edge -- edge
drop table if exists edge cascade; drop table if exists edge cascade;
create table edge ( create table edge (
@@ -584,9 +667,9 @@ create table edge (
version int not null, version int not null,
mac text not null, mac text not null,
ip inet not null, ip inet not null,
port int,
isp int not null, isp int not null,
prov text not null, area_id int,
city text not null,
status int not null default 0, status int not null default 0,
rtt int default 0, rtt int default 0,
loss int default 0, loss int default 0,
@@ -596,20 +679,19 @@ create table edge (
); );
create unique index udx_edge_mac on edge (mac) where deleted_at is null; create unique index udx_edge_mac on edge (mac) where deleted_at is null;
create index idx_edge_isp on edge (isp) where deleted_at is null; create index idx_edge_isp on edge (isp) where deleted_at is null;
create index idx_edge_prov on edge (prov) where deleted_at is null; create index idx_edge_area_id on edge (area_id) where deleted_at is null;
create index idx_edge_city on edge (city) where deleted_at is null;
create index idx_edge_created_at on edge (created_at) where deleted_at is null; create index idx_edge_created_at on edge (created_at) where deleted_at is null;
-- edge表字段注释 -- edge表字段注释
comment on table edge is '节点表'; comment on table edge is '节点表';
comment on column edge.id is '节点ID'; comment on column edge.id is '节点ID';
comment on column edge.type is '节点类型1-自建'; comment on column edge.type is '节点类型1-自建2-GOST chain';
comment on column edge.version is '节点版本'; comment on column edge.version is '节点版本';
comment on column edge.mac is '节点 mac 地址'; comment on column edge.mac is '节点 mac 地址或 GOST chain 名称';
comment on column edge.ip is '节点地址'; comment on column edge.ip is '节点地址或 GOST chain addr 的 IP';
comment on column edge.port is 'GOST chain addr 的端口';
comment on column edge.isp is '运营商1-电信2-联通3-移动'; comment on column edge.isp is '运营商1-电信2-联通3-移动';
comment on column edge.prov is '省份'; comment on column edge.area_id is '城市地区ID';
comment on column edge.city is '城市';
comment on column edge.status is '节点状态0-离线1-正常'; comment on column edge.status is '节点状态0-离线1-正常';
comment on column edge.rtt is '最近平均延迟'; comment on column edge.rtt is '最近平均延迟';
comment on column edge.loss is '最近丢包率'; comment on column edge.loss is '最近丢包率';
@@ -1177,6 +1259,10 @@ alter table channel
add constraint fk_channel_user_id foreign key (user_id) references "user" (id) on delete cascade; add constraint fk_channel_user_id foreign key (user_id) references "user" (id) on delete cascade;
alter table channel alter table channel
add constraint fk_channel_proxy_id foreign key (proxy_id) references proxy (id) on delete set null; add constraint fk_channel_proxy_id foreign key (proxy_id) references proxy (id) on delete set null;
alter table area
add constraint fk_area_parent_id foreign key (parent_id) references area (id) on delete set null;
alter table edge
add constraint fk_edge_area_id foreign key (area_id) references area (id) on delete restrict;
alter table channel alter table channel
add constraint fk_channel_edge_id foreign key (edge_id) references edge (id) on delete set null; add constraint fk_channel_edge_id foreign key (edge_id) references edge (id) on delete set null;
alter table channel alter table channel
@@ -1228,6 +1314,10 @@ alter table coupon_user
alter table coupon_user alter table coupon_user
add constraint fk_coupon_user_coupon_id foreign key (coupon_id) references coupon (id) on delete cascade; add constraint fk_coupon_user_coupon_id foreign key (coupon_id) references coupon (id) on delete cascade;
-- article表外键
alter table article
add constraint fk_article_group_id foreign key (group_id) references article_group (id) on delete restrict;
-- product_sku表外键 -- product_sku表外键
alter table product_sku alter table product_sku
add constraint fk_product_sku_product_id foreign key (product_id) references product (id) on delete cascade; add constraint fk_product_sku_product_id foreign key (product_id) references product (id) on delete cascade;

View File

@@ -16,7 +16,7 @@ func FindSession(accessToken string, now time.Time) (*m.Session, error) {
Preload(field.Associations). Preload(field.Associations).
Where( Where(
q.Session.AccessToken.Eq(accessToken), q.Session.AccessToken.Eq(accessToken),
q.Session.AccessTokenExpires.Gt(now), q.Session.AccessTokenExpires.Gt(now.UTC()),
).First() ).First()
} }
@@ -25,7 +25,7 @@ func FindSessionByRefresh(refreshToken string, now time.Time) (*m.Session, error
Preload(field.Associations). Preload(field.Associations).
Where( Where(
q.Session.RefreshToken.Eq(refreshToken), q.Session.RefreshToken.Eq(refreshToken),
q.Session.RefreshTokenExpires.Gt(now), q.Session.RefreshTokenExpires.Gt(now.UTC()),
).First() ).First()
} }

View File

@@ -74,6 +74,14 @@ const (
ScopeProxyWrite = string("proxy:write") // 写入代理 ScopeProxyWrite = string("proxy:write") // 写入代理
ScopeProxyWriteStatus = string("proxy:write:status") // 更改代理状态 ScopeProxyWriteStatus = string("proxy:write:status") // 更改代理状态
ScopeArticle = string("article") // 文档
ScopeArticleRead = string("article:read") // 读取文档列表
ScopeArticleWrite = string("article:write") // 写入文档
ScopeArticleGroup = string("article_group") // 文档分组
ScopeArticleGroupRead = string("article_group:read") // 读取文档分组列表
ScopeArticleGroupWrite = string("article_group:write") // 写入文档分组
ScopeTrade = string("trade") // 交易 ScopeTrade = string("trade") // 交易
ScopeTradeRead = string("trade:read") // 读取交易列表 ScopeTradeRead = string("trade:read") // 读取交易列表
ScopeTradeReadOfUser = string("trade:read:of_user") // 读取指定用户的交易列表 ScopeTradeReadOfUser = string("trade:read:of_user") // 读取指定用户的交易列表

View File

@@ -52,7 +52,8 @@ func ErrorHandler(c *fiber.Ctx, err error) error {
case errors.As(err, &servErr): case errors.As(err, &servErr):
code = fiber.StatusInternalServerError code = fiber.StatusInternalServerError
message = err.Error() slog.Warn("服务端错误", slog.String("error", servErr.Error()))
message = "服务端错误"
case errors.As(err, &timeErr): case errors.As(err, &timeErr):
code = fiber.StatusBadRequest code = fiber.StatusBadRequest

9
web/events/edges.go Normal file
View File

@@ -0,0 +1,9 @@
package events
import "github.com/hibiken/asynq"
const RefreshEdge = "edge:refresh"
func NewRefreshEdge() *asynq.Task {
return asynq.NewTask(RefreshEdge, nil)
}

280
web/globals/gost.go Normal file
View File

@@ -0,0 +1,280 @@
package globals
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"platform/web/core"
"strings"
)
var ErrGostNotFound = errors.New("gost resource not found")
func IsGostNotFound(err error) bool {
return errors.Is(err, ErrGostNotFound)
}
type GostClient interface {
ListChains() ([]*GostChainConfig, error)
GetChain(name string) (*GostChainConfig, error)
CreateChain(chain *GostChainConfig) error
DeleteChain(name string) error
SaveConfig() error
CreateService(service *GostServiceConfig) error
DeleteService(name string) error
CreateAuther(auther *GostAutherConfig) error
DeleteAuther(name string) error
CreateAdmission(admission *GostAdmissionConfig) error
DeleteAdmission(name string) error
}
type gostClient struct {
baseURL string
pathPrefix string
username string
password string
}
var GostInitializer = func(host string, port int, pathPrefix, username, password string) GostClient {
baseURL := strings.TrimSpace(host)
if !strings.Contains(baseURL, "://") {
baseURL = fmt.Sprintf("http://%s:%d", baseURL, port)
}
return &gostClient{
baseURL: strings.TrimRight(baseURL, "/"),
pathPrefix: normalizeGostPathPrefix(pathPrefix),
username: username,
password: password,
}
}
func NewGost(host string, port int, pathPrefix, username, password string) GostClient {
return GostInitializer(host, port, pathPrefix, username, password)
}
type GostChainConfig struct {
Name string `json:"name"`
Hops []GostHopConfig `json:"hops,omitempty"`
}
type GostHopConfig struct {
Name string `json:"name,omitempty"`
Nodes []GostNodeConfig `json:"nodes,omitempty"`
}
type GostNodeConfig struct {
Name string `json:"name,omitempty"`
Addr string `json:"addr"`
Connector GostConnectorConfig `json:"connector"`
Dialer GostDialerConfig `json:"dialer"`
}
type GostConnectorConfig struct {
Type string `json:"type"`
}
type GostDialerConfig struct {
Type string `json:"type"`
}
type GostServiceConfig struct {
Name string `json:"name"`
Addr string `json:"addr"`
Admission string `json:"admission,omitempty"`
Handler GostHandlerConfig `json:"handler"`
Listener GostListenerConfig `json:"listener"`
Recorders []GostRecorderConfig `json:"recorders,omitempty"`
}
type GostHandlerConfig struct {
Type string `json:"type"`
Chain string `json:"chain,omitempty"`
Auther string `json:"auther,omitempty"`
}
type GostListenerConfig struct {
Type string `json:"type"`
}
type GostRecorderConfig struct {
Name string `json:"name"`
Record string `json:"record"`
}
type GostAutherConfig struct {
Name string `json:"name"`
Auths []GostAuthConfig `json:"auths"`
}
type GostAuthConfig struct {
Username string `json:"username"`
Password string `json:"password"`
}
type GostAdmissionConfig struct {
Name string `json:"name"`
Whitelist bool `json:"whitelist"`
Matchers []string `json:"matchers"`
}
func (c *gostClient) GetChain(name string) (*GostChainConfig, error) {
body, err := c.get("/config/chains/" + url.PathEscape(name))
if err != nil {
return nil, err
}
if len(body) == 0 {
return &GostChainConfig{Name: name}, nil
}
var direct GostChainConfig
if err := json.Unmarshal(body, &direct); err == nil && direct.Name != "" {
return &direct, nil
}
var wrapper struct {
Data *GostChainConfig `json:"data"`
}
if err := json.Unmarshal(body, &wrapper); err == nil && wrapper.Data != nil && wrapper.Data.Name != "" {
return wrapper.Data, nil
}
return &GostChainConfig{Name: name}, nil
}
func (c *gostClient) ListChains() ([]*GostChainConfig, error) {
body, err := c.get("/config/chains")
if err != nil {
return nil, err
}
if len(body) == 0 {
return nil, nil
}
var resp struct {
Data struct {
Count int `json:"count"`
List []*GostChainConfig `json:"list"`
} `json:"data"`
}
if err := json.Unmarshal(body, &resp); err != nil {
return nil, fmt.Errorf("parse gost chain list failed %s: %w", string(body), err)
}
return resp.Data.List, nil
}
func (c *gostClient) CreateChain(chain *GostChainConfig) error {
return c.create("/config/chains", chain)
}
func (c *gostClient) DeleteChain(name string) error {
return c.delete("/config/chains/" + url.PathEscape(name))
}
func (c *gostClient) SaveConfig() error {
return c.create("/config", nil)
}
func (c *gostClient) CreateService(service *GostServiceConfig) error {
return c.create("/config/services", service)
}
func (c *gostClient) DeleteService(name string) error {
return c.delete("/config/services/" + url.PathEscape(name))
}
func (c *gostClient) CreateAuther(auther *GostAutherConfig) error {
return c.create("/config/authers", auther)
}
func (c *gostClient) DeleteAuther(name string) error {
return c.delete("/config/authers/" + url.PathEscape(name))
}
func (c *gostClient) CreateAdmission(admission *GostAdmissionConfig) error {
return c.create("/config/admissions", admission)
}
func (c *gostClient) DeleteAdmission(name string) error {
return c.delete("/config/admissions/" + url.PathEscape(name))
}
func (c *gostClient) create(path string, payload any) error {
_, err := c.request(http.MethodPost, path, payload)
return err
}
func (c *gostClient) get(path string) ([]byte, error) {
body, err := c.request(http.MethodGet, path, nil)
if err != nil {
return nil, err
}
return body, nil
}
func (c *gostClient) delete(path string) error {
_, err := c.request(http.MethodDelete, path, nil)
return err
}
func (c *gostClient) request(method string, path string, payload any) ([]byte, error) {
var bodyReader io.Reader
if payload != nil {
data, err := json.Marshal(payload)
if err != nil {
return nil, err
}
bodyReader = bytes.NewReader(data)
}
req, err := http.NewRequest(method, c.endpoint(path), bodyReader)
if err != nil {
return nil, err
}
req.SetBasicAuth(c.username, c.password)
if payload != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := core.Fetch(req)
if err != nil {
return nil, err
}
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode == http.StatusBadRequest {
return nil, fmt.Errorf("%w: %s", ErrGostNotFound, string(body))
}
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
return nil, fmt.Errorf("gost api %s %s failed: %d %s", method, path, resp.StatusCode, string(body))
}
return body, nil
}
func (c *gostClient) endpoint(path string) string {
return c.baseURL + c.pathPrefix + path
}
func normalizeGostPathPrefix(prefix string) string {
prefix = strings.TrimSpace(prefix)
if prefix == "" {
return ""
}
if !strings.HasPrefix(prefix, "/") {
prefix = "/" + prefix
}
return strings.TrimRight(prefix, "/")
}

107
web/globals/gost_test.go Normal file
View File

@@ -0,0 +1,107 @@
package globals
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func TestGostClientChainOperations(t *testing.T) {
var (
created *GostChainConfig
deleted []string
saved bool
)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
username, password, ok := r.BasicAuth()
if !ok || username != "user" || password != "pass" {
t.Errorf("unexpected auth: ok=%v username=%q password=%q", ok, username, password)
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
switch {
case r.Method == http.MethodGet && r.URL.Path == "/api/config/chains":
_ = json.NewEncoder(w).Encode(map[string]any{
"count": 2,
"list": []map[string]any{
{"name": "old-a"},
{"name": "old-b"},
},
})
case r.Method == http.MethodPost && r.URL.Path == "/api/config/chains":
if err := json.NewDecoder(r.Body).Decode(&created); err != nil {
t.Errorf("Decode chain failed: %v", err)
http.Error(w, "bad request", http.StatusBadRequest)
return
}
_, _ = w.Write([]byte(`{}`))
case r.Method == http.MethodDelete && r.URL.Path == "/api/config/chains/old-a":
deleted = append(deleted, "old-a")
_, _ = w.Write([]byte(`{}`))
case r.Method == http.MethodDelete && r.URL.Path == "/api/config/chains/old-b":
deleted = append(deleted, "old-b")
_, _ = w.Write([]byte(`{}`))
case r.Method == http.MethodPost && r.URL.Path == "/api/config":
saved = true
_, _ = w.Write([]byte(`{}`))
default:
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
http.NotFound(w, r)
}
}))
defer server.Close()
client := NewGost(server.URL, 9700, "/api", "user", "pass")
chains, err := client.ListChains()
if err != nil {
t.Fatalf("ListChains returned error: %v", err)
}
if len(chains) != 2 || chains[0].Name != "old-a" || chains[1].Name != "old-b" {
t.Fatalf("unexpected chains: %#v", chains)
}
if err := client.DeleteChain(chains[0].Name); err != nil {
t.Fatalf("DeleteChain old-a returned error: %v", err)
}
if err := client.DeleteChain(chains[1].Name); err != nil {
t.Fatalf("DeleteChain old-b returned error: %v", err)
}
if len(deleted) != 2 {
t.Fatalf("unexpected deleted chains: %#v", deleted)
}
err = client.CreateChain(&GostChainConfig{
Name: "edge-a",
Hops: []GostHopConfig{{
Nodes: []GostNodeConfig{{
Addr: "192.0.2.1:1080",
Connector: GostConnectorConfig{Type: "socks5"},
Dialer: GostDialerConfig{Type: "tcp"},
}},
}},
})
if err != nil {
t.Fatalf("CreateChain returned error: %v", err)
}
if created == nil || created.Name != "edge-a" {
t.Fatalf("unexpected created chain: %#v", created)
}
if len(created.Hops) != 1 || len(created.Hops[0].Nodes) != 1 {
t.Fatalf("unexpected created chain hops: %#v", created.Hops)
}
node := created.Hops[0].Nodes[0]
if node.Addr != "192.0.2.1:1080" || node.Connector.Type != "socks5" || node.Dialer.Type != "tcp" {
t.Fatalf("unexpected created node: %#v", node)
}
if err := client.SaveConfig(); err != nil {
t.Fatalf("SaveConfig returned error: %v", err)
}
if !saved {
t.Fatal("expected SaveConfig request")
}
}

29
web/handlers/area.go Normal file
View File

@@ -0,0 +1,29 @@
package handlers
import (
"platform/web/auth"
s "platform/web/services"
"github.com/gofiber/fiber/v2"
)
func ListArea(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitOfficialClient()
if err != nil {
return err
}
list, err := s.Area.ListAreas()
if err != nil {
return err
}
return c.JSON(list)
}
type ListAreaRespItem struct {
ID int32 `json:"id"`
Name string `json:"name"`
Level int `json:"level"`
ParentID *int32 `json:"parent_id,omitempty"`
}

170
web/handlers/article.go Normal file
View File

@@ -0,0 +1,170 @@
package handlers
import (
"platform/pkg/env"
"platform/web/auth"
"platform/web/core"
g "platform/web/globals"
s "platform/web/services"
"strings"
"github.com/gofiber/fiber/v2"
)
func NavArticle(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitOfficialClient()
if err != nil {
return err
}
list, err := s.Article.Nav()
if err != nil {
return err
}
return c.JSON(list)
}
func GetArticle(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitOfficialClient()
if err != nil {
return err
}
var req core.IdReq
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
article, err := s.Article.GetPublic(req.Id)
if err != nil {
return err
}
return c.JSON(article)
}
func PageArticleByAdmin(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeArticleRead)
if err != nil {
return err
}
var req s.PageArticleReq
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
list, total, err := s.Article.Page(&req)
if err != nil {
return err
}
return c.JSON(core.PageResp{
Total: int(total),
Page: req.GetPage(),
Size: req.GetSize(),
List: list,
})
}
func GetArticleByAdmin(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeArticleRead)
if err != nil {
return err
}
var req core.IdReq
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
article, err := s.Article.GetByAdmin(req.Id)
if err != nil {
return err
}
return c.JSON(article)
}
func CreateArticle(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeArticleWrite)
if err != nil {
return err
}
var req s.CreateArticleData
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
return s.Article.Create(req)
}
func UpdateArticle(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeArticleWrite)
if err != nil {
return err
}
var req s.UpdateArticleData
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
return s.Article.Update(req)
}
func DeleteArticle(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeArticleWrite)
if err != nil {
return err
}
var req core.IdReq
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
return s.Article.Delete(req.Id)
}
func UploadArticleImage(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeArticleWrite)
if err != nil {
return err
}
fileHeader, err := c.FormFile("file")
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "缺少上传文件 file")
}
result, err := s.Article.UploadImage(fileHeader, articleUploadBaseURL(c))
if err != nil {
return err
}
return c.JSON(result)
}
func articleUploadBaseURL(c *fiber.Ctx) string {
if env.UploadPublicBaseURL != "" {
return strings.TrimRight(env.UploadPublicBaseURL, "/")
}
scheme := c.Protocol()
if forwardedProto := c.Get("X-Forwarded-Proto"); forwardedProto != "" {
scheme = strings.TrimSpace(strings.Split(forwardedProto, ",")[0])
}
host := c.Get(fiber.HeaderHost)
if forwardedHost := c.Get("X-Forwarded-Host"); forwardedHost != "" {
host = strings.TrimSpace(strings.Split(forwardedHost, ",")[0])
}
if host == "" {
return ""
}
return scheme + "://" + host
}

View File

@@ -0,0 +1,90 @@
package handlers
import (
"platform/web/auth"
"platform/web/core"
g "platform/web/globals"
s "platform/web/services"
"github.com/gofiber/fiber/v2"
)
func PageArticleGroupByAdmin(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeArticleGroupRead)
if err != nil {
return err
}
var req s.PageArticleGroupReq
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
list, total, err := s.ArticleGroup.Page(&req)
if err != nil {
return err
}
return c.JSON(core.PageResp{
Total: int(total),
Page: req.GetPage(),
Size: req.GetSize(),
List: list,
})
}
func ListArticleGroupByAdmin(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeArticleGroupRead)
if err != nil {
return err
}
list, err := s.ArticleGroup.All()
if err != nil {
return err
}
return c.JSON(list)
}
func CreateArticleGroup(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeArticleGroupWrite)
if err != nil {
return err
}
var req s.CreateArticleGroupData
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
return s.ArticleGroup.Create(req)
}
func UpdateArticleGroup(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeArticleGroupWrite)
if err != nil {
return err
}
var req s.UpdateArticleGroupData
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
return s.ArticleGroup.Update(req)
}
func DeleteArticleGroup(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeArticleGroupWrite)
if err != nil {
return err
}
var req core.IdReq
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
return s.ArticleGroup.Delete(req.Id)
}

View File

@@ -1,7 +1,6 @@
package handlers package handlers
import ( import (
"platform/pkg/u"
"platform/web/auth" "platform/web/auth"
"platform/web/core" "platform/web/core"
g "platform/web/globals" g "platform/web/globals"
@@ -31,12 +30,10 @@ func PageBalanceActivity(c *fiber.Ctx) error {
do = do.Where(q.Bill.As("Bill").BillNo.Eq(*req.BillNo)) do = do.Where(q.Bill.As("Bill").BillNo.Eq(*req.BillNo))
} }
if req.CreatedAtStart != nil { if req.CreatedAtStart != nil {
t := u.DateHead(*req.CreatedAtStart) do = do.Where(q.BalanceActivity.CreatedAt.Gte(req.CreatedAtStart.UTC()))
do = do.Where(q.BalanceActivity.CreatedAt.Gte(t))
} }
if req.CreatedAtEnd != nil { if req.CreatedAtEnd != nil {
t := u.DateTail(*req.CreatedAtEnd) do = do.Where(q.BalanceActivity.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
do = do.Where(q.BalanceActivity.CreatedAt.Lte(t))
} }
// 查询余额变动列表 // 查询余额变动列表
@@ -93,16 +90,14 @@ func PageBalanceActivityByAdmin(c *fiber.Ctx) error {
do = do.Where(q.Bill.As("Bill").BillNo.Eq(*req.BillNo)) do = do.Where(q.Bill.As("Bill").BillNo.Eq(*req.BillNo))
} }
if req.CreatedAtStart != nil { if req.CreatedAtStart != nil {
t := u.DateHead(*req.CreatedAtStart) do = do.Where(q.BalanceActivity.CreatedAt.Gte(req.CreatedAtStart.UTC()))
do = do.Where(q.BalanceActivity.CreatedAt.Gte(t))
} }
if req.CreatedAtEnd != nil { if req.CreatedAtEnd != nil {
t := u.DateTail(*req.CreatedAtEnd) do = do.Where(q.BalanceActivity.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
do = do.Where(q.BalanceActivity.CreatedAt.Lte(t))
} }
// 查询余额变动列表 // 查询余额变动列表
list, total, err := q.BalanceActivity.Debug(). list, total, err := q.BalanceActivity.
Joins(q.BalanceActivity.User, q.BalanceActivity.Admin, q.BalanceActivity.Bill). Joins(q.BalanceActivity.User, q.BalanceActivity.Admin, q.BalanceActivity.Bill).
Select( Select(
q.BalanceActivity.ALL, q.BalanceActivity.ALL,
@@ -155,12 +150,10 @@ func PageBalanceActivityOfUserByAdmin(c *fiber.Ctx) error {
do = do.Where(q.Bill.As("Bill").BillNo.Eq(*req.BillNo)) do = do.Where(q.Bill.As("Bill").BillNo.Eq(*req.BillNo))
} }
if req.CreatedAtStart != nil { if req.CreatedAtStart != nil {
t := u.DateHead(*req.CreatedAtStart) do = do.Where(q.BalanceActivity.CreatedAt.Gte(req.CreatedAtStart.UTC()))
do = do.Where(q.BalanceActivity.CreatedAt.Gte(t))
} }
if req.CreatedAtEnd != nil { if req.CreatedAtEnd != nil {
t := u.DateTail(*req.CreatedAtEnd) do = do.Where(q.BalanceActivity.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
do = do.Where(q.BalanceActivity.CreatedAt.Lte(t))
} }
// 查询余额变动列表 // 查询余额变动列表

View File

@@ -1,7 +1,6 @@
package handlers package handlers
import ( import (
"platform/pkg/u"
"platform/web/auth" "platform/web/auth"
"platform/web/core" "platform/web/core"
c "platform/web/core" c "platform/web/core"
@@ -29,13 +28,18 @@ func PageBatch(ctx *fiber.Ctx) error {
// 查询批次 // 查询批次
conds := q.LogsUserUsage.Where(q.LogsUserUsage.UserID.Eq(authCtx.User.ID)) conds := q.LogsUserUsage.Where(q.LogsUserUsage.UserID.Eq(authCtx.User.ID))
if req.TimeStart != nil { if req.TimeStart != nil {
conds.Where(q.LogsUserUsage.Time.Gte(*req.TimeStart)) conds.Where(q.LogsUserUsage.Time.Gte(req.TimeStart.UTC()))
} }
if req.TimeEnd != nil { if req.TimeEnd != nil {
conds.Where(q.LogsUserUsage.Time.Lte(*req.TimeEnd)) conds.Where(q.LogsUserUsage.Time.Lte(req.TimeEnd.UTC()))
}
if req.ResourceNo != nil {
conds.Where(q.Resource.As("Resource").ResourceNo.Eq(*req.ResourceNo))
} }
list, total, err := q.LogsUserUsage.Where(conds). list, total, err := q.LogsUserUsage.
Joins(q.LogsUserUsage.Resource).
Where(conds).
Order(q.LogsUserUsage.Time.Desc()). Order(q.LogsUserUsage.Time.Desc()).
FindByPage(req.GetOffset(), req.GetLimit()) FindByPage(req.GetOffset(), req.GetLimit())
if err != nil { if err != nil {
@@ -53,6 +57,7 @@ func PageBatch(ctx *fiber.Ctx) error {
type PageResourceBatchReq struct { type PageResourceBatchReq struct {
c.PageReq c.PageReq
ResourceNo *string `json:"resource_no"`
TimeStart *time.Time `json:"time_start"` TimeStart *time.Time `json:"time_start"`
TimeEnd *time.Time `json:"time_end"` TimeEnd *time.Time `json:"time_end"`
} }
@@ -89,12 +94,10 @@ func PageBatchByAdmin(c *fiber.Ctx) error {
do = do.Where(q.LogsUserUsage.ISP.Eq(*req.Isp)) do = do.Where(q.LogsUserUsage.ISP.Eq(*req.Isp))
} }
if req.CreatedAtStart != nil { if req.CreatedAtStart != nil {
time := u.DateHead(*req.CreatedAtStart) do = do.Where(q.LogsUserUsage.Time.Gte(req.CreatedAtStart.UTC()))
do = do.Where(q.LogsUserUsage.Time.Gte(time))
} }
if req.CreatedAtEnd != nil { if req.CreatedAtEnd != nil {
time := u.DateTail(*req.CreatedAtEnd) do = do.Where(q.LogsUserUsage.Time.Lte(req.CreatedAtEnd.UTC()))
do = do.Where(q.LogsUserUsage.Time.Lte(time))
} }
list, total, err := q.LogsUserUsage. list, total, err := q.LogsUserUsage.
@@ -104,6 +107,7 @@ func PageBatchByAdmin(c *fiber.Ctx) error {
q.User.As("User").Phone.As("User__phone"), q.User.As("User").Phone.As("User__phone"),
q.User.As("User").Name.As("User__name"), q.User.As("User").Name.As("User__name"),
q.Resource.As("Resource").ResourceNo.As("Resource__resource_no"), q.Resource.As("Resource").ResourceNo.As("Resource__resource_no"),
q.Resource.As("Resource").Type.As("Resource__type"),
). ).
Where(do). Where(do).
Order(q.LogsUserUsage.Time.Desc()). Order(q.LogsUserUsage.Time.Desc()).
@@ -158,12 +162,10 @@ func PageBatchOfUserByAdmin(ctx *fiber.Ctx) error {
do = do.Where(q.LogsUserUsage.ISP.Eq(*req.Isp)) do = do.Where(q.LogsUserUsage.ISP.Eq(*req.Isp))
} }
if req.CreatedAtStart != nil { if req.CreatedAtStart != nil {
t := u.DateHead(*req.CreatedAtStart) do = do.Where(q.LogsUserUsage.Time.Gte(req.CreatedAtStart.UTC()))
do = do.Where(q.LogsUserUsage.Time.Gte(t))
} }
if req.CreatedAtEnd != nil { if req.CreatedAtEnd != nil {
t := u.DateTail(*req.CreatedAtEnd) do = do.Where(q.LogsUserUsage.Time.Lte(req.CreatedAtEnd.UTC()))
do = do.Where(q.LogsUserUsage.Time.Lte(t))
} }
list, total, err := q.LogsUserUsage. list, total, err := q.LogsUserUsage.
@@ -173,6 +175,7 @@ func PageBatchOfUserByAdmin(ctx *fiber.Ctx) error {
q.User.As("User").Phone.As("User__phone"), q.User.As("User").Phone.As("User__phone"),
q.User.As("User").Name.As("User__name"), q.User.As("User").Name.As("User__name"),
q.Resource.As("Resource").ResourceNo.As("Resource__resource_no"), q.Resource.As("Resource").ResourceNo.As("Resource__resource_no"),
q.Resource.As("Resource").Type.As("Resource__type"),
). ).
Where(do). Where(do).
Order(q.LogsUserUsage.Time.Desc()). Order(q.LogsUserUsage.Time.Desc()).

View File

@@ -1,7 +1,6 @@
package handlers package handlers
import ( import (
"platform/pkg/u"
"platform/web/auth" "platform/web/auth"
"platform/web/core" "platform/web/core"
g "platform/web/globals" g "platform/web/globals"
@@ -40,12 +39,10 @@ func PageBillByAdmin(c *fiber.Ctx) error {
do = do.Where(q.Bill.BillNo.Eq(*req.BillNo)) do = do.Where(q.Bill.BillNo.Eq(*req.BillNo))
} }
if req.CreatedAtStart != nil { if req.CreatedAtStart != nil {
time := u.DateHead(*req.CreatedAtStart) do = do.Where(q.Bill.CreatedAt.Gte(req.CreatedAtStart.UTC()))
do = do.Where(q.Bill.CreatedAt.Gte(time))
} }
if req.CreatedAtEnd != nil { if req.CreatedAtEnd != nil {
time := u.DateHead(*req.CreatedAtEnd) do = do.Where(q.Bill.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
do = do.Where(q.Bill.CreatedAt.Lte(time))
} }
if req.ProductCode != nil { if req.ProductCode != nil {
do = do.Where(q.Resource.As("Resource").Code.Eq(*req.ProductCode)) do = do.Where(q.Resource.As("Resource").Code.Eq(*req.ProductCode))
@@ -72,6 +69,7 @@ func PageBillByAdmin(c *fiber.Ctx) error {
q.Trade.As("Trade").InnerNo.As("Trade__inner_no"), q.Trade.As("Trade").InnerNo.As("Trade__inner_no"),
q.Trade.As("Trade").Acquirer.As("Trade__acquirer"), q.Trade.As("Trade").Acquirer.As("Trade__acquirer"),
q.Resource.As("Resource").ResourceNo.As("Resource__resource_no"), q.Resource.As("Resource").ResourceNo.As("Resource__resource_no"),
q.Resource.As("Resource").Type.As("Resource__type"),
). ).
Where(do). Where(do).
Order(q.Bill.CreatedAt.Desc()). Order(q.Bill.CreatedAt.Desc()).
@@ -127,12 +125,10 @@ func PageBillOfUserByAdmin(c *fiber.Ctx) error {
do = do.Where(q.Bill.BillNo.Eq(*req.BillNo)) do = do.Where(q.Bill.BillNo.Eq(*req.BillNo))
} }
if req.CreatedAtStart != nil { if req.CreatedAtStart != nil {
time := u.DateHead(*req.CreatedAtStart) do = do.Where(q.Bill.CreatedAt.Gte(req.CreatedAtStart.UTC()))
do = do.Where(q.Bill.CreatedAt.Gte(time))
} }
if req.CreatedAtEnd != nil { if req.CreatedAtEnd != nil {
time := u.DateHead(*req.CreatedAtEnd) do = do.Where(q.Bill.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
do = do.Where(q.Bill.CreatedAt.Lte(time))
} }
if req.ProductCode != nil { if req.ProductCode != nil {
do = do.Where(q.Resource.As("Resource").Code.Eq(*req.ProductCode)) do = do.Where(q.Resource.As("Resource").Code.Eq(*req.ProductCode))
@@ -156,6 +152,7 @@ func PageBillOfUserByAdmin(c *fiber.Ctx) error {
q.Trade.As("Trade").InnerNo.As("Trade__inner_no"), q.Trade.As("Trade").InnerNo.As("Trade__inner_no"),
q.Trade.As("Trade").Acquirer.As("Trade__acquirer"), q.Trade.As("Trade").Acquirer.As("Trade__acquirer"),
q.Resource.As("Resource").ResourceNo.As("Resource__resource_no"), q.Resource.As("Resource").ResourceNo.As("Resource__resource_no"),
q.Resource.As("Resource").Type.As("Resource__type"),
). ).
Where(do). Where(do).
Order(q.Bill.CreatedAt.Desc()). Order(q.Bill.CreatedAt.Desc()).
@@ -207,10 +204,10 @@ func ListBill(c *fiber.Ctx) error {
do.Where(q.Bill.Type.Eq(int(*req.Type))) do.Where(q.Bill.Type.Eq(int(*req.Type)))
} }
if req.CreateAfter != nil { if req.CreateAfter != nil {
do.Where(q.Bill.CreatedAt.Gte(*req.CreateAfter)) do = do.Where(q.Bill.CreatedAt.Gte(req.CreateAfter.UTC()))
} }
if req.CreateBefore != nil { if req.CreateBefore != nil {
do.Where(q.Bill.CreatedAt.Lte(*req.CreateBefore)) do = do.Where(q.Bill.CreatedAt.Lte(req.CreateBefore.UTC()))
} }
if req.BillNo != nil && *req.BillNo != "" { if req.BillNo != nil && *req.BillNo != "" {
do.Where(q.Bill.BillNo.Eq(*req.BillNo)) do.Where(q.Bill.BillNo.Eq(*req.BillNo))

View File

@@ -15,90 +15,6 @@ import (
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
// PageChannelByAdmin 分页查询所有通道
func PageChannelByAdmin(c *fiber.Ctx) error {
// 检查权限
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeChannelRead)
if err != nil {
return err
}
// 解析请求参数
var req PageChannelsByAdminReq
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
// 构建查询条件
do := q.Channel.Where()
if req.UserPhone != nil {
do = do.Where(q.User.As("User").Phone.Eq(*req.UserPhone))
}
if req.ResourceNo != nil {
do = do.Where(q.Resource.As("Resource").ResourceNo.Eq(*req.ResourceNo))
}
if req.BatchNo != nil {
do = do.Where(q.Channel.BatchNo.Eq(*req.BatchNo))
}
if req.ProxyHost != nil {
do = do.Where(q.Channel.Host.Eq(*req.ProxyHost))
}
if req.ProxyPort != nil {
do = do.Where(q.Channel.Port.Eq(*req.ProxyPort))
}
if req.NodeIP != nil {
ip, err := orm.ParseInet(*req.NodeIP)
if err != nil {
return core.NewBizErr("查询参数 ip 格式不正确")
}
do = do.Where(q.Channel.IP.Eq(ip))
}
if req.ExpiredAtStart != nil {
time := u.DateHead(*req.ExpiredAtStart)
do = do.Where(q.Channel.ExpiredAt.Gte(time))
}
if req.ExpiredAtEnd != nil {
time := u.DateHead(*req.ExpiredAtEnd)
do = do.Where(q.Channel.ExpiredAt.Lte(time))
}
// 查询通道列表
list, total, err := q.Channel.
Joins(q.Channel.User, q.Channel.Resource).
Select(
q.Channel.ALL,
q.Resource.As("Resource").ResourceNo.As("Resource__resource_no"),
q.User.As("User").Phone.As("User__phone"),
q.User.As("User").Name.As("User__name"),
).
Where(do).
Order(q.Channel.CreatedAt.Desc()).
FindByPage(req.GetOffset(), req.GetLimit())
if err != nil {
return err
}
// 返回结果
return c.JSON(core.PageResp{
List: list,
Total: int(total),
Page: req.GetPage(),
Size: req.GetSize(),
})
}
type PageChannelsByAdminReq struct {
core.PageReq
UserPhone *string `json:"user_phone"`
ResourceNo *string `json:"resource_no"`
BatchNo *string `json:"batch_no"`
ProxyHost *string `json:"proxy_host"`
ProxyPort *uint16 `json:"proxy_port"`
NodeIP *string `json:"node_ip" validator:"omitempty,ip"`
ExpiredAtStart *time.Time `json:"expired_at_start"`
ExpiredAtEnd *time.Time `json:"expired_at_end"`
}
// ListChannel 分页查询当前用户通道 // ListChannel 分页查询当前用户通道
func ListChannel(c *fiber.Ctx) error { func ListChannel(c *fiber.Ctx) error {
// 检查权限 // 检查权限
@@ -126,10 +42,10 @@ func ListChannel(c *fiber.Ctx) error {
} }
if req.ExpireAfter != nil { if req.ExpireAfter != nil {
cond.Where(q.Channel.ExpiredAt.Gte(*req.ExpireAfter)) cond = cond.Where(q.Channel.ExpiredAt.Gte(req.ExpireAfter.UTC()))
} }
if req.ExpireBefore != nil { if req.ExpireBefore != nil {
cond.Where(q.Channel.ExpiredAt.Lte(*req.ExpireBefore)) cond = cond.Where(q.Channel.ExpiredAt.Lte(req.ExpireBefore.UTC()))
} }
// 查询数据 // 查询数据
@@ -186,41 +102,32 @@ func CreateChannel(c *fiber.Ctx) error {
} }
// 创建通道 // 创建通道
var isp *m.EdgeISP no, err := s.FindResourceNoById(req.ResourceId)
if err != nil {
return err
}
areaID, err := s.Area.FindIdByFilter(req.Prov, req.City)
if err != nil {
return err
}
filter := &s.EdgeFilter{AreaID: areaID}
if req.Isp != nil { if req.Isp != nil {
isp = u.X(m.ToEdgeISP(*req.Isp)) filter.Isp = u.X(m.ToEdgeISP(*req.Isp))
} }
result, err := s.Channel.CreateChannels( result, err := s.Channel.CreateChannels(
ip, ip,
req.ResourceId, no,
req.AuthType == s.ChannelAuthTypeIp, req.AuthType == s.ChannelAuthTypeIp,
req.AuthType == s.ChannelAuthTypePass, req.AuthType == s.ChannelAuthTypePass,
req.Count, req.Count,
&s.EdgeFilter{ filter,
Isp: isp,
Prov: req.Prov,
City: req.City,
},
) )
if err != nil { if err != nil {
return err return err
} }
// 返回结果 // 返回结果
var resp = make([]*CreateChannelRespItem, len(result)) return c.JSON(buildCreateChannelResp(result, req.Protocol, req.AuthType))
for i, channel := range result {
resp[i] = &CreateChannelRespItem{
Proto: req.Protocol,
Host: channel.Host,
IP: channel.Proxy.IP.String(),
Port: channel.Port,
}
if req.AuthType == s.ChannelAuthTypePass {
resp[i].Username = channel.Username
resp[i].Password = channel.Password
}
}
return c.JSON(resp)
} }
type CreateChannelReq struct { type CreateChannelReq struct {
@@ -233,6 +140,96 @@ type CreateChannelReq struct {
Isp *int `json:"isp"` Isp *int `json:"isp"`
} }
// CreateChannelV2 创建新通道 v2使用 resource_no 替代 resource_id
func CreateChannelV2(c *fiber.Ctx) error {
// 不检查权限,允许 api 调用
// 解析参数
req := new(CreateChannelReqV2)
if err := g.Validator.ParseBody(c, req); err != nil {
return core.NewBizErr("解析参数失败", err)
}
ip, err := netip.ParseAddr(c.IP())
if err != nil {
return core.NewBizErr("获取客户端地址失败", err)
}
// 创建通道
areaID, err := s.Area.FindIdByFilter(req.Prov, req.City)
if err != nil {
return err
}
filter := &s.EdgeFilter{AreaID: areaID}
if req.Isp != nil {
filter.Isp = u.X(m.ToEdgeISP(*req.Isp))
}
result, err := s.Channel.CreateChannels(
ip,
req.ResourceNo,
req.AuthType == s.ChannelAuthTypeIp,
req.AuthType == s.ChannelAuthTypePass,
req.Count,
filter,
)
if err != nil {
return err
}
// 返回结果
return c.JSON(buildCreateChannelResp(result, req.Protocol, req.AuthType))
}
type CreateChannelReqV2 struct {
ResourceNo string `json:"resource_no" validate:"required"`
AuthType s.ChannelAuthType `json:"auth_type" validate:"required"`
Protocol int `json:"protocol" validate:"required"`
Count int `json:"count" validate:"required"`
Prov *string `json:"prov"`
City *string `json:"city"`
Isp *int `json:"isp"`
}
// CreateChannelV3 创建新通道 v3使用 resource_no + area_id
func CreateChannelV3(c *fiber.Ctx) error {
req := new(CreateChannelReqV3)
if err := g.Validator.ParseBody(c, req); err != nil {
return core.NewBizErr("解析参数失败", err)
}
ip, err := netip.ParseAddr(c.IP())
if err != nil {
return core.NewBizErr("获取客户端地址失败", err)
}
filter := &s.EdgeFilter{AreaID: req.AreaID}
if req.Isp != nil {
filter.Isp = u.X(m.ToEdgeISP(*req.Isp))
}
result, err := s.Channel.CreateChannels(
ip,
req.ResourceNo,
req.AuthType == s.ChannelAuthTypeIp,
req.AuthType == s.ChannelAuthTypePass,
req.Count,
filter,
)
if err != nil {
return err
}
return c.JSON(buildCreateChannelResp(result, req.Protocol, req.AuthType))
}
type CreateChannelReqV3 struct {
ResourceNo string `json:"resource_no" validate:"required"`
AuthType s.ChannelAuthType `json:"auth_type" validate:"required"`
Protocol int `json:"protocol" validate:"required"`
Count int `json:"count" validate:"required"`
AreaID *int32 `json:"area_id"`
Isp *int `json:"isp"`
}
type CreateChannelRespItem struct { type CreateChannelRespItem struct {
Proto int `json:"-"` Proto int `json:"-"`
Host string `json:"host"` Host string `json:"host"`
@@ -269,6 +266,97 @@ type RemoveChannelsReq struct {
Batch string `json:"batch" validate:"required"` Batch string `json:"batch" validate:"required"`
} }
// PageChannelByAdmin 分页查询所有通道
func PageChannelByAdmin(c *fiber.Ctx) error {
// 检查权限
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeChannelRead)
if err != nil {
return err
}
// 解析请求参数
var req PageChannelsByAdminReq
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
// 构建查询条件
do := q.Channel.Where()
if req.UserPhone != nil {
do = do.Where(q.User.As("User").Phone.Eq(*req.UserPhone))
}
if req.ResourceNo != nil {
do = do.Where(q.Resource.As("Resource").ResourceNo.Eq(*req.ResourceNo))
}
if req.BatchNo != nil {
do = do.Where(q.Channel.BatchNo.Eq(*req.BatchNo))
}
if req.ProxyHost != nil {
do = do.Where(q.Channel.Host.Eq(*req.ProxyHost))
}
if req.ProxyPort != nil {
do = do.Where(q.Channel.Port.Eq(*req.ProxyPort))
}
if req.NodeIP != nil {
ip, err := orm.ParseInet(*req.NodeIP)
if err != nil {
return core.NewBizErr("查询参数 ip 格式不正确")
}
do = do.Where(q.Channel.IP.Eq(ip))
}
if req.ExpiredAtStart != nil {
do = do.Where(q.Channel.ExpiredAt.Gte(req.ExpiredAtStart.UTC()))
}
if req.ExpiredAtEnd != nil {
do = do.Where(q.Channel.ExpiredAt.Lte(req.ExpiredAtEnd.UTC()))
}
if req.Expired != nil {
if *req.Expired {
do = do.Where(q.Channel.ExpiredAt.Lte(time.Now().UTC()))
} else {
do = do.Where(q.Channel.ExpiredAt.Gt(time.Now().UTC()))
}
}
// 查询通道列表
list, total, err := q.Channel.
Joins(q.Channel.User, q.Channel.Resource).
Select(
q.Channel.ALL,
q.Resource.As("Resource").ResourceNo.As("Resource__resource_no"),
q.Resource.As("Resource").Type.As("Resource__type"),
q.User.As("User").Phone.As("User__phone"),
q.User.As("User").Name.As("User__name"),
).
Where(do).
Order(q.Channel.CreatedAt.Desc()).
FindByPage(req.GetOffset(), req.GetLimit())
if err != nil {
return err
}
// 返回结果
return c.JSON(core.PageResp{
List: list,
Total: int(total),
Page: req.GetPage(),
Size: req.GetSize(),
})
}
type PageChannelsByAdminReq struct {
core.PageReq
UserPhone *string `json:"user_phone"`
ResourceNo *string `json:"resource_no"`
BatchNo *string `json:"batch_no"`
ProxyHost *string `json:"proxy_host"`
ProxyPort *uint16 `json:"proxy_port"`
NodeIP *string `json:"node_ip" validator:"omitempty,ip"`
ExpiredAtStart *time.Time `json:"expired_at_start"`
ExpiredAtEnd *time.Time `json:"expired_at_end"`
Expired *bool `json:"expired"`
}
// PageChannelOfUserByAdmin 分页查询指定用户的通道 // PageChannelOfUserByAdmin 分页查询指定用户的通道
func PageChannelOfUserByAdmin(c *fiber.Ctx) error { func PageChannelOfUserByAdmin(c *fiber.Ctx) error {
// 检查权限 // 检查权限
@@ -298,12 +386,10 @@ func PageChannelOfUserByAdmin(c *fiber.Ctx) error {
do = do.Where(q.Channel.Port.Eq(*req.ProxyPort)) do = do.Where(q.Channel.Port.Eq(*req.ProxyPort))
} }
if req.ExpiredAtStart != nil { if req.ExpiredAtStart != nil {
t := u.DateHead(*req.ExpiredAtStart) do = do.Where(q.Channel.ExpiredAt.Gte(req.ExpiredAtStart.UTC()))
do = do.Where(q.Channel.ExpiredAt.Gte(t))
} }
if req.ExpiredAtEnd != nil { if req.ExpiredAtEnd != nil {
t := u.DateHead(*req.ExpiredAtEnd) do = do.Where(q.Channel.ExpiredAt.Lte(req.ExpiredAtEnd.UTC()))
do = do.Where(q.Channel.ExpiredAt.Lte(t))
} }
// 查询通道列表 // 查询通道列表
@@ -312,6 +398,7 @@ func PageChannelOfUserByAdmin(c *fiber.Ctx) error {
Select( Select(
q.Channel.ALL, q.Channel.ALL,
q.Resource.As("Resource").ResourceNo.As("Resource__resource_no"), q.Resource.As("Resource").ResourceNo.As("Resource__resource_no"),
q.Resource.As("Resource").Type.As("Resource__type"),
q.User.As("User").Phone.As("User__phone"), q.User.As("User").Phone.As("User__phone"),
q.User.As("User").Name.As("User__name"), q.User.As("User").Name.As("User__name"),
). ).
@@ -370,3 +457,20 @@ type SyncChannelClearExpiredByAdminReq struct {
type SyncChannelClearExpiredByAdminResp struct { type SyncChannelClearExpiredByAdminResp struct {
Count int `json:"count"` Count int `json:"count"`
} }
func buildCreateChannelResp(result []*m.Channel, protocol int, authType s.ChannelAuthType) []*CreateChannelRespItem {
resp := make([]*CreateChannelRespItem, len(result))
for i, channel := range result {
resp[i] = &CreateChannelRespItem{
Proto: protocol,
Host: channel.Host,
IP: channel.Proxy.IP.String(),
Port: channel.Port,
}
if authType == s.ChannelAuthTypePass {
resp[i].Username = channel.Username
resp[i].Password = channel.Password
}
}
return resp
}

View File

@@ -2,7 +2,6 @@ package handlers
import ( import (
"errors" "errors"
"platform/pkg/u"
"platform/web/auth" "platform/web/auth"
"platform/web/core" "platform/web/core"
g "platform/web/globals" g "platform/web/globals"
@@ -276,28 +275,28 @@ func couponUserPageConditions(req CouponUserPageFilter) []gen.Condition {
} }
if req.Expired != nil { if req.Expired != nil {
if *req.Expired { if *req.Expired {
conds = append(conds, q.CouponUser.ExpireAt.IsNotNull(), q.CouponUser.ExpireAt.Lte(time.Now())) conds = append(conds, q.CouponUser.ExpireAt.IsNotNull(), q.CouponUser.ExpireAt.Lte(time.Now().UTC()))
} else { } else {
conds = append(conds, q.CouponUser.Where(q.CouponUser.ExpireAt.IsNull()).Or(q.CouponUser.ExpireAt.Gt(time.Now()))) conds = append(conds, q.CouponUser.Where(q.CouponUser.ExpireAt.IsNull()).Or(q.CouponUser.ExpireAt.Gt(time.Now().UTC())))
} }
} }
if req.CreatedAtStart != nil { if req.CreatedAtStart != nil {
conds = append(conds, q.CouponUser.CreatedAt.Gte(u.DateHead(*req.CreatedAtStart))) conds = append(conds, q.CouponUser.CreatedAt.Gte(req.CreatedAtStart.UTC()))
} }
if req.CreatedAtEnd != nil { if req.CreatedAtEnd != nil {
conds = append(conds, q.CouponUser.CreatedAt.Lte(u.DateTail(*req.CreatedAtEnd))) conds = append(conds, q.CouponUser.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
} }
if req.ExpireAtStart != nil { if req.ExpireAtStart != nil {
conds = append(conds, q.CouponUser.ExpireAt.Gte(u.DateHead(*req.ExpireAtStart))) conds = append(conds, q.CouponUser.ExpireAt.Gte(req.ExpireAtStart.UTC()))
} }
if req.ExpireAtEnd != nil { if req.ExpireAtEnd != nil {
conds = append(conds, q.CouponUser.ExpireAt.Lte(u.DateTail(*req.ExpireAtEnd))) conds = append(conds, q.CouponUser.ExpireAt.Lte(req.ExpireAtEnd.UTC()))
} }
if req.UsedAtStart != nil { if req.UsedAtStart != nil {
conds = append(conds, q.CouponUser.UsedAt.Gte(u.DateHead(*req.UsedAtStart))) conds = append(conds, q.CouponUser.UsedAt.Gte(req.UsedAtStart.UTC()))
} }
if req.UsedAtEnd != nil { if req.UsedAtEnd != nil {
conds = append(conds, q.CouponUser.UsedAt.Lte(u.DateTail(*req.UsedAtEnd))) conds = append(conds, q.CouponUser.UsedAt.Lte(req.UsedAtEnd.UTC()))
} }
return conds return conds
} }

View File

@@ -1,149 +0,0 @@
package handlers
import (
s "platform/web/services"
"github.com/gofiber/fiber/v2"
)
type RegisterEdgeReq struct {
Name string `json:"name" validate:"required"`
Version int `json:"version" validate:"required"`
}
type RegisterEdgeResp struct {
Id int32 `json:"id"`
Host string `json:"host"`
}
func AssignEdge(c *fiber.Ctx) (err error) {
return c.JSON(map[string]any{
"error": "接口暂不可用",
})
// // 验证请求参数
// var req = new(RegisterEdgeReq)
// err = g.Validator.Validate(c, req)
// if err != nil {
// return err
// }
// // 全局锁,防止并发注册
// var mutex = g.Redsync.NewMutex("edge:discovery")
// if err := mutex.Lock(); err != nil {
// return errors.New("服务繁忙,请稍后重试")
// }
// defer func() {
// if ok, err := mutex.Unlock(); err != nil {
// slog.Error("解锁失败", slog.Bool("ok", ok), slog.Any("err", err))
// }
// }()
// // 检查节点
// var fwd *m.Proxy
// var edge *m.Edge
// edge, err = q.Edge.
// Where(q.Edge.Mac.Eq(req.Name)).
// Take()
// if errors.Is(err, gorm.ErrRecordNotFound) {
// // 挑选合适的转发服务
// fwd, err = q.Proxy.
// LeftJoin(q.Edge, q.Edge.ProxyID.EqCol(q.Proxy.ID), q.Edge.Status.Eq(1)).
// Select(q.Proxy.ALL, q.Edge.ALL.Count().As("count")).
// Where(q.Proxy.Type.Eq(int32(proxy2.TypeSelfHosted))).
// Group(q.Proxy.ID).
// Order(field.NewField("", "count").Desc()).
// First()
// if err != nil {
// return err
// }
// // 保存节点信息
// edge = &m.Edge{
// Name: req.Name,
// Version: int32(req.Version),
// Type: int32(edge2.TypeSelfHosted),
// ProxyID: &fwd.ID,
// }
// err = q.Edge.Create(edge)
// if err != nil {
// return err
// }
// } else if err == nil {
// // 获取已配置的转发服务
// fwd, err = q.Proxy.
// Where(q.Proxy.ID.Eq(*edge.ProxyID)).
// Take()
// if err != nil {
// return err
// }
// // 节点已存在,更新版本号
// if edge.Version < int32(req.Version) {
// _, err = q.Edge.
// Where(q.Edge.ID.Eq(edge.ID)).
// UpdateSimple(q.Edge.Version.Value(int32(req.Version)))
// if err != nil {
// return err
// }
// }
// } else {
// return err
// }
// // 返回服务地址
// return c.JSON(RegisterEdgeResp{
// Id: edge.ID,
// Host: fwd.Host,
// })
}
type AllEdgesAvailableReq struct {
s.EdgeFilter
Count int `json:"count"`
}
type AllEdgesAvailableRespItem struct {
Ip string `json:"ip"` // 节点地址
Port int32 `json:"port"` // 代理服务端口
Isp string `json:"isp"` // 运营商
Prov string `json:"prov"` // 省份
City string `json:"city"` // 城市
Status int32 `json:"status"` // 节点状态0-离线1-正常
}
func AllEdgesAvailable(c *fiber.Ctx) (err error) {
return c.JSON(map[string]any{
"error": "接口暂不可用",
})
// // 检查权限
// _, err = auth.GetAuthCtx(c).PermitSecretClient()
// if err != nil {
// return err
// }
// // 验证请求参数
// var req = new(AllEdgesAvailableReq)
// err = g.Validator.Validate(c, req)
// if err != nil {
// return err
// }
// // 获取可用的转发服务
// infos, err := s.Edge.AllEdges(req.Count, req.EdgeFilter)
// if err != nil {
// return err
// }
// // 返回结果
// var edges = make([]AllEdgesAvailableRespItem, len(infos))
// for i, info := range infos {
// edges[i] = AllEdgesAvailableRespItem{
// Ip: info.Host,
// Port: u.Z(info.ProxyPort),
// Isp: edge2.ISP(info.Isp).String(),
// Prov: info.Prov,
// City: info.City,
// Status: info.Status,
// }
// }
// return c.JSON(edges)
}

View File

@@ -142,7 +142,7 @@ func IdentifyCallbackNew(c *fiber.Ctx) error {
} }
// 更新用户实名认证状态 // 更新用户实名认证状态
_, err = q.User. r, err := q.User.
Where(q.User.ID.Eq(info.Uid)). Where(q.User.ID.Eq(info.Uid)).
UpdateSimple( UpdateSimple(
q.User.IDType.Value(info.Type), q.User.IDType.Value(info.Type),
@@ -153,6 +153,9 @@ func IdentifyCallbackNew(c *fiber.Ctx) error {
if err != nil { if err != nil {
return renderIdenResult(c, false, "保存实名认证信息失败,请联系客服处理") return renderIdenResult(c, false, "保存实名认证信息失败,请联系客服处理")
} }
if r.RowsAffected == 0 {
return renderIdenResult(c, false, "用户状态已失效")
}
// 返回结果页面 // 返回结果页面
return renderIdenResult(c, true, "实名认证成功,请在扫码页面点击按钮完成认证") return renderIdenResult(c, true, "实名认证成功,请在扫码页面点击按钮完成认证")
@@ -172,7 +175,7 @@ func DebugIdentifyClear(c *fiber.Ctx) error {
return core.NewServErr("需要提供手机号") return core.NewServErr("需要提供手机号")
} }
_, err := q.User. r, err := q.User.
Where( Where(
q.User.Phone.Eq(phone), q.User.Phone.Eq(phone),
). ).
@@ -184,6 +187,9 @@ func DebugIdentifyClear(c *fiber.Ctx) error {
if err != nil { if err != nil {
return core.NewServErr("清除实名认证失败") return core.NewServErr("清除实名认证失败")
} }
if r.RowsAffected == 0 {
return core.NewServErr("用户状态已失效")
}
return c.SendString("实名信息已清除") return c.SendString("实名信息已清除")
} }

View File

@@ -5,11 +5,14 @@ import (
"platform/web/core" "platform/web/core"
g "platform/web/globals" g "platform/web/globals"
s "platform/web/services" s "platform/web/services"
"time"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
// ====================
// admin 路由
// ====================
func PageProxyByAdmin(c *fiber.Ctx) error { func PageProxyByAdmin(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyRead) _, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyRead)
if err != nil { if err != nil {
@@ -102,6 +105,42 @@ func UpdateProxyStatus(c *fiber.Ctx) error {
return c.JSON(nil) return c.JSON(nil)
} }
func SyncProxyPorts(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyWrite)
if err != nil {
return err
}
var req core.IdReq
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
if err := s.Proxy.SyncPorts(req.Id); err != nil {
return err
}
return c.JSON(nil)
}
func SyncProxyChains(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyWrite)
if err != nil {
return err
}
var req core.IdReq
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
if err := s.Proxy.SyncChains(req.Id); err != nil {
return err
}
return c.JSON(nil)
}
func RemoveProxy(c *fiber.Ctx) error { func RemoveProxy(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyWrite) _, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyWrite)
if err != nil { if err != nil {
@@ -119,347 +158,3 @@ func RemoveProxy(c *fiber.Ctx) error {
return c.JSON(nil) return c.JSON(nil)
} }
// ====================
// region 报告上线
func ProxyReportOnline(c *fiber.Ctx) (err error) {
return c.JSON(map[string]any{
"error": "接口暂不可用",
})
// // 检查接口权限
// _, err = auth2.GetAuthCtx(c).PermitSecretClient()
// if err != nil {
// return err
// }
// // 验证请求参数
// var req = new(ProxyReportOnlineReq)
// err = g.Validator.Validate(c, req)
// if err != nil {
// return err
// }
// // 创建代理
// var ip = c.Context().RemoteIP()
// var secretBytes = make([]byte, 16)
// if _, err := rand.Read(secretBytes); err != nil {
// return err
// }
// var secret = base32.StdEncoding.
// WithPadding(base32.NoPadding).
// EncodeToString(secretBytes)
// slog.Debug("生成随机密钥", "ip", ip, "secret", secret)
// var proxy = &m.Proxy{
// Mac: req.Name,
// Version: int32(req.Version),
// Type: m.ProxyTypeSelfHosted,
// IP: ip,
// Secret: &secret,
// Status: 1,
// }
// err = q.Proxy.
// Clauses(clause.OnConflict{
// UpdateAll: true,
// Columns: []clause.Column{
// {Name: q.Proxy.Mac.ColumnName().String()},
// },
// }).
// Create(proxy)
// if err != nil {
// return err
// }
// // 获取边缘节点信息
// data, err := q.Edge.Where(
// q.Edge.ProxyID.Eq(proxy.ID),
// ).Find()
// if err != nil {
// return err
// }
// edges := make([]*ProxyEdge, len(data))
// for i, edge := range data {
// edges[i] = &ProxyEdge{
// Id: edge.ID,
// Port: edge.ProxyPort,
// Prov: &edge.Prov,
// City: &edge.City,
// Isp: u.P(edge2.ISP(edge.Isp).String()),
// Status: &edge.Status,
// Loss: edge.Loss,
// Rtt: edge.Rtt,
// }
// }
// // 获取许可配置
// channels, err := q.Channel.Where(
// q.Channel.ProxyID.Eq(proxy.ID),
// q.Channel.Expiration.Gt(orm.LocalDateTime(time.Now())),
// ).Find()
// if err != nil {
// return err
// }
// var permits = make([]*ProxyPermit, len(channels))
// for i, channel := range channels {
// if channel.EdgeID == nil {
// return core.NewBizErr(fmt.Sprintf("权限解析异常通道缺少边缘节点ID %d", channel.ID))
// }
// permits[i] = &ProxyPermit{
// Id: *channel.EdgeID,
// Expire: time.Time(channel.Expiration),
// Whitelists: u.P(strings.Split(u.Z(channel.Whitelists), ",")),
// Username: channel.Username,
// Password: channel.Password,
// }
// }
// slog.Debug("注册转发服务", "ip", ip, "id", proxy.ID)
// return c.JSON(&ProxyReportOnlineResp{
// Id: proxy.ID,
// Secret: secret,
// Edges: edges,
// Permits: permits,
// })
}
type ProxyReportOnlineReq struct {
Name string `json:"name" validate:"required"`
Version int `json:"version" validate:"required"`
}
type ProxyReportOnlineResp struct {
Id int32 `json:"id"`
Secret string `json:"secret"`
Permits []*ProxyPermit `json:"permits"`
Edges []*ProxyEdge `json:"edges"`
}
// region 报告下线
func ProxyReportOffline(c *fiber.Ctx) (err error) {
return c.JSON(map[string]any{
"error": "接口暂不可用",
})
// // 检查接口权限
// _, err = auth2.GetAuthCtx(c).PermitSecretClient()
// if err != nil {
// return err
// }
// // 验证请求参数
// var req = new(ProxyReportOfflineReq)
// err = g.Validator.Validate(c, req)
// if err != nil {
// return err
// }
// // 下线转发服务
// _, err = q.Proxy.
// Where(q.Proxy.ID.Eq(req.Id)).
// UpdateSimple(q.Proxy.Status.Value(0))
// if err != nil {
// return err
// }
// // 下线所有相关的边缘节点
// _, err = q.Edge.
// Where(q.Edge.ProxyID.Eq(req.Id)).
// UpdateSimple(q.Edge.Status.Value(0))
// if err != nil {
// return err
// }
// return nil
}
type ProxyReportOfflineReq struct {
Id int32 `json:"id" validate:"required"`
}
// region 报告更新
func ProxyReportUpdate(c *fiber.Ctx) (err error) {
return c.JSON(map[string]any{
"error": "接口暂不可用",
})
// // 检查接口权限
// _, err = auth2.GetAuthCtx(c).PermitSecretClient()
// if err != nil {
// return err
// }
// // 验证请求参数
// var req = new(ProxyReportUpdateReq)
// err = g.Validator.Validate(c, req)
// if err != nil {
// return err
// }
// // 更新节点信息
// var idsActive = make([]int32, 0, len(req.Edges))
// var idsInactive = make([]int32, 0, len(req.Edges))
// var idsIspUnknown = make([]int32, 0, len(req.Edges))
// var idsIspTelecom = make([]int32, 0, len(req.Edges))
// var idsIspUnicom = make([]int32, 0, len(req.Edges))
// var idsIspMobile = make([]int32, 0, len(req.Edges))
// var otherEdges = make([]*ProxyEdge, 0, len(req.Edges))
// for _, edge := range req.Edges {
// // 检查更新ISP
// if edge.Isp != nil {
// switch edge2.ISPFromStr(*edge.Isp) {
// case edge2.IspUnknown:
// idsIspUnknown = append(idsIspUnknown, edge.Id)
// case edge2.IspChinaTelecom:
// idsIspTelecom = append(idsIspTelecom, edge.Id)
// case edge2.IspChinaUnicom:
// idsIspUnicom = append(idsIspUnicom, edge.Id)
// case edge2.IspChinaMobile:
// idsIspMobile = append(idsIspMobile, edge.Id)
// }
// }
// // 检查更新状态
// if edge.Status != nil {
// if *edge.Status == 1 {
// idsActive = append(idsActive, edge.Id)
// } else {
// idsInactive = append(idsInactive, edge.Id)
// }
// }
// // 无法分类更新
// if edge.Host != nil || edge.Port != nil || edge.Prov != nil || edge.City != nil {
// otherEdges = append(otherEdges, edge)
// continue
// }
// }
// slog.Debug("更新边缘节点信息",
// "active", len(idsActive),
// "inactive", len(idsInactive),
// "isp_unknown", len(idsIspUnknown),
// "isp_telecom", len(idsIspTelecom),
// "isp_unicom", len(idsIspUnicom),
// "isp_mobile", len(idsIspMobile),
// "other_edges", len(otherEdges),
// )
// err = q.Q.Transaction(func(q *q.Query) error {
// // 更新边缘节点状态
// if len(idsActive) > 0 {
// _, err = q.Edge.Debug().
// Where(q.Edge.ID.In(idsActive...)).
// UpdateSimple(q.Edge.Status.Value(1))
// if err != nil {
// return err
// }
// }
// if len(idsInactive) > 0 {
// _, err = q.Edge.Debug().
// Where(q.Edge.ID.In(idsInactive...)).
// UpdateSimple(q.Edge.Status.Value(0))
// if err != nil {
// return err
// }
// }
// // 更新边缘节点ISP
// if len(idsIspUnknown) > 0 {
// _, err = q.Edge.Debug().
// Where(q.Edge.ID.In(idsIspUnknown...)).
// UpdateSimple(q.Edge.Isp.Value(int32(edge2.IspUnknown)))
// if err != nil {
// return err
// }
// }
// if len(idsIspTelecom) > 0 {
// _, err = q.Edge.Debug().
// Where(q.Edge.ID.In(idsIspTelecom...)).
// UpdateSimple(q.Edge.Isp.Value(int32(edge2.IspChinaTelecom)))
// if err != nil {
// return err
// }
// }
// if len(idsIspUnicom) > 0 {
// _, err = q.Edge.Debug().
// Where(q.Edge.ID.In(idsIspUnicom...)).
// UpdateSimple(q.Edge.Isp.Value(int32(edge2.IspChinaUnicom)))
// if err != nil {
// return err
// }
// }
// if len(idsIspMobile) > 0 {
// _, err = q.Edge.Debug().
// Where(q.Edge.ID.In(idsIspMobile...)).
// UpdateSimple(q.Edge.Isp.Value(int32(edge2.IspChinaMobile)))
// if err != nil {
// return err
// }
// }
// // 更新其他边缘节点信息
// for _, edge := range otherEdges {
// do := q.Edge.Debug().Where(q.Edge.ID.Eq(edge.Id))
// var assigns = make([]field.AssignExpr, 0, 5)
// if edge.Host != nil {
// assigns = append(assigns, q.Edge.Host.Value(*edge.Host))
// }
// if edge.Port != nil {
// assigns = append(assigns, q.Edge.ProxyPort.Value(*edge.Port))
// }
// if edge.Prov != nil {
// assigns = append(assigns, q.Edge.Prov.Value(*edge.Prov))
// }
// if edge.City != nil {
// assigns = append(assigns, q.Edge.City.Value(*edge.City))
// }
// // 更新边缘节点
// _, err := do.UpdateSimple(assigns...)
// if err != nil {
// return fmt.Errorf("更新边缘节点 %d 失败: %w", edge.Id, err)
// }
// }
// return nil
// })
// if err != nil {
// return err
// }
// return nil
}
type ProxyReportUpdateReq struct {
Id int32 `json:"id" validate:"required"`
Edges []*ProxyEdge `json:"edges" validate:"required"`
}
type ProxyPermit struct {
Id int32 `json:"id"`
Expire time.Time `json:"expire"`
Whitelists *[]string `json:"whitelists"`
Username *string `json:"username"`
Password *string `json:"password"`
}
type ProxyEdge struct {
Id int32 `json:"id"`
Host *string `json:"host,omitempty"` // 边缘节点地址
Port *int32 `json:"port,omitempty"` // 边缘节点代理端口
Prov *string `json:"prov,omitempty"`
City *string `json:"city,omitempty"`
Isp *string `json:"isp,omitempty"`
Status *int32 `json:"status,omitempty"`
Loss *int32 `json:"loss,omitempty"` // 丢包率
Rtt *int32 `json:"latency,omitempty"` // 延迟
}

View File

@@ -44,26 +44,26 @@ func PageResourceShort(c *fiber.Ctx) error {
do.Where(q.ResourceShort.As(q.Resource.Short.Name()).Type.Eq(*req.Type)) do.Where(q.ResourceShort.As(q.Resource.Short.Name()).Type.Eq(*req.Type))
} }
if req.CreateAfter != nil { if req.CreateAfter != nil {
do.Where(q.Resource.CreatedAt.Gte(*req.CreateAfter)) do = do.Where(q.Resource.CreatedAt.Gte(req.CreateAfter.UTC()))
} }
if req.CreateBefore != nil { if req.CreateBefore != nil {
do.Where(q.Resource.CreatedAt.Lte(*req.CreateBefore)) do = do.Where(q.Resource.CreatedAt.Lte(req.CreateBefore.UTC()))
} }
if req.ExpireAfter != nil { if req.ExpireAfter != nil {
do.Where(q.ResourceShort.As(q.Resource.Short.Name()).ExpireAt.Gte(*req.ExpireAfter)) do = do.Where(q.ResourceShort.As(q.Resource.Short.Name()).ExpireAt.Gte(req.ExpireAfter.UTC()))
} }
if req.ExpireBefore != nil { if req.ExpireBefore != nil {
do.Where(q.ResourceShort.As(q.Resource.Short.Name()).ExpireAt.Lte(*req.ExpireBefore)) do = do.Where(q.ResourceShort.As(q.Resource.Short.Name()).ExpireAt.Lte(req.ExpireBefore.UTC()))
} }
if req.Status != nil { if req.Status != nil {
var short = q.ResourceShort.As(q.Resource.Short.Name()) var short = q.ResourceShort.As(q.Resource.Short.Name())
switch *req.Status { switch *req.Status {
case 1: case 1:
var timeCond = q.Resource.Where(short.Type.Eq(int(m.ResourceModeTime)), short.ExpireAt.Gte(time.Now())) var timeCond = q.Resource.Where(short.Type.Eq(int(m.ResourceModeTime)), short.ExpireAt.Gte(time.Now().UTC()))
var quotaCond = q.Resource.Where(short.Type.Eq(int(m.ResourceModeQuota)), short.Quota.GtCol(short.Used)) var quotaCond = q.Resource.Where(short.Type.Eq(int(m.ResourceModeQuota)), short.Quota.GtCol(short.Used))
do.Where(q.Resource.Where(timeCond).Or(quotaCond)) do.Where(q.Resource.Where(timeCond).Or(quotaCond))
case 2: case 2:
var timeCond = q.Resource.Where(short.Type.Eq(int(m.ResourceModeTime)), short.ExpireAt.Lte(time.Now())) var timeCond = q.Resource.Where(short.Type.Eq(int(m.ResourceModeTime)), short.ExpireAt.Lte(time.Now().UTC()))
var quotaCond = q.Resource.Where(short.Type.Eq(int(m.ResourceModeQuota)), short.Quota.LteCol(short.Used)) var quotaCond = q.Resource.Where(short.Type.Eq(int(m.ResourceModeQuota)), short.Quota.LteCol(short.Used))
do.Where(q.Resource.Where(timeCond).Or(quotaCond)) do.Where(q.Resource.Where(timeCond).Or(quotaCond))
} }
@@ -141,26 +141,26 @@ func PageResourceLong(c *fiber.Ctx) error {
do.Where(q.ResourceLong.As(q.Resource.Long.Name()).Type.Eq(int(*req.Type))) do.Where(q.ResourceLong.As(q.Resource.Long.Name()).Type.Eq(int(*req.Type)))
} }
if req.CreateAfter != nil { if req.CreateAfter != nil {
do.Where(q.Resource.CreatedAt.Gte(*req.CreateAfter)) do = do.Where(q.Resource.CreatedAt.Gte(req.CreateAfter.UTC()))
} }
if req.CreateBefore != nil { if req.CreateBefore != nil {
do.Where(q.Resource.CreatedAt.Lte(*req.CreateBefore)) do = do.Where(q.Resource.CreatedAt.Lte(req.CreateBefore.UTC()))
} }
if req.ExpireAfter != nil { if req.ExpireAfter != nil {
do.Where(q.ResourceLong.As(q.Resource.Long.Name()).ExpireAt.Gte(*req.ExpireAfter)) do = do.Where(q.ResourceLong.As(q.Resource.Long.Name()).ExpireAt.Gte(req.ExpireAfter.UTC()))
} }
if req.ExpireBefore != nil { if req.ExpireBefore != nil {
do.Where(q.ResourceLong.As(q.Resource.Long.Name()).ExpireAt.Lte(*req.ExpireBefore)) do = do.Where(q.ResourceLong.As(q.Resource.Long.Name()).ExpireAt.Lte(req.ExpireBefore.UTC()))
} }
if req.Status != nil { if req.Status != nil {
var long = q.ResourceLong.As(q.Resource.Long.Name()) var long = q.ResourceLong.As(q.Resource.Long.Name())
switch *req.Status { switch *req.Status {
case 1: case 1:
var timeCond = q.Resource.Where(long.Type.Eq(int(m.ResourceModeTime)), long.ExpireAt.Gte(time.Now())) var timeCond = q.Resource.Where(long.Type.Eq(int(m.ResourceModeTime)), long.ExpireAt.Gte(time.Now().UTC()))
var quotaCond = q.Resource.Where(long.Type.Eq(int(m.ResourceModeQuota)), long.Quota.GtCol(long.Used)) var quotaCond = q.Resource.Where(long.Type.Eq(int(m.ResourceModeQuota)), long.Quota.GtCol(long.Used))
do.Where(q.Resource.Where(timeCond).Or(quotaCond)) do.Where(q.Resource.Where(timeCond).Or(quotaCond))
case 2: case 2:
var timeCond = q.Resource.Where(long.Type.Eq(int(m.ResourceModeTime)), long.ExpireAt.Lte(time.Now())) var timeCond = q.Resource.Where(long.Type.Eq(int(m.ResourceModeTime)), long.ExpireAt.Lte(time.Now().UTC()))
var quotaCond = q.Resource.Where(long.Type.Eq(int(m.ResourceModeQuota)), long.Quota.LteCol(long.Used)) var quotaCond = q.Resource.Where(long.Type.Eq(int(m.ResourceModeQuota)), long.Quota.LteCol(long.Used))
do.Where(q.Resource.Where(timeCond).Or(quotaCond)) do.Where(q.Resource.Where(timeCond).Or(quotaCond))
} }
@@ -235,18 +235,16 @@ func PageResourceShortByAdmin(c *fiber.Ctx) error {
do = do.Where(q.ResourceShort.As("Short").Type.Eq(int(*req.Mode))) do = do.Where(q.ResourceShort.As("Short").Type.Eq(int(*req.Mode)))
} }
if req.CreatedAtStart != nil { if req.CreatedAtStart != nil {
time := u.DateHead(*req.CreatedAtStart) do = do.Where(q.Resource.CreatedAt.Gte(req.CreatedAtStart.UTC()))
do = do.Where(q.Resource.CreatedAt.Gte(time))
} }
if req.CreatedAtEnd != nil { if req.CreatedAtEnd != nil {
time := u.DateTail(*req.CreatedAtEnd) do = do.Where(q.Resource.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
do = do.Where(q.Resource.CreatedAt.Lte(time))
} }
if req.Expired != nil { if req.Expired != nil {
if *req.Expired { if *req.Expired {
do = do.Where(q.Resource.Where( do = do.Where(q.Resource.Where(
q.ResourceShort.As("Short").Type.Eq(int(m.ResourceModeTime)), q.ResourceShort.As("Short").Type.Eq(int(m.ResourceModeTime)),
q.ResourceShort.As("Short").ExpireAt.Lte(time.Now()), q.ResourceShort.As("Short").ExpireAt.Lte(time.Now().UTC()),
).Or( ).Or(
q.ResourceShort.As("Short").Type.Eq(int(m.ResourceModeQuota)), q.ResourceShort.As("Short").Type.Eq(int(m.ResourceModeQuota)),
q.ResourceShort.As("Short").Quota.LteCol(q.ResourceShort.As("Short").Used), q.ResourceShort.As("Short").Quota.LteCol(q.ResourceShort.As("Short").Used),
@@ -254,7 +252,7 @@ func PageResourceShortByAdmin(c *fiber.Ctx) error {
} else { } else {
do = do.Where(q.Resource.Where( do = do.Where(q.Resource.Where(
q.ResourceShort.As("Short").Type.Eq(int(m.ResourceModeTime)), q.ResourceShort.As("Short").Type.Eq(int(m.ResourceModeTime)),
q.ResourceShort.As("Short").ExpireAt.Gt(time.Now()), q.ResourceShort.As("Short").ExpireAt.Gt(time.Now().UTC()),
).Or( ).Or(
q.ResourceShort.As("Short").Type.Eq(int(m.ResourceModeQuota)), q.ResourceShort.As("Short").Type.Eq(int(m.ResourceModeQuota)),
q.ResourceShort.As("Short").Quota.GtCol(q.ResourceShort.As("Short").Used), q.ResourceShort.As("Short").Quota.GtCol(q.ResourceShort.As("Short").Used),
@@ -329,16 +327,16 @@ func PageResourceLongByAdmin(c *fiber.Ctx) error {
do = do.Where(q.ResourceLong.As("Long").Type.Eq(*req.Mode)) do = do.Where(q.ResourceLong.As("Long").Type.Eq(*req.Mode))
} }
if req.CreatedAtStart != nil { if req.CreatedAtStart != nil {
do = do.Where(q.Resource.CreatedAt.Gte(*req.CreatedAtStart)) do = do.Where(q.Resource.CreatedAt.Gte(req.CreatedAtStart.UTC()))
} }
if req.CreatedAtEnd != nil { if req.CreatedAtEnd != nil {
do = do.Where(q.Resource.CreatedAt.Lte(*req.CreatedAtEnd)) do = do.Where(q.Resource.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
} }
if req.Expired != nil { if req.Expired != nil {
if *req.Expired { if *req.Expired {
do = do.Where(q.Resource.Where( do = do.Where(q.Resource.Where(
q.ResourceLong.As("Long").Type.Eq(int(m.ResourceModeTime)), q.ResourceLong.As("Long").Type.Eq(int(m.ResourceModeTime)),
q.ResourceLong.As("Long").ExpireAt.Lte(time.Now()), q.ResourceLong.As("Long").ExpireAt.Lte(time.Now().UTC()),
).Or( ).Or(
q.ResourceLong.As("Long").Type.Eq(int(m.ResourceModeQuota)), q.ResourceLong.As("Long").Type.Eq(int(m.ResourceModeQuota)),
q.ResourceLong.As("Long").Quota.LteCol(q.ResourceLong.As("Long").Used), q.ResourceLong.As("Long").Quota.LteCol(q.ResourceLong.As("Long").Used),
@@ -346,7 +344,7 @@ func PageResourceLongByAdmin(c *fiber.Ctx) error {
} else { } else {
do = do.Where(q.Resource.Where( do = do.Where(q.Resource.Where(
q.ResourceLong.As("Long").Type.Eq(int(m.ResourceModeTime)), q.ResourceLong.As("Long").Type.Eq(int(m.ResourceModeTime)),
q.ResourceLong.As("Long").ExpireAt.Gt(time.Now()), q.ResourceLong.As("Long").ExpireAt.Gt(time.Now().UTC()),
).Or( ).Or(
q.ResourceLong.As("Long").Type.Eq(int(m.ResourceModeQuota)), q.ResourceLong.As("Long").Type.Eq(int(m.ResourceModeQuota)),
q.ResourceLong.As("Long").Quota.GtCol(q.ResourceLong.As("Long").Used), q.ResourceLong.As("Long").Quota.GtCol(q.ResourceLong.As("Long").Used),
@@ -418,15 +416,13 @@ func PageResourceShortOfUserByAdmin(c *fiber.Ctx) error {
do = do.Where(q.ResourceShort.As("Short").Type.Eq(int(*req.Mode))) do = do.Where(q.ResourceShort.As("Short").Type.Eq(int(*req.Mode)))
} }
if req.CreatedAtStart != nil { if req.CreatedAtStart != nil {
t := u.DateHead(*req.CreatedAtStart) do = do.Where(q.Resource.CreatedAt.Gte(req.CreatedAtStart.UTC()))
do = do.Where(q.Resource.CreatedAt.Gte(t))
} }
if req.CreatedAtEnd != nil { if req.CreatedAtEnd != nil {
t := u.DateTail(*req.CreatedAtEnd) do = do.Where(q.Resource.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
do = do.Where(q.Resource.CreatedAt.Lte(t))
} }
list, total, err := q.Resource. list, total, err := q.Resource.Debug().
Joins(q.Resource.User, q.Resource.Short, q.Resource.Short.Sku). Joins(q.Resource.User, q.Resource.Short, q.Resource.Short.Sku).
Select( Select(
q.Resource.ALL, q.Resource.ALL,
@@ -489,12 +485,10 @@ func PageResourceLongOfUserByAdmin(c *fiber.Ctx) error {
do = do.Where(q.ResourceLong.As("Long").Type.Eq(*req.Mode)) do = do.Where(q.ResourceLong.As("Long").Type.Eq(*req.Mode))
} }
if req.CreatedAtStart != nil { if req.CreatedAtStart != nil {
t := u.DateHead(*req.CreatedAtStart) do = do.Where(q.Resource.CreatedAt.Gte(req.CreatedAtStart.UTC()))
do = do.Where(q.Resource.CreatedAt.Gte(t))
} }
if req.CreatedAtEnd != nil { if req.CreatedAtEnd != nil {
t := u.DateTail(*req.CreatedAtEnd) do = do.Where(q.Resource.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
do = do.Where(q.Resource.CreatedAt.Lte(t))
} }
list, total, err := q.Resource. list, total, err := q.Resource.
@@ -554,6 +548,8 @@ func AllActiveResource(c *fiber.Ctx) error {
Joins( Joins(
q.Resource.Short, q.Resource.Short,
q.Resource.Long, q.Resource.Long,
q.Resource.Short.Sku,
q.Resource.Long.Sku,
). ).
Where( Where(
q.Resource.UserID.Eq(authCtx.User.ID), q.Resource.UserID.Eq(authCtx.User.ID),
@@ -562,9 +558,9 @@ func AllActiveResource(c *fiber.Ctx) error {
q.Resource.Type.Eq(int(m.ResourceTypeShort)), q.Resource.Type.Eq(int(m.ResourceTypeShort)),
q.ResourceShort.As(q.Resource.Short.Name()).Where( q.ResourceShort.As(q.Resource.Short.Name()).Where(
short.Type.Eq(int(m.ResourceModeTime)), short.Type.Eq(int(m.ResourceModeTime)),
short.ExpireAt.Gte(now), short.ExpireAt.Gte(now.UTC()),
q.ResourceShort.As(q.Resource.Short.Name()). q.ResourceShort.As(q.Resource.Short.Name()).
Where(short.LastAt.Lt(u.Today())). Where(short.LastAt.Lt(u.Today().UTC())).
Or(short.Quota.GtCol(short.Daily)), Or(short.Quota.GtCol(short.Daily)),
).Or( ).Or(
short.Type.Eq(int(m.ResourceModeQuota)), short.Type.Eq(int(m.ResourceModeQuota)),
@@ -574,9 +570,9 @@ func AllActiveResource(c *fiber.Ctx) error {
q.Resource.Type.Eq(int(m.ResourceTypeLong)), q.Resource.Type.Eq(int(m.ResourceTypeLong)),
q.ResourceLong.As(q.Resource.Long.Name()).Where( q.ResourceLong.As(q.Resource.Long.Name()).Where(
long.Type.Eq(int(m.ResourceModeTime)), long.Type.Eq(int(m.ResourceModeTime)),
long.ExpireAt.Gte(now), long.ExpireAt.Gte(now.UTC()),
q.ResourceLong.As(q.Resource.Long.Name()). q.ResourceLong.As(q.Resource.Long.Name()).
Where(long.LastAt.Lt(u.Today())). Where(long.LastAt.Lt(u.Today().UTC())).
Or(long.Quota.GtCol(long.Daily)), Or(long.Quota.GtCol(long.Daily)),
).Or( ).Or(
long.Type.Eq(int(m.ResourceModeQuota)), long.Type.Eq(int(m.ResourceModeQuota)),
@@ -590,6 +586,15 @@ func AllActiveResource(c *fiber.Ctx) error {
return err return err
} }
for _, resource := range resources {
switch resource.Type {
case m.ResourceTypeShort:
resource.Short.Sku = &m.ProductSku{Name: resource.Short.Sku.Name}
case m.ResourceTypeLong:
resource.Long.Sku = &m.ProductSku{Name: resource.Long.Sku.Name}
}
}
return c.JSON(resources) return c.JSON(resources)
} }
@@ -755,10 +760,10 @@ func StatisticResourceUsage(c *fiber.Ctx) error {
) )
if req.TimeAfter != nil { if req.TimeAfter != nil {
do.Where(q.LogsUserUsage.Time.Gte(*req.TimeAfter)) do = do.Where(q.LogsUserUsage.Time.Gte(req.TimeAfter.UTC()))
} }
if req.TimeBefore != nil { if req.TimeBefore != nil {
do.Where(q.LogsUserUsage.Time.Lte(*req.TimeBefore)) do = do.Where(q.LogsUserUsage.Time.Lte(req.TimeBefore.UTC()))
} }
var data = new(StatisticResourceUsageResp) var data = new(StatisticResourceUsageResp)

View File

@@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"log/slog" "log/slog"
"platform/pkg/env" "platform/pkg/env"
"platform/pkg/u"
"platform/web/auth" "platform/web/auth"
"platform/web/core" "platform/web/core"
g "platform/web/globals" g "platform/web/globals"
@@ -53,12 +52,10 @@ func PageTradeByAdmin(c *fiber.Ctx) error {
do = do.Where(q.Trade.Status.Eq(*req.Status)) do = do.Where(q.Trade.Status.Eq(*req.Status))
} }
if req.CreatedAtStart != nil { if req.CreatedAtStart != nil {
time := u.DateHead(*req.CreatedAtStart) do = do.Where(q.Trade.CreatedAt.Gte(req.CreatedAtStart.UTC()))
do = do.Where(q.Trade.CreatedAt.Gte(time))
} }
if req.CreatedAtEnd != nil { if req.CreatedAtEnd != nil {
time := u.DateTail(*req.CreatedAtEnd) do = do.Where(q.Trade.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
do = do.Where(q.Trade.CreatedAt.Lte(time))
} }
// 查询用户列表 // 查询用户列表
@@ -129,12 +126,10 @@ func PageTradeOfUserByAdmin(c *fiber.Ctx) error {
do = do.Where(q.Trade.Status.Eq(*req.Status)) do = do.Where(q.Trade.Status.Eq(*req.Status))
} }
if req.CreatedAtStart != nil { if req.CreatedAtStart != nil {
time := u.DateHead(*req.CreatedAtStart) do = do.Where(q.Trade.CreatedAt.Gte(req.CreatedAtStart.UTC()))
do = do.Where(q.Trade.CreatedAt.Gte(time))
} }
if req.CreatedAtEnd != nil { if req.CreatedAtEnd != nil {
time := u.DateTail(*req.CreatedAtEnd) do = do.Where(q.Trade.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
do = do.Where(q.Trade.CreatedAt.Lte(time))
} }
// 查询订单列表 // 查询订单列表

View File

@@ -8,6 +8,7 @@ import (
m "platform/web/models" m "platform/web/models"
q "platform/web/queries" q "platform/web/queries"
s "platform/web/services" s "platform/web/services"
"time"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/shopspring/decimal" "github.com/shopspring/decimal"
@@ -65,6 +66,12 @@ func PageUserByAdmin(c *fiber.Ctx) error {
do = do.Where(q.User.AdminID.IsNull()) do = do.Where(q.User.AdminID.IsNull())
} }
} }
if req.CreatedAtStart != nil {
do = do.Where(q.User.CreatedAt.Gte(req.CreatedAtStart.UTC()))
}
if req.CreatedAtEnd != nil {
do = do.Where(q.User.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
}
// 查询用户列表 // 查询用户列表
users, total, err := q.User. users, total, err := q.User.
@@ -107,6 +114,8 @@ type PageUserByAdminReq struct {
Identified *bool `json:"identified,omitempty"` Identified *bool `json:"identified,omitempty"`
Enabled *bool `json:"enabled,omitempty"` Enabled *bool `json:"enabled,omitempty"`
Assigned *bool `json:"assigned,omitempty"` Assigned *bool `json:"assigned,omitempty"`
CreatedAtStart *time.Time `json:"created_at_start,omitempty"`
CreatedAtEnd *time.Time `json:"created_at_end,omitempty"`
} }
// 管理员获取单个用户 // 管理员获取单个用户
@@ -274,7 +283,7 @@ func BindAdmin(c *fiber.Ctx) error {
} }
// 更新用户信息 // 更新用户信息
result, err := q.User.Where( r, err := q.User.Where(
q.User.ID.Eq(int32(req.UserID)), q.User.ID.Eq(int32(req.UserID)),
q.User.AdminID.IsNull(), q.User.AdminID.IsNull(),
).UpdateColumnSimple( ).UpdateColumnSimple(
@@ -283,7 +292,7 @@ func BindAdmin(c *fiber.Ctx) error {
if err != nil { if err != nil {
return err return err
} }
if result.RowsAffected == 0 { if r.RowsAffected == 0 {
return core.NewBizErr("用户已绑定管理员") return core.NewBizErr("用户已绑定管理员")
} }
@@ -323,7 +332,7 @@ func UpdateUser(c *fiber.Ctx) error {
if req.ContactWechat != nil { if req.ContactWechat != nil {
do = append(do, q.User.ContactWechat.Value(*req.ContactWechat)) do = append(do, q.User.ContactWechat.Value(*req.ContactWechat))
} }
_, err = q.User. r, err := q.User.
Where(q.User.ID.Eq(authCtx.User.ID)). Where(q.User.ID.Eq(authCtx.User.ID)).
UpdateSimple(do...) UpdateSimple(do...)
if errors.Is(err, gorm.ErrDuplicatedKey) { if errors.Is(err, gorm.ErrDuplicatedKey) {
@@ -332,6 +341,9 @@ func UpdateUser(c *fiber.Ctx) error {
if err != nil { if err != nil {
return err return err
} }
if r.RowsAffected == 0 {
return core.NewBizErr("用户状态已过期")
}
// 返回结果 // 返回结果
return c.SendStatus(fiber.StatusNoContent) return c.SendStatus(fiber.StatusNoContent)
@@ -359,7 +371,7 @@ func UpdateAccount(c *fiber.Ctx) error {
} }
// 更新用户信息 // 更新用户信息
_, err = q.User. r, err := q.User.
Where(q.User.ID.Eq(authCtx.User.ID)). Where(q.User.ID.Eq(authCtx.User.ID)).
Updates(m.User{ Updates(m.User{
Username: &req.Username, Username: &req.Username,
@@ -368,6 +380,9 @@ func UpdateAccount(c *fiber.Ctx) error {
if err != nil { if err != nil {
return err return err
} }
if r.RowsAffected == 0 {
return core.NewBizErr("用户状态已过期")
}
// 返回结果 // 返回结果
return c.SendStatus(fiber.StatusNoContent) return c.SendStatus(fiber.StatusNoContent)
@@ -410,12 +425,15 @@ func UpdatePassword(c *fiber.Ctx) error {
return err return err
} }
_, err = q.User. r, err := q.User.
Where(q.User.ID.Eq(authCtx.User.ID)). Where(q.User.ID.Eq(authCtx.User.ID)).
UpdateColumn(q.User.Password, newHash) UpdateColumn(q.User.Password, newHash)
if err != nil { if err != nil {
return err return err
} }
if r.RowsAffected == 0 {
return core.NewBizErr("用户状态已过期")
}
// 返回结果 // 返回结果
return c.SendStatus(fiber.StatusNoContent) return c.SendStatus(fiber.StatusNoContent)

View File

@@ -156,7 +156,7 @@ func UpdateWhitelist(c *fiber.Ctx) error {
} }
// 更新白名单 // 更新白名单
_, err = q.Whitelist. r, err := q.Whitelist.
Where( Where(
q.Whitelist.ID.Eq(req.ID), q.Whitelist.ID.Eq(req.ID),
q.Whitelist.UserID.Eq(authCtx.User.ID), q.Whitelist.UserID.Eq(authCtx.User.ID),
@@ -168,6 +168,9 @@ func UpdateWhitelist(c *fiber.Ctx) error {
if err != nil { if err != nil {
return err return err
} }
if r.RowsAffected == 0 {
return core.NewBizErr("白名单状态已过期")
}
return nil return nil
} }
@@ -201,7 +204,7 @@ func RemoveWhitelist(c *fiber.Ctx) error {
} }
// 删除白名单 // 删除白名单
_, err = q.Whitelist. r, err := q.Whitelist.
Where( Where(
q.Whitelist.ID.In(ids...), q.Whitelist.ID.In(ids...),
q.Whitelist.UserID.Eq(authCtx.User.ID), q.Whitelist.UserID.Eq(authCtx.User.ID),
@@ -212,6 +215,9 @@ func RemoveWhitelist(c *fiber.Ctx) error {
if err != nil { if err != nil {
return err return err
} }
if r.RowsAffected == 0 {
return core.NewBizErr("白名单状态已过期")
}
return nil return nil
} }

View File

@@ -1,11 +1,14 @@
package web package web
import ( import (
"net/http"
"platform/pkg/env"
"platform/web/auth" "platform/web/auth"
"github.com/gofiber/contrib/otelfiber/v2" "github.com/gofiber/contrib/otelfiber/v2"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors" "github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/filesystem"
"github.com/gofiber/fiber/v2/middleware/logger" "github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/fiber/v2/middleware/recover" "github.com/gofiber/fiber/v2/middleware/recover"
"github.com/gofiber/fiber/v2/middleware/requestid" "github.com/gofiber/fiber/v2/middleware/requestid"
@@ -66,6 +69,11 @@ func ApplyMiddlewares(app *fiber.App) {
}, },
})) }))
// static uploads
app.Use("/uploads", filesystem.New(filesystem.Config{
Root: http.Dir(env.UploadDir),
}))
// authenticate // authenticate
app.Use(auth.Authenticate()) app.Use(auth.Authenticate())
} }

20
web/models/area.go Normal file
View File

@@ -0,0 +1,20 @@
package models
import "platform/web/core"
// Area 地区表
type Area struct {
core.Model
Name string `json:"name" gorm:"column:name"` // 地区名称
Level AreaLevel `json:"level" gorm:"column:level"` // 地区层级1-省2-市
ParentID *int32 `json:"parent_id,omitempty" gorm:"column:parent_id"` // 父级地区ID
Parent *Area `json:"parent,omitempty" gorm:"foreignKey:ParentID"`
}
// AreaLevel 地区层级枚举
type AreaLevel int
const (
AreaLevelProvince AreaLevel = 1 // 省
AreaLevelCity AreaLevel = 2 // 市
)

23
web/models/article.go Normal file
View File

@@ -0,0 +1,23 @@
package models
import "platform/web/core"
// Article 文章表
type Article struct {
core.Model
GroupID int32 `json:"group_id" gorm:"column:group_id"` // 分组ID
Title string `json:"title" gorm:"column:title"` // 文章标题
Content *string `json:"content,omitempty" gorm:"column:content"` // 文章内容
Sort int32 `json:"sort" gorm:"column:sort"` // 文章排序
Status ArticleStatus `json:"status" gorm:"column:status"` // 文章状态0-禁用1-正常
Group *ArticleGroup `json:"group,omitempty" gorm:"foreignKey:GroupID"` // 分组
}
// ArticleStatus 文章状态
type ArticleStatus int
const (
ArticleStatusDisabled ArticleStatus = 0 // 禁用
ArticleStatusEnabled ArticleStatus = 1 // 正常
)

View File

@@ -0,0 +1,20 @@
package models
import "platform/web/core"
// ArticleGroup 文章分组表
type ArticleGroup struct {
core.Model
Name string `json:"name" gorm:"column:name"` // 分组名称
Code string `json:"code" gorm:"column:code"` // 分组编码
Sort int32 `json:"sort" gorm:"column:sort"` // 分组排序
Status ArticleGroupStatus `json:"status" gorm:"column:status"` // 分组状态0-禁用1-正常
}
// ArticleGroupStatus 分组状态
type ArticleGroupStatus int
const (
ArticleGroupStatusDisabled ArticleGroupStatus = 0 // 禁用
ArticleGroupStatusEnabled ArticleGroupStatus = 1 // 正常
)

View File

@@ -8,16 +8,17 @@ import (
// Edge 节点表 // Edge 节点表
type Edge struct { type Edge struct {
core.Model core.Model
Type EdgeType `json:"type" gorm:"column:type"` // 节点类型1-自建 Type EdgeType `json:"type" gorm:"column:type"` // 节点类型1-自建2-GOST chain
Version int32 `json:"version" gorm:"column:version"` // 节点版本 Version int32 `json:"version" gorm:"column:version"` // 节点版本
Mac string `json:"mac" gorm:"column:mac"` // 节点 mac 地址 Mac string `json:"mac" gorm:"column:mac"` // 节点 mac 地址或 GOST chain 名称
IP orm.Inet `json:"ip" gorm:"column:ip;not null"` // 节点地址 IP orm.Inet `json:"ip" gorm:"column:ip;not null"` // 节点地址或 GOST chain addr 的 IP
Port *uint16 `json:"port,omitempty" gorm:"column:port"` // GOST chain addr 的端口
ISP EdgeISP `json:"isp" gorm:"column:isp"` // 运营商0-未知1-电信2-联通3-移动 ISP EdgeISP `json:"isp" gorm:"column:isp"` // 运营商0-未知1-电信2-联通3-移动
Prov string `json:"prov" gorm:"column:prov"` // 省份 AreaID *int32 `json:"area_id,omitempty" gorm:"column:area_id"` // 城市地区ID
City string `json:"city" gorm:"column:city"` // 城市
Status EdgeStatus `json:"status" gorm:"column:status"` // 节点状态0-离线1-正常 Status EdgeStatus `json:"status" gorm:"column:status"` // 节点状态0-离线1-正常
RTT int32 `json:"rtt" gorm:"column:rtt"` // 最近平均延迟 RTT int32 `json:"rtt" gorm:"column:rtt"` // 最近平均延迟
Loss int32 `json:"loss" gorm:"column:loss"` // 最近丢包率 Loss int32 `json:"loss" gorm:"column:loss"` // 最近丢包率
Area *Area `json:"area,omitempty" gorm:"foreignKey:AreaID"` // 地区
} }
// EdgeType 节点类型枚举 // EdgeType 节点类型枚举
@@ -25,6 +26,7 @@ type EdgeType int
const ( const (
EdgeTypeSelfBuilt EdgeType = 1 // 自建 EdgeTypeSelfBuilt EdgeType = 1 // 自建
EdgeTypeGostChain EdgeType = 2 // GOST chain
) )
// EdgeStatus 节点状态枚举 // EdgeStatus 节点状态枚举
@@ -39,6 +41,7 @@ const (
type EdgeISP int type EdgeISP int
const ( const (
EdgeISPUnknown EdgeISP = 0 // 未知/任意
EdgeISPTelecom EdgeISP = 1 // 电信 EdgeISPTelecom EdgeISP = 1 // 电信
EdgeISPUnicom EdgeISP = 2 // 联通 EdgeISPUnicom EdgeISP = 2 // 联通
EdgeISPMobile EdgeISP = 3 // 移动 EdgeISPMobile EdgeISP = 3 // 移动

View File

@@ -14,6 +14,7 @@ type Proxy struct {
Mac string `json:"mac" gorm:"column:mac"` // 代理服务名称 Mac string `json:"mac" gorm:"column:mac"` // 代理服务名称
IP orm.Inet `json:"ip" gorm:"column:ip;not null"` // 代理服务地址 IP orm.Inet `json:"ip" gorm:"column:ip;not null"` // 代理服务地址
Host *string `json:"host,omitempty" gorm:"column:host"` // 代理服务域名 Host *string `json:"host,omitempty" gorm:"column:host"` // 代理服务域名
Port *int `json:"port,omitempty" gorm:"column:port"` // 代理服务端口
Secret *string `json:"secret,omitempty" gorm:"column:secret"` // 代理服务密钥 Secret *string `json:"secret,omitempty" gorm:"column:secret"` // 代理服务密钥
Type ProxyType `json:"type" gorm:"column:type"` // 代理服务类型1-自有2-白银 Type ProxyType `json:"type" gorm:"column:type"` // 代理服务类型1-自有2-白银
Status ProxyStatus `json:"status" gorm:"column:status"` // 代理服务状态0-离线1-在线 Status ProxyStatus `json:"status" gorm:"column:status"` // 代理服务状态0-离线1-在线
@@ -28,6 +29,7 @@ type ProxyType int
const ( const (
ProxyTypeSelfHosted ProxyType = 1 // 自有 ProxyTypeSelfHosted ProxyType = 1 // 自有
ProxyTypeBaiYin ProxyType = 2 // 白银 ProxyTypeBaiYin ProxyType = 2 // 白银
ProxyTypeGost ProxyType = 3 // GOST
) )
// ProxyStatus 代理服务状态枚举 // ProxyStatus 代理服务状态枚举

443
web/queries/area.gen.go Normal file
View File

@@ -0,0 +1,443 @@
// 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 newArea(db *gorm.DB, opts ...gen.DOOption) area {
_area := area{}
_area.areaDo.UseDB(db, opts...)
_area.areaDo.UseModel(&models.Area{})
tableName := _area.areaDo.TableName()
_area.ALL = field.NewAsterisk(tableName)
_area.ID = field.NewInt32(tableName, "id")
_area.CreatedAt = field.NewTime(tableName, "created_at")
_area.UpdatedAt = field.NewTime(tableName, "updated_at")
_area.DeletedAt = field.NewField(tableName, "deleted_at")
_area.Name = field.NewString(tableName, "name")
_area.Level = field.NewInt(tableName, "level")
_area.ParentID = field.NewInt32(tableName, "parent_id")
_area.Parent = areaBelongsToParent{
db: db.Session(&gorm.Session{}),
RelationField: field.NewRelation("Parent", "models.Area"),
Parent: struct {
field.RelationField
}{
RelationField: field.NewRelation("Parent.Parent", "models.Area"),
},
}
_area.fillFieldMap()
return _area
}
type area struct {
areaDo
ALL field.Asterisk
ID field.Int32
CreatedAt field.Time
UpdatedAt field.Time
DeletedAt field.Field
Name field.String
Level field.Int
ParentID field.Int32
Parent areaBelongsToParent
fieldMap map[string]field.Expr
}
func (a area) Table(newTableName string) *area {
a.areaDo.UseTable(newTableName)
return a.updateTableName(newTableName)
}
func (a area) As(alias string) *area {
a.areaDo.DO = *(a.areaDo.As(alias).(*gen.DO))
return a.updateTableName(alias)
}
func (a *area) updateTableName(table string) *area {
a.ALL = field.NewAsterisk(table)
a.ID = field.NewInt32(table, "id")
a.CreatedAt = field.NewTime(table, "created_at")
a.UpdatedAt = field.NewTime(table, "updated_at")
a.DeletedAt = field.NewField(table, "deleted_at")
a.Name = field.NewString(table, "name")
a.Level = field.NewInt(table, "level")
a.ParentID = field.NewInt32(table, "parent_id")
a.fillFieldMap()
return a
}
func (a *area) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
_f, ok := a.fieldMap[fieldName]
if !ok || _f == nil {
return nil, false
}
_oe, ok := _f.(field.OrderExpr)
return _oe, ok
}
func (a *area) fillFieldMap() {
a.fieldMap = make(map[string]field.Expr, 8)
a.fieldMap["id"] = a.ID
a.fieldMap["created_at"] = a.CreatedAt
a.fieldMap["updated_at"] = a.UpdatedAt
a.fieldMap["deleted_at"] = a.DeletedAt
a.fieldMap["name"] = a.Name
a.fieldMap["level"] = a.Level
a.fieldMap["parent_id"] = a.ParentID
}
func (a area) clone(db *gorm.DB) area {
a.areaDo.ReplaceConnPool(db.Statement.ConnPool)
a.Parent.db = db.Session(&gorm.Session{Initialized: true})
a.Parent.db.Statement.ConnPool = db.Statement.ConnPool
return a
}
func (a area) replaceDB(db *gorm.DB) area {
a.areaDo.ReplaceDB(db)
a.Parent.db = db.Session(&gorm.Session{})
return a
}
type areaBelongsToParent struct {
db *gorm.DB
field.RelationField
Parent struct {
field.RelationField
}
}
func (a areaBelongsToParent) Where(conds ...field.Expr) *areaBelongsToParent {
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 areaBelongsToParent) WithContext(ctx context.Context) *areaBelongsToParent {
a.db = a.db.WithContext(ctx)
return &a
}
func (a areaBelongsToParent) Session(session *gorm.Session) *areaBelongsToParent {
a.db = a.db.Session(session)
return &a
}
func (a areaBelongsToParent) Model(m *models.Area) *areaBelongsToParentTx {
return &areaBelongsToParentTx{a.db.Model(m).Association(a.Name())}
}
func (a areaBelongsToParent) Unscoped() *areaBelongsToParent {
a.db = a.db.Unscoped()
return &a
}
type areaBelongsToParentTx struct{ tx *gorm.Association }
func (a areaBelongsToParentTx) Find() (result *models.Area, err error) {
return result, a.tx.Find(&result)
}
func (a areaBelongsToParentTx) Append(values ...*models.Area) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Append(targetValues...)
}
func (a areaBelongsToParentTx) Replace(values ...*models.Area) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Replace(targetValues...)
}
func (a areaBelongsToParentTx) Delete(values ...*models.Area) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Delete(targetValues...)
}
func (a areaBelongsToParentTx) Clear() error {
return a.tx.Clear()
}
func (a areaBelongsToParentTx) Count() int64 {
return a.tx.Count()
}
func (a areaBelongsToParentTx) Unscoped() *areaBelongsToParentTx {
a.tx = a.tx.Unscoped()
return &a
}
type areaDo struct{ gen.DO }
func (a areaDo) Debug() *areaDo {
return a.withDO(a.DO.Debug())
}
func (a areaDo) WithContext(ctx context.Context) *areaDo {
return a.withDO(a.DO.WithContext(ctx))
}
func (a areaDo) ReadDB() *areaDo {
return a.Clauses(dbresolver.Read)
}
func (a areaDo) WriteDB() *areaDo {
return a.Clauses(dbresolver.Write)
}
func (a areaDo) Session(config *gorm.Session) *areaDo {
return a.withDO(a.DO.Session(config))
}
func (a areaDo) Clauses(conds ...clause.Expression) *areaDo {
return a.withDO(a.DO.Clauses(conds...))
}
func (a areaDo) Returning(value interface{}, columns ...string) *areaDo {
return a.withDO(a.DO.Returning(value, columns...))
}
func (a areaDo) Not(conds ...gen.Condition) *areaDo {
return a.withDO(a.DO.Not(conds...))
}
func (a areaDo) Or(conds ...gen.Condition) *areaDo {
return a.withDO(a.DO.Or(conds...))
}
func (a areaDo) Select(conds ...field.Expr) *areaDo {
return a.withDO(a.DO.Select(conds...))
}
func (a areaDo) Where(conds ...gen.Condition) *areaDo {
return a.withDO(a.DO.Where(conds...))
}
func (a areaDo) Order(conds ...field.Expr) *areaDo {
return a.withDO(a.DO.Order(conds...))
}
func (a areaDo) Distinct(cols ...field.Expr) *areaDo {
return a.withDO(a.DO.Distinct(cols...))
}
func (a areaDo) Omit(cols ...field.Expr) *areaDo {
return a.withDO(a.DO.Omit(cols...))
}
func (a areaDo) Join(table schema.Tabler, on ...field.Expr) *areaDo {
return a.withDO(a.DO.Join(table, on...))
}
func (a areaDo) LeftJoin(table schema.Tabler, on ...field.Expr) *areaDo {
return a.withDO(a.DO.LeftJoin(table, on...))
}
func (a areaDo) RightJoin(table schema.Tabler, on ...field.Expr) *areaDo {
return a.withDO(a.DO.RightJoin(table, on...))
}
func (a areaDo) Group(cols ...field.Expr) *areaDo {
return a.withDO(a.DO.Group(cols...))
}
func (a areaDo) Having(conds ...gen.Condition) *areaDo {
return a.withDO(a.DO.Having(conds...))
}
func (a areaDo) Limit(limit int) *areaDo {
return a.withDO(a.DO.Limit(limit))
}
func (a areaDo) Offset(offset int) *areaDo {
return a.withDO(a.DO.Offset(offset))
}
func (a areaDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *areaDo {
return a.withDO(a.DO.Scopes(funcs...))
}
func (a areaDo) Unscoped() *areaDo {
return a.withDO(a.DO.Unscoped())
}
func (a areaDo) Create(values ...*models.Area) error {
if len(values) == 0 {
return nil
}
return a.DO.Create(values)
}
func (a areaDo) CreateInBatches(values []*models.Area, batchSize int) error {
return a.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 (a areaDo) Save(values ...*models.Area) error {
if len(values) == 0 {
return nil
}
return a.DO.Save(values)
}
func (a areaDo) First() (*models.Area, error) {
if result, err := a.DO.First(); err != nil {
return nil, err
} else {
return result.(*models.Area), nil
}
}
func (a areaDo) Take() (*models.Area, error) {
if result, err := a.DO.Take(); err != nil {
return nil, err
} else {
return result.(*models.Area), nil
}
}
func (a areaDo) Last() (*models.Area, error) {
if result, err := a.DO.Last(); err != nil {
return nil, err
} else {
return result.(*models.Area), nil
}
}
func (a areaDo) Find() ([]*models.Area, error) {
result, err := a.DO.Find()
return result.([]*models.Area), err
}
func (a areaDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*models.Area, err error) {
buf := make([]*models.Area, 0, batchSize)
err = a.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 (a areaDo) FindInBatches(result *[]*models.Area, batchSize int, fc func(tx gen.Dao, batch int) error) error {
return a.DO.FindInBatches(result, batchSize, fc)
}
func (a areaDo) Attrs(attrs ...field.AssignExpr) *areaDo {
return a.withDO(a.DO.Attrs(attrs...))
}
func (a areaDo) Assign(attrs ...field.AssignExpr) *areaDo {
return a.withDO(a.DO.Assign(attrs...))
}
func (a areaDo) Joins(fields ...field.RelationField) *areaDo {
for _, _f := range fields {
a = *a.withDO(a.DO.Joins(_f))
}
return &a
}
func (a areaDo) Preload(fields ...field.RelationField) *areaDo {
for _, _f := range fields {
a = *a.withDO(a.DO.Preload(_f))
}
return &a
}
func (a areaDo) FirstOrInit() (*models.Area, error) {
if result, err := a.DO.FirstOrInit(); err != nil {
return nil, err
} else {
return result.(*models.Area), nil
}
}
func (a areaDo) FirstOrCreate() (*models.Area, error) {
if result, err := a.DO.FirstOrCreate(); err != nil {
return nil, err
} else {
return result.(*models.Area), nil
}
}
func (a areaDo) FindByPage(offset int, limit int) (result []*models.Area, count int64, err error) {
result, err = a.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 = a.Offset(-1).Limit(-1).Count()
return
}
func (a areaDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
count, err = a.Count()
if err != nil {
return
}
err = a.Offset(offset).Limit(limit).Scan(result)
return
}
func (a areaDo) Scan(result interface{}) (err error) {
return a.DO.Scan(result)
}
func (a areaDo) Delete(models ...*models.Area) (result gen.ResultInfo, err error) {
return a.DO.Delete(models)
}
func (a *areaDo) withDO(do gen.Dao) *areaDo {
a.DO = *do.(*gen.DO)
return a
}

442
web/queries/article.gen.go Normal file
View File

@@ -0,0 +1,442 @@
// 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 newArticle(db *gorm.DB, opts ...gen.DOOption) article {
_article := article{}
_article.articleDo.UseDB(db, opts...)
_article.articleDo.UseModel(&models.Article{})
tableName := _article.articleDo.TableName()
_article.ALL = field.NewAsterisk(tableName)
_article.ID = field.NewInt32(tableName, "id")
_article.CreatedAt = field.NewTime(tableName, "created_at")
_article.UpdatedAt = field.NewTime(tableName, "updated_at")
_article.DeletedAt = field.NewField(tableName, "deleted_at")
_article.GroupID = field.NewInt32(tableName, "group_id")
_article.Title = field.NewString(tableName, "title")
_article.Content = field.NewString(tableName, "content")
_article.Sort = field.NewInt32(tableName, "sort")
_article.Status = field.NewInt(tableName, "status")
_article.Group = articleBelongsToGroup{
db: db.Session(&gorm.Session{}),
RelationField: field.NewRelation("Group", "models.ArticleGroup"),
}
_article.fillFieldMap()
return _article
}
type article struct {
articleDo
ALL field.Asterisk
ID field.Int32
CreatedAt field.Time
UpdatedAt field.Time
DeletedAt field.Field
GroupID field.Int32
Title field.String
Content field.String
Sort field.Int32
Status field.Int
Group articleBelongsToGroup
fieldMap map[string]field.Expr
}
func (a article) Table(newTableName string) *article {
a.articleDo.UseTable(newTableName)
return a.updateTableName(newTableName)
}
func (a article) As(alias string) *article {
a.articleDo.DO = *(a.articleDo.As(alias).(*gen.DO))
return a.updateTableName(alias)
}
func (a *article) updateTableName(table string) *article {
a.ALL = field.NewAsterisk(table)
a.ID = field.NewInt32(table, "id")
a.CreatedAt = field.NewTime(table, "created_at")
a.UpdatedAt = field.NewTime(table, "updated_at")
a.DeletedAt = field.NewField(table, "deleted_at")
a.GroupID = field.NewInt32(table, "group_id")
a.Title = field.NewString(table, "title")
a.Content = field.NewString(table, "content")
a.Sort = field.NewInt32(table, "sort")
a.Status = field.NewInt(table, "status")
a.fillFieldMap()
return a
}
func (a *article) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
_f, ok := a.fieldMap[fieldName]
if !ok || _f == nil {
return nil, false
}
_oe, ok := _f.(field.OrderExpr)
return _oe, ok
}
func (a *article) fillFieldMap() {
a.fieldMap = make(map[string]field.Expr, 10)
a.fieldMap["id"] = a.ID
a.fieldMap["created_at"] = a.CreatedAt
a.fieldMap["updated_at"] = a.UpdatedAt
a.fieldMap["deleted_at"] = a.DeletedAt
a.fieldMap["group_id"] = a.GroupID
a.fieldMap["title"] = a.Title
a.fieldMap["content"] = a.Content
a.fieldMap["sort"] = a.Sort
a.fieldMap["status"] = a.Status
}
func (a article) clone(db *gorm.DB) article {
a.articleDo.ReplaceConnPool(db.Statement.ConnPool)
a.Group.db = db.Session(&gorm.Session{Initialized: true})
a.Group.db.Statement.ConnPool = db.Statement.ConnPool
return a
}
func (a article) replaceDB(db *gorm.DB) article {
a.articleDo.ReplaceDB(db)
a.Group.db = db.Session(&gorm.Session{})
return a
}
type articleBelongsToGroup struct {
db *gorm.DB
field.RelationField
}
func (a articleBelongsToGroup) Where(conds ...field.Expr) *articleBelongsToGroup {
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 articleBelongsToGroup) WithContext(ctx context.Context) *articleBelongsToGroup {
a.db = a.db.WithContext(ctx)
return &a
}
func (a articleBelongsToGroup) Session(session *gorm.Session) *articleBelongsToGroup {
a.db = a.db.Session(session)
return &a
}
func (a articleBelongsToGroup) Model(m *models.Article) *articleBelongsToGroupTx {
return &articleBelongsToGroupTx{a.db.Model(m).Association(a.Name())}
}
func (a articleBelongsToGroup) Unscoped() *articleBelongsToGroup {
a.db = a.db.Unscoped()
return &a
}
type articleBelongsToGroupTx struct{ tx *gorm.Association }
func (a articleBelongsToGroupTx) Find() (result *models.ArticleGroup, err error) {
return result, a.tx.Find(&result)
}
func (a articleBelongsToGroupTx) Append(values ...*models.ArticleGroup) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Append(targetValues...)
}
func (a articleBelongsToGroupTx) Replace(values ...*models.ArticleGroup) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Replace(targetValues...)
}
func (a articleBelongsToGroupTx) Delete(values ...*models.ArticleGroup) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Delete(targetValues...)
}
func (a articleBelongsToGroupTx) Clear() error {
return a.tx.Clear()
}
func (a articleBelongsToGroupTx) Count() int64 {
return a.tx.Count()
}
func (a articleBelongsToGroupTx) Unscoped() *articleBelongsToGroupTx {
a.tx = a.tx.Unscoped()
return &a
}
type articleDo struct{ gen.DO }
func (a articleDo) Debug() *articleDo {
return a.withDO(a.DO.Debug())
}
func (a articleDo) WithContext(ctx context.Context) *articleDo {
return a.withDO(a.DO.WithContext(ctx))
}
func (a articleDo) ReadDB() *articleDo {
return a.Clauses(dbresolver.Read)
}
func (a articleDo) WriteDB() *articleDo {
return a.Clauses(dbresolver.Write)
}
func (a articleDo) Session(config *gorm.Session) *articleDo {
return a.withDO(a.DO.Session(config))
}
func (a articleDo) Clauses(conds ...clause.Expression) *articleDo {
return a.withDO(a.DO.Clauses(conds...))
}
func (a articleDo) Returning(value interface{}, columns ...string) *articleDo {
return a.withDO(a.DO.Returning(value, columns...))
}
func (a articleDo) Not(conds ...gen.Condition) *articleDo {
return a.withDO(a.DO.Not(conds...))
}
func (a articleDo) Or(conds ...gen.Condition) *articleDo {
return a.withDO(a.DO.Or(conds...))
}
func (a articleDo) Select(conds ...field.Expr) *articleDo {
return a.withDO(a.DO.Select(conds...))
}
func (a articleDo) Where(conds ...gen.Condition) *articleDo {
return a.withDO(a.DO.Where(conds...))
}
func (a articleDo) Order(conds ...field.Expr) *articleDo {
return a.withDO(a.DO.Order(conds...))
}
func (a articleDo) Distinct(cols ...field.Expr) *articleDo {
return a.withDO(a.DO.Distinct(cols...))
}
func (a articleDo) Omit(cols ...field.Expr) *articleDo {
return a.withDO(a.DO.Omit(cols...))
}
func (a articleDo) Join(table schema.Tabler, on ...field.Expr) *articleDo {
return a.withDO(a.DO.Join(table, on...))
}
func (a articleDo) LeftJoin(table schema.Tabler, on ...field.Expr) *articleDo {
return a.withDO(a.DO.LeftJoin(table, on...))
}
func (a articleDo) RightJoin(table schema.Tabler, on ...field.Expr) *articleDo {
return a.withDO(a.DO.RightJoin(table, on...))
}
func (a articleDo) Group(cols ...field.Expr) *articleDo {
return a.withDO(a.DO.Group(cols...))
}
func (a articleDo) Having(conds ...gen.Condition) *articleDo {
return a.withDO(a.DO.Having(conds...))
}
func (a articleDo) Limit(limit int) *articleDo {
return a.withDO(a.DO.Limit(limit))
}
func (a articleDo) Offset(offset int) *articleDo {
return a.withDO(a.DO.Offset(offset))
}
func (a articleDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *articleDo {
return a.withDO(a.DO.Scopes(funcs...))
}
func (a articleDo) Unscoped() *articleDo {
return a.withDO(a.DO.Unscoped())
}
func (a articleDo) Create(values ...*models.Article) error {
if len(values) == 0 {
return nil
}
return a.DO.Create(values)
}
func (a articleDo) CreateInBatches(values []*models.Article, batchSize int) error {
return a.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 (a articleDo) Save(values ...*models.Article) error {
if len(values) == 0 {
return nil
}
return a.DO.Save(values)
}
func (a articleDo) First() (*models.Article, error) {
if result, err := a.DO.First(); err != nil {
return nil, err
} else {
return result.(*models.Article), nil
}
}
func (a articleDo) Take() (*models.Article, error) {
if result, err := a.DO.Take(); err != nil {
return nil, err
} else {
return result.(*models.Article), nil
}
}
func (a articleDo) Last() (*models.Article, error) {
if result, err := a.DO.Last(); err != nil {
return nil, err
} else {
return result.(*models.Article), nil
}
}
func (a articleDo) Find() ([]*models.Article, error) {
result, err := a.DO.Find()
return result.([]*models.Article), err
}
func (a articleDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*models.Article, err error) {
buf := make([]*models.Article, 0, batchSize)
err = a.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 (a articleDo) FindInBatches(result *[]*models.Article, batchSize int, fc func(tx gen.Dao, batch int) error) error {
return a.DO.FindInBatches(result, batchSize, fc)
}
func (a articleDo) Attrs(attrs ...field.AssignExpr) *articleDo {
return a.withDO(a.DO.Attrs(attrs...))
}
func (a articleDo) Assign(attrs ...field.AssignExpr) *articleDo {
return a.withDO(a.DO.Assign(attrs...))
}
func (a articleDo) Joins(fields ...field.RelationField) *articleDo {
for _, _f := range fields {
a = *a.withDO(a.DO.Joins(_f))
}
return &a
}
func (a articleDo) Preload(fields ...field.RelationField) *articleDo {
for _, _f := range fields {
a = *a.withDO(a.DO.Preload(_f))
}
return &a
}
func (a articleDo) FirstOrInit() (*models.Article, error) {
if result, err := a.DO.FirstOrInit(); err != nil {
return nil, err
} else {
return result.(*models.Article), nil
}
}
func (a articleDo) FirstOrCreate() (*models.Article, error) {
if result, err := a.DO.FirstOrCreate(); err != nil {
return nil, err
} else {
return result.(*models.Article), nil
}
}
func (a articleDo) FindByPage(offset int, limit int) (result []*models.Article, count int64, err error) {
result, err = a.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 = a.Offset(-1).Limit(-1).Count()
return
}
func (a articleDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
count, err = a.Count()
if err != nil {
return
}
err = a.Offset(offset).Limit(limit).Scan(result)
return
}
func (a articleDo) Scan(result interface{}) (err error) {
return a.DO.Scan(result)
}
func (a articleDo) Delete(models ...*models.Article) (result gen.ResultInfo, err error) {
return a.DO.Delete(models)
}
func (a *articleDo) withDO(do gen.Dao) *articleDo {
a.DO = *do.(*gen.DO)
return a
}

View File

@@ -0,0 +1,347 @@
// 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 newArticleGroup(db *gorm.DB, opts ...gen.DOOption) articleGroup {
_articleGroup := articleGroup{}
_articleGroup.articleGroupDo.UseDB(db, opts...)
_articleGroup.articleGroupDo.UseModel(&models.ArticleGroup{})
tableName := _articleGroup.articleGroupDo.TableName()
_articleGroup.ALL = field.NewAsterisk(tableName)
_articleGroup.ID = field.NewInt32(tableName, "id")
_articleGroup.CreatedAt = field.NewTime(tableName, "created_at")
_articleGroup.UpdatedAt = field.NewTime(tableName, "updated_at")
_articleGroup.DeletedAt = field.NewField(tableName, "deleted_at")
_articleGroup.Name = field.NewString(tableName, "name")
_articleGroup.Code = field.NewString(tableName, "code")
_articleGroup.Sort = field.NewInt32(tableName, "sort")
_articleGroup.Status = field.NewInt(tableName, "status")
_articleGroup.fillFieldMap()
return _articleGroup
}
type articleGroup struct {
articleGroupDo
ALL field.Asterisk
ID field.Int32
CreatedAt field.Time
UpdatedAt field.Time
DeletedAt field.Field
Name field.String
Code field.String
Sort field.Int32
Status field.Int
fieldMap map[string]field.Expr
}
func (a articleGroup) Table(newTableName string) *articleGroup {
a.articleGroupDo.UseTable(newTableName)
return a.updateTableName(newTableName)
}
func (a articleGroup) As(alias string) *articleGroup {
a.articleGroupDo.DO = *(a.articleGroupDo.As(alias).(*gen.DO))
return a.updateTableName(alias)
}
func (a *articleGroup) updateTableName(table string) *articleGroup {
a.ALL = field.NewAsterisk(table)
a.ID = field.NewInt32(table, "id")
a.CreatedAt = field.NewTime(table, "created_at")
a.UpdatedAt = field.NewTime(table, "updated_at")
a.DeletedAt = field.NewField(table, "deleted_at")
a.Name = field.NewString(table, "name")
a.Code = field.NewString(table, "code")
a.Sort = field.NewInt32(table, "sort")
a.Status = field.NewInt(table, "status")
a.fillFieldMap()
return a
}
func (a *articleGroup) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
_f, ok := a.fieldMap[fieldName]
if !ok || _f == nil {
return nil, false
}
_oe, ok := _f.(field.OrderExpr)
return _oe, ok
}
func (a *articleGroup) fillFieldMap() {
a.fieldMap = make(map[string]field.Expr, 8)
a.fieldMap["id"] = a.ID
a.fieldMap["created_at"] = a.CreatedAt
a.fieldMap["updated_at"] = a.UpdatedAt
a.fieldMap["deleted_at"] = a.DeletedAt
a.fieldMap["name"] = a.Name
a.fieldMap["code"] = a.Code
a.fieldMap["sort"] = a.Sort
a.fieldMap["status"] = a.Status
}
func (a articleGroup) clone(db *gorm.DB) articleGroup {
a.articleGroupDo.ReplaceConnPool(db.Statement.ConnPool)
return a
}
func (a articleGroup) replaceDB(db *gorm.DB) articleGroup {
a.articleGroupDo.ReplaceDB(db)
return a
}
type articleGroupDo struct{ gen.DO }
func (a articleGroupDo) Debug() *articleGroupDo {
return a.withDO(a.DO.Debug())
}
func (a articleGroupDo) WithContext(ctx context.Context) *articleGroupDo {
return a.withDO(a.DO.WithContext(ctx))
}
func (a articleGroupDo) ReadDB() *articleGroupDo {
return a.Clauses(dbresolver.Read)
}
func (a articleGroupDo) WriteDB() *articleGroupDo {
return a.Clauses(dbresolver.Write)
}
func (a articleGroupDo) Session(config *gorm.Session) *articleGroupDo {
return a.withDO(a.DO.Session(config))
}
func (a articleGroupDo) Clauses(conds ...clause.Expression) *articleGroupDo {
return a.withDO(a.DO.Clauses(conds...))
}
func (a articleGroupDo) Returning(value interface{}, columns ...string) *articleGroupDo {
return a.withDO(a.DO.Returning(value, columns...))
}
func (a articleGroupDo) Not(conds ...gen.Condition) *articleGroupDo {
return a.withDO(a.DO.Not(conds...))
}
func (a articleGroupDo) Or(conds ...gen.Condition) *articleGroupDo {
return a.withDO(a.DO.Or(conds...))
}
func (a articleGroupDo) Select(conds ...field.Expr) *articleGroupDo {
return a.withDO(a.DO.Select(conds...))
}
func (a articleGroupDo) Where(conds ...gen.Condition) *articleGroupDo {
return a.withDO(a.DO.Where(conds...))
}
func (a articleGroupDo) Order(conds ...field.Expr) *articleGroupDo {
return a.withDO(a.DO.Order(conds...))
}
func (a articleGroupDo) Distinct(cols ...field.Expr) *articleGroupDo {
return a.withDO(a.DO.Distinct(cols...))
}
func (a articleGroupDo) Omit(cols ...field.Expr) *articleGroupDo {
return a.withDO(a.DO.Omit(cols...))
}
func (a articleGroupDo) Join(table schema.Tabler, on ...field.Expr) *articleGroupDo {
return a.withDO(a.DO.Join(table, on...))
}
func (a articleGroupDo) LeftJoin(table schema.Tabler, on ...field.Expr) *articleGroupDo {
return a.withDO(a.DO.LeftJoin(table, on...))
}
func (a articleGroupDo) RightJoin(table schema.Tabler, on ...field.Expr) *articleGroupDo {
return a.withDO(a.DO.RightJoin(table, on...))
}
func (a articleGroupDo) Group(cols ...field.Expr) *articleGroupDo {
return a.withDO(a.DO.Group(cols...))
}
func (a articleGroupDo) Having(conds ...gen.Condition) *articleGroupDo {
return a.withDO(a.DO.Having(conds...))
}
func (a articleGroupDo) Limit(limit int) *articleGroupDo {
return a.withDO(a.DO.Limit(limit))
}
func (a articleGroupDo) Offset(offset int) *articleGroupDo {
return a.withDO(a.DO.Offset(offset))
}
func (a articleGroupDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *articleGroupDo {
return a.withDO(a.DO.Scopes(funcs...))
}
func (a articleGroupDo) Unscoped() *articleGroupDo {
return a.withDO(a.DO.Unscoped())
}
func (a articleGroupDo) Create(values ...*models.ArticleGroup) error {
if len(values) == 0 {
return nil
}
return a.DO.Create(values)
}
func (a articleGroupDo) CreateInBatches(values []*models.ArticleGroup, batchSize int) error {
return a.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 (a articleGroupDo) Save(values ...*models.ArticleGroup) error {
if len(values) == 0 {
return nil
}
return a.DO.Save(values)
}
func (a articleGroupDo) First() (*models.ArticleGroup, error) {
if result, err := a.DO.First(); err != nil {
return nil, err
} else {
return result.(*models.ArticleGroup), nil
}
}
func (a articleGroupDo) Take() (*models.ArticleGroup, error) {
if result, err := a.DO.Take(); err != nil {
return nil, err
} else {
return result.(*models.ArticleGroup), nil
}
}
func (a articleGroupDo) Last() (*models.ArticleGroup, error) {
if result, err := a.DO.Last(); err != nil {
return nil, err
} else {
return result.(*models.ArticleGroup), nil
}
}
func (a articleGroupDo) Find() ([]*models.ArticleGroup, error) {
result, err := a.DO.Find()
return result.([]*models.ArticleGroup), err
}
func (a articleGroupDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*models.ArticleGroup, err error) {
buf := make([]*models.ArticleGroup, 0, batchSize)
err = a.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 (a articleGroupDo) FindInBatches(result *[]*models.ArticleGroup, batchSize int, fc func(tx gen.Dao, batch int) error) error {
return a.DO.FindInBatches(result, batchSize, fc)
}
func (a articleGroupDo) Attrs(attrs ...field.AssignExpr) *articleGroupDo {
return a.withDO(a.DO.Attrs(attrs...))
}
func (a articleGroupDo) Assign(attrs ...field.AssignExpr) *articleGroupDo {
return a.withDO(a.DO.Assign(attrs...))
}
func (a articleGroupDo) Joins(fields ...field.RelationField) *articleGroupDo {
for _, _f := range fields {
a = *a.withDO(a.DO.Joins(_f))
}
return &a
}
func (a articleGroupDo) Preload(fields ...field.RelationField) *articleGroupDo {
for _, _f := range fields {
a = *a.withDO(a.DO.Preload(_f))
}
return &a
}
func (a articleGroupDo) FirstOrInit() (*models.ArticleGroup, error) {
if result, err := a.DO.FirstOrInit(); err != nil {
return nil, err
} else {
return result.(*models.ArticleGroup), nil
}
}
func (a articleGroupDo) FirstOrCreate() (*models.ArticleGroup, error) {
if result, err := a.DO.FirstOrCreate(); err != nil {
return nil, err
} else {
return result.(*models.ArticleGroup), nil
}
}
func (a articleGroupDo) FindByPage(offset int, limit int) (result []*models.ArticleGroup, count int64, err error) {
result, err = a.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 = a.Offset(-1).Limit(-1).Count()
return
}
func (a articleGroupDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
count, err = a.Count()
if err != nil {
return
}
err = a.Offset(offset).Limit(limit).Scan(result)
return
}
func (a articleGroupDo) Scan(result interface{}) (err error) {
return a.DO.Scan(result)
}
func (a articleGroupDo) Delete(models ...*models.ArticleGroup) (result gen.ResultInfo, err error) {
return a.DO.Delete(models)
}
func (a *articleGroupDo) withDO(do gen.Dao) *articleGroupDo {
a.DO = *do.(*gen.DO)
return a
}

View File

@@ -218,6 +218,12 @@ func newChannel(db *gorm.DB, opts ...gen.DOOption) channel {
} }
Edge struct { Edge struct {
field.RelationField field.RelationField
Area struct {
field.RelationField
Parent struct {
field.RelationField
}
}
} }
}{ }{
RelationField: field.NewRelation("Proxy.Channels", "models.Channel"), RelationField: field.NewRelation("Proxy.Channels", "models.Channel"),
@@ -238,8 +244,27 @@ func newChannel(db *gorm.DB, opts ...gen.DOOption) channel {
}, },
Edge: struct { Edge: struct {
field.RelationField field.RelationField
Area struct {
field.RelationField
Parent struct {
field.RelationField
}
}
}{ }{
RelationField: field.NewRelation("Proxy.Channels.Edge", "models.Edge"), RelationField: field.NewRelation("Proxy.Channels.Edge", "models.Edge"),
Area: struct {
field.RelationField
Parent struct {
field.RelationField
}
}{
RelationField: field.NewRelation("Proxy.Channels.Edge.Area", "models.Area"),
Parent: struct {
field.RelationField
}{
RelationField: field.NewRelation("Proxy.Channels.Edge.Area.Parent", "models.Area"),
},
},
}, },
}, },
} }
@@ -617,6 +642,12 @@ type channelBelongsToProxy struct {
} }
Edge struct { Edge struct {
field.RelationField field.RelationField
Area struct {
field.RelationField
Parent struct {
field.RelationField
}
}
} }
} }
} }

View File

@@ -35,12 +35,22 @@ func newEdge(db *gorm.DB, opts ...gen.DOOption) edge {
_edge.Version = field.NewInt32(tableName, "version") _edge.Version = field.NewInt32(tableName, "version")
_edge.Mac = field.NewString(tableName, "mac") _edge.Mac = field.NewString(tableName, "mac")
_edge.IP = field.NewField(tableName, "ip") _edge.IP = field.NewField(tableName, "ip")
_edge.Port = field.NewUint16(tableName, "port")
_edge.ISP = field.NewInt(tableName, "isp") _edge.ISP = field.NewInt(tableName, "isp")
_edge.Prov = field.NewString(tableName, "prov") _edge.AreaID = field.NewInt32(tableName, "area_id")
_edge.City = field.NewString(tableName, "city")
_edge.Status = field.NewInt(tableName, "status") _edge.Status = field.NewInt(tableName, "status")
_edge.RTT = field.NewInt32(tableName, "rtt") _edge.RTT = field.NewInt32(tableName, "rtt")
_edge.Loss = field.NewInt32(tableName, "loss") _edge.Loss = field.NewInt32(tableName, "loss")
_edge.Area = edgeBelongsToArea{
db: db.Session(&gorm.Session{}),
RelationField: field.NewRelation("Area", "models.Area"),
Parent: struct {
field.RelationField
}{
RelationField: field.NewRelation("Area.Parent", "models.Area"),
},
}
_edge.fillFieldMap() _edge.fillFieldMap()
@@ -59,12 +69,13 @@ type edge struct {
Version field.Int32 Version field.Int32
Mac field.String Mac field.String
IP field.Field IP field.Field
Port field.Uint16
ISP field.Int ISP field.Int
Prov field.String AreaID field.Int32
City field.String
Status field.Int Status field.Int
RTT field.Int32 RTT field.Int32
Loss field.Int32 Loss field.Int32
Area edgeBelongsToArea
fieldMap map[string]field.Expr fieldMap map[string]field.Expr
} }
@@ -89,9 +100,9 @@ func (e *edge) updateTableName(table string) *edge {
e.Version = field.NewInt32(table, "version") e.Version = field.NewInt32(table, "version")
e.Mac = field.NewString(table, "mac") e.Mac = field.NewString(table, "mac")
e.IP = field.NewField(table, "ip") e.IP = field.NewField(table, "ip")
e.Port = field.NewUint16(table, "port")
e.ISP = field.NewInt(table, "isp") e.ISP = field.NewInt(table, "isp")
e.Prov = field.NewString(table, "prov") e.AreaID = field.NewInt32(table, "area_id")
e.City = field.NewString(table, "city")
e.Status = field.NewInt(table, "status") e.Status = field.NewInt(table, "status")
e.RTT = field.NewInt32(table, "rtt") e.RTT = field.NewInt32(table, "rtt")
e.Loss = field.NewInt32(table, "loss") e.Loss = field.NewInt32(table, "loss")
@@ -111,7 +122,7 @@ func (e *edge) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
} }
func (e *edge) fillFieldMap() { func (e *edge) fillFieldMap() {
e.fieldMap = make(map[string]field.Expr, 14) e.fieldMap = make(map[string]field.Expr, 15)
e.fieldMap["id"] = e.ID e.fieldMap["id"] = e.ID
e.fieldMap["created_at"] = e.CreatedAt e.fieldMap["created_at"] = e.CreatedAt
e.fieldMap["updated_at"] = e.UpdatedAt e.fieldMap["updated_at"] = e.UpdatedAt
@@ -120,24 +131,113 @@ func (e *edge) fillFieldMap() {
e.fieldMap["version"] = e.Version e.fieldMap["version"] = e.Version
e.fieldMap["mac"] = e.Mac e.fieldMap["mac"] = e.Mac
e.fieldMap["ip"] = e.IP e.fieldMap["ip"] = e.IP
e.fieldMap["port"] = e.Port
e.fieldMap["isp"] = e.ISP e.fieldMap["isp"] = e.ISP
e.fieldMap["prov"] = e.Prov e.fieldMap["area_id"] = e.AreaID
e.fieldMap["city"] = e.City
e.fieldMap["status"] = e.Status e.fieldMap["status"] = e.Status
e.fieldMap["rtt"] = e.RTT e.fieldMap["rtt"] = e.RTT
e.fieldMap["loss"] = e.Loss e.fieldMap["loss"] = e.Loss
} }
func (e edge) clone(db *gorm.DB) edge { func (e edge) clone(db *gorm.DB) edge {
e.edgeDo.ReplaceConnPool(db.Statement.ConnPool) e.edgeDo.ReplaceConnPool(db.Statement.ConnPool)
e.Area.db = db.Session(&gorm.Session{Initialized: true})
e.Area.db.Statement.ConnPool = db.Statement.ConnPool
return e return e
} }
func (e edge) replaceDB(db *gorm.DB) edge { func (e edge) replaceDB(db *gorm.DB) edge {
e.edgeDo.ReplaceDB(db) e.edgeDo.ReplaceDB(db)
e.Area.db = db.Session(&gorm.Session{})
return e return e
} }
type edgeBelongsToArea struct {
db *gorm.DB
field.RelationField
Parent struct {
field.RelationField
}
}
func (a edgeBelongsToArea) Where(conds ...field.Expr) *edgeBelongsToArea {
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 edgeBelongsToArea) WithContext(ctx context.Context) *edgeBelongsToArea {
a.db = a.db.WithContext(ctx)
return &a
}
func (a edgeBelongsToArea) Session(session *gorm.Session) *edgeBelongsToArea {
a.db = a.db.Session(session)
return &a
}
func (a edgeBelongsToArea) Model(m *models.Edge) *edgeBelongsToAreaTx {
return &edgeBelongsToAreaTx{a.db.Model(m).Association(a.Name())}
}
func (a edgeBelongsToArea) Unscoped() *edgeBelongsToArea {
a.db = a.db.Unscoped()
return &a
}
type edgeBelongsToAreaTx struct{ tx *gorm.Association }
func (a edgeBelongsToAreaTx) Find() (result *models.Area, err error) {
return result, a.tx.Find(&result)
}
func (a edgeBelongsToAreaTx) Append(values ...*models.Area) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Append(targetValues...)
}
func (a edgeBelongsToAreaTx) Replace(values ...*models.Area) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Replace(targetValues...)
}
func (a edgeBelongsToAreaTx) Delete(values ...*models.Area) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Delete(targetValues...)
}
func (a edgeBelongsToAreaTx) Clear() error {
return a.tx.Clear()
}
func (a edgeBelongsToAreaTx) Count() int64 {
return a.tx.Count()
}
func (a edgeBelongsToAreaTx) Unscoped() *edgeBelongsToAreaTx {
a.tx = a.tx.Unscoped()
return &a
}
type edgeDo struct{ gen.DO } type edgeDo struct{ gen.DO }
func (e edgeDo) Debug() *edgeDo { func (e edgeDo) Debug() *edgeDo {

View File

@@ -20,6 +20,9 @@ var (
Admin *admin Admin *admin
AdminRole *adminRole AdminRole *adminRole
Announcement *announcement Announcement *announcement
Area *area
Article *article
ArticleGroup *articleGroup
BalanceActivity *balanceActivity BalanceActivity *balanceActivity
Bill *bill Bill *bill
Channel *channel Channel *channel
@@ -59,6 +62,9 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
Admin = &Q.Admin Admin = &Q.Admin
AdminRole = &Q.AdminRole AdminRole = &Q.AdminRole
Announcement = &Q.Announcement Announcement = &Q.Announcement
Area = &Q.Area
Article = &Q.Article
ArticleGroup = &Q.ArticleGroup
BalanceActivity = &Q.BalanceActivity BalanceActivity = &Q.BalanceActivity
Bill = &Q.Bill Bill = &Q.Bill
Channel = &Q.Channel Channel = &Q.Channel
@@ -99,6 +105,9 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
Admin: newAdmin(db, opts...), Admin: newAdmin(db, opts...),
AdminRole: newAdminRole(db, opts...), AdminRole: newAdminRole(db, opts...),
Announcement: newAnnouncement(db, opts...), Announcement: newAnnouncement(db, opts...),
Area: newArea(db, opts...),
Article: newArticle(db, opts...),
ArticleGroup: newArticleGroup(db, opts...),
BalanceActivity: newBalanceActivity(db, opts...), BalanceActivity: newBalanceActivity(db, opts...),
Bill: newBill(db, opts...), Bill: newBill(db, opts...),
Channel: newChannel(db, opts...), Channel: newChannel(db, opts...),
@@ -140,6 +149,9 @@ type Query struct {
Admin admin Admin admin
AdminRole adminRole AdminRole adminRole
Announcement announcement Announcement announcement
Area area
Article article
ArticleGroup articleGroup
BalanceActivity balanceActivity BalanceActivity balanceActivity
Bill bill Bill bill
Channel channel Channel channel
@@ -182,6 +194,9 @@ func (q *Query) clone(db *gorm.DB) *Query {
Admin: q.Admin.clone(db), Admin: q.Admin.clone(db),
AdminRole: q.AdminRole.clone(db), AdminRole: q.AdminRole.clone(db),
Announcement: q.Announcement.clone(db), Announcement: q.Announcement.clone(db),
Area: q.Area.clone(db),
Article: q.Article.clone(db),
ArticleGroup: q.ArticleGroup.clone(db),
BalanceActivity: q.BalanceActivity.clone(db), BalanceActivity: q.BalanceActivity.clone(db),
Bill: q.Bill.clone(db), Bill: q.Bill.clone(db),
Channel: q.Channel.clone(db), Channel: q.Channel.clone(db),
@@ -231,6 +246,9 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
Admin: q.Admin.replaceDB(db), Admin: q.Admin.replaceDB(db),
AdminRole: q.AdminRole.replaceDB(db), AdminRole: q.AdminRole.replaceDB(db),
Announcement: q.Announcement.replaceDB(db), Announcement: q.Announcement.replaceDB(db),
Area: q.Area.replaceDB(db),
Article: q.Article.replaceDB(db),
ArticleGroup: q.ArticleGroup.replaceDB(db),
BalanceActivity: q.BalanceActivity.replaceDB(db), BalanceActivity: q.BalanceActivity.replaceDB(db),
Bill: q.Bill.replaceDB(db), Bill: q.Bill.replaceDB(db),
Channel: q.Channel.replaceDB(db), Channel: q.Channel.replaceDB(db),
@@ -270,6 +288,9 @@ type queryCtx struct {
Admin *adminDo Admin *adminDo
AdminRole *adminRoleDo AdminRole *adminRoleDo
Announcement *announcementDo Announcement *announcementDo
Area *areaDo
Article *articleDo
ArticleGroup *articleGroupDo
BalanceActivity *balanceActivityDo BalanceActivity *balanceActivityDo
Bill *billDo Bill *billDo
Channel *channelDo Channel *channelDo
@@ -309,6 +330,9 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
Admin: q.Admin.WithContext(ctx), Admin: q.Admin.WithContext(ctx),
AdminRole: q.AdminRole.WithContext(ctx), AdminRole: q.AdminRole.WithContext(ctx),
Announcement: q.Announcement.WithContext(ctx), Announcement: q.Announcement.WithContext(ctx),
Area: q.Area.WithContext(ctx),
Article: q.Article.WithContext(ctx),
ArticleGroup: q.ArticleGroup.WithContext(ctx),
BalanceActivity: q.BalanceActivity.WithContext(ctx), BalanceActivity: q.BalanceActivity.WithContext(ctx),
Bill: q.Bill.WithContext(ctx), Bill: q.Bill.WithContext(ctx),
Channel: q.Channel.WithContext(ctx), Channel: q.Channel.WithContext(ctx),

View File

@@ -35,6 +35,7 @@ func newProxy(db *gorm.DB, opts ...gen.DOOption) proxy {
_proxy.Mac = field.NewString(tableName, "mac") _proxy.Mac = field.NewString(tableName, "mac")
_proxy.IP = field.NewField(tableName, "ip") _proxy.IP = field.NewField(tableName, "ip")
_proxy.Host = field.NewString(tableName, "host") _proxy.Host = field.NewString(tableName, "host")
_proxy.Port = field.NewInt(tableName, "port")
_proxy.Secret = field.NewString(tableName, "secret") _proxy.Secret = field.NewString(tableName, "secret")
_proxy.Type = field.NewInt(tableName, "type") _proxy.Type = field.NewInt(tableName, "type")
_proxy.Status = field.NewInt(tableName, "status") _proxy.Status = field.NewInt(tableName, "status")
@@ -261,8 +262,27 @@ func newProxy(db *gorm.DB, opts ...gen.DOOption) proxy {
}, },
Edge: struct { Edge: struct {
field.RelationField field.RelationField
Area struct {
field.RelationField
Parent struct {
field.RelationField
}
}
}{ }{
RelationField: field.NewRelation("Channels.Edge", "models.Edge"), RelationField: field.NewRelation("Channels.Edge", "models.Edge"),
Area: struct {
field.RelationField
Parent struct {
field.RelationField
}
}{
RelationField: field.NewRelation("Channels.Edge.Area", "models.Area"),
Parent: struct {
field.RelationField
}{
RelationField: field.NewRelation("Channels.Edge.Area.Parent", "models.Area"),
},
},
}, },
} }
@@ -283,6 +303,7 @@ type proxy struct {
Mac field.String Mac field.String
IP field.Field IP field.Field
Host field.String Host field.String
Port field.Int
Secret field.String Secret field.String
Type field.Int Type field.Int
Status field.Int Status field.Int
@@ -312,6 +333,7 @@ func (p *proxy) updateTableName(table string) *proxy {
p.Mac = field.NewString(table, "mac") p.Mac = field.NewString(table, "mac")
p.IP = field.NewField(table, "ip") p.IP = field.NewField(table, "ip")
p.Host = field.NewString(table, "host") p.Host = field.NewString(table, "host")
p.Port = field.NewInt(table, "port")
p.Secret = field.NewString(table, "secret") p.Secret = field.NewString(table, "secret")
p.Type = field.NewInt(table, "type") p.Type = field.NewInt(table, "type")
p.Status = field.NewInt(table, "status") p.Status = field.NewInt(table, "status")
@@ -332,7 +354,7 @@ func (p *proxy) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
} }
func (p *proxy) fillFieldMap() { func (p *proxy) fillFieldMap() {
p.fieldMap = make(map[string]field.Expr, 13) p.fieldMap = make(map[string]field.Expr, 14)
p.fieldMap["id"] = p.ID p.fieldMap["id"] = p.ID
p.fieldMap["created_at"] = p.CreatedAt p.fieldMap["created_at"] = p.CreatedAt
p.fieldMap["updated_at"] = p.UpdatedAt p.fieldMap["updated_at"] = p.UpdatedAt
@@ -341,6 +363,7 @@ func (p *proxy) fillFieldMap() {
p.fieldMap["mac"] = p.Mac p.fieldMap["mac"] = p.Mac
p.fieldMap["ip"] = p.IP p.fieldMap["ip"] = p.IP
p.fieldMap["host"] = p.Host p.fieldMap["host"] = p.Host
p.fieldMap["port"] = p.Port
p.fieldMap["secret"] = p.Secret p.fieldMap["secret"] = p.Secret
p.fieldMap["type"] = p.Type p.fieldMap["type"] = p.Type
p.fieldMap["status"] = p.Status p.fieldMap["status"] = p.Status
@@ -431,6 +454,12 @@ type proxyHasManyChannels struct {
} }
Edge struct { Edge struct {
field.RelationField field.RelationField
Area struct {
field.RelationField
Parent struct {
field.RelationField
}
}
} }
} }

View File

@@ -4,6 +4,7 @@ import (
"platform/pkg/env" "platform/pkg/env"
auth2 "platform/web/auth" auth2 "platform/web/auth"
"platform/web/core" "platform/web/core"
"platform/web/globals"
"platform/web/handlers" "platform/web/handlers"
"time" "time"
@@ -14,9 +15,10 @@ import (
func ApplyRouters(app *fiber.App) { func ApplyRouters(app *fiber.App) {
api := app.Group("/api") api := app.Group("/api")
publicRouter(api)
clientRouter(api)
userRouter(api) userRouter(api)
adminRouter(api) adminRouter(api)
clientRouter(api)
// 回调 // 回调
callbacks := app.Group("/callback") callbacks := app.Group("/callback")
@@ -28,7 +30,7 @@ func ApplyRouters(app *fiber.App) {
debug.Get("/sms/:phone", handlers.DebugGetSmsCode) debug.Get("/sms/:phone", handlers.DebugGetSmsCode)
debug.Get("/iden/clear/:phone", handlers.DebugIdentifyClear) debug.Get("/iden/clear/:phone", handlers.DebugIdentifyClear)
debug.Get("/session/now", func(ctx *fiber.Ctx) error { debug.Get("/session/now", func(ctx *fiber.Ctx) error {
rs, err := q.Session.Where(q.Session.AccessTokenExpires.Gt(time.Now())).Find() rs, err := q.Session.Where(q.Session.AccessTokenExpires.Gt(time.Now().UTC())).Find()
if err != nil { if err != nil {
return err return err
} }
@@ -37,11 +39,21 @@ func ApplyRouters(app *fiber.App) {
debug.Get("/test/err", func(ctx *fiber.Ctx) error { debug.Get("/test/err", func(ctx *fiber.Ctx) error {
return core.NewBizErr("测试错误") return core.NewBizErr("测试错误")
}) })
debug.Get("/trade/status/:trade_no", func(ctx *fiber.Ctx) error {
tradeNo := ctx.Params("trade_no")
resp, err := globals.SFTPay.QueryTrade(&globals.QueryTradeReq{
MchOrderNo: &tradeNo,
})
if err != nil {
return err
}
return ctx.JSON(resp)
})
} }
} }
// 用户接口路由 // 公开接口路由
func userRouter(api fiber.Router) { func publicRouter(api fiber.Router) {
// 认证 // 认证
auth := api.Group("/auth") auth := api.Group("/auth")
auth.Get("/authorize", auth2.AuthorizeGet) auth.Get("/authorize", auth2.AuthorizeGet)
@@ -50,6 +62,43 @@ func userRouter(api fiber.Router) {
auth.Post("/revoke", auth2.Revoke) auth.Post("/revoke", auth2.Revoke)
auth.Post("/introspect", auth2.Introspect) auth.Post("/introspect", auth2.Introspect)
// 套餐
resource := api.Group("/resource")
resource.Post("/price", handlers.ResourcePrice)
// 交易
trade := api.Group("/trade")
trade.Get("/check", handlers.TradeCheck)
// 前台
inquiry := api.Group("/inquiry")
inquiry.Post("/create", handlers.CreateInquiry)
// 产品
product := api.Group("/product")
product.Post("/list", handlers.AllProduct)
}
// 客户端接口路由
func clientRouter(api fiber.Router) {
client := api
// 验证短信令牌
client.Post("/verify/sms", handlers.SendSmsCode)
// 网关
// 通道管理
channel := client.Group("/channel")
channel.Post("/remove", handlers.RemoveChannels)
// 文章查看
article := api.Group("/article")
article.Post("/nav", handlers.NavArticle)
article.Post("/get", handlers.GetArticle)
}
// 用户接口路由
func userRouter(api fiber.Router) {
// 用户 // 用户
user := api.Group("/user") user := api.Group("/user")
user.Post("/update", handlers.UpdateUser) user.Post("/update", handlers.UpdateUser)
@@ -83,13 +132,18 @@ func userRouter(api fiber.Router) {
channel := api.Group("/channel") channel := api.Group("/channel")
channel.Post("/list", handlers.ListChannel) channel.Post("/list", handlers.ListChannel)
channel.Post("/create", handlers.CreateChannel) channel.Post("/create", handlers.CreateChannel)
channel.Post("/create/v2", handlers.CreateChannelV2)
channel.Post("/create/v3", handlers.CreateChannelV3)
// 地区
area := api.Group("/area")
area.Post("/list", handlers.ListArea)
// 交易 // 交易
trade := api.Group("/trade") trade := api.Group("/trade")
trade.Post("/create", handlers.TradeCreate) trade.Post("/create", handlers.TradeCreate)
trade.Post("/complete", handlers.TradeComplete) trade.Post("/complete", handlers.TradeComplete)
trade.Post("/cancel", handlers.TradeCancel) trade.Post("/cancel", handlers.TradeCancel)
trade.Get("/check", handlers.TradeCheck)
// 账单 // 账单
bill := api.Group("/bill") bill := api.Group("/bill")
@@ -108,47 +162,11 @@ func userRouter(api fiber.Router) {
announcement := api.Group("/announcement") announcement := api.Group("/announcement")
announcement.Post("/list", handlers.ListAnnouncements) announcement.Post("/list", handlers.ListAnnouncements)
// 网关
proxy := api.Group("/proxy")
proxy.Post("/online", handlers.ProxyReportOnline)
proxy.Post("/offline", handlers.ProxyReportOffline)
proxy.Post("/update", handlers.ProxyReportUpdate)
// 节点
edge := api.Group("/edge")
edge.Post("/assign", handlers.AssignEdge)
edge.Post("/all", handlers.AllEdgesAvailable)
// 前台
inquiry := api.Group("/inquiry")
inquiry.Post("/create", handlers.CreateInquiry)
// 产品
product := api.Group("/product")
product.Post("/list", handlers.AllProduct)
// 认证 // 认证
verify := api.Group("/verify") verify := api.Group("/verify")
verify.Post("/sms/password", handlers.SendSmsCodeForPassword) verify.Post("/sms/password", handlers.SendSmsCodeForPassword)
} }
// 客户端接口路由
func clientRouter(api fiber.Router) {
client := api
// 验证短信令牌
client.Post("/verify/sms", handlers.SendSmsCode)
// 套餐定价查询
resource := client.Group("/resource")
resource.Post("/price", handlers.ResourcePrice)
// 通道管理
channel := client.Group("/channel")
channel.Post("/remove", handlers.RemoveChannels)
}
// 管理员接口路由 // 管理员接口路由
func adminRouter(api fiber.Router) { func adminRouter(api fiber.Router) {
api = api.Group("/admin") api = api.Group("/admin")
@@ -214,6 +232,8 @@ func adminRouter(api fiber.Router) {
proxy.Post("/create", handlers.CreateProxy) proxy.Post("/create", handlers.CreateProxy)
proxy.Post("/update", handlers.UpdateProxy) proxy.Post("/update", handlers.UpdateProxy)
proxy.Post("/update/status", handlers.UpdateProxyStatus) proxy.Post("/update/status", handlers.UpdateProxyStatus)
proxy.Post("/sync/ports", handlers.SyncProxyPorts)
proxy.Post("/sync/chains", handlers.SyncProxyChains)
proxy.Post("/remove", handlers.RemoveProxy) proxy.Post("/remove", handlers.RemoveProxy)
// trade 交易 // trade 交易
@@ -273,4 +293,21 @@ func adminRouter(api fiber.Router) {
couponUser.Post("/create", handlers.CreateCouponUserByAdmin) couponUser.Post("/create", handlers.CreateCouponUserByAdmin)
couponUser.Post("/update", handlers.UpdateCouponUserByAdmin) couponUser.Post("/update", handlers.UpdateCouponUserByAdmin)
couponUser.Post("/remove", handlers.DeleteCouponUserByAdmin) couponUser.Post("/remove", handlers.DeleteCouponUserByAdmin)
// article 文档
var article = api.Group("/article")
article.Post("/page", handlers.PageArticleByAdmin)
article.Post("/get", handlers.GetArticleByAdmin)
article.Post("/create", handlers.CreateArticle)
article.Post("/update", handlers.UpdateArticle)
article.Post("/remove", handlers.DeleteArticle)
article.Post("/upload", handlers.UploadArticleImage)
// article-group 文档分组
var articleGroup = api.Group("/article-group")
articleGroup.Post("/page", handlers.PageArticleGroupByAdmin)
articleGroup.Post("/list", handlers.ListArticleGroupByAdmin)
articleGroup.Post("/create", handlers.CreateArticleGroup)
articleGroup.Post("/update", handlers.UpdateArticleGroup)
articleGroup.Post("/remove", handlers.DeleteArticleGroup)
} }

View File

@@ -110,7 +110,7 @@ func (s *adminService) Update(update *UpdateAdmin) error {
return q.Q.Transaction(func(q *q.Query) error { return q.Q.Transaction(func(q *q.Query) error {
// 更新管理员基本信息 // 更新管理员基本信息
if len(simples) > 0 { if len(simples) > 0 {
_, err := q.Admin. r, err := q.Admin.
Where( Where(
q.Admin.ID.Eq(update.Id), q.Admin.ID.Eq(update.Id),
q.Admin.Lock.Is(false), q.Admin.Lock.Is(false),
@@ -119,6 +119,9 @@ func (s *adminService) Update(update *UpdateAdmin) error {
if err != nil { if err != nil {
return err return err
} }
if r.RowsAffected == 0 {
return core.NewBizErr("管理员状态已过期")
}
} }
// 更新角色关联 // 更新角色关联
@@ -157,11 +160,17 @@ type UpdateAdmin struct {
} }
func (s *adminService) Remove(id int32) error { func (s *adminService) Remove(id int32) error {
_, err := q.Admin. r, err := q.Admin.
Where( Where(
q.Admin.ID.Eq(id), q.Admin.ID.Eq(id),
q.Admin.Lock.Is(false), q.Admin.Lock.Is(false),
). ).
UpdateColumn(q.Admin.DeletedAt, time.Now()) UpdateColumn(q.Admin.DeletedAt, time.Now())
if err != nil {
return err return err
}
if r.RowsAffected == 0 {
return core.NewBizErr("管理员状态已过期")
}
return nil
} }

View File

@@ -137,8 +137,14 @@ type UpdateAdminRole struct {
} }
func (r *adminRoleService) RemoveAdminRole(id int32) error { func (r *adminRoleService) RemoveAdminRole(id int32) error {
_, err := q.AdminRole.Where(q.AdminRole.ID.Eq(id)).UpdateColumn(q.AdminRole.DeletedAt, time.Now()) rs, err := q.AdminRole.Where(q.AdminRole.ID.Eq(id)).UpdateColumn(q.AdminRole.DeletedAt, time.Now())
if err != nil {
return err return err
}
if rs.RowsAffected == 0 {
return core.NewBizErr("管理员角色状态已过期")
}
return nil
} }
var AdminRoleModifyLock = "platform:admin_role_permissions:modify" var AdminRoleModifyLock = "platform:admin_role_permissions:modify"

112
web/services/area.go Normal file
View File

@@ -0,0 +1,112 @@
package services
import (
"errors"
"platform/pkg/u"
"platform/web/core"
m "platform/web/models"
q "platform/web/queries"
"gorm.io/gorm"
)
var Area = &areaService{}
type areaService struct{}
func (s *areaService) ListAreas() ([]*m.Area, error) {
areas, err := q.Area.
Order(q.Area.Level, q.Area.ParentID, q.Area.ID).
Find()
if err != nil {
return nil, core.NewServErr("查询地区失败", err)
}
return areas, nil
}
func (s *areaService) FindIdByFilter(prov *string, city *string) (*int32, error) {
prov = u.N(prov)
city = u.N(city)
if prov == nil && city == nil {
return nil, nil
}
switch {
case prov != nil && city == nil:
area, err := q.Area.
Where(
q.Area.Level.Eq(int(m.AreaLevelProvince)),
q.Area.Name.Eq(*prov),
).
Order(q.Area.ID).
Take()
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrAreaNotExist
}
if err != nil {
return nil, core.NewServErr("查询地区失败", err)
}
return u.P(area.ID), nil
case prov == nil && city != nil:
area, err := q.Area.
Where(
q.Area.Level.Eq(int(m.AreaLevelCity)),
q.Area.Name.Eq(*city),
).
Order(q.Area.ID).
Take()
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrAreaNotExist
}
if err != nil {
return nil, core.NewServErr("查询地区失败", err)
}
return u.P(area.ID), nil
default:
province, err := q.Area.
Where(
q.Area.Level.Eq(int(m.AreaLevelProvince)),
q.Area.Name.Eq(*prov),
).
Order(q.Area.ID).
Take()
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrAreaNotExist
}
if err != nil {
return nil, core.NewServErr("查询地区失败", err)
}
area, err := q.Area.
Where(
q.Area.ParentID.Eq(province.ID),
q.Area.Level.Eq(int(m.AreaLevelCity)),
q.Area.Name.Eq(*city),
).
Order(q.Area.ID).
Take()
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrAreaNotExist
}
if err != nil {
return nil, core.NewServErr("查询地区失败", err)
}
return u.P(area.ID), nil
}
}
func (s *areaService) Get(id int32) (*m.Area, error) {
area, err := q.Area.
Preload(q.Area.Parent).
Where(q.Area.ID.Eq(id)).
Take()
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrAreaNotExist
}
if err != nil {
return nil, core.NewServErr("查询地区失败", err)
}
return area, nil
}
var ErrAreaNotExist = core.NewBizErr("地区不存在")

389
web/services/article.go Normal file
View File

@@ -0,0 +1,389 @@
package services
import (
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"path"
"path/filepath"
"platform/pkg/env"
"platform/pkg/u"
"platform/web/core"
m "platform/web/models"
q "platform/web/queries"
"strings"
"time"
"github.com/google/uuid"
"gorm.io/gen/field"
"gorm.io/gorm"
)
var Article = &articleService{}
type articleService struct{}
var articleUploadMimeExt = map[string]string{
"image/gif": ".gif",
"image/jpeg": ".jpg",
"image/png": ".png",
"image/webp": ".webp",
}
type ArticleUploadResult struct {
URL string `json:"url"`
Path string `json:"path"`
OriginalName string `json:"original_name"`
Size int64 `json:"size"`
MimeType string `json:"mime_type"`
}
func (s *articleService) UploadImage(fileHeader *multipart.FileHeader, baseURL string) (*ArticleUploadResult, error) {
if fileHeader == nil {
return nil, core.NewBizErr("缺少上传文件")
}
if fileHeader.Size > int64(env.ArticleUploadMaxBytes) {
return nil, core.NewBizErr(fmt.Sprintf("图片大小不能超过 %s", formatUploadSizeLimit(env.ArticleUploadMaxBytes)))
}
mimeType, ext, err := detectArticleImage(fileHeader)
if err != nil {
return nil, err
}
now := time.Now()
year := now.Format("2006")
month := now.Format("01")
fileName := uuid.NewString() + ext
relativePath := path.Join("/uploads", "article", year, month, fileName)
targetDir := filepath.Join(env.UploadDir, "article", year, month)
finalPath := filepath.Join(targetDir, fileName)
if err := os.MkdirAll(targetDir, 0o755); err != nil {
return nil, core.NewServErr("创建上传目录失败", err)
}
src, err := fileHeader.Open()
if err != nil {
return nil, core.NewServErr("打开上传文件失败", err)
}
defer src.Close()
tmp, err := os.CreateTemp(targetDir, "upload-*"+ext)
if err != nil {
return nil, core.NewServErr("创建临时文件失败", err)
}
tmpPath := tmp.Name()
finished := false
defer func() {
if !finished {
_ = tmp.Close()
_ = os.Remove(tmpPath)
}
}()
limitedReader := &io.LimitedReader{R: src, N: int64(env.ArticleUploadMaxBytes) + 1}
written, err := io.Copy(tmp, limitedReader)
if err != nil {
return nil, core.NewServErr("保存上传文件失败", err)
}
if written > int64(env.ArticleUploadMaxBytes) {
return nil, core.NewBizErr(fmt.Sprintf("图片大小不能超过 %s", formatUploadSizeLimit(env.ArticleUploadMaxBytes)))
}
if err := tmp.Close(); err != nil {
return nil, core.NewServErr("关闭临时文件失败", err)
}
if err := os.Rename(tmpPath, finalPath); err != nil {
return nil, core.NewServErr("保存上传文件失败", err)
}
finished = true
cleanBaseURL := strings.TrimRight(baseURL, "/")
url := relativePath
if cleanBaseURL != "" {
url = cleanBaseURL + relativePath
}
return &ArticleUploadResult{
URL: url,
Path: relativePath,
OriginalName: filepath.Base(fileHeader.Filename),
Size: written,
MimeType: mimeType,
}, nil
}
func detectArticleImage(fileHeader *multipart.FileHeader) (string, string, error) {
file, err := fileHeader.Open()
if err != nil {
return "", "", core.NewServErr("打开上传文件失败", err)
}
defer file.Close()
buf := make([]byte, 512)
n, err := io.ReadFull(file, buf)
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
return "", "", core.NewServErr("读取上传文件失败", err)
}
mimeType := http.DetectContentType(buf[:n])
ext, ok := articleUploadMimeExt[mimeType]
if !ok {
return "", "", core.NewBizErr("仅支持 JPG、PNG、WEBP、GIF 图片")
}
return mimeType, ext, nil
}
func formatUploadSizeLimit(bytes int) string {
if bytes%(1024*1024) == 0 {
return fmt.Sprintf("%d MB", bytes/(1024*1024))
}
if bytes%1024 == 0 {
return fmt.Sprintf("%d KB", bytes/1024)
}
return fmt.Sprintf("%d bytes", bytes)
}
func (s *articleService) Page(req *PageArticleReq) (result []*m.Article, count int64, err error) {
do := q.Article.Where()
if req.Keyword != nil && *req.Keyword != "" {
do = do.Where(q.Article.Title.Like("%" + *req.Keyword + "%"))
}
if req.GroupID != nil {
do = do.Where(q.Article.GroupID.Eq(*req.GroupID))
}
if req.Status != nil {
do = do.Where(q.Article.Status.Eq(int(*req.Status)))
}
return q.Article.
Preload(q.Article.Group).
Where(do).
Omit(q.Article.Content).
Order(q.Article.Sort, q.Article.CreatedAt).
FindByPage(req.GetOffset(), req.GetLimit())
}
type PageArticleReq struct {
core.PageReq
Keyword *string `json:"keyword,omitempty"`
GroupID *int32 `json:"group_id,omitempty"`
Status *m.ArticleStatus `json:"status,omitempty"`
}
func (s *articleService) GetByAdmin(id int32) (*m.Article, error) {
article, err := q.Article.
Preload(q.Article.Group).
Where(q.Article.ID.Eq(id)).
Take()
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, core.NewBizErr("文档不存在")
}
if err != nil {
return nil, err
}
return article, nil
}
func (s *articleService) Create(data CreateArticleData) error {
if err := s.ensureGroupExists(data.GroupID); err != nil {
return err
}
return q.Article.Create(&m.Article{
GroupID: data.GroupID,
Title: data.Title,
Content: data.Content,
Sort: u.Else(data.Sort, 0),
Status: u.Else(data.Status, m.ArticleStatusEnabled),
})
}
type CreateArticleData struct {
GroupID int32 `json:"group_id" validate:"required"`
Title string `json:"title" validate:"required"`
Content *string `json:"content"`
Sort *int32 `json:"sort"`
Status *m.ArticleStatus `json:"status"`
}
func (s *articleService) Update(data UpdateArticleData) error {
if data.GroupID != nil {
if err := s.ensureGroupExists(*data.GroupID); err != nil {
return err
}
}
do := make([]field.AssignExpr, 0)
if data.GroupID != nil {
do = append(do, q.Article.GroupID.Value(*data.GroupID))
}
if data.Title != nil {
do = append(do, q.Article.Title.Value(*data.Title))
}
if data.Content != nil {
do = append(do, q.Article.Content.Value(*data.Content))
}
if data.Sort != nil {
do = append(do, q.Article.Sort.Value(*data.Sort))
}
if data.Status != nil {
do = append(do, q.Article.Status.Value(int(*data.Status)))
}
if len(do) == 0 {
return nil
}
r, err := q.Article.Where(q.Article.ID.Eq(data.ID)).UpdateSimple(do...)
if err != nil {
return err
}
if r.RowsAffected == 0 {
return core.NewBizErr("文档状态已过期")
}
return nil
}
type UpdateArticleData struct {
ID int32 `json:"id" validate:"required"`
GroupID *int32 `json:"group_id"`
Title *string `json:"title"`
Content *string `json:"content"`
Sort *int32 `json:"sort"`
Status *m.ArticleStatus `json:"status"`
}
func (s *articleService) Delete(id int32) error {
r, err := q.Article.Where(q.Article.ID.Eq(id)).UpdateColumn(q.Article.DeletedAt, time.Now())
if err != nil {
return err
}
if r.RowsAffected == 0 {
return core.NewBizErr("文档状态已过期")
}
return nil
}
func (s *articleService) Nav() ([]*ArticleNavGroup, error) {
groups, err := q.ArticleGroup.
Where(q.ArticleGroup.Status.Eq(int(m.ArticleGroupStatusEnabled))).
Order(q.ArticleGroup.Sort, q.ArticleGroup.CreatedAt).
Find()
if err != nil {
return nil, err
}
if len(groups) == 0 {
return []*ArticleNavGroup{}, nil
}
groupIDs := make([]int32, 0, len(groups))
result := make([]*ArticleNavGroup, 0, len(groups))
groupMap := make(map[int32]*ArticleNavGroup, len(groups))
for _, group := range groups {
groupIDs = append(groupIDs, group.ID)
item := &ArticleNavGroup{
ID: group.ID,
Name: group.Name,
Code: group.Code,
Articles: []*ArticleNavArticle{},
}
result = append(result, item)
groupMap[group.ID] = item
}
articles, err := q.Article.
Where(
q.Article.GroupID.In(groupIDs...),
q.Article.Status.Eq(int(m.ArticleStatusEnabled)),
).
Omit(q.Article.Content).
Order(q.Article.Sort, q.Article.CreatedAt).
Find()
if err != nil {
return nil, err
}
for _, article := range articles {
group := groupMap[article.GroupID]
if group == nil {
continue
}
group.Articles = append(group.Articles, &ArticleNavArticle{
ID: article.ID,
Title: article.Title,
UpdatedAt: article.UpdatedAt,
})
}
return result, nil
}
type ArticleNavGroup struct {
ID int32 `json:"id"`
Name string `json:"name"`
Code string `json:"code"`
Articles []*ArticleNavArticle `json:"articles"`
}
type ArticleNavArticle struct {
ID int32 `json:"id"`
Title string `json:"title"`
UpdatedAt time.Time `json:"updated_at"`
}
func (s *articleService) GetPublic(id int32) (*ArticlePublicDetail, error) {
article, err := q.Article.
Preload(q.Article.Group).
Where(
q.Article.ID.Eq(id),
q.Article.Status.Eq(int(m.ArticleStatusEnabled)),
).
Take()
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, core.NewBizErr("文档不存在")
}
if err != nil {
return nil, err
}
if article.Group == nil || article.Group.Status != m.ArticleGroupStatusEnabled {
return nil, core.NewBizErr("文档不存在")
}
return &ArticlePublicDetail{
ID: article.ID,
Title: article.Title,
Content: article.Content,
UpdatedAt: article.UpdatedAt,
Group: &ArticlePublicGroup{
ID: article.Group.ID,
Name: article.Group.Name,
Code: article.Group.Code,
},
}, nil
}
type ArticlePublicDetail struct {
ID int32 `json:"id"`
Title string `json:"title"`
Content *string `json:"content,omitempty"`
UpdatedAt time.Time `json:"updated_at"`
Group *ArticlePublicGroup `json:"group"`
}
type ArticlePublicGroup struct {
ID int32 `json:"id"`
Name string `json:"name"`
Code string `json:"code"`
}
func (s *articleService) ensureGroupExists(groupID int32) error {
_, err := q.ArticleGroup.Where(q.ArticleGroup.ID.Eq(groupID)).Take()
if errors.Is(err, gorm.ErrRecordNotFound) {
return core.NewBizErr("文档分组不存在")
}
return err
}

View File

@@ -0,0 +1,128 @@
package services
import (
"errors"
"platform/pkg/u"
"platform/web/core"
m "platform/web/models"
q "platform/web/queries"
"time"
"gorm.io/gen/field"
"gorm.io/gorm"
)
var ArticleGroup = &articleGroupService{}
type articleGroupService struct{}
func (s *articleGroupService) All() (result []*m.ArticleGroup, err error) {
return q.ArticleGroup.
Order(q.ArticleGroup.Sort, q.ArticleGroup.CreatedAt).
Find()
}
func (s *articleGroupService) Page(req *PageArticleGroupReq) (result []*m.ArticleGroup, count int64, err error) {
do := q.ArticleGroup.Where()
if req.Keyword != nil && *req.Keyword != "" {
do = do.Where(
q.ArticleGroup.Where(
q.ArticleGroup.Name.Like("%" + *req.Keyword + "%"),
).Or(
q.ArticleGroup.Code.Like("%" + *req.Keyword + "%"),
),
)
}
if req.Status != nil {
do = do.Where(q.ArticleGroup.Status.Eq(int(*req.Status)))
}
return q.ArticleGroup.
Where(do).
Order(q.ArticleGroup.Sort, q.ArticleGroup.CreatedAt).
FindByPage(req.GetOffset(), req.GetLimit())
}
type PageArticleGroupReq struct {
core.PageReq
Keyword *string `json:"keyword,omitempty"`
Status *m.ArticleGroupStatus `json:"status,omitempty"`
}
func (s *articleGroupService) Create(data CreateArticleGroupData) error {
err := q.ArticleGroup.Create(&m.ArticleGroup{
Name: data.Name,
Code: data.Code,
Sort: u.Else(data.Sort, 0),
Status: u.Else(data.Status, m.ArticleGroupStatusEnabled),
})
if errors.Is(err, gorm.ErrDuplicatedKey) {
return core.NewBizErr("文档分组编码已存在")
}
return err
}
type CreateArticleGroupData struct {
Name string `json:"name" validate:"required"`
Code string `json:"code" validate:"required"`
Sort *int32 `json:"sort"`
Status *m.ArticleGroupStatus `json:"status"`
}
func (s *articleGroupService) Update(data UpdateArticleGroupData) error {
do := make([]field.AssignExpr, 0)
if data.Name != nil {
do = append(do, q.ArticleGroup.Name.Value(*data.Name))
}
if data.Code != nil {
do = append(do, q.ArticleGroup.Code.Value(*data.Code))
}
if data.Sort != nil {
do = append(do, q.ArticleGroup.Sort.Value(*data.Sort))
}
if data.Status != nil {
do = append(do, q.ArticleGroup.Status.Value(int(*data.Status)))
}
if len(do) == 0 {
return nil
}
r, err := q.ArticleGroup.Where(q.ArticleGroup.ID.Eq(data.ID)).UpdateSimple(do...)
if errors.Is(err, gorm.ErrDuplicatedKey) {
return core.NewBizErr("文档分组编码已存在")
}
if err != nil {
return err
}
if r.RowsAffected == 0 {
return core.NewBizErr("文档分组状态已过期")
}
return nil
}
type UpdateArticleGroupData struct {
ID int32 `json:"id" validate:"required"`
Name *string `json:"name"`
Code *string `json:"code"`
Sort *int32 `json:"sort"`
Status *m.ArticleGroupStatus `json:"status"`
}
func (s *articleGroupService) Delete(id int32) error {
count, err := q.Article.Where(q.Article.GroupID.Eq(id)).Count()
if err != nil {
return err
}
if count > 0 {
return core.NewBizErr("分组下仍有关联文章,无法删除")
}
r, err := q.ArticleGroup.Where(q.ArticleGroup.ID.Eq(id)).UpdateColumn(q.ArticleGroup.DeletedAt, time.Now())
if err != nil {
return err
}
if r.RowsAffected == 0 {
return core.NewBizErr("文档分组状态已过期")
}
return nil
}

View File

@@ -0,0 +1,144 @@
package services
import (
"bytes"
"encoding/base64"
"mime/multipart"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"platform/pkg/env"
)
func TestArticleUploadImageSuccess(t *testing.T) {
restore := snapshotUploadEnv()
defer restore()
env.UploadDir = t.TempDir()
env.ArticleUploadMaxBytes = 5 * 1024 * 1024
fileHeader := newMultipartFileHeader(t, "file", "pixel.png", mustDecodeBase64(t, onePixelPNGBase64))
result, err := Article.UploadImage(fileHeader, "https://example.com")
if err != nil {
t.Fatalf("UploadImage returned error: %v", err)
}
if result.MimeType != "image/png" {
t.Fatalf("unexpected mime type: %s", result.MimeType)
}
if !strings.HasPrefix(result.Path, "/uploads/article/") {
t.Fatalf("unexpected path: %s", result.Path)
}
if result.URL != "https://example.com"+result.Path {
t.Fatalf("unexpected url: %s", result.URL)
}
if result.OriginalName != "pixel.png" {
t.Fatalf("unexpected original name: %s", result.OriginalName)
}
savedPath := filepath.Join(env.UploadDir, filepath.FromSlash(strings.TrimPrefix(result.Path, "/uploads/")))
info, err := os.Stat(savedPath)
if err != nil {
t.Fatalf("saved file not found: %v", err)
}
if info.Size() != result.Size {
t.Fatalf("unexpected saved size: got %d want %d", info.Size(), result.Size)
}
}
func TestArticleUploadImageRejectsUnsupportedType(t *testing.T) {
restore := snapshotUploadEnv()
defer restore()
env.UploadDir = t.TempDir()
env.ArticleUploadMaxBytes = 5 * 1024 * 1024
fileHeader := newMultipartFileHeader(t, "file", "note.txt", []byte("not an image"))
_, err := Article.UploadImage(fileHeader, "https://example.com")
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "仅支持 JPG、PNG、WEBP、GIF 图片") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestArticleUploadImageRejectsOversizeFile(t *testing.T) {
restore := snapshotUploadEnv()
defer restore()
env.UploadDir = t.TempDir()
env.ArticleUploadMaxBytes = 8
fileHeader := newMultipartFileHeader(t, "file", "large.png", bytes.Repeat([]byte("a"), 9))
_, err := Article.UploadImage(fileHeader, "https://example.com")
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "图片大小不能超过") {
t.Fatalf("unexpected error: %v", err)
}
}
func newMultipartFileHeader(t *testing.T, fieldName string, fileName string, content []byte) *multipart.FileHeader {
t.Helper()
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, err := writer.CreateFormFile(fieldName, fileName)
if err != nil {
t.Fatalf("CreateFormFile failed: %v", err)
}
if _, err := part.Write(content); err != nil {
t.Fatalf("Write content failed: %v", err)
}
if err := writer.Close(); err != nil {
t.Fatalf("Close multipart writer failed: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/", &body)
req.Header.Set("Content-Type", writer.FormDataContentType())
if err := req.ParseMultipartForm(int64(body.Len()) + 1024); err != nil {
t.Fatalf("ParseMultipartForm failed: %v", err)
}
file, fileHeader, err := req.FormFile(fieldName)
if err != nil {
t.Fatalf("FormFile failed: %v", err)
}
_ = file.Close()
return fileHeader
}
func mustDecodeBase64(t *testing.T, value string) []byte {
t.Helper()
data, err := base64.StdEncoding.DecodeString(value)
if err != nil {
t.Fatalf("DecodeString failed: %v", err)
}
return data
}
func snapshotUploadEnv() func() {
uploadDir := env.UploadDir
uploadPublicBaseURL := env.UploadPublicBaseURL
articleUploadMaxBytes := env.ArticleUploadMaxBytes
return func() {
env.UploadDir = uploadDir
env.UploadPublicBaseURL = uploadPublicBaseURL
env.ArticleUploadMaxBytes = articleUploadMaxBytes
}
}
const onePixelPNGBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+a7KQAAAAASUVORK5CYII="

View File

@@ -4,45 +4,447 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"log/slog"
"math/rand/v2" "math/rand/v2"
"net/netip" "net/netip"
"platform/pkg/u" "platform/pkg/u"
"platform/web/core" "platform/web/core"
e "platform/web/events"
g "platform/web/globals" g "platform/web/globals"
"platform/web/globals/orm"
m "platform/web/models" m "platform/web/models"
q "platform/web/queries" q "platform/web/queries"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/hibiken/asynq"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
"gorm.io/gen"
"gorm.io/gen/field" "gorm.io/gen/field"
) )
// 通道服务 // 通道服务
var Channel = &channelServer{ var Channel = &channelServer{
provider: &channelBaiyinProvider{}, provider: &channelGostProvider{},
} }
type ChannelServiceProvider interface { type channelProvider interface {
CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter *EdgeFilter) ([]*m.Channel, error) selectProxy(count int) (*m.Proxy, error)
RemoveChannels(batch string) error prepareCreate(ctx *channelCreateContext) (*channelCreateResult, error)
ClearExpiredChannels(proxyId int32) (int, error) removeRemote(batchNo string, batch *usedChanBatch) error
} }
type channelServer struct { type channelServer struct {
provider ChannelServiceProvider provider channelProvider
} }
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, resourceNo string, authWhitelist bool, authPassword bool, count int, edgeFilter *EdgeFilter) ([]*m.Channel, error) {
return s.provider.CreateChannels(source, resourceId, authWhitelist, authPassword, count, edgeFilter) if edgeFilter == nil {
edgeFilter = &EdgeFilter{}
}
var area *m.Area
if edgeFilter.AreaID != nil {
var err error
area, err = Area.Get(*edgeFilter.AreaID)
if err != nil {
return nil, err
}
if err := validateChannelArea(area); err != nil {
return nil, err
}
}
now := time.Now()
batchNo := ID.GenReadable("bat")
var channels []*m.Channel
var whitelistText *string
err := g.Redsync.WithLock(lockChannelCreateKey(resourceNo), func() error {
resource, whitelists, err := ensure(now, source, resourceNo, authWhitelist, count)
if err != nil {
return err
}
if authWhitelist {
joined := strings.Join(whitelists, ",")
whitelistText = &joined
}
expire := now.Add(resource.Live)
proxy, err := s.provider.selectProxy(count)
if err != nil {
return err
}
ports, err := selectPorts(proxy.ID, batchNo, count, expire)
if err != nil {
return err
}
createCtx := &channelCreateContext{
Now: now,
Source: source,
Resource: resource,
Proxy: proxy,
BatchNo: batchNo,
Ports: ports,
Expire: expire,
Count: count,
Filter: edgeFilter,
Area: area,
AuthWhitelist: authWhitelist,
AuthPassword: authPassword,
Whitelists: whitelists,
WhitelistText: whitelistText,
}
result, err := s.provider.prepareCreate(createCtx)
if err != nil {
return err
}
if result.applyRemote != nil {
if err := result.applyRemote(); err != nil {
return err
}
}
if err := persistChannelCreate(createCtx, result.Channels); err != nil {
return err
}
channels = result.Channels
return nil
})
if err != nil {
return nil, err
}
return channels, nil
} }
func (s *channelServer) RemoveChannels(batch string) error { func (s *channelServer) RemoveChannels(batch string) error {
return s.provider.RemoveChannels(batch) return g.Redsync.WithLock(lockChannelRemoveKey(batch), func() error {
start := time.Now()
usedBatch, err := findUsedChanBatch(batch)
if err != nil {
return err
}
if usedBatch == nil {
slog.Debug("通道为空,跳过清理", "batch", batch)
return nil
}
if err := s.provider.removeRemote(batch, usedBatch); err != nil {
return err
}
if err := freeChans(usedBatch.ProxyID, batch); err != nil {
return err
}
slog.Debug("清除通道配置", "proxy", usedBatch.ProxyID, "batch", batch, "duration", time.Since(start).String())
return nil
})
} }
func (s *channelServer) ClearExpiredChannels(proxyId int32) (int, error) { func (s *channelServer) ClearExpiredChannels(proxyId int32) (int, error) {
return s.provider.ClearExpiredChannels(proxyId) batchSet, err := findExpiredChannelBatches(proxyId, time.Now())
if err != nil {
return 0, err
}
slog.Info("批量清理过期通道", "count", len(batchSet))
for batchNo := range batchSet {
if err := s.RemoveChannels(batchNo); err != nil {
slog.Error("清理过期通道失败", "batch", batchNo, "error", err)
}
}
return len(batchSet), nil
}
type channelCreateContext struct {
Now time.Time
Source netip.Addr
Resource *ResourceView
Proxy *m.Proxy
BatchNo string
Ports []netip.AddrPort
Expire time.Time
Count int
Filter *EdgeFilter
Area *m.Area
AuthWhitelist bool
AuthPassword bool
Whitelists []string
WhitelistText *string
}
type channelCreateResult struct {
Channels []*m.Channel
applyRemote func() error
}
func newBaseChannel(ctx *channelCreateContext, port uint16) *m.Channel {
prov, city := areaProvinceCity(ctx.Area)
return &m.Channel{
UserID: ctx.Resource.User.ID,
ResourceID: ctx.Resource.ID,
BatchNo: ctx.BatchNo,
ProxyID: ctx.Proxy.ID,
Host: u.Else(ctx.Proxy.Host, ctx.Proxy.IP.String()),
Port: port,
FilterISP: ctx.Filter.Isp,
FilterProv: prov,
FilterCity: city,
ExpiredAt: ctx.Expire,
Proxy: ctx.Proxy,
}
}
func applyChannelAuth(ctx *channelCreateContext, channel *m.Channel) (username string, password string, ok bool) {
if ctx.AuthWhitelist {
channel.Whitelists = ctx.WhitelistText
}
if !ctx.AuthPassword {
return "", "", false
}
username, password = genPassPair()
channel.Username = &username
channel.Password = &password
return username, password, true
}
func persistChannelCreate(ctx *channelCreateContext, channels []*m.Channel) error {
prov, city := areaProvinceCity(ctx.Area)
return q.Q.Transaction(func(tx *q.Query) error {
var (
result gen.ResultInfo
err error
)
switch ctx.Resource.Type {
case m.ResourceTypeShort:
result, err = tx.ResourceShort.
Where(
tx.ResourceShort.ID.Eq(*ctx.Resource.ShortId),
tx.ResourceShort.Used.Eq(ctx.Resource.Used),
tx.ResourceShort.Daily.Eq(ctx.Resource.Daily),
).
UpdateSimple(
tx.ResourceShort.Used.Add(int32(ctx.Count)),
tx.ResourceShort.Daily.Value(int32(ctx.Resource.Today+ctx.Count)),
tx.ResourceShort.LastAt.Value(ctx.Now),
)
case m.ResourceTypeLong:
result, err = tx.ResourceLong.
Where(
tx.ResourceLong.ID.Eq(*ctx.Resource.LongId),
tx.ResourceLong.Used.Eq(ctx.Resource.Used),
tx.ResourceLong.Daily.Eq(ctx.Resource.Daily),
).
UpdateSimple(
tx.ResourceLong.Used.Add(int32(ctx.Count)),
tx.ResourceLong.Daily.Value(int32(ctx.Resource.Today+ctx.Count)),
tx.ResourceLong.LastAt.Value(ctx.Now),
)
default:
return core.NewBizErr("套餐类型不正确,无法更新")
}
if err != nil {
return core.NewServErr("更新套餐使用记录失败", err)
}
if result.RowsAffected == 0 {
return core.NewBizErr("套餐状态已过期")
}
if err := tx.Channel.Omit(field.AssociationFields).Create(channels...); err != nil {
return core.NewServErr("保存通道失败", err)
}
if err := tx.LogsUserUsage.Create(&m.LogsUserUsage{
UserID: ctx.Resource.User.ID,
ResourceID: ctx.Resource.ID,
BatchNo: ctx.BatchNo,
Count: int32(ctx.Count),
ISP: u.X(ctx.Filter.Isp.String()),
Prov: u.Ternary(ctx.Filter.AreaID != nil, prov, nil),
City: u.Ternary(ctx.Filter.AreaID != nil, city, nil),
IP: orm.Inet{Addr: ctx.Source},
Time: ctx.Now,
}); err != nil {
return core.NewServErr("保存用户使用记录失败", err)
}
return nil
})
}
func validateChannelArea(area *m.Area) error {
if area == nil {
return nil
}
switch area.Level {
case m.AreaLevelProvince:
return nil
case m.AreaLevelCity:
if area.ParentID == nil || area.Parent == nil {
return core.NewServErr("地区数据异常", nil)
}
return nil
default:
return core.NewBizErr("地区层级不支持")
}
}
func areaProvinceCity(area *m.Area) (prov *string, city *string) {
if area == nil {
return nil, nil
}
switch area.Level {
case m.AreaLevelProvince:
return u.P(area.Name), nil
case m.AreaLevelCity:
return u.P(area.Parent.Name), u.P(area.Name)
default:
return nil, nil
}
}
func findExpiredChannelBatches(proxyId int32, now time.Time) (map[string]struct{}, error) {
keys, err := g.Redis.Keys(context.Background(), usedChansKey(proxyId, "*")).Result()
if err != nil {
return nil, core.NewServErr("查询使用中通道失败", err)
}
if len(keys) == 0 {
return map[string]struct{}{}, nil
}
batchList := make([]string, len(keys))
batchSet := make(map[string]struct{}, len(keys))
for i, key := range keys {
parsed, err := parseUsedChanKey(key)
if err != nil {
return nil, err
}
batchList[i] = parsed.BatchNo
batchSet[parsed.BatchNo] = struct{}{}
}
var batchQueried []struct{ BatchNo string }
err = q.Channel.
Select(q.Channel.BatchNo).
Where(
q.Channel.BatchNo.In(batchList...),
q.Channel.ExpiredAt.Gte(now.UTC()),
).
Group(q.Channel.BatchNo).
Scan(&batchQueried)
if err != nil {
return nil, core.NewServErr("查询过期通道失败", err)
}
for _, batch := range batchQueried {
delete(batchSet, batch.BatchNo)
}
return batchSet, nil
}
func lockChannelCreateKey(resourceNo string) string {
return fmt.Sprintf("platform:channel:create:%s", resourceNo)
}
func lockChannelRemoveKey(bid string) string {
return fmt.Sprintf("platform:batch:remove_expired:%s", bid)
}
func selectPorts(proxyId int32, batchNo string, count int, expire time.Time) ([]netip.AddrPort, error) {
chans, err := lockChans(proxyId, batchNo, count)
if err != nil {
return nil, core.NewBizErr("无可用通道,请稍后再试", err)
}
_, err = g.Asynq.Enqueue(
e.NewRemoveChannel(batchNo),
asynq.ProcessAt(expire),
)
if err != nil {
return nil, core.NewServErr("注册异步关闭通道任务失败", err)
}
return chans, nil
}
func selectProxyByType(proxyType m.ProxyType, count int) (*m.Proxy, error) {
proxies, err := q.Proxy.Where(
q.Proxy.Type.Eq(int(proxyType)),
q.Proxy.Status.Eq(int(m.ProxyStatusOnline)),
).Find()
if err != nil {
return nil, core.NewBizErr("获取可用代理失败", err)
}
if len(proxies) == 0 {
return nil, core.NewBizErr("无可用代理")
}
var bestProxy *m.Proxy
maxCount := -1
for _, proxy := range proxies {
idCount, err := g.Redis.SCard(context.Background(), freeChansKey(proxy.ID)).Result()
if err != nil {
return nil, core.NewServErr("查询可用通道数量失败", err)
}
if idCount > int64(maxCount) {
maxCount = int(idCount)
bestProxy = proxy
}
}
if maxCount < count {
return nil, core.NewBizErr("无空闲代理")
}
return bestProxy, nil
}
func (s *channelServer) RefreshEdges() error {
// 仅白银网关支持边缘节点刷新GOST 不参与此流程。
proxies, err := q.Proxy.Where(
q.Proxy.Type.Eq(int(m.ProxyTypeBaiYin)),
q.Proxy.Status.Eq(int(m.ProxyStatusOnline)),
).Find()
if err != nil {
return fmt.Errorf("查询网关失败: %w", err)
}
for _, proxy := range proxies {
gateway, err := proxyGateway(proxy)
if err != nil {
return core.NewServErr("创建代理网关失败", err)
}
// 选取随机节点
edges, err := gateway.GatewayEdge(&g.GatewayEdgeReq{
Assigned: u.P(false),
Limit: u.P(1000),
})
if err != nil {
return fmt.Errorf("获取边缘节点失败: %w", err)
}
// 提交断开配置
edgeIds := make([]string, 0, len(edges))
for id, _ := range edges {
edgeIds = append(edgeIds, id)
}
g.Cloud.CloudDisconnect(&g.CloudDisconnectReq{
Uuid: proxy.Mac,
Edge: &edgeIds,
})
}
return nil
} }
// 授权方式 // 授权方式
@@ -72,12 +474,23 @@ func genPassPair() (string, string) {
return string(username), string(password) return string(username), string(password)
} }
func FindResourceNoById(resourceId int32) (string, error) {
resource, err := q.Resource.
Select(q.Resource.ResourceNo).
Where(q.Resource.ID.Eq(resourceId)).
Take()
if err != nil {
return "", ErrResourceNotExist
}
return u.Z(resource.ResourceNo), nil
}
// 查找资源 // 查找资源
func findResource(resourceId int32, now time.Time) (*ResourceView, error) { func findResourceViewByNo(resourceNo string, now time.Time) (*ResourceView, error) {
resource, err := q.Resource. resource, err := q.Resource.
Preload(field.Associations). Preload(field.Associations).
Where( Where(
q.Resource.ID.Eq(resourceId), q.Resource.ResourceNo.Eq(resourceNo),
q.Resource.Active.Is(true), q.Resource.Active.Is(true),
). ).
Take() Take()
@@ -88,7 +501,7 @@ func findResource(resourceId int32, now time.Time) (*ResourceView, error) {
return nil, ErrResourceNotExist return nil, ErrResourceNotExist
} }
var info = &ResourceView{ var info = &ResourceView{
Id: resource.ID, ID: resource.ID,
User: *resource.User, User: *resource.User,
Active: resource.Active, Active: resource.Active,
Type: resource.Type, Type: resource.Type,
@@ -114,7 +527,7 @@ func findResource(resourceId int32, now time.Time) (*ResourceView, error) {
var sub = resource.Long var sub = resource.Long
info.LongId = &sub.ID info.LongId = &sub.ID
info.ExpireAt = sub.ExpireAt info.ExpireAt = sub.ExpireAt
info.Live = time.Duration(sub.Live) * time.Hour info.Live = time.Duration(sub.Live) * time.Minute
info.Mode = sub.Type info.Mode = sub.Type
info.Quota = sub.Quota info.Quota = sub.Quota
info.Used = sub.Used info.Used = sub.Used
@@ -134,7 +547,7 @@ func findResource(resourceId int32, now time.Time) (*ResourceView, error) {
// ResourceView 套餐数据的简化视图,便于直接获取主要数据 // ResourceView 套餐数据的简化视图,便于直接获取主要数据
type ResourceView struct { type ResourceView struct {
Id int32 ID int32
User m.User User m.User
Active bool Active bool
Type m.ResourceType Type m.ResourceType
@@ -152,13 +565,13 @@ type ResourceView struct {
} }
// 检查用户是否可提取 // 检查用户是否可提取
func ensure(now time.Time, source netip.Addr, resourceId int32, authWhitelist bool, count int) (*ResourceView, []string, error) { func ensure(now time.Time, source netip.Addr, resourceNo string, authWhitelist bool, count int) (*ResourceView, []string, error) {
if count > 400 { if count > 400 {
return nil, nil, core.NewBizErr("单次最多提取 400 个") return nil, nil, core.NewBizErr("单次最多提取 400 个")
} }
// 获取用户套餐 // 获取用户套餐
resource, err := findResource(resourceId, now) resource, err := findResourceViewByNo(resourceNo, now)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -228,6 +641,91 @@ func usedChansKey(proxy int32, batch string) string {
return "channel:used:" + strconv.Itoa(int(proxy)) + ":" + batch return "channel:used:" + strconv.Itoa(int(proxy)) + ":" + batch
} }
type usedChanBatch struct {
ProxyID int32
Chans []netip.AddrPort
}
type usedChanKey struct {
ProxyID int32
BatchNo string
}
func findUsedChanBatch(batch string) (*usedChanBatch, error) {
keys, err := g.Redis.Keys(context.Background(), "channel:used:*:"+batch).Result()
if err != nil {
return nil, core.NewServErr("查询使用中通道失败", err)
}
key, ok, err := selectUsedChanBatchKey(batch, keys)
if err != nil {
return nil, err
}
if !ok {
return nil, nil
}
chans, err := g.Redis.LRange(context.Background(), key, 0, -1).Result()
if err != nil {
return nil, core.NewServErr("查询使用中通道失败", err)
}
return parseUsedChanBatch(key, chans)
}
func selectUsedChanBatchKey(batch string, keys []string) (string, bool, error) {
switch len(keys) {
case 0:
return "", false, nil
case 1:
return keys[0], true, nil
default:
slog.Error("batchNo 全局唯一约束被破坏", "batch", batch, "keys", keys)
return "", false, core.NewServErr(
fmt.Sprintf("检测到重复 usedChans 键batchNo 全局唯一被破坏: %s", batch),
fmt.Errorf("keys=%s", strings.Join(keys, ",")),
)
}
}
func parseUsedChanBatch(key string, chans []string) (*usedChanBatch, error) {
parsed, err := parseUsedChanKey(key)
if err != nil {
return nil, err
}
addrs := make([]netip.AddrPort, len(chans))
for i, ch := range chans {
addr, err := netip.ParseAddrPort(ch)
if err != nil {
return nil, core.NewServErr(fmt.Sprintf("解析通道数据失败: %s", ch), err)
}
addrs[i] = addr
}
return &usedChanBatch{
ProxyID: parsed.ProxyID,
Chans: addrs,
}, nil
}
func parseUsedChanKey(key string) (*usedChanKey, error) {
parts := strings.Split(key, ":")
if len(parts) != 4 {
return nil, core.NewServErr(fmt.Sprintf("使用中通道键格式错误: %s", key), nil)
}
proxyID, err := strconv.Atoi(parts[2])
if err != nil {
return nil, core.NewServErr(fmt.Sprintf("使用中通道键格式错误: %s", key), err)
}
return &usedChanKey{
ProxyID: int32(proxyID),
BatchNo: parts[3],
}, nil
}
// 扩容通道 // 扩容通道
func regChans(proxy int32, chans []netip.AddrPort) error { func regChans(proxy int32, chans []netip.AddrPort) error {
strs := make([]any, len(chans)) strs := make([]any, len(chans))
@@ -328,12 +826,24 @@ redis.call("DEL", batch_key)
return 1 return 1
`) `)
// 节点筛选条件
type EdgeFilter struct {
Isp *m.EdgeISP `json:"isp"`
AreaID *int32 `json:"area_id"`
}
func (f *EdgeFilter) IsEmpty() bool {
if f == nil {
return true
}
return u.X(f.Isp.String()) == nil && f.AreaID == nil
}
// 错误信息 // 错误信息
var ( var (
ErrResourceNotExist = core.NewBizErr("套餐不存在") ErrResourceNotExist = core.NewBizErr("套餐不存在")
ErrResourceInvalid = core.NewBizErr("套餐不可用")
ErrResourceExhausted = core.NewBizErr("套餐已用完") ErrResourceExhausted = core.NewBizErr("套餐已用完")
ErrResourceExpired = core.NewBizErr("套餐已过期") ErrResourceExpired = core.NewBizErr("套餐已过期")
ErrResourceDailyLimit = core.NewBizErr("套餐每日配额已用完") ErrResourceDailyLimit = core.NewBizErr("套餐每日配额已用完")
ErrEdgesNoAvailable = core.NewBizErr("没有可用的节点")
) )

View File

@@ -1,486 +1,152 @@
package services package services
import ( import (
"context"
"encoding/json"
"fmt" "fmt"
"log/slog" "log/slog"
"net/netip"
"platform/pkg/env"
"platform/pkg/u" "platform/pkg/u"
"platform/web/core" "platform/web/core"
e "platform/web/events"
g "platform/web/globals" g "platform/web/globals"
"platform/web/globals/orm"
m "platform/web/models" m "platform/web/models"
q "platform/web/queries" q "platform/web/queries"
"strings"
"time"
"github.com/hibiken/asynq"
"gorm.io/gen"
"gorm.io/gen/field"
) )
type channelBaiyinProvider struct{} type channelBaiyinProvider struct{}
func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, filter *EdgeFilter) ([]*m.Channel, error) { func (s *channelBaiyinProvider) selectProxy(count int) (*m.Proxy, error) {
if filter == nil { return selectProxyByType(m.ProxyTypeBaiYin, count)
return nil, core.NewBizErr("缺少节点过滤条件") }
}
now := time.Now() func (s *channelBaiyinProvider) prepareCreate(ctx *channelCreateContext) (*channelCreateResult, error) {
batchNo := ID.GenReadable("bat") gateway, err := proxyGateway(ctx.Proxy)
// 检查并获取套餐与白名单
resource, whitelists, err := ensure(now, source, resourceId, authWhitelist, count)
if err != nil { if err != nil {
return nil, err return nil, core.NewServErr("创建代理网关失败", err)
} }
prov, city := areaProvinceCity(ctx.Area)
user := resource.User channels := make([]*m.Channel, len(ctx.Ports))
expire := now.Add(resource.Live) chanConfigs := make([]*g.PortConfigsReq, len(ctx.Ports))
for i, portRef := range ctx.Ports {
// 选择代理 channel := newBaseChannel(ctx, portRef.Port())
proxy, gateway, err := selectProxy(count) cfg := &g.PortConfigsReq{
if err != nil { Port: int(portRef.Port()),
return nil, err
}
// 取用端口
chans, err := selectPorts(proxy.ID, batchNo, count, expire)
if err != nil {
return nil, err
}
// 节点查询到提交,需要锁定防止并发取用
channels := make([]*m.Channel, count)
err = g.Redsync.WithLock(lockChannelCreateKey(), func() error {
// 取用节点
edges, err := selectEdges(gateway, filter, count)
if err != nil {
return err
}
// 绑定节点端口
chanConfigs := make([]*g.PortConfigsReq, count)
edgeConfigs := make([]string, 0, count)
for i := range count {
ch := chans[i]
edge := edges[i]
// 通道数据
channels[i] = &m.Channel{
UserID: user.ID,
ResourceID: resourceId,
BatchNo: batchNo,
ProxyID: proxy.ID,
Host: u.Else(proxy.Host, proxy.IP.String()),
Port: ch.Port(),
EdgeRef: u.P(edge.EdgeID),
FilterISP: filter.Isp,
FilterProv: filter.Prov,
FilterCity: filter.City,
ExpiredAt: expire,
Proxy: proxy,
}
// 通道配置数据
chanConfigs[i] = &g.PortConfigsReq{
Port: int(ch.Port()),
Status: true, Status: true,
Edge: &[]string{edge.EdgeID}, AutoEdgeConfig: &g.AutoEdgeConfig{
Province: u.Z(prov),
City: u.Z(city),
Isp: ctx.Filter.Isp.String(),
Count: u.P(1),
},
} }
// 白名单模式 if ctx.AuthWhitelist {
if authWhitelist { cfg.Whitelist = &ctx.Whitelists
channels[i].Whitelists = u.P(strings.Join(whitelists, ",")) }
chanConfigs[i].Whitelist = &whitelists if username, password, ok := applyChannelAuth(ctx, channel); ok {
cfg.Userpass = u.P(username + ":" + password)
} }
// 密码模式 channels[i] = channel
if authPassword { chanConfigs[i] = cfg
username, password := genPassPair()
channels[i].Username = &username
channels[i].Password = &password
chanConfigs[i].Userpass = u.P(username + ":" + password)
} }
// 连接配置数据 return &channelCreateResult{
if edge.Type == EdgeInfoCloud { Channels: channels,
edgeConfigs = append(edgeConfigs, edge.EdgeID) applyRemote: func() error {
slog.Debug("提交代理端口配置", "proxy", ctx.Proxy.IP.String(), "total_count", len(chanConfigs))
if err := ensureEdges(ctx.Proxy, gateway, ctx.Area, ctx.Filter.Isp, ctx.Count); err != nil {
slog.Warn("ensureEdges 失败", "err", err)
} }
} if len(chanConfigs) > 0 {
// 提交配置
slog.Debug("提交代理端口配置", "proxy", proxy.IP.String(), "total_count", len(chanConfigs), "remote_count", len(edgeConfigs))
if env.RunMode == env.RunModeProd {
// 连接节点到网关
if err := g.Cloud.CloudConnect(&g.CloudConnectReq{Uuid: proxy.Mac, Edge: &edgeConfigs}); err != nil {
return core.NewServErr("连接云平台失败", err)
}
// 启用网关代理通道
if err := gateway.GatewayPortConfigs(chanConfigs); err != nil { if err := gateway.GatewayPortConfigs(chanConfigs); err != nil {
slog.Warn("提交代理端口配置失败", "error", err.Error()) slog.Warn("提交代理端口配置失败", "error", err.Error())
return core.NewServErr(fmt.Sprintf("配置代理 %s 端口失败", proxy.IP.String()), err) return core.NewServErr(fmt.Sprintf("配置代理 %s 端口失败", ctx.Proxy.IP.String()), err)
}
} else {
for _, item := range chanConfigs {
str, _ := json.Marshal(item)
fmt.Println(string(str))
} }
} }
// 保存数据
err = q.Q.Transaction(func(q *q.Query) error {
// 更新使用记录
var result gen.ResultInfo
var err error
switch resource.Type {
case m.ResourceTypeShort:
result, err = q.ResourceShort.
Where(
q.ResourceShort.ID.Eq(*resource.ShortId),
q.ResourceShort.Used.Eq(resource.Used),
q.ResourceShort.Daily.Eq(resource.Daily),
).
UpdateSimple(
q.ResourceShort.Used.Add(int32(count)),
q.ResourceShort.Daily.Value(int32(resource.Today+count)),
q.ResourceShort.LastAt.Value(now),
)
case m.ResourceTypeLong:
result, err = q.ResourceLong.
Where(
q.ResourceLong.ID.Eq(*resource.LongId),
q.ResourceLong.Used.Eq(resource.Used),
q.ResourceLong.Daily.Eq(resource.Daily),
).
UpdateSimple(
q.ResourceLong.Used.Add(int32(count)),
q.ResourceLong.Daily.Value(int32(resource.Today+count)),
q.ResourceLong.LastAt.Value(now),
)
default:
return core.NewBizErr("套餐类型不正确,无法更新", nil)
}
if err != nil {
return core.NewServErr("更新套餐使用记录失败", err)
}
if result.RowsAffected == 0 {
return core.NewBizErr("提取太频繁,请稍后再试", nil)
}
// 保存通道
err = q.Channel.
Omit(field.AssociationFields).
Create(channels...)
if err != nil {
return core.NewServErr("保存通道失败", err)
}
// 保存提取记录
err = q.LogsUserUsage.Create(&m.LogsUserUsage{
UserID: user.ID,
ResourceID: resourceId,
BatchNo: batchNo,
Count: int32(count),
ISP: u.X(filter.Isp.String()),
Prov: filter.Prov,
City: filter.City,
IP: orm.Inet{Addr: source},
Time: now,
})
if err != nil {
return core.NewServErr("保存用户使用记录失败", err)
}
return nil return nil
}) },
if err != nil { }, nil
return err
}
return nil
})
if err != nil {
return nil, err
}
return channels, nil
} }
func (s *channelBaiyinProvider) RemoveChannels(batch string) error { func (s *channelBaiyinProvider) removeRemote(_ string, batch *usedChanBatch) error {
return g.Redsync.WithLock(lockChannelRemoveKey(batch), func() error { configs := make([]*g.PortConfigsReq, len(batch.Chans))
start := time.Now() for i, ch := range batch.Chans {
// 获取连接数据
channels, err := q.Channel.Where(q.Channel.BatchNo.Eq(batch)).Find()
if err != nil {
return core.NewServErr(fmt.Sprintf("获取通道数据失败batch%s", batch), err)
}
if len(channels) == 0 {
slog.Warn(fmt.Sprintf("未找到通道数据batch%s", batch))
return nil
}
proxy, err := q.Proxy.Where(q.Proxy.ID.Eq(channels[0].ProxyID)).Take()
if err != nil {
return core.NewServErr(fmt.Sprintf("获取代理数据失败batch%s", batch), err)
}
// 检查通道是否存在
exist, err := g.Redis.Exists(context.Background(), usedChansKey(proxy.ID, batch)).Result()
if err != nil {
return core.NewServErr("查询使用中通道失败", err)
}
if exist == 0 {
return nil // 没有使用中通道,已经被清理过了
}
// 准备配置数据
edgeConfigs := make([]string, len(channels))
configs := make([]*g.PortConfigsReq, len(channels))
for i, channel := range channels {
if channel.EdgeRef != nil {
edgeConfigs[i] = *channel.EdgeRef
} else {
slog.Warn(fmt.Sprintf("通道 %d 没有保存节点引用", channel.ID))
}
configs[i] = &g.PortConfigsReq{ configs[i] = &g.PortConfigsReq{
Status: false, Port: int(ch.Port()),
Port: int(channel.Port),
Edge: &[]string{}, Edge: &[]string{},
AutoEdgeConfig: &g.AutoEdgeConfig{Count: u.P(0)},
Status: false,
} }
} }
// 提交配置 proxy, err := q.Proxy.Where(q.Proxy.ID.Eq(batch.ProxyID)).Take()
if env.RunMode == env.RunModeProd {
// 清空通道配置
secret := strings.Split(u.Z(proxy.Secret), ":")
if len(secret) != 2 {
return core.NewServErr(fmt.Sprintf("代理 %s 密钥格式错误", proxy.IP.String()), nil)
}
gateway := g.NewGateway(proxy.IP.String(), secret[0], secret[1])
err := gateway.GatewayPortConfigs(configs)
if err != nil { if err != nil {
return core.NewServErr("获取代理数据失败", err)
}
gateway, err := proxyGateway(proxy)
if err != nil {
return core.NewServErr("创建代理网关失败", err)
}
if err = gateway.GatewayPortConfigs(configs); err != nil {
return core.NewServErr(fmt.Sprintf("清空代理 %s 端口配置失败", proxy.IP.String()), err) return core.NewServErr(fmt.Sprintf("清空代理 %s 端口配置失败", proxy.IP.String()), err)
} }
// 断开节点连接
_, err = g.Cloud.CloudDisconnect(&g.CloudDisconnectReq{
Uuid: proxy.Mac,
Edge: &edgeConfigs,
})
if err != nil {
slog.Warn("断开云平台连接失败", "error", err.Error())
return core.NewServErr("断开云平台连接失败", err)
}
} else {
for _, item := range configs {
str, _ := json.Marshal(item)
fmt.Println(string(str))
}
}
// 释放端口
err = freeChans(proxy.ID, batch)
if err != nil {
return err
}
slog.Debug("清除代理端口配置", "proxy", proxy.ID, "batch", batch, "duration", time.Since(start).String())
return nil return nil
})
} }
// ClearExpiredChannels 清理指定代理的过期通道,并返回清理数量(现在理论上不会有需要手动批量清理的通道,未来可以废弃) // ensureEdges 检查本地节点是否足够,如果不足从云端连入
func (s *channelBaiyinProvider) ClearExpiredChannels(proxyId int32) (int, error) {
now := time.Now()
// 获取未清理通道
keys, err := g.Redis.Keys(context.Background(), usedChansKey(proxyId, "*")).Result()
if err != nil {
return 0, core.NewServErr("查询使用中通道失败", err)
}
if len(keys) == 0 {
return 0, nil
}
batchList := make([]string, len(keys))
batchSet := make(map[string]struct{}, len(keys))
for i, key := range keys {
parts := strings.Split(key, ":")
if len(parts) != 4 {
return 0, core.NewServErr(fmt.Sprintf("使用中通道键格式错误: %s", key), nil)
}
batchList[i] = parts[3]
batchSet[parts[3]] = struct{}{}
}
// 排除未过期通道
var batchQueried []struct{ BatchNo string }
err = q.Channel.
Select(q.Channel.BatchNo).
Where(
q.Channel.BatchNo.In(batchList...),
q.Channel.ExpiredAt.Gte(now),
).
Group(q.Channel.BatchNo).
Scan(&batchQueried)
if err != nil {
return 0, core.NewServErr("查询过期通道失败", err)
}
for _, batch := range batchQueried {
delete(batchSet, batch.BatchNo)
}
// 清理过期通道
slog.Info("批量清理过期通道", "count", len(batchSet))
for batchNo, _ := range batchSet {
err := s.RemoveChannels(batchNo)
if err != nil {
slog.Error("清理过期通道失败", "batch", batchNo, "error", err)
}
}
return len(batchSet), nil
}
func lockChannelCreateKey() string {
return "platform:channel:create"
}
func lockChannelRemoveKey(bid string) string {
return fmt.Sprintf("platform:batch:remove_expired:%s", bid)
}
func selectProxy(count int) (*m.Proxy, g.GatewayClient, error) {
// 获取在线节点
proxies, err := q.Proxy.Where(
q.Proxy.Type.Eq(int(m.ProxyTypeBaiYin)),
q.Proxy.Status.Eq(int(m.ProxyStatusOnline)),
).Find()
if err != nil {
return nil, nil, core.NewBizErr("获取可用代理失败", err)
}
if len(proxies) == 0 {
return nil, nil, core.NewBizErr("无可用代理")
}
proxyIDs := make([]int32, 0, len(proxies))
proxyMap := make(map[int32]*m.Proxy, len(proxies))
for _, item := range proxies {
proxyIDs = append(proxyIDs, item.ID)
proxyMap[item.ID] = item
}
// 获取最空闲节点
maxId := int32(0)
maxCount := -1
for _, id := range proxyIDs {
idCount, err := g.Redis.SCard(context.Background(), freeChansKey(id)).Result()
if err != nil {
return nil, nil, fmt.Errorf("查询可用通道数量失败: %w", err)
}
if idCount > int64(maxCount) {
maxCount = int(idCount)
maxId = id
}
}
if maxCount < count {
return nil, nil, core.NewBizErr("无可用代理")
}
proxy := proxyMap[maxId]
secret := strings.Split(u.Z(proxy.Secret), ":")
if len(secret) != 2 {
return nil, nil, core.NewServErr(fmt.Sprintf("代理 %s 密钥格式错误", proxy.IP.String()), nil)
}
gateway := g.NewGateway(proxy.IP.String(), secret[0], secret[1])
return proxy, gateway, nil
}
func selectPorts(proxyId int32, batchNo string, count int, expire time.Time) ([]netip.AddrPort, error) {
chans, err := lockChans(proxyId, batchNo, count)
if err != nil {
return nil, core.NewBizErr("无可用通道,请稍后再试", err)
}
_, err = g.Asynq.Enqueue(
e.NewRemoveChannel(batchNo),
asynq.ProcessAt(expire),
)
if err != nil {
return nil, core.NewServErr("注册异步关闭通道任务失败", err)
}
return chans, nil
}
// selectEdges 选择节点,优先本地节点,失败重试,直到达到重试次数限制
// 本地节点通过 Assigned = false 排除已分配节点 // 本地节点通过 Assigned = false 排除已分配节点
// 云端节点通过 NoRepeat = true 排除已分配节点 // 云端节点通过 NoRepeat = true 排除已分配节点
func selectEdges(gateway g.GatewayClient, filter *EdgeFilter, count int) ([]EdgeInfo, error) { func ensureEdges(proxy *m.Proxy, gateway g.GatewayClient, area *m.Area, isp *m.EdgeISP, count int) error {
edges := make([]EdgeInfo, 0, count) prov, city := areaProvinceCity(area)
if prov == nil && city == nil && u.X(isp.String()) == nil {
return nil // 没有过滤条件,直接返回空,避免无意义的查询
}
// 先查本地 // 先查本地
localEdgesResp, err := gateway.GatewayEdge(&g.GatewayEdgeReq{ localEdges, err := gateway.GatewayEdge(&g.GatewayEdgeReq{
Province: filter.Prov, Province: prov,
City: filter.City, City: city,
Isp: u.X(filter.Isp.String()), Isp: u.X(isp.String()),
Limit: &count, Limit: &count,
Assigned: u.P(false), Assigned: u.P(false),
}) })
if err != nil { if err != nil {
return nil, core.NewBizErr("获取可用节点失败[1]", err) return core.NewBizErr("检查可用节点失败[1]", err)
} }
if len(localEdges) >= count {
for id, _ := range localEdgesResp { return nil // 本地节点足够,直接返回空,后续逻辑会优先使用本地节点
edges = append(edges, EdgeInfo{
Type: EdgeInfoLocal,
EdgeID: id,
})
}
if len(edges) >= count {
return edges, nil
} }
// 再查云端 // 再查云端
remaining := count - len(edges) remaining := count - len(localEdges)
cloudEdgesResp, err := g.Cloud.CloudEdges(&g.CloudEdgesReq{ cloudEdges, err := g.Cloud.CloudEdges(&g.CloudEdgesReq{
Province: filter.Prov, Province: prov,
City: filter.City, City: city,
Isp: u.X(filter.Isp.String()), Isp: u.X(isp.String()),
Limit: &remaining, Limit: &remaining,
NoRepeat: u.P(true), NoRepeat: u.P(true),
ActiveTime: u.P(3600), ActiveTime: u.P(3600),
IpUnchangedTime: u.P(3600), IpUnchangedTime: u.P(3600),
}) })
if err != nil { if err != nil {
return nil, core.NewBizErr("获取可用节点失败[2]", err) return core.NewBizErr("检查可用节点失败[2]", err)
}
if len(cloudEdges.Edges) < remaining {
return core.NewBizErr("地区可用节点数量不足")
} }
for _, edge := range cloudEdgesResp.Edges { // 连入云端节点
edges = append(edges, EdgeInfo{ edges := make([]string, remaining)
Type: EdgeInfoCloud, for i, edge := range cloudEdges.Edges {
EdgeID: edge.EdgeID, edges[i] = edge.EdgeID
})
}
if len(edges) < count {
return nil, core.NewBizErr("地区可用节点数量不足")
} }
return edges, nil if err := g.Cloud.CloudConnect(&g.CloudConnectReq{Uuid: proxy.Mac, Edge: &edges}); err != nil {
return core.NewServErr("连接云平台失败", err)
}
return nil
} }
type EdgeInfo struct { type EdgeInfo struct {

View File

@@ -0,0 +1,211 @@
package services
import (
"fmt"
"platform/pkg/env"
"platform/pkg/u"
"platform/web/core"
g "platform/web/globals"
m "platform/web/models"
q "platform/web/queries"
"strings"
"gorm.io/gen/field"
)
type channelGostProvider struct{}
func (s *channelGostProvider) prepareCreate(ctx *channelCreateContext) (*channelCreateResult, error) {
edges, err := s.selectEdge(ctx.Filter, ctx.Area, ctx.Count)
if err != nil {
return nil, err
}
client, err := proxyGost(ctx.Proxy)
if err != nil {
return nil, err
}
admissions := make([]*g.GostAdmissionConfig, 0, ctx.Count)
authers := make([]*g.GostAutherConfig, 0, ctx.Count)
services := make([]*g.GostServiceConfig, len(ctx.Ports))
channels := make([]*m.Channel, len(ctx.Ports))
for i, portRef := range ctx.Ports {
edge := edges[i]
port := portRef.Port()
serviceName := gostServiceName(ctx.BatchNo, port)
channel := newBaseChannel(ctx, port)
channel.EdgeID = u.P(edge.ID)
channel.EdgeRef = u.P(edge.Mac)
channel.IP = u.P(edge.IP)
service := &g.GostServiceConfig{
Name: serviceName,
Addr: fmt.Sprintf(":%d", port),
Handler: g.GostHandlerConfig{
Type: "auto",
Chain: edge.Mac,
},
Listener: g.GostListenerConfig{
Type: "tcp",
},
Recorders: []g.GostRecorderConfig{
{Name: "record-file", Record: "recorder.service.handler"},
},
}
if ctx.AuthWhitelist {
service.Admission = gostAdmissionName(ctx.BatchNo, port)
admissions = append(admissions, &g.GostAdmissionConfig{
Name: service.Admission,
Whitelist: true,
Matchers: ctx.Whitelists,
})
}
if username, password, ok := applyChannelAuth(ctx, channel); ok {
service.Handler.Auther = gostAutherName(ctx.BatchNo, port)
authers = append(authers, &g.GostAutherConfig{
Name: service.Handler.Auther,
Auths: []g.GostAuthConfig{{
Username: username,
Password: password,
}},
})
}
services[i] = service
channels[i] = channel
}
return &channelCreateResult{
Channels: channels,
applyRemote: func() error {
for _, admission := range admissions {
if err := client.CreateAdmission(admission); err != nil {
return core.NewServErr(fmt.Sprintf("创建 GOST admission 失败: %s", admission.Name), err)
}
}
for _, auther := range authers {
if err := client.CreateAuther(auther); err != nil {
return core.NewServErr(fmt.Sprintf("创建 GOST auther 失败: %s", auther.Name), err)
}
}
for _, service := range services {
if err := client.CreateService(service); err != nil {
return core.NewServErr(fmt.Sprintf("创建 GOST service 失败: %s", service.Name), err)
}
}
return nil
},
}, nil
}
func (s *channelGostProvider) removeRemote(batchNo string, batch *usedChanBatch) error {
proxy, err := q.Proxy.Where(q.Proxy.ID.Eq(batch.ProxyID)).Take()
if err != nil {
return core.NewServErr("获取代理数据失败", err)
}
client, err := proxyGost(proxy)
if err != nil {
return core.NewServErr("创建 GOST 客户端失败", err)
}
var deleteErrs []error
for _, ch := range batch.Chans {
port := ch.Port()
serviceName := gostServiceName(batchNo, port)
deleteErrs = append(deleteErrs, deleteGostResource("service", serviceName, func() error {
return client.DeleteService(serviceName)
}))
autherName := gostAutherName(batchNo, port)
deleteErrs = append(deleteErrs, deleteGostResource("auther", autherName, func() error {
return client.DeleteAuther(autherName)
}))
admissionName := gostAdmissionName(batchNo, port)
deleteErrs = append(deleteErrs, deleteGostResource("admission", admissionName, func() error {
return client.DeleteAdmission(admissionName)
}))
}
return u.CombineErrors(deleteErrs)
}
func (s *channelGostProvider) selectProxy(count int) (*m.Proxy, error) {
return selectProxyByType(m.ProxyTypeGost, count)
}
func (s *channelGostProvider) selectEdge(filter *EdgeFilter, area *m.Area, count int) ([]*m.Edge, error) {
if filter == nil {
filter = &EdgeFilter{}
}
do := q.Edge.Where(
q.Edge.Type.Eq(int(m.EdgeTypeGostChain)),
q.Edge.Status.Eq(int(m.EdgeStatusNormal)),
)
if filter.Isp != nil {
do = do.Where(q.Edge.ISP.In(int(m.EdgeISPUnknown), int(*filter.Isp)))
}
if area != nil {
switch area.Level {
case m.AreaLevelProvince:
edgeArea := q.Area.As("EdgeArea")
do = do.
Where(edgeArea.ParentID.Eq(area.ID)).
Join(edgeArea, edgeArea.ID.EqCol(q.Edge.AreaID))
case m.AreaLevelCity:
do = do.Where(q.Edge.AreaID.Eq(area.ID))
default:
return nil, core.NewBizErr("地区层级不支持")
}
}
edges, err := do.
Order(field.NewUnsafeFieldRaw("random()")).
Limit(count).
Find()
if err != nil {
return nil, core.NewBizErr("查询可用节点失败", err)
}
if len(edges) == 0 {
return nil, core.NewBizErr("地区可用节点数量不足")
}
result := make([]*m.Edge, count)
for i := range count {
result[i] = edges[i%len(edges)]
}
return result, nil
}
func proxyGost(proxy *m.Proxy) (g.GostClient, error) {
secret := strings.Split(u.Z(proxy.Secret), ":")
if len(secret) != 2 {
return nil, core.NewServErr(fmt.Sprintf("代理 %s 密钥格式错误", proxy.IP.String()), nil)
}
host := u.Else(proxy.Host, proxy.IP.String())
return g.NewGost(host, env.GostApiPort, env.GostApiPathPrefix, secret[0], secret[1]), nil
}
func deleteGostResource(kind string, name string, deleteFn func() error) error {
if err := deleteFn(); err != nil && !g.IsGostNotFound(err) {
return core.NewServErr(fmt.Sprintf("删除 GOST %s 配置失败: %s", kind, name), err)
}
return nil
}
func gostServiceName(batchNo string, port uint16) string {
return fmt.Sprintf("gost-svc-%s-%d", batchNo, port)
}
func gostAutherName(batchNo string, port uint16) string {
return fmt.Sprintf("gost-auther-%s-%d", batchNo, port)
}
func gostAdmissionName(batchNo string, port uint16) string {
return fmt.Sprintf("gost-adm-%s-%d", batchNo, port)
}

View File

@@ -87,8 +87,14 @@ func (s *couponService) Update(data UpdateCouponData) error {
do = append(do, q.Coupon.ExpireType.Value(int(*data.ExpireType))) do = append(do, q.Coupon.ExpireType.Value(int(*data.ExpireType)))
} }
_, err := q.Coupon.Where(q.Coupon.ID.Eq(data.ID)).UpdateSimple(do...) r, err := q.Coupon.Where(q.Coupon.ID.Eq(data.ID)).UpdateSimple(do...)
if err != nil {
return err return err
}
if r.RowsAffected == 0 {
return core.NewBizErr("优惠券状态已过期")
}
return nil
} }
type UpdateCouponData struct { type UpdateCouponData struct {
@@ -104,8 +110,14 @@ type UpdateCouponData struct {
} }
func (s *couponService) Delete(id int32) error { func (s *couponService) Delete(id int32) error {
_, err := q.Coupon.Where(q.Coupon.ID.Eq(id)).UpdateColumn(q.Coupon.DeletedAt, time.Now()) r, err := q.Coupon.Where(q.Coupon.ID.Eq(id)).UpdateColumn(q.Coupon.DeletedAt, time.Now())
if err != nil {
return err return err
}
if r.RowsAffected == 0 {
return core.NewBizErr("优惠券状态已过期")
}
return nil
} }
func (s *couponService) Assign(couponID int32, userID int32) error { func (s *couponService) Assign(couponID int32, userID int32) error {
@@ -122,7 +134,7 @@ func (s *couponService) GetUserCoupon(uid int32, cuid int32, amount decimal.Deci
q.CouponUser.ID.Eq(cuid), q.CouponUser.ID.Eq(cuid),
q.CouponUser.UserID.Eq(uid), q.CouponUser.UserID.Eq(uid),
q.CouponUser.Status.Eq(int(m.CouponUserStatusUnused)), q.CouponUser.Status.Eq(int(m.CouponUserStatusUnused)),
q.CouponUser.Where(q.CouponUser.ExpireAt.IsNull()).Or(q.CouponUser.ExpireAt.Gt(time.Now())), q.CouponUser.Where(q.CouponUser.ExpireAt.IsNull()).Or(q.CouponUser.ExpireAt.Gt(time.Now().UTC())),
).Take() ).Take()
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, core.NewBizErr("优惠券不存在或已失效") return nil, core.NewBizErr("优惠券不存在或已失效")
@@ -140,7 +152,7 @@ func (s *couponService) GetUserCoupon(uid int32, cuid int32, amount decimal.Deci
} }
func (s *couponService) UseCoupon(q *q.Query, cuid int32) error { func (s *couponService) UseCoupon(q *q.Query, cuid int32) error {
_, err := q.CouponUser. r, err := q.CouponUser.
Where( Where(
q.CouponUser.ID.Eq(cuid), q.CouponUser.ID.Eq(cuid),
q.CouponUser.Status.Eq(int(m.CouponUserStatusUnused)), q.CouponUser.Status.Eq(int(m.CouponUserStatusUnused)),
@@ -149,5 +161,11 @@ func (s *couponService) UseCoupon(q *q.Query, cuid int32) error {
q.CouponUser.Status.Value(int(m.CouponUserStatusUsed)), q.CouponUser.Status.Value(int(m.CouponUserStatusUsed)),
q.CouponUser.UsedAt.Value(time.Now()), q.CouponUser.UsedAt.Value(time.Now()),
) )
return err if err != nil {
return core.NewBizErr("使用优惠券失败", err)
}
if r.RowsAffected == 0 {
return core.NewBizErr("优惠券状态已过期")
}
return nil
} }

View File

@@ -1,39 +0,0 @@
package services
import (
m "platform/web/models"
q "platform/web/queries"
)
var Edge = &edgeService{}
type edgeService struct{}
func (s *edgeService) AllEdges(count int, filter EdgeFilter) ([]*m.Edge, error) {
do := q.Edge.Where()
if filter.Prov != nil {
do = do.Where(q.Edge.Prov.Eq(*filter.Prov))
}
if filter.City != nil {
do = do.Where(q.Edge.City.Eq(*filter.City))
}
if filter.Isp != nil {
do = do.Where(q.Edge.ISP.Eq(int(*filter.Isp)))
}
if count > 0 {
do = do.Limit(count)
}
edges, err := q.Edge.Where(do).Find()
if err != nil {
return nil, err
}
return edges, nil
}
type EdgeFilter struct {
Isp *m.EdgeISP `json:"isp"`
Prov *string `json:"prov"`
City *string `json:"city"`
}

View File

@@ -117,8 +117,14 @@ func (s *productService) UpdateProduct(update *UpdateProductData) error {
if update.Status != nil { if update.Status != nil {
do = append(do, q.Product.Status.Value(*update.Status)) do = append(do, q.Product.Status.Value(*update.Status))
} }
_, err := q.Product.Where(q.Product.ID.Eq(update.Id)).UpdateSimple(do...) r, err := q.Product.Where(q.Product.ID.Eq(update.Id)).UpdateSimple(do...)
if err != nil {
return err return err
}
if r.RowsAffected == 0 {
return core.NewBizErr("产品状态已过期")
}
return nil
} }
type UpdateProductData struct { type UpdateProductData struct {
@@ -132,6 +138,12 @@ type UpdateProductData struct {
// 删除产品 // 删除产品
func (s *productService) DeleteProduct(id int32) error { func (s *productService) DeleteProduct(id int32) error {
_, err := q.Product.Where(q.Product.ID.Eq(id)).UpdateColumn(q.Product.DeletedAt, time.Now()) r, err := q.Product.Where(q.Product.ID.Eq(id)).UpdateColumn(q.Product.DeletedAt, time.Now())
if err != nil {
return err return err
}
if r.RowsAffected == 0 {
return core.NewBizErr("产品状态已过期")
}
return nil
} }

View File

@@ -43,8 +43,14 @@ func (s *productDiscountService) Update(data UpdateProductDiscountData) (err err
do = append(do, q.ProductDiscount.Discount.Value(*data.Discount)) do = append(do, q.ProductDiscount.Discount.Value(*data.Discount))
} }
_, err = q.ProductDiscount.Where(q.ProductDiscount.ID.Eq(data.ID)).UpdateSimple(do...) r, err := q.ProductDiscount.Where(q.ProductDiscount.ID.Eq(data.ID)).UpdateSimple(do...)
if err != nil {
return err return err
}
if r.RowsAffected == 0 {
return core.NewServErr("产品折扣状态已过期")
}
return nil
} }
type UpdateProductDiscountData struct { type UpdateProductDiscountData struct {
@@ -54,6 +60,12 @@ type UpdateProductDiscountData struct {
} }
func (s *productDiscountService) Delete(id int32) (err error) { func (s *productDiscountService) Delete(id int32) (err error) {
_, err = q.ProductDiscount.Where(q.ProductDiscount.ID.Eq(id)).UpdateColumn(q.ProductDiscount.DeletedAt, time.Now()) r, err := q.ProductDiscount.Where(q.ProductDiscount.ID.Eq(id)).UpdateColumn(q.ProductDiscount.DeletedAt, time.Now())
return if err != nil {
return err
}
if r.RowsAffected == 0 {
return core.NewServErr("产品折扣状态已过期")
}
return nil
} }

View File

@@ -111,8 +111,14 @@ func (s *productSkuService) Update(update UpdateProductSkuData) (err error) {
do = append(do, q.ProductSku.CountMin.Value(*update.CountMin)) do = append(do, q.ProductSku.CountMin.Value(*update.CountMin))
} }
_, err = q.ProductSku.Where(q.ProductSku.ID.Eq(update.ID)).UpdateSimple(do...) r, err := q.ProductSku.Where(q.ProductSku.ID.Eq(update.ID)).UpdateSimple(do...)
if err != nil {
return err return err
}
if r.RowsAffected == 0 {
return core.NewServErr("产品套餐状态已过期")
}
return nil
} }
type UpdateProductSkuData struct { type UpdateProductSkuData struct {
@@ -128,15 +134,27 @@ type UpdateProductSkuData struct {
} }
func (s *productSkuService) Delete(id int32) (err error) { func (s *productSkuService) Delete(id int32) (err error) {
_, err = q.ProductSku.Where(q.ProductSku.ID.Eq(id)).UpdateColumn(q.ProductSku.DeletedAt, time.Now()) r, err := q.ProductSku.Where(q.ProductSku.ID.Eq(id)).UpdateColumn(q.ProductSku.DeletedAt, time.Now())
return if err != nil {
return err
}
if r.RowsAffected == 0 {
return core.NewServErr("产品套餐状态已过期")
}
return nil
} }
func (s *productSkuService) BatchUpdateDiscount(data BatchUpdateSkuDiscountData) (err error) { func (s *productSkuService) BatchUpdateDiscount(data BatchUpdateSkuDiscountData) (err error) {
_, err = q.ProductSku.Where(q.ProductSku.ProductID.Eq(data.ProductID)).UpdateSimple( r, err := q.ProductSku.Where(q.ProductSku.ProductID.Eq(data.ProductID)).UpdateSimple(
q.ProductSku.DiscountId.Value(data.DiscountID), q.ProductSku.DiscountId.Value(data.DiscountID),
) )
return if err != nil {
return err
}
if r.RowsAffected == 0 {
return core.NewServErr("产品套餐状态已过期")
}
return nil
} }
type BatchUpdateSkuDiscountData struct { type BatchUpdateSkuDiscountData struct {

View File

@@ -2,6 +2,8 @@ package services
import ( import (
"context" "context"
"errors"
"fmt"
"net/netip" "net/netip"
"platform/pkg/u" "platform/pkg/u"
"platform/web/core" "platform/web/core"
@@ -9,9 +11,11 @@ import (
"platform/web/globals/orm" "platform/web/globals/orm"
m "platform/web/models" m "platform/web/models"
q "platform/web/queries" q "platform/web/queries"
"strings"
"time" "time"
"gorm.io/gen/field" "gorm.io/gen/field"
"gorm.io/gorm"
) )
var Proxy = &proxyService{} var Proxy = &proxyService{}
@@ -21,11 +25,20 @@ type proxyService struct{}
func hasUsedChans(proxyID int32) (bool, error) { func hasUsedChans(proxyID int32) (bool, error) {
ctx := context.Background() ctx := context.Background()
pattern := usedChansKey(proxyID, "*") pattern := usedChansKey(proxyID, "*")
keys, _, err := g.Redis.Scan(ctx, 0, pattern, 1).Result() var cursor uint64
for {
keys, next, err := g.Redis.Scan(ctx, cursor, pattern, 100).Result()
if err != nil { if err != nil {
return false, err return false, err
} }
return len(keys) > 0, nil if len(keys) > 0 {
return true, nil
}
if next == 0 {
return false, nil
}
cursor = next
}
} }
func rebuildFreeChans(proxyID int32, addr netip.Addr) error { func rebuildFreeChans(proxyID int32, addr netip.Addr) error {
@@ -62,6 +75,7 @@ type CreateProxy struct {
Mac string `json:"mac" validate:"required"` Mac string `json:"mac" validate:"required"`
IP string `json:"ip" validate:"required"` IP string `json:"ip" validate:"required"`
Host *string `json:"host"` Host *string `json:"host"`
Port *int `json:"port"`
Secret *string `json:"secret"` Secret *string `json:"secret"`
Type *m.ProxyType `json:"type"` Type *m.ProxyType `json:"type"`
Status *m.ProxyStatus `json:"status"` Status *m.ProxyStatus `json:"status"`
@@ -78,6 +92,7 @@ func (s *proxyService) Create(create *CreateProxy) error {
Mac: create.Mac, Mac: create.Mac,
IP: orm.Inet{Addr: addr}, IP: orm.Inet{Addr: addr},
Host: create.Host, Host: create.Host,
Port: create.Port,
Secret: create.Secret, Secret: create.Secret,
Type: u.Else(create.Type, m.ProxyTypeSelfHosted), Type: u.Else(create.Type, m.ProxyTypeSelfHosted),
Status: u.Else(create.Status, m.ProxyStatusOffline), Status: u.Else(create.Status, m.ProxyStatusOffline),
@@ -97,6 +112,7 @@ type UpdateProxy struct {
Mac *string `json:"mac"` Mac *string `json:"mac"`
IP *string `json:"ip"` IP *string `json:"ip"`
Host *string `json:"host"` Host *string `json:"host"`
Port *int `json:"port"`
Secret *string `json:"secret"` Secret *string `json:"secret"`
} }
@@ -119,6 +135,9 @@ func (s *proxyService) Update(update *UpdateProxy) error {
if update.Host != nil { if update.Host != nil {
simples = append(simples, q.Proxy.Host.Value(*update.Host)) simples = append(simples, q.Proxy.Host.Value(*update.Host))
} }
if update.Port != nil {
simples = append(simples, q.Proxy.Port.Value(*update.Port))
}
if update.Secret != nil { if update.Secret != nil {
hasSideEffect = true hasSideEffect = true
simples = append(simples, q.Proxy.Secret.Value(*update.Secret)) simples = append(simples, q.Proxy.Secret.Value(*update.Secret))
@@ -153,6 +172,117 @@ func (s *proxyService) Update(update *UpdateProxy) error {
return nil return nil
} }
func (s *proxyService) SyncPorts(id int32) error {
proxy, err := findOfflineProxy(id)
if err != nil {
return err
}
used, err := hasUsedChans(id)
if err != nil {
return core.NewServErr("检查代理通道状态失败", err)
}
if used {
return core.NewBizErr("代理存在未关闭通道,禁止重建端口池")
}
return rebuildFreeChans(id, proxy.IP.Addr)
}
func (s *proxyService) SyncChains(id int32) error {
proxy, err := findOfflineProxy(id)
if err != nil {
return err
}
if proxy.Type != m.ProxyTypeGost {
return core.NewBizErr("仅 GOST 代理支持重建代理链")
}
chains, err := buildGostChainsFromEdges()
if err != nil {
return err
}
client, err := proxyGost(proxy)
if err != nil {
return core.NewServErr("创建 GOST 客户端失败", err)
}
oldChains, err := client.ListChains()
if err != nil {
return core.NewServErr("查询 GOST chains 失败", err)
}
for _, chain := range oldChains {
if err := client.DeleteChain(chain.Name); err != nil {
return core.NewServErr(fmt.Sprintf("删除 GOST chain 失败: %s", chain.Name), err)
}
}
for _, chain := range chains {
if err := client.CreateChain(chain); err != nil {
return core.NewServErr(fmt.Sprintf("创建 GOST chain 失败: %s", chain.Name), err)
}
}
if err := client.SaveConfig(); err != nil {
return core.NewServErr("保存 GOST 配置失败", err)
}
return nil
}
func findOfflineProxy(id int32) (*m.Proxy, error) {
proxy, err := q.Proxy.Where(q.Proxy.ID.Eq(id)).Take()
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, core.NewBizErr("代理不存在")
}
if err != nil {
return nil, core.NewServErr("获取代理数据失败", err)
}
if proxy.Status != m.ProxyStatusOffline {
return nil, core.NewBizErr("代理未下线,禁止同步")
}
return proxy, nil
}
func buildGostChainsFromEdges() ([]*g.GostChainConfig, error) {
edges, err := q.Edge.
Where(q.Edge.Type.Eq(int(m.EdgeTypeGostChain))).
Order(q.Edge.ID).
Find()
if err != nil {
return nil, core.NewServErr("查询 GOST edge 数据失败", err)
}
chains := make([]*g.GostChainConfig, len(edges))
for i, edge := range edges {
if strings.TrimSpace(edge.Mac) == "" {
return nil, core.NewBizErr(fmt.Sprintf("GOST edge %d chain 名称为空", edge.ID))
}
if !edge.IP.Addr.IsValid() {
return nil, core.NewBizErr(fmt.Sprintf("GOST edge %s IP 无效", edge.Mac))
}
if edge.Port == nil || *edge.Port == 0 {
return nil, core.NewBizErr(fmt.Sprintf("GOST edge %s 端口为空", edge.Mac))
}
chains[i] = &g.GostChainConfig{
Name: edge.Mac,
Hops: []g.GostHopConfig{{
Nodes: []g.GostNodeConfig{{
Addr: netip.AddrPortFrom(edge.IP.Addr, *edge.Port).String(),
Connector: g.GostConnectorConfig{
Type: "socks5",
},
Dialer: g.GostDialerConfig{
Type: "tcp",
},
}},
}},
}
}
return chains, nil
}
func (s *proxyService) Remove(id int32) error { func (s *proxyService) Remove(id int32) error {
used, err := hasUsedChans(id) used, err := hasUsedChans(id)
if err != nil { if err != nil {
@@ -208,3 +338,14 @@ func (s *proxyService) UpdateStatus(update *UpdateProxyStatus) error {
return err return err
}) })
} }
func proxyGateway(proxy *m.Proxy) (g.GatewayClient, error) {
secret := strings.Split(u.Z(proxy.Secret), ":")
if len(secret) != 2 {
return nil, core.NewServErr(fmt.Sprintf("代理 %s 密钥格式错误", proxy.IP.String()), nil)
}
gateway := g.NewGateway(proxy.IP.String(), secret[0], secret[1])
return gateway, nil
}

View File

@@ -130,12 +130,15 @@ func (s *resourceService) Update(data *UpdateResourceData) error {
do = append(do, q.Resource.CheckIP.Value(*data.CheckIP)) do = append(do, q.Resource.CheckIP.Value(*data.CheckIP))
} }
_, err := q.Resource. r, err := q.Resource.
Where(q.Resource.ID.Eq(data.Id)). Where(q.Resource.ID.Eq(data.Id)).
UpdateSimple(do...) UpdateSimple(do...)
if err != nil { if err != nil {
return core.NewServErr("更新套餐失败", err) return core.NewServErr("更新套餐失败", err)
} }
if r.RowsAffected == 0 {
return core.NewBizErr("套餐状态已过期")
}
return nil return nil
} }

View File

@@ -284,7 +284,7 @@ func (s *tradeService) OnCompleteTrade(user *m.User, interNo string, outerNo str
err = q.Q.Transaction(func(q *q.Query) error { err = q.Q.Transaction(func(q *q.Query) error {
// 更新交易信息 // 更新交易信息
_, err := q.Trade. r, err := q.Trade.
Where( Where(
q.Trade.InnerNo.Eq(interNo), q.Trade.InnerNo.Eq(interNo),
q.Trade.Status.Eq(int(m.TradeStatusPending)), q.Trade.Status.Eq(int(m.TradeStatusPending)),
@@ -299,6 +299,9 @@ func (s *tradeService) OnCompleteTrade(user *m.User, interNo string, outerNo str
if err != nil { if err != nil {
return core.NewServErr("更新交易信息失败", err) return core.NewServErr("更新交易信息失败", err)
} }
if r.RowsAffected == 0 {
return core.NewBizErr("交易状态已过期")
}
switch trade.Type { switch trade.Type {
case m.TradeTypeRecharge: case m.TradeTypeRecharge:
@@ -406,7 +409,7 @@ func (s *tradeService) CancelTrade(ref *TradeRef) error {
return nil return nil
} }
func (s *tradeService) OnCancelTrade(tradeNo string, now time.Time) error { func (s *tradeService) OnCancelTrade(tradeNo string, now time.Time) error {
_, err := q.Trade. r, err := q.Trade.
Where( Where(
q.Trade.InnerNo.Eq(tradeNo), q.Trade.InnerNo.Eq(tradeNo),
q.Trade.Status.Eq(int(m.TradeStatusPending)), q.Trade.Status.Eq(int(m.TradeStatusPending)),
@@ -418,6 +421,9 @@ func (s *tradeService) OnCancelTrade(tradeNo string, now time.Time) error {
if err != nil { if err != nil {
return core.NewServErr("更新交易状态失败", err) return core.NewServErr("更新交易状态失败", err)
} }
if r.RowsAffected == 0 {
return core.NewBizErr("交易状态已过期")
}
return nil return nil
} }

View File

@@ -50,7 +50,7 @@ func (s *userService) UpdateBalance(q *q.Query, user *m.User, amount decimal.Dec
} }
// 更新余额 // 更新余额
_, err := q.User. r, err := q.User.
Where( Where(
q.User.ID.Eq(user.ID), q.User.ID.Eq(user.ID),
q.User.Balance.Eq(user.Balance), q.User.Balance.Eq(user.Balance),
@@ -61,6 +61,9 @@ func (s *userService) UpdateBalance(q *q.Query, user *m.User, amount decimal.Dec
if err != nil { if err != nil {
return core.NewServErr("更新用户余额失败", err) return core.NewServErr("更新用户余额失败", err)
} }
if r.RowsAffected == 0 {
return core.NewBizErr("余额状态已过期")
}
// 新增动账记录 // 新增动账记录
err = q.BalanceActivity.Create(&m.BalanceActivity{ err = q.BalanceActivity.Create(&m.BalanceActivity{
@@ -204,12 +207,18 @@ func (s *userService) UpdateByAdmin(data UpdateUserByAdminData) error {
return nil return nil
} }
_, err := q.User.Where(q.User.ID.Eq(data.ID)).UpdateSimple(do...) r, err := q.User.Where(q.User.ID.Eq(data.ID)).UpdateSimple(do...)
if errors.Is(err, gorm.ErrDuplicatedKey) { if errors.Is(err, gorm.ErrDuplicatedKey) {
return core.NewBizErr("账号已存在,请检查手机号/用户名/邮箱是否重复") return core.NewBizErr("账号已存在,请检查手机号/用户名/邮箱是否重复")
} }
if err != nil {
return err return err
}
if r.RowsAffected == 0 {
return core.NewBizErr("用户状态已过期")
}
return nil
} }
type UpdateUserByAdminData struct { type UpdateUserByAdminData struct {
@@ -231,6 +240,12 @@ type UpdateUserByAdminData struct {
} }
func (s *userService) RemoveByAdmin(id int32) error { func (s *userService) RemoveByAdmin(id int32) error {
_, err := q.User.Where(q.User.ID.Eq(id)).UpdateColumn(q.User.DeletedAt, time.Now()) r, err := q.User.Where(q.User.ID.Eq(id)).UpdateColumn(q.User.DeletedAt, time.Now())
if err != nil {
return err return err
}
if r.RowsAffected == 0 {
return core.NewBizErr("用户状态已过期")
}
return nil
} }

View File

@@ -52,3 +52,13 @@ func HandleRemoveChannel(_ context.Context, task *asynq.Task) (err error) {
} }
return nil return nil
} }
func HandleRefreshEdges(_ context.Context, task *asynq.Task) (err error) {
slog.Info("[event]刷新边缘节点")
err = s.Channel.RefreshEdges()
if err != nil {
return fmt.Errorf("刷新边缘节点失败: %w", err)
}
return nil
}

View File

@@ -42,6 +42,10 @@ func RunApp(pCtx context.Context) error {
return RunTask(ctx) return RunTask(ctx)
}) })
g.Go(func() error {
return RunCron(ctx)
})
return g.Wait() return g.Wait()
} }
@@ -49,7 +53,6 @@ func RunApp(pCtx context.Context) error {
var fs embed.FS var fs embed.FS
func RunWeb(ctx context.Context) error { func RunWeb(ctx context.Context) error {
fiber := fiber.New(fiber.Config{ fiber := fiber.New(fiber.Config{
ProxyHeader: fiber.HeaderXForwardedFor, ProxyHeader: fiber.HeaderXForwardedFor,
ErrorHandler: ErrorHandler, ErrorHandler: ErrorHandler,
@@ -80,7 +83,7 @@ func RunWeb(ctx context.Context) error {
} }
func RunTask(ctx context.Context) error { func RunTask(ctx context.Context) error {
var server = asynq.NewServerFromRedisClient(deps.Redis, asynq.Config{ server := asynq.NewServerFromRedisClient(deps.Redis, asynq.Config{
ShutdownTimeout: 5 * time.Second, ShutdownTimeout: 5 * time.Second,
ErrorHandler: asynq.ErrorHandlerFunc(func(ctx context.Context, task *asynq.Task, err error) { ErrorHandler: asynq.ErrorHandlerFunc(func(ctx context.Context, task *asynq.Task, err error) {
slog.Error("任务执行失败", "task", task.Type(), "error", err) slog.Error("任务执行失败", "task", task.Type(), "error", err)
@@ -91,6 +94,7 @@ func RunTask(ctx context.Context) error {
var mux = asynq.NewServeMux() var mux = asynq.NewServeMux()
mux.HandleFunc(events.RemoveChannel, tasks.HandleRemoveChannel) mux.HandleFunc(events.RemoveChannel, tasks.HandleRemoveChannel)
mux.HandleFunc(events.CloseTrade, tasks.HandleCompleteTrade) mux.HandleFunc(events.CloseTrade, tasks.HandleCompleteTrade)
mux.HandleFunc(events.RefreshEdge, tasks.HandleRefreshEdges)
// 停止服务 // 停止服务
go func() { go func() {
@@ -107,6 +111,29 @@ func RunTask(ctx context.Context) error {
return nil return nil
} }
func RunCron(ctx context.Context) error {
cron := asynq.NewSchedulerFromRedisClient(deps.Redis, &asynq.SchedulerOpts{
Logger: &AppAsynqLogger{},
Location: time.Local,
})
cron.Register("0/10 * * * *", events.NewRefreshEdge())
// 停止服务
go func() {
<-ctx.Done()
cron.Shutdown()
}()
// 启动服务
err := cron.Run()
if err != nil {
return fmt.Errorf("定时任务服务运行失败: %w", err)
}
return nil
}
type AppAsynqLogger struct{} type AppAsynqLogger struct{}
func (l *AppAsynqLogger) Debug(args ...any) { func (l *AppAsynqLogger) Debug(args ...any) {