mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 03:52:26 +08:00
- groups 新增 i18n JSONB 列(迁移 242):语言代码 -> {name, description},只存译文;
name/description 原列语义不变,仍用于唯一性校验、搜索与各类日志
- 用户侧接口由后端按请求语言输出:取 Accept-Language 中权重最高的一项,先按完整
语言代码、再按主语言匹配;该语言有译文的字段换成译文,否则保持原字段,且不再带出
译文表。覆盖密钥、可用分组、订阅(含进度与汇总)、兑换、使用记录、可用渠道、
模型广场、套餐列表
- 管理端接口一律返回原字段;分组管理接口额外带 i18n 供编辑
- 创建/编辑分组:名称、描述的标题行右侧新增语言下拉,同一个输入框在默认内容与各语言
版本之间切换;语种跟随前端 availableLocales
- 管理端写入:创建/更新接受 i18n(更新时缺省不修改,传入则整体替换),校验语言代码
形态与名称长度并丢弃空译文;简易模式同样放行;复制分组只带走描述译文
- 切换界面语言后整页刷新,让已加载的数据换成新语言;模型广场响应加
Vary: Accept-Language
- 网关路径,以及只带 group_name 字符串的统计、错误记录出口不做本地化
192 lines
6.0 KiB
Go
192 lines
6.0 KiB
Go
package handler
|
|
|
|
import (
|
|
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
|
middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// SubscriptionSummaryItem represents a subscription item in summary
|
|
type SubscriptionSummaryItem struct {
|
|
ID int64 `json:"id"`
|
|
GroupID int64 `json:"group_id"`
|
|
GroupName string `json:"group_name"`
|
|
Status string `json:"status"`
|
|
DailyUsedUSD float64 `json:"daily_used_usd,omitempty"`
|
|
DailyLimitUSD float64 `json:"daily_limit_usd,omitempty"`
|
|
WeeklyUsedUSD float64 `json:"weekly_used_usd,omitempty"`
|
|
WeeklyLimitUSD float64 `json:"weekly_limit_usd,omitempty"`
|
|
MonthlyUsedUSD float64 `json:"monthly_used_usd,omitempty"`
|
|
MonthlyLimitUSD float64 `json:"monthly_limit_usd,omitempty"`
|
|
ExpiresAt *string `json:"expires_at,omitempty"`
|
|
}
|
|
|
|
// SubscriptionProgressInfo represents subscription with progress info
|
|
type SubscriptionProgressInfo struct {
|
|
Subscription *dto.UserSubscription `json:"subscription"`
|
|
Progress *service.SubscriptionProgress `json:"progress"`
|
|
}
|
|
|
|
// SubscriptionHandler handles user subscription operations
|
|
type SubscriptionHandler struct {
|
|
subscriptionService *service.SubscriptionService
|
|
}
|
|
|
|
// NewSubscriptionHandler creates a new user subscription handler
|
|
func NewSubscriptionHandler(subscriptionService *service.SubscriptionService) *SubscriptionHandler {
|
|
return &SubscriptionHandler{
|
|
subscriptionService: subscriptionService,
|
|
}
|
|
}
|
|
|
|
// List handles listing current user's subscriptions
|
|
// GET /api/v1/subscriptions
|
|
func (h *SubscriptionHandler) List(c *gin.Context) {
|
|
subject, ok := middleware2.GetAuthSubjectFromContext(c)
|
|
if !ok {
|
|
response.Unauthorized(c, "User not found in context")
|
|
return
|
|
}
|
|
|
|
subscriptions, err := h.subscriptionService.ListUserSubscriptions(c.Request.Context(), subject.UserID)
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
out := make([]dto.UserSubscription, 0, len(subscriptions))
|
|
for i := range subscriptions {
|
|
out = append(out, *dto.UserSubscriptionFromService(&subscriptions[i]).Localized(c.GetHeader("Accept-Language")))
|
|
}
|
|
response.Success(c, out)
|
|
}
|
|
|
|
// GetActive handles getting current user's active subscriptions
|
|
// GET /api/v1/subscriptions/active
|
|
func (h *SubscriptionHandler) GetActive(c *gin.Context) {
|
|
subject, ok := middleware2.GetAuthSubjectFromContext(c)
|
|
if !ok {
|
|
response.Unauthorized(c, "User not found in context")
|
|
return
|
|
}
|
|
|
|
subscriptions, err := h.subscriptionService.ListActiveUserSubscriptions(c.Request.Context(), subject.UserID)
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
out := make([]dto.UserSubscription, 0, len(subscriptions))
|
|
for i := range subscriptions {
|
|
out = append(out, *dto.UserSubscriptionFromService(&subscriptions[i]).Localized(c.GetHeader("Accept-Language")))
|
|
}
|
|
response.Success(c, out)
|
|
}
|
|
|
|
// GetProgress handles getting subscription progress for current user
|
|
// GET /api/v1/subscriptions/progress
|
|
func (h *SubscriptionHandler) GetProgress(c *gin.Context) {
|
|
subject, ok := middleware2.GetAuthSubjectFromContext(c)
|
|
if !ok {
|
|
response.Unauthorized(c, "User not found in context")
|
|
return
|
|
}
|
|
|
|
// Get all active subscriptions with progress
|
|
subscriptions, err := h.subscriptionService.ListActiveUserSubscriptions(c.Request.Context(), subject.UserID)
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
result := make([]SubscriptionProgressInfo, 0, len(subscriptions))
|
|
for i := range subscriptions {
|
|
sub := &subscriptions[i]
|
|
progress, err := h.subscriptionService.GetSubscriptionProgress(c.Request.Context(), sub.ID)
|
|
if err != nil {
|
|
// Skip subscriptions with errors
|
|
continue
|
|
}
|
|
if sub.Group != nil {
|
|
progress.GroupName = sub.Group.I18n.LocalizeName(c.GetHeader("Accept-Language"), progress.GroupName)
|
|
}
|
|
result = append(result, SubscriptionProgressInfo{
|
|
Subscription: dto.UserSubscriptionFromService(sub).Localized(c.GetHeader("Accept-Language")),
|
|
Progress: progress,
|
|
})
|
|
}
|
|
|
|
response.Success(c, result)
|
|
}
|
|
|
|
// GetSummary handles getting a summary of current user's subscription status
|
|
// GET /api/v1/subscriptions/summary
|
|
func (h *SubscriptionHandler) GetSummary(c *gin.Context) {
|
|
subject, ok := middleware2.GetAuthSubjectFromContext(c)
|
|
if !ok {
|
|
response.Unauthorized(c, "User not found in context")
|
|
return
|
|
}
|
|
|
|
// Get all active subscriptions
|
|
subscriptions, err := h.subscriptionService.ListActiveUserSubscriptions(c.Request.Context(), subject.UserID)
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
var totalUsed float64
|
|
items := make([]SubscriptionSummaryItem, 0, len(subscriptions))
|
|
|
|
for _, sub := range subscriptions {
|
|
item := SubscriptionSummaryItem{
|
|
ID: sub.ID,
|
|
GroupID: sub.GroupID,
|
|
Status: sub.Status,
|
|
DailyUsedUSD: sub.DailyUsageUSD,
|
|
WeeklyUsedUSD: sub.WeeklyUsageUSD,
|
|
MonthlyUsedUSD: sub.MonthlyUsageUSD,
|
|
}
|
|
|
|
// Add group info if preloaded
|
|
if sub.Group != nil {
|
|
item.GroupName = sub.Group.I18n.LocalizeName(c.GetHeader("Accept-Language"), sub.Group.Name)
|
|
if sub.Group.DailyLimitUSD != nil {
|
|
item.DailyLimitUSD = *sub.Group.DailyLimitUSD
|
|
}
|
|
if sub.Group.WeeklyLimitUSD != nil {
|
|
item.WeeklyLimitUSD = *sub.Group.WeeklyLimitUSD
|
|
}
|
|
if sub.Group.MonthlyLimitUSD != nil {
|
|
item.MonthlyLimitUSD = *sub.Group.MonthlyLimitUSD
|
|
}
|
|
}
|
|
|
|
// Format expiration time
|
|
if !sub.ExpiresAt.IsZero() {
|
|
formatted := sub.ExpiresAt.Format("2006-01-02T15:04:05Z07:00")
|
|
item.ExpiresAt = &formatted
|
|
}
|
|
|
|
// Track total usage (use monthly as the most comprehensive)
|
|
totalUsed += sub.MonthlyUsageUSD
|
|
|
|
items = append(items, item)
|
|
}
|
|
|
|
summary := struct {
|
|
ActiveCount int `json:"active_count"`
|
|
TotalUsedUSD float64 `json:"total_used_usd"`
|
|
Subscriptions []SubscriptionSummaryItem `json:"subscriptions"`
|
|
}{
|
|
ActiveCount: len(subscriptions),
|
|
TotalUsedUSD: totalUsed,
|
|
Subscriptions: items,
|
|
}
|
|
|
|
response.Success(c, summary)
|
|
}
|