- 新增 SubscriptionExpiryService 定时任务,每分钟更新过期订阅状态 - 订阅列表支持服务端排序(按过期时间、状态、创建时间) - 实时显示正确的过期状态,无需等待定时任务 - 允许对已过期订阅进行续期操作 - DataTable 组件支持 serverSideSort 模式
283 lines
8.5 KiB
Go
283 lines
8.5 KiB
Go
package admin
|
|
|
|
import (
|
|
"strconv"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
|
"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"
|
|
)
|
|
|
|
// toResponsePagination converts pagination.PaginationResult to response.PaginationResult
|
|
func toResponsePagination(p *pagination.PaginationResult) *response.PaginationResult {
|
|
if p == nil {
|
|
return nil
|
|
}
|
|
return &response.PaginationResult{
|
|
Total: p.Total,
|
|
Page: p.Page,
|
|
PageSize: p.PageSize,
|
|
Pages: p.Pages,
|
|
}
|
|
}
|
|
|
|
// SubscriptionHandler handles admin subscription management
|
|
type SubscriptionHandler struct {
|
|
subscriptionService *service.SubscriptionService
|
|
}
|
|
|
|
// NewSubscriptionHandler creates a new admin subscription handler
|
|
func NewSubscriptionHandler(subscriptionService *service.SubscriptionService) *SubscriptionHandler {
|
|
return &SubscriptionHandler{
|
|
subscriptionService: subscriptionService,
|
|
}
|
|
}
|
|
|
|
// AssignSubscriptionRequest represents assign subscription request
|
|
type AssignSubscriptionRequest struct {
|
|
UserID int64 `json:"user_id" binding:"required"`
|
|
GroupID int64 `json:"group_id" binding:"required"`
|
|
ValidityDays int `json:"validity_days" binding:"omitempty,max=36500"` // max 100 years
|
|
Notes string `json:"notes"`
|
|
}
|
|
|
|
// BulkAssignSubscriptionRequest represents bulk assign subscription request
|
|
type BulkAssignSubscriptionRequest struct {
|
|
UserIDs []int64 `json:"user_ids" binding:"required,min=1"`
|
|
GroupID int64 `json:"group_id" binding:"required"`
|
|
ValidityDays int `json:"validity_days" binding:"omitempty,max=36500"` // max 100 years
|
|
Notes string `json:"notes"`
|
|
}
|
|
|
|
// AdjustSubscriptionRequest represents adjust subscription request (extend or shorten)
|
|
type AdjustSubscriptionRequest struct {
|
|
Days int `json:"days" binding:"required,min=-36500,max=36500"` // negative to shorten, positive to extend
|
|
}
|
|
|
|
// List handles listing all subscriptions with pagination and filters
|
|
// GET /api/v1/admin/subscriptions
|
|
func (h *SubscriptionHandler) List(c *gin.Context) {
|
|
page, pageSize := response.ParsePagination(c)
|
|
|
|
// Parse optional filters
|
|
var userID, groupID *int64
|
|
if userIDStr := c.Query("user_id"); userIDStr != "" {
|
|
if id, err := strconv.ParseInt(userIDStr, 10, 64); err == nil {
|
|
userID = &id
|
|
}
|
|
}
|
|
if groupIDStr := c.Query("group_id"); groupIDStr != "" {
|
|
if id, err := strconv.ParseInt(groupIDStr, 10, 64); err == nil {
|
|
groupID = &id
|
|
}
|
|
}
|
|
status := c.Query("status")
|
|
|
|
// Parse sorting parameters
|
|
sortBy := c.DefaultQuery("sort_by", "created_at")
|
|
sortOrder := c.DefaultQuery("sort_order", "desc")
|
|
|
|
subscriptions, pagination, err := h.subscriptionService.List(c.Request.Context(), page, pageSize, userID, groupID, status, sortBy, sortOrder)
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
out := make([]dto.AdminUserSubscription, 0, len(subscriptions))
|
|
for i := range subscriptions {
|
|
out = append(out, *dto.UserSubscriptionFromServiceAdmin(&subscriptions[i]))
|
|
}
|
|
response.PaginatedWithResult(c, out, toResponsePagination(pagination))
|
|
}
|
|
|
|
// GetByID handles getting a subscription by ID
|
|
// GET /api/v1/admin/subscriptions/:id
|
|
func (h *SubscriptionHandler) GetByID(c *gin.Context) {
|
|
subscriptionID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
if err != nil {
|
|
response.BadRequest(c, "Invalid subscription ID")
|
|
return
|
|
}
|
|
|
|
subscription, err := h.subscriptionService.GetByID(c.Request.Context(), subscriptionID)
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
response.Success(c, dto.UserSubscriptionFromServiceAdmin(subscription))
|
|
}
|
|
|
|
// GetProgress handles getting subscription usage progress
|
|
// GET /api/v1/admin/subscriptions/:id/progress
|
|
func (h *SubscriptionHandler) GetProgress(c *gin.Context) {
|
|
subscriptionID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
if err != nil {
|
|
response.BadRequest(c, "Invalid subscription ID")
|
|
return
|
|
}
|
|
|
|
progress, err := h.subscriptionService.GetSubscriptionProgress(c.Request.Context(), subscriptionID)
|
|
if err != nil {
|
|
response.NotFound(c, "Subscription not found")
|
|
return
|
|
}
|
|
|
|
response.Success(c, progress)
|
|
}
|
|
|
|
// Assign handles assigning a subscription to a user
|
|
// POST /api/v1/admin/subscriptions/assign
|
|
func (h *SubscriptionHandler) Assign(c *gin.Context) {
|
|
var req AssignSubscriptionRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, "Invalid request: "+err.Error())
|
|
return
|
|
}
|
|
|
|
// Get admin user ID from context
|
|
adminID := getAdminIDFromContext(c)
|
|
|
|
subscription, err := h.subscriptionService.AssignSubscription(c.Request.Context(), &service.AssignSubscriptionInput{
|
|
UserID: req.UserID,
|
|
GroupID: req.GroupID,
|
|
ValidityDays: req.ValidityDays,
|
|
AssignedBy: adminID,
|
|
Notes: req.Notes,
|
|
})
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
response.Success(c, dto.UserSubscriptionFromServiceAdmin(subscription))
|
|
}
|
|
|
|
// BulkAssign handles bulk assigning subscriptions to multiple users
|
|
// POST /api/v1/admin/subscriptions/bulk-assign
|
|
func (h *SubscriptionHandler) BulkAssign(c *gin.Context) {
|
|
var req BulkAssignSubscriptionRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, "Invalid request: "+err.Error())
|
|
return
|
|
}
|
|
|
|
// Get admin user ID from context
|
|
adminID := getAdminIDFromContext(c)
|
|
|
|
result, err := h.subscriptionService.BulkAssignSubscription(c.Request.Context(), &service.BulkAssignSubscriptionInput{
|
|
UserIDs: req.UserIDs,
|
|
GroupID: req.GroupID,
|
|
ValidityDays: req.ValidityDays,
|
|
AssignedBy: adminID,
|
|
Notes: req.Notes,
|
|
})
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
response.Success(c, dto.BulkAssignResultFromService(result))
|
|
}
|
|
|
|
// Extend handles adjusting a subscription (extend or shorten)
|
|
// POST /api/v1/admin/subscriptions/:id/extend
|
|
func (h *SubscriptionHandler) Extend(c *gin.Context) {
|
|
subscriptionID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
if err != nil {
|
|
response.BadRequest(c, "Invalid subscription ID")
|
|
return
|
|
}
|
|
|
|
var req AdjustSubscriptionRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, "Invalid request: "+err.Error())
|
|
return
|
|
}
|
|
|
|
subscription, err := h.subscriptionService.ExtendSubscription(c.Request.Context(), subscriptionID, req.Days)
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
response.Success(c, dto.UserSubscriptionFromServiceAdmin(subscription))
|
|
}
|
|
|
|
// Revoke handles revoking a subscription
|
|
// DELETE /api/v1/admin/subscriptions/:id
|
|
func (h *SubscriptionHandler) Revoke(c *gin.Context) {
|
|
subscriptionID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
if err != nil {
|
|
response.BadRequest(c, "Invalid subscription ID")
|
|
return
|
|
}
|
|
|
|
err = h.subscriptionService.RevokeSubscription(c.Request.Context(), subscriptionID)
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
response.Success(c, gin.H{"message": "Subscription revoked successfully"})
|
|
}
|
|
|
|
// ListByGroup handles listing subscriptions for a specific group
|
|
// GET /api/v1/admin/groups/:id/subscriptions
|
|
func (h *SubscriptionHandler) ListByGroup(c *gin.Context) {
|
|
groupID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
if err != nil {
|
|
response.BadRequest(c, "Invalid group ID")
|
|
return
|
|
}
|
|
|
|
page, pageSize := response.ParsePagination(c)
|
|
|
|
subscriptions, pagination, err := h.subscriptionService.ListGroupSubscriptions(c.Request.Context(), groupID, page, pageSize)
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
out := make([]dto.AdminUserSubscription, 0, len(subscriptions))
|
|
for i := range subscriptions {
|
|
out = append(out, *dto.UserSubscriptionFromServiceAdmin(&subscriptions[i]))
|
|
}
|
|
response.PaginatedWithResult(c, out, toResponsePagination(pagination))
|
|
}
|
|
|
|
// ListByUser handles listing subscriptions for a specific user
|
|
// GET /api/v1/admin/users/:id/subscriptions
|
|
func (h *SubscriptionHandler) ListByUser(c *gin.Context) {
|
|
userID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
if err != nil {
|
|
response.BadRequest(c, "Invalid user ID")
|
|
return
|
|
}
|
|
|
|
subscriptions, err := h.subscriptionService.ListUserSubscriptions(c.Request.Context(), userID)
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
out := make([]dto.AdminUserSubscription, 0, len(subscriptions))
|
|
for i := range subscriptions {
|
|
out = append(out, *dto.UserSubscriptionFromServiceAdmin(&subscriptions[i]))
|
|
}
|
|
response.Success(c, out)
|
|
}
|
|
|
|
// Helper function to get admin ID from context
|
|
func getAdminIDFromContext(c *gin.Context) int64 {
|
|
subject, ok := middleware2.GetAuthSubjectFromContext(c)
|
|
if !ok {
|
|
return 0
|
|
}
|
|
return subject.UserID
|
|
}
|