Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 25cacf0bca | |||
| 513fe78815 | |||
| ebac8042ea | |||
| dd482dd6b0 | |||
| c5453557ae | |||
| b00782b3f6 | |||
| 1b39b2d411 | |||
| 7f30b6be4e | |||
| 0dfbbe5939 | |||
| 32e56b1a0f | |||
| b436a6cade | |||
| dd08655e2c | |||
| 9fe6cb4bf5 | |||
| cf4bc4932a | |||
| dbc909c736 | |||
| 71554da541 | |||
| 8f89503c88 | |||
| 80f04c92ec | |||
| ccbc6f0b67 | |||
| d273731e31 | |||
| 65f8ee360b | |||
| 042c8d1a51 | |||
| a0b0be2b8e | |||
| 8fc1d30578 | |||
| a4d9c28702 | |||
| ccb8db555e | |||
| e70f2337cb | |||
| d59f4ca37f | |||
| 0edc883084 | |||
| d26106eb00 | |||
| 6e14ea65d0 | |||
| 982cbb4cab | |||
| a964fe4d69 | |||
| 6db3caaecb | |||
| fd475d3e63 | |||
| 9b3546b45f | |||
| b8c8c7d7b1 | |||
| 58b8849d8d |
@@ -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
2
.gitignore
vendored
@@ -19,3 +19,5 @@ scripts/*
|
||||
!scripts/env/dev/
|
||||
!scripts/pre/
|
||||
!scripts/sql/
|
||||
|
||||
*/uploads/
|
||||
|
||||
@@ -19,7 +19,7 @@ WORKDIR /app
|
||||
|
||||
ENV TZ=Asia/Shanghai
|
||||
|
||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories
|
||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
||||
RUN apk add --no-cache ca-certificates tzdata
|
||||
|
||||
COPY --from=builder /build/bin/platform_linux_amd64 /app/platform
|
||||
|
||||
51
README.md
51
README.md
@@ -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 | 长效动态代理 |
|
||||
|
||||
### 节点分配与存储逻辑
|
||||
|
||||
提取:
|
||||
|
||||
@@ -35,6 +35,9 @@ func main() {
|
||||
m.Admin{},
|
||||
m.AdminRole{},
|
||||
m.Announcement{},
|
||||
m.Area{},
|
||||
m.Article{},
|
||||
m.ArticleGroup{},
|
||||
m.Bill{},
|
||||
m.Channel{},
|
||||
m.Client{},
|
||||
|
||||
@@ -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:
|
||||
|
||||
6219
docs/api.yaml
6219
docs/api.yaml
File diff suppressed because it is too large
Load Diff
36
go.mod
36
go.mod
@@ -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
80
go.sum
@@ -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
29
pkg/env/env.go
vendored
@@ -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))
|
||||
|
||||
51
pkg/u/u.go
51
pkg/u/u.go
@@ -53,6 +53,18 @@ func X[T comparable](v T) *T {
|
||||
return &v
|
||||
}
|
||||
|
||||
// N 零值视为 nil
|
||||
func N[T comparable](v *T) *T {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
var zero T
|
||||
if *v == zero {
|
||||
return nil
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// ====================
|
||||
// 数组
|
||||
// ====================
|
||||
@@ -69,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 {
|
||||
@@ -110,3 +113,21 @@ func CombineErrors(errs []error) error {
|
||||
}
|
||||
return combinedErr
|
||||
}
|
||||
|
||||
// ====================
|
||||
// 业务
|
||||
// ====================
|
||||
|
||||
func MaskPhone(phone string) string {
|
||||
if len(phone) < 11 {
|
||||
return phone
|
||||
}
|
||||
return phone[:3] + "****" + phone[7:]
|
||||
}
|
||||
|
||||
func MaskIdNo(idNo string) string {
|
||||
if len(idNo) < 18 {
|
||||
return idNo
|
||||
}
|
||||
return idNo[:3] + "*********" + idNo[14:]
|
||||
}
|
||||
|
||||
@@ -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
258
scripts/sql/fill-area.sql
Normal 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
6601
scripts/sql/fill-edge.sql
Normal file
File diff suppressed because it is too large
Load Diff
@@ -15,9 +15,93 @@ insert into admin (username, password, name, lock) values ('admin', '', '超级
|
||||
-- region 产品
|
||||
-- ====================
|
||||
|
||||
insert into product (code, name, description) values ('short', '短效动态', '短效动态');
|
||||
insert into product (code, name, description) values ('long', '长效动态', '长效动态');
|
||||
insert into product (code, name, description) values ('static', '长效静态', '长效静态');
|
||||
delete from product where true;
|
||||
|
||||
insert into product (code, name, description, sort) values ('short', '短效动态', '短效动态', 1);
|
||||
insert into product (code, name, description, sort) values ('long', '长效动态', '长效动态', 2);
|
||||
insert into product (code, name, description, sort) values ('static', '长效静态', '长效静态', 3);
|
||||
|
||||
-- ====================
|
||||
-- region 套餐
|
||||
-- ====================
|
||||
|
||||
delete from product_sku where true;
|
||||
|
||||
insert into product_sku (product_id, code, name, price, price_min, sort) values
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=quota&live=3&expire=0', '短效动态包量 3 分钟', 10.00, 10.00, 1),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=quota&live=5&expire=0', '短效动态包量 5 分钟', 10.00, 10.00, 2),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=quota&live=10&expire=0', '短效动态包量 10 分钟', 10.00, 10.00, 3),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=quota&live=15&expire=0', '短效动态包量 15 分钟', 10.00, 10.00, 4),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=quota&live=30&expire=0', '短效动态包量 30 分钟', 10.00, 10.00, 5),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=3&expire=7', '短效动态包时 3 分钟 7 天', 10.00, 10.00, 6),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=5&expire=7', '短效动态包时 5 分钟 7 天', 10.00, 10.00, 7),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=10&expire=7', '短效动态包时 10 分钟 7 天', 10.00, 10.00, 8),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=15&expire=7', '短效动态包时 15 分钟 7 天', 10.00, 10.00, 9),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=30&expire=7', '短效动态包时 30 分钟 7 天', 10.00, 10.00, 10),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=3&expire=15', '短效动态包时 3 分钟 15 天', 10.00, 10.00, 11),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=5&expire=15', '短效动态包时 5 分钟 15 天', 10.00, 10.00, 12),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=10&expire=15', '短效动态包时 10 分钟 15 天', 10.00, 10.00, 13),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=15&expire=15', '短效动态包时 15 分钟 15 天', 10.00, 10.00, 14),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=30&expire=15', '短效动态包时 30 分钟 15 天', 10.00, 10.00, 15),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=3&expire=30', '短效动态包时 3 分钟 30 天', 10.00, 10.00, 16),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=5&expire=30', '短效动态包时 5 分钟 30 天', 10.00, 10.00, 17),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=10&expire=30', '短效动态包时 10 分钟 30 天', 10.00, 10.00, 18),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=15&expire=30', '短效动态包时 15 分钟 30 天', 10.00, 10.00, 19),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=30&expire=30', '短效动态包时 30 分钟 30 天', 10.00, 10.00, 20),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=3&expire=90', '短效动态包时 3 分钟 90 天', 10.00, 10.00, 21),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=5&expire=90', '短效动态包时 5 分钟 90 天', 10.00, 10.00, 22),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=10&expire=90', '短效动态包时 10 分钟 90 天', 10.00, 10.00, 23),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=15&expire=90', '短效动态包时 15 分钟 90 天', 10.00, 10.00, 24),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=30&expire=90', '短效动态包时 30 分钟 90 天', 10.00, 10.00, 25),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=3&expire=180', '短效动态包时 3 分钟 180 天', 10.00, 10.00, 26),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=5&expire=180', '短效动态包时 5 分钟 180 天', 10.00, 10.00, 27),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=10&expire=180', '短效动态包时 10 分钟 180 天', 10.00, 10.00, 28),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=15&expire=180', '短效动态包时 15 分钟 180 天', 10.00, 10.00, 29),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=30&expire=180', '短效动态包时 30 分钟 180 天', 10.00, 10.00, 30),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=3&expire=365', '短效动态包时 3 分钟 365 天', 10.00, 10.00, 31),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=5&expire=365', '短效动态包时 5 分钟 365 天', 10.00, 10.00, 32),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=10&expire=365', '短效动态包时 10 分钟 365 天', 10.00, 10.00, 33),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=15&expire=365', '短效动态包时 15 分钟 365 天', 10.00, 10.00, 34),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=30&expire=365', '短效动态包时 30 分钟 365 天', 10.00, 10.00, 35)
|
||||
;
|
||||
|
||||
insert into product_sku (product_id, code, name, price, price_min, sort) values
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=quota&live=60&expire=0', '长效动态包量 1 小时', 10.00, 10.00, 1),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=quota&live=240&expire=0', '长效动态包量 4 小时', 10.00, 10.00, 2),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=quota&live=480&expire=0', '长效动态包量 8 小时', 10.00, 10.00, 3),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=quota&live=720&expire=0', '长效动态包量 12 小时', 10.00, 10.00, 4),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=quota&live=1440&expire=0', '长效动态包量 24 小时', 10.00, 10.00, 5),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=60&expire=7', '长效动态包时 1 小时 7 天', 10.00, 10.00, 6),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=240&expire=7', '长效动态包时 4 小时 7 天', 10.00, 10.00, 7),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=480&expire=7', '长效动态包时 8 小时 7 天', 10.00, 10.00, 8),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=720&expire=7', '长效动态包时 12 小时 7 天', 10.00, 10.00, 9),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=1440&expire=7', '长效动态包时 24 小时 7 天', 10.00, 10.00, 10),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=60&expire=15', '长效动态包时 1 小时 15 天', 10.00, 10.00, 11),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=240&expire=15', '长效动态包时 4 小时 15 天', 10.00, 10.00, 12),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=480&expire=15', '长效动态包时 8 小时 15 天', 10.00, 10.00, 13),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=720&expire=15', '长效动态包时 12 小时 15 天', 10.00, 10.00, 14),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=1440&expire=15', '长效动态包时 24 小时 15 天', 10.00, 10.00, 15),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=60&expire=30', '长效动态包时 1 小时 30 天', 10.00, 10.00, 16),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=240&expire=30', '长效动态包时 4 小时 30 天', 10.00, 10.00, 17),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=480&expire=30', '长效动态包时 8 小时 30 天', 10.00, 10.00, 18),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=720&expire=30', '长效动态包时 12 小时 30 天', 10.00, 10.00, 19),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=1440&expire=30', '长效动态包时 24 小时 30 天', 10.00, 10.00, 20),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=60&expire=90', '长效动态包时 1 小时 90 天', 10.00, 10.00, 21),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=240&expire=90', '长效动态包时 4 小时 90 天', 10.00, 10.00, 22),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=480&expire=90', '长效动态包时 8 小时 90 天', 10.00, 10.00, 23),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=720&expire=90', '长效动态包时 12 小时 90 天', 10.00, 10.00, 24),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=1440&expire=90', '长效动态包时 24 小时 90 天', 10.00, 10.00, 25),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=60&expire=180', '长效动态包时 1 小时 180 天', 10.00, 10.00, 26),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=240&expire=180', '长效动态包时 4 小时 180 天', 10.00, 10.00, 27),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=480&expire=180', '长效动态包时 8 小时 180 天', 10.00, 10.00, 28),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=720&expire=180', '长效动态包时 12 小时 180 天', 10.00, 10.00, 29),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=1440&expire=180','长效动态包时 24 小时 180 天', 10.00, 10.00, 30),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=60&expire=365', '长效动态包时 1 小时 365 天', 10.00, 10.00, 31),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=240&expire=365', '长效动态包时 4 小时 365 天', 10.00, 10.00, 32),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=480&expire=365', '长效动态包时 8 小时 365 天', 10.00, 10.00, 33),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=720&expire=365', '长效动态包时 12 小时 365 天', 10.00, 10.00, 34),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=1440&expire=365','长效动态包时 24 小时 365 天', 10.00, 10.00, 35)
|
||||
;
|
||||
|
||||
-- ====================
|
||||
-- region 权限
|
||||
@@ -42,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
|
||||
@@ -51,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
|
||||
-- --------------------------
|
||||
@@ -127,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);
|
||||
@@ -142,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 子权限
|
||||
@@ -165,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
|
||||
-- --------------------------
|
||||
|
||||
@@ -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 '最近丢包率';
|
||||
@@ -761,6 +843,8 @@ create table product_sku (
|
||||
price decimal not null,
|
||||
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
|
||||
@@ -778,6 +862,8 @@ comment on column product_sku.code is 'SKU 代码:格式为 key=value,key=valu
|
||||
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 '删除时间';
|
||||
@@ -814,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
|
||||
@@ -828,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 '删除时间';
|
||||
@@ -1105,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 '创建时间';
|
||||
@@ -1171,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
|
||||
@@ -1222,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;
|
||||
|
||||
81
scripts/sql/update.sql
Normal file
81
scripts/sql/update.sql
Normal file
@@ -0,0 +1,81 @@
|
||||
-- ====================
|
||||
-- region 套餐更新
|
||||
-- ====================
|
||||
|
||||
-- --------------------------
|
||||
-- 短效动态
|
||||
-- --------------------------
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包量 3 分钟', price = 10.00, price_min = 10.00, sort = 1 where code = 'mode=quota&live=3&expire=0' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包量 5 分钟', price = 10.00, price_min = 10.00, sort = 2 where code = 'mode=quota&live=5&expire=0' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包量 10 分钟', price = 10.00, price_min = 10.00, sort = 3 where code = 'mode=quota&live=10&expire=0' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包量 15 分钟', price = 10.00, price_min = 10.00, sort = 4 where code = 'mode=quota&live=15&expire=0' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包量 30 分钟', price = 10.00, price_min = 10.00, sort = 5 where code = 'mode=quota&live=30&expire=0' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 3 分钟 7 天', price = 10.00, price_min = 10.00, sort = 6 where code = 'mode=time&live=3&expire=7' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 5 分钟 7 天', price = 10.00, price_min = 10.00, sort = 7 where code = 'mode=time&live=5&expire=7' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 10 分钟 7 天', price = 10.00, price_min = 10.00, sort = 8 where code = 'mode=time&live=10&expire=7' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 15 分钟 7 天', price = 10.00, price_min = 10.00, sort = 9 where code = 'mode=time&live=15&expire=7' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 30 分钟 7 天', price = 10.00, price_min = 10.00, sort = 10 where code = 'mode=time&live=30&expire=7' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 3 分钟 15 天', price = 10.00, price_min = 10.00, sort = 11 where code = 'mode=time&live=3&expire=15' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 5 分钟 15 天', price = 10.00, price_min = 10.00, sort = 12 where code = 'mode=time&live=5&expire=15' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 10 分钟 15 天', price = 10.00, price_min = 10.00, sort = 13 where code = 'mode=time&live=10&expire=15' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 15 分钟 15 天', price = 10.00, price_min = 10.00, sort = 14 where code = 'mode=time&live=15&expire=15' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 30 分钟 15 天', price = 10.00, price_min = 10.00, sort = 15 where code = 'mode=time&live=30&expire=15' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 3 分钟 30 天', price = 10.00, price_min = 10.00, sort = 16 where code = 'mode=time&live=3&expire=30' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 5 分钟 30 天', price = 10.00, price_min = 10.00, sort = 17 where code = 'mode=time&live=5&expire=30' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 10 分钟 30 天', price = 10.00, price_min = 10.00, sort = 18 where code = 'mode=time&live=10&expire=30' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 15 分钟 30 天', price = 10.00, price_min = 10.00, sort = 19 where code = 'mode=time&live=15&expire=30' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 30 分钟 30 天', price = 10.00, price_min = 10.00, sort = 20 where code = 'mode=time&live=30&expire=30' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 3 分钟 90 天', price = 10.00, price_min = 10.00, sort = 21 where code = 'mode=time&live=3&expire=90' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 5 分钟 90 天', price = 10.00, price_min = 10.00, sort = 22 where code = 'mode=time&live=5&expire=90' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 10 分钟 90 天', price = 10.00, price_min = 10.00, sort = 23 where code = 'mode=time&live=10&expire=90' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 15 分钟 90 天', price = 10.00, price_min = 10.00, sort = 24 where code = 'mode=time&live=15&expire=90' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 30 分钟 90 天', price = 10.00, price_min = 10.00, sort = 25 where code = 'mode=time&live=30&expire=90' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 3 分钟 180 天', price = 10.00, price_min = 10.00, sort = 26 where code = 'mode=time&live=3&expire=180' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 5 分钟 180 天', price = 10.00, price_min = 10.00, sort = 27 where code = 'mode=time&live=5&expire=180' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 10 分钟 180 天', price = 10.00, price_min = 10.00, sort = 28 where code = 'mode=time&live=10&expire=180' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 15 分钟 180 天', price = 10.00, price_min = 10.00, sort = 29 where code = 'mode=time&live=15&expire=180' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 30 分钟 180 天', price = 10.00, price_min = 10.00, sort = 30 where code = 'mode=time&live=30&expire=180' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 3 分钟 365 天', price = 10.00, price_min = 10.00, sort = 31 where code = 'mode=time&live=3&expire=365' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 5 分钟 365 天', price = 10.00, price_min = 10.00, sort = 32 where code = 'mode=time&live=5&expire=365' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 10 分钟 365 天', price = 10.00, price_min = 10.00, sort = 33 where code = 'mode=time&live=10&expire=365' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 15 分钟 365 天', price = 10.00, price_min = 10.00, sort = 34 where code = 'mode=time&live=15&expire=365' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 30 分钟 365 天', price = 10.00, price_min = 10.00, sort = 35 where code = 'mode=time&live=30&expire=365' and deleted_at is null;
|
||||
|
||||
-- --------------------------
|
||||
-- 长效动态
|
||||
-- --------------------------
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包量 1 小时', price = 10.00, price_min = 10.00, sort = 1 where code = 'mode=quota&live=60&expire=0' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包量 4 小时', price = 10.00, price_min = 10.00, sort = 2 where code = 'mode=quota&live=240&expire=0' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包量 8 小时', price = 10.00, price_min = 10.00, sort = 3 where code = 'mode=quota&live=480&expire=0' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包量 12 小时', price = 10.00, price_min = 10.00, sort = 4 where code = 'mode=quota&live=720&expire=0' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包量 24 小时', price = 10.00, price_min = 10.00, sort = 5 where code = 'mode=quota&live=1440&expire=0' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 1 小时 7 天', price = 10.00, price_min = 10.00, sort = 6 where code = 'mode=time&live=60&expire=7' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 4 小时 7 天', price = 10.00, price_min = 10.00, sort = 7 where code = 'mode=time&live=240&expire=7' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 8 小时 7 天', price = 10.00, price_min = 10.00, sort = 8 where code = 'mode=time&live=480&expire=7' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 12 小时 7 天', price = 10.00, price_min = 10.00, sort = 9 where code = 'mode=time&live=720&expire=7' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 24 小时 7 天', price = 10.00, price_min = 10.00, sort = 10 where code = 'mode=time&live=1440&expire=7' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 1 小时 15 天', price = 10.00, price_min = 10.00, sort = 11 where code = 'mode=time&live=60&expire=15' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 4 小时 15 天', price = 10.00, price_min = 10.00, sort = 12 where code = 'mode=time&live=240&expire=15' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 8 小时 15 天', price = 10.00, price_min = 10.00, sort = 13 where code = 'mode=time&live=480&expire=15' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 12 小时 15 天', price = 10.00, price_min = 10.00, sort = 14 where code = 'mode=time&live=720&expire=15' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 24 小时 15 天', price = 10.00, price_min = 10.00, sort = 15 where code = 'mode=time&live=1440&expire=15' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 1 小时 30 天', price = 10.00, price_min = 10.00, sort = 16 where code = 'mode=time&live=60&expire=30' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 4 小时 30 天', price = 10.00, price_min = 10.00, sort = 17 where code = 'mode=time&live=240&expire=30' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 8 小时 30 天', price = 10.00, price_min = 10.00, sort = 18 where code = 'mode=time&live=480&expire=30' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 12 小时 30 天', price = 10.00, price_min = 10.00, sort = 19 where code = 'mode=time&live=720&expire=30' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 24 小时 30 天', price = 10.00, price_min = 10.00, sort = 20 where code = 'mode=time&live=1440&expire=30' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 1 小时 90 天', price = 10.00, price_min = 10.00, sort = 21 where code = 'mode=time&live=60&expire=90' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 4 小时 90 天', price = 10.00, price_min = 10.00, sort = 22 where code = 'mode=time&live=240&expire=90' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 8 小时 90 天', price = 10.00, price_min = 10.00, sort = 23 where code = 'mode=time&live=480&expire=90' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 12 小时 90 天', price = 10.00, price_min = 10.00, sort = 24 where code = 'mode=time&live=720&expire=90' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 24 小时 90 天', price = 10.00, price_min = 10.00, sort = 25 where code = 'mode=time&live=1440&expire=90' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 1 小时 180 天', price = 10.00, price_min = 10.00, sort = 26 where code = 'mode=time&live=60&expire=180' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 4 小时 180 天', price = 10.00, price_min = 10.00, sort = 27 where code = 'mode=time&live=240&expire=180' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 8 小时 180 天', price = 10.00, price_min = 10.00, sort = 28 where code = 'mode=time&live=480&expire=180' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 12 小时 180 天', price = 10.00, price_min = 10.00, sort = 29 where code = 'mode=time&live=720&expire=180' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 24 小时 180 天', price = 10.00, price_min = 10.00, sort = 30 where code = 'mode=time&live=1440&expire=180' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 1 小时 365 天', price = 10.00, price_min = 10.00, sort = 31 where code = 'mode=time&live=60&expire=365' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 4 小时 365 天', price = 10.00, price_min = 10.00, sort = 32 where code = 'mode=time&live=240&expire=365' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 8 小时 365 天', price = 10.00, price_min = 10.00, sort = 33 where code = 'mode=time&live=480&expire=365' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 12 小时 365 天', price = 10.00, price_min = 10.00, sort = 34 where code = 'mode=time&live=720&expire=365' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 24 小时 365 天', price = 10.00, price_min = 10.00, sort = 35 where code = 'mode=time&live=1440&expire=365' and deleted_at is null;
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"platform/pkg/u"
|
||||
"platform/web/core"
|
||||
m "platform/web/models"
|
||||
q "platform/web/queries"
|
||||
@@ -50,9 +49,8 @@ func authUser(loginType PwdLoginType, username, password string) (user *m.User,
|
||||
}
|
||||
if user == nil {
|
||||
user = &m.User{
|
||||
Phone: username,
|
||||
Username: u.P(username),
|
||||
Status: m.UserStatusEnabled,
|
||||
Phone: username,
|
||||
Status: m.UserStatusEnabled,
|
||||
}
|
||||
}
|
||||
case PwdLoginByEmail:
|
||||
@@ -79,7 +77,7 @@ func authUser(loginType PwdLoginType, username, password string) (user *m.User,
|
||||
|
||||
func authUserBySms(tx *q.Query, username, code string) (*m.User, error) {
|
||||
// 验证验证码
|
||||
err := s.Verifier.VerifySms(context.Background(), username, code)
|
||||
err := s.Verifier.VerifySms(context.Background(), username, code, s.VerifierSmsPurposeLogin)
|
||||
if err != nil {
|
||||
return nil, core.NewBizErr("短信认证失败", err)
|
||||
}
|
||||
|
||||
@@ -537,10 +537,10 @@ func introspectUser(ctx *fiber.Ctx, authCtx *AuthCtx) error {
|
||||
|
||||
// 掩码敏感信息
|
||||
if profile.Phone != "" {
|
||||
profile.Phone = maskPhone(profile.Phone)
|
||||
profile.Phone = u.MaskPhone(profile.Phone)
|
||||
}
|
||||
if profile.IDNo != nil && *profile.IDNo != "" {
|
||||
profile.IDNo = u.P(maskIdNo(*profile.IDNo))
|
||||
profile.IDNo = u.P(u.MaskIdNo(*profile.IDNo))
|
||||
}
|
||||
|
||||
return ctx.JSON(struct {
|
||||
@@ -579,20 +579,6 @@ func introspectAdmin(ctx *fiber.Ctx, authCtx *AuthCtx) error {
|
||||
}{profile, list})
|
||||
}
|
||||
|
||||
func maskPhone(phone string) string {
|
||||
if len(phone) < 11 {
|
||||
return phone
|
||||
}
|
||||
return phone[:3] + "****" + phone[7:]
|
||||
}
|
||||
|
||||
func maskIdNo(idNo string) string {
|
||||
if len(idNo) < 18 {
|
||||
return idNo
|
||||
}
|
||||
return idNo[:3] + "*********" + idNo[14:]
|
||||
}
|
||||
|
||||
type CodeContext struct {
|
||||
UserID int32 `json:"user_id"`
|
||||
ClientID int32 `json:"client_id"`
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -12,9 +12,9 @@ type IModel interface {
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
ID int32 `json:"id" gorm:"column:id;primaryKey"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"`
|
||||
ID int32 `json:"id,omitzero" gorm:"column:id;primaryKey"`
|
||||
CreatedAt time.Time `json:"created_at,omitzero" gorm:"column:created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitzero" gorm:"column:updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"column:deleted_at"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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") // 读取交易列表
|
||||
|
||||
@@ -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
9
web/events/edges.go
Normal 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
280
web/globals/gost.go
Normal 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
107
web/globals/gost_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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
29
web/handlers/area.go
Normal 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
170
web/handlers/article.go
Normal 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
|
||||
}
|
||||
90
web/handlers/article_group.go
Normal file
90
web/handlers/article_group.go
Normal 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)
|
||||
}
|
||||
@@ -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 {
|
||||
// 检查权限
|
||||
@@ -30,17 +86,18 @@ func PageBalanceActivityByAdmin(c *fiber.Ctx) error {
|
||||
if req.UserPhone != nil {
|
||||
do = do.Where(q.User.As("User").Phone.Eq(*req.UserPhone))
|
||||
}
|
||||
if req.BillNo != nil {
|
||||
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,
|
||||
@@ -68,6 +125,7 @@ func PageBalanceActivityByAdmin(c *fiber.Ctx) error {
|
||||
type PageBalanceActivityByAdminReq struct {
|
||||
core.PageReq
|
||||
UserPhone *string `json:"user_phone,omitempty"`
|
||||
BillNo *string `json:"bill_no,omitempty"`
|
||||
CreatedAtStart *time.Time `json:"created_at_start,omitempty"`
|
||||
CreatedAtEnd *time.Time `json:"created_at_end,omitempty"`
|
||||
}
|
||||
@@ -88,13 +146,14 @@ func PageBalanceActivityOfUserByAdmin(c *fiber.Ctx) error {
|
||||
|
||||
// 构造查询条件
|
||||
do := q.BalanceActivity.Where(q.BalanceActivity.UserID.Eq(req.UserID))
|
||||
if req.BillNo != nil {
|
||||
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()))
|
||||
}
|
||||
|
||||
// 查询余额变动列表
|
||||
@@ -124,6 +183,7 @@ func PageBalanceActivityOfUserByAdmin(c *fiber.Ctx) error {
|
||||
type PageBalanceActivityOfUserByAdminReq struct {
|
||||
core.PageReq
|
||||
UserID int32 `json:"user_id" validate:"required"`
|
||||
BillNo *string `json:"bill_no,omitempty"`
|
||||
CreatedAtStart *time.Time `json:"created_at_start,omitempty"`
|
||||
CreatedAtEnd *time.Time `json:"created_at_end,omitempty"`
|
||||
}
|
||||
|
||||
@@ -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()).
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
329
web/handlers/coupon_user.go
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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("实名信息已清除")
|
||||
}
|
||||
|
||||
@@ -34,6 +34,15 @@ func AllProductByAdmin(c *fiber.Ctx) error {
|
||||
type AllProductsByAdminReq struct {
|
||||
}
|
||||
|
||||
func AllProduct(c *fiber.Ctx) error {
|
||||
infos, err := s.Product.AllProductSaleInfos()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(infos)
|
||||
}
|
||||
|
||||
func CreateProduct(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProductWrite)
|
||||
if err != nil {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -797,10 +828,7 @@ type CreateResourceReq struct {
|
||||
// ResourcePrice 套餐价格
|
||||
func ResourcePrice(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
_, err := auth.GetAuthCtx(c).PermitSecretClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ac := auth.GetAuthCtx(c)
|
||||
|
||||
// 解析请求参数
|
||||
var req = new(CreateResourceReq)
|
||||
@@ -809,7 +837,7 @@ func ResourcePrice(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// 获取套餐价格
|
||||
detail, err := req.TradeDetail(nil)
|
||||
detail, err := req.TradeDetail(ac.User)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -817,11 +845,13 @@ func ResourcePrice(c *fiber.Ctx) error {
|
||||
// 计算折扣
|
||||
return c.JSON(ResourcePriceResp{
|
||||
Price: detail.Amount.StringFixed(2),
|
||||
Discounted: detail.Actual.StringFixed(2),
|
||||
Discounted: detail.Discounted.StringFixed(2),
|
||||
Actual: detail.Actual.StringFixed(2),
|
||||
})
|
||||
}
|
||||
|
||||
type ResourcePriceResp struct {
|
||||
Price string `json:"price"`
|
||||
Discounted string `json:"discounted_price"`
|
||||
Discounted string `json:"discounted"`
|
||||
Actual string `json:"actual"`
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -8,10 +8,12 @@ import (
|
||||
m "platform/web/models"
|
||||
q "platform/web/queries"
|
||||
s "platform/web/services"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/shopspring/decimal"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gen/field"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -64,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.
|
||||
@@ -101,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"`
|
||||
}
|
||||
|
||||
// 管理员获取单个用户
|
||||
@@ -259,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
|
||||
}
|
||||
@@ -273,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(
|
||||
@@ -282,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("用户已绑定管理员")
|
||||
}
|
||||
|
||||
@@ -305,30 +315,45 @@ func UpdateUser(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
_, err = q.User.
|
||||
do := make([]field.AssignExpr, 0)
|
||||
if req.Username != nil && *req.Username != "" {
|
||||
do = append(do, q.User.Username.Value(*req.Username))
|
||||
}
|
||||
if req.Email != nil {
|
||||
if *req.Email == "" {
|
||||
do = append(do, q.User.Email.Null())
|
||||
} else {
|
||||
do = append(do, q.User.Email.Value(*req.Email))
|
||||
}
|
||||
}
|
||||
if req.ContactQQ != nil {
|
||||
do = append(do, q.User.ContactQQ.Value(*req.ContactQQ))
|
||||
}
|
||||
if req.ContactWechat != nil {
|
||||
do = append(do, q.User.ContactWechat.Value(*req.ContactWechat))
|
||||
}
|
||||
r, err := q.User.
|
||||
Where(q.User.ID.Eq(authCtx.User.ID)).
|
||||
Updates(m.User{
|
||||
Username: &req.Username,
|
||||
Email: &req.Email,
|
||||
ContactQQ: &req.ContactQQ,
|
||||
ContactWechat: &req.ContactWechat,
|
||||
})
|
||||
UpdateSimple(do...)
|
||||
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||
return core.NewBizErr("用户名或邮箱已被占用")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if r.RowsAffected == 0 {
|
||||
return core.NewBizErr("用户状态已过期")
|
||||
}
|
||||
|
||||
// 返回结果
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
}
|
||||
|
||||
type UpdateUserReq struct {
|
||||
Username string `json:"username" validate:"omitempty,min=3,max=20"`
|
||||
Email string `json:"email" validate:"omitempty,email"`
|
||||
ContactQQ string `json:"contact_qq" validate:"omitempty,qq"`
|
||||
ContactWechat string `json:"contact_wechat" validate:"omitempty,wechat"`
|
||||
Username *string `json:"username" validate:"omitempty,min=3,max=20"`
|
||||
Email *string `json:"email" validate:"omitempty,email"`
|
||||
ContactQQ *string `json:"contact_qq" validate:"omitempty,qq"`
|
||||
ContactWechat *string `json:"contact_wechat" validate:"omitempty,wechat"`
|
||||
}
|
||||
|
||||
// 更新账号信息
|
||||
@@ -346,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,
|
||||
@@ -355,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)
|
||||
@@ -379,16 +407,14 @@ func UpdatePassword(c *fiber.Ctx) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 验证手机号
|
||||
if req.Phone != authCtx.User.Phone {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "手机号码不正确")
|
||||
}
|
||||
|
||||
// 验证手机令牌
|
||||
if req.Code == "" {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "手机号码和验证码不能为空")
|
||||
return fiber.NewError(fiber.StatusBadRequest, "验证码不能为空")
|
||||
}
|
||||
err = s.Verifier.VerifySms(c.Context(), authCtx.User.Phone, req.Code, s.VerifierSmsPurposePassword)
|
||||
if errors.Is(err, s.ErrVerifierServiceInvalid) {
|
||||
return core.NewBizErr(s.ErrVerifierServiceInvalid.Error())
|
||||
}
|
||||
err = s.Verifier.VerifySms(c.Context(), req.Phone, req.Code)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -399,19 +425,21 @@ 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)
|
||||
}
|
||||
|
||||
type UpdatePasswordReq struct {
|
||||
Phone string `json:"phone"`
|
||||
Code string `json:"code"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"platform/pkg/env"
|
||||
"platform/web/auth"
|
||||
"platform/web/services"
|
||||
s "platform/web/services"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
@@ -13,12 +14,11 @@ import (
|
||||
)
|
||||
|
||||
type VerifierReq struct {
|
||||
Purpose services.VerifierSmsPurpose `json:"purpose"`
|
||||
Phone string `json:"phone"`
|
||||
Purpose s.VerifierSmsPurpose `json:"purpose"`
|
||||
Phone string `json:"phone"`
|
||||
}
|
||||
|
||||
func SmsCode(c *fiber.Ctx) error {
|
||||
|
||||
func SendSmsCode(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitOfficialClient()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -38,9 +38,9 @@ func SmsCode(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// 发送身份验证码
|
||||
err = services.Verifier.SendSms(c.Context(), req.Phone, req.Purpose)
|
||||
err = s.Verifier.SendSms(c.Context(), req.Phone, req.Purpose)
|
||||
if err != nil {
|
||||
var sErr services.VerifierServiceSendLimitErr
|
||||
var sErr s.VerifierServiceSendLimitErr
|
||||
if errors.As(err, &sErr) {
|
||||
return fiber.NewError(fiber.StatusTooManyRequests, strconv.Itoa(int(sErr)))
|
||||
}
|
||||
@@ -51,6 +51,23 @@ func SmsCode(c *fiber.Ctx) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func SendSmsCodeForPassword(c *fiber.Ctx) error {
|
||||
ac, err := auth.GetAuthCtx(c).PermitUser()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.Verifier.SendSms(c.Context(), ac.User.Phone, s.VerifierSmsPurposePassword); err != nil {
|
||||
var sErr s.VerifierServiceSendLimitErr
|
||||
if errors.As(err, &sErr) {
|
||||
return fiber.NewError(fiber.StatusTooManyRequests, strconv.Itoa(int(sErr)))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DebugGetSmsCode(c *fiber.Ctx) error {
|
||||
if env.RunMode != env.RunModeDev {
|
||||
return fiber.NewError(fiber.StatusForbidden, "not allowed")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
20
web/models/area.go
Normal 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
23
web/models/article.go
Normal 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 // 正常
|
||||
)
|
||||
20
web/models/article_group.go
Normal file
20
web/models/article_group.go
Normal 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 // 正常
|
||||
)
|
||||
@@ -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 // 已禁用
|
||||
)
|
||||
|
||||
@@ -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 // 移动
|
||||
|
||||
@@ -12,6 +12,8 @@ type Product struct {
|
||||
Description *string `json:"description,omitempty" gorm:"column:description"` // 产品描述
|
||||
Sort int32 `json:"sort" gorm:"column:sort"` // 排序
|
||||
Status ProductStatus `json:"status" gorm:"column:status"` // 产品状态:0-禁用,1-正常
|
||||
|
||||
Skus []*ProductSku `json:"skus,omitempty" gorm:"foreignKey:ProductID"` // 产品包含的SKU列表
|
||||
}
|
||||
|
||||
// ProductStatus 产品状态枚举
|
||||
|
||||
@@ -16,6 +16,8 @@ type ProductSku struct {
|
||||
Price decimal.Decimal `json:"price" gorm:"column:price"` // 定价
|
||||
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"`
|
||||
|
||||
@@ -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 代理服务状态枚举
|
||||
|
||||
@@ -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
443
web/queries/area.gen.go
Normal 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
442
web/queries/article.gen.go
Normal 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
|
||||
}
|
||||
347
web/queries/article_group.gen.go
Normal file
347
web/queries/article_group.gen.go
Normal 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
|
||||
}
|
||||
@@ -145,6 +145,9 @@ func newBalanceActivity(db *gorm.DB, opts ...gen.DOOption) balanceActivity {
|
||||
field.RelationField
|
||||
Product struct {
|
||||
field.RelationField
|
||||
Skus struct {
|
||||
field.RelationField
|
||||
}
|
||||
}
|
||||
Discount struct {
|
||||
field.RelationField
|
||||
@@ -173,6 +176,9 @@ func newBalanceActivity(db *gorm.DB, opts ...gen.DOOption) balanceActivity {
|
||||
field.RelationField
|
||||
Product struct {
|
||||
field.RelationField
|
||||
Skus struct {
|
||||
field.RelationField
|
||||
}
|
||||
}
|
||||
Discount struct {
|
||||
field.RelationField
|
||||
@@ -184,6 +190,9 @@ func newBalanceActivity(db *gorm.DB, opts ...gen.DOOption) balanceActivity {
|
||||
field.RelationField
|
||||
Product struct {
|
||||
field.RelationField
|
||||
Skus struct {
|
||||
field.RelationField
|
||||
}
|
||||
}
|
||||
Discount struct {
|
||||
field.RelationField
|
||||
@@ -192,8 +201,16 @@ func newBalanceActivity(db *gorm.DB, opts ...gen.DOOption) balanceActivity {
|
||||
RelationField: field.NewRelation("Bill.Resource.Short.Sku", "models.ProductSku"),
|
||||
Product: struct {
|
||||
field.RelationField
|
||||
Skus struct {
|
||||
field.RelationField
|
||||
}
|
||||
}{
|
||||
RelationField: field.NewRelation("Bill.Resource.Short.Sku.Product", "models.Product"),
|
||||
Skus: struct {
|
||||
field.RelationField
|
||||
}{
|
||||
RelationField: field.NewRelation("Bill.Resource.Short.Sku.Product.Skus", "models.ProductSku"),
|
||||
},
|
||||
},
|
||||
Discount: struct {
|
||||
field.RelationField
|
||||
@@ -482,6 +499,9 @@ type balanceActivityBelongsToBill struct {
|
||||
field.RelationField
|
||||
Product struct {
|
||||
field.RelationField
|
||||
Skus struct {
|
||||
field.RelationField
|
||||
}
|
||||
}
|
||||
Discount struct {
|
||||
field.RelationField
|
||||
|
||||
@@ -143,6 +143,9 @@ func newBill(db *gorm.DB, opts ...gen.DOOption) bill {
|
||||
field.RelationField
|
||||
Product struct {
|
||||
field.RelationField
|
||||
Skus struct {
|
||||
field.RelationField
|
||||
}
|
||||
}
|
||||
Discount struct {
|
||||
field.RelationField
|
||||
@@ -154,6 +157,9 @@ func newBill(db *gorm.DB, opts ...gen.DOOption) bill {
|
||||
field.RelationField
|
||||
Product struct {
|
||||
field.RelationField
|
||||
Skus struct {
|
||||
field.RelationField
|
||||
}
|
||||
}
|
||||
Discount struct {
|
||||
field.RelationField
|
||||
@@ -162,8 +168,16 @@ func newBill(db *gorm.DB, opts ...gen.DOOption) bill {
|
||||
RelationField: field.NewRelation("Resource.Short.Sku", "models.ProductSku"),
|
||||
Product: struct {
|
||||
field.RelationField
|
||||
Skus struct {
|
||||
field.RelationField
|
||||
}
|
||||
}{
|
||||
RelationField: field.NewRelation("Resource.Short.Sku.Product", "models.Product"),
|
||||
Skus: struct {
|
||||
field.RelationField
|
||||
}{
|
||||
RelationField: field.NewRelation("Resource.Short.Sku.Product.Skus", "models.ProductSku"),
|
||||
},
|
||||
},
|
||||
Discount: struct {
|
||||
field.RelationField
|
||||
@@ -540,6 +554,9 @@ type billBelongsToResource struct {
|
||||
field.RelationField
|
||||
Product struct {
|
||||
field.RelationField
|
||||
Skus struct {
|
||||
field.RelationField
|
||||
}
|
||||
}
|
||||
Discount struct {
|
||||
field.RelationField
|
||||
|
||||
@@ -138,6 +138,9 @@ func newChannel(db *gorm.DB, opts ...gen.DOOption) channel {
|
||||
field.RelationField
|
||||
Product struct {
|
||||
field.RelationField
|
||||
Skus struct {
|
||||
field.RelationField
|
||||
}
|
||||
}
|
||||
Discount struct {
|
||||
field.RelationField
|
||||
@@ -149,6 +152,9 @@ func newChannel(db *gorm.DB, opts ...gen.DOOption) channel {
|
||||
field.RelationField
|
||||
Product struct {
|
||||
field.RelationField
|
||||
Skus struct {
|
||||
field.RelationField
|
||||
}
|
||||
}
|
||||
Discount struct {
|
||||
field.RelationField
|
||||
@@ -157,8 +163,16 @@ func newChannel(db *gorm.DB, opts ...gen.DOOption) channel {
|
||||
RelationField: field.NewRelation("Resource.Short.Sku", "models.ProductSku"),
|
||||
Product: struct {
|
||||
field.RelationField
|
||||
Skus struct {
|
||||
field.RelationField
|
||||
}
|
||||
}{
|
||||
RelationField: field.NewRelation("Resource.Short.Sku.Product", "models.Product"),
|
||||
Skus: struct {
|
||||
field.RelationField
|
||||
}{
|
||||
RelationField: field.NewRelation("Resource.Short.Sku.Product.Skus", "models.ProductSku"),
|
||||
},
|
||||
},
|
||||
Discount: struct {
|
||||
field.RelationField
|
||||
@@ -204,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"),
|
||||
@@ -224,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"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -490,6 +529,9 @@ type channelBelongsToResource struct {
|
||||
field.RelationField
|
||||
Product struct {
|
||||
field.RelationField
|
||||
Skus struct {
|
||||
field.RelationField
|
||||
}
|
||||
}
|
||||
Discount struct {
|
||||
field.RelationField
|
||||
@@ -600,6 +642,12 @@ type channelBelongsToProxy struct {
|
||||
}
|
||||
Edge struct {
|
||||
field.RelationField
|
||||
Area struct {
|
||||
field.RelationField
|
||||
Parent struct {
|
||||
field.RelationField
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -128,6 +128,9 @@ func newLogsUserUsage(db *gorm.DB, opts ...gen.DOOption) logsUserUsage {
|
||||
field.RelationField
|
||||
Product struct {
|
||||
field.RelationField
|
||||
Skus struct {
|
||||
field.RelationField
|
||||
}
|
||||
}
|
||||
Discount struct {
|
||||
field.RelationField
|
||||
@@ -139,6 +142,9 @@ func newLogsUserUsage(db *gorm.DB, opts ...gen.DOOption) logsUserUsage {
|
||||
field.RelationField
|
||||
Product struct {
|
||||
field.RelationField
|
||||
Skus struct {
|
||||
field.RelationField
|
||||
}
|
||||
}
|
||||
Discount struct {
|
||||
field.RelationField
|
||||
@@ -147,8 +153,16 @@ func newLogsUserUsage(db *gorm.DB, opts ...gen.DOOption) logsUserUsage {
|
||||
RelationField: field.NewRelation("Resource.Short.Sku", "models.ProductSku"),
|
||||
Product: struct {
|
||||
field.RelationField
|
||||
Skus struct {
|
||||
field.RelationField
|
||||
}
|
||||
}{
|
||||
RelationField: field.NewRelation("Resource.Short.Sku.Product", "models.Product"),
|
||||
Skus: struct {
|
||||
field.RelationField
|
||||
}{
|
||||
RelationField: field.NewRelation("Resource.Short.Sku.Product.Skus", "models.ProductSku"),
|
||||
},
|
||||
},
|
||||
Discount: struct {
|
||||
field.RelationField
|
||||
@@ -391,6 +405,9 @@ type logsUserUsageBelongsToResource struct {
|
||||
field.RelationField
|
||||
Product struct {
|
||||
field.RelationField
|
||||
Skus struct {
|
||||
field.RelationField
|
||||
}
|
||||
}
|
||||
Discount struct {
|
||||
field.RelationField
|
||||
|
||||
@@ -36,6 +36,29 @@ func newProduct(db *gorm.DB, opts ...gen.DOOption) product {
|
||||
_product.Description = field.NewString(tableName, "description")
|
||||
_product.Sort = field.NewInt32(tableName, "sort")
|
||||
_product.Status = field.NewInt(tableName, "status")
|
||||
_product.Skus = productHasManySkus{
|
||||
db: db.Session(&gorm.Session{}),
|
||||
|
||||
RelationField: field.NewRelation("Skus", "models.ProductSku"),
|
||||
Product: struct {
|
||||
field.RelationField
|
||||
Skus struct {
|
||||
field.RelationField
|
||||
}
|
||||
}{
|
||||
RelationField: field.NewRelation("Skus.Product", "models.Product"),
|
||||
Skus: struct {
|
||||
field.RelationField
|
||||
}{
|
||||
RelationField: field.NewRelation("Skus.Product.Skus", "models.ProductSku"),
|
||||
},
|
||||
},
|
||||
Discount: struct {
|
||||
field.RelationField
|
||||
}{
|
||||
RelationField: field.NewRelation("Skus.Discount", "models.ProductDiscount"),
|
||||
},
|
||||
}
|
||||
|
||||
_product.fillFieldMap()
|
||||
|
||||
@@ -55,6 +78,7 @@ type product struct {
|
||||
Description field.String
|
||||
Sort field.Int32
|
||||
Status field.Int
|
||||
Skus productHasManySkus
|
||||
|
||||
fieldMap map[string]field.Expr
|
||||
}
|
||||
@@ -96,7 +120,7 @@ func (p *product) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
|
||||
}
|
||||
|
||||
func (p *product) fillFieldMap() {
|
||||
p.fieldMap = make(map[string]field.Expr, 9)
|
||||
p.fieldMap = make(map[string]field.Expr, 10)
|
||||
p.fieldMap["id"] = p.ID
|
||||
p.fieldMap["created_at"] = p.CreatedAt
|
||||
p.fieldMap["updated_at"] = p.UpdatedAt
|
||||
@@ -106,18 +130,113 @@ func (p *product) fillFieldMap() {
|
||||
p.fieldMap["description"] = p.Description
|
||||
p.fieldMap["sort"] = p.Sort
|
||||
p.fieldMap["status"] = p.Status
|
||||
|
||||
}
|
||||
|
||||
func (p product) clone(db *gorm.DB) product {
|
||||
p.productDo.ReplaceConnPool(db.Statement.ConnPool)
|
||||
p.Skus.db = db.Session(&gorm.Session{Initialized: true})
|
||||
p.Skus.db.Statement.ConnPool = db.Statement.ConnPool
|
||||
return p
|
||||
}
|
||||
|
||||
func (p product) replaceDB(db *gorm.DB) product {
|
||||
p.productDo.ReplaceDB(db)
|
||||
p.Skus.db = db.Session(&gorm.Session{})
|
||||
return p
|
||||
}
|
||||
|
||||
type productHasManySkus struct {
|
||||
db *gorm.DB
|
||||
|
||||
field.RelationField
|
||||
|
||||
Product struct {
|
||||
field.RelationField
|
||||
Skus struct {
|
||||
field.RelationField
|
||||
}
|
||||
}
|
||||
Discount struct {
|
||||
field.RelationField
|
||||
}
|
||||
}
|
||||
|
||||
func (a productHasManySkus) Where(conds ...field.Expr) *productHasManySkus {
|
||||
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 productHasManySkus) WithContext(ctx context.Context) *productHasManySkus {
|
||||
a.db = a.db.WithContext(ctx)
|
||||
return &a
|
||||
}
|
||||
|
||||
func (a productHasManySkus) Session(session *gorm.Session) *productHasManySkus {
|
||||
a.db = a.db.Session(session)
|
||||
return &a
|
||||
}
|
||||
|
||||
func (a productHasManySkus) Model(m *models.Product) *productHasManySkusTx {
|
||||
return &productHasManySkusTx{a.db.Model(m).Association(a.Name())}
|
||||
}
|
||||
|
||||
func (a productHasManySkus) Unscoped() *productHasManySkus {
|
||||
a.db = a.db.Unscoped()
|
||||
return &a
|
||||
}
|
||||
|
||||
type productHasManySkusTx struct{ tx *gorm.Association }
|
||||
|
||||
func (a productHasManySkusTx) Find() (result []*models.ProductSku, err error) {
|
||||
return result, a.tx.Find(&result)
|
||||
}
|
||||
|
||||
func (a productHasManySkusTx) Append(values ...*models.ProductSku) (err error) {
|
||||
targetValues := make([]interface{}, len(values))
|
||||
for i, v := range values {
|
||||
targetValues[i] = v
|
||||
}
|
||||
return a.tx.Append(targetValues...)
|
||||
}
|
||||
|
||||
func (a productHasManySkusTx) Replace(values ...*models.ProductSku) (err error) {
|
||||
targetValues := make([]interface{}, len(values))
|
||||
for i, v := range values {
|
||||
targetValues[i] = v
|
||||
}
|
||||
return a.tx.Replace(targetValues...)
|
||||
}
|
||||
|
||||
func (a productHasManySkusTx) Delete(values ...*models.ProductSku) (err error) {
|
||||
targetValues := make([]interface{}, len(values))
|
||||
for i, v := range values {
|
||||
targetValues[i] = v
|
||||
}
|
||||
return a.tx.Delete(targetValues...)
|
||||
}
|
||||
|
||||
func (a productHasManySkusTx) Clear() error {
|
||||
return a.tx.Clear()
|
||||
}
|
||||
|
||||
func (a productHasManySkusTx) Count() int64 {
|
||||
return a.tx.Count()
|
||||
}
|
||||
|
||||
func (a productHasManySkusTx) Unscoped() *productHasManySkusTx {
|
||||
a.tx = a.tx.Unscoped()
|
||||
return &a
|
||||
}
|
||||
|
||||
type productDo struct{ gen.DO }
|
||||
|
||||
func (p productDo) Debug() *productDo {
|
||||
|
||||
@@ -38,10 +38,33 @@ func newProductSku(db *gorm.DB, opts ...gen.DOOption) productSku {
|
||||
_productSku.Price = field.NewField(tableName, "price")
|
||||
_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{}),
|
||||
|
||||
RelationField: field.NewRelation("Product", "models.Product"),
|
||||
Skus: struct {
|
||||
field.RelationField
|
||||
Product struct {
|
||||
field.RelationField
|
||||
}
|
||||
Discount struct {
|
||||
field.RelationField
|
||||
}
|
||||
}{
|
||||
RelationField: field.NewRelation("Product.Skus", "models.ProductSku"),
|
||||
Product: struct {
|
||||
field.RelationField
|
||||
}{
|
||||
RelationField: field.NewRelation("Product.Skus.Product", "models.Product"),
|
||||
},
|
||||
Discount: struct {
|
||||
field.RelationField
|
||||
}{
|
||||
RelationField: field.NewRelation("Product.Skus.Discount", "models.ProductDiscount"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_productSku.Discount = productSkuBelongsToDiscount{
|
||||
@@ -70,6 +93,8 @@ type productSku struct {
|
||||
Price field.Field
|
||||
PriceMin field.Field
|
||||
Status field.Int32
|
||||
Sort field.Int32
|
||||
CountMin field.Int32
|
||||
Product productSkuBelongsToProduct
|
||||
|
||||
Discount productSkuBelongsToDiscount
|
||||
@@ -100,6 +125,8 @@ func (p *productSku) updateTableName(table string) *productSku {
|
||||
p.Price = field.NewField(table, "price")
|
||||
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()
|
||||
|
||||
@@ -116,7 +143,7 @@ func (p *productSku) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
|
||||
}
|
||||
|
||||
func (p *productSku) fillFieldMap() {
|
||||
p.fieldMap = make(map[string]field.Expr, 13)
|
||||
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
|
||||
@@ -128,6 +155,8 @@ func (p *productSku) fillFieldMap() {
|
||||
p.fieldMap["price"] = p.Price
|
||||
p.fieldMap["price_min"] = p.PriceMin
|
||||
p.fieldMap["status"] = p.Status
|
||||
p.fieldMap["sort"] = p.Sort
|
||||
p.fieldMap["count_min"] = p.CountMin
|
||||
|
||||
}
|
||||
|
||||
@@ -151,6 +180,16 @@ type productSkuBelongsToProduct struct {
|
||||
db *gorm.DB
|
||||
|
||||
field.RelationField
|
||||
|
||||
Skus struct {
|
||||
field.RelationField
|
||||
Product struct {
|
||||
field.RelationField
|
||||
}
|
||||
Discount struct {
|
||||
field.RelationField
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a productSkuBelongsToProduct) Where(conds ...field.Expr) *productSkuBelongsToProduct {
|
||||
|
||||
@@ -115,8 +115,16 @@ func newProductSkuUser(db *gorm.DB, opts ...gen.DOOption) productSkuUser {
|
||||
RelationField: field.NewRelation("ProductSku", "models.ProductSku"),
|
||||
Product: struct {
|
||||
field.RelationField
|
||||
Skus struct {
|
||||
field.RelationField
|
||||
}
|
||||
}{
|
||||
RelationField: field.NewRelation("ProductSku.Product", "models.Product"),
|
||||
Skus: struct {
|
||||
field.RelationField
|
||||
}{
|
||||
RelationField: field.NewRelation("ProductSku.Product.Skus", "models.ProductSku"),
|
||||
},
|
||||
},
|
||||
Discount: struct {
|
||||
field.RelationField
|
||||
@@ -331,6 +339,9 @@ type productSkuUserBelongsToProductSku struct {
|
||||
|
||||
Product struct {
|
||||
field.RelationField
|
||||
Skus struct {
|
||||
field.RelationField
|
||||
}
|
||||
}
|
||||
Discount struct {
|
||||
field.RelationField
|
||||
|
||||
@@ -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")
|
||||
@@ -153,6 +154,9 @@ func newProxy(db *gorm.DB, opts ...gen.DOOption) proxy {
|
||||
field.RelationField
|
||||
Product struct {
|
||||
field.RelationField
|
||||
Skus struct {
|
||||
field.RelationField
|
||||
}
|
||||
}
|
||||
Discount struct {
|
||||
field.RelationField
|
||||
@@ -181,6 +185,9 @@ func newProxy(db *gorm.DB, opts ...gen.DOOption) proxy {
|
||||
field.RelationField
|
||||
Product struct {
|
||||
field.RelationField
|
||||
Skus struct {
|
||||
field.RelationField
|
||||
}
|
||||
}
|
||||
Discount struct {
|
||||
field.RelationField
|
||||
@@ -192,6 +199,9 @@ func newProxy(db *gorm.DB, opts ...gen.DOOption) proxy {
|
||||
field.RelationField
|
||||
Product struct {
|
||||
field.RelationField
|
||||
Skus struct {
|
||||
field.RelationField
|
||||
}
|
||||
}
|
||||
Discount struct {
|
||||
field.RelationField
|
||||
@@ -200,8 +210,16 @@ func newProxy(db *gorm.DB, opts ...gen.DOOption) proxy {
|
||||
RelationField: field.NewRelation("Channels.Resource.Short.Sku", "models.ProductSku"),
|
||||
Product: struct {
|
||||
field.RelationField
|
||||
Skus struct {
|
||||
field.RelationField
|
||||
}
|
||||
}{
|
||||
RelationField: field.NewRelation("Channels.Resource.Short.Sku.Product", "models.Product"),
|
||||
Skus: struct {
|
||||
field.RelationField
|
||||
}{
|
||||
RelationField: field.NewRelation("Channels.Resource.Short.Sku.Product.Skus", "models.ProductSku"),
|
||||
},
|
||||
},
|
||||
Discount: struct {
|
||||
field.RelationField
|
||||
@@ -244,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"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -266,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
|
||||
@@ -295,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")
|
||||
@@ -315,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
|
||||
@@ -324,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
|
||||
@@ -387,6 +427,9 @@ type proxyHasManyChannels struct {
|
||||
field.RelationField
|
||||
Product struct {
|
||||
field.RelationField
|
||||
Skus struct {
|
||||
field.RelationField
|
||||
}
|
||||
}
|
||||
Discount struct {
|
||||
field.RelationField
|
||||
@@ -411,6 +454,12 @@ type proxyHasManyChannels struct {
|
||||
}
|
||||
Edge struct {
|
||||
field.RelationField
|
||||
Area struct {
|
||||
field.RelationField
|
||||
Parent struct {
|
||||
field.RelationField
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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{}),
|
||||
|
||||
@@ -44,6 +45,9 @@ func newResource(db *gorm.DB, opts ...gen.DOOption) resource {
|
||||
field.RelationField
|
||||
Product struct {
|
||||
field.RelationField
|
||||
Skus struct {
|
||||
field.RelationField
|
||||
}
|
||||
}
|
||||
Discount struct {
|
||||
field.RelationField
|
||||
@@ -52,8 +56,16 @@ func newResource(db *gorm.DB, opts ...gen.DOOption) resource {
|
||||
RelationField: field.NewRelation("Short.Sku", "models.ProductSku"),
|
||||
Product: struct {
|
||||
field.RelationField
|
||||
Skus struct {
|
||||
field.RelationField
|
||||
}
|
||||
}{
|
||||
RelationField: field.NewRelation("Short.Sku.Product", "models.Product"),
|
||||
Skus: struct {
|
||||
field.RelationField
|
||||
}{
|
||||
RelationField: field.NewRelation("Short.Sku.Product.Skus", "models.ProductSku"),
|
||||
},
|
||||
},
|
||||
Discount: struct {
|
||||
field.RelationField
|
||||
@@ -174,6 +186,7 @@ type resource struct {
|
||||
Active field.Bool
|
||||
Type field.Int
|
||||
Code field.String
|
||||
CheckIP field.Bool
|
||||
Short resourceHasOneShort
|
||||
|
||||
Long resourceHasOneLong
|
||||
@@ -206,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()
|
||||
|
||||
@@ -222,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
|
||||
@@ -232,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
|
||||
|
||||
}
|
||||
|
||||
@@ -266,6 +281,9 @@ type resourceHasOneShort struct {
|
||||
field.RelationField
|
||||
Product struct {
|
||||
field.RelationField
|
||||
Skus struct {
|
||||
field.RelationField
|
||||
}
|
||||
}
|
||||
Discount struct {
|
||||
field.RelationField
|
||||
|
||||
@@ -43,8 +43,16 @@ func newResourceLong(db *gorm.DB, opts ...gen.DOOption) resourceLong {
|
||||
RelationField: field.NewRelation("Sku", "models.ProductSku"),
|
||||
Product: struct {
|
||||
field.RelationField
|
||||
Skus struct {
|
||||
field.RelationField
|
||||
}
|
||||
}{
|
||||
RelationField: field.NewRelation("Sku.Product", "models.Product"),
|
||||
Skus: struct {
|
||||
field.RelationField
|
||||
}{
|
||||
RelationField: field.NewRelation("Sku.Product.Skus", "models.ProductSku"),
|
||||
},
|
||||
},
|
||||
Discount: struct {
|
||||
field.RelationField
|
||||
@@ -149,6 +157,9 @@ type resourceLongHasOneSku struct {
|
||||
|
||||
Product struct {
|
||||
field.RelationField
|
||||
Skus struct {
|
||||
field.RelationField
|
||||
}
|
||||
}
|
||||
Discount struct {
|
||||
field.RelationField
|
||||
|
||||
@@ -43,8 +43,16 @@ func newResourceShort(db *gorm.DB, opts ...gen.DOOption) resourceShort {
|
||||
RelationField: field.NewRelation("Sku", "models.ProductSku"),
|
||||
Product: struct {
|
||||
field.RelationField
|
||||
Skus struct {
|
||||
field.RelationField
|
||||
}
|
||||
}{
|
||||
RelationField: field.NewRelation("Sku.Product", "models.Product"),
|
||||
Skus: struct {
|
||||
field.RelationField
|
||||
}{
|
||||
RelationField: field.NewRelation("Sku.Product.Skus", "models.ProductSku"),
|
||||
},
|
||||
},
|
||||
Discount: struct {
|
||||
field.RelationField
|
||||
@@ -149,6 +157,9 @@ type resourceShortHasOneSku struct {
|
||||
|
||||
Product struct {
|
||||
field.RelationField
|
||||
Skus struct {
|
||||
field.RelationField
|
||||
}
|
||||
}
|
||||
Discount struct {
|
||||
field.RelationField
|
||||
|
||||
155
web/routes.go
155
web/routes.go
@@ -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,56 +132,39 @@ 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)
|
||||
}
|
||||
|
||||
// 客户端接口路由
|
||||
func clientRouter(api fiber.Router) {
|
||||
client := api
|
||||
|
||||
// 验证短信令牌
|
||||
client.Post("/verify/sms", handlers.SmsCode)
|
||||
|
||||
// 套餐定价查询
|
||||
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)
|
||||
// 认证
|
||||
verify := api.Group("/verify")
|
||||
verify.Post("/sms/password", handlers.SendSmsCodeForPassword)
|
||||
}
|
||||
|
||||
// 管理员接口路由
|
||||
@@ -187,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")
|
||||
@@ -235,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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
112
web/services/area.go
Normal 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
389
web/services/article.go
Normal 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
|
||||
}
|
||||
128
web/services/article_group.go
Normal file
128
web/services/article_group.go
Normal 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
|
||||
}
|
||||
144
web/services/article_upload_test.go
Normal file
144
web/services/article_upload_test.go
Normal 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="
|
||||
@@ -16,7 +16,7 @@ func (s *billService) CreateForBalance(q *q.Query, uid, tradeId int32, detail *T
|
||||
TradeID: &tradeId,
|
||||
Type: m.BillTypeRecharge,
|
||||
Info: &detail.Subject,
|
||||
Amount: detail.Amount,
|
||||
Amount: detail.Discounted,
|
||||
Actual: detail.Actual,
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ func (s *billService) CreateForResource(q *q.Query, uid, resourceId int32, trade
|
||||
CouponUserID: detail.CouponUserId,
|
||||
Type: m.BillTypeConsume,
|
||||
Info: &detail.Subject,
|
||||
Amount: detail.Amount,
|
||||
Amount: detail.Discounted,
|
||||
Actual: detail.Actual,
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
@@ -94,7 +513,7 @@ func findResource(resourceId int32, now time.Time) (*ResourceView, error) {
|
||||
var sub = resource.Short
|
||||
info.ShortId = &sub.ID
|
||||
info.ExpireAt = sub.ExpireAt
|
||||
info.Live = time.Duration(sub.Live) * time.Second
|
||||
info.Live = time.Duration(sub.Live) * time.Minute
|
||||
info.Mode = sub.Type
|
||||
info.Quota = sub.Quota
|
||||
info.Used = sub.Used
|
||||
@@ -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("没有可用的节点")
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
211
web/services/channel_gost.go
Normal file
211
web/services/channel_gost.go
Normal 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)
|
||||
}
|
||||
@@ -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
255
web/services/coupon_user.go
Normal 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
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -20,6 +20,62 @@ func (s *productService) AllProducts() ([]*m.Product, error) {
|
||||
Find()
|
||||
}
|
||||
|
||||
func (s *productService) AllProductSaleInfos() ([]*m.Product, error) {
|
||||
products, err := q.Product.
|
||||
Select(
|
||||
q.Product.ID,
|
||||
q.Product.Code,
|
||||
q.Product.Name,
|
||||
q.Product.Description,
|
||||
q.Product.Sort,
|
||||
).
|
||||
Where(
|
||||
q.Product.Status.Eq(int(m.ProductStatusEnabled)),
|
||||
).
|
||||
Order(q.Product.Sort).
|
||||
Find()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pids := make([]int32, len(products))
|
||||
for i, p := range products {
|
||||
pids[i] = p.ID
|
||||
}
|
||||
|
||||
skus, err := q.ProductSku.
|
||||
Select(
|
||||
q.ProductSku.ID,
|
||||
q.ProductSku.ProductID,
|
||||
q.ProductSku.Name,
|
||||
q.ProductSku.Code,
|
||||
q.ProductSku.Price,
|
||||
q.ProductSku.CountMin,
|
||||
).
|
||||
Where(
|
||||
q.ProductSku.ProductID.In(pids...),
|
||||
q.ProductSku.Status.Eq(int32(m.SkuStatusEnabled)),
|
||||
).
|
||||
Order(q.ProductSku.Sort).
|
||||
Find()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pmap := make(map[int32]*m.Product, len(products))
|
||||
for _, p := range products {
|
||||
pmap[p.ID] = p
|
||||
p.Skus = make([]*m.ProductSku, 0)
|
||||
}
|
||||
for _, s := range skus {
|
||||
if p, ok := pmap[s.ProductID]; ok {
|
||||
p.Skus = append(p.Skus, s)
|
||||
}
|
||||
}
|
||||
|
||||
return products, nil
|
||||
}
|
||||
|
||||
// 新增产品
|
||||
func (s *productService) CreateProduct(create *CreateProductData) error {
|
||||
return q.Product.Create(&m.Product{
|
||||
@@ -61,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 {
|
||||
@@ -76,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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ func (s *productSkuService) All(product_code string) (result []*m.ProductSku, er
|
||||
Joins(q.ProductSku.Product).
|
||||
Where(q.Product.As("Product").Code.Eq(product_code)).
|
||||
Select(q.ProductSku.ALL).
|
||||
Order(q.ProductSku.CreatedAt.Desc()).
|
||||
Order(q.ProductSku.Sort).
|
||||
Find()
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ func (s *productSkuService) Page(req *core.PageReq, productId *int32) (result []
|
||||
return q.ProductSku.
|
||||
Joins(q.ProductSku.Discount, q.ProductSku.Product).
|
||||
Where(do...).
|
||||
Order(q.ProductSku.ID).
|
||||
Order(q.ProductSku.Sort).
|
||||
FindByPage(req.GetOffset(), req.GetLimit())
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,59 +122,70 @@ 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, error) {
|
||||
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, core.NewServErr("产品不可用", 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))
|
||||
}
|
||||
|
||||
// 原价
|
||||
price := sku.Price
|
||||
amount := price.Mul(decimal.NewFromInt32(count))
|
||||
amountMin := sku.PriceMin.Mul(decimal.NewFromInt32(count))
|
||||
amount := sku.Price.Mul(decimal.NewFromInt32(count))
|
||||
|
||||
// 折扣价
|
||||
discount := sku.Discount
|
||||
if discount == nil {
|
||||
return nil, nil, nil, decimal.Zero, decimal.Zero, core.NewServErr("价格查询失败", err)
|
||||
return nil, nil, nil, decimal.Zero, decimal.Zero, decimal.Zero, core.NewServErr("产品未配置折扣", err)
|
||||
}
|
||||
|
||||
discountRate := discount.Rate()
|
||||
if user != nil && user.DiscountID != nil { // 用户特殊优惠
|
||||
uDiscount, err := q.ProductDiscount.Where(q.ProductDiscount.ID.Eq(*user.DiscountID)).Take()
|
||||
if err != nil {
|
||||
return nil, nil, nil, decimal.Zero, decimal.Zero, core.NewServErr("客户特殊价查询失败", err)
|
||||
return nil, nil, nil, decimal.Zero, decimal.Zero, decimal.Zero, core.NewServErr("客户特殊价查询失败", err)
|
||||
}
|
||||
|
||||
uDiscountRate := uDiscount.Rate()
|
||||
if uDiscountRate.Cmp(discountRate) > 0 {
|
||||
if uDiscountRate.Cmp(discountRate) < 0 {
|
||||
discountRate = uDiscountRate
|
||||
discount = uDiscount
|
||||
}
|
||||
@@ -186,20 +199,20 @@ func (s *resourceService) CalcPrice(skuCode string, count int32, user *m.User, c
|
||||
var err error
|
||||
coupon, err = Coupon.GetUserCoupon(user.ID, *cuid, discounted)
|
||||
if err != nil {
|
||||
return nil, nil, nil, decimal.Zero, decimal.Zero, err
|
||||
return nil, nil, nil, decimal.Zero, decimal.Zero, decimal.Zero, err
|
||||
}
|
||||
couponApplied = discounted.Sub(coupon.Coupon.Amount)
|
||||
}
|
||||
|
||||
// 约束到最低价格
|
||||
if discounted.Cmp(sku.PriceMin) < 0 {
|
||||
discounted = sku.PriceMin.Copy()
|
||||
if discounted.Cmp(amountMin) < 0 {
|
||||
discounted = amountMin.Copy()
|
||||
}
|
||||
if couponApplied.Cmp(sku.PriceMin) < 0 {
|
||||
couponApplied = sku.PriceMin.Copy()
|
||||
if couponApplied.Cmp(amountMin) < 0 {
|
||||
couponApplied = amountMin.Copy()
|
||||
}
|
||||
|
||||
return sku, discount, coupon, discounted, couponApplied, nil
|
||||
return sku, discount, coupon, amount.RoundCeil(2), discounted.RoundCeil(2), couponApplied.RoundCeil(2), nil
|
||||
}
|
||||
|
||||
type CreateResourceData struct {
|
||||
@@ -260,7 +273,7 @@ func (data *CreateResourceData) Code() string {
|
||||
}
|
||||
|
||||
func (data *CreateResourceData) TradeDetail(user *m.User) (*TradeDetail, error) {
|
||||
sku, discount, coupon, amount, actual, err := Resource.CalcPrice(data.Code(), data.Count(), user, data.CouponId)
|
||||
sku, discount, coupon, amount, discounted, actual, err := Resource.CalcPrice(data.Code(), data.Count(), user, data.CouponId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -277,7 +290,7 @@ func (data *CreateResourceData) TradeDetail(user *m.User) (*TradeDetail, error)
|
||||
data,
|
||||
m.TradeTypePurchase,
|
||||
sku.Name,
|
||||
amount, actual,
|
||||
amount, discounted, actual,
|
||||
discountId, couponUserId,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -619,6 +625,7 @@ type TradeDetail struct {
|
||||
Type m.TradeType `json:"type"`
|
||||
Subject string `json:"subject"`
|
||||
Amount decimal.Decimal `json:"amount"`
|
||||
Discounted decimal.Decimal `json:"discounted"`
|
||||
Actual decimal.Decimal `json:"actual"`
|
||||
DiscountId *int32 `json:"discount_id,omitempty"`
|
||||
CouponUserId *int32 `json:"coupon_id,omitempty"`
|
||||
|
||||
@@ -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,12 +91,15 @@ 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,
|
||||
m.TradeTypeRecharge,
|
||||
fmt.Sprintf("账户充值 - %s元", amount.StringFixed(2)),
|
||||
amount, amount,
|
||||
amount, amount, amount,
|
||||
nil, nil,
|
||||
}, nil
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -89,8 +89,8 @@ func (s *verifierService) SendSms(ctx context.Context, phone string, purpose Ver
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *verifierService) VerifySms(ctx context.Context, phone, code string) error {
|
||||
key := smsKey(phone, VerifierSmsPurposeLogin)
|
||||
func (s *verifierService) VerifySms(ctx context.Context, phone, code string, purpose VerifierSmsPurpose) error {
|
||||
key := smsKey(phone, purpose)
|
||||
keyLock := key + ":lock"
|
||||
|
||||
err := g.Redis.Watch(ctx, func(tx *redis.Tx) error {
|
||||
@@ -146,7 +146,8 @@ func smsKey(phone string, purpose VerifierSmsPurpose) string {
|
||||
type VerifierSmsPurpose int
|
||||
|
||||
const (
|
||||
VerifierSmsPurposeLogin VerifierSmsPurpose = iota // 登录
|
||||
VerifierSmsPurposeLogin VerifierSmsPurpose = iota // 登录
|
||||
VerifierSmsPurposePassword // 修改密码
|
||||
)
|
||||
|
||||
// region 服务异常
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
62
web/web.go
62
web/web.go
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user