34 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
80f04c92ec 修复请求错误消息上报问题 2026-05-13 18:07:44 +08:00
ccbc6f0b67 完善提取处理流程,解决提取并发问题 2026-05-13 16:17:57 +08:00
d273731e31 修复购买数量低于限制的问题 2026-05-11 17:39:22 +08:00
65f8ee360b 完善通道管理机制 & 增强 otel 记录字段 2026-05-11 11:04:21 +08:00
042c8d1a51 完善 otel 配置 2026-05-08 13:53:47 +08:00
a0b0be2b8e 实现定时通道过期清理 2026-05-08 09:33:41 +08:00
8fc1d30578 优化白名单与通道提取功能 2026-05-07 12:43:15 +08:00
a4d9c28702 实现已发放优惠券的管理接口 2026-04-29 16:59:14 +08:00
ccb8db555e 放开提取接口权限 2026-04-28 18:00:24 +08:00
e70f2337cb 发放优惠券 2026-04-27 17:13:06 +08:00
d59f4ca37f 用户余额查询 2026-04-25 14:15:37 +08:00
0edc883084 用户修改套餐 ip 检查功能接口 2026-04-23 13:47:22 +08:00
d26106eb00 修复运行边界条件问题 2026-04-22 17:11:55 +08:00
6e14ea65d0 套餐白名单检查逻辑 & 检查订单金额 2026-04-21 18:09:53 +08:00
982cbb4cab 新增最小购买数量控制 & 购买前检查实名 2026-04-20 16:24:22 +08:00
a964fe4d69 实现代理网关管理接口 2026-04-18 13:12:40 +08:00
6db3caaecb 修复一些边界问题 2026-04-17 17:21:10 +08:00
80 changed files with 18925 additions and 1769 deletions

View File

@@ -1,6 +1,9 @@
# 应用配置
RUN_MODE=development
DEBUG_HTTP_DUMP=false
UPLOAD_DIR=./data/uploads
UPLOAD_PUBLIC_BASE_URL=
ARTICLE_UPLOAD_MAX_BYTES=5242880
# 数据库配置
DB_HOST=127.0.0.1
@@ -16,6 +19,7 @@ REDIS_PORT=6379
# otel 配置
OTEL_HOST=127.0.0.1
OTEL_PORT=4317
OTEL_NAME_SUFFIX=dev
# 白银节点
BAIYIN_CLOUD_URL=

2
.gitignore vendored
View File

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

View File

@@ -1,44 +1,54 @@
## TODO
- edge.area_id 可为空,代表节点无固定地区
- 后台展示 mac, ip:port实际地区
上传文件平铺到 uploads不分子文件夹
错误提示增强,展示整链路信息
交易信息持久化
用户请求需要检查数据权限
订单关闭问题,在前端关闭窗口后直接调用了全部订单接口,应改成先确认再关闭
用反射实现环境变量解析,以简化函数签名
- 取消订单接口改成只允许管理员调用
- 新增关闭订单接口,关闭订单的逻辑是先尝试完成,如果订单未支付则取消订单
---
分离 task 的客户端支持多进程prefork 必要!)
jsonb 类型转换问题,考虑一个高效的 any 到 struct 转换工具
慢速请求底层调用埋点监控
数据库转模型文件
冷数据迁移方案
## 开发环境
## 开发流程
### 更新表结构的流程
### 新建数据表流程
1. 编辑 `scripts/sql/init.sql` 文件,参照原有格式,需要注意:
1. 创建 model 文件
2. 将 model 按照格式添加声明到 `cmd/gen/main.go`
3. 编辑 `scripts/sql/init.sql` 文件,参照原有格式,需要注意:
- 先写 drop table if exists 语句,确保脚本可以幂等执行
- 编写 create table 语句,按需添加审计字段和软删除字段 (created_at, updated_at, deleted_at)
- 为有必要的字段添加索引
- 为数据表及其字段添加注释
- 在文件末尾创建数据表流程全部结束后,为字段添加外键
2. 建议用数据库工具检查差异并增量更新,或者手动增量更新
3. 创建 model 文件,并将其添加到 gen 代码中
4. 生成查询文件
4. 调用 `go run ./cmd/gen/main.go` 生成查询文件
### 权限管理
### 更新数据表流程
`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` 文件的权限区域添加或修改权限条目
## 业务逻辑
@@ -49,13 +59,6 @@ jsonb 类型转换问题,考虑一个高效的 any 到 struct 转换工具
3. 异步回调事件,收到支付成功事件后自动完成订单
4. 用户退出支付界面,客户端主动发起关闭订单
### 产品字典表
| 代码 | 产品 |
| ----- | ------------ |
| short | 短效动态代理 |
| long | 长效动态代理 |
### 节点分配与存储逻辑
提取:

View File

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

View File

@@ -23,7 +23,7 @@ services:
restart: unless-stopped
postgres-migrate:
image: postgres:17
image: postgres:17.7
environment:
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_PASSWORD: ${DB_PASSWORD}
@@ -31,6 +31,23 @@ services:
ports:
- "5433:5432"
asynqmon:
image: hibiken/asynqmon:latest
environment:
- REDIS_ADDR=redis:6379
ports:
- "9800:8080"
depends_on:
- redis
gost:
image: gogost/gost
command: >
-api test:test@:9700
ports:
- "9700:9700"
restart: unless-stopped
volumes:
postgres_data:
redis_data:

File diff suppressed because it is too large Load Diff

36
go.mod
View File

@@ -23,11 +23,12 @@ require (
github.com/smartwalle/alipay/v3 v3.2.28
github.com/valyala/fasthttp v1.68.0
github.com/wechatpay-apiv3/wechatpay-go v0.2.21
go.opentelemetry.io/otel v1.38.0
go.opentelemetry.io/otel v1.43.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0
go.opentelemetry.io/otel/sdk v1.38.0
golang.org/x/crypto v0.45.0
golang.org/x/sync v0.18.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/sync v0.20.0
gorm.io/datatypes v1.2.7
gorm.io/driver/postgres v1.6.0
gorm.io/gen v0.3.27
@@ -59,7 +60,7 @@ require (
github.com/gofiber/utils v1.1.0 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect
github.com/gomodule/redigo v2.0.0+incompatible // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
@@ -86,20 +87,19 @@ require (
github.com/valyala/bytebufferpool v1.0.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.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/proto/otlp v1.10.0 // indirect
golang.org/x/mod v0.33.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.39.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251124214823-79d6a2a48846 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect
google.golang.org/grpc v1.77.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
golang.org/x/tools v0.42.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/grpc v1.80.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gorm.io/driver/mysql v1.6.0 // indirect
gorm.io/hints v1.1.2 // indirect

80
go.sum
View File

@@ -154,8 +154,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -277,22 +277,22 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib v1.38.0 h1:msaHYZ13HfLIbqXsGwZZQBg5zgxwumlZ1mCkXn3E7LM=
go.opentelemetry.io/contrib v1.38.0/go.mod h1:4Vp7Az5Dez02V1lCi9OqLvSmSz0lbZu/O2r4XZsqwB0=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -309,8 +309,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@@ -321,8 +321,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -344,8 +344,8 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -357,8 +357,8 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -379,8 +379,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -403,8 +403,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -419,35 +419,35 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto/googleapis/api v0.0.0-20251124214823-79d6a2a48846 h1:ZdyUkS9po3H7G0tuh955QVyyotWvOD4W0aEapeGeUYk=
google.golang.org/genproto/googleapis/api v0.0.0-20251124214823-79d6a2a48846/go.mod h1:Fk4kyraUvqD7i5H6S43sj2W98fbZa75lpZz/eUyhfO0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=

29
pkg/env/env.go vendored
View File

@@ -18,12 +18,15 @@ const (
)
var (
RunMode = RunModeProd
LogLevel = slog.LevelDebug
TradeExpire = 15 * 60 // 交易过期时间,单位秒。默认 900 秒15 分钟)
SessionAccessExpire = 60 * 60 * 2 // 访问令牌过期时间,单位秒。默认 2 小时
SessionRefreshExpire = 60 * 60 * 24 * 7 // 刷新令牌过期时间,单位秒。默认 7 天
DebugHttpDump = false // 是否打印请求和响应的原始数据
RunMode = RunModeProd
LogLevel = slog.LevelDebug
TradeExpire = 15 * 60 // 交易过期时间,单位秒。默认 900 秒15 分钟)
SessionAccessExpire = 60 * 60 * 2 // 访问令牌过期时间,单位秒。默认 2 小时
SessionRefreshExpire = 60 * 60 * 24 * 7 // 刷新令牌过期时间,单位秒。默认 7 天
DebugHttpDump = false // 是否打印请求和响应的原始数据
UploadDir = "./data/uploads"
UploadPublicBaseURL = ""
ArticleUploadMaxBytes = 5 * 1024 * 1024
DbHost = "localhost"
DbPort = "5432"
@@ -35,12 +38,16 @@ var (
RedisPort = "6379"
RedisPassword = ""
OtelHost string
OtelPort string
OtelHost string
OtelPort string
OtelNameSuffix string
BaiyinCloudUrl string
BaiyinTokenUrl string
GostApiPort = 9700
GostApiPathPrefix = ""
IdenCallbackUrl string
IdenAccessKey string
IdenSecretKey string
@@ -105,6 +112,9 @@ func Init() {
errs = append(errs, parse(&SessionAccessExpire, "SESSION_ACCESS_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(&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(&DbPort, "DB_PORT", true, nil))
@@ -118,9 +128,12 @@ func Init() {
errs = append(errs, parse(&OtelHost, "OTEL_HOST", true, nil))
errs = append(errs, parse(&OtelPort, "OTEL_PORT", true, nil))
errs = append(errs, parse(&OtelNameSuffix, "OTEL_NAME_SUFFIX", true, 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(&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(&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 {
var y, m, d = date.Date()
return time.Date(y, m, d, 0, 0, 0, 0, date.Location())
}
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 IsSameDate(date1, date2 time.Time) bool {
var y1, m1, d1 = date1.Local().Date()
var y2, m2, d2 = date2.Local().Date()
return y1 == y2 && m1 == m2 && d1 == d2
}
func Today() time.Time {
return DateHead(time.Now())
}
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
var y, m, d = time.Now().Date()
return time.Date(y, m, d, 0, 0, 0, 0, time.Local)
}
func IsToday(date time.Time) bool {

View File

@@ -9,8 +9,8 @@ if ($confrim -ne "y") {
exit 0
}
docker build -t repo.lanhuip.com:8554/lanhu/platform:latest .
docker build -t repo.lanhuip.com:8554/lanhu/platform:$($args[0]) .
docker build -t repo.lanhuip.com/lanhu/platform:latest .
docker build -t repo.lanhuip.com/lanhu/platform:$($args[0]) .
docker push repo.lanhuip.com:8554/lanhu/platform:latest
docker push repo.lanhuip.com:8554/lanhu/platform:$($args[0])
docker push repo.lanhuip.com/lanhu/platform:latest
docker push repo.lanhuip.com/lanhu/platform:$($args[0])

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

@@ -126,7 +126,11 @@ insert into permission (name, description, sort) values
('channel', 'IP', 11),
('trade', '交易', 12),
('bill', '账单', 13),
('balance_activity', '余额变动', 14);
('balance_activity', '余额变动', 14),
('proxy', '代理', 15),
('coupon_user', '已发放优惠券', 16),
('article', '文档', 17),
('article_group', '文档分组', 18);
-- --------------------------
-- level 2
@@ -135,74 +139,94 @@ insert into permission (name, description, sort) values
-- permission 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'permission' and deleted_at is null), 'permission:read', '读取权限列表', 1),
((select id from permission where name = 'permission' and deleted_at is null), 'permission:write', '写入权限', 2);
((select id from permission where name = 'permission' and deleted_at is null), 'permission:write', '编辑权限', 2);
-- admin_role 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'admin_role' and deleted_at is null), 'admin_role:read', '读取管理员角色列表', 1),
((select id from permission where name = 'admin_role' and deleted_at is null), 'admin_role:write', '写入管理员角色', 2);
((select id from permission where name = 'admin_role' and deleted_at is null), 'admin_role:write', '编辑管理员角色', 2);
-- admin 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'admin' and deleted_at is null), 'admin:read', '读取管理员列表', 1),
((select id from permission where name = 'admin' and deleted_at is null), 'admin:write', '写入管理员', 2);
((select id from permission where name = 'admin' and deleted_at is null), 'admin:write', '编辑管理员', 2);
-- product 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'product' and deleted_at is null), 'product:read', '读取产品列表', 1),
((select id from permission where name = 'product' and deleted_at is null), 'product:write', '写入产品', 2);
((select id from permission where name = 'product' and deleted_at is null), 'product:write', '编辑产品', 2);
-- product_sku 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'product_sku' and deleted_at is null), 'product_sku:read', '读取产品套餐列表', 1),
((select id from permission where name = 'product_sku' and deleted_at is null), 'product_sku:write', '写入产品套餐', 2);
((select id from permission where name = 'product_sku' and deleted_at is null), 'product_sku:write', '编辑产品套餐', 2);
-- discount 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'discount' and deleted_at is null), 'discount:read', '读取折扣列表', 1),
((select id from permission where name = 'discount' and deleted_at is null), 'discount:write', '写入折扣', 2);
((select id from permission where name = 'discount' and deleted_at is null), 'discount:write', '编辑折扣', 2);
-- resource 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'resource' and deleted_at is null), 'resource:read', '读取用户套餐列表', 1),
((select id from permission where name = 'resource' and deleted_at is null), 'resource:write', '写入用户套餐', 2),
((select id from permission where name = 'resource' and deleted_at is null), 'resource:write', '编辑用户套餐', 2),
((select id from permission where name = 'resource' and deleted_at is null), 'resource:short', '短效动态套餐', 3),
((select id from permission where name = 'resource' and deleted_at is null), 'resource:long', '长效动态套餐', 4);
-- user 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'user' and deleted_at is null), 'user:read', '读取用户列表', 1),
((select id from permission where name = 'user' and deleted_at is null), 'user:write', '写入用户', 2);
((select id from permission where name = 'user' and deleted_at is null), 'user:write', '编辑用户', 2);
-- coupon 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'coupon' and deleted_at is null), 'coupon:read', '读取优惠券列表', 1),
((select id from permission where name = 'coupon' and deleted_at is null), 'coupon:write', '写入优惠券', 2);
((select id from permission where name = 'coupon' and deleted_at is null), 'coupon:write', '编辑优惠券', 2);
-- batch 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'batch' and deleted_at is null), 'batch:read', '读取批次列表', 1),
((select id from permission where name = 'batch' and deleted_at is null), 'batch:write', '写入批次', 2);
((select id from permission where name = 'batch' and deleted_at is null), 'batch:write', '编辑批次', 2);
-- channel 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'channel' and deleted_at is null), 'channel:read', '读取 IP 列表', 1),
((select id from permission where name = 'channel' and deleted_at is null), 'channel:write', '写入 IP', 2);
((select id from permission where name = 'channel' and deleted_at is null), 'channel:write', '编辑 IP', 2);
-- proxy 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'proxy' and deleted_at is null), 'proxy:read', '读取代理列表', 1),
((select id from permission where name = 'proxy' and deleted_at is null), 'proxy:write', '编辑代理', 2);
-- trade 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'trade' and deleted_at is null), 'trade:read', '读取交易列表', 1),
((select id from permission where name = 'trade' and deleted_at is null), 'trade:write', '写入交易', 2);
((select id from permission where name = 'trade' and deleted_at is null), 'trade:write', '编辑交易', 2);
-- bill 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'bill' and deleted_at is null), 'bill:read', '读取账单列表', 1),
((select id from permission where name = 'bill' and deleted_at is null), 'bill:write', '写入账单', 2);
((select id from permission where name = 'bill' and deleted_at is null), 'bill:write', '编辑账单', 2);
-- balance_activity 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'balance_activity' and deleted_at is null), 'balance_activity:read', '读取余额变动列表', 1);
-- coupon_user 子权限
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: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
-- --------------------------
@@ -211,6 +235,14 @@ insert into permission (parent_id, name, description, sort) values
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'product_sku:write' and deleted_at is null), 'product_sku:write:status', '更改产品套餐状态', 1);
-- proxy:write 子权限
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);
-- 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 子权限
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);
@@ -226,7 +258,7 @@ insert into permission (parent_id, name, description, sort) values
-- user:write 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'user:write' and deleted_at is null), 'user:write:balance', '写入用户余额', 1),
((select id from permission where name = 'user:write' and deleted_at is null), 'user:write:balance', '编辑用户余额', 1),
((select id from permission where name = 'user:write' and deleted_at is null), 'user:write:bind', '用户认领', 2);
-- batch:read 子权限
@@ -249,6 +281,18 @@ insert into permission (parent_id, name, description, sort) values
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'balance_activity:read' and deleted_at is null), 'balance_activity:read:of_user', '读取指定用户的余额变动列表', 1);
-- coupon:write 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'coupon:write' and deleted_at is null), 'coupon:write:assign', '发放优惠券', 1);
-- coupon_user:read 子权限
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);
-- 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
-- --------------------------

View File

@@ -143,6 +143,62 @@ comment on column announcement.created_at is '创建时间';
comment on column announcement.updated_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
drop table if exists inquiry cascade;
create table inquiry (
@@ -549,6 +605,7 @@ create table proxy (
mac text not null,
ip inet not null,
host text,
port int,
secret text,
type 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 column proxy.id is '代理服务ID';
comment on column proxy.version is '代理服务版本';
comment on column proxy.port is '代理服务端口';
comment on column proxy.mac is '代理服务名称';
comment on column proxy.ip is '代理服务地址';
comment on column proxy.host 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.meta is '代理服务元信息';
comment on column proxy.created_at is '创建时间';
comment on column proxy.updated_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
drop table if exists edge cascade;
create table edge (
@@ -584,9 +667,9 @@ create table edge (
version int not null,
mac text not null,
ip inet not null,
port int,
isp int not null,
prov text not null,
city text not null,
area_id int,
status int not null default 0,
rtt 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 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_city on edge (city) where deleted_at is null;
create index idx_edge_area_id on edge (area_id) where deleted_at is null;
create index idx_edge_created_at on edge (created_at) where deleted_at is null;
-- edge表字段注释
comment on table edge is '节点表';
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.mac is '节点 mac 地址';
comment on column edge.ip is '节点地址';
comment on column edge.mac is '节点 mac 地址或 GOST chain 名称';
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.prov is '省份';
comment on column edge.city is '城市';
comment on column edge.area_id is '城市地区ID';
comment on column edge.status is '节点状态0-离线1-正常';
comment on column edge.rtt is '最近平均延迟';
comment on column edge.loss is '最近丢包率';
@@ -762,6 +844,7 @@ create table product_sku (
price_min decimal not null,
status int not null default 1,
sort int not null default 0,
count_min int not null default 1,
created_at timestamptz default current_timestamp,
updated_at timestamptz default current_timestamp,
deleted_at timestamptz
@@ -780,6 +863,7 @@ comment on column product_sku.name is 'SKU 可读名称';
comment on column product_sku.price_min is '最低价格';
comment on column product_sku.status is 'SKU状态0-禁用1-正常';
comment on column product_sku.sort is '排序';
comment on column product_sku.count_min is '最小购买数量';
comment on column product_sku.created_at is '创建时间';
comment on column product_sku.updated_at is '更新时间';
comment on column product_sku.deleted_at is '删除时间';
@@ -816,6 +900,7 @@ create table resource (
code text,
type int not null,
active bool not null default true,
checkip bool not null default true,
created_at timestamptz default current_timestamp,
updated_at timestamptz default current_timestamp,
deleted_at timestamptz
@@ -830,9 +915,10 @@ comment on table resource is '套餐表';
comment on column resource.id is '套餐ID';
comment on column resource.user_id is '用户ID';
comment on column resource.resource_no is '套餐编号';
comment on column resource.active is '套餐状态';
comment on column resource.type is '套餐类型1-短效动态2-长效动态';
comment on column resource.code is '产品编码';
comment on column resource.type is '套餐类型1-短效动态2-长效动态';
comment on column resource.active is '套餐状态';
comment on column resource.checkip is '提取时是否检查 ip 地址';
comment on column resource.created_at is '创建时间';
comment on column resource.updated_at is '更新时间';
comment on column resource.deleted_at is '删除时间';
@@ -1107,7 +1193,7 @@ comment on table coupon_user is '优惠券发放表';
comment on column coupon_user.id is '记录ID';
comment on column coupon_user.coupon_id is '优惠券ID';
comment on column coupon_user.user_id is '用户ID';
comment on column coupon_user.status is '使用状态0-未使用1-已使用';
comment on column coupon_user.status is '使用状态0-未使用1-已使用2-已禁用';
comment on column coupon_user.expire_at is '过期时间';
comment on column coupon_user.used_at is '使用时间';
comment on column coupon_user.created_at is '创建时间';
@@ -1173,6 +1259,10 @@ alter table channel
add constraint fk_channel_user_id foreign key (user_id) references "user" (id) on delete cascade;
alter table channel
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
add constraint fk_channel_edge_id foreign key (edge_id) references edge (id) on delete set null;
alter table channel
@@ -1224,6 +1314,10 @@ 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;
-- article表外键
alter table article
add constraint fk_article_group_id foreign key (group_id) references article_group (id) on delete restrict;
-- product_sku表外键
alter table product_sku
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).
Where(
q.Session.AccessToken.Eq(accessToken),
q.Session.AccessTokenExpires.Gt(now),
q.Session.AccessTokenExpires.Gt(now.UTC()),
).First()
}
@@ -25,7 +25,7 @@ func FindSessionByRefresh(refreshToken string, now time.Time) (*m.Session, error
Preload(field.Associations).
Where(
q.Session.RefreshToken.Eq(refreshToken),
q.Session.RefreshTokenExpires.Gt(now),
q.Session.RefreshTokenExpires.Gt(now.UTC()),
).First()
}

View File

@@ -118,7 +118,7 @@ func Query(in any) url.Values {
case int:
out.Add(name, strconv.Itoa(value))
case bool:
if tags[1] == "b2i" {
if len(tags) > 1 && tags[1] == "b2i" {
out.Add(name, u.Ternary(value, "1", "0"))
} else {
out.Add(name, strconv.FormatBool(value))

View File

@@ -48,19 +48,39 @@ const (
ScopeUserWriteBalanceDec = string("user:write:balance:dec") // 减少用户余额
ScopeUserWriteBind = string("user:write:bind") // 用户认领
ScopeCoupon = string("coupon") // 优惠券
ScopeCouponRead = string("coupon:read") // 读取优惠券列表
ScopeCouponWrite = string("coupon:write") // 写入优惠券
ScopeCoupon = string("coupon") // 优惠券
ScopeCouponRead = string("coupon:read") // 读取优惠券列表
ScopeCouponWrite = string("coupon:write") // 写入优惠券
ScopeCouponWriteAssign = string("coupon:write:assign") // 发放优惠券
ScopeCouponUser = string("coupon_user") // 用户优惠券
ScopeCouponUserRead = string("coupon_user:read") // 读取用户优惠券列表
ScopeCouponUserReadOfUser = string("coupon_user:read:of_user") // 读取指定用户的用户优惠券列表
ScopeCouponUserWrite = string("coupon_user:write") // 写入用户优惠券
ScopeBatch = string("batch") // 批次
ScopeBatchRead = string("batch:read") // 读取批次列表
ScopeBatchReadOfUser = string("batch:read:of_user") // 读取指定用户的批次列表
ScopeBatchWrite = string("batch:write") // 写入批次
ScopeChannel = string("channel") // IP
ScopeChannelRead = string("channel:read") // 读取 IP 列表
ScopeChannelReadOfUser = string("channel:read:of_user") // 读取指定用户的 IP 列表
ScopeChannelWrite = string("channel:write") // 写入 IP
ScopeChannel = string("channel") // IP
ScopeChannelRead = string("channel:read") // 读取 IP 列表
ScopeChannelReadOfUser = string("channel:read:of_user") // 读取指定用户的 IP 列表
ScopeChannelWrite = string("channel:write") // 写入 IP
ScopeChannelWriteClearExpired = string("channel:write:clear_expired") // 清理过期 IP
ScopeProxy = string("proxy") // 代理
ScopeProxyRead = string("proxy:read") // 读取代理列表
ScopeProxyWrite = string("proxy:write") // 写入代理
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") // 交易
ScopeTradeRead = string("trade:read") // 读取交易列表

View File

@@ -52,7 +52,8 @@ func ErrorHandler(c *fiber.Ctx, err error) error {
case errors.As(err, &servErr):
code = fiber.StatusInternalServerError
message = err.Error()
slog.Warn("服务端错误", slog.String("error", servErr.Error()))
message = "服务端错误"
case errors.As(err, &timeErr):
code = fiber.StatusBadRequest
@@ -79,7 +80,6 @@ func ErrorHandler(c *fiber.Ctx, err error) error {
slog.Warn("未处理的异常", slog.String("type", t.String()), slog.String("error", err.Error()))
}
slog.Warn(message)
c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
return c.Status(code).SendString(message)
}

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")
}
}

View File

@@ -3,6 +3,7 @@ package globals
import (
"context"
"fmt"
"log/slog"
"platform/pkg/env"
"go.opentelemetry.io/otel"
@@ -17,11 +18,17 @@ import (
var tp *trace.TracerProvider
func initOtel(ctx context.Context) error {
addr := env.OtelHost + ":" + env.OtelPort
name := "lanhu-platform"
if env.OtelNameSuffix != "" {
name += "-" + env.OtelNameSuffix
}
slog.Info("初始化 otel", "endpoint", addr, "service_suffix", name)
if env.OtelHost == "" || env.OtelPort == "" {
return nil
}
addr := env.OtelHost + ":" + env.OtelPort
exporter, err := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint(addr),
otlptracegrpc.WithInsecure(),
@@ -36,7 +43,7 @@ func initOtel(ctx context.Context) error {
trace.WithResource(
resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String("lanhu-platform"),
semconv.ServiceNameKey.String(name),
),
),
)

View File

@@ -15,12 +15,12 @@ func PageAdminByAdmin(c *fiber.Ctx) error {
return err
}
var req PageAdminsReq
var req core.PageReq
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
list, total, err := s.Admin.Page(req.PageReq)
list, total, err := s.Admin.Page(req)
if err != nil {
return err
}
@@ -33,10 +33,6 @@ func PageAdminByAdmin(c *fiber.Ctx) error {
})
}
type PageAdminsReq struct {
core.PageReq
}
func AllAdminByAdmin(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeAdminRead)
if err != nil {

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
import (
"platform/pkg/u"
"platform/web/auth"
"platform/web/core"
g "platform/web/globals"
@@ -11,6 +10,63 @@ import (
"github.com/gofiber/fiber/v2"
)
// PageBalanceActivity 分页查询当前用户的余额变动记录
func PageBalanceActivity(c *fiber.Ctx) error {
// 获取当前用户ID
authCtx, err := auth.GetAuthCtx(c).PermitUser()
if err != nil {
return err
}
// 解析请求参数
req := new(PageBalanceActivityByUserReq)
if err := g.Validator.ParseBody(c, req); err != nil {
return err
}
// 构造查询条件
do := q.BalanceActivity.Where(q.BalanceActivity.UserID.Eq(authCtx.User.ID))
if req.BillNo != nil {
do = do.Where(q.Bill.As("Bill").BillNo.Eq(*req.BillNo))
}
if req.CreatedAtStart != nil {
do = do.Where(q.BalanceActivity.CreatedAt.Gte(req.CreatedAtStart.UTC()))
}
if req.CreatedAtEnd != nil {
do = do.Where(q.BalanceActivity.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
}
// 查询余额变动列表
list, total, err := q.BalanceActivity.
Joins(q.BalanceActivity.Bill).
Select(
q.BalanceActivity.ALL,
q.Bill.As("Bill").ID.As("Bill__id"),
q.Bill.As("Bill").BillNo.As("Bill__bill_no"),
).
Where(do).
Order(q.BalanceActivity.CreatedAt.Desc()).
FindByPage(req.GetOffset(), req.GetLimit())
if err != nil {
return core.NewBizErr("获取数据失败", err)
}
// 返回结果
return c.JSON(core.PageResp{
List: list,
Total: int(total),
Page: req.GetPage(),
Size: req.GetSize(),
})
}
type PageBalanceActivityByUserReq struct {
core.PageReq
BillNo *string `json:"bill_no,omitempty"`
CreatedAtStart *time.Time `json:"created_at_start,omitempty"`
CreatedAtEnd *time.Time `json:"created_at_end,omitempty"`
}
// PageBalanceActivityByAdmin 分页查询所有余额变动记录
func PageBalanceActivityByAdmin(c *fiber.Ctx) error {
// 检查权限
@@ -34,16 +90,14 @@ func PageBalanceActivityByAdmin(c *fiber.Ctx) error {
do = do.Where(q.Bill.As("Bill").BillNo.Eq(*req.BillNo))
}
if req.CreatedAtStart != nil {
t := u.DateHead(*req.CreatedAtStart)
do = do.Where(q.BalanceActivity.CreatedAt.Gte(t))
do = do.Where(q.BalanceActivity.CreatedAt.Gte(req.CreatedAtStart.UTC()))
}
if req.CreatedAtEnd != nil {
t := u.DateTail(*req.CreatedAtEnd)
do = do.Where(q.BalanceActivity.CreatedAt.Lte(t))
do = do.Where(q.BalanceActivity.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
}
// 查询余额变动列表
list, total, err := q.BalanceActivity.Debug().
list, total, err := q.BalanceActivity.
Joins(q.BalanceActivity.User, q.BalanceActivity.Admin, q.BalanceActivity.Bill).
Select(
q.BalanceActivity.ALL,
@@ -96,12 +150,10 @@ func PageBalanceActivityOfUserByAdmin(c *fiber.Ctx) error {
do = do.Where(q.Bill.As("Bill").BillNo.Eq(*req.BillNo))
}
if req.CreatedAtStart != nil {
t := u.DateHead(*req.CreatedAtStart)
do = do.Where(q.BalanceActivity.CreatedAt.Gte(t))
do = do.Where(q.BalanceActivity.CreatedAt.Gte(req.CreatedAtStart.UTC()))
}
if req.CreatedAtEnd != nil {
t := u.DateTail(*req.CreatedAtEnd)
do = do.Where(q.BalanceActivity.CreatedAt.Lte(t))
do = do.Where(q.BalanceActivity.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
}
// 查询余额变动列表

View File

@@ -1,7 +1,6 @@
package handlers
import (
"platform/pkg/u"
"platform/web/auth"
"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))
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 {
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()).
FindByPage(req.GetOffset(), req.GetLimit())
if err != nil {
@@ -53,8 +57,9 @@ func PageBatch(ctx *fiber.Ctx) error {
type PageResourceBatchReq struct {
c.PageReq
TimeStart *time.Time `json:"time_start"`
TimeEnd *time.Time `json:"time_end"`
ResourceNo *string `json:"resource_no"`
TimeStart *time.Time `json:"time_start"`
TimeEnd *time.Time `json:"time_end"`
}
// PageBatchByAdmin 分页查询所有提取记录
@@ -89,12 +94,10 @@ func PageBatchByAdmin(c *fiber.Ctx) error {
do = do.Where(q.LogsUserUsage.ISP.Eq(*req.Isp))
}
if req.CreatedAtStart != nil {
time := u.DateHead(*req.CreatedAtStart)
do = do.Where(q.LogsUserUsage.Time.Gte(time))
do = do.Where(q.LogsUserUsage.Time.Gte(req.CreatedAtStart.UTC()))
}
if req.CreatedAtEnd != nil {
time := u.DateTail(*req.CreatedAtEnd)
do = do.Where(q.LogsUserUsage.Time.Lte(time))
do = do.Where(q.LogsUserUsage.Time.Lte(req.CreatedAtEnd.UTC()))
}
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").Name.As("User__name"),
q.Resource.As("Resource").ResourceNo.As("Resource__resource_no"),
q.Resource.As("Resource").Type.As("Resource__type"),
).
Where(do).
Order(q.LogsUserUsage.Time.Desc()).
@@ -158,12 +162,10 @@ func PageBatchOfUserByAdmin(ctx *fiber.Ctx) error {
do = do.Where(q.LogsUserUsage.ISP.Eq(*req.Isp))
}
if req.CreatedAtStart != nil {
t := u.DateHead(*req.CreatedAtStart)
do = do.Where(q.LogsUserUsage.Time.Gte(t))
do = do.Where(q.LogsUserUsage.Time.Gte(req.CreatedAtStart.UTC()))
}
if req.CreatedAtEnd != nil {
t := u.DateTail(*req.CreatedAtEnd)
do = do.Where(q.LogsUserUsage.Time.Lte(t))
do = do.Where(q.LogsUserUsage.Time.Lte(req.CreatedAtEnd.UTC()))
}
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").Name.As("User__name"),
q.Resource.As("Resource").ResourceNo.As("Resource__resource_no"),
q.Resource.As("Resource").Type.As("Resource__type"),
).
Where(do).
Order(q.LogsUserUsage.Time.Desc()).

View File

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

View File

@@ -15,90 +15,6 @@ import (
"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 分页查询当前用户通道
func ListChannel(c *fiber.Ctx) error {
// 检查权限
@@ -126,10 +42,10 @@ func ListChannel(c *fiber.Ctx) error {
}
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 {
cond.Where(q.Channel.ExpiredAt.Lte(*req.ExpireBefore))
cond = cond.Where(q.Channel.ExpiredAt.Lte(req.ExpireBefore.UTC()))
}
// 查询数据
@@ -172,12 +88,7 @@ type ListChannelsReq struct {
// CreateChannel 创建新通道
func CreateChannel(c *fiber.Ctx) error {
// 检查权限
_, err := auth.GetAuthCtx(c).PermitUser()
if err != nil {
return err
}
// 不检查权限,允许 api 调用
// 解析参数
req := new(CreateChannelReq)
@@ -191,40 +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 {
isp = u.X(m.ToEdgeISP(*req.Isp))
filter.Isp = u.X(m.ToEdgeISP(*req.Isp))
}
result, err := s.Channel.CreateChannels(
ip,
req.ResourceId,
no,
req.AuthType == s.ChannelAuthTypeIp,
req.AuthType == s.ChannelAuthTypePass,
req.Count,
s.EdgeFilter{
Isp: isp,
Prov: req.Prov,
City: req.City,
},
filter,
)
if err != nil {
return err
}
// 返回结果
var resp = make([]*CreateChannelRespItem, len(result))
for i, channel := range result {
resp[i] = &CreateChannelRespItem{
Proto: req.Protocol,
Host: channel.Host,
Port: channel.Port,
}
if req.AuthType == s.ChannelAuthTypePass {
resp[i].Username = channel.Username
resp[i].Password = channel.Password
}
}
return c.JSON(resp)
return c.JSON(buildCreateChannelResp(result, req.Protocol, req.AuthType))
}
type CreateChannelReq struct {
@@ -237,9 +140,100 @@ type CreateChannelReq struct {
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 {
Proto int `json:"-"`
Host string `json:"host"`
IP string `json:"ip"`
Port uint16 `json:"port"`
Username *string `json:"username,omitempty"`
Password *string `json:"password,omitempty"`
@@ -272,6 +266,97 @@ type RemoveChannelsReq struct {
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 分页查询指定用户的通道
func PageChannelOfUserByAdmin(c *fiber.Ctx) error {
// 检查权限
@@ -301,12 +386,10 @@ func PageChannelOfUserByAdmin(c *fiber.Ctx) error {
do = do.Where(q.Channel.Port.Eq(*req.ProxyPort))
}
if req.ExpiredAtStart != nil {
t := u.DateHead(*req.ExpiredAtStart)
do = do.Where(q.Channel.ExpiredAt.Gte(t))
do = do.Where(q.Channel.ExpiredAt.Gte(req.ExpiredAtStart.UTC()))
}
if req.ExpiredAtEnd != nil {
t := u.DateHead(*req.ExpiredAtEnd)
do = do.Where(q.Channel.ExpiredAt.Lte(t))
do = do.Where(q.Channel.ExpiredAt.Lte(req.ExpiredAtEnd.UTC()))
}
// 查询通道列表
@@ -315,6 +398,7 @@ func PageChannelOfUserByAdmin(c *fiber.Ctx) error {
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"),
).
@@ -344,3 +428,49 @@ type PageChannelOfUserByAdminReq struct {
ExpiredAtStart *time.Time `json:"expired_at_start"`
ExpiredAtEnd *time.Time `json:"expired_at_end"`
}
// SyncChannelClearExpiredByAdmin 清理过期通道
func SyncChannelClearExpiredByAdmin(c *fiber.Ctx) error {
if _, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeChannelWriteClearExpired); err != nil {
return err
}
var req SyncChannelClearExpiredByAdminReq
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
count, err := s.Channel.ClearExpiredChannels(req.ProxyID)
if err != nil {
return err
}
return c.JSON(SyncChannelClearExpiredByAdminResp{
Count: count,
})
}
type SyncChannelClearExpiredByAdminReq struct {
ProxyID int32 `json:"proxy_id" validate:"required"`
}
type SyncChannelClearExpiredByAdminResp struct {
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

@@ -103,3 +103,27 @@ func DeleteCoupon(c *fiber.Ctx) error {
return nil
}
func AssignCoupon(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeCouponWriteAssign)
if err != nil {
return err
}
var req AssignCouponReq
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
err = s.Coupon.Assign(req.CouponID, req.UserID)
if err != nil {
return err
}
return nil
}
type AssignCouponReq struct {
CouponID int32 `json:"coupon_id" validate:"required"`
UserID int32 `json:"user_id" validate:"required"`
}

329
web/handlers/coupon_user.go Normal file
View File

@@ -0,0 +1,329 @@
package handlers
import (
"errors"
"platform/web/auth"
"platform/web/core"
g "platform/web/globals"
m "platform/web/models"
q "platform/web/queries"
s "platform/web/services"
"time"
"github.com/gofiber/fiber/v2"
"gorm.io/gen"
"gorm.io/gen/field"
"gorm.io/gorm"
)
// PageCouponUser 分页查询当前用户已发放优惠券
func PageCouponUser(c *fiber.Ctx) error {
authCtx, err := auth.GetAuthCtx(c).PermitUser()
if err != nil {
return err
}
var req PageCouponUserReq
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
conds := couponUserPageConditions(req.CouponUserPageFilter)
conds = append(conds, q.CouponUser.UserID.Eq(authCtx.User.ID))
list, total, err := q.CouponUser.
Joins(q.CouponUser.Coupon).
Select(couponUserSelect(false)...).
Where(conds...).
Order(q.CouponUser.CreatedAt.Desc()).
FindByPage(req.GetOffset(), req.GetLimit())
if err != nil {
return core.NewBizErr("获取数据失败", err)
}
return c.JSON(core.PageResp{
List: list,
Total: int(total),
Page: req.GetPage(),
Size: req.GetSize(),
})
}
type PageCouponUserReq struct {
core.PageReq
CouponUserPageFilter
}
// GetCouponUser 获取当前用户已发放优惠券详情
func GetCouponUser(c *fiber.Ctx) error {
authCtx, err := auth.GetAuthCtx(c).PermitUser()
if err != nil {
return err
}
var req core.IdReq
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
item, err := q.CouponUser.
Joins(q.CouponUser.Coupon).
Select(couponUserSelect(false)...).
Where(
q.CouponUser.ID.Eq(req.Id),
q.CouponUser.UserID.Eq(authCtx.User.ID),
).
Take()
if errors.Is(err, gorm.ErrRecordNotFound) {
return core.NewBizErr("已发放优惠券不存在")
}
if err != nil {
return core.NewBizErr("获取数据失败", err)
}
return c.JSON(item)
}
// PageCouponUserByAdmin 分页查询全部已发放优惠券
func PageCouponUserByAdmin(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeCouponUserRead)
if err != nil {
return err
}
var req PageCouponUserByAdminReq
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
conds := couponUserPageConditions(req.CouponUserPageFilter)
if req.UserID != nil {
conds = append(conds, q.CouponUser.UserID.Eq(*req.UserID))
}
if req.UserPhone != nil {
conds = append(conds, q.User.As("User").Phone.Eq(*req.UserPhone))
}
list, total, err := q.CouponUser.
Joins(q.CouponUser.Coupon, q.CouponUser.User).
Select(couponUserSelect(true)...).
Where(conds...).
Order(q.CouponUser.CreatedAt.Desc()).
FindByPage(req.GetOffset(), req.GetLimit())
if err != nil {
return core.NewBizErr("获取数据失败", err)
}
return c.JSON(core.PageResp{
List: list,
Total: int(total),
Page: req.GetPage(),
Size: req.GetSize(),
})
}
type PageCouponUserByAdminReq struct {
core.PageReq
CouponUserPageFilter
UserID *int32 `json:"user_id,omitempty"`
UserPhone *string `json:"user_phone,omitempty"`
}
// PageCouponUserOfUserByAdmin 分页查询指定用户已发放优惠券
func PageCouponUserOfUserByAdmin(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeCouponUserReadOfUser)
if err != nil {
return err
}
var req PageCouponUserOfUserByAdminReq
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
conds := couponUserPageConditions(req.CouponUserPageFilter)
conds = append(conds, q.CouponUser.UserID.Eq(req.UserID))
list, total, err := q.CouponUser.
Joins(q.CouponUser.Coupon, q.CouponUser.User).
Select(couponUserSelect(true)...).
Where(conds...).
Order(q.CouponUser.CreatedAt.Desc()).
FindByPage(req.GetOffset(), req.GetLimit())
if err != nil {
return core.NewBizErr("获取数据失败", err)
}
return c.JSON(core.PageResp{
List: list,
Total: int(total),
Page: req.GetPage(),
Size: req.GetSize(),
})
}
type PageCouponUserOfUserByAdminReq struct {
core.PageReq
CouponUserPageFilter
UserID int32 `json:"user_id" validate:"required"`
}
// GetCouponUserByAdmin 获取已发放优惠券详情
func GetCouponUserByAdmin(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeCouponUserRead)
if err != nil {
return err
}
var req core.IdReq
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
item, err := q.CouponUser.
Joins(q.CouponUser.Coupon, q.CouponUser.User).
Select(couponUserSelect(true)...).
Where(q.CouponUser.ID.Eq(req.Id)).
Take()
if errors.Is(err, gorm.ErrRecordNotFound) {
return core.NewBizErr("已发放优惠券不存在")
}
if err != nil {
return core.NewBizErr("获取数据失败", err)
}
return c.JSON(item)
}
func CreateCouponUserByAdmin(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeCouponUserWrite)
if err != nil {
return err
}
var req s.CreateCouponUserData
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
if err := s.CouponUser.Create(req); err != nil {
return err
}
return nil
}
func UpdateCouponUserByAdmin(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeCouponUserWrite)
if err != nil {
return err
}
var req s.UpdateCouponUserData
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
if err := s.CouponUser.Update(req); err != nil {
return err
}
return nil
}
func DeleteCouponUserByAdmin(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeCouponUserWrite)
if err != nil {
return err
}
var req core.IdReq
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
if err := s.CouponUser.Delete(req.Id); err != nil {
return err
}
return nil
}
type CouponUserPageFilter struct {
CouponID *int32 `json:"coupon_id,omitempty"`
CouponName *string `json:"coupon_name,omitempty"`
Status *m.CouponUserStatus `json:"status,omitempty"`
Expired *bool `json:"expired,omitempty"`
CreatedAtStart *time.Time `json:"created_at_start,omitempty"`
CreatedAtEnd *time.Time `json:"created_at_end,omitempty"`
ExpireAtStart *time.Time `json:"expire_at_start,omitempty"`
ExpireAtEnd *time.Time `json:"expire_at_end,omitempty"`
UsedAtStart *time.Time `json:"used_at_start,omitempty"`
UsedAtEnd *time.Time `json:"used_at_end,omitempty"`
}
func couponUserPageConditions(req CouponUserPageFilter) []gen.Condition {
conds := make([]gen.Condition, 0)
if req.CouponID != nil {
conds = append(conds, q.CouponUser.CouponID.Eq(*req.CouponID))
}
if req.CouponName != nil {
conds = append(conds, q.Coupon.As("Coupon").Name.Like("%"+*req.CouponName+"%"))
}
if req.Status != nil {
conds = append(conds, q.CouponUser.Status.Eq(int(*req.Status)))
}
if req.Expired != nil {
if *req.Expired {
conds = append(conds, q.CouponUser.ExpireAt.IsNotNull(), q.CouponUser.ExpireAt.Lte(time.Now().UTC()))
} else {
conds = append(conds, q.CouponUser.Where(q.CouponUser.ExpireAt.IsNull()).Or(q.CouponUser.ExpireAt.Gt(time.Now().UTC())))
}
}
if req.CreatedAtStart != nil {
conds = append(conds, q.CouponUser.CreatedAt.Gte(req.CreatedAtStart.UTC()))
}
if req.CreatedAtEnd != nil {
conds = append(conds, q.CouponUser.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
}
if req.ExpireAtStart != nil {
conds = append(conds, q.CouponUser.ExpireAt.Gte(req.ExpireAtStart.UTC()))
}
if req.ExpireAtEnd != nil {
conds = append(conds, q.CouponUser.ExpireAt.Lte(req.ExpireAtEnd.UTC()))
}
if req.UsedAtStart != nil {
conds = append(conds, q.CouponUser.UsedAt.Gte(req.UsedAtStart.UTC()))
}
if req.UsedAtEnd != nil {
conds = append(conds, q.CouponUser.UsedAt.Lte(req.UsedAtEnd.UTC()))
}
return conds
}
func couponUserSelect(includeUser bool) []field.Expr {
cols := []field.Expr{
q.CouponUser.ALL,
q.Coupon.As("Coupon").ID.As("Coupon__id"),
q.Coupon.As("Coupon").Name.As("Coupon__name"),
q.Coupon.As("Coupon").Amount.As("Coupon__amount"),
q.Coupon.As("Coupon").MinAmount.As("Coupon__min_amount"),
q.Coupon.As("Coupon").Count_.As("Coupon__count"),
q.Coupon.As("Coupon").Status.As("Coupon__status"),
q.Coupon.As("Coupon").ExpireType.As("Coupon__expire_type"),
q.Coupon.As("Coupon").ExpireAt.As("Coupon__expire_at"),
q.Coupon.As("Coupon").ExpireIn.As("Coupon__expire_in"),
q.Coupon.As("Coupon").CreatedAt.As("Coupon__created_at"),
q.Coupon.As("Coupon").UpdatedAt.As("Coupon__updated_at"),
}
if includeUser {
cols = append(cols,
q.User.As("User").ID.As("User__id"),
q.User.As("User").Phone.As("User__phone"),
q.User.As("User").Name.As("User__name"),
)
}
return cols
}

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

View File

@@ -1,401 +1,160 @@
package handlers
import (
"net/netip"
"platform/pkg/env"
"platform/web/auth"
"platform/web/core"
"platform/web/globals"
g "platform/web/globals"
s "platform/web/services"
"time"
"github.com/gofiber/fiber/v2"
)
func DebugRegisterProxyBaiYin(c *fiber.Ctx) error {
if env.RunMode != env.RunModeDev {
return fiber.ErrNotFound
}
// ====================
// admin 路由
// ====================
err := s.Proxy.RegisterBaiyin("1a:2b:3c:4d:5e:6f", netip.AddrFrom4([4]byte{127, 0, 0, 1}), "test", "test")
if err != nil {
return core.NewServErr("注册失败", err)
}
return nil
}
// 注册白银代理网关
func ProxyRegisterBaiYin(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitOfficialClient()
func PageProxyByAdmin(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyRead)
if err != nil {
return err
}
req := new(RegisterProxyBaiyinReq)
err = globals.Validator.ParseBody(c, req)
var req core.PageReq
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
list, total, err := s.Proxy.Page(req)
if err != nil {
return err
}
addr, err := netip.ParseAddr(req.IP)
return c.JSON(core.PageResp{
List: list,
Total: int(total),
Page: req.GetPage(),
Size: req.GetSize(),
})
}
func AllProxyByAdmin(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyRead)
if err != nil {
return core.NewServErr("IP地址格式错误", err)
return err
}
err = s.Proxy.RegisterBaiyin(req.Name, addr, req.Username, req.Password)
list, err := s.Proxy.All()
if err != nil {
return core.NewServErr("注册失败", err)
return err
}
return nil
return c.JSON(list)
}
type RegisterProxyBaiyinReq struct {
Name string `json:"name" validate:"required"`
IP string `json:"ip" validate:"required"`
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
func CreateProxy(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyWrite)
if err != nil {
return err
}
var req s.CreateProxy
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
if err := s.Proxy.Create(&req); err != nil {
return err
}
return c.JSON(nil)
}
// region 报告上线
func ProxyReportOnline(c *fiber.Ctx) (err error) {
return c.JSON(map[string]any{
"error": "接口暂不可用",
})
func UpdateProxy(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyWrite)
if err != nil {
return err
}
// // 检查接口权限
// _, err = auth2.GetAuthCtx(c).PermitSecretClient()
// if err != nil {
// return err
// }
var req s.UpdateProxy
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
// // 验证请求参数
// var req = new(ProxyReportOnlineReq)
// err = g.Validator.Validate(c, req)
// if err != nil {
// return err
// }
if err := s.Proxy.Update(&req); 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,
// })
return c.JSON(nil)
}
type ProxyReportOnlineReq struct {
Name string `json:"name" validate:"required"`
Version int `json:"version" validate:"required"`
func UpdateProxyStatus(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyWriteStatus)
if err != nil {
return err
}
var req s.UpdateProxyStatus
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
if err := s.Proxy.UpdateStatus(&req); err != nil {
return err
}
return c.JSON(nil)
}
type ProxyReportOnlineResp struct {
Id int32 `json:"id"`
Secret string `json:"secret"`
Permits []*ProxyPermit `json:"permits"`
Edges []*ProxyEdge `json:"edges"`
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)
}
// region 报告下线
func ProxyReportOffline(c *fiber.Ctx) (err error) {
return c.JSON(map[string]any{
"error": "接口暂不可用",
})
func SyncProxyChains(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyWrite)
if err != nil {
return err
}
// // 检查接口权限
// _, err = auth2.GetAuthCtx(c).PermitSecretClient()
// if err != nil {
// return err
// }
var req core.IdReq
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
// // 验证请求参数
// var req = new(ProxyReportOfflineReq)
// err = g.Validator.Validate(c, req)
// if err != nil {
// return err
// }
if err := s.Proxy.SyncChains(req.Id); 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
return c.JSON(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"` // 延迟
func RemoveProxy(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.Remove(req.Id); err != nil {
return err
}
return c.JSON(nil)
}

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))
}
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 {
do.Where(q.Resource.CreatedAt.Lte(*req.CreateBefore))
do = do.Where(q.Resource.CreatedAt.Lte(req.CreateBefore.UTC()))
}
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 {
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 {
var short = q.ResourceShort.As(q.Resource.Short.Name())
switch *req.Status {
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))
do.Where(q.Resource.Where(timeCond).Or(quotaCond))
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))
do.Where(q.Resource.Where(timeCond).Or(quotaCond))
}
@@ -84,6 +84,7 @@ func PageResourceShort(c *fiber.Ctx) error {
total = int64(len(resource) + req.GetOffset())
} else {
total, err = q.Resource.
Joins(q.Resource.Short).
Where(do).
Count()
if err != nil {
@@ -140,26 +141,26 @@ func PageResourceLong(c *fiber.Ctx) error {
do.Where(q.ResourceLong.As(q.Resource.Long.Name()).Type.Eq(int(*req.Type)))
}
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 {
do.Where(q.Resource.CreatedAt.Lte(*req.CreateBefore))
do = do.Where(q.Resource.CreatedAt.Lte(req.CreateBefore.UTC()))
}
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 {
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 {
var long = q.ResourceLong.As(q.Resource.Long.Name())
switch *req.Status {
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))
do.Where(q.Resource.Where(timeCond).Or(quotaCond))
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))
do.Where(q.Resource.Where(timeCond).Or(quotaCond))
}
@@ -180,6 +181,7 @@ func PageResourceLong(c *fiber.Ctx) error {
total = int64(len(resource) + req.GetOffset())
} else {
total, err = q.Resource.
Joins(q.Resource.Long).
Where(do).
Count()
if err != nil {
@@ -233,18 +235,16 @@ func PageResourceShortByAdmin(c *fiber.Ctx) error {
do = do.Where(q.ResourceShort.As("Short").Type.Eq(int(*req.Mode)))
}
if req.CreatedAtStart != nil {
time := u.DateHead(*req.CreatedAtStart)
do = do.Where(q.Resource.CreatedAt.Gte(time))
do = do.Where(q.Resource.CreatedAt.Gte(req.CreatedAtStart.UTC()))
}
if req.CreatedAtEnd != nil {
time := u.DateTail(*req.CreatedAtEnd)
do = do.Where(q.Resource.CreatedAt.Lte(time))
do = do.Where(q.Resource.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
}
if req.Expired != nil {
if *req.Expired {
do = do.Where(q.Resource.Where(
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(
q.ResourceShort.As("Short").Type.Eq(int(m.ResourceModeQuota)),
q.ResourceShort.As("Short").Quota.LteCol(q.ResourceShort.As("Short").Used),
@@ -252,7 +252,7 @@ func PageResourceShortByAdmin(c *fiber.Ctx) error {
} else {
do = do.Where(q.Resource.Where(
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(
q.ResourceShort.As("Short").Type.Eq(int(m.ResourceModeQuota)),
q.ResourceShort.As("Short").Quota.GtCol(q.ResourceShort.As("Short").Used),
@@ -327,16 +327,16 @@ func PageResourceLongByAdmin(c *fiber.Ctx) error {
do = do.Where(q.ResourceLong.As("Long").Type.Eq(*req.Mode))
}
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 {
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 {
do = do.Where(q.Resource.Where(
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(
q.ResourceLong.As("Long").Type.Eq(int(m.ResourceModeQuota)),
q.ResourceLong.As("Long").Quota.LteCol(q.ResourceLong.As("Long").Used),
@@ -344,7 +344,7 @@ func PageResourceLongByAdmin(c *fiber.Ctx) error {
} else {
do = do.Where(q.Resource.Where(
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(
q.ResourceLong.As("Long").Type.Eq(int(m.ResourceModeQuota)),
q.ResourceLong.As("Long").Quota.GtCol(q.ResourceLong.As("Long").Used),
@@ -416,15 +416,13 @@ func PageResourceShortOfUserByAdmin(c *fiber.Ctx) error {
do = do.Where(q.ResourceShort.As("Short").Type.Eq(int(*req.Mode)))
}
if req.CreatedAtStart != nil {
t := u.DateHead(*req.CreatedAtStart)
do = do.Where(q.Resource.CreatedAt.Gte(t))
do = do.Where(q.Resource.CreatedAt.Gte(req.CreatedAtStart.UTC()))
}
if req.CreatedAtEnd != nil {
t := u.DateTail(*req.CreatedAtEnd)
do = do.Where(q.Resource.CreatedAt.Lte(t))
do = do.Where(q.Resource.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
}
list, total, err := q.Resource.
list, total, err := q.Resource.Debug().
Joins(q.Resource.User, q.Resource.Short, q.Resource.Short.Sku).
Select(
q.Resource.ALL,
@@ -487,12 +485,10 @@ func PageResourceLongOfUserByAdmin(c *fiber.Ctx) error {
do = do.Where(q.ResourceLong.As("Long").Type.Eq(*req.Mode))
}
if req.CreatedAtStart != nil {
t := u.DateHead(*req.CreatedAtStart)
do = do.Where(q.Resource.CreatedAt.Gte(t))
do = do.Where(q.Resource.CreatedAt.Gte(req.CreatedAtStart.UTC()))
}
if req.CreatedAtEnd != nil {
t := u.DateTail(*req.CreatedAtEnd)
do = do.Where(q.Resource.CreatedAt.Lte(t))
do = do.Where(q.Resource.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
}
list, total, err := q.Resource.
@@ -552,6 +548,8 @@ func AllActiveResource(c *fiber.Ctx) error {
Joins(
q.Resource.Short,
q.Resource.Long,
q.Resource.Short.Sku,
q.Resource.Long.Sku,
).
Where(
q.Resource.UserID.Eq(authCtx.User.ID),
@@ -560,9 +558,9 @@ func AllActiveResource(c *fiber.Ctx) error {
q.Resource.Type.Eq(int(m.ResourceTypeShort)),
q.ResourceShort.As(q.Resource.Short.Name()).Where(
short.Type.Eq(int(m.ResourceModeTime)),
short.ExpireAt.Gte(now),
short.ExpireAt.Gte(now.UTC()),
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.Type.Eq(int(m.ResourceModeQuota)),
@@ -572,9 +570,9 @@ func AllActiveResource(c *fiber.Ctx) error {
q.Resource.Type.Eq(int(m.ResourceTypeLong)),
q.ResourceLong.As(q.Resource.Long.Name()).Where(
long.Type.Eq(int(m.ResourceModeTime)),
long.ExpireAt.Gte(now),
long.ExpireAt.Gte(now.UTC()),
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.Type.Eq(int(m.ResourceModeQuota)),
@@ -588,6 +586,15 @@ func AllActiveResource(c *fiber.Ctx) error {
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)
}
@@ -609,6 +616,30 @@ func UpdateResourceByAdmin(c *fiber.Ctx) error {
return c.JSON(nil)
}
func UpdateResourceCheckIP(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitUser()
if err != nil {
return err
}
var req struct {
core.IdReq
CheckIP bool `json:"checkip"`
}
if err := c.BodyParser(&req); err != nil {
return err
}
if err := s.Resource.Update(&s.UpdateResourceData{
IdReq: req.IdReq,
CheckIP: &req.CheckIP,
}); err != nil {
return err
}
return c.JSON(nil)
}
// StatisticResourceFree 统计每日可用
func StatisticResourceFree(c *fiber.Ctx) error {
// 检查权限
@@ -729,10 +760,10 @@ func StatisticResourceUsage(c *fiber.Ctx) error {
)
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 {
do.Where(q.LogsUserUsage.Time.Lte(*req.TimeBefore))
do = do.Where(q.LogsUserUsage.Time.Lte(req.TimeBefore.UTC()))
}
var data = new(StatisticResourceUsageResp)

View File

@@ -5,7 +5,6 @@ import (
"fmt"
"log/slog"
"platform/pkg/env"
"platform/pkg/u"
"platform/web/auth"
"platform/web/core"
g "platform/web/globals"
@@ -53,12 +52,10 @@ func PageTradeByAdmin(c *fiber.Ctx) error {
do = do.Where(q.Trade.Status.Eq(*req.Status))
}
if req.CreatedAtStart != nil {
time := u.DateHead(*req.CreatedAtStart)
do = do.Where(q.Trade.CreatedAt.Gte(time))
do = do.Where(q.Trade.CreatedAt.Gte(req.CreatedAtStart.UTC()))
}
if req.CreatedAtEnd != nil {
time := u.DateTail(*req.CreatedAtEnd)
do = do.Where(q.Trade.CreatedAt.Lte(time))
do = do.Where(q.Trade.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
}
// 查询用户列表
@@ -129,12 +126,10 @@ func PageTradeOfUserByAdmin(c *fiber.Ctx) error {
do = do.Where(q.Trade.Status.Eq(*req.Status))
}
if req.CreatedAtStart != nil {
time := u.DateHead(*req.CreatedAtStart)
do = do.Where(q.Trade.CreatedAt.Gte(time))
do = do.Where(q.Trade.CreatedAt.Gte(req.CreatedAtStart.UTC()))
}
if req.CreatedAtEnd != nil {
time := u.DateTail(*req.CreatedAtEnd)
do = do.Where(q.Trade.CreatedAt.Lte(time))
do = do.Where(q.Trade.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
}
// 查询订单列表
@@ -182,6 +177,9 @@ func TradeCreate(c *fiber.Ctx) error {
if err != nil {
return err
}
if authCtx.User.IDType == m.UserIDTypeUnverified {
return core.NewBizErr("请先实名认证后再购买")
}
// 解析请求参数
req := new(TradeCreateReq)

View File

@@ -8,6 +8,7 @@ import (
m "platform/web/models"
q "platform/web/queries"
s "platform/web/services"
"time"
"github.com/gofiber/fiber/v2"
"github.com/shopspring/decimal"
@@ -65,6 +66,12 @@ func PageUserByAdmin(c *fiber.Ctx) error {
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.
@@ -102,11 +109,13 @@ func PageUserByAdmin(c *fiber.Ctx) error {
type PageUserByAdminReq struct {
core.PageReq
Account *string `json:"account,omitempty"`
Name *string `json:"name,omitempty"`
Identified *bool `json:"identified,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
Assigned *bool `json:"assigned,omitempty"`
Account *string `json:"account,omitempty"`
Name *string `json:"name,omitempty"`
Identified *bool `json:"identified,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
Assigned *bool `json:"assigned,omitempty"`
CreatedAtStart *time.Time `json:"created_at_start,omitempty"`
CreatedAtEnd *time.Time `json:"created_at_end,omitempty"`
}
// 管理员获取单个用户
@@ -260,7 +269,7 @@ type UpdateUserBalanceByAdminData struct {
// 绑定管理员
func BindAdmin(c *fiber.Ctx) error {
// 检查权限
authCtx, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeUserWrite)
authCtx, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeUserWriteBind)
if err != nil {
return err
}
@@ -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.AdminID.IsNull(),
).UpdateColumnSimple(
@@ -283,7 +292,7 @@ func BindAdmin(c *fiber.Ctx) error {
if err != nil {
return err
}
if result.RowsAffected == 0 {
if r.RowsAffected == 0 {
return core.NewBizErr("用户已绑定管理员")
}
@@ -323,7 +332,7 @@ func UpdateUser(c *fiber.Ctx) error {
if req.ContactWechat != nil {
do = append(do, q.User.ContactWechat.Value(*req.ContactWechat))
}
_, err = q.User.
r, err := q.User.
Where(q.User.ID.Eq(authCtx.User.ID)).
UpdateSimple(do...)
if errors.Is(err, gorm.ErrDuplicatedKey) {
@@ -332,6 +341,9 @@ func UpdateUser(c *fiber.Ctx) error {
if err != nil {
return err
}
if r.RowsAffected == 0 {
return core.NewBizErr("用户状态已过期")
}
// 返回结果
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)).
Updates(m.User{
Username: &req.Username,
@@ -368,6 +380,9 @@ func UpdateAccount(c *fiber.Ctx) error {
if err != nil {
return err
}
if r.RowsAffected == 0 {
return core.NewBizErr("用户状态已过期")
}
// 返回结果
return c.SendStatus(fiber.StatusNoContent)
@@ -410,12 +425,15 @@ func UpdatePassword(c *fiber.Ctx) error {
return err
}
_, err = q.User.
r, err := q.User.
Where(q.User.ID.Eq(authCtx.User.ID)).
UpdateColumn(q.User.Password, newHash)
if err != nil {
return err
}
if r.RowsAffected == 0 {
return core.NewBizErr("用户状态已过期")
}
// 返回结果
return c.SendStatus(fiber.StatusNoContent)

View File

@@ -2,6 +2,7 @@ package handlers
import (
"errors"
"fmt"
"platform/pkg/env"
"platform/pkg/u"
"platform/web/auth"
@@ -97,13 +98,31 @@ func CreateWhitelist(c *fiber.Ctx) error {
}
// 创建白名单
err = q.Whitelist.Create(&m.Whitelist{
UserID: authCtx.User.ID,
IP: u.Z(ip),
Remark: &req.Remark,
uid := authCtx.User.ID
err = g.Redsync.WithLock(whitelistKey(uid), func() error {
count, err := q.Whitelist.Where(
q.Whitelist.UserID.Eq(uid),
).Count()
if err != nil {
return core.NewServErr("获取白名单数量失败", err)
}
if count >= 5 {
return core.NewBizErr("白名单数量已达上限")
}
err = q.Whitelist.Create(&m.Whitelist{
UserID: authCtx.User.ID,
IP: u.Z(ip),
Remark: &req.Remark,
})
if err != nil {
return core.NewServErr("添加白名单失败", err)
}
return nil
})
if err != nil {
return core.NewServErr("添加白名单失败", err)
return err
}
return nil
@@ -137,7 +156,7 @@ func UpdateWhitelist(c *fiber.Ctx) error {
}
// 更新白名单
_, err = q.Whitelist.
r, err := q.Whitelist.
Where(
q.Whitelist.ID.Eq(req.ID),
q.Whitelist.UserID.Eq(authCtx.User.ID),
@@ -149,6 +168,9 @@ func UpdateWhitelist(c *fiber.Ctx) error {
if err != nil {
return err
}
if r.RowsAffected == 0 {
return core.NewBizErr("白名单状态已过期")
}
return nil
}
@@ -182,7 +204,7 @@ func RemoveWhitelist(c *fiber.Ctx) error {
}
// 删除白名单
_, err = q.Whitelist.
r, err := q.Whitelist.
Where(
q.Whitelist.ID.In(ids...),
q.Whitelist.UserID.Eq(authCtx.User.ID),
@@ -193,6 +215,9 @@ func RemoveWhitelist(c *fiber.Ctx) error {
if err != nil {
return err
}
if r.RowsAffected == 0 {
return core.NewBizErr("白名单状态已过期")
}
return nil
}
@@ -206,3 +231,7 @@ func secureAddr(str string) (*orm.Inet, error) {
}
return ip, nil
}
func whitelistKey(userID int32) string {
return fmt.Sprintf("platform:whitelist:add:%d", userID)
}

View File

@@ -1,16 +1,21 @@
package web
import (
"net/http"
"platform/pkg/env"
"platform/web/auth"
"github.com/gofiber/contrib/otelfiber/v2"
"github.com/gofiber/fiber/v2"
"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/recover"
"github.com/gofiber/fiber/v2/middleware/requestid"
"github.com/google/uuid"
"github.com/jxskiss/base62"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
func ApplyMiddlewares(app *fiber.App) {
@@ -20,13 +25,8 @@ func ApplyMiddlewares(app *fiber.App) {
EnableStackTrace: true,
}))
// cors
app.Use(cors.New(cors.Config{
AllowCredentials: true,
AllowOriginsFunc: func(origin string) bool {
return true
},
}))
// metric
app.Use(otelfiber.Middleware())
// logger
app.Use(logger.New(logger.Config{
@@ -35,8 +35,31 @@ func ApplyMiddlewares(app *fiber.App) {
},
}))
// metric
app.Use(otelfiber.Middleware())
// 补充 otel span attr
app.Use(func(c *fiber.Ctx) error {
err := c.Next()
span := trace.SpanFromContext(c.UserContext())
if !span.IsRecording() {
return err
}
str := ""
if err != nil {
str = err.Error()
}
span.SetAttributes(attribute.String("http.response.error", str))
return err
})
// cors
app.Use(cors.New(cors.Config{
AllowCredentials: true,
AllowOriginsFunc: func(origin string) bool {
return true
},
}))
// request id
app.Use(requestid.New(requestid.Config{
@@ -46,6 +69,11 @@ func ApplyMiddlewares(app *fiber.App) {
},
}))
// static uploads
app.Use("/uploads", filesystem.New(filesystem.Config{
Root: http.Dir(env.UploadDir),
}))
// 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

@@ -4,13 +4,13 @@ import "time"
// CouponUser 优惠券发放表
type CouponUser struct {
ID int32 `json:"id" gorm:"column:id;primaryKey"` // 记录ID
CouponID int32 `json:"coupon_id" gorm:"column:coupon_id"` // 优惠券ID
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
Status CouponStatus `json:"status" gorm:"column:status"` // 使用状态0-未使用1-已使用
ExpireAt *time.Time `json:"expire_at,omitempty" gorm:"column:expire_at"` // 过期时间
UsedAt *time.Time `json:"used_at,omitempty" gorm:"column:used_at"` // 使用时间
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"` // 创建时间
ID int32 `json:"id" gorm:"column:id;primaryKey"` // 记录ID
CouponID int32 `json:"coupon_id" gorm:"column:coupon_id"` // 优惠券ID
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
Status CouponUserStatus `json:"status" gorm:"column:status"` // 使用状态0-未使用1-已使用2-已禁用
ExpireAt *time.Time `json:"expire_at,omitempty" gorm:"column:expire_at"` // 过期时间
UsedAt *time.Time `json:"used_at,omitempty" gorm:"column:used_at"` // 使用时间
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"` // 创建时间
Coupon *Coupon `json:"coupon,omitempty" gorm:"foreignKey:CouponID"`
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
@@ -20,6 +20,7 @@ type CouponUser struct {
type CouponUserStatus int
const (
CouponUserStatusUnused CouponUserStatus = 0 // 未使用
CouponUserStatusUsed CouponUserStatus = 1 // 已使用
CouponUserStatusUnused CouponUserStatus = 0 // 未使用
CouponUserStatusUsed CouponUserStatus = 1 // 已使用
CouponUserStatusDisabled CouponUserStatus = 2 // 已禁用
)

View File

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

View File

@@ -17,6 +17,7 @@ type ProductSku struct {
PriceMin decimal.Decimal `json:"price_min" gorm:"column:price_min"` // 最低价格
Status SkuStatus `json:"status" gorm:"column:status"` // SKU 状态0-禁用1-正常
Sort int32 `json:"sort" gorm:"column:sort"` // 排序
CountMin int32 `json:"count_min" gorm:"column:count_min"` // 最小购买数量
Product *Product `json:"product,omitempty" gorm:"foreignKey:ProductID"`
Discount *ProductDiscount `json:"discount,omitempty" gorm:"foreignKey:DiscountId"`

View File

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

View File

@@ -12,6 +12,7 @@ type Resource struct {
Active bool `json:"active" gorm:"column:active"` // 套餐状态
Type ResourceType `json:"type" gorm:"column:type"` // 套餐类型1-短效动态2-长效动态
Code string `json:"code" gorm:"column:code"` // 产品编码
CheckIP bool `json:"checkip" gorm:"column:checkip"` // 是否检查IP
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
Short *ResourceShort `json:"short,omitempty" gorm:"foreignKey:ResourceID"`

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 {
field.RelationField
Area struct {
field.RelationField
Parent struct {
field.RelationField
}
}
}
}{
RelationField: field.NewRelation("Proxy.Channels", "models.Channel"),
@@ -238,8 +244,27 @@ func newChannel(db *gorm.DB, opts ...gen.DOOption) channel {
},
Edge: struct {
field.RelationField
Area struct {
field.RelationField
Parent struct {
field.RelationField
}
}
}{
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 {
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.Mac = field.NewString(tableName, "mac")
_edge.IP = field.NewField(tableName, "ip")
_edge.Port = field.NewUint16(tableName, "port")
_edge.ISP = field.NewInt(tableName, "isp")
_edge.Prov = field.NewString(tableName, "prov")
_edge.City = field.NewString(tableName, "city")
_edge.AreaID = field.NewInt32(tableName, "area_id")
_edge.Status = field.NewInt(tableName, "status")
_edge.RTT = field.NewInt32(tableName, "rtt")
_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()
@@ -59,12 +69,13 @@ type edge struct {
Version field.Int32
Mac field.String
IP field.Field
Port field.Uint16
ISP field.Int
Prov field.String
City field.String
AreaID field.Int32
Status field.Int
RTT field.Int32
Loss field.Int32
Area edgeBelongsToArea
fieldMap map[string]field.Expr
}
@@ -89,9 +100,9 @@ func (e *edge) updateTableName(table string) *edge {
e.Version = field.NewInt32(table, "version")
e.Mac = field.NewString(table, "mac")
e.IP = field.NewField(table, "ip")
e.Port = field.NewUint16(table, "port")
e.ISP = field.NewInt(table, "isp")
e.Prov = field.NewString(table, "prov")
e.City = field.NewString(table, "city")
e.AreaID = field.NewInt32(table, "area_id")
e.Status = field.NewInt(table, "status")
e.RTT = field.NewInt32(table, "rtt")
e.Loss = field.NewInt32(table, "loss")
@@ -111,7 +122,7 @@ func (e *edge) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
}
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["created_at"] = e.CreatedAt
e.fieldMap["updated_at"] = e.UpdatedAt
@@ -120,24 +131,113 @@ func (e *edge) fillFieldMap() {
e.fieldMap["version"] = e.Version
e.fieldMap["mac"] = e.Mac
e.fieldMap["ip"] = e.IP
e.fieldMap["port"] = e.Port
e.fieldMap["isp"] = e.ISP
e.fieldMap["prov"] = e.Prov
e.fieldMap["city"] = e.City
e.fieldMap["area_id"] = e.AreaID
e.fieldMap["status"] = e.Status
e.fieldMap["rtt"] = e.RTT
e.fieldMap["loss"] = e.Loss
}
func (e edge) clone(db *gorm.DB) edge {
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
}
func (e edge) replaceDB(db *gorm.DB) edge {
e.edgeDo.ReplaceDB(db)
e.Area.db = db.Session(&gorm.Session{})
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 }
func (e edgeDo) Debug() *edgeDo {

View File

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

View File

@@ -39,6 +39,7 @@ func newProductSku(db *gorm.DB, opts ...gen.DOOption) productSku {
_productSku.PriceMin = field.NewField(tableName, "price_min")
_productSku.Status = field.NewInt32(tableName, "status")
_productSku.Sort = field.NewInt32(tableName, "sort")
_productSku.CountMin = field.NewInt32(tableName, "count_min")
_productSku.Product = productSkuBelongsToProduct{
db: db.Session(&gorm.Session{}),
@@ -93,6 +94,7 @@ type productSku struct {
PriceMin field.Field
Status field.Int32
Sort field.Int32
CountMin field.Int32
Product productSkuBelongsToProduct
Discount productSkuBelongsToDiscount
@@ -124,6 +126,7 @@ func (p *productSku) updateTableName(table string) *productSku {
p.PriceMin = field.NewField(table, "price_min")
p.Status = field.NewInt32(table, "status")
p.Sort = field.NewInt32(table, "sort")
p.CountMin = field.NewInt32(table, "count_min")
p.fillFieldMap()
@@ -140,7 +143,7 @@ func (p *productSku) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
}
func (p *productSku) fillFieldMap() {
p.fieldMap = make(map[string]field.Expr, 14)
p.fieldMap = make(map[string]field.Expr, 15)
p.fieldMap["id"] = p.ID
p.fieldMap["created_at"] = p.CreatedAt
p.fieldMap["updated_at"] = p.UpdatedAt
@@ -153,6 +156,7 @@ func (p *productSku) fillFieldMap() {
p.fieldMap["price_min"] = p.PriceMin
p.fieldMap["status"] = p.Status
p.fieldMap["sort"] = p.Sort
p.fieldMap["count_min"] = p.CountMin
}

View File

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

View File

@@ -36,6 +36,7 @@ func newResource(db *gorm.DB, opts ...gen.DOOption) resource {
_resource.Active = field.NewBool(tableName, "active")
_resource.Type = field.NewInt(tableName, "type")
_resource.Code = field.NewString(tableName, "code")
_resource.CheckIP = field.NewBool(tableName, "checkip")
_resource.Short = resourceHasOneShort{
db: db.Session(&gorm.Session{}),
@@ -185,6 +186,7 @@ type resource struct {
Active field.Bool
Type field.Int
Code field.String
CheckIP field.Bool
Short resourceHasOneShort
Long resourceHasOneLong
@@ -217,6 +219,7 @@ func (r *resource) updateTableName(table string) *resource {
r.Active = field.NewBool(table, "active")
r.Type = field.NewInt(table, "type")
r.Code = field.NewString(table, "code")
r.CheckIP = field.NewBool(table, "checkip")
r.fillFieldMap()
@@ -233,7 +236,7 @@ func (r *resource) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
}
func (r *resource) fillFieldMap() {
r.fieldMap = make(map[string]field.Expr, 13)
r.fieldMap = make(map[string]field.Expr, 14)
r.fieldMap["id"] = r.ID
r.fieldMap["created_at"] = r.CreatedAt
r.fieldMap["updated_at"] = r.UpdatedAt
@@ -243,6 +246,7 @@ func (r *resource) fillFieldMap() {
r.fieldMap["active"] = r.Active
r.fieldMap["type"] = r.Type
r.fieldMap["code"] = r.Code
r.fieldMap["checkip"] = r.CheckIP
}

View File

@@ -3,6 +3,8 @@ package web
import (
"platform/pkg/env"
auth2 "platform/web/auth"
"platform/web/core"
"platform/web/globals"
"platform/web/handlers"
"time"
@@ -13,9 +15,10 @@ import (
func ApplyRouters(app *fiber.App) {
api := app.Group("/api")
publicRouter(api)
clientRouter(api)
userRouter(api)
adminRouter(api)
clientRouter(api)
// 回调
callbacks := app.Group("/callback")
@@ -25,20 +28,32 @@ func ApplyRouters(app *fiber.App) {
if env.RunMode == env.RunModeDev {
debug := app.Group("/debug")
debug.Get("/sms/:phone", handlers.DebugGetSmsCode)
debug.Get("/proxy/register", handlers.DebugRegisterProxyBaiYin)
debug.Get("/iden/clear/:phone", handlers.DebugIdentifyClear)
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 {
return err
}
return ctx.JSON(rs)
})
debug.Get("/test/err", func(ctx *fiber.Ctx) error {
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.Get("/authorize", auth2.AuthorizeGet)
@@ -47,6 +62,43 @@ func userRouter(api fiber.Router) {
auth.Post("/revoke", auth2.Revoke)
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.Post("/update", handlers.UpdateUser)
@@ -67,6 +119,7 @@ func userRouter(api fiber.Router) {
resource.Post("/list/short", handlers.PageResourceShort)
resource.Post("/list/long", handlers.PageResourceLong)
resource.Post("/create", handlers.CreateResource)
resource.Post("/update/checkip", handlers.UpdateResourceCheckIP)
resource.Post("/statistics/free", handlers.StatisticResourceFree)
resource.Post("/statistics/usage", handlers.StatisticResourceUsage)
@@ -79,66 +132,41 @@ func userRouter(api fiber.Router) {
channel := api.Group("/channel")
channel.Post("/list", handlers.ListChannel)
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.Post("/create", handlers.TradeCreate)
trade.Post("/complete", handlers.TradeComplete)
trade.Post("/cancel", handlers.TradeCancel)
trade.Get("/check", handlers.TradeCheck)
// 账单
bill := api.Group("/bill")
bill.Post("/list", handlers.ListBill)
// 余额变动
balance := api.Group("/balance")
balance.Post("/page", handlers.PageBalanceActivity)
// 已发放优惠券
couponUser := api.Group("/coupon-user")
couponUser.Post("/page", handlers.PageCouponUser)
couponUser.Post("/get", handlers.GetCouponUser)
// 公告
announcement := api.Group("/announcement")
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.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)
// 代理网关注册
proxy := client.Group("/proxy")
proxy.Post("/register/baidyin", handlers.ProxyRegisterBaiYin)
}
// 管理员接口路由
func adminRouter(api fiber.Router) {
api = api.Group("/admin")
@@ -195,6 +223,18 @@ func adminRouter(api fiber.Router) {
var channel = api.Group("/channel")
channel.Post("/page", handlers.PageChannelByAdmin)
channel.Post("/page/of-user", handlers.PageChannelOfUserByAdmin)
channel.Post("/sync/clear-expired", handlers.SyncChannelClearExpiredByAdmin)
// proxy 代理
var proxy = api.Group("/proxy")
proxy.Post("/all", handlers.AllProxyByAdmin)
proxy.Post("/page", handlers.PageProxyByAdmin)
proxy.Post("/create", handlers.CreateProxy)
proxy.Post("/update", handlers.UpdateProxy)
proxy.Post("/update/status", handlers.UpdateProxyStatus)
proxy.Post("/sync/ports", handlers.SyncProxyPorts)
proxy.Post("/sync/chains", handlers.SyncProxyChains)
proxy.Post("/remove", handlers.RemoveProxy)
// trade 交易
var trade = api.Group("/trade")
@@ -243,4 +283,31 @@ func adminRouter(api fiber.Router) {
coupon.Post("/create", handlers.CreateCoupon)
coupon.Post("/update", handlers.UpdateCoupon)
coupon.Post("/remove", handlers.DeleteCoupon)
coupon.Post("/update/assign", handlers.AssignCoupon)
// coupon-user 已发放优惠券
var couponUser = api.Group("/coupon-user")
couponUser.Post("/page", handlers.PageCouponUserByAdmin)
couponUser.Post("/page/of-user", handlers.PageCouponUserOfUserByAdmin)
couponUser.Post("/get", handlers.GetCouponUserByAdmin)
couponUser.Post("/create", handlers.CreateCouponUserByAdmin)
couponUser.Post("/update", handlers.UpdateCouponUserByAdmin)
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 {
// 更新管理员基本信息
if len(simples) > 0 {
_, err := q.Admin.
r, err := q.Admin.
Where(
q.Admin.ID.Eq(update.Id),
q.Admin.Lock.Is(false),
@@ -119,6 +119,9 @@ func (s *adminService) Update(update *UpdateAdmin) error {
if err != nil {
return err
}
if r.RowsAffected == 0 {
return core.NewBizErr("管理员状态已过期")
}
}
// 更新角色关联
@@ -157,11 +160,17 @@ type UpdateAdmin struct {
}
func (s *adminService) Remove(id int32) error {
_, err := q.Admin.
r, err := q.Admin.
Where(
q.Admin.ID.Eq(id),
q.Admin.Lock.Is(false),
).
UpdateColumn(q.Admin.DeletedAt, time.Now())
return err
if err != nil {
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 {
_, err := q.AdminRole.Where(q.AdminRole.ID.Eq(id)).UpdateColumn(q.AdminRole.DeletedAt, time.Now())
return err
rs, err := q.AdminRole.Where(q.AdminRole.ID.Eq(id)).UpdateColumn(q.AdminRole.DeletedAt, time.Now())
if err != nil {
return err
}
if rs.RowsAffected == 0 {
return core.NewBizErr("管理员角色状态已过期")
}
return nil
}
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,40 +4,447 @@ import (
"context"
"errors"
"fmt"
"log/slog"
"math/rand/v2"
"net/netip"
"platform/pkg/u"
"platform/web/core"
e "platform/web/events"
g "platform/web/globals"
"platform/web/globals/orm"
m "platform/web/models"
q "platform/web/queries"
"strconv"
"strings"
"time"
"github.com/hibiken/asynq"
"github.com/redis/go-redis/v9"
"gorm.io/gen"
"gorm.io/gen/field"
)
// 通道服务
var Channel = &channelServer{
provider: &channelBaiyinProvider{},
provider: &channelGostProvider{},
}
type ChannelServiceProvider interface {
CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter ...EdgeFilter) ([]*m.Channel, error)
RemoveChannels(batch string) error
type channelProvider interface {
selectProxy(count int) (*m.Proxy, error)
prepareCreate(ctx *channelCreateContext) (*channelCreateResult, error)
removeRemote(batchNo string, batch *usedChanBatch) error
}
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) {
return s.provider.CreateChannels(source, resourceId, authWhitelist, authPassword, count, edgeFilter...)
func (s *channelServer) CreateChannels(source netip.Addr, resourceNo string, authWhitelist bool, authPassword bool, count int, edgeFilter *EdgeFilter) ([]*m.Channel, error) {
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 {
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) {
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
}
// 授权方式
@@ -67,12 +474,23 @@ func genPassPair() (string, string) {
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.
Preload(field.Associations).
Where(
q.Resource.ID.Eq(resourceId),
q.Resource.ResourceNo.Eq(resourceNo),
q.Resource.Active.Is(true),
).
Take()
@@ -83,10 +501,11 @@ func findResource(resourceId int32, now time.Time) (*ResourceView, error) {
return nil, ErrResourceNotExist
}
var info = &ResourceView{
Id: resource.ID,
User: *resource.User,
Active: resource.Active,
Type: resource.Type,
ID: resource.ID,
User: *resource.User,
Active: resource.Active,
Type: resource.Type,
CheckIP: resource.CheckIP,
}
switch resource.Type {
@@ -108,7 +527,7 @@ func findResource(resourceId int32, now time.Time) (*ResourceView, error) {
var sub = resource.Long
info.LongId = &sub.ID
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.Quota = sub.Quota
info.Used = sub.Used
@@ -128,7 +547,7 @@ func findResource(resourceId int32, now time.Time) (*ResourceView, error) {
// ResourceView 套餐数据的简化视图,便于直接获取主要数据
type ResourceView struct {
Id int32
ID int32
User m.User
Active bool
Type m.ResourceType
@@ -142,16 +561,17 @@ type ResourceView struct {
Daily int32
LastAt *time.Time
Today int // 今日用量
CheckIP bool
}
// 检查用户是否可提取
func ensure(now time.Time, source netip.Addr, resourceId int32, 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 {
return nil, nil, core.NewBizErr("单次最多提取 400 个")
}
// 获取用户套餐
resource, err := findResource(resourceId, now)
resource, err := findResourceViewByNo(resourceNo, now)
if err != nil {
return nil, nil, err
}
@@ -170,6 +590,10 @@ func ensure(now time.Time, source netip.Addr, resourceId int32, count int) (*Res
return nil, nil, err
}
if authWhitelist && len(whitelists) == 0 {
return nil, nil, core.NewBizErr("当前白名单为空,请先添加白名单")
}
ips := make([]string, len(whitelists))
pass := false
for i, item := range whitelists {
@@ -178,7 +602,7 @@ func ensure(now time.Time, source netip.Addr, resourceId int32, count int) (*Res
pass = true
}
}
if !pass {
if resource.CheckIP && !pass {
return nil, nil, core.NewBizErr(fmt.Sprintf("IP 地址 %s 不在白名单内", source.String()))
}
@@ -209,10 +633,98 @@ func ensure(now time.Time, source netip.Addr, resourceId int32, count int) (*Res
return resource, ips, nil
}
var (
freeChansKey = "channel:free"
usedChansKey = "channel:used"
)
func freeChansKey(proxy int32) string {
return "channel:free:" + strconv.Itoa(int(proxy))
}
func usedChansKey(proxy int32, batch string) string {
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 {
@@ -221,7 +733,7 @@ func regChans(proxy int32, chans []netip.AddrPort) error {
strs[i] = ch.String()
}
key := freeChansKey + ":" + strconv.Itoa(int(proxy))
key := freeChansKey(proxy)
err := g.Redis.SAdd(context.Background(), key, strs...).Err()
if err != nil {
return fmt.Errorf("扩容通道失败: %w", err)
@@ -231,8 +743,8 @@ func regChans(proxy int32, chans []netip.AddrPort) error {
// 缩容通道
func remChans(proxy int32) error {
key := freeChansKey + ":" + strconv.Itoa(int(proxy))
err := g.Redis.SRem(context.Background(), key).Err()
key := freeChansKey(proxy)
err := g.Redis.Del(context.Background(), key).Err()
if err != nil {
return fmt.Errorf("缩容通道失败: %w", err)
}
@@ -241,13 +753,12 @@ func remChans(proxy int32) error {
// 取用通道
func lockChans(proxy int32, batch string, count int) ([]netip.AddrPort, error) {
pid := strconv.Itoa(int(proxy))
chans, err := RedisScriptLockChans.Run(
context.Background(),
g.Redis,
[]string{
freeChansKey + ":" + pid,
usedChansKey + ":" + pid + ":" + batch,
freeChansKey(proxy),
usedChansKey(proxy, batch),
},
count,
).StringSlice()
@@ -268,11 +779,12 @@ func lockChans(proxy int32, batch string, count int) ([]netip.AddrPort, error) {
}
var RedisScriptLockChans = redis.NewScript(`
local free_key = KEYS[1]
local free_key = KEYS[1]
local batch_key = KEYS[2]
local count = tonumber(ARGV[1])
if redis.call("SCARD", free_key) < count then
local free_count = redis.call("SCARD", free_key)
if count <= 0 or free_count < count then
return nil
end
@@ -284,13 +796,12 @@ return ports
// 归还通道
func freeChans(proxy int32, batch string) error {
pid := strconv.Itoa(int(proxy))
err := RedisScriptFreeChans.Run(
context.Background(),
g.Redis,
[]string{
freeChansKey + ":" + pid,
usedChansKey + ":" + pid + ":" + batch,
freeChansKey(proxy),
usedChansKey(proxy, batch),
},
).Err()
if err != nil {
@@ -301,25 +812,38 @@ func freeChans(proxy int32, batch string) error {
}
var RedisScriptFreeChans = redis.NewScript(`
local free_key = KEYS[1]
local free_key = KEYS[1]
local batch_key = KEYS[2]
local chans = redis.call("LRANGE", batch_key, 0, -1)
redis.call("DEL", batch_key)
if redis.call("EXISTS", free_key) == 1 then
redis.call("SADD", free_key, unpack(chans))
if #chans == 0 then
return 1
end
redis.call("SADD", free_key, unpack(chans))
redis.call("DEL", batch_key)
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 (
ErrResourceNotExist = core.NewBizErr("套餐不存在")
ErrResourceInvalid = core.NewBizErr("套餐不可用")
ErrResourceExhausted = core.NewBizErr("套餐已用完")
ErrResourceExpired = core.NewBizErr("套餐已过期")
ErrResourceDailyLimit = core.NewBizErr("套餐每日配额已用完")
ErrEdgesNoAvailable = core.NewBizErr("没有可用的节点")
)

View File

@@ -1,326 +1,162 @@
package services
import (
"encoding/json"
"fmt"
"log/slog"
"net/netip"
"platform/pkg/env"
"platform/pkg/u"
"platform/web/core"
e "platform/web/events"
g "platform/web/globals"
"platform/web/globals/orm"
m "platform/web/models"
q "platform/web/queries"
"strings"
"time"
"github.com/hibiken/asynq"
"gorm.io/gen"
"gorm.io/gen/field"
)
type channelBaiyinProvider struct{}
func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter ...EdgeFilter) ([]*m.Channel, error) {
var filter *EdgeFilter = nil
if len(edgeFilter) > 0 {
filter = &edgeFilter[0]
}
now := time.Now()
batch := ID.GenReadable("bat")
// 检查并获取套餐与白名单
resource, whitelists, err := ensure(now, source, resourceId, count)
if err != nil {
return nil, err
}
user := resource.User
expire := now.Add(resource.Live)
// 选择代理
proxyResult := struct {
m.Proxy
Count int
}{}
err = q.Proxy.
LeftJoin(q.Channel, q.Channel.ProxyID.EqCol(q.Proxy.ID), q.Channel.ExpiredAt.Gt(now)).
Select(q.Proxy.ALL, field.NewUnsafeFieldRaw("10000 - count(*)").As("count")).
Where(
q.Proxy.Type.Eq(int(m.ProxyTypeBaiYin)),
q.Proxy.Status.Eq(int(m.ProxyStatusOnline)),
).
Group(q.Proxy.ID).
Order(field.NewField("", "count")).
Limit(1).Scan(&proxyResult)
if err != nil {
return nil, core.NewBizErr("获取可用代理失败", err)
}
if proxyResult.Count < count {
return nil, core.NewBizErr("无可用主机,请稍后再试")
}
proxy := proxyResult.Proxy
// 获取可用通道
chans, err := lockChans(proxy.ID, batch, count)
if err != nil {
return nil, core.NewBizErr("无可用通道,请稍后再试", err)
}
// 获取可用节点
edgesResp, err := g.Cloud.CloudEdges(&g.CloudEdgesReq{
Province: filter.Prov,
City: filter.City,
Isp: u.X(filter.Isp.String()),
Limit: &count,
NoRepeat: u.P(true),
NoDayRepeat: u.P(true),
ActiveTime: u.P(3600),
IpUnchangedTime: u.P(3600),
Sort: u.P("ip_unchanged_time_asc"),
})
if err != nil {
return nil, core.NewBizErr("获取可用节点失败", err)
}
if edgesResp.Total != count && len(edgesResp.Edges) != count {
return nil, core.NewBizErr("地区可用节点数量不足 [%s, %s] [%s]")
}
edges := edgesResp.Edges
// 准备通道数据
channels := make([]*m.Channel, count)
chanConfigs := make([]*g.PortConfigsReq, count)
edgeConfigs := make([]string, count)
for i := range count {
ch := chans[i]
edge := edges[i]
if err != nil {
return nil, core.NewBizErr("解析通道地址失败", err)
}
// 通道数据
channels[i] = &m.Channel{
UserID: user.ID,
ResourceID: resourceId,
BatchNo: batch,
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,
}
// 通道配置数据
chanConfigs[i] = &g.PortConfigsReq{
Port: int(ch.Port()),
Status: true,
Edge: &[]string{edge.EdgeID},
}
// 白名单模式
if authWhitelist {
channels[i].Whitelists = u.P(strings.Join(whitelists, ","))
chanConfigs[i].Whitelist = &whitelists
}
// 密码模式
if authPassword {
username, password := genPassPair()
channels[i].Username = &username
channels[i].Password = &password
chanConfigs[i].Userpass = u.P(username + ":" + password)
}
// 连接配置数据
edgeConfigs[i] = edge.EdgeID
}
// 提交异步任务关闭通道
_, err = g.Asynq.Enqueue(
e.NewRemoveChannel(batch),
asynq.ProcessAt(expire),
)
if err != nil {
return nil, core.NewServErr("提交关闭通道任务失败", err)
}
// 保存数据
err = q.Q.Transaction(func(q *q.Query) error {
var rs gen.ResultInfo
// 根据套餐类型和模式更新使用记录
isShortType := resource.Type == m.ResourceTypeShort
isLongType := resource.Type == m.ResourceTypeLong
switch {
case isShortType:
rs, 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 isLongType:
rs, 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.NewServErr("套餐类型不正确,无法更新", nil)
}
if err != nil {
return core.NewServErr("更新套餐使用记录失败", err)
}
if rs.RowsAffected == 0 {
return core.NewServErr("套餐使用记录不存在")
}
// 保存通道
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: batch,
Count: int32(count),
ISP: u.P(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
})
if err != nil {
return nil, err
}
// 提交配置
secret := strings.Split(u.Z(proxy.Secret), ":")
gateway := g.NewGateway(proxy.IP.String(), secret[0], secret[1])
if env.RunMode == env.RunModeProd {
// 连接节点到网关
err = g.Cloud.CloudConnect(&g.CloudConnectReq{
Uuid: proxy.Mac,
Edge: &edgeConfigs,
})
if err != nil {
return nil, core.NewServErr("连接云平台失败", err)
}
// 启用网关代理通道
err = gateway.GatewayPortConfigs(chanConfigs)
if err != nil {
return nil, core.NewServErr(fmt.Sprintf("配置代理 %s 端口失败", proxy.IP.String()), err)
}
} else {
slog.Debug("提交代理端口配置", "proxy", proxy.IP.String())
for _, item := range chanConfigs {
str, _ := json.Marshal(item)
fmt.Println(string(str))
}
}
return channels, nil
func (s *channelBaiyinProvider) selectProxy(count int) (*m.Proxy, error) {
return selectProxyByType(m.ProxyTypeBaiYin, count)
}
func (s *channelBaiyinProvider) RemoveChannels(batch string) error {
start := time.Now()
// 获取连接数据
channels, err := q.Channel.Where(q.Channel.BatchNo.Eq(batch)).Find()
func (s *channelBaiyinProvider) prepareCreate(ctx *channelCreateContext) (*channelCreateResult, error) {
gateway, err := proxyGateway(ctx.Proxy)
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
return nil, core.NewServErr("创建代理网关失败", err)
}
prov, city := areaProvinceCity(ctx.Area)
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)
}
// 准备配置数据
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))
channels := make([]*m.Channel, len(ctx.Ports))
chanConfigs := make([]*g.PortConfigsReq, len(ctx.Ports))
for i, portRef := range ctx.Ports {
channel := newBaseChannel(ctx, portRef.Port())
cfg := &g.PortConfigsReq{
Port: int(portRef.Port()),
Status: true,
AutoEdgeConfig: &g.AutoEdgeConfig{
Province: u.Z(prov),
City: u.Z(city),
Isp: ctx.Filter.Isp.String(),
Count: u.P(1),
},
}
if ctx.AuthWhitelist {
cfg.Whitelist = &ctx.Whitelists
}
if username, password, ok := applyChannelAuth(ctx, channel); ok {
cfg.Userpass = u.P(username + ":" + password)
}
channels[i] = channel
chanConfigs[i] = cfg
}
return &channelCreateResult{
Channels: channels,
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 {
if err := gateway.GatewayPortConfigs(chanConfigs); err != nil {
slog.Warn("提交代理端口配置失败", "error", err.Error())
return core.NewServErr(fmt.Sprintf("配置代理 %s 端口失败", ctx.Proxy.IP.String()), err)
}
}
return nil
},
}, nil
}
func (s *channelBaiyinProvider) removeRemote(_ string, batch *usedChanBatch) error {
configs := make([]*g.PortConfigsReq, len(batch.Chans))
for i, ch := range batch.Chans {
configs[i] = &g.PortConfigsReq{
Status: false,
Port: int(channel.Port),
Edge: &[]string{},
Port: int(ch.Port()),
Edge: &[]string{},
AutoEdgeConfig: &g.AutoEdgeConfig{Count: u.P(0)},
Status: false,
}
}
// 提交配置
if env.RunMode == env.RunModeProd {
// 断开节点连接
g.Cloud.CloudDisconnect(&g.CloudDisconnectReq{
Uuid: proxy.Mac,
Edge: &edgeConfigs,
})
// 清空通道配置
secret := strings.Split(*proxy.Secret, ":")
gateway := g.NewGateway(proxy.IP.String(), secret[0], secret[1])
err := gateway.GatewayPortConfigs(configs)
if err != nil {
return core.NewServErr(fmt.Sprintf("清空代理 %s 端口配置失败", proxy.IP.String()), err)
}
} else {
slog.Debug("清除代理端口配置", "proxy", proxy.IP)
for _, item := range configs {
str, _ := json.Marshal(item)
fmt.Println(string(str))
}
}
// 释放端口
err = freeChans(proxy.ID, batch)
proxy, err := q.Proxy.Where(q.Proxy.ID.Eq(batch.ProxyID)).Take()
if err != nil {
return err
return core.NewServErr("获取代理数据失败", err)
}
slog.Debug("清除代理端口配置", "time", time.Since(start).String())
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 nil
}
// ensureEdges 检查本地节点是否足够,如果不足从云端连入
// 本地节点通过 Assigned = false 排除已分配节点
// 云端节点通过 NoRepeat = true 排除已分配节点
func ensureEdges(proxy *m.Proxy, gateway g.GatewayClient, area *m.Area, isp *m.EdgeISP, count int) error {
prov, city := areaProvinceCity(area)
if prov == nil && city == nil && u.X(isp.String()) == nil {
return nil // 没有过滤条件,直接返回空,避免无意义的查询
}
// 先查本地
localEdges, err := gateway.GatewayEdge(&g.GatewayEdgeReq{
Province: prov,
City: city,
Isp: u.X(isp.String()),
Limit: &count,
Assigned: u.P(false),
})
if err != nil {
return core.NewBizErr("检查可用节点失败[1]", err)
}
if len(localEdges) >= count {
return nil // 本地节点足够,直接返回空,后续逻辑会优先使用本地节点
}
// 再查云端
remaining := count - len(localEdges)
cloudEdges, err := g.Cloud.CloudEdges(&g.CloudEdgesReq{
Province: prov,
City: city,
Isp: u.X(isp.String()),
Limit: &remaining,
NoRepeat: u.P(true),
ActiveTime: u.P(3600),
IpUnchangedTime: u.P(3600),
})
if err != nil {
return core.NewBizErr("检查可用节点失败[2]", err)
}
if len(cloudEdges.Edges) < remaining {
return core.NewBizErr("地区可用节点数量不足")
}
// 连入云端节点
edges := make([]string, remaining)
for i, edge := range cloudEdges.Edges {
edges[i] = edge.EdgeID
}
if err := g.Cloud.CloudConnect(&g.CloudConnectReq{Uuid: proxy.Mac, Edge: &edges}); err != nil {
return core.NewServErr("连接云平台失败", err)
}
return nil
}
type EdgeInfo struct {
Type EdgeInfoType
EdgeID string
}
type EdgeInfoType string
const (
EdgeInfoLocal EdgeInfoType = "local"
EdgeInfoCloud EdgeInfoType = "cloud"
)

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

@@ -68,17 +68,33 @@ func (s *couponService) Update(data UpdateCouponData) error {
do = append(do, q.Coupon.Status.Value(int(*data.Status)))
}
if data.ExpireType != nil {
switch *data.ExpireType {
case m.CouponExpireTypeNever:
do = append(do, q.Coupon.ExpireAt.Null(), q.Coupon.ExpireIn.Null())
case m.CouponExpireTypeFixed:
if data.ExpireAt == nil {
return core.NewBizErr("expire_at 不能为空")
}
do = append(do, q.Coupon.ExpireAt.Value(*data.ExpireAt), q.Coupon.ExpireIn.Null())
case m.CouponExpireTypeRelative:
if data.ExpireIn == nil {
return core.NewBizErr("expire_in 不能为空")
}
do = append(do, q.Coupon.ExpireAt.Null(), q.Coupon.ExpireIn.Value(*data.ExpireIn))
}
do = append(do, q.Coupon.ExpireType.Value(int(*data.ExpireType)))
}
if data.ExpireAt != nil {
do = append(do, q.Coupon.ExpireAt.Value(*data.ExpireAt))
}
if data.ExpireIn != nil {
do = append(do, q.Coupon.ExpireIn.Value(*data.ExpireIn))
}
_, err := q.Coupon.Where(q.Coupon.ID.Eq(data.ID)).UpdateSimple(do...)
return err
r, err := q.Coupon.Where(q.Coupon.ID.Eq(data.ID)).UpdateSimple(do...)
if err != nil {
return err
}
if r.RowsAffected == 0 {
return core.NewBizErr("优惠券状态已过期")
}
return nil
}
type UpdateCouponData struct {
@@ -94,8 +110,21 @@ type UpdateCouponData struct {
}
func (s *couponService) Delete(id int32) error {
_, err := q.Coupon.Where(q.Coupon.ID.Eq(id)).UpdateColumn(q.Coupon.DeletedAt, time.Now())
return err
r, err := q.Coupon.Where(q.Coupon.ID.Eq(id)).UpdateColumn(q.Coupon.DeletedAt, time.Now())
if err != nil {
return err
}
if r.RowsAffected == 0 {
return core.NewBizErr("优惠券状态已过期")
}
return nil
}
func (s *couponService) Assign(couponID int32, userID int32) error {
return CouponUser.Create(CreateCouponUserData{
CouponID: couponID,
UserID: userID,
})
}
// GetUserCoupon 获取用户的指定优惠券
@@ -105,7 +134,7 @@ func (s *couponService) GetUserCoupon(uid int32, cuid int32, amount decimal.Deci
q.CouponUser.ID.Eq(cuid),
q.CouponUser.UserID.Eq(uid),
q.CouponUser.Status.Eq(int(m.CouponUserStatusUnused)),
q.CouponUser.ExpireAt.Gt(time.Now()),
q.CouponUser.Where(q.CouponUser.ExpireAt.IsNull()).Or(q.CouponUser.ExpireAt.Gt(time.Now().UTC())),
).Take()
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, core.NewBizErr("优惠券不存在或已失效")
@@ -123,7 +152,7 @@ func (s *couponService) GetUserCoupon(uid int32, cuid int32, amount decimal.Deci
}
func (s *couponService) UseCoupon(q *q.Query, cuid int32) error {
_, err := q.CouponUser.
r, err := q.CouponUser.
Where(
q.CouponUser.ID.Eq(cuid),
q.CouponUser.Status.Eq(int(m.CouponUserStatusUnused)),
@@ -132,5 +161,11 @@ func (s *couponService) UseCoupon(q *q.Query, cuid int32) error {
q.CouponUser.Status.Value(int(m.CouponUserStatusUsed)),
q.CouponUser.UsedAt.Value(time.Now()),
)
return err
if err != nil {
return core.NewBizErr("使用优惠券失败", err)
}
if r.RowsAffected == 0 {
return core.NewBizErr("优惠券状态已过期")
}
return nil
}

255
web/services/coupon_user.go Normal file
View File

@@ -0,0 +1,255 @@
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 CouponUser = &couponUserService{}
type couponUserService struct{}
func (s *couponUserService) Create(data CreateCouponUserData) error {
now := time.Now()
status := u.Else(data.Status, m.CouponUserStatusUnused)
if err := validateCouponUserStatus(status); err != nil {
return err
}
return q.Q.Transaction(func(tx *q.Query) error {
coupon, err := tx.Coupon.Where(tx.Coupon.ID.Eq(data.CouponID)).Take()
if errors.Is(err, gorm.ErrRecordNotFound) {
return core.NewBizErr("优惠券不存在")
}
if err != nil {
return core.NewServErr("获取优惠券数据失败", err)
}
if coupon.Status != m.CouponStatusEnabled {
return core.NewBizErr("优惠券不可用")
}
if coupon.Count <= 0 {
return core.NewBizErr("优惠券已发放完")
}
_, err = tx.User.Where(tx.User.ID.Eq(data.UserID)).Take()
if errors.Is(err, gorm.ErrRecordNotFound) {
return core.NewBizErr("用户不存在")
}
if err != nil {
return core.NewServErr("获取用户数据失败", err)
}
expireAt := data.ExpireAt
if expireAt == nil {
expireAt = couponUserExpireAt(coupon, now)
}
usedAt := data.UsedAt
if status == m.CouponUserStatusUsed && usedAt == nil {
usedAt = &now
}
if status == m.CouponUserStatusUnused {
usedAt = nil
}
err = tx.CouponUser.Create(&m.CouponUser{
UserID: data.UserID,
CouponID: data.CouponID,
Status: status,
ExpireAt: expireAt,
UsedAt: usedAt,
})
if err != nil {
return core.NewServErr("发放优惠券失败", err)
}
return adjustCouponCount(tx, coupon.ID, -1)
})
}
type CreateCouponUserData struct {
CouponID int32 `json:"coupon_id" validate:"required"`
UserID int32 `json:"user_id" validate:"required"`
Status *m.CouponUserStatus `json:"status"`
ExpireAt *time.Time `json:"expire_at"`
UsedAt *time.Time `json:"used_at"`
}
func (s *couponUserService) Update(data UpdateCouponUserData) error {
return q.Q.Transaction(func(tx *q.Query) error {
current, err := tx.CouponUser.Where(tx.CouponUser.ID.Eq(data.ID)).Take()
if errors.Is(err, gorm.ErrRecordNotFound) {
return core.NewBizErr("已发放优惠券不存在")
}
if err != nil {
return core.NewServErr("获取已发放优惠券失败", err)
}
do := make([]field.AssignExpr, 0)
if data.ExpireAtClear != nil && *data.ExpireAtClear {
do = append(do, tx.CouponUser.ExpireAt.Null())
} else if data.ExpireAt != nil {
do = append(do, tx.CouponUser.ExpireAt.Value(*data.ExpireAt))
}
if data.UsedAtClear != nil && *data.UsedAtClear {
do = append(do, tx.CouponUser.UsedAt.Null())
} else if data.UsedAt != nil {
do = append(do, tx.CouponUser.UsedAt.Value(*data.UsedAt))
}
if data.Status != nil {
if err := validateCouponUserStatus(*data.Status); err != nil {
return err
}
if current.Status != *data.Status {
if current.Status == m.CouponUserStatusUsed {
return core.NewBizErr("已使用的优惠券不能修改状态")
}
if current.Status == m.CouponUserStatusDisabled && *data.Status == m.CouponUserStatusUsed {
return core.NewBizErr("已禁用的优惠券不能标记为已使用")
}
switch *data.Status {
case m.CouponUserStatusUnused:
if current.Status == m.CouponUserStatusDisabled {
if err := adjustCouponCount(tx, current.CouponID, -1); err != nil {
return err
}
}
if data.UsedAt == nil && (data.UsedAtClear == nil || !*data.UsedAtClear) {
do = append(do, tx.CouponUser.UsedAt.Null())
}
case m.CouponUserStatusUsed:
if data.UsedAt == nil && (data.UsedAtClear == nil || !*data.UsedAtClear) {
do = append(do, tx.CouponUser.UsedAt.Value(time.Now()))
}
case m.CouponUserStatusDisabled:
if current.Status == m.CouponUserStatusUnused {
if err := adjustCouponCount(tx, current.CouponID, 1); err != nil {
return err
}
}
}
do = append(do, tx.CouponUser.Status.Value(int(*data.Status)))
}
}
if len(do) == 0 {
return nil
}
result, err := tx.CouponUser.
Where(
tx.CouponUser.ID.Eq(data.ID),
tx.CouponUser.Status.Eq(int(current.Status)),
).
UpdateSimple(do...)
if err != nil {
return core.NewServErr("更新已发放优惠券失败", err)
}
if result.RowsAffected == 0 {
return core.NewBizErr("已发放优惠券状态已变化,请重试")
}
return nil
})
}
type UpdateCouponUserData struct {
ID int32 `json:"id" validate:"required"`
Status *m.CouponUserStatus `json:"status"`
ExpireAt *time.Time `json:"expire_at"`
ExpireAtClear *bool `json:"expire_at_clear"`
UsedAt *time.Time `json:"used_at"`
UsedAtClear *bool `json:"used_at_clear"`
}
func (s *couponUserService) Delete(id int32) error {
status := m.CouponUserStatusDisabled
return s.Update(UpdateCouponUserData{
ID: id,
Status: &status,
})
}
func (s *couponUserService) DeleteOfUser(id int32, userID int32) error {
assigned, err := q.CouponUser.Where(
q.CouponUser.ID.Eq(id),
q.CouponUser.UserID.Eq(userID),
).Take()
if errors.Is(err, gorm.ErrRecordNotFound) {
return core.NewBizErr("已发放优惠券不存在")
}
if err != nil {
return core.NewServErr("获取已发放优惠券失败", err)
}
if assigned.Status != m.CouponUserStatusUnused {
return core.NewBizErr("只能撤销未使用的优惠券")
}
return s.Delete(id)
}
func couponUserExpireAt(coupon *m.Coupon, now time.Time) *time.Time {
if coupon == nil {
return nil
}
switch coupon.ExpireType {
case m.CouponExpireTypeFixed:
return coupon.ExpireAt
case m.CouponExpireTypeRelative:
if coupon.ExpireIn == nil {
return nil
}
expireAt := now.Add(time.Duration(*coupon.ExpireIn) * 24 * time.Hour)
return &expireAt
default:
return nil
}
}
func validateCouponUserStatus(status m.CouponUserStatus) error {
switch status {
case m.CouponUserStatusUnused, m.CouponUserStatusUsed, m.CouponUserStatusDisabled:
return nil
default:
return core.NewBizErr("优惠券发放状态无效")
}
}
func adjustCouponCount(tx *q.Query, couponID int32, delta int32) error {
coupon, err := tx.Coupon.Where(tx.Coupon.ID.Eq(couponID)).Take()
if errors.Is(err, gorm.ErrRecordNotFound) {
return core.NewBizErr("优惠券不存在")
}
if err != nil {
return core.NewServErr("获取优惠券数据失败", err)
}
next := coupon.Count + delta
if next < 0 {
return core.NewBizErr("优惠券已发放完")
}
result, err := tx.Coupon.
Where(tx.Coupon.ID.Eq(couponID), tx.Coupon.Count_.Eq(coupon.Count)).
UpdateSimple(tx.Coupon.Count_.Value(next))
if err != nil {
return core.NewServErr("更新优惠券数量失败", err)
}
if result.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

@@ -50,6 +50,7 @@ func (s *productService) AllProductSaleInfos() ([]*m.Product, error) {
q.ProductSku.Name,
q.ProductSku.Code,
q.ProductSku.Price,
q.ProductSku.CountMin,
).
Where(
q.ProductSku.ProductID.In(pids...),
@@ -116,8 +117,14 @@ func (s *productService) UpdateProduct(update *UpdateProductData) error {
if update.Status != nil {
do = append(do, q.Product.Status.Value(*update.Status))
}
_, err := q.Product.Where(q.Product.ID.Eq(update.Id)).UpdateSimple(do...)
return err
r, err := q.Product.Where(q.Product.ID.Eq(update.Id)).UpdateSimple(do...)
if err != nil {
return err
}
if r.RowsAffected == 0 {
return core.NewBizErr("产品状态已过期")
}
return nil
}
type UpdateProductData struct {
@@ -131,6 +138,12 @@ type UpdateProductData struct {
// 删除产品
func (s *productService) DeleteProduct(id int32) error {
_, err := q.Product.Where(q.Product.ID.Eq(id)).UpdateColumn(q.Product.DeletedAt, time.Now())
return err
r, err := q.Product.Where(q.Product.ID.Eq(id)).UpdateColumn(q.Product.DeletedAt, time.Now())
if err != nil {
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))
}
_, err = q.ProductDiscount.Where(q.ProductDiscount.ID.Eq(data.ID)).UpdateSimple(do...)
return err
r, err := q.ProductDiscount.Where(q.ProductDiscount.ID.Eq(data.ID)).UpdateSimple(do...)
if err != nil {
return err
}
if r.RowsAffected == 0 {
return core.NewServErr("产品折扣状态已过期")
}
return nil
}
type UpdateProductDiscountData struct {
@@ -54,6 +60,12 @@ type UpdateProductDiscountData struct {
}
func (s *productDiscountService) Delete(id int32) (err error) {
_, err = q.ProductDiscount.Where(q.ProductDiscount.ID.Eq(id)).UpdateColumn(q.ProductDiscount.DeletedAt, time.Now())
return
r, err := q.ProductDiscount.Where(q.ProductDiscount.ID.Eq(id)).UpdateColumn(q.ProductDiscount.DeletedAt, time.Now())
if err != nil {
return err
}
if r.RowsAffected == 0 {
return core.NewServErr("产品折扣状态已过期")
}
return nil
}

View File

@@ -47,6 +47,11 @@ func (s *productSkuService) Create(create CreateProductSkuData) (err error) {
return core.NewBizErr("产品最低价格的格式不正确", err)
}
countMin := int32(1)
if create.CountMin != nil {
countMin = *create.CountMin
}
return q.ProductSku.Create(&m.ProductSku{
ProductID: create.ProductID,
DiscountId: create.DiscountID,
@@ -54,6 +59,8 @@ func (s *productSkuService) Create(create CreateProductSkuData) (err error) {
Name: create.Name,
Price: price,
PriceMin: priceMin,
Sort: create.Sort,
CountMin: countMin,
})
}
@@ -64,6 +71,8 @@ type CreateProductSkuData struct {
Name string `json:"name"`
Price string `json:"price"`
PriceMin string `json:"price_min"`
Sort int32 `json:"sort"`
CountMin *int32 `json:"count_min"`
}
func (s *productSkuService) Update(update UpdateProductSkuData) (err error) {
@@ -95,9 +104,21 @@ func (s *productSkuService) Update(update UpdateProductSkuData) (err error) {
if update.Status != nil {
do = append(do, q.ProductSku.Status.Value(*update.Status))
}
if update.Sort != nil {
do = append(do, q.ProductSku.Sort.Value(*update.Sort))
}
if update.CountMin != nil {
do = append(do, q.ProductSku.CountMin.Value(*update.CountMin))
}
_, err = q.ProductSku.Where(q.ProductSku.ID.Eq(update.ID)).UpdateSimple(do...)
return err
r, err := q.ProductSku.Where(q.ProductSku.ID.Eq(update.ID)).UpdateSimple(do...)
if err != nil {
return err
}
if r.RowsAffected == 0 {
return core.NewServErr("产品套餐状态已过期")
}
return nil
}
type UpdateProductSkuData struct {
@@ -108,18 +129,32 @@ type UpdateProductSkuData struct {
Price *string `json:"price"`
PriceMin string `json:"price_min"`
Status *int32 `json:"status"`
Sort *int32 `json:"sort"`
CountMin *int32 `json:"count_min"`
}
func (s *productSkuService) Delete(id int32) (err error) {
_, err = q.ProductSku.Where(q.ProductSku.ID.Eq(id)).UpdateColumn(q.ProductSku.DeletedAt, time.Now())
return
r, err := q.ProductSku.Where(q.ProductSku.ID.Eq(id)).UpdateColumn(q.ProductSku.DeletedAt, time.Now())
if err != nil {
return err
}
if r.RowsAffected == 0 {
return core.NewServErr("产品套餐状态已过期")
}
return nil
}
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),
)
return
if err != nil {
return err
}
if r.RowsAffected == 0 {
return core.NewServErr("产品套餐状态已过期")
}
return nil
}
type BatchUpdateSkuDiscountData struct {

View File

@@ -1,65 +1,351 @@
package services
import (
"context"
"errors"
"fmt"
"net/netip"
"platform/pkg/u"
"platform/web/core"
g "platform/web/globals"
"platform/web/globals/orm"
m "platform/web/models"
q "platform/web/queries"
"strings"
"time"
"gorm.io/gen/field"
"gorm.io/gorm"
)
var Proxy = &proxyService{}
type proxyService struct{}
// AllProxies 获取所有代理
func (s *proxyService) AllProxies(proxyType m.ProxyType, channels bool) ([]*m.Proxy, error) {
proxies, err := q.Proxy.Where(
q.Proxy.Type.Eq(int(proxyType)),
q.Proxy.Status.Eq(int(m.ProxyStatusOnline)),
).Preload(
q.Proxy.Channels.On(q.Channel.ExpiredAt.Gte(time.Now())),
).Find()
if err != nil {
return nil, err
func hasUsedChans(proxyID int32) (bool, error) {
ctx := context.Background()
pattern := usedChansKey(proxyID, "*")
var cursor uint64
for {
keys, next, err := g.Redis.Scan(ctx, cursor, pattern, 100).Result()
if err != nil {
return false, err
}
if len(keys) > 0 {
return true, nil
}
if next == 0 {
return false, nil
}
cursor = next
}
return proxies, nil
}
// RegisterBaiyin 注册新代理服务
func (s *proxyService) RegisterBaiyin(Name string, IP netip.Addr, username, password string) error {
// 保存代理信息
proxy := &m.Proxy{
Version: 0,
Mac: Name,
IP: orm.Inet{Addr: IP},
Secret: u.P(fmt.Sprintf("%s:%s", username, password)),
Type: m.ProxyTypeBaiYin,
Status: m.ProxyStatusOnline,
}
if err := q.Proxy.Create(proxy); err != nil {
return core.NewServErr("保存通道数据失败")
func rebuildFreeChans(proxyID int32, addr netip.Addr) error {
if err := remChans(proxyID); err != nil {
return err
}
// 添加可用通道到 redis
chans := make([]netip.AddrPort, 10000)
for i := range 10000 {
chans[i] = netip.AddrPortFrom(IP, uint16(i+10000))
chans[i] = netip.AddrPortFrom(addr, uint16(i+10000))
}
err := regChans(proxy.ID, chans)
if err := regChans(proxyID, chans); err != nil {
return err
}
return nil
}
func (s *proxyService) Page(req core.PageReq) (result []*m.Proxy, count int64, err error) {
return q.Proxy.
Omit(q.Proxy.Version, q.Proxy.Meta).
Order(q.Proxy.CreatedAt.Desc()).
FindByPage(req.GetOffset(), req.GetLimit())
}
func (s *proxyService) All() (result []*m.Proxy, err error) {
return q.Proxy.
Omit(q.Proxy.Version, q.Proxy.Meta).
Order(q.Proxy.CreatedAt.Desc()).
Find()
}
type CreateProxy struct {
Mac string `json:"mac" validate:"required"`
IP string `json:"ip" validate:"required"`
Host *string `json:"host"`
Port *int `json:"port"`
Secret *string `json:"secret"`
Type *m.ProxyType `json:"type"`
Status *m.ProxyStatus `json:"status"`
}
func (s *proxyService) Create(create *CreateProxy) error {
addr, err := netip.ParseAddr(create.IP)
if err != nil {
return core.NewServErr("添加通道失败", err)
return core.NewServErr("IP地址格式错误", err)
}
return q.Q.Transaction(func(tx *q.Query) error {
proxy := &m.Proxy{
Mac: create.Mac,
IP: orm.Inet{Addr: addr},
Host: create.Host,
Port: create.Port,
Secret: create.Secret,
Type: u.Else(create.Type, m.ProxyTypeSelfHosted),
Status: u.Else(create.Status, m.ProxyStatusOffline),
}
if err := tx.Proxy.Create(proxy); err != nil {
return core.NewServErr("保存代理数据失败", err)
}
if err := rebuildFreeChans(proxy.ID, addr); err != nil {
return core.NewServErr("初始化代理通道失败", err)
}
return nil
})
}
type UpdateProxy struct {
ID int32 `json:"id" validate:"required"`
Mac *string `json:"mac"`
IP *string `json:"ip"`
Host *string `json:"host"`
Port *int `json:"port"`
Secret *string `json:"secret"`
}
func (s *proxyService) Update(update *UpdateProxy) error {
simples := make([]field.AssignExpr, 0)
hasSideEffect := false
if update.Mac != nil {
hasSideEffect = true
simples = append(simples, q.Proxy.Mac.Value(*update.Mac))
}
if update.IP != nil {
addr, err := netip.ParseAddr(*update.IP)
if err != nil {
return core.NewServErr("IP地址格式错误", err)
}
hasSideEffect = true
simples = append(simples, q.Proxy.IP.Value(orm.Inet{Addr: addr}))
}
if update.Host != nil {
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 {
hasSideEffect = true
simples = append(simples, q.Proxy.Secret.Value(*update.Secret))
}
if len(simples) == 0 {
return nil
}
if hasSideEffect {
used, err := hasUsedChans(update.ID)
if err != nil {
return core.NewServErr("检查代理通道状态失败", err)
}
if used {
return core.NewBizErr("代理存在未关闭通道,禁止修改")
}
}
rs, err := q.Proxy.
Where(
q.Proxy.ID.Eq(update.ID),
q.Proxy.Status.Eq(int(m.ProxyStatusOffline)),
).
UpdateSimple(simples...)
if err != nil {
return err
}
if rs.RowsAffected == 0 {
return core.NewBizErr("代理未下线,禁止修改")
}
return nil
}
// UnregisterBaiyin 注销代理服务
func (s *proxyService) UnregisterBaiyin(id int) error {
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 {
used, err := hasUsedChans(id)
if err != nil {
return core.NewServErr("检查代理通道状态失败", err)
}
if used {
return core.NewBizErr("代理存在未关闭通道,禁止删除")
}
rs, err := q.Proxy.
Where(
q.Proxy.ID.Eq(id),
q.Proxy.Status.Eq(int(m.ProxyStatusOffline)),
).
UpdateColumn(q.Proxy.DeletedAt, time.Now())
if err != nil {
return err
}
if rs.RowsAffected == 0 {
return core.NewBizErr("代理未下线,禁止删除")
}
if err := remChans(id); err != nil {
return core.NewServErr("注销代理通道失败", err)
}
return nil
}
type UpdateProxyStatus struct {
ID int32 `json:"id" validate:"required"`
Status m.ProxyStatus `json:"status"`
}
func (s *proxyService) UpdateStatus(update *UpdateProxyStatus) error {
return q.Q.Transaction(func(tx *q.Query) error {
proxy, err := q.Proxy.Where(q.Proxy.ID.Eq(update.ID)).Take()
if err != nil {
return err
}
if proxy.Status == update.Status {
return nil
}
if update.Status == m.ProxyStatusOnline {
if err := rebuildFreeChans(proxy.ID, proxy.IP.Addr); err != nil {
return core.NewServErr("初始化代理通道失败", err)
}
}
_, err = q.Proxy.
Where(q.Proxy.ID.Eq(update.ID)).
UpdateSimple(q.Proxy.Status.Value(int(update.Status)))
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

@@ -2,6 +2,7 @@ package services
import (
"fmt"
"log/slog"
"platform/pkg/u"
"platform/web/core"
m "platform/web/models"
@@ -62,9 +63,10 @@ func (s *resourceService) Create(q *q.Query, uid int32, now time.Time, data *Cre
var resource = m.Resource{
UserID: uid,
ResourceNo: u.P(ID.GenReadable("res")),
Active: true,
Type: data.Type,
Code: data.Type.Code(),
Active: true,
CheckIP: true,
}
switch data.Type {
@@ -120,38 +122,49 @@ func (s *resourceService) Create(q *q.Query, uid int32, now time.Time, data *Cre
}
func (s *resourceService) Update(data *UpdateResourceData) error {
if data.Active == nil {
return core.NewBizErr("更新套餐失败active 不能为空")
}
do := make([]field.AssignExpr, 0)
if data.Active != nil {
do = append(do, q.Resource.Active.Value(*data.Active))
}
if data.CheckIP != nil {
do = append(do, q.Resource.CheckIP.Value(*data.CheckIP))
}
_, err := q.Resource.
r, err := q.Resource.
Where(q.Resource.ID.Eq(data.Id)).
UpdateSimple(do...)
if err != nil {
return core.NewServErr("更新套餐失败", err)
}
if r.RowsAffected == 0 {
return core.NewBizErr("套餐状态已过期")
}
return nil
}
type UpdateResourceData struct {
core.IdReq
Active *bool `json:"active"`
Active *bool `json:"active"`
CheckIP *bool `json:"checkip"`
}
func (s *resourceService) CalcPrice(skuCode string, count int32, user *m.User, cuid *int32) (*m.ProductSku, *m.ProductDiscount, *m.CouponUser, decimal.Decimal, decimal.Decimal, decimal.Decimal, error) {
if count <= 0 {
return nil, nil, nil, decimal.Zero, decimal.Zero, decimal.Zero, core.NewBizErr("购买数量必须大于 0")
}
sku, err := q.ProductSku.
Joins(q.ProductSku.Discount).
Where(q.ProductSku.Code.Eq(skuCode), q.ProductSku.Status.Eq(int32(m.SkuStatusEnabled))).
Take()
if err != nil {
return nil, nil, nil, decimal.Zero, decimal.Zero, decimal.Zero, core.NewServErr(fmt.Sprintf("产品不可用 %s", skuCode), err)
slog.Debug("查询产品失败", "skuCode", skuCode)
return nil, nil, nil, decimal.Zero, decimal.Zero, decimal.Zero, core.NewBizErr("产品不可用", err)
}
if count < sku.CountMin {
return nil, nil, nil, decimal.Zero, decimal.Zero, decimal.Zero, core.NewBizErr(fmt.Sprintf("购买数量不能少于 %d", sku.CountMin))
}
// 原价
@@ -161,7 +174,7 @@ func (s *resourceService) CalcPrice(skuCode string, count int32, user *m.User, c
// 折扣价
discount := sku.Discount
if discount == nil {
return nil, nil, nil, decimal.Zero, decimal.Zero, decimal.Zero, core.NewServErr("价格查询失败", err)
return nil, nil, nil, decimal.Zero, decimal.Zero, decimal.Zero, core.NewServErr("产品未配置折扣", err)
}
discountRate := discount.Rate()
@@ -199,7 +212,7 @@ func (s *resourceService) CalcPrice(skuCode string, count int32, user *m.User, c
couponApplied = amountMin.Copy()
}
return sku, discount, coupon, amount, discounted, couponApplied, nil
return sku, discount, coupon, amount.RoundCeil(2), discounted.RoundCeil(2), couponApplied.RoundCeil(2), nil
}
type CreateResourceData struct {

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.Trade.
r, err := q.Trade.
Where(
q.Trade.InnerNo.Eq(interNo),
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 {
return core.NewServErr("更新交易信息失败", err)
}
if r.RowsAffected == 0 {
return core.NewBizErr("交易状态已过期")
}
switch trade.Type {
case m.TradeTypeRecharge:
@@ -406,7 +409,7 @@ func (s *tradeService) CancelTrade(ref *TradeRef) error {
return nil
}
func (s *tradeService) OnCancelTrade(tradeNo string, now time.Time) error {
_, err := q.Trade.
r, err := q.Trade.
Where(
q.Trade.InnerNo.Eq(tradeNo),
q.Trade.Status.Eq(int(m.TradeStatusPending)),
@@ -418,6 +421,9 @@ func (s *tradeService) OnCancelTrade(tradeNo string, now time.Time) error {
if err != nil {
return core.NewServErr("更新交易状态失败", err)
}
if r.RowsAffected == 0 {
return core.NewBizErr("交易状态已过期")
}
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(
q.User.ID.Eq(user.ID),
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 {
return core.NewServErr("更新用户余额失败", err)
}
if r.RowsAffected == 0 {
return core.NewBizErr("余额状态已过期")
}
// 新增动账记录
err = q.BalanceActivity.Create(&m.BalanceActivity{
@@ -88,6 +91,9 @@ type UpdateBalanceData struct {
}
func (data *UpdateBalanceData) TradeDetail(user *m.User) (*TradeDetail, error) {
if data.Amount <= 0 {
return nil, core.NewBizErr("充值金额必须大于0")
}
amount := decimal.NewFromInt(int64(data.Amount)).Div(decimal.NewFromInt(100))
return &TradeDetail{
data,
@@ -201,12 +207,18 @@ func (s *userService) UpdateByAdmin(data UpdateUserByAdminData) error {
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) {
return core.NewBizErr("账号已存在,请检查手机号/用户名/邮箱是否重复")
}
if err != nil {
return err
}
if r.RowsAffected == 0 {
return core.NewBizErr("用户状态已过期")
}
return err
return nil
}
type UpdateUserByAdminData struct {
@@ -228,6 +240,12 @@ type UpdateUserByAdminData struct {
}
func (s *userService) RemoveByAdmin(id int32) error {
_, err := q.User.Where(q.User.ID.Eq(id)).UpdateColumn(q.User.DeletedAt, time.Now())
return err
r, err := q.User.Where(q.User.ID.Eq(id)).UpdateColumn(q.User.DeletedAt, time.Now())
if err != nil {
return err
}
if r.RowsAffected == 0 {
return core.NewBizErr("用户状态已过期")
}
return nil
}

View File

@@ -13,6 +13,7 @@ import (
)
func HandleCompleteTrade(_ context.Context, task *asynq.Task) error {
slog.Info("[event]尝试结束交易")
var event events.CloseTradeData
if err := json.Unmarshal(task.Payload(), &event); err != nil {
return fmt.Errorf("解析任务参数失败: %w", err)
@@ -30,11 +31,11 @@ func HandleCompleteTrade(_ context.Context, task *asynq.Task) error {
}
if err := s.Trade.CompleteTrade(user, &data); err != nil {
slog.Debug("完成交易失败[异步结束订单]", "err", err)
slog.Debug("结束交易失败:完成交易失败", "err", err)
// 交易无法完成,关闭交易
if err := s.Trade.CancelTrade(&data); err != nil {
return fmt.Errorf("取消交易失败[异步结束订单]: %w", err)
return fmt.Errorf("结束交易失败:取消交易失败: %w", err)
}
}
@@ -43,9 +44,21 @@ func HandleCompleteTrade(_ context.Context, task *asynq.Task) error {
func HandleRemoveChannel(_ context.Context, task *asynq.Task) (err error) {
batch := string(task.Payload())
slog.Info("[event]删除通道", "batch", batch)
err = s.Channel.RemoveChannels(batch)
if err != nil {
return fmt.Errorf("删除通道失败: %w", err)
}
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)
})
g.Go(func() error {
return RunCron(ctx)
})
return g.Wait()
}
@@ -49,7 +53,6 @@ func RunApp(pCtx context.Context) error {
var fs embed.FS
func RunWeb(ctx context.Context) error {
fiber := fiber.New(fiber.Config{
ProxyHeader: fiber.HeaderXForwardedFor,
ErrorHandler: ErrorHandler,
@@ -80,16 +83,18 @@ func RunWeb(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,
ErrorHandler: asynq.ErrorHandlerFunc(func(ctx context.Context, task *asynq.Task, err error) {
slog.Error("任务执行失败", "task", task.Type(), "error", err)
}),
Logger: &AppAsynqLogger{},
})
var mux = asynq.NewServeMux()
mux.HandleFunc(events.RemoveChannel, tasks.HandleRemoveChannel)
mux.HandleFunc(events.CloseTrade, tasks.HandleCompleteTrade)
mux.HandleFunc(events.RefreshEdge, tasks.HandleRefreshEdges)
// 停止服务
go func() {
@@ -105,3 +110,56 @@ func RunTask(ctx context.Context) error {
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{}
func (l *AppAsynqLogger) Debug(args ...any) {
slog.Debug("Asynq", anyToAttrs(args)...)
}
func (l *AppAsynqLogger) Info(args ...any) {
slog.Info("Asynq", anyToAttrs(args)...)
}
func (l *AppAsynqLogger) Warn(args ...any) {
slog.Warn("Asynq", anyToAttrs(args)...)
}
func (l *AppAsynqLogger) Error(args ...any) {
slog.Error("Asynq", anyToAttrs(args)...)
}
func (l *AppAsynqLogger) Fatal(args ...any) {
slog.Error("Asynq[Fatal]", anyToAttrs(args)...)
}
func anyToAttrs(args ...any) []any {
attrs := make([]any, len(args))
for i, arg := range args {
attrs[i] = slog.Any(fmt.Sprintf("arg%d", i), arg)
}
return attrs
}