Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a0b0be2b8e | |||
| 8fc1d30578 | |||
| a4d9c28702 | |||
| ccb8db555e | |||
| e70f2337cb | |||
| d59f4ca37f | |||
| 0edc883084 | |||
| d26106eb00 |
12
README.md
12
README.md
@@ -1,8 +1,16 @@
|
||||
## TODO
|
||||
|
||||
proxy 的删除和更新,锁粒度应该有问题
|
||||
- 选择代理部分,可以检查 redis 中的可用端口数量,无需查数据库
|
||||
- 取用端口后直接写入关闭任务,避免中途失败导致端口泄漏
|
||||
-
|
||||
|
||||
最低价格 0.01
|
||||
---
|
||||
|
||||
错误提示增强,展示整链路信息
|
||||
|
||||
ip 提取频率限制,在 ensure 函数加逻辑,通过 redis 或者 pg 计算分钟内提取次数,只允许每分钟提取 30 次
|
||||
|
||||
proxy 的删除和更新,锁粒度应该有问题
|
||||
|
||||
交易信息持久化
|
||||
|
||||
|
||||
@@ -127,7 +127,8 @@ insert into permission (name, description, sort) values
|
||||
('trade', '交易', 12),
|
||||
('bill', '账单', 13),
|
||||
('balance_activity', '余额变动', 14),
|
||||
('proxy', '代理', 15);
|
||||
('proxy', '代理', 15),
|
||||
('coupon_user', '已发放优惠券', 16);
|
||||
|
||||
-- --------------------------
|
||||
-- level 2
|
||||
@@ -136,79 +137,84 @@ insert into permission (name, description, sort) values
|
||||
-- permission 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'permission' and deleted_at is null), 'permission:read', '读取权限列表', 1),
|
||||
((select id from permission where name = 'permission' and deleted_at is null), 'permission:write', '写入权限', 2);
|
||||
((select id from permission where name = 'permission' and deleted_at is null), 'permission:write', '编辑权限', 2);
|
||||
|
||||
-- admin_role 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'admin_role' and deleted_at is null), 'admin_role:read', '读取管理员角色列表', 1),
|
||||
((select id from permission where name = 'admin_role' and deleted_at is null), 'admin_role:write', '写入管理员角色', 2);
|
||||
((select id from permission where name = 'admin_role' and deleted_at is null), 'admin_role:write', '编辑管理员角色', 2);
|
||||
|
||||
-- admin 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'admin' and deleted_at is null), 'admin:read', '读取管理员列表', 1),
|
||||
((select id from permission where name = 'admin' and deleted_at is null), 'admin:write', '写入管理员', 2);
|
||||
((select id from permission where name = 'admin' and deleted_at is null), 'admin:write', '编辑管理员', 2);
|
||||
|
||||
-- product 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'product' and deleted_at is null), 'product:read', '读取产品列表', 1),
|
||||
((select id from permission where name = 'product' and deleted_at is null), 'product:write', '写入产品', 2);
|
||||
((select id from permission where name = 'product' and deleted_at is null), 'product:write', '编辑产品', 2);
|
||||
|
||||
-- product_sku 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'product_sku' and deleted_at is null), 'product_sku:read', '读取产品套餐列表', 1),
|
||||
((select id from permission where name = 'product_sku' and deleted_at is null), 'product_sku:write', '写入产品套餐', 2);
|
||||
((select id from permission where name = 'product_sku' and deleted_at is null), 'product_sku:write', '编辑产品套餐', 2);
|
||||
|
||||
-- discount 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'discount' and deleted_at is null), 'discount:read', '读取折扣列表', 1),
|
||||
((select id from permission where name = 'discount' and deleted_at is null), 'discount:write', '写入折扣', 2);
|
||||
((select id from permission where name = 'discount' and deleted_at is null), 'discount:write', '编辑折扣', 2);
|
||||
|
||||
-- resource 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'resource' and deleted_at is null), 'resource:read', '读取用户套餐列表', 1),
|
||||
((select id from permission where name = 'resource' and deleted_at is null), 'resource:write', '写入用户套餐', 2),
|
||||
((select id from permission where name = 'resource' and deleted_at is null), 'resource:write', '编辑用户套餐', 2),
|
||||
((select id from permission where name = 'resource' and deleted_at is null), 'resource:short', '短效动态套餐', 3),
|
||||
((select id from permission where name = 'resource' and deleted_at is null), 'resource:long', '长效动态套餐', 4);
|
||||
|
||||
-- user 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'user' and deleted_at is null), 'user:read', '读取用户列表', 1),
|
||||
((select id from permission where name = 'user' and deleted_at is null), 'user:write', '写入用户', 2);
|
||||
((select id from permission where name = 'user' and deleted_at is null), 'user:write', '编辑用户', 2);
|
||||
|
||||
-- coupon 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'coupon' and deleted_at is null), 'coupon:read', '读取优惠券列表', 1),
|
||||
((select id from permission where name = 'coupon' and deleted_at is null), 'coupon:write', '写入优惠券', 2);
|
||||
((select id from permission where name = 'coupon' and deleted_at is null), 'coupon:write', '编辑优惠券', 2);
|
||||
|
||||
-- batch 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'batch' and deleted_at is null), 'batch:read', '读取批次列表', 1),
|
||||
((select id from permission where name = 'batch' and deleted_at is null), 'batch:write', '写入批次', 2);
|
||||
((select id from permission where name = 'batch' and deleted_at is null), 'batch:write', '编辑批次', 2);
|
||||
|
||||
-- channel 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'channel' and deleted_at is null), 'channel:read', '读取 IP 列表', 1),
|
||||
((select id from permission where name = 'channel' and deleted_at is null), 'channel:write', '写入 IP', 2);
|
||||
((select id from permission where name = 'channel' and deleted_at is null), 'channel:write', '编辑 IP', 2);
|
||||
|
||||
-- proxy 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'proxy' and deleted_at is null), 'proxy:read', '读取代理列表', 1),
|
||||
((select id from permission where name = 'proxy' and deleted_at is null), 'proxy:write', '写入代理', 2);
|
||||
((select id from permission where name = 'proxy' and deleted_at is null), 'proxy:write', '编辑代理', 2);
|
||||
|
||||
-- trade 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'trade' and deleted_at is null), 'trade:read', '读取交易列表', 1),
|
||||
((select id from permission where name = 'trade' and deleted_at is null), 'trade:write', '写入交易', 2);
|
||||
((select id from permission where name = 'trade' and deleted_at is null), 'trade:write', '编辑交易', 2);
|
||||
|
||||
-- bill 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'bill' and deleted_at is null), 'bill:read', '读取账单列表', 1),
|
||||
((select id from permission where name = 'bill' and deleted_at is null), 'bill:write', '写入账单', 2);
|
||||
((select id from permission where name = 'bill' and deleted_at is null), 'bill:write', '编辑账单', 2);
|
||||
|
||||
-- balance_activity 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'balance_activity' and deleted_at is null), 'balance_activity:read', '读取余额变动列表', 1);
|
||||
|
||||
-- coupon_user 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'coupon_user' and deleted_at is null), 'coupon_user:read', '读取已发放优惠券列表', 1),
|
||||
((select id from permission where name = 'coupon_user' and deleted_at is null), 'coupon_user:write', '编辑已发放优惠券', 2);
|
||||
|
||||
-- --------------------------
|
||||
-- level 3
|
||||
-- --------------------------
|
||||
@@ -236,7 +242,7 @@ insert into permission (parent_id, name, description, sort) values
|
||||
|
||||
-- user:write 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'user:write' and deleted_at is null), 'user:write:balance', '写入用户余额', 1),
|
||||
((select id from permission where name = 'user:write' and deleted_at is null), 'user:write:balance', '编辑用户余额', 1),
|
||||
((select id from permission where name = 'user:write' and deleted_at is null), 'user:write:bind', '用户认领', 2);
|
||||
|
||||
-- batch:read 子权限
|
||||
@@ -259,6 +265,14 @@ insert into permission (parent_id, name, description, sort) values
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = '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
|
||||
-- --------------------------
|
||||
|
||||
@@ -1111,7 +1111,7 @@ comment on table coupon_user is '优惠券发放表';
|
||||
comment on column coupon_user.id is '记录ID';
|
||||
comment on column coupon_user.coupon_id is '优惠券ID';
|
||||
comment on column coupon_user.user_id is '用户ID';
|
||||
comment on column coupon_user.status is '使用状态:0-未使用,1-已使用';
|
||||
comment on column coupon_user.status is '使用状态:0-未使用,1-已使用,2-已禁用';
|
||||
comment on column coupon_user.expire_at is '过期时间';
|
||||
comment on column coupon_user.used_at is '使用时间';
|
||||
comment on column coupon_user.created_at is '创建时间';
|
||||
|
||||
@@ -118,7 +118,7 @@ func Query(in any) url.Values {
|
||||
case int:
|
||||
out.Add(name, strconv.Itoa(value))
|
||||
case bool:
|
||||
if tags[1] == "b2i" {
|
||||
if len(tags) > 1 && tags[1] == "b2i" {
|
||||
out.Add(name, u.Ternary(value, "1", "0"))
|
||||
} else {
|
||||
out.Add(name, strconv.FormatBool(value))
|
||||
|
||||
@@ -51,6 +51,12 @@ const (
|
||||
ScopeCoupon = string("coupon") // 优惠券
|
||||
ScopeCouponRead = string("coupon:read") // 读取优惠券列表
|
||||
ScopeCouponWrite = string("coupon:write") // 写入优惠券
|
||||
ScopeCouponWriteAssign = string("coupon:write:assign") // 发放优惠券
|
||||
|
||||
ScopeCouponUser = string("coupon_user") // 用户优惠券
|
||||
ScopeCouponUserRead = string("coupon_user:read") // 读取用户优惠券列表
|
||||
ScopeCouponUserReadOfUser = string("coupon_user:read:of_user") // 读取指定用户的用户优惠券列表
|
||||
ScopeCouponUserWrite = string("coupon_user:write") // 写入用户优惠券
|
||||
|
||||
ScopeBatch = string("batch") // 批次
|
||||
ScopeBatchRead = string("batch:read") // 读取批次列表
|
||||
|
||||
@@ -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(message)
|
||||
c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
|
||||
return c.Status(code).SendString(message)
|
||||
}
|
||||
|
||||
@@ -9,3 +9,9 @@ const RemoveChannel = "channel:remove"
|
||||
func NewRemoveChannel(batch string) *asynq.Task {
|
||||
return asynq.NewTask(RemoveChannel, []byte(batch))
|
||||
}
|
||||
|
||||
const ClearExpiredChannels = "channel:clear_expired"
|
||||
|
||||
func NewClearExpiredChannels() *asynq.Task {
|
||||
return asynq.NewTask(ClearExpiredChannels, nil)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,65 @@ import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// PageBalanceActivity 分页查询当前用户的余额变动记录
|
||||
func PageBalanceActivity(c *fiber.Ctx) error {
|
||||
// 获取当前用户ID
|
||||
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 解析请求参数
|
||||
req := new(PageBalanceActivityByUserReq)
|
||||
if err := g.Validator.ParseBody(c, req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 构造查询条件
|
||||
do := q.BalanceActivity.Where(q.BalanceActivity.UserID.Eq(authCtx.User.ID))
|
||||
if req.BillNo != nil {
|
||||
do = do.Where(q.Bill.As("Bill").BillNo.Eq(*req.BillNo))
|
||||
}
|
||||
if req.CreatedAtStart != nil {
|
||||
t := u.DateHead(*req.CreatedAtStart)
|
||||
do = do.Where(q.BalanceActivity.CreatedAt.Gte(t))
|
||||
}
|
||||
if req.CreatedAtEnd != nil {
|
||||
t := u.DateTail(*req.CreatedAtEnd)
|
||||
do = do.Where(q.BalanceActivity.CreatedAt.Lte(t))
|
||||
}
|
||||
|
||||
// 查询余额变动列表
|
||||
list, total, err := q.BalanceActivity.
|
||||
Joins(q.BalanceActivity.Bill).
|
||||
Select(
|
||||
q.BalanceActivity.ALL,
|
||||
q.Bill.As("Bill").ID.As("Bill__id"),
|
||||
q.Bill.As("Bill").BillNo.As("Bill__bill_no"),
|
||||
).
|
||||
Where(do).
|
||||
Order(q.BalanceActivity.CreatedAt.Desc()).
|
||||
FindByPage(req.GetOffset(), req.GetLimit())
|
||||
if err != nil {
|
||||
return core.NewBizErr("获取数据失败", err)
|
||||
}
|
||||
|
||||
// 返回结果
|
||||
return c.JSON(core.PageResp{
|
||||
List: list,
|
||||
Total: int(total),
|
||||
Page: req.GetPage(),
|
||||
Size: req.GetSize(),
|
||||
})
|
||||
}
|
||||
|
||||
type PageBalanceActivityByUserReq struct {
|
||||
core.PageReq
|
||||
BillNo *string `json:"bill_no,omitempty"`
|
||||
CreatedAtStart *time.Time `json:"created_at_start,omitempty"`
|
||||
CreatedAtEnd *time.Time `json:"created_at_end,omitempty"`
|
||||
}
|
||||
|
||||
// PageBalanceActivityByAdmin 分页查询所有余额变动记录
|
||||
func PageBalanceActivityByAdmin(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
|
||||
@@ -172,12 +172,7 @@ type ListChannelsReq struct {
|
||||
|
||||
// CreateChannel 创建新通道
|
||||
func CreateChannel(c *fiber.Ctx) error {
|
||||
|
||||
// 检查权限
|
||||
_, err := auth.GetAuthCtx(c).PermitUser()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 不检查权限,允许 api 调用
|
||||
|
||||
// 解析参数
|
||||
req := new(CreateChannelReq)
|
||||
@@ -217,6 +212,7 @@ func CreateChannel(c *fiber.Ctx) error {
|
||||
resp[i] = &CreateChannelRespItem{
|
||||
Proto: req.Protocol,
|
||||
Host: channel.Host,
|
||||
IP: channel.Proxy.IP.String(),
|
||||
Port: channel.Port,
|
||||
}
|
||||
if req.AuthType == s.ChannelAuthTypePass {
|
||||
@@ -240,6 +236,7 @@ type CreateChannelReq struct {
|
||||
type CreateChannelRespItem struct {
|
||||
Proto int `json:"-"`
|
||||
Host string `json:"host"`
|
||||
IP string `json:"ip"`
|
||||
Port uint16 `json:"port"`
|
||||
Username *string `json:"username,omitempty"`
|
||||
Password *string `json:"password,omitempty"`
|
||||
|
||||
@@ -103,3 +103,27 @@ func DeleteCoupon(c *fiber.Ctx) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func AssignCoupon(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeCouponWriteAssign)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req AssignCouponReq
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.Coupon.Assign(req.CouponID, req.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type AssignCouponReq struct {
|
||||
CouponID int32 `json:"coupon_id" validate:"required"`
|
||||
UserID int32 `json:"user_id" validate:"required"`
|
||||
}
|
||||
|
||||
330
web/handlers/coupon_user.go
Normal file
330
web/handlers/coupon_user.go
Normal file
@@ -0,0 +1,330 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"platform/pkg/u"
|
||||
"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()))
|
||||
} else {
|
||||
conds = append(conds, q.CouponUser.Where(q.CouponUser.ExpireAt.IsNull()).Or(q.CouponUser.ExpireAt.Gt(time.Now())))
|
||||
}
|
||||
}
|
||||
if req.CreatedAtStart != nil {
|
||||
conds = append(conds, q.CouponUser.CreatedAt.Gte(u.DateHead(*req.CreatedAtStart)))
|
||||
}
|
||||
if req.CreatedAtEnd != nil {
|
||||
conds = append(conds, q.CouponUser.CreatedAt.Lte(u.DateTail(*req.CreatedAtEnd)))
|
||||
}
|
||||
if req.ExpireAtStart != nil {
|
||||
conds = append(conds, q.CouponUser.ExpireAt.Gte(u.DateHead(*req.ExpireAtStart)))
|
||||
}
|
||||
if req.ExpireAtEnd != nil {
|
||||
conds = append(conds, q.CouponUser.ExpireAt.Lte(u.DateTail(*req.ExpireAtEnd)))
|
||||
}
|
||||
if req.UsedAtStart != nil {
|
||||
conds = append(conds, q.CouponUser.UsedAt.Gte(u.DateHead(*req.UsedAtStart)))
|
||||
}
|
||||
if req.UsedAtEnd != nil {
|
||||
conds = append(conds, q.CouponUser.UsedAt.Lte(u.DateTail(*req.UsedAtEnd)))
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -84,6 +84,7 @@ func PageResourceShort(c *fiber.Ctx) error {
|
||||
total = int64(len(resource) + req.GetOffset())
|
||||
} else {
|
||||
total, err = q.Resource.
|
||||
Joins(q.Resource.Short).
|
||||
Where(do).
|
||||
Count()
|
||||
if err != nil {
|
||||
@@ -180,6 +181,7 @@ func PageResourceLong(c *fiber.Ctx) error {
|
||||
total = int64(len(resource) + req.GetOffset())
|
||||
} else {
|
||||
total, err = q.Resource.
|
||||
Joins(q.Resource.Long).
|
||||
Where(do).
|
||||
Count()
|
||||
if err != nil {
|
||||
@@ -609,6 +611,30 @@ func UpdateResourceByAdmin(c *fiber.Ctx) error {
|
||||
return c.JSON(nil)
|
||||
}
|
||||
|
||||
func UpdateResourceCheckIP(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitUser()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req struct {
|
||||
core.IdReq
|
||||
CheckIP bool `json:"checkip"`
|
||||
}
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.Resource.Update(&s.UpdateResourceData{
|
||||
IdReq: req.IdReq,
|
||||
CheckIP: &req.CheckIP,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(nil)
|
||||
}
|
||||
|
||||
// StatisticResourceFree 统计每日可用
|
||||
func StatisticResourceFree(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
|
||||
@@ -260,7 +260,7 @@ type UpdateUserBalanceByAdminData struct {
|
||||
// 绑定管理员
|
||||
func BindAdmin(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
authCtx, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeUserWrite)
|
||||
authCtx, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeUserWriteBind)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"platform/pkg/env"
|
||||
"platform/pkg/u"
|
||||
"platform/web/auth"
|
||||
@@ -97,6 +98,18 @@ func CreateWhitelist(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// 创建白名单
|
||||
uid := authCtx.User.ID
|
||||
err = g.Redsync.WithLock(whitelistKey(uid), func() error {
|
||||
count, err := q.Whitelist.Where(
|
||||
q.Whitelist.UserID.Eq(uid),
|
||||
).Count()
|
||||
if err != nil {
|
||||
return core.NewServErr("获取白名单数量失败", err)
|
||||
}
|
||||
if count >= 5 {
|
||||
return core.NewBizErr("白名单数量已达上限")
|
||||
}
|
||||
|
||||
err = q.Whitelist.Create(&m.Whitelist{
|
||||
UserID: authCtx.User.ID,
|
||||
IP: u.Z(ip),
|
||||
@@ -106,6 +119,12 @@ func CreateWhitelist(c *fiber.Ctx) error {
|
||||
return core.NewServErr("添加白名单失败", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -206,3 +225,7 @@ func secureAddr(str string) (*orm.Inet, error) {
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
func whitelistKey(userID int32) string {
|
||||
return fmt.Sprintf("platform:whitelist:add:%d", userID)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ type CouponUser struct {
|
||||
ID int32 `json:"id" gorm:"column:id;primaryKey"` // 记录ID
|
||||
CouponID int32 `json:"coupon_id" gorm:"column:coupon_id"` // 优惠券ID
|
||||
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
||||
Status CouponStatus `json:"status" gorm:"column:status"` // 使用状态:0-未使用,1-已使用
|
||||
Status CouponUserStatus `json:"status" gorm:"column:status"` // 使用状态:0-未使用,1-已使用,2-已禁用
|
||||
ExpireAt *time.Time `json:"expire_at,omitempty" gorm:"column:expire_at"` // 过期时间
|
||||
UsedAt *time.Time `json:"used_at,omitempty" gorm:"column:used_at"` // 使用时间
|
||||
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"` // 创建时间
|
||||
@@ -22,4 +22,5 @@ type CouponUserStatus int
|
||||
const (
|
||||
CouponUserStatusUnused CouponUserStatus = 0 // 未使用
|
||||
CouponUserStatusUsed CouponUserStatus = 1 // 已使用
|
||||
CouponUserStatusDisabled CouponUserStatus = 2 // 已禁用
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"platform/pkg/env"
|
||||
auth2 "platform/web/auth"
|
||||
"platform/web/handlers"
|
||||
s "platform/web/services"
|
||||
"time"
|
||||
|
||||
q "platform/web/queries"
|
||||
@@ -33,6 +34,12 @@ func ApplyRouters(app *fiber.App) {
|
||||
}
|
||||
return ctx.JSON(rs)
|
||||
})
|
||||
debug.Get("/channel/clear-expired", func(ctx *fiber.Ctx) error {
|
||||
if err := s.Channel.ClearExpiredChannels(); err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.SendStatus(fiber.StatusOK)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +73,7 @@ func userRouter(api fiber.Router) {
|
||||
resource.Post("/list/short", handlers.PageResourceShort)
|
||||
resource.Post("/list/long", handlers.PageResourceLong)
|
||||
resource.Post("/create", handlers.CreateResource)
|
||||
resource.Post("/update/checkip", handlers.UpdateResourceCheckIP)
|
||||
|
||||
resource.Post("/statistics/free", handlers.StatisticResourceFree)
|
||||
resource.Post("/statistics/usage", handlers.StatisticResourceUsage)
|
||||
@@ -90,6 +98,15 @@ func userRouter(api fiber.Router) {
|
||||
bill := api.Group("/bill")
|
||||
bill.Post("/list", handlers.ListBill)
|
||||
|
||||
// 余额变动
|
||||
balance := api.Group("/balance")
|
||||
balance.Post("/page", handlers.PageBalanceActivity)
|
||||
|
||||
// 已发放优惠券
|
||||
couponUser := api.Group("/coupon-user")
|
||||
couponUser.Post("/page", handlers.PageCouponUser)
|
||||
couponUser.Post("/get", handlers.GetCouponUser)
|
||||
|
||||
// 公告
|
||||
announcement := api.Group("/announcement")
|
||||
announcement.Post("/list", handlers.ListAnnouncements)
|
||||
@@ -248,4 +265,14 @@ func adminRouter(api fiber.Router) {
|
||||
coupon.Post("/create", handlers.CreateCoupon)
|
||||
coupon.Post("/update", handlers.UpdateCoupon)
|
||||
coupon.Post("/remove", handlers.DeleteCoupon)
|
||||
coupon.Post("/update/assign", handlers.AssignCoupon)
|
||||
|
||||
// coupon-user 已发放优惠券
|
||||
var couponUser = api.Group("/coupon-user")
|
||||
couponUser.Post("/page", handlers.PageCouponUserByAdmin)
|
||||
couponUser.Post("/page/of-user", handlers.PageCouponUserOfUserByAdmin)
|
||||
couponUser.Post("/get", handlers.GetCouponUserByAdmin)
|
||||
couponUser.Post("/create", handlers.CreateCouponUserByAdmin)
|
||||
couponUser.Post("/update", handlers.UpdateCouponUserByAdmin)
|
||||
couponUser.Post("/remove", handlers.DeleteCouponUserByAdmin)
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ var Channel = &channelServer{
|
||||
type ChannelServiceProvider interface {
|
||||
CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter *EdgeFilter) ([]*m.Channel, error)
|
||||
RemoveChannels(batch string) error
|
||||
ClearExpiredChannels() error
|
||||
}
|
||||
|
||||
type channelServer struct {
|
||||
@@ -40,6 +41,10 @@ func (s *channelServer) RemoveChannels(batch string) error {
|
||||
return s.provider.RemoveChannels(batch)
|
||||
}
|
||||
|
||||
func (s *channelServer) ClearExpiredChannels() error {
|
||||
return s.provider.ClearExpiredChannels()
|
||||
}
|
||||
|
||||
// 授权方式
|
||||
type ChannelAuthType int
|
||||
|
||||
@@ -147,7 +152,7 @@ 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, resourceId int32, authWhitelist bool, count int) (*ResourceView, []string, error) {
|
||||
if count > 400 {
|
||||
return nil, nil, core.NewBizErr("单次最多提取 400 个")
|
||||
}
|
||||
@@ -172,6 +177,10 @@ func ensure(now time.Time, source netip.Addr, resourceId int32, count int) (*Res
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if authWhitelist && len(whitelists) == 0 {
|
||||
return nil, nil, core.NewBizErr("当前白名单为空,请先添加白名单")
|
||||
}
|
||||
|
||||
ips := make([]string, len(whitelists))
|
||||
pass := false
|
||||
for i, item := range whitelists {
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"gorm.io/gen"
|
||||
"gorm.io/gen/field"
|
||||
)
|
||||
|
||||
@@ -32,7 +31,7 @@ func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int
|
||||
batch := ID.GenReadable("bat")
|
||||
|
||||
// 检查并获取套餐与白名单
|
||||
resource, whitelists, err := ensure(now, source, resourceId, count)
|
||||
resource, whitelists, err := ensure(now, source, resourceId, authWhitelist, count)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -63,7 +62,7 @@ func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int
|
||||
}
|
||||
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()
|
||||
@@ -86,30 +85,30 @@ func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int
|
||||
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"),
|
||||
})
|
||||
_, err = g.Asynq.Enqueue(
|
||||
e.NewRemoveChannel(batch),
|
||||
asynq.ProcessAt(expire),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, core.NewBizErr("获取可用节点失败", err)
|
||||
return nil, core.NewServErr("提交关闭通道任务失败", err)
|
||||
}
|
||||
if edgesResp.Total != count && len(edgesResp.Edges) != count {
|
||||
return nil, core.NewBizErr("地区可用节点数量不足")
|
||||
}
|
||||
edges := edgesResp.Edges
|
||||
|
||||
// 准备通道数据
|
||||
// 取用节点
|
||||
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])
|
||||
|
||||
edges, err := getAvailableEdges(gateway, filter, count)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 绑定节点到端口
|
||||
channels := make([]*m.Channel, count)
|
||||
chanConfigs := make([]*g.PortConfigsReq, count)
|
||||
edgeConfigs := make([]string, count)
|
||||
edgeConfigs := make([]string, 0, count)
|
||||
for i := range count {
|
||||
ch := chans[i]
|
||||
edge := edges[i]
|
||||
@@ -127,6 +126,7 @@ func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int
|
||||
FilterProv: filter.Prov,
|
||||
FilterCity: filter.City,
|
||||
ExpiredAt: expire,
|
||||
Proxy: &proxy,
|
||||
}
|
||||
|
||||
// 通道配置数据
|
||||
@@ -151,29 +151,18 @@ func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int
|
||||
}
|
||||
|
||||
// 连接配置数据
|
||||
edgeConfigs[i] = edge.EdgeID
|
||||
if edge.Type == EdgeInfoCloud {
|
||||
edgeConfigs = append(edgeConfigs, 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.
|
||||
// 更新使用记录
|
||||
var err error
|
||||
switch resource.Type {
|
||||
case m.ResourceTypeShort:
|
||||
_, err = q.ResourceShort.
|
||||
Where(
|
||||
q.ResourceShort.ID.Eq(*resource.ShortId),
|
||||
q.ResourceShort.Used.Eq(resource.Used),
|
||||
@@ -185,8 +174,8 @@ func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int
|
||||
q.ResourceShort.LastAt.Value(now),
|
||||
)
|
||||
|
||||
case isLongType:
|
||||
rs, err = q.ResourceLong.
|
||||
case m.ResourceTypeLong:
|
||||
_, err = q.ResourceLong.
|
||||
Where(
|
||||
q.ResourceLong.ID.Eq(*resource.LongId),
|
||||
q.ResourceLong.Used.Eq(resource.Used),
|
||||
@@ -204,9 +193,6 @@ func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int
|
||||
if err != nil {
|
||||
return core.NewServErr("更新套餐使用记录失败", err)
|
||||
}
|
||||
if rs.RowsAffected == 0 {
|
||||
return core.NewServErr("套餐使用记录不存在")
|
||||
}
|
||||
|
||||
// 保存通道
|
||||
err = q.Channel.
|
||||
@@ -239,8 +225,6 @@ func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int
|
||||
}
|
||||
|
||||
// 提交配置
|
||||
secret := strings.Split(u.Z(proxy.Secret), ":")
|
||||
gateway := g.NewGateway(proxy.IP.String(), secret[0], secret[1])
|
||||
if env.RunMode == env.RunModeProd {
|
||||
|
||||
// 连接节点到网关
|
||||
@@ -255,10 +239,11 @@ func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int
|
||||
// 启用网关代理通道
|
||||
err = gateway.GatewayPortConfigs(chanConfigs)
|
||||
if err != nil {
|
||||
slog.Warn("提交代理端口配置失败", "error", err.Error())
|
||||
return nil, core.NewServErr(fmt.Sprintf("配置代理 %s 端口失败", proxy.IP.String()), err)
|
||||
}
|
||||
} else {
|
||||
slog.Debug("提交代理端口配置", "proxy", proxy.IP.String())
|
||||
slog.Debug("提交代理端口配置", "proxy", proxy.IP.String(), "count", len(chanConfigs), "remote", len(edgeConfigs))
|
||||
for _, item := range chanConfigs {
|
||||
str, _ := json.Marshal(item)
|
||||
fmt.Println(string(str))
|
||||
@@ -306,12 +291,6 @@ func (s *channelBaiyinProvider) RemoveChannels(batch string) error {
|
||||
// 提交配置
|
||||
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])
|
||||
@@ -319,8 +298,18 @@ func (s *channelBaiyinProvider) RemoveChannels(batch string) error {
|
||||
if err != nil {
|
||||
return core.NewServErr(fmt.Sprintf("清空代理 %s 端口配置失败", proxy.IP.String()), err)
|
||||
}
|
||||
|
||||
// 断开节点连接
|
||||
_, err = g.Cloud.CloudDisconnect(&g.CloudDisconnectReq{
|
||||
Uuid: proxy.Mac,
|
||||
Edge: &edgeConfigs,
|
||||
})
|
||||
if err != nil {
|
||||
slog.Warn("断开云平台连接失败", "error", err.Error())
|
||||
return core.NewServErr("断开云平台连接失败", err)
|
||||
}
|
||||
|
||||
} else {
|
||||
slog.Debug("清除代理端口配置", "proxy", proxy.IP)
|
||||
for _, item := range configs {
|
||||
str, _ := json.Marshal(item)
|
||||
fmt.Println(string(str))
|
||||
@@ -333,6 +322,91 @@ func (s *channelBaiyinProvider) RemoveChannels(batch string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
slog.Debug("清除代理端口配置", "duration", time.Since(start).String())
|
||||
slog.Debug("清除代理端口配置", "proxy", proxy.IP.String(), "batch", batch, "duration", time.Since(start).String())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *channelBaiyinProvider) ClearExpiredChannels() error {
|
||||
now := time.Now().Add(time.Hour)
|
||||
var batches []struct{ BatchNo string }
|
||||
err := q.Channel.Select(q.Channel.BatchNo).Where(q.Channel.ExpiredAt.Lt(now)).Group(q.Channel.BatchNo).Scan(&batches)
|
||||
if err != nil {
|
||||
return core.NewServErr("查询过期通道失败", err)
|
||||
}
|
||||
|
||||
for _, batch := range batches {
|
||||
err := s.RemoveChannels(batch.BatchNo)
|
||||
if err != nil {
|
||||
slog.Error("清理过期通道失败", "batch", batch.BatchNo, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getAvailableEdges(gateway g.GatewayClient, filter *EdgeFilter, count int) ([]EdgeInfo, error) {
|
||||
edges := make([]EdgeInfo, 0, count)
|
||||
|
||||
// 先查本地
|
||||
localEdgesResp, 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 nil, core.NewBizErr("获取可用节点失败[1]", err)
|
||||
}
|
||||
|
||||
for id, _ := range localEdgesResp {
|
||||
edges = append(edges, EdgeInfo{
|
||||
Type: EdgeInfoLocal,
|
||||
EdgeID: id,
|
||||
})
|
||||
}
|
||||
if len(edges) >= count {
|
||||
return edges, nil
|
||||
}
|
||||
|
||||
// 再查云端无重复
|
||||
remaining := count - len(edges)
|
||||
cloudEdgesResp, 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 nil, core.NewBizErr("获取可用节点失败[2]", err)
|
||||
}
|
||||
|
||||
for _, edge := range cloudEdgesResp.Edges {
|
||||
edges = append(edges, EdgeInfo{
|
||||
Type: EdgeInfoCloud,
|
||||
EdgeID: edge.EdgeID,
|
||||
})
|
||||
}
|
||||
if len(edges) >= count {
|
||||
return edges, nil
|
||||
}
|
||||
|
||||
// 不能和已有的重复,如果有重复则再次查询云端补足,二次提取还有重复则放弃
|
||||
|
||||
return nil, core.NewBizErr("地区可用节点数量不足")
|
||||
}
|
||||
|
||||
type EdgeInfo struct {
|
||||
Type EdgeInfoType
|
||||
EdgeID string
|
||||
}
|
||||
|
||||
type EdgeInfoType string
|
||||
|
||||
const (
|
||||
EdgeInfoLocal EdgeInfoType = "local"
|
||||
EdgeInfoCloud EdgeInfoType = "cloud"
|
||||
)
|
||||
|
||||
@@ -68,14 +68,24 @@ func (s *couponService) Update(data UpdateCouponData) error {
|
||||
do = append(do, q.Coupon.Status.Value(int(*data.Status)))
|
||||
}
|
||||
if data.ExpireType != nil {
|
||||
switch *data.ExpireType {
|
||||
case m.CouponExpireTypeNever:
|
||||
do = append(do, q.Coupon.ExpireAt.Null(), q.Coupon.ExpireIn.Null())
|
||||
|
||||
case m.CouponExpireTypeFixed:
|
||||
if data.ExpireAt == nil {
|
||||
return core.NewBizErr("expire_at 不能为空")
|
||||
}
|
||||
do = append(do, q.Coupon.ExpireAt.Value(*data.ExpireAt), q.Coupon.ExpireIn.Null())
|
||||
|
||||
case m.CouponExpireTypeRelative:
|
||||
if data.ExpireIn == nil {
|
||||
return core.NewBizErr("expire_in 不能为空")
|
||||
}
|
||||
do = append(do, q.Coupon.ExpireAt.Null(), q.Coupon.ExpireIn.Value(*data.ExpireIn))
|
||||
}
|
||||
do = append(do, q.Coupon.ExpireType.Value(int(*data.ExpireType)))
|
||||
}
|
||||
if data.ExpireAt != nil {
|
||||
do = append(do, q.Coupon.ExpireAt.Value(*data.ExpireAt))
|
||||
}
|
||||
if data.ExpireIn != nil {
|
||||
do = append(do, q.Coupon.ExpireIn.Value(*data.ExpireIn))
|
||||
}
|
||||
|
||||
_, err := q.Coupon.Where(q.Coupon.ID.Eq(data.ID)).UpdateSimple(do...)
|
||||
return err
|
||||
@@ -98,6 +108,13 @@ func (s *couponService) Delete(id int32) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *couponService) Assign(couponID int32, userID int32) error {
|
||||
return CouponUser.Create(CreateCouponUserData{
|
||||
CouponID: couponID,
|
||||
UserID: userID,
|
||||
})
|
||||
}
|
||||
|
||||
// GetUserCoupon 获取用户的指定优惠券
|
||||
func (s *couponService) GetUserCoupon(uid int32, cuid int32, amount decimal.Decimal) (*m.CouponUser, error) {
|
||||
// 获取优惠券
|
||||
@@ -105,7 +122,7 @@ func (s *couponService) GetUserCoupon(uid int32, cuid int32, amount decimal.Deci
|
||||
q.CouponUser.ID.Eq(cuid),
|
||||
q.CouponUser.UserID.Eq(uid),
|
||||
q.CouponUser.Status.Eq(int(m.CouponUserStatusUnused)),
|
||||
q.CouponUser.ExpireAt.Gt(time.Now()),
|
||||
q.CouponUser.Where(q.CouponUser.ExpireAt.IsNull()).Or(q.CouponUser.ExpireAt.Gt(time.Now())),
|
||||
).Take()
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, core.NewBizErr("优惠券不存在或已失效")
|
||||
|
||||
255
web/services/coupon_user.go
Normal file
255
web/services/coupon_user.go
Normal file
@@ -0,0 +1,255 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"platform/pkg/u"
|
||||
"platform/web/core"
|
||||
m "platform/web/models"
|
||||
q "platform/web/queries"
|
||||
"time"
|
||||
|
||||
"gorm.io/gen/field"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var CouponUser = &couponUserService{}
|
||||
|
||||
type couponUserService struct{}
|
||||
|
||||
func (s *couponUserService) Create(data CreateCouponUserData) error {
|
||||
now := time.Now()
|
||||
status := u.Else(data.Status, m.CouponUserStatusUnused)
|
||||
if err := validateCouponUserStatus(status); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return q.Q.Transaction(func(tx *q.Query) error {
|
||||
coupon, err := tx.Coupon.Where(tx.Coupon.ID.Eq(data.CouponID)).Take()
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return core.NewBizErr("优惠券不存在")
|
||||
}
|
||||
if err != nil {
|
||||
return core.NewServErr("获取优惠券数据失败", err)
|
||||
}
|
||||
if coupon.Status != m.CouponStatusEnabled {
|
||||
return core.NewBizErr("优惠券不可用")
|
||||
}
|
||||
if coupon.Count <= 0 {
|
||||
return core.NewBizErr("优惠券已发放完")
|
||||
}
|
||||
|
||||
_, err = tx.User.Where(tx.User.ID.Eq(data.UserID)).Take()
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return core.NewBizErr("用户不存在")
|
||||
}
|
||||
if err != nil {
|
||||
return core.NewServErr("获取用户数据失败", err)
|
||||
}
|
||||
|
||||
expireAt := data.ExpireAt
|
||||
if expireAt == nil {
|
||||
expireAt = couponUserExpireAt(coupon, now)
|
||||
}
|
||||
|
||||
usedAt := data.UsedAt
|
||||
if status == m.CouponUserStatusUsed && usedAt == nil {
|
||||
usedAt = &now
|
||||
}
|
||||
if status == m.CouponUserStatusUnused {
|
||||
usedAt = nil
|
||||
}
|
||||
|
||||
err = tx.CouponUser.Create(&m.CouponUser{
|
||||
UserID: data.UserID,
|
||||
CouponID: data.CouponID,
|
||||
Status: status,
|
||||
ExpireAt: expireAt,
|
||||
UsedAt: usedAt,
|
||||
})
|
||||
if err != nil {
|
||||
return core.NewServErr("发放优惠券失败", err)
|
||||
}
|
||||
|
||||
return adjustCouponCount(tx, coupon.ID, -1)
|
||||
})
|
||||
}
|
||||
|
||||
type CreateCouponUserData struct {
|
||||
CouponID int32 `json:"coupon_id" validate:"required"`
|
||||
UserID int32 `json:"user_id" validate:"required"`
|
||||
Status *m.CouponUserStatus `json:"status"`
|
||||
ExpireAt *time.Time `json:"expire_at"`
|
||||
UsedAt *time.Time `json:"used_at"`
|
||||
}
|
||||
|
||||
func (s *couponUserService) Update(data UpdateCouponUserData) error {
|
||||
return q.Q.Transaction(func(tx *q.Query) error {
|
||||
current, err := tx.CouponUser.Where(tx.CouponUser.ID.Eq(data.ID)).Take()
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return core.NewBizErr("已发放优惠券不存在")
|
||||
}
|
||||
if err != nil {
|
||||
return core.NewServErr("获取已发放优惠券失败", err)
|
||||
}
|
||||
|
||||
do := make([]field.AssignExpr, 0)
|
||||
if data.ExpireAtClear != nil && *data.ExpireAtClear {
|
||||
do = append(do, tx.CouponUser.ExpireAt.Null())
|
||||
} else if data.ExpireAt != nil {
|
||||
do = append(do, tx.CouponUser.ExpireAt.Value(*data.ExpireAt))
|
||||
}
|
||||
|
||||
if data.UsedAtClear != nil && *data.UsedAtClear {
|
||||
do = append(do, tx.CouponUser.UsedAt.Null())
|
||||
} else if data.UsedAt != nil {
|
||||
do = append(do, tx.CouponUser.UsedAt.Value(*data.UsedAt))
|
||||
}
|
||||
|
||||
if data.Status != nil {
|
||||
if err := validateCouponUserStatus(*data.Status); err != nil {
|
||||
return err
|
||||
}
|
||||
if current.Status != *data.Status {
|
||||
if current.Status == m.CouponUserStatusUsed {
|
||||
return core.NewBizErr("已使用的优惠券不能修改状态")
|
||||
}
|
||||
if current.Status == m.CouponUserStatusDisabled && *data.Status == m.CouponUserStatusUsed {
|
||||
return core.NewBizErr("已禁用的优惠券不能标记为已使用")
|
||||
}
|
||||
|
||||
switch *data.Status {
|
||||
case m.CouponUserStatusUnused:
|
||||
if current.Status == m.CouponUserStatusDisabled {
|
||||
if err := adjustCouponCount(tx, current.CouponID, -1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if data.UsedAt == nil && (data.UsedAtClear == nil || !*data.UsedAtClear) {
|
||||
do = append(do, tx.CouponUser.UsedAt.Null())
|
||||
}
|
||||
|
||||
case m.CouponUserStatusUsed:
|
||||
if data.UsedAt == nil && (data.UsedAtClear == nil || !*data.UsedAtClear) {
|
||||
do = append(do, tx.CouponUser.UsedAt.Value(time.Now()))
|
||||
}
|
||||
|
||||
case m.CouponUserStatusDisabled:
|
||||
if current.Status == m.CouponUserStatusUnused {
|
||||
if err := adjustCouponCount(tx, current.CouponID, 1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
do = append(do, tx.CouponUser.Status.Value(int(*data.Status)))
|
||||
}
|
||||
}
|
||||
|
||||
if len(do) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result, err := tx.CouponUser.
|
||||
Where(
|
||||
tx.CouponUser.ID.Eq(data.ID),
|
||||
tx.CouponUser.Status.Eq(int(current.Status)),
|
||||
).
|
||||
UpdateSimple(do...)
|
||||
if err != nil {
|
||||
return core.NewServErr("更新已发放优惠券失败", err)
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return core.NewBizErr("已发放优惠券状态已变化,请重试")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
type UpdateCouponUserData struct {
|
||||
ID int32 `json:"id" validate:"required"`
|
||||
Status *m.CouponUserStatus `json:"status"`
|
||||
ExpireAt *time.Time `json:"expire_at"`
|
||||
ExpireAtClear *bool `json:"expire_at_clear"`
|
||||
UsedAt *time.Time `json:"used_at"`
|
||||
UsedAtClear *bool `json:"used_at_clear"`
|
||||
}
|
||||
|
||||
func (s *couponUserService) Delete(id int32) error {
|
||||
status := m.CouponUserStatusDisabled
|
||||
return s.Update(UpdateCouponUserData{
|
||||
ID: id,
|
||||
Status: &status,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *couponUserService) DeleteOfUser(id int32, userID int32) error {
|
||||
assigned, err := q.CouponUser.Where(
|
||||
q.CouponUser.ID.Eq(id),
|
||||
q.CouponUser.UserID.Eq(userID),
|
||||
).Take()
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return core.NewBizErr("已发放优惠券不存在")
|
||||
}
|
||||
if err != nil {
|
||||
return core.NewServErr("获取已发放优惠券失败", err)
|
||||
}
|
||||
if assigned.Status != m.CouponUserStatusUnused {
|
||||
return core.NewBizErr("只能撤销未使用的优惠券")
|
||||
}
|
||||
|
||||
return s.Delete(id)
|
||||
}
|
||||
|
||||
func couponUserExpireAt(coupon *m.Coupon, now time.Time) *time.Time {
|
||||
if coupon == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch coupon.ExpireType {
|
||||
case m.CouponExpireTypeFixed:
|
||||
return coupon.ExpireAt
|
||||
case m.CouponExpireTypeRelative:
|
||||
if coupon.ExpireIn == nil {
|
||||
return nil
|
||||
}
|
||||
expireAt := now.Add(time.Duration(*coupon.ExpireIn) * 24 * time.Hour)
|
||||
return &expireAt
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func validateCouponUserStatus(status m.CouponUserStatus) error {
|
||||
switch status {
|
||||
case m.CouponUserStatusUnused, m.CouponUserStatusUsed, m.CouponUserStatusDisabled:
|
||||
return nil
|
||||
default:
|
||||
return core.NewBizErr("优惠券发放状态无效")
|
||||
}
|
||||
}
|
||||
|
||||
func adjustCouponCount(tx *q.Query, couponID int32, delta int32) error {
|
||||
coupon, err := tx.Coupon.Where(tx.Coupon.ID.Eq(couponID)).Take()
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return core.NewBizErr("优惠券不存在")
|
||||
}
|
||||
if err != nil {
|
||||
return core.NewServErr("获取优惠券数据失败", err)
|
||||
}
|
||||
|
||||
next := coupon.Count + delta
|
||||
if next < 0 {
|
||||
return core.NewBizErr("优惠券已发放完")
|
||||
}
|
||||
|
||||
result, err := tx.Coupon.
|
||||
Where(tx.Coupon.ID.Eq(couponID), tx.Coupon.Count_.Eq(coupon.Count)).
|
||||
UpdateSimple(tx.Coupon.Count_.Value(next))
|
||||
if err != nil {
|
||||
return core.NewServErr("更新优惠券数量失败", err)
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return core.NewBizErr("优惠券库存已变化,请重试")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -63,9 +63,10 @@ func (s *resourceService) Create(q *q.Query, uid int32, now time.Time, data *Cre
|
||||
var resource = m.Resource{
|
||||
UserID: uid,
|
||||
ResourceNo: u.P(ID.GenReadable("res")),
|
||||
Active: true,
|
||||
Type: data.Type,
|
||||
Code: data.Type.Code(),
|
||||
Active: true,
|
||||
CheckIP: true,
|
||||
}
|
||||
switch data.Type {
|
||||
|
||||
|
||||
@@ -49,3 +49,11 @@ func HandleRemoveChannel(_ context.Context, task *asynq.Task) (err error) {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func HandleClearExpiredChannels(_ context.Context, _ *asynq.Task) (err error) {
|
||||
err = s.Channel.ClearExpiredChannels()
|
||||
if err != nil {
|
||||
return fmt.Errorf("清理过期通道失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
28
web/web.go
28
web/web.go
@@ -42,6 +42,10 @@ func RunApp(pCtx context.Context) error {
|
||||
return RunTask(ctx)
|
||||
})
|
||||
|
||||
g.Go(func() error {
|
||||
return RunCron(ctx)
|
||||
})
|
||||
|
||||
return g.Wait()
|
||||
}
|
||||
|
||||
@@ -90,6 +94,7 @@ func RunTask(ctx context.Context) error {
|
||||
var mux = asynq.NewServeMux()
|
||||
mux.HandleFunc(events.RemoveChannel, tasks.HandleRemoveChannel)
|
||||
mux.HandleFunc(events.CloseTrade, tasks.HandleCompleteTrade)
|
||||
mux.HandleFunc(events.ClearExpiredChannels, tasks.HandleClearExpiredChannels)
|
||||
|
||||
// 停止服务
|
||||
go func() {
|
||||
@@ -105,3 +110,26 @@ func RunTask(ctx context.Context) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func RunCron(ctx context.Context) error {
|
||||
server := asynq.NewSchedulerFromRedisClient(deps.Redis, &asynq.SchedulerOpts{
|
||||
Location: time.Local,
|
||||
})
|
||||
|
||||
// 每小时清理一次一小时之前的过期通道
|
||||
server.Register("0 * * * *", asynq.NewTask(events.ClearExpiredChannels, nil))
|
||||
|
||||
// 停止服务
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
server.Shutdown()
|
||||
}()
|
||||
|
||||
// 启动服务
|
||||
err := server.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("定时任务服务运行失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user