21 Commits

Author SHA1 Message Date
32e56b1a0f 新增提取函数,实现通过套餐编号提取 2026-05-23 13:50:52 +08:00
b436a6cade 套餐查询返回类型信息 2026-05-21 16:31:59 +08:00
dd08655e2c 查询所有用户套餐时返回名称 2026-05-20 13:31:45 +08:00
9fe6cb4bf5 清理 debug 输出 2026-05-19 14:58:04 +08:00
cf4bc4932a 查询使用 utc 时间 2026-05-19 14:56:47 +08:00
dbc909c736 修复长效时间问题 2026-05-19 13:37:33 +08:00
71554da541 修复提取并发问题 & 修复接口时区问题 2026-05-18 13:54:01 +08:00
8f89503c88 收紧数据保存检查 2026-05-14 14:23:01 +08:00
80f04c92ec 修复请求错误消息上报问题 2026-05-13 18:07:44 +08:00
ccbc6f0b67 完善提取处理流程,解决提取并发问题 2026-05-13 16:17:57 +08:00
d273731e31 修复购买数量低于限制的问题 2026-05-11 17:39:22 +08:00
65f8ee360b 完善通道管理机制 & 增强 otel 记录字段 2026-05-11 11:04:21 +08:00
042c8d1a51 完善 otel 配置 2026-05-08 13:53:47 +08:00
a0b0be2b8e 实现定时通道过期清理 2026-05-08 09:33:41 +08:00
8fc1d30578 优化白名单与通道提取功能 2026-05-07 12:43:15 +08:00
a4d9c28702 实现已发放优惠券的管理接口 2026-04-29 16:59:14 +08:00
ccb8db555e 放开提取接口权限 2026-04-28 18:00:24 +08:00
e70f2337cb 发放优惠券 2026-04-27 17:13:06 +08:00
d59f4ca37f 用户余额查询 2026-04-25 14:15:37 +08:00
0edc883084 用户修改套餐 ip 检查功能接口 2026-04-23 13:47:22 +08:00
d26106eb00 修复运行边界条件问题 2026-04-22 17:11:55 +08:00
46 changed files with 2021 additions and 675 deletions

View File

@@ -16,6 +16,7 @@ REDIS_PORT=6379
# otel 配置 # otel 配置
OTEL_HOST=127.0.0.1 OTEL_HOST=127.0.0.1
OTEL_PORT=4317 OTEL_PORT=4317
OTEL_NAME_SUFFIX=dev
# 白银节点 # 白银节点
BAIYIN_CLOUD_URL= BAIYIN_CLOUD_URL=

View File

@@ -1,12 +1,16 @@
## TODO ## TODO
proxy 的删除和更新,锁粒度应该有问题 ---
最低价格 0.01 用反射实现环境变量解析,以简化函数签名
错误提示增强,展示整链路信息
交易信息持久化 交易信息持久化
用反射实现环境变量解析,以简化函数签名 订单关闭问题,在前端关闭窗口后直接调用了全部订单接口,应改成先确认再关闭
- 取消订单接口改成只允许管理员调用
- 新增关闭订单接口,关闭订单的逻辑是先尝试完成,如果订单未支付则取消订单
--- ---

36
go.mod
View File

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

80
go.sum
View File

@@ -154,8 +154,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/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-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/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.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= 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.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 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 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/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 h1:msaHYZ13HfLIbqXsGwZZQBg5zgxwumlZ1mCkXn3E7LM=
go.opentelemetry.io/contrib v1.38.0/go.mod h1:4Vp7Az5Dez02V1lCi9OqLvSmSz0lbZu/O2r4XZsqwB0= 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.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= 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 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= 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.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= 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 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 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= 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.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 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.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= 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/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-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 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.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.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.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= 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-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-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/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.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 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.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 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/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-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/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.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.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.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.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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-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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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.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.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.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 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/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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.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.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 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 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= 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= 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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 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.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= 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-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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/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.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= 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.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 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-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 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-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
google.golang.org/genproto/googleapis/api v0.0.0-20251124214823-79d6a2a48846/go.mod h1:Fk4kyraUvqD7i5H6S43sj2W98fbZa75lpZz/eUyhfO0= 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-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk= google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= 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.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 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.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.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.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= 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-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-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 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.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.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 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.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 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 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/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=

6
pkg/env/env.go vendored
View File

@@ -35,8 +35,9 @@ var (
RedisPort = "6379" RedisPort = "6379"
RedisPassword = "" RedisPassword = ""
OtelHost string OtelHost string
OtelPort string OtelPort string
OtelNameSuffix string
BaiyinCloudUrl string BaiyinCloudUrl string
BaiyinTokenUrl string BaiyinTokenUrl string
@@ -118,6 +119,7 @@ func Init() {
errs = append(errs, parse(&OtelHost, "OTEL_HOST", true, nil)) errs = append(errs, parse(&OtelHost, "OTEL_HOST", true, nil))
errs = append(errs, parse(&OtelPort, "OTEL_PORT", 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(&BaiyinCloudUrl, "BAIYIN_CLOUD_URL", false, nil))
errs = append(errs, parse(&BaiyinTokenUrl, "BAIYIN_TOKEN_URL", false, nil)) errs = append(errs, parse(&BaiyinTokenUrl, "BAIYIN_TOKEN_URL", false, nil))

View File

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

View File

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

View File

@@ -127,7 +127,8 @@ insert into permission (name, description, sort) values
('trade', '交易', 12), ('trade', '交易', 12),
('bill', '账单', 13), ('bill', '账单', 13),
('balance_activity', '余额变动', 14), ('balance_activity', '余额变动', 14),
('proxy', '代理', 15); ('proxy', '代理', 15),
('coupon_user', '已发放优惠券', 16);
-- -------------------------- -- --------------------------
-- level 2 -- level 2
@@ -136,79 +137,84 @@ insert into permission (name, description, sort) values
-- permission 子权限 -- permission 子权限
insert into permission (parent_id, name, description, sort) values 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: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 子权限 -- admin_role 子权限
insert into permission (parent_id, name, description, sort) values 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: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 子权限 -- admin 子权限
insert into permission (parent_id, name, description, sort) values 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: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 子权限 -- product 子权限
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' and deleted_at is null), 'product:read', '读取产品列表', 1), ((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 子权限 -- product_sku 子权限
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' and deleted_at is null), 'product_sku:read', '读取产品套餐列表', 1), ((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 子权限 -- discount 子权限
insert into permission (parent_id, name, description, sort) values 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: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 子权限 -- resource 子权限
insert into permission (parent_id, name, description, sort) values insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'resource' and deleted_at is null), 'resource:read', '读取用户套餐列表', 1), ((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:short', '短效动态套餐', 3),
((select id from permission where name = 'resource' and deleted_at is null), 'resource:long', '长效动态套餐', 4); ((select id from permission where name = 'resource' and deleted_at is null), 'resource:long', '长效动态套餐', 4);
-- user 子权限 -- user 子权限
insert into permission (parent_id, name, description, sort) values 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: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 子权限 -- coupon 子权限
insert into permission (parent_id, name, description, sort) values insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'coupon' and deleted_at is null), 'coupon:read', '读取优惠券列表', 1), ((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 子权限 -- batch 子权限
insert into permission (parent_id, name, description, sort) values 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: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 子权限 -- channel 子权限
insert into permission (parent_id, name, description, sort) values 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: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 子权限 -- proxy 子权限
insert into permission (parent_id, name, description, sort) values insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'proxy' and deleted_at is null), 'proxy:read', '读取代理列表', 1), ((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); ((select id from permission where name = 'proxy' and deleted_at is null), 'proxy:write', '编辑代理', 2);
-- trade 子权限 -- trade 子权限
insert into permission (parent_id, name, description, sort) values 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: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 子权限 -- bill 子权限
insert into permission (parent_id, name, description, sort) values 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: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 子权限 -- balance_activity 子权限
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' and deleted_at is null), 'balance_activity:read', '读取余额变动列表', 1); ((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);
-- -------------------------- -- --------------------------
-- level 3 -- level 3
-- -------------------------- -- --------------------------
@@ -236,7 +242,7 @@ insert into permission (parent_id, name, description, sort) values
-- user:write 子权限 -- user:write 子权限
insert into permission (parent_id, name, description, sort) values 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); ((select id from permission where name = 'user:write' and deleted_at is null), 'user:write:bind', '用户认领', 2);
-- batch:read 子权限 -- batch:read 子权限
@@ -259,6 +265,14 @@ insert into permission (parent_id, name, description, sort) values
insert into permission (parent_id, name, description, sort) values insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'balance_activity:read' and deleted_at is null), 'balance_activity:read:of_user', '读取指定用户的余额变动列表', 1); ((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);
-- -------------------------- -- --------------------------
-- level 4 -- level 4
-- -------------------------- -- --------------------------

View File

@@ -1111,7 +1111,7 @@ comment on table coupon_user is '优惠券发放表';
comment on column coupon_user.id is '记录ID'; comment on column coupon_user.id is '记录ID';
comment on column coupon_user.coupon_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.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.expire_at is '过期时间';
comment on column coupon_user.used_at is '使用时间'; comment on column coupon_user.used_at is '使用时间';
comment on column coupon_user.created_at is '创建时间'; comment on column coupon_user.created_at is '创建时间';

View File

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

View File

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

View File

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

View File

@@ -79,7 +79,6 @@ func ErrorHandler(c *fiber.Ctx, err error) error {
slog.Warn("未处理的异常", slog.String("type", t.String()), slog.String("error", err.Error())) slog.Warn("未处理的异常", slog.String("type", t.String()), slog.String("error", err.Error()))
} }
slog.Warn(message)
c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
return c.Status(code).SendString(message) return c.Status(code).SendString(message)
} }

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

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

View File

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

View File

@@ -1,7 +1,6 @@
package handlers package handlers
import ( import (
"platform/pkg/u"
"platform/web/auth" "platform/web/auth"
"platform/web/core" "platform/web/core"
g "platform/web/globals" g "platform/web/globals"
@@ -11,6 +10,63 @@ import (
"github.com/gofiber/fiber/v2" "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 分页查询所有余额变动记录 // PageBalanceActivityByAdmin 分页查询所有余额变动记录
func PageBalanceActivityByAdmin(c *fiber.Ctx) error { func PageBalanceActivityByAdmin(c *fiber.Ctx) error {
// 检查权限 // 检查权限
@@ -34,16 +90,14 @@ func PageBalanceActivityByAdmin(c *fiber.Ctx) error {
do = do.Where(q.Bill.As("Bill").BillNo.Eq(*req.BillNo)) do = do.Where(q.Bill.As("Bill").BillNo.Eq(*req.BillNo))
} }
if req.CreatedAtStart != nil { if req.CreatedAtStart != nil {
t := u.DateHead(*req.CreatedAtStart) do = do.Where(q.BalanceActivity.CreatedAt.Gte(req.CreatedAtStart.UTC()))
do = do.Where(q.BalanceActivity.CreatedAt.Gte(t))
} }
if req.CreatedAtEnd != nil { if req.CreatedAtEnd != nil {
t := u.DateTail(*req.CreatedAtEnd) do = do.Where(q.BalanceActivity.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
do = do.Where(q.BalanceActivity.CreatedAt.Lte(t))
} }
// 查询余额变动列表 // 查询余额变动列表
list, total, err := q.BalanceActivity.Debug(). list, total, err := q.BalanceActivity.
Joins(q.BalanceActivity.User, q.BalanceActivity.Admin, q.BalanceActivity.Bill). Joins(q.BalanceActivity.User, q.BalanceActivity.Admin, q.BalanceActivity.Bill).
Select( Select(
q.BalanceActivity.ALL, q.BalanceActivity.ALL,
@@ -96,12 +150,10 @@ func PageBalanceActivityOfUserByAdmin(c *fiber.Ctx) error {
do = do.Where(q.Bill.As("Bill").BillNo.Eq(*req.BillNo)) do = do.Where(q.Bill.As("Bill").BillNo.Eq(*req.BillNo))
} }
if req.CreatedAtStart != nil { if req.CreatedAtStart != nil {
t := u.DateHead(*req.CreatedAtStart) do = do.Where(q.BalanceActivity.CreatedAt.Gte(req.CreatedAtStart.UTC()))
do = do.Where(q.BalanceActivity.CreatedAt.Gte(t))
} }
if req.CreatedAtEnd != nil { if req.CreatedAtEnd != nil {
t := u.DateTail(*req.CreatedAtEnd) do = do.Where(q.BalanceActivity.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
do = do.Where(q.BalanceActivity.CreatedAt.Lte(t))
} }
// 查询余额变动列表 // 查询余额变动列表

View File

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

View File

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

View File

@@ -15,90 +15,6 @@ import (
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
// PageChannelByAdmin 分页查询所有通道
func PageChannelByAdmin(c *fiber.Ctx) error {
// 检查权限
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeChannelRead)
if err != nil {
return err
}
// 解析请求参数
var req PageChannelsByAdminReq
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
// 构建查询条件
do := q.Channel.Where()
if req.UserPhone != nil {
do = do.Where(q.User.As("User").Phone.Eq(*req.UserPhone))
}
if req.ResourceNo != nil {
do = do.Where(q.Resource.As("Resource").ResourceNo.Eq(*req.ResourceNo))
}
if req.BatchNo != nil {
do = do.Where(q.Channel.BatchNo.Eq(*req.BatchNo))
}
if req.ProxyHost != nil {
do = do.Where(q.Channel.Host.Eq(*req.ProxyHost))
}
if req.ProxyPort != nil {
do = do.Where(q.Channel.Port.Eq(*req.ProxyPort))
}
if req.NodeIP != nil {
ip, err := orm.ParseInet(*req.NodeIP)
if err != nil {
return core.NewBizErr("查询参数 ip 格式不正确")
}
do = do.Where(q.Channel.IP.Eq(ip))
}
if req.ExpiredAtStart != nil {
time := u.DateHead(*req.ExpiredAtStart)
do = do.Where(q.Channel.ExpiredAt.Gte(time))
}
if req.ExpiredAtEnd != nil {
time := u.DateHead(*req.ExpiredAtEnd)
do = do.Where(q.Channel.ExpiredAt.Lte(time))
}
// 查询通道列表
list, total, err := q.Channel.
Joins(q.Channel.User, q.Channel.Resource).
Select(
q.Channel.ALL,
q.Resource.As("Resource").ResourceNo.As("Resource__resource_no"),
q.User.As("User").Phone.As("User__phone"),
q.User.As("User").Name.As("User__name"),
).
Where(do).
Order(q.Channel.CreatedAt.Desc()).
FindByPage(req.GetOffset(), req.GetLimit())
if err != nil {
return err
}
// 返回结果
return c.JSON(core.PageResp{
List: list,
Total: int(total),
Page: req.GetPage(),
Size: req.GetSize(),
})
}
type PageChannelsByAdminReq struct {
core.PageReq
UserPhone *string `json:"user_phone"`
ResourceNo *string `json:"resource_no"`
BatchNo *string `json:"batch_no"`
ProxyHost *string `json:"proxy_host"`
ProxyPort *uint16 `json:"proxy_port"`
NodeIP *string `json:"node_ip" validator:"omitempty,ip"`
ExpiredAtStart *time.Time `json:"expired_at_start"`
ExpiredAtEnd *time.Time `json:"expired_at_end"`
}
// ListChannel 分页查询当前用户通道 // ListChannel 分页查询当前用户通道
func ListChannel(c *fiber.Ctx) error { func ListChannel(c *fiber.Ctx) error {
// 检查权限 // 检查权限
@@ -126,10 +42,10 @@ func ListChannel(c *fiber.Ctx) error {
} }
if req.ExpireAfter != nil { if req.ExpireAfter != nil {
cond.Where(q.Channel.ExpiredAt.Gte(*req.ExpireAfter)) cond = cond.Where(q.Channel.ExpiredAt.Gte(req.ExpireAfter.UTC()))
} }
if req.ExpireBefore != nil { if req.ExpireBefore != nil {
cond.Where(q.Channel.ExpiredAt.Lte(*req.ExpireBefore)) cond = cond.Where(q.Channel.ExpiredAt.Lte(req.ExpireBefore.UTC()))
} }
// 查询数据 // 查询数据
@@ -172,12 +88,7 @@ type ListChannelsReq struct {
// CreateChannel 创建新通道 // CreateChannel 创建新通道
func CreateChannel(c *fiber.Ctx) error { func CreateChannel(c *fiber.Ctx) error {
// 不检查权限,允许 api 调用
// 检查权限
_, err := auth.GetAuthCtx(c).PermitUser()
if err != nil {
return err
}
// 解析参数 // 解析参数
req := new(CreateChannelReq) req := new(CreateChannelReq)
@@ -191,13 +102,17 @@ func CreateChannel(c *fiber.Ctx) error {
} }
// 创建通道 // 创建通道
no, err := s.FindResourceNoById(req.ResourceId)
if err != nil {
return err
}
var isp *m.EdgeISP var isp *m.EdgeISP
if req.Isp != nil { if req.Isp != nil {
isp = u.X(m.ToEdgeISP(*req.Isp)) isp = u.X(m.ToEdgeISP(*req.Isp))
} }
result, err := s.Channel.CreateChannels( result, err := s.Channel.CreateChannels(
ip, ip, no,
req.ResourceId,
req.AuthType == s.ChannelAuthTypeIp, req.AuthType == s.ChannelAuthTypeIp,
req.AuthType == s.ChannelAuthTypePass, req.AuthType == s.ChannelAuthTypePass,
req.Count, req.Count,
@@ -217,6 +132,7 @@ func CreateChannel(c *fiber.Ctx) error {
resp[i] = &CreateChannelRespItem{ resp[i] = &CreateChannelRespItem{
Proto: req.Protocol, Proto: req.Protocol,
Host: channel.Host, Host: channel.Host,
IP: channel.Proxy.IP.String(),
Port: channel.Port, Port: channel.Port,
} }
if req.AuthType == s.ChannelAuthTypePass { if req.AuthType == s.ChannelAuthTypePass {
@@ -237,9 +153,73 @@ type CreateChannelReq struct {
Isp *int `json:"isp"` Isp *int `json:"isp"`
} }
// CreateChannelV2 创建新通道 v2使用 resource_no 替代 resource_id
func CreateChannelV2(c *fiber.Ctx) error {
// 不检查权限,允许 api 调用
// 解析参数
req := new(CreateChannelReqV2)
if err := g.Validator.ParseBody(c, req); err != nil {
return core.NewBizErr("解析参数失败", err)
}
ip, err := netip.ParseAddr(c.IP())
if err != nil {
return core.NewBizErr("获取客户端地址失败", err)
}
// 创建通道
var isp *m.EdgeISP
if req.Isp != nil {
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,
&s.EdgeFilter{
Isp: isp,
Prov: req.Prov,
City: req.City,
},
)
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,
IP: channel.Proxy.IP.String(),
Port: channel.Port,
}
if req.AuthType == s.ChannelAuthTypePass {
resp[i].Username = channel.Username
resp[i].Password = channel.Password
}
}
return c.JSON(resp)
}
type 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"`
}
type CreateChannelRespItem struct { type CreateChannelRespItem struct {
Proto int `json:"-"` Proto int `json:"-"`
Host string `json:"host"` Host string `json:"host"`
IP string `json:"ip"`
Port uint16 `json:"port"` Port uint16 `json:"port"`
Username *string `json:"username,omitempty"` Username *string `json:"username,omitempty"`
Password *string `json:"password,omitempty"` Password *string `json:"password,omitempty"`
@@ -272,6 +252,97 @@ type RemoveChannelsReq struct {
Batch string `json:"batch" validate:"required"` Batch string `json:"batch" validate:"required"`
} }
// PageChannelByAdmin 分页查询所有通道
func PageChannelByAdmin(c *fiber.Ctx) error {
// 检查权限
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeChannelRead)
if err != nil {
return err
}
// 解析请求参数
var req PageChannelsByAdminReq
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
// 构建查询条件
do := q.Channel.Where()
if req.UserPhone != nil {
do = do.Where(q.User.As("User").Phone.Eq(*req.UserPhone))
}
if req.ResourceNo != nil {
do = do.Where(q.Resource.As("Resource").ResourceNo.Eq(*req.ResourceNo))
}
if req.BatchNo != nil {
do = do.Where(q.Channel.BatchNo.Eq(*req.BatchNo))
}
if req.ProxyHost != nil {
do = do.Where(q.Channel.Host.Eq(*req.ProxyHost))
}
if req.ProxyPort != nil {
do = do.Where(q.Channel.Port.Eq(*req.ProxyPort))
}
if req.NodeIP != nil {
ip, err := orm.ParseInet(*req.NodeIP)
if err != nil {
return core.NewBizErr("查询参数 ip 格式不正确")
}
do = do.Where(q.Channel.IP.Eq(ip))
}
if req.ExpiredAtStart != nil {
do = do.Where(q.Channel.ExpiredAt.Gte(req.ExpiredAtStart.UTC()))
}
if req.ExpiredAtEnd != nil {
do = do.Where(q.Channel.ExpiredAt.Lte(req.ExpiredAtEnd.UTC()))
}
if req.Expired != nil {
if *req.Expired {
do = do.Where(q.Channel.ExpiredAt.Lte(time.Now().UTC()))
} else {
do = do.Where(q.Channel.ExpiredAt.Gt(time.Now().UTC()))
}
}
// 查询通道列表
list, total, err := q.Channel.
Joins(q.Channel.User, q.Channel.Resource).
Select(
q.Channel.ALL,
q.Resource.As("Resource").ResourceNo.As("Resource__resource_no"),
q.Resource.As("Resource").Type.As("Resource__type"),
q.User.As("User").Phone.As("User__phone"),
q.User.As("User").Name.As("User__name"),
).
Where(do).
Order(q.Channel.CreatedAt.Desc()).
FindByPage(req.GetOffset(), req.GetLimit())
if err != nil {
return err
}
// 返回结果
return c.JSON(core.PageResp{
List: list,
Total: int(total),
Page: req.GetPage(),
Size: req.GetSize(),
})
}
type PageChannelsByAdminReq struct {
core.PageReq
UserPhone *string `json:"user_phone"`
ResourceNo *string `json:"resource_no"`
BatchNo *string `json:"batch_no"`
ProxyHost *string `json:"proxy_host"`
ProxyPort *uint16 `json:"proxy_port"`
NodeIP *string `json:"node_ip" validator:"omitempty,ip"`
ExpiredAtStart *time.Time `json:"expired_at_start"`
ExpiredAtEnd *time.Time `json:"expired_at_end"`
Expired *bool `json:"expired"`
}
// PageChannelOfUserByAdmin 分页查询指定用户的通道 // PageChannelOfUserByAdmin 分页查询指定用户的通道
func PageChannelOfUserByAdmin(c *fiber.Ctx) error { func PageChannelOfUserByAdmin(c *fiber.Ctx) error {
// 检查权限 // 检查权限
@@ -301,12 +372,10 @@ func PageChannelOfUserByAdmin(c *fiber.Ctx) error {
do = do.Where(q.Channel.Port.Eq(*req.ProxyPort)) do = do.Where(q.Channel.Port.Eq(*req.ProxyPort))
} }
if req.ExpiredAtStart != nil { if req.ExpiredAtStart != nil {
t := u.DateHead(*req.ExpiredAtStart) do = do.Where(q.Channel.ExpiredAt.Gte(req.ExpiredAtStart.UTC()))
do = do.Where(q.Channel.ExpiredAt.Gte(t))
} }
if req.ExpiredAtEnd != nil { if req.ExpiredAtEnd != nil {
t := u.DateHead(*req.ExpiredAtEnd) do = do.Where(q.Channel.ExpiredAt.Lte(req.ExpiredAtEnd.UTC()))
do = do.Where(q.Channel.ExpiredAt.Lte(t))
} }
// 查询通道列表 // 查询通道列表
@@ -315,6 +384,7 @@ func PageChannelOfUserByAdmin(c *fiber.Ctx) error {
Select( Select(
q.Channel.ALL, q.Channel.ALL,
q.Resource.As("Resource").ResourceNo.As("Resource__resource_no"), q.Resource.As("Resource").ResourceNo.As("Resource__resource_no"),
q.Resource.As("Resource").Type.As("Resource__type"),
q.User.As("User").Phone.As("User__phone"), q.User.As("User").Phone.As("User__phone"),
q.User.As("User").Name.As("User__name"), q.User.As("User").Name.As("User__name"),
). ).
@@ -344,3 +414,32 @@ type PageChannelOfUserByAdminReq struct {
ExpiredAtStart *time.Time `json:"expired_at_start"` ExpiredAtStart *time.Time `json:"expired_at_start"`
ExpiredAtEnd *time.Time `json:"expired_at_end"` 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"`
}

View File

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

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

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

View File

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

View File

@@ -120,6 +120,8 @@ func RemoveProxy(c *fiber.Ctx) error {
return c.JSON(nil) return c.JSON(nil)
} }
// ====================
// region 报告上线 // region 报告上线
func ProxyReportOnline(c *fiber.Ctx) (err error) { func ProxyReportOnline(c *fiber.Ctx) (err error) {
return c.JSON(map[string]any{ return c.JSON(map[string]any{

View File

@@ -44,26 +44,26 @@ func PageResourceShort(c *fiber.Ctx) error {
do.Where(q.ResourceShort.As(q.Resource.Short.Name()).Type.Eq(*req.Type)) do.Where(q.ResourceShort.As(q.Resource.Short.Name()).Type.Eq(*req.Type))
} }
if req.CreateAfter != nil { if req.CreateAfter != nil {
do.Where(q.Resource.CreatedAt.Gte(*req.CreateAfter)) do = do.Where(q.Resource.CreatedAt.Gte(req.CreateAfter.UTC()))
} }
if req.CreateBefore != nil { if req.CreateBefore != nil {
do.Where(q.Resource.CreatedAt.Lte(*req.CreateBefore)) do = do.Where(q.Resource.CreatedAt.Lte(req.CreateBefore.UTC()))
} }
if req.ExpireAfter != nil { if req.ExpireAfter != nil {
do.Where(q.ResourceShort.As(q.Resource.Short.Name()).ExpireAt.Gte(*req.ExpireAfter)) do = do.Where(q.ResourceShort.As(q.Resource.Short.Name()).ExpireAt.Gte(req.ExpireAfter.UTC()))
} }
if req.ExpireBefore != nil { if req.ExpireBefore != nil {
do.Where(q.ResourceShort.As(q.Resource.Short.Name()).ExpireAt.Lte(*req.ExpireBefore)) do = do.Where(q.ResourceShort.As(q.Resource.Short.Name()).ExpireAt.Lte(req.ExpireBefore.UTC()))
} }
if req.Status != nil { if req.Status != nil {
var short = q.ResourceShort.As(q.Resource.Short.Name()) var short = q.ResourceShort.As(q.Resource.Short.Name())
switch *req.Status { switch *req.Status {
case 1: case 1:
var timeCond = q.Resource.Where(short.Type.Eq(int(m.ResourceModeTime)), short.ExpireAt.Gte(time.Now())) var timeCond = q.Resource.Where(short.Type.Eq(int(m.ResourceModeTime)), short.ExpireAt.Gte(time.Now().UTC()))
var quotaCond = q.Resource.Where(short.Type.Eq(int(m.ResourceModeQuota)), short.Quota.GtCol(short.Used)) var quotaCond = q.Resource.Where(short.Type.Eq(int(m.ResourceModeQuota)), short.Quota.GtCol(short.Used))
do.Where(q.Resource.Where(timeCond).Or(quotaCond)) do.Where(q.Resource.Where(timeCond).Or(quotaCond))
case 2: case 2:
var timeCond = q.Resource.Where(short.Type.Eq(int(m.ResourceModeTime)), short.ExpireAt.Lte(time.Now())) var timeCond = q.Resource.Where(short.Type.Eq(int(m.ResourceModeTime)), short.ExpireAt.Lte(time.Now().UTC()))
var quotaCond = q.Resource.Where(short.Type.Eq(int(m.ResourceModeQuota)), short.Quota.LteCol(short.Used)) var quotaCond = q.Resource.Where(short.Type.Eq(int(m.ResourceModeQuota)), short.Quota.LteCol(short.Used))
do.Where(q.Resource.Where(timeCond).Or(quotaCond)) do.Where(q.Resource.Where(timeCond).Or(quotaCond))
} }
@@ -84,6 +84,7 @@ func PageResourceShort(c *fiber.Ctx) error {
total = int64(len(resource) + req.GetOffset()) total = int64(len(resource) + req.GetOffset())
} else { } else {
total, err = q.Resource. total, err = q.Resource.
Joins(q.Resource.Short).
Where(do). Where(do).
Count() Count()
if err != nil { 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))) do.Where(q.ResourceLong.As(q.Resource.Long.Name()).Type.Eq(int(*req.Type)))
} }
if req.CreateAfter != nil { if req.CreateAfter != nil {
do.Where(q.Resource.CreatedAt.Gte(*req.CreateAfter)) do = do.Where(q.Resource.CreatedAt.Gte(req.CreateAfter.UTC()))
} }
if req.CreateBefore != nil { if req.CreateBefore != nil {
do.Where(q.Resource.CreatedAt.Lte(*req.CreateBefore)) do = do.Where(q.Resource.CreatedAt.Lte(req.CreateBefore.UTC()))
} }
if req.ExpireAfter != nil { if req.ExpireAfter != nil {
do.Where(q.ResourceLong.As(q.Resource.Long.Name()).ExpireAt.Gte(*req.ExpireAfter)) do = do.Where(q.ResourceLong.As(q.Resource.Long.Name()).ExpireAt.Gte(req.ExpireAfter.UTC()))
} }
if req.ExpireBefore != nil { if req.ExpireBefore != nil {
do.Where(q.ResourceLong.As(q.Resource.Long.Name()).ExpireAt.Lte(*req.ExpireBefore)) do = do.Where(q.ResourceLong.As(q.Resource.Long.Name()).ExpireAt.Lte(req.ExpireBefore.UTC()))
} }
if req.Status != nil { if req.Status != nil {
var long = q.ResourceLong.As(q.Resource.Long.Name()) var long = q.ResourceLong.As(q.Resource.Long.Name())
switch *req.Status { switch *req.Status {
case 1: case 1:
var timeCond = q.Resource.Where(long.Type.Eq(int(m.ResourceModeTime)), long.ExpireAt.Gte(time.Now())) var timeCond = q.Resource.Where(long.Type.Eq(int(m.ResourceModeTime)), long.ExpireAt.Gte(time.Now().UTC()))
var quotaCond = q.Resource.Where(long.Type.Eq(int(m.ResourceModeQuota)), long.Quota.GtCol(long.Used)) var quotaCond = q.Resource.Where(long.Type.Eq(int(m.ResourceModeQuota)), long.Quota.GtCol(long.Used))
do.Where(q.Resource.Where(timeCond).Or(quotaCond)) do.Where(q.Resource.Where(timeCond).Or(quotaCond))
case 2: case 2:
var timeCond = q.Resource.Where(long.Type.Eq(int(m.ResourceModeTime)), long.ExpireAt.Lte(time.Now())) var timeCond = q.Resource.Where(long.Type.Eq(int(m.ResourceModeTime)), long.ExpireAt.Lte(time.Now().UTC()))
var quotaCond = q.Resource.Where(long.Type.Eq(int(m.ResourceModeQuota)), long.Quota.LteCol(long.Used)) var quotaCond = q.Resource.Where(long.Type.Eq(int(m.ResourceModeQuota)), long.Quota.LteCol(long.Used))
do.Where(q.Resource.Where(timeCond).Or(quotaCond)) do.Where(q.Resource.Where(timeCond).Or(quotaCond))
} }
@@ -180,6 +181,7 @@ func PageResourceLong(c *fiber.Ctx) error {
total = int64(len(resource) + req.GetOffset()) total = int64(len(resource) + req.GetOffset())
} else { } else {
total, err = q.Resource. total, err = q.Resource.
Joins(q.Resource.Long).
Where(do). Where(do).
Count() Count()
if err != nil { 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))) do = do.Where(q.ResourceShort.As("Short").Type.Eq(int(*req.Mode)))
} }
if req.CreatedAtStart != nil { if req.CreatedAtStart != nil {
time := u.DateHead(*req.CreatedAtStart) do = do.Where(q.Resource.CreatedAt.Gte(req.CreatedAtStart.UTC()))
do = do.Where(q.Resource.CreatedAt.Gte(time))
} }
if req.CreatedAtEnd != nil { if req.CreatedAtEnd != nil {
time := u.DateTail(*req.CreatedAtEnd) do = do.Where(q.Resource.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
do = do.Where(q.Resource.CreatedAt.Lte(time))
} }
if req.Expired != nil { if req.Expired != nil {
if *req.Expired { if *req.Expired {
do = do.Where(q.Resource.Where( do = do.Where(q.Resource.Where(
q.ResourceShort.As("Short").Type.Eq(int(m.ResourceModeTime)), q.ResourceShort.As("Short").Type.Eq(int(m.ResourceModeTime)),
q.ResourceShort.As("Short").ExpireAt.Lte(time.Now()), q.ResourceShort.As("Short").ExpireAt.Lte(time.Now().UTC()),
).Or( ).Or(
q.ResourceShort.As("Short").Type.Eq(int(m.ResourceModeQuota)), q.ResourceShort.As("Short").Type.Eq(int(m.ResourceModeQuota)),
q.ResourceShort.As("Short").Quota.LteCol(q.ResourceShort.As("Short").Used), q.ResourceShort.As("Short").Quota.LteCol(q.ResourceShort.As("Short").Used),
@@ -252,7 +252,7 @@ func PageResourceShortByAdmin(c *fiber.Ctx) error {
} else { } else {
do = do.Where(q.Resource.Where( do = do.Where(q.Resource.Where(
q.ResourceShort.As("Short").Type.Eq(int(m.ResourceModeTime)), q.ResourceShort.As("Short").Type.Eq(int(m.ResourceModeTime)),
q.ResourceShort.As("Short").ExpireAt.Gt(time.Now()), q.ResourceShort.As("Short").ExpireAt.Gt(time.Now().UTC()),
).Or( ).Or(
q.ResourceShort.As("Short").Type.Eq(int(m.ResourceModeQuota)), q.ResourceShort.As("Short").Type.Eq(int(m.ResourceModeQuota)),
q.ResourceShort.As("Short").Quota.GtCol(q.ResourceShort.As("Short").Used), q.ResourceShort.As("Short").Quota.GtCol(q.ResourceShort.As("Short").Used),
@@ -327,16 +327,16 @@ func PageResourceLongByAdmin(c *fiber.Ctx) error {
do = do.Where(q.ResourceLong.As("Long").Type.Eq(*req.Mode)) do = do.Where(q.ResourceLong.As("Long").Type.Eq(*req.Mode))
} }
if req.CreatedAtStart != nil { if req.CreatedAtStart != nil {
do = do.Where(q.Resource.CreatedAt.Gte(*req.CreatedAtStart)) do = do.Where(q.Resource.CreatedAt.Gte(req.CreatedAtStart.UTC()))
} }
if req.CreatedAtEnd != nil { if req.CreatedAtEnd != nil {
do = do.Where(q.Resource.CreatedAt.Lte(*req.CreatedAtEnd)) do = do.Where(q.Resource.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
} }
if req.Expired != nil { if req.Expired != nil {
if *req.Expired { if *req.Expired {
do = do.Where(q.Resource.Where( do = do.Where(q.Resource.Where(
q.ResourceLong.As("Long").Type.Eq(int(m.ResourceModeTime)), q.ResourceLong.As("Long").Type.Eq(int(m.ResourceModeTime)),
q.ResourceLong.As("Long").ExpireAt.Lte(time.Now()), q.ResourceLong.As("Long").ExpireAt.Lte(time.Now().UTC()),
).Or( ).Or(
q.ResourceLong.As("Long").Type.Eq(int(m.ResourceModeQuota)), q.ResourceLong.As("Long").Type.Eq(int(m.ResourceModeQuota)),
q.ResourceLong.As("Long").Quota.LteCol(q.ResourceLong.As("Long").Used), q.ResourceLong.As("Long").Quota.LteCol(q.ResourceLong.As("Long").Used),
@@ -344,7 +344,7 @@ func PageResourceLongByAdmin(c *fiber.Ctx) error {
} else { } else {
do = do.Where(q.Resource.Where( do = do.Where(q.Resource.Where(
q.ResourceLong.As("Long").Type.Eq(int(m.ResourceModeTime)), q.ResourceLong.As("Long").Type.Eq(int(m.ResourceModeTime)),
q.ResourceLong.As("Long").ExpireAt.Gt(time.Now()), q.ResourceLong.As("Long").ExpireAt.Gt(time.Now().UTC()),
).Or( ).Or(
q.ResourceLong.As("Long").Type.Eq(int(m.ResourceModeQuota)), q.ResourceLong.As("Long").Type.Eq(int(m.ResourceModeQuota)),
q.ResourceLong.As("Long").Quota.GtCol(q.ResourceLong.As("Long").Used), q.ResourceLong.As("Long").Quota.GtCol(q.ResourceLong.As("Long").Used),
@@ -416,15 +416,13 @@ func PageResourceShortOfUserByAdmin(c *fiber.Ctx) error {
do = do.Where(q.ResourceShort.As("Short").Type.Eq(int(*req.Mode))) do = do.Where(q.ResourceShort.As("Short").Type.Eq(int(*req.Mode)))
} }
if req.CreatedAtStart != nil { if req.CreatedAtStart != nil {
t := u.DateHead(*req.CreatedAtStart) do = do.Where(q.Resource.CreatedAt.Gte(req.CreatedAtStart.UTC()))
do = do.Where(q.Resource.CreatedAt.Gte(t))
} }
if req.CreatedAtEnd != nil { if req.CreatedAtEnd != nil {
t := u.DateTail(*req.CreatedAtEnd) do = do.Where(q.Resource.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
do = do.Where(q.Resource.CreatedAt.Lte(t))
} }
list, total, err := q.Resource. list, total, err := q.Resource.Debug().
Joins(q.Resource.User, q.Resource.Short, q.Resource.Short.Sku). Joins(q.Resource.User, q.Resource.Short, q.Resource.Short.Sku).
Select( Select(
q.Resource.ALL, q.Resource.ALL,
@@ -487,12 +485,10 @@ func PageResourceLongOfUserByAdmin(c *fiber.Ctx) error {
do = do.Where(q.ResourceLong.As("Long").Type.Eq(*req.Mode)) do = do.Where(q.ResourceLong.As("Long").Type.Eq(*req.Mode))
} }
if req.CreatedAtStart != nil { if req.CreatedAtStart != nil {
t := u.DateHead(*req.CreatedAtStart) do = do.Where(q.Resource.CreatedAt.Gte(req.CreatedAtStart.UTC()))
do = do.Where(q.Resource.CreatedAt.Gte(t))
} }
if req.CreatedAtEnd != nil { if req.CreatedAtEnd != nil {
t := u.DateTail(*req.CreatedAtEnd) do = do.Where(q.Resource.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
do = do.Where(q.Resource.CreatedAt.Lte(t))
} }
list, total, err := q.Resource. list, total, err := q.Resource.
@@ -552,6 +548,8 @@ func AllActiveResource(c *fiber.Ctx) error {
Joins( Joins(
q.Resource.Short, q.Resource.Short,
q.Resource.Long, q.Resource.Long,
q.Resource.Short.Sku,
q.Resource.Long.Sku,
). ).
Where( Where(
q.Resource.UserID.Eq(authCtx.User.ID), q.Resource.UserID.Eq(authCtx.User.ID),
@@ -560,9 +558,9 @@ func AllActiveResource(c *fiber.Ctx) error {
q.Resource.Type.Eq(int(m.ResourceTypeShort)), q.Resource.Type.Eq(int(m.ResourceTypeShort)),
q.ResourceShort.As(q.Resource.Short.Name()).Where( q.ResourceShort.As(q.Resource.Short.Name()).Where(
short.Type.Eq(int(m.ResourceModeTime)), short.Type.Eq(int(m.ResourceModeTime)),
short.ExpireAt.Gte(now), short.ExpireAt.Gte(now.UTC()),
q.ResourceShort.As(q.Resource.Short.Name()). q.ResourceShort.As(q.Resource.Short.Name()).
Where(short.LastAt.Lt(u.Today())). Where(short.LastAt.Lt(u.Today().UTC())).
Or(short.Quota.GtCol(short.Daily)), Or(short.Quota.GtCol(short.Daily)),
).Or( ).Or(
short.Type.Eq(int(m.ResourceModeQuota)), short.Type.Eq(int(m.ResourceModeQuota)),
@@ -572,9 +570,9 @@ func AllActiveResource(c *fiber.Ctx) error {
q.Resource.Type.Eq(int(m.ResourceTypeLong)), q.Resource.Type.Eq(int(m.ResourceTypeLong)),
q.ResourceLong.As(q.Resource.Long.Name()).Where( q.ResourceLong.As(q.Resource.Long.Name()).Where(
long.Type.Eq(int(m.ResourceModeTime)), long.Type.Eq(int(m.ResourceModeTime)),
long.ExpireAt.Gte(now), long.ExpireAt.Gte(now.UTC()),
q.ResourceLong.As(q.Resource.Long.Name()). q.ResourceLong.As(q.Resource.Long.Name()).
Where(long.LastAt.Lt(u.Today())). Where(long.LastAt.Lt(u.Today().UTC())).
Or(long.Quota.GtCol(long.Daily)), Or(long.Quota.GtCol(long.Daily)),
).Or( ).Or(
long.Type.Eq(int(m.ResourceModeQuota)), long.Type.Eq(int(m.ResourceModeQuota)),
@@ -588,6 +586,15 @@ func AllActiveResource(c *fiber.Ctx) error {
return err return err
} }
for _, resource := range resources {
switch resource.Type {
case m.ResourceTypeShort:
resource.Short.Sku = &m.ProductSku{Name: resource.Short.Sku.Name}
case m.ResourceTypeLong:
resource.Long.Sku = &m.ProductSku{Name: resource.Long.Sku.Name}
}
}
return c.JSON(resources) return c.JSON(resources)
} }
@@ -609,6 +616,30 @@ func UpdateResourceByAdmin(c *fiber.Ctx) error {
return c.JSON(nil) 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 统计每日可用 // StatisticResourceFree 统计每日可用
func StatisticResourceFree(c *fiber.Ctx) error { func StatisticResourceFree(c *fiber.Ctx) error {
// 检查权限 // 检查权限
@@ -729,10 +760,10 @@ func StatisticResourceUsage(c *fiber.Ctx) error {
) )
if req.TimeAfter != nil { if req.TimeAfter != nil {
do.Where(q.LogsUserUsage.Time.Gte(*req.TimeAfter)) do = do.Where(q.LogsUserUsage.Time.Gte(req.TimeAfter.UTC()))
} }
if req.TimeBefore != nil { if req.TimeBefore != nil {
do.Where(q.LogsUserUsage.Time.Lte(*req.TimeBefore)) do = do.Where(q.LogsUserUsage.Time.Lte(req.TimeBefore.UTC()))
} }
var data = new(StatisticResourceUsageResp) var data = new(StatisticResourceUsageResp)

View File

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

View File

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

View File

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

View File

@@ -11,6 +11,8 @@ import (
"github.com/gofiber/fiber/v2/middleware/requestid" "github.com/gofiber/fiber/v2/middleware/requestid"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jxskiss/base62" "github.com/jxskiss/base62"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
) )
func ApplyMiddlewares(app *fiber.App) { func ApplyMiddlewares(app *fiber.App) {
@@ -20,13 +22,8 @@ func ApplyMiddlewares(app *fiber.App) {
EnableStackTrace: true, EnableStackTrace: true,
})) }))
// cors // metric
app.Use(cors.New(cors.Config{ app.Use(otelfiber.Middleware())
AllowCredentials: true,
AllowOriginsFunc: func(origin string) bool {
return true
},
}))
// logger // logger
app.Use(logger.New(logger.Config{ app.Use(logger.New(logger.Config{
@@ -35,8 +32,31 @@ func ApplyMiddlewares(app *fiber.App) {
}, },
})) }))
// metric // 补充 otel span attr
app.Use(otelfiber.Middleware()) 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 // request id
app.Use(requestid.New(requestid.Config{ app.Use(requestid.New(requestid.Config{

View File

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

View File

@@ -3,6 +3,8 @@ package web
import ( import (
"platform/pkg/env" "platform/pkg/env"
auth2 "platform/web/auth" auth2 "platform/web/auth"
"platform/web/core"
"platform/web/globals"
"platform/web/handlers" "platform/web/handlers"
"time" "time"
@@ -27,12 +29,26 @@ func ApplyRouters(app *fiber.App) {
debug.Get("/sms/:phone", handlers.DebugGetSmsCode) debug.Get("/sms/:phone", handlers.DebugGetSmsCode)
debug.Get("/iden/clear/:phone", handlers.DebugIdentifyClear) debug.Get("/iden/clear/:phone", handlers.DebugIdentifyClear)
debug.Get("/session/now", func(ctx *fiber.Ctx) error { debug.Get("/session/now", func(ctx *fiber.Ctx) error {
rs, err := q.Session.Where(q.Session.AccessTokenExpires.Gt(time.Now())).Find() rs, err := q.Session.Where(q.Session.AccessTokenExpires.Gt(time.Now().UTC())).Find()
if err != nil { if err != nil {
return err return err
} }
return ctx.JSON(rs) 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)
})
} }
} }
@@ -66,6 +82,7 @@ func userRouter(api fiber.Router) {
resource.Post("/list/short", handlers.PageResourceShort) resource.Post("/list/short", handlers.PageResourceShort)
resource.Post("/list/long", handlers.PageResourceLong) resource.Post("/list/long", handlers.PageResourceLong)
resource.Post("/create", handlers.CreateResource) resource.Post("/create", handlers.CreateResource)
resource.Post("/update/checkip", handlers.UpdateResourceCheckIP)
resource.Post("/statistics/free", handlers.StatisticResourceFree) resource.Post("/statistics/free", handlers.StatisticResourceFree)
resource.Post("/statistics/usage", handlers.StatisticResourceUsage) resource.Post("/statistics/usage", handlers.StatisticResourceUsage)
@@ -78,6 +95,7 @@ func userRouter(api fiber.Router) {
channel := api.Group("/channel") channel := api.Group("/channel")
channel.Post("/list", handlers.ListChannel) channel.Post("/list", handlers.ListChannel)
channel.Post("/create", handlers.CreateChannel) channel.Post("/create", handlers.CreateChannel)
channel.Post("/create/v2", handlers.CreateChannelV2)
// 交易 // 交易
trade := api.Group("/trade") trade := api.Group("/trade")
@@ -90,6 +108,15 @@ func userRouter(api fiber.Router) {
bill := api.Group("/bill") bill := api.Group("/bill")
bill.Post("/list", handlers.ListBill) 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 := api.Group("/announcement")
announcement.Post("/list", handlers.ListAnnouncements) announcement.Post("/list", handlers.ListAnnouncements)
@@ -191,6 +218,7 @@ func adminRouter(api fiber.Router) {
var channel = api.Group("/channel") var channel = api.Group("/channel")
channel.Post("/page", handlers.PageChannelByAdmin) channel.Post("/page", handlers.PageChannelByAdmin)
channel.Post("/page/of-user", handlers.PageChannelOfUserByAdmin) channel.Post("/page/of-user", handlers.PageChannelOfUserByAdmin)
channel.Post("/sync/clear-expired", handlers.SyncChannelClearExpiredByAdmin)
// proxy 代理 // proxy 代理
var proxy = api.Group("/proxy") var proxy = api.Group("/proxy")
@@ -248,4 +276,14 @@ func adminRouter(api fiber.Router) {
coupon.Post("/create", handlers.CreateCoupon) coupon.Post("/create", handlers.CreateCoupon)
coupon.Post("/update", handlers.UpdateCoupon) coupon.Post("/update", handlers.UpdateCoupon)
coupon.Post("/remove", handlers.DeleteCoupon) 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)
} }

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"math/rand/v2" "math/rand/v2"
"net/netip" "net/netip"
"platform/pkg/env"
"platform/pkg/u" "platform/pkg/u"
"platform/web/core" "platform/web/core"
g "platform/web/globals" g "platform/web/globals"
@@ -24,22 +25,69 @@ var Channel = &channelServer{
} }
type ChannelServiceProvider interface { type ChannelServiceProvider interface {
CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter *EdgeFilter) ([]*m.Channel, error) CreateChannels(source netip.Addr, resourceNo string, authWhitelist bool, authPassword bool, count int, edgeFilter *EdgeFilter) ([]*m.Channel, error)
RemoveChannels(batch string) error RemoveChannels(batch string) error
ClearExpiredChannels(proxyId int32) (int, error)
} }
type channelServer struct { type channelServer struct {
provider ChannelServiceProvider provider ChannelServiceProvider
} }
func (s *channelServer) CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter *EdgeFilter) ([]*m.Channel, error) { func (s *channelServer) CreateChannels(source netip.Addr, resourceNo string, authWhitelist bool, authPassword bool, count int, edgeFilter *EdgeFilter) ([]*m.Channel, error) {
return s.provider.CreateChannels(source, resourceId, authWhitelist, authPassword, count, edgeFilter) return s.provider.CreateChannels(source, resourceNo, authWhitelist, authPassword, count, edgeFilter)
} }
func (s *channelServer) RemoveChannels(batch string) error { func (s *channelServer) RemoveChannels(batch string) error {
return s.provider.RemoveChannels(batch) return s.provider.RemoveChannels(batch)
} }
func (s *channelServer) ClearExpiredChannels(proxyId int32) (int, error) {
return s.provider.ClearExpiredChannels(proxyId)
}
func (s *channelServer) RefreshEdges() error {
if env.RunMode != env.RunModeProd {
return nil
}
// 找到所有网关
proxies, err := q.Proxy.Where(
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
}
// 授权方式 // 授权方式
type ChannelAuthType int type ChannelAuthType int
@@ -67,12 +115,23 @@ func genPassPair() (string, string) {
return string(username), string(password) return string(username), string(password)
} }
func FindResourceNoById(resourceId int32) (string, error) {
resource, err := q.Resource.
Select(q.Resource.ResourceNo).
Where(q.Resource.ID.Eq(resourceId)).
Take()
if err != nil {
return "", ErrResourceNotExist
}
return u.Z(resource.ResourceNo), nil
}
// 查找资源 // 查找资源
func findResource(resourceId int32, now time.Time) (*ResourceView, error) { func findResourceViewByNo(resourceNo string, now time.Time) (*ResourceView, error) {
resource, err := q.Resource. resource, err := q.Resource.
Preload(field.Associations). Preload(field.Associations).
Where( Where(
q.Resource.ID.Eq(resourceId), q.Resource.ResourceNo.Eq(resourceNo),
q.Resource.Active.Is(true), q.Resource.Active.Is(true),
). ).
Take() Take()
@@ -83,7 +142,7 @@ func findResource(resourceId int32, now time.Time) (*ResourceView, error) {
return nil, ErrResourceNotExist return nil, ErrResourceNotExist
} }
var info = &ResourceView{ var info = &ResourceView{
Id: resource.ID, ID: resource.ID,
User: *resource.User, User: *resource.User,
Active: resource.Active, Active: resource.Active,
Type: resource.Type, Type: resource.Type,
@@ -109,7 +168,7 @@ func findResource(resourceId int32, now time.Time) (*ResourceView, error) {
var sub = resource.Long var sub = resource.Long
info.LongId = &sub.ID info.LongId = &sub.ID
info.ExpireAt = sub.ExpireAt info.ExpireAt = sub.ExpireAt
info.Live = time.Duration(sub.Live) * time.Hour info.Live = time.Duration(sub.Live) * time.Minute
info.Mode = sub.Type info.Mode = sub.Type
info.Quota = sub.Quota info.Quota = sub.Quota
info.Used = sub.Used info.Used = sub.Used
@@ -129,7 +188,7 @@ func findResource(resourceId int32, now time.Time) (*ResourceView, error) {
// ResourceView 套餐数据的简化视图,便于直接获取主要数据 // ResourceView 套餐数据的简化视图,便于直接获取主要数据
type ResourceView struct { type ResourceView struct {
Id int32 ID int32
User m.User User m.User
Active bool Active bool
Type m.ResourceType Type m.ResourceType
@@ -147,13 +206,13 @@ type ResourceView struct {
} }
// 检查用户是否可提取 // 检查用户是否可提取
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 { if count > 400 {
return nil, nil, core.NewBizErr("单次最多提取 400 个") return nil, nil, core.NewBizErr("单次最多提取 400 个")
} }
// 获取用户套餐 // 获取用户套餐
resource, err := findResource(resourceId, now) resource, err := findResourceViewByNo(resourceNo, now)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -172,6 +231,10 @@ func ensure(now time.Time, source netip.Addr, resourceId int32, count int) (*Res
return nil, nil, err return nil, nil, err
} }
if authWhitelist && len(whitelists) == 0 {
return nil, nil, core.NewBizErr("当前白名单为空,请先添加白名单")
}
ips := make([]string, len(whitelists)) ips := make([]string, len(whitelists))
pass := false pass := false
for i, item := range whitelists { for i, item := range whitelists {
@@ -211,10 +274,13 @@ func ensure(now time.Time, source netip.Addr, resourceId int32, count int) (*Res
return resource, ips, nil return resource, ips, nil
} }
var ( func freeChansKey(proxy int32) string {
freeChansKey = "channel:free" return "channel:free:" + strconv.Itoa(int(proxy))
usedChansKey = "channel:used" }
)
func usedChansKey(proxy int32, batch string) string {
return "channel:used:" + strconv.Itoa(int(proxy)) + ":" + batch
}
// 扩容通道 // 扩容通道
func regChans(proxy int32, chans []netip.AddrPort) error { func regChans(proxy int32, chans []netip.AddrPort) error {
@@ -223,7 +289,7 @@ func regChans(proxy int32, chans []netip.AddrPort) error {
strs[i] = ch.String() strs[i] = ch.String()
} }
key := freeChansKey + ":" + strconv.Itoa(int(proxy)) key := freeChansKey(proxy)
err := g.Redis.SAdd(context.Background(), key, strs...).Err() err := g.Redis.SAdd(context.Background(), key, strs...).Err()
if err != nil { if err != nil {
return fmt.Errorf("扩容通道失败: %w", err) return fmt.Errorf("扩容通道失败: %w", err)
@@ -233,7 +299,7 @@ func regChans(proxy int32, chans []netip.AddrPort) error {
// 缩容通道 // 缩容通道
func remChans(proxy int32) error { func remChans(proxy int32) error {
key := freeChansKey + ":" + strconv.Itoa(int(proxy)) key := freeChansKey(proxy)
err := g.Redis.Del(context.Background(), key).Err() err := g.Redis.Del(context.Background(), key).Err()
if err != nil { if err != nil {
return fmt.Errorf("缩容通道失败: %w", err) return fmt.Errorf("缩容通道失败: %w", err)
@@ -243,13 +309,12 @@ func remChans(proxy int32) error {
// 取用通道 // 取用通道
func lockChans(proxy int32, batch string, count int) ([]netip.AddrPort, error) { func lockChans(proxy int32, batch string, count int) ([]netip.AddrPort, error) {
pid := strconv.Itoa(int(proxy))
chans, err := RedisScriptLockChans.Run( chans, err := RedisScriptLockChans.Run(
context.Background(), context.Background(),
g.Redis, g.Redis,
[]string{ []string{
freeChansKey + ":" + pid, freeChansKey(proxy),
usedChansKey + ":" + pid + ":" + batch, usedChansKey(proxy, batch),
}, },
count, count,
).StringSlice() ).StringSlice()
@@ -287,13 +352,12 @@ return ports
// 归还通道 // 归还通道
func freeChans(proxy int32, batch string) error { func freeChans(proxy int32, batch string) error {
pid := strconv.Itoa(int(proxy))
err := RedisScriptFreeChans.Run( err := RedisScriptFreeChans.Run(
context.Background(), context.Background(),
g.Redis, g.Redis,
[]string{ []string{
freeChansKey + ":" + pid, freeChansKey(proxy),
usedChansKey + ":" + pid + ":" + batch, usedChansKey(proxy, batch),
}, },
).Err() ).Err()
if err != nil { if err != nil {

View File

@@ -1,6 +1,7 @@
package services package services
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log/slog" "log/slog"
@@ -23,213 +24,180 @@ import (
type channelBaiyinProvider struct{} type channelBaiyinProvider struct{}
func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, filter *EdgeFilter) ([]*m.Channel, error) { func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceNo string, authWhitelist bool, authPassword bool, count int, filter *EdgeFilter) ([]*m.Channel, error) {
if filter == nil { if filter == nil {
return nil, core.NewBizErr("缺少节点过滤条件") return nil, core.NewBizErr("缺少节点过滤条件")
} }
now := time.Now() now := time.Now()
batch := ID.GenReadable("bat") batchNo := ID.GenReadable("bat")
channels := make([]*m.Channel, count)
// 检查并获取套餐与白名单 // 资源锁,防止并发扣减失败导致的端口悬空问题
resource, whitelists, err := ensure(now, source, resourceId, count) err := g.Redsync.WithLock(lockChannelCreateKey(resourceNo), func() error {
if err != nil { // 检查并获取套餐与白名单
return nil, err resource, whitelists, err := ensure(now, source, resourceNo, authWhitelist, count)
}
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
// 锁内确认状态并锁定端口,避免与状态切换并发穿透
var chans []netip.AddrPort
err = g.Redsync.WithLock(proxyStatusLockKey(proxy.ID), func() error {
lockedProxy, err := q.Proxy.Where(q.Proxy.ID.Eq(proxy.ID)).Take()
if err != nil { if err != nil {
return err return err
} }
if lockedProxy.Status != m.ProxyStatusOnline {
return core.NewBizErr("无可用主机,请稍后再试")
}
chans, err = lockChans(proxy.ID, batch, count) user := resource.User
expire := now.Add(resource.Live)
// 选择代理
proxy, gateway, err := selectProxy(count)
if err != nil { if err != nil {
return core.NewBizErr("无可用通道,请稍后再试", err) return err
} }
proxy = *lockedProxy // 取用端口
return nil chans, err := selectPorts(proxy.ID, batchNo, count, expire)
})
if err != nil {
return nil, 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("地区可用节点数量不足")
}
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]
// 通道数据
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 { if err != nil {
return core.NewServErr("更新套餐使用记录失败", err) return err
}
if rs.RowsAffected == 0 {
return core.NewServErr("套餐使用记录不存在")
} }
// 保存通道 // 绑定节点端口
err = q.Channel. chanConfigs := make([]*g.PortConfigsReq, count)
Omit(field.AssociationFields). edgeConfigs := make([]string, 0, count)
Create(channels...) for i := range count {
if err != nil { ch := chans[i]
return core.NewServErr("保存通道失败", err)
// 通道数据
channels[i] = &m.Channel{
UserID: user.ID,
ResourceID: resource.ID,
BatchNo: batchNo,
ProxyID: proxy.ID,
Host: u.Else(proxy.Host, proxy.IP.String()),
Port: ch.Port(),
FilterISP: filter.Isp,
FilterProv: filter.Prov,
FilterCity: filter.City,
ExpiredAt: expire,
Proxy: proxy,
}
// 通道配置数据
chanConfigs[i] = &g.PortConfigsReq{
Port: int(ch.Port()),
Status: true,
AutoEdgeConfig: &g.AutoEdgeConfig{
Province: u.Z(filter.Prov),
City: u.Z(filter.City),
Isp: filter.Isp.String(),
Count: u.P(1),
},
}
// 白名单模式
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)
}
} }
// 保存提取记录 // 提交配置
err = q.LogsUserUsage.Create(&m.LogsUserUsage{ slog.Debug("提交代理端口配置", "proxy", proxy.IP.String(), "total_count", len(chanConfigs), "remote_count", len(edgeConfigs))
UserID: user.ID, if env.RunMode == env.RunModeProd {
ResourceID: resourceId,
BatchNo: batch, // 从云端补足节点
Count: int32(count), err := ensureEdges(proxy, gateway, filter, count)
ISP: u.P(filter.Isp.String()), if err != nil {
Prov: filter.Prov, slog.Warn("ensureEdges 失败", "err", err) // 不阻止通道创建,继续走后续流程
City: filter.City, }
IP: orm.Inet{Addr: source},
Time: now, // 启用网关代理通道
if len(chanConfigs) > 0 {
if err := gateway.GatewayPortConfigs(chanConfigs); err != nil {
slog.Warn("提交代理端口配置失败", "error", err.Error())
return core.NewServErr(fmt.Sprintf("配置代理 %s 端口失败", proxy.IP.String()), err)
}
}
} else {
for _, item := range chanConfigs {
str, _ := json.Marshal(item)
fmt.Println(string(str))
}
}
// 保存数据
err = q.Q.Transaction(func(q *q.Query) error {
// 更新使用记录
var result gen.ResultInfo
var err error
switch resource.Type {
case m.ResourceTypeShort:
result, err = q.ResourceShort.
Where(
q.ResourceShort.ID.Eq(*resource.ShortId),
q.ResourceShort.Used.Eq(resource.Used),
q.ResourceShort.Daily.Eq(resource.Daily),
).
UpdateSimple(
q.ResourceShort.Used.Add(int32(count)),
q.ResourceShort.Daily.Value(int32(resource.Today+count)),
q.ResourceShort.LastAt.Value(now),
)
case m.ResourceTypeLong:
result, err = q.ResourceLong.
Where(
q.ResourceLong.ID.Eq(*resource.LongId),
q.ResourceLong.Used.Eq(resource.Used),
q.ResourceLong.Daily.Eq(resource.Daily),
).
UpdateSimple(
q.ResourceLong.Used.Add(int32(count)),
q.ResourceLong.Daily.Value(int32(resource.Today+count)),
q.ResourceLong.LastAt.Value(now),
)
default:
return core.NewBizErr("套餐类型不正确,无法更新")
}
if err != nil {
return core.NewServErr("更新套餐使用记录失败", err)
}
if result.RowsAffected == 0 {
return core.NewBizErr("套餐状态已过期")
}
// 保存通道
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: resource.ID,
BatchNo: batchNo,
Count: int32(count),
ISP: u.X(filter.Isp.String()),
Prov: filter.Prov,
City: filter.City,
IP: orm.Inet{Addr: source},
Time: now,
})
if err != nil {
return core.NewServErr("保存用户使用记录失败", err)
}
return nil
}) })
if err != nil { if err != nil {
return core.NewServErr("保存用户使用记录失败", err) return err
} }
return nil return nil
@@ -238,101 +206,269 @@ func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int
return nil, err 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 return channels, nil
} }
func (s *channelBaiyinProvider) RemoveChannels(batch string) error { func (s *channelBaiyinProvider) RemoveChannels(batchNo string) error {
start := time.Now() return g.Redsync.WithLock(lockChannelRemoveKey(batchNo), func() error {
start := time.Now()
// 获取连接数据 // 获取连接数据
channels, err := q.Channel.Where(q.Channel.BatchNo.Eq(batch)).Find() channels, err := q.Channel.Where(q.Channel.BatchNo.Eq(batchNo)).Find()
if err != nil {
return core.NewServErr(fmt.Sprintf("获取通道数据失败batch%s", batch), err)
}
if len(channels) == 0 {
slog.Warn(fmt.Sprintf("未找到通道数据batch%s", batch))
return nil
}
proxy, err := q.Proxy.Where(q.Proxy.ID.Eq(channels[0].ProxyID)).Take()
if err != nil {
return core.NewServErr(fmt.Sprintf("获取代理数据失败batch%s", batch), err)
}
// 准备配置数据
edgeConfigs := make([]string, len(channels))
configs := make([]*g.PortConfigsReq, len(channels))
for i, channel := range channels {
if channel.EdgeRef != nil {
edgeConfigs[i] = *channel.EdgeRef
} else {
slog.Warn(fmt.Sprintf("通道 %d 没有保存节点引用", channel.ID))
}
configs[i] = &g.PortConfigsReq{
Status: false,
Port: int(channel.Port),
Edge: &[]string{},
}
}
// 提交配置
if env.RunMode == env.RunModeProd {
// 断开节点连接
g.Cloud.CloudDisconnect(&g.CloudDisconnectReq{
Uuid: proxy.Mac,
Edge: &edgeConfigs,
})
// 清空通道配置
secret := strings.Split(u.Z(proxy.Secret), ":")
gateway := g.NewGateway(proxy.IP.String(), secret[0], secret[1])
err := gateway.GatewayPortConfigs(configs)
if err != nil { if err != nil {
return core.NewServErr(fmt.Sprintf("清空代理 %s 端口配置失败", proxy.IP.String()), err) return core.NewServErr(fmt.Sprintf("获取通道数据失败batch%s", batchNo), err)
} }
} else { if len(channels) == 0 {
slog.Debug("清除代理端口配置", "proxy", proxy.IP) slog.Warn(fmt.Sprintf("未找到通道数据batch%s", batchNo))
for _, item := range configs { return nil
str, _ := json.Marshal(item)
fmt.Println(string(str))
} }
}
// 释放端口 proxy, err := q.Proxy.Where(q.Proxy.ID.Eq(channels[0].ProxyID)).Take()
err = freeChans(proxy.ID, batch) if err != nil {
return core.NewServErr(fmt.Sprintf("获取代理数据失败batch%s", batchNo), err)
}
// 检查通道是否存在
chans, err := g.Redis.LRange(context.Background(), usedChansKey(proxy.ID, batchNo), 0, -1).Result()
if err != nil {
return core.NewServErr("查询使用中通道失败", err)
}
if len(chans) == 0 {
slog.Debug("通道为空,跳过清理", "key", usedChansKey(proxy.ID, batchNo))
return nil // 没有使用中通道,已经被清理过了
}
// 准备配置数据
configs := make([]*g.PortConfigsReq, len(chans))
for i, ch := range chans {
ap, err := netip.ParseAddrPort(ch)
if err != nil {
return core.NewServErr(fmt.Sprintf("解析通道数据失败: %s", ch), err)
}
configs[i] = &g.PortConfigsReq{
Port: int(ap.Port()),
Edge: &[]string{},
AutoEdgeConfig: &g.AutoEdgeConfig{Count: u.P(0)},
Status: false,
}
}
// 提交配置
if env.RunMode == env.RunModeProd {
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)
}
} else {
for _, item := range configs {
str, _ := json.Marshal(item)
fmt.Println(string(str))
}
}
// 释放端口
err = freeChans(proxy.ID, batchNo)
if err != nil {
return err
}
slog.Debug("清除代理端口配置", "proxy", proxy.ID, "batch", batchNo, "duration", time.Since(start).String())
return nil
})
}
// ClearExpiredChannels 清理指定代理的过期通道,并返回清理数量(现在理论上不会有需要手动批量清理的通道,未来可以废弃)
func (s *channelBaiyinProvider) ClearExpiredChannels(proxyId int32) (int, error) {
now := time.Now()
// 获取未清理通道
keys, err := g.Redis.Keys(context.Background(), usedChansKey(proxyId, "*")).Result()
if err != nil { if err != nil {
return err return 0, core.NewServErr("查询使用中通道失败", err)
}
if len(keys) == 0 {
return 0, nil
}
batchList := make([]string, len(keys))
batchSet := make(map[string]struct{}, len(keys))
for i, key := range keys {
parts := strings.Split(key, ":")
if len(parts) != 4 {
return 0, core.NewServErr(fmt.Sprintf("使用中通道键格式错误: %s", key), nil)
}
batchList[i] = parts[3]
batchSet[parts[3]] = struct{}{}
}
// 排除未过期通道
var batchQueried []struct{ BatchNo string }
err = q.Channel.
Select(q.Channel.BatchNo).
Where(
q.Channel.BatchNo.In(batchList...),
q.Channel.ExpiredAt.Gte(now.UTC()),
).
Group(q.Channel.BatchNo).
Scan(&batchQueried)
if err != nil {
return 0, core.NewServErr("查询过期通道失败", err)
}
for _, batch := range batchQueried {
delete(batchSet, batch.BatchNo)
}
// 清理过期通道
slog.Info("批量清理过期通道", "count", len(batchSet))
for batchNo, _ := range batchSet {
err := s.RemoveChannels(batchNo)
if err != nil {
slog.Error("清理过期通道失败", "batch", batchNo, "error", err)
}
}
return len(batchSet), nil
}
func lockChannelCreateKey(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 selectProxy(count int) (*m.Proxy, g.GatewayClient, error) {
// 获取在线节点
proxies, err := q.Proxy.Where(
q.Proxy.Type.Eq(int(m.ProxyTypeBaiYin)),
q.Proxy.Status.Eq(int(m.ProxyStatusOnline)),
).Find()
if err != nil {
return nil, nil, core.NewBizErr("获取可用代理失败", err)
}
if len(proxies) == 0 {
return nil, nil, core.NewBizErr("无可用代理")
}
proxyIDs := make([]int32, 0, len(proxies))
proxyMap := make(map[int32]*m.Proxy, len(proxies))
for _, item := range proxies {
proxyIDs = append(proxyIDs, item.ID)
proxyMap[item.ID] = item
}
// 获取最空闲节点
maxId := int32(0)
maxCount := -1
for _, id := range proxyIDs {
idCount, err := g.Redis.SCard(context.Background(), freeChansKey(id)).Result()
if err != nil {
return nil, nil, fmt.Errorf("查询可用通道数量失败: %w", err)
}
if idCount > int64(maxCount) {
maxCount = int(idCount)
maxId = id
}
}
if maxCount < count {
return nil, nil, core.NewBizErr("无可用代理")
}
proxy := proxyMap[maxId]
gateway, err := proxyGateway(proxy)
if err != nil {
return nil, nil, core.NewServErr("创建代理网关失败", err)
}
return proxy, gateway, nil
}
func selectPorts(proxyId int32, batchNo string, count int, expire time.Time) ([]netip.AddrPort, error) {
chans, err := lockChans(proxyId, batchNo, count)
if err != nil {
return nil, core.NewBizErr("无可用通道,请稍后再试", err)
}
_, err = g.Asynq.Enqueue(
e.NewRemoveChannel(batchNo),
asynq.ProcessAt(expire),
)
if err != nil {
return nil, core.NewServErr("注册异步关闭通道任务失败", err)
}
return chans, nil
}
// ensureEdges 检查本地节点是否足够,如果不足从云端连入
// 本地节点通过 Assigned = false 排除已分配节点
// 云端节点通过 NoRepeat = true 排除已分配节点
func ensureEdges(proxy *m.Proxy, gateway g.GatewayClient, filter *EdgeFilter, count int) error {
if filter.IsEmpty() {
return nil // 没有过滤条件,直接返回空,避免无意义的查询
}
// 先查本地
localEdges, err := gateway.GatewayEdge(&g.GatewayEdgeReq{
Province: filter.Prov,
City: filter.City,
Isp: u.X(filter.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: filter.Prov,
City: filter.City,
Isp: u.X(filter.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)
} }
slog.Debug("清除代理端口配置", "duration", time.Since(start).String())
return nil return nil
} }
type EdgeInfo struct {
Type EdgeInfoType
EdgeID string
}
type EdgeInfoType string
const (
EdgeInfoLocal EdgeInfoType = "local"
EdgeInfoCloud EdgeInfoType = "cloud"
)

View File

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

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

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

View File

@@ -1,6 +1,7 @@
package services package services
import ( import (
"platform/pkg/u"
m "platform/web/models" m "platform/web/models"
q "platform/web/queries" q "platform/web/queries"
) )
@@ -37,3 +38,15 @@ type EdgeFilter struct {
Prov *string `json:"prov"` Prov *string `json:"prov"`
City *string `json:"city"` City *string `json:"city"`
} }
func (f *EdgeFilter) IsEmpty() bool {
if f == nil {
return true
}
if f.Isp.String() == "" || u.Z(f.Prov) != "" || u.Z(f.City) != "" {
return false
}
return false
}

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ import (
"platform/web/globals/orm" "platform/web/globals/orm"
m "platform/web/models" m "platform/web/models"
q "platform/web/queries" q "platform/web/queries"
"strconv" "strings"
"time" "time"
"gorm.io/gen/field" "gorm.io/gen/field"
@@ -20,13 +20,9 @@ var Proxy = &proxyService{}
type proxyService struct{} type proxyService struct{}
func proxyStatusLockKey(id int32) string {
return fmt.Sprintf("platform:proxy:status:%d", id)
}
func hasUsedChans(proxyID int32) (bool, error) { func hasUsedChans(proxyID int32) (bool, error) {
ctx := context.Background() ctx := context.Background()
pattern := usedChansKey + ":" + strconv.Itoa(int(proxyID)) + ":*" pattern := usedChansKey(proxyID, "*")
keys, _, err := g.Redis.Scan(ctx, 0, pattern, 1).Result() keys, _, err := g.Redis.Scan(ctx, 0, pattern, 1).Result()
if err != nil { if err != nil {
return false, err return false, err
@@ -192,7 +188,7 @@ type UpdateProxyStatus struct {
} }
func (s *proxyService) UpdateStatus(update *UpdateProxyStatus) error { func (s *proxyService) UpdateStatus(update *UpdateProxyStatus) error {
return g.Redsync.WithLock(proxyStatusLockKey(update.ID), func() error { return q.Q.Transaction(func(tx *q.Query) error {
proxy, err := q.Proxy.Where(q.Proxy.ID.Eq(update.ID)).Take() proxy, err := q.Proxy.Where(q.Proxy.ID.Eq(update.ID)).Take()
if err != nil { if err != nil {
return err return err
@@ -214,3 +210,14 @@ func (s *proxyService) UpdateStatus(update *UpdateProxyStatus) error {
return err return err
}) })
} }
func proxyGateway(proxy *m.Proxy) (g.GatewayClient, error) {
secret := strings.Split(u.Z(proxy.Secret), ":")
if len(secret) != 2 {
return nil, core.NewServErr(fmt.Sprintf("代理 %s 密钥格式错误", proxy.IP.String()), nil)
}
gateway := g.NewGateway(proxy.IP.String(), secret[0], secret[1])
return gateway, nil
}

View File

@@ -63,9 +63,10 @@ func (s *resourceService) Create(q *q.Query, uid int32, now time.Time, data *Cre
var resource = m.Resource{ var resource = m.Resource{
UserID: uid, UserID: uid,
ResourceNo: u.P(ID.GenReadable("res")), ResourceNo: u.P(ID.GenReadable("res")),
Active: true,
Type: data.Type, Type: data.Type,
Code: data.Type.Code(), Code: data.Type.Code(),
Active: true,
CheckIP: true,
} }
switch data.Type { switch data.Type {
@@ -129,12 +130,15 @@ func (s *resourceService) Update(data *UpdateResourceData) error {
do = append(do, q.Resource.CheckIP.Value(*data.CheckIP)) do = append(do, q.Resource.CheckIP.Value(*data.CheckIP))
} }
_, err := q.Resource. r, err := q.Resource.
Where(q.Resource.ID.Eq(data.Id)). Where(q.Resource.ID.Eq(data.Id)).
UpdateSimple(do...) UpdateSimple(do...)
if err != nil { if err != nil {
return core.NewServErr("更新套餐失败", err) return core.NewServErr("更新套餐失败", err)
} }
if r.RowsAffected == 0 {
return core.NewBizErr("套餐状态已过期")
}
return nil return nil
} }
@@ -159,6 +163,10 @@ func (s *resourceService) CalcPrice(skuCode string, count int32, user *m.User, c
return nil, nil, nil, decimal.Zero, decimal.Zero, decimal.Zero, core.NewBizErr("产品不可用", err) 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))
}
// 原价 // 原价
amountMin := sku.PriceMin.Mul(decimal.NewFromInt32(count)) amountMin := sku.PriceMin.Mul(decimal.NewFromInt32(count))
amount := sku.Price.Mul(decimal.NewFromInt32(count)) amount := sku.Price.Mul(decimal.NewFromInt32(count))

View File

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

View File

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

View File

@@ -13,6 +13,7 @@ import (
) )
func HandleCompleteTrade(_ context.Context, task *asynq.Task) error { func HandleCompleteTrade(_ context.Context, task *asynq.Task) error {
slog.Info("[event]尝试结束交易")
var event events.CloseTradeData var event events.CloseTradeData
if err := json.Unmarshal(task.Payload(), &event); err != nil { if err := json.Unmarshal(task.Payload(), &event); err != nil {
return fmt.Errorf("解析任务参数失败: %w", err) 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 { 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 { 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) { func HandleRemoveChannel(_ context.Context, task *asynq.Task) (err error) {
batch := string(task.Payload()) batch := string(task.Payload())
slog.Info("[event]删除通道", "batch", batch)
err = s.Channel.RemoveChannels(batch) err = s.Channel.RemoveChannels(batch)
if err != nil { if err != nil {
return fmt.Errorf("删除通道失败: %w", err) return fmt.Errorf("删除通道失败: %w", err)
} }
return nil return nil
} }
func HandleRefreshEdges(_ context.Context, task *asynq.Task) (err error) {
slog.Info("[event]刷新边缘节点")
err = s.Channel.RefreshEdges()
if err != nil {
return fmt.Errorf("刷新边缘节点失败: %w", err)
}
return nil
}

View File

@@ -42,6 +42,10 @@ func RunApp(pCtx context.Context) error {
return RunTask(ctx) return RunTask(ctx)
}) })
g.Go(func() error {
return RunCron(ctx)
})
return g.Wait() return g.Wait()
} }
@@ -80,16 +84,18 @@ func RunWeb(ctx context.Context) error {
} }
func RunTask(ctx context.Context) error { func RunTask(ctx context.Context) error {
var server = asynq.NewServerFromRedisClient(deps.Redis, asynq.Config{ server := asynq.NewServerFromRedisClient(deps.Redis, asynq.Config{
ShutdownTimeout: 5 * time.Second, ShutdownTimeout: 5 * time.Second,
ErrorHandler: asynq.ErrorHandlerFunc(func(ctx context.Context, task *asynq.Task, err error) { ErrorHandler: asynq.ErrorHandlerFunc(func(ctx context.Context, task *asynq.Task, err error) {
slog.Error("任务执行失败", "task", task.Type(), "error", err) slog.Error("任务执行失败", "task", task.Type(), "error", err)
}), }),
Logger: &AppAsynqLogger{},
}) })
var mux = asynq.NewServeMux() var mux = asynq.NewServeMux()
mux.HandleFunc(events.RemoveChannel, tasks.HandleRemoveChannel) mux.HandleFunc(events.RemoveChannel, tasks.HandleRemoveChannel)
mux.HandleFunc(events.CloseTrade, tasks.HandleCompleteTrade) mux.HandleFunc(events.CloseTrade, tasks.HandleCompleteTrade)
mux.HandleFunc(events.RefreshEdge, tasks.HandleRefreshEdges)
// 停止服务 // 停止服务
go func() { go func() {
@@ -105,3 +111,56 @@ func RunTask(ctx context.Context) error {
return nil return nil
} }
func RunCron(ctx context.Context) error {
cron := asynq.NewSchedulerFromRedisClient(deps.Redis, &asynq.SchedulerOpts{
Logger: &AppAsynqLogger{},
Location: time.Local,
})
cron.Register("0/10 * * * *", events.NewRefreshEdge())
// 停止服务
go func() {
<-ctx.Done()
cron.Shutdown()
}()
// 启动服务
err := cron.Run()
if err != nil {
return fmt.Errorf("定时任务服务运行失败: %w", err)
}
return nil
}
type AppAsynqLogger struct{}
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
}