diff --git a/README.md b/README.md index bb67e71..1d1e433 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,7 @@ ## todo - channel 接口 - - 每个用户-节点为一条数据,联查白名单 - - 重新梳理逻辑流程,简化循环 - - 端口分配时加锁 + - 待验证 - 长效业务接入 - 微信支付 - 页面 账户总览 @@ -11,6 +9,8 @@ - 页面 使用记录 - 代理数据表的 secret 字段 aes 加密存储 +- globals 合并到 services 或者反之 + ### 下阶段 - 支付回调处理 diff --git a/web/handlers/channel.go b/web/handlers/channel.go index a763ebd..f8dc0db 100644 --- a/web/handlers/channel.go +++ b/web/handlers/channel.go @@ -97,13 +97,23 @@ type CreateChannelReq struct { Isp string `json:"isp"` } +type CreateChannelRespItem struct { + Proto s.ChannelProtocol `json:"-"` + Host string `json:"host"` + Port int32 `json:"port"` + Username *string `json:"username,omitempty"` + Password *string `json:"password,omitempty"` +} + func CreateChannel(c *fiber.Ctx) error { + // 检查权限 authContext, err := auth.Protect(c, []auth.PayloadType{auth.PayloadUser}, []string{}) if err != nil { return err } - // 获取用户信息 + + // 检查用户其他权限 user, err := q.User. Where(q.User.ID.Eq(authContext.Payload.Id)). Take() @@ -140,6 +150,7 @@ func CreateChannel(c *fiber.Ctx) error { isp = "移动" } + // 创建通道 result, err := s.Channel.CreateChannel( c.Context(), authContext, @@ -157,7 +168,20 @@ func CreateChannel(c *fiber.Ctx) error { return err } - return c.JSON(result) + // 返回结果 + var resp = make([]*CreateChannelRespItem, len(result)) + for i, channel := range result { + resp[i] = &CreateChannelRespItem{ + Proto: req.Protocol, + Host: channel.ProxyHost, + Port: channel.ProxyPort, + } + if req.AuthType == s.ChannelAuthTypePass { + resp[i].Username = &channel.Username + resp[i].Password = &channel.Password + } + } + return c.JSON(resp) } type CreateChannelResultType string diff --git a/web/services/channel.go b/web/services/channel.go index 5ae5f64..72d1ced 100644 --- a/web/services/channel.go +++ b/web/services/channel.go @@ -16,7 +16,7 @@ import ( "platform/web/auth" "platform/web/core" g "platform/web/globals" - "platform/web/models" + m "platform/web/models" q "platform/web/queries" "strconv" "strings" @@ -32,23 +32,6 @@ var Channel = &channelService{} type channelService struct { } -type ChannelAuthType int - -const ( - ChannelAuthTypeAll ChannelAuthType = iota - ChannelAuthTypeIp - ChannelAuthTypePass -) - -type ChannelProtocol int32 - -const ( - ProtocolAll ChannelProtocol = iota - ProtocolHTTP - ProtocolHttps - ProtocolSocks5 -) - type ResourceInfo struct { Id int32 UserId int32 @@ -135,7 +118,7 @@ func (s *channelService) RemoveChannels(ctx context.Context, authCtx *auth.Conte // 组织数据 var configMap = make(map[int32][]g.PortConfigsReq, len(proxies)) - var proxyMap = make(map[int32]*models.Proxy, len(proxies)) + var proxyMap = make(map[int32]*m.Proxy, len(proxies)) for _, proxy := range proxies { configMap[proxy.ID] = make([]g.PortConfigsReq, 0) proxyMap[proxy.ID] = proxy @@ -245,185 +228,152 @@ func (s *channelService) CreateChannel( authType ChannelAuthType, count int, nodeFilter ...NodeFilterConfig, -) ([]*PortInfo, error) { +) (newChannels []*m.Channel, err error) { var now = time.Now() - - var step = time.Now() var rid = ctx.Value(requestid.ConfigDefault.ContextKey).(string) - - filter := NodeFilterConfig{} + var filter = NodeFilterConfig{} if len(nodeFilter) > 0 { filter = nodeFilter[0] } - var addr []*PortInfo - err := q.Q.Transaction(func(q *q.Query) error { + err = q.Q.Transaction(func(q *q.Query) (err error) { // 查找套餐 - step = time.Now() - - var resource = new(ResourceInfo) - data := q.Resource.As("data") - pss := q.ResourcePss.As("pss") - err := data.Scopes(orm.Alias(data)). - Select( - data.ID, data.UserID, data.Active, - pss.Type, pss.Live, pss.DailyUsed, pss.DailyLimit, pss.DailyLast, pss.Quota, pss.Used, pss.Expire, - ). - LeftJoin(q.ResourcePss.As("pss"), pss.ResourceID.EqCol(data.ID)). - Where(data.ID.Eq(resourceId)). - Scan(&resource) + resource, err := findResource(q, rid, resourceId, authCtx, count) if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - // 禁止 id 猜测 - return ChannelServiceErr("无权限访问") + return err + } + + // 查找网关 + proxies, err := findProxies(q, rid) + if err != nil { + return err + } + + // 查找已使用的节点 + channels, err := findChannels(q, rid, proxies) + if err != nil { + return err + } + + // 查找白名单 + var whitelist *[]string + if authType == ChannelAuthTypeIp { + whitelist, err = findWhitelist(q, rid, authCtx.Payload.Id) + if err != nil { + return err } - return err } - slog.Debug("查找套餐", "rid", rid, "step", time.Since(step)) - - // 检查用户权限 - err = checkUser(authCtx, resource, count) + // 分配节点 + var expire = now.Add(time.Duration(resource.Live) * time.Second) + newChannels, err = calcChannels(proxies, channels, whitelist, count, authCtx.Payload.Id, protocol, authType, expire, filter) if err != nil { return err } - // 申请节点 - step = time.Now() - - edgeAssigns, err := assignEdge(q, count, filter) - if err != nil { - return err - } - - slog.Debug("申请节点", "rid", rid, "total", time.Since(step)) - - // 分配端口 - step = time.Now() - - expiration := core.LocalDateTime(now.Add(time.Duration(resource.Live) * time.Second)) - _addr, channels, err := assignPort(q, edgeAssigns, authCtx.Payload.Id, protocol, authType, expiration, filter) - if err != nil { - return err - } - addr = _addr - - slog.Debug("分配端口", "rid", rid, "total", time.Since(step)) - // 更新套餐使用记录 - step = time.Now() - - toUpdate := models.ResourcePss{ - Used: resource.Used + int32(count), - DailyLast: core.LocalDateTime(now), - } - var last = time.Time(resource.DailyLast) - if now.Year() != last.Year() || now.Month() != last.Month() || now.Day() != last.Day() { - toUpdate.DailyUsed = int32(count) - } else { - toUpdate.DailyUsed = resource.DailyUsed + int32(count) - } - _, err = q.ResourcePss. - Where(q.ResourcePss.ResourceID.Eq(resourceId)). - Select( - q.ResourcePss.Used, - q.ResourcePss.DailyUsed, - q.ResourcePss.DailyLast, - ). - Updates(toUpdate) + err = updateResource(rid, resource, count, now) if err != nil { return err } - slog.Debug("更新套餐使用记录", "rid", rid, "step", time.Since(step)) + // 保存通道 + err = saveChannels(newChannels) + if err != nil { + return err + } // 缓存通道数据 - step = time.Now() - - err = cache(ctx, channels) + err = cacheChannels(ctx, rid, newChannels) if err != nil { return err } - slog.Debug("缓存通道数据", "rid", rid, "step", time.Since(step)) - return nil }, &sql.TxOptions{Isolation: sql.LevelRepeatableRead}) if err != nil { return nil, err } - return addr, nil + return newChannels, nil } -func checkUser(authCtx *auth.Context, resource *ResourceInfo, count int) error { +func findResource(q *q.Query, rid string, resourceId int32, authCtx *auth.Context, count int) (*ResourceInfo, error) { + var step = time.Now() - // 检查使用人 - if authCtx.Payload.Type == auth.PayloadUser && authCtx.Payload.Id != resource.UserId { - return core.ForbiddenErr("无权限访问") + // 查找套餐 + var resource = new(ResourceInfo) + data := q.Resource.As("data") + pss := q.ResourcePss.As("pss") + err := data.Scopes(orm.Alias(data)). + Select( + data.ID, data.UserID, data.Active, + pss.Type, pss.Live, pss.DailyUsed, pss.DailyLimit, pss.DailyLast, pss.Quota, pss.Used, pss.Expire, + ). + LeftJoin(q.ResourcePss.As("pss"), pss.ResourceID.EqCol(data.ID)). + Where( + data.ID.Eq(resourceId), + data.UserID.Eq(authCtx.Payload.Id), + ). + Scan(&resource) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrResourceNotExist // 防止 id 猜测 + } + return nil, err } // 检查套餐状态 if !resource.Active { - return ChannelServiceErr("套餐已失效") + return nil, ErrResourceInvalid } // 检查每日限额 today := time.Now().Format("2006-01-02") == time.Time(resource.DailyLast).Format("2006-01-02") dailyRemain := int(math.Max(float64(resource.DailyLimit-resource.DailyUsed), 0)) if today && dailyRemain < count { - return ChannelServiceErr("套餐每日配额不足") + return nil, ErrResourceDailyLimit } // 检查时间或配额 if resource.Type == 1 { // 包时 if time.Time(resource.Expire).Before(time.Now()) { - return ChannelServiceErr("套餐已过期") + return nil, ErrResourceExpired } } else { // 包量 remain := int(math.Max(float64(resource.Quota-resource.Used), 0)) if remain < count { - return ChannelServiceErr("套餐配额不足") + return nil, ErrResourceExhausted } } - return nil + slog.Debug("查找套餐", "rid", rid, "step", time.Since(step)) + return resource, nil } -// assignEdge 分配边缘节点数量 -func assignEdge(q *q.Query, count int, filter NodeFilterConfig) (*AssignEdgeResult, error) { - - // 查询可以使用的网关 +func findProxies(q *q.Query, rid string) (proxies []*m.Proxy, err error) { var step = time.Now() - proxies, err := q.Proxy. + proxies, err = q.Proxy. Where(q.Proxy.Type.Eq(1)). Find() if err != nil { return nil, err } - slog.Debug("查找网关", "step", time.Since(step)) + slog.Debug("查找网关", "rid", rid, "step", time.Since(step)) + return proxies, nil +} - // 查询已配置的节点 - step = time.Now() - - rProxyConfigs, err := g.Cloud.CloudAutoQuery() - if err != nil { - return nil, err - } - - slog.Debug("查询已配置节点 (remote)", "step", time.Since(step)) - - // 查询已使用的节点 - step = time.Now() +func findChannels(q *q.Query, rid string, proxies []*m.Proxy) (channels []*m.Channel, err error) { + var step = time.Now() var proxyIds = make([]int32, len(proxies)) for i, proxy := range proxies { proxyIds[i] = proxy.ID } - channels, err := q.Channel.Debug(). + channels, err = q.Channel.Debug(). Select( q.Channel.ProxyID, q.Channel.ProxyPort). @@ -437,74 +387,111 @@ func assignEdge(q *q.Query, count int, filter NodeFilterConfig) (*AssignEdgeResu if err != nil { return nil, err } - var proxyUses = make(map[int32]int, len(channels)) - for _, channel := range channels { - proxyUses[channel.ProxyID]++ + + slog.Debug("查找已使用节点", "rid", rid, "step", time.Since(step)) + return channels, nil +} + +func findWhitelist(q *q.Query, rid string, userId int32) (*[]string, error) { + var step = time.Now() + + // 按需查找用户白名单 + var whitelist []string + err := q.Whitelist. + Where(q.Whitelist.UserID.Eq(userId)). + Select(q.Whitelist.Host). + Scan(&whitelist) + if err != nil { + return nil, err + } + if len(whitelist) == 0 { + return nil, ChannelServiceErr("用户没有白名单") } - slog.Debug("查找已使用节点", "step", time.Since(step)) + slog.Debug("查找用户白名单", "rid", rid, "step", time.Since(step)) + return &whitelist, nil +} - // 组织数据 - var infos = make([]*ProxyInfo, len(proxies)) - for i, proxy := range proxies { - infos[i] = &ProxyInfo{ - proxy: proxy, - used: proxyUses[proxy.ID], - } +func calcChannels( + proxies []*m.Proxy, + allChannels []*m.Channel, + whitelist *[]string, + count int, + userId int32, + protocol ChannelProtocol, + authType ChannelAuthType, + expiration time.Time, + filter NodeFilterConfig, +) ([]*m.Channel, error) { + var step = time.Now() - rConfigs, ok := rProxyConfigs[proxy.Name] - if !ok { - infos[i].count = 0 - continue - } + // 查询已配置的节点 + remoteConfigs, err := g.Cloud.CloudAutoQuery() + if err != nil { + return nil, err + } - for _, rConfig := range rConfigs { - if rConfig.Isp == filter.Isp && rConfig.City == filter.City && rConfig.Province == filter.Prov { - infos[i].count = rConfig.Count + // 统计已用节点量与端口查找表 + var proxyUses = make(map[int32]int, len(allChannels)) + var portsMap = make(map[uint64]struct{}) + for _, channel := range allChannels { + proxyUses[channel.ProxyID]++ + key := uint64(channel.ProxyID)<<32 | uint64(channel.ProxyPort) + portsMap[key] = struct{}{} + } + + // 计算分配额度 + var total = len(allChannels) + count + var avg = int(math.Ceil(float64(total) / float64(len(proxies)))) + + // 分配节点 + var newChannels []*m.Channel + for _, proxy := range proxies { + + // 分配前后的节点量 + var prev = proxyUses[proxy.ID] + var next = int(math.Max(float64(prev), float64(int(math.Min(float64(avg), float64(total)))))) + total -= next + + // 网关配置的节点量 + var count = 0 + remoteConfig, ok := remoteConfigs[proxy.Name] + if ok { + for _, config := range remoteConfig { + if config.Isp == filter.Isp && config.City == filter.City && config.Province == filter.Prov { + count = config.Count + break + } } } - } - // 分配新增的节点 - var configs = make([]*ProxyConfig, len(proxies)) - var needed = len(channels) + count - avg := int(math.Ceil(float64(needed) / float64(len(proxies)))) - for i, info := range infos { - var prev = info.used - var next = int(math.Min(float64(avg), float64(needed))) - - info.used = int(math.Max(float64(prev), float64(next))) - needed -= info.used - - if env.DebugExternalChange && info.used > info.count { + if env.DebugExternalChange && next > count { step = time.Now() - slog.Debug("新增新节点", "proxy", info.proxy.Name, "used", info.used, "count", info.count) - - rConfigs := rProxyConfigs[info.proxy.Name] - + var multiple float64 = 2 // 扩张倍数 var newConfig = g.AutoConfig{ Province: filter.Prov, City: filter.City, Isp: filter.Isp, - Count: int(math.Ceil(float64(info.used) * 2)), + Count: int(math.Ceil(float64(next) * multiple)), } + var newConfigs []g.AutoConfig - var update = false - for _, rConfig := range rConfigs { - if rConfig.Isp == filter.Isp && rConfig.City == filter.City && rConfig.Province == filter.Prov { - newConfigs = append(newConfigs, newConfig) - update = true - } else { - newConfigs = append(newConfigs, rConfig) - } - } - if !update { + if count == 0 { newConfigs = append(newConfigs, newConfig) + } else { + newConfigs = make([]g.AutoConfig, len(remoteConfig)) + for i, config := range remoteConfig { + if config.Isp == filter.Isp && config.City == filter.City && config.Province == filter.Prov { + count = config.Count + break + } + newConfigs[i] = config + } } err := g.Cloud.CloudConnect(g.CloudConnectReq{ - Uuid: info.proxy.Name, + Uuid: proxy.Name, Edge: nil, AutoConfig: newConfigs, }) @@ -512,86 +499,23 @@ func assignEdge(q *q.Query, count int, filter NodeFilterConfig) (*AssignEdgeResu return nil, err } - slog.Debug("分配新增的节点", "step", time.Since(step)) + slog.Debug("提交节点配置", + slog.Duration("step", time.Since(step)), + slog.String("proxy", proxy.Name), + slog.Int("used", prev), + slog.Int("count", next), + ) } - configs[i] = &ProxyConfig{ - proxy: info.proxy, - count: int(math.Max(float64(next-prev), 0)), + // 节点增量 + var acc = next - prev + if acc <= 0 { + continue } - } - - return &AssignEdgeResult{ - configs: configs, - channels: channels, - }, nil -} - -type ProxyInfo struct { - proxy *models.Proxy - used int - count int -} - -type AssignEdgeResult struct { - configs []*ProxyConfig - channels []*models.Channel -} - -type ProxyConfig struct { - proxy *models.Proxy - count int -} - -// assignPort 分配指定数量的端口 -func assignPort( - q *q.Query, - proxies *AssignEdgeResult, - userId int32, - protocol ChannelProtocol, - authType ChannelAuthType, - expiration core.LocalDateTime, - filter NodeFilterConfig, -) ([]*PortInfo, []*models.Channel, error) { - var step time.Time - - var configs = proxies.configs - var exists = proxies.channels - - // 端口查找表 - var portsMap = make(map[uint64]struct{}) - for _, channel := range exists { - key := uint64(channel.ProxyID)<<32 | uint64(channel.ProxyPort) - portsMap[key] = struct{}{} - } - println(len(portsMap)) - - // 查找用户白名单 - var whitelist []string - if authType == ChannelAuthTypeIp { - err := q.Whitelist. - Where(q.Whitelist.UserID.Eq(userId)). - Select(q.Whitelist.Host). - Scan(&whitelist) - if err != nil { - return nil, nil, err - } - if len(whitelist) == 0 { - return nil, nil, ChannelServiceErr("用户没有白名单") - } - } - - // 配置启用代理 - var result []*PortInfo - var channels []*models.Channel - for _, config := range configs { - var err error - var proxy = config.proxy - var count = config.count // 筛选可用端口 - var configs = make([]g.PortConfigsReq, 0, count) - for port := 10000; port < 20000 && len(configs) < count; port++ { + var portConfigs = make([]g.PortConfigsReq, 0, acc) + for port := 10000; port < 20000 && len(portConfigs) < acc; port++ { // 跳过存在的端口 key := uint64(proxy.ID)<<32 | uint64(port) _, ok := portsMap[key] @@ -600,8 +524,7 @@ func assignPort( } // 配置新端口 - var i = len(configs) - configs = append(configs, g.PortConfigsReq{ + var portConf = g.PortConfigsReq{ Port: port, Edge: nil, Status: true, @@ -612,78 +535,42 @@ func assignPort( Count: u.P(1), PacketLoss: 30, }, - }) + } + var newChannel = &m.Channel{ + UserID: userId, + ProxyID: proxy.ID, + ProxyHost: proxy.Host, + ProxyPort: int32(port), + Protocol: int32(protocol), + Expiration: core.LocalDateTime(expiration), + } switch authType { + case ChannelAuthTypeIp: - configs[i].Whitelist = &whitelist - configs[i].Userpass = u.P("") - for _, item := range whitelist { - channels = append(channels, &models.Channel{ - UserID: userId, - ProxyID: proxy.ID, - UserHost: item, - ProxyHost: proxy.Host, - ProxyPort: int32(port), - AuthIP: true, - AuthPass: false, - Protocol: int32(protocol), - Expiration: expiration, - }) - } - result = append(result, &PortInfo{ - Proto: protocol, - Host: proxy.Host, - Port: port, - }) + portConf.Whitelist = whitelist + portConf.Userpass = u.P("") + newChannel.AuthIP = true + case ChannelAuthTypePass: username, password := genPassPair() - configs[i].Whitelist = &[]string{} - configs[i].Userpass = u.P(fmt.Sprintf("%s:%s", username, password)) - channels = append(channels, &models.Channel{ - UserID: userId, - ProxyID: proxy.ID, - ProxyHost: proxy.Host, - ProxyPort: int32(port), - AuthIP: false, - AuthPass: true, - Username: username, - Password: password, - Protocol: int32(protocol), - Expiration: expiration, - }) - result = append(result, &PortInfo{ - Proto: protocol, - Host: proxy.Host, - Port: port, - Username: &username, - Password: &password, - }) + portConf.Whitelist = &[]string{} + portConf.Userpass = u.P(fmt.Sprintf("%s:%s", username, password)) + newChannel.AuthPass = true + newChannel.Username = username + newChannel.Password = password + default: - return nil, nil, ChannelServiceErr("不支持的通道认证方式") + return nil, ChannelServiceErr("不支持的通道认证方式") } + + portConfigs = append(portConfigs, portConf) + newChannels = append(newChannels, newChannel) } - - if len(configs) < count { - return nil, nil, ChannelServiceErr("网关端口数量到达上限,无法分配") + if len(portConfigs) < acc { + return nil, ChannelServiceErr("网关端口数量到达上限,无法分配") } - // 保存到数据库 - step = time.Now() - - err = q.Channel. - Omit( - q.Channel.NodeID, - q.Channel.NodeHost, - q.Channel.DeletedAt, - ). - Create(channels...) - if err != nil { - return nil, nil, err - } - - slog.Debug("保存到数据库", "step", time.Since(step)) - // 提交端口配置并更新节点列表 if env.DebugExternalChange { step = time.Now() @@ -694,24 +581,77 @@ func assignPort( secret[0], secret[1], ) - err = gateway.GatewayPortConfigs(configs) + err = gateway.GatewayPortConfigs(portConfigs) if err != nil { - return nil, nil, err + return nil, err } slog.Debug("提交端口配置", "step", time.Since(step)) } } - return result, channels, nil + slog.Debug("申请节点", "rid", step, "total", time.Since(step)) + return newChannels, nil } -type PortInfo struct { - Proto ChannelProtocol `json:"-"` - Host string `json:"host"` - Port int `json:"port"` - Username *string `json:"username,omitempty"` - Password *string `json:"password,omitempty"` +func updateResource(rid string, resource *ResourceInfo, count int, now time.Time) (err error) { + var step = time.Now() + + toUpdate := m.ResourcePss{ + Used: resource.Used + int32(count), + DailyLast: core.LocalDateTime(now), + } + var last = time.Time(resource.DailyLast) + if now.Year() != last.Year() || now.Month() != last.Month() || now.Day() != last.Day() { + toUpdate.DailyUsed = int32(count) + } else { + toUpdate.DailyUsed = resource.DailyUsed + int32(count) + } + _, err = q.ResourcePss. + Where(q.ResourcePss.ResourceID.Eq(resource.Id)). + Select( + q.ResourcePss.Used, + q.ResourcePss.DailyUsed, + q.ResourcePss.DailyLast, + ). + Updates(toUpdate) + if err != nil { + return err + } + + slog.Debug("更新套餐使用记录", "rid", rid, "step", time.Since(step)) + return nil +} + +func saveChannels(channels []*m.Channel) (err error) { + // 保存到数据库 + var step = time.Now() + + err = q.Channel. + Omit( + q.Channel.NodeID, + q.Channel.NodeHost, + q.Channel.DeletedAt, + ). + Create(channels...) + if err != nil { + return err + } + + slog.Debug("保存到数据库", "step", time.Since(step)) + return nil +} + +func cacheChannels(ctx context.Context, rid string, channels []*m.Channel) (err error) { + var step = time.Now() + + err = cache(ctx, channels) + if err != nil { + return err + } + + slog.Debug("缓存通道数据", "rid", rid, "step", time.Since(step)) + return nil } // endregion @@ -735,11 +675,11 @@ func genPassPair() (string, string) { return string(username), string(password) } -func chKey(channel *models.Channel) string { +func chKey(channel *m.Channel) string { return fmt.Sprintf("channel:%d", channel.ID) } -func cache(ctx context.Context, channels []*models.Channel) error { +func cache(ctx context.Context, channels []*m.Channel) error { if len(channels) == 0 { return nil } @@ -769,7 +709,7 @@ func cache(ctx context.Context, channels []*models.Channel) error { return nil } -func deleteCache(ctx context.Context, channels []*models.Channel) error { +func deleteCache(ctx context.Context, channels []*m.Channel) error { if len(channels) == 0 { return nil } @@ -786,8 +726,33 @@ func deleteCache(ctx context.Context, channels []*models.Channel) error { return nil } +type ChannelAuthType int + +const ( + ChannelAuthTypeAll ChannelAuthType = iota + ChannelAuthTypeIp + ChannelAuthTypePass +) + +type ChannelProtocol int32 + +const ( + ProtocolAll ChannelProtocol = iota + ProtocolHTTP + ProtocolHttps + ProtocolSocks5 +) + type ChannelServiceErr string func (c ChannelServiceErr) Error() string { return string(c) } + +const ( + ErrResourceNotExist = ChannelServiceErr("套餐不存在") + ErrResourceInvalid = ChannelServiceErr("套餐不可用") + ErrResourceExhausted = ChannelServiceErr("套餐已用完") + ErrResourceExpired = ChannelServiceErr("套餐已过期") + ErrResourceDailyLimit = ChannelServiceErr("套餐每日配额已用完") +)