v1.0.0
This commit is contained in:
@@ -235,4 +235,98 @@ func (r *GormApiCallRepository) FindByTransactionId(ctx context.Context, transac
|
||||
return nil, err
|
||||
}
|
||||
return &call, nil
|
||||
}
|
||||
|
||||
// ListWithFiltersAndProductName 管理端:根据条件筛选所有API调用记录(包含产品名称)
|
||||
func (r *GormApiCallRepository) ListWithFiltersAndProductName(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (map[string]string, []*entities.ApiCall, int64, error) {
|
||||
var callsWithProduct []*ApiCallWithProduct
|
||||
var total int64
|
||||
|
||||
// 构建基础查询条件
|
||||
whereCondition := "1=1"
|
||||
whereArgs := []interface{}{}
|
||||
|
||||
// 应用筛选条件
|
||||
if filters != nil {
|
||||
// 用户ID筛选
|
||||
if userId, ok := filters["user_id"].(string); ok && userId != "" {
|
||||
whereCondition += " AND ac.user_id = ?"
|
||||
whereArgs = append(whereArgs, userId)
|
||||
}
|
||||
|
||||
// 时间范围筛选
|
||||
if startTime, ok := filters["start_time"].(time.Time); ok {
|
||||
whereCondition += " AND ac.created_at >= ?"
|
||||
whereArgs = append(whereArgs, startTime)
|
||||
}
|
||||
if endTime, ok := filters["end_time"].(time.Time); ok {
|
||||
whereCondition += " AND ac.created_at <= ?"
|
||||
whereArgs = append(whereArgs, endTime)
|
||||
}
|
||||
|
||||
// TransactionID筛选
|
||||
if transactionId, ok := filters["transaction_id"].(string); ok && transactionId != "" {
|
||||
whereCondition += " AND ac.transaction_id LIKE ?"
|
||||
whereArgs = append(whereArgs, "%"+transactionId+"%")
|
||||
}
|
||||
|
||||
// 产品名称筛选
|
||||
if productName, ok := filters["product_name"].(string); ok && productName != "" {
|
||||
whereCondition += " AND p.name LIKE ?"
|
||||
whereArgs = append(whereArgs, "%"+productName+"%")
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if status, ok := filters["status"].(string); ok && status != "" {
|
||||
whereCondition += " AND ac.status = ?"
|
||||
whereArgs = append(whereArgs, status)
|
||||
}
|
||||
}
|
||||
|
||||
// 构建JOIN查询
|
||||
query := r.GetDB(ctx).Table("api_calls ac").
|
||||
Select("ac.*, p.name as product_name").
|
||||
Joins("LEFT JOIN product p ON ac.product_id = p.id").
|
||||
Where(whereCondition, whereArgs...)
|
||||
|
||||
// 获取总数
|
||||
var count int64
|
||||
err := query.Count(&count).Error
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
total = count
|
||||
|
||||
// 应用排序和分页
|
||||
if options.Sort != "" {
|
||||
query = query.Order("ac." + options.Sort + " " + options.Order)
|
||||
} else {
|
||||
query = query.Order("ac.created_at DESC")
|
||||
}
|
||||
|
||||
if options.Page > 0 && options.PageSize > 0 {
|
||||
offset := (options.Page - 1) * options.PageSize
|
||||
query = query.Offset(offset).Limit(options.PageSize)
|
||||
}
|
||||
|
||||
// 执行查询
|
||||
err = query.Find(&callsWithProduct).Error
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
|
||||
// 转换为entities.ApiCall并构建产品名称映射
|
||||
var calls []*entities.ApiCall
|
||||
productNameMap := make(map[string]string)
|
||||
|
||||
for _, c := range callsWithProduct {
|
||||
call := c.ApiCall
|
||||
calls = append(calls, &call)
|
||||
// 构建产品ID到产品名称的映射
|
||||
if c.ProductName != "" {
|
||||
productNameMap[call.ID] = c.ProductName
|
||||
}
|
||||
}
|
||||
|
||||
return productNameMap, calls, total, nil
|
||||
}
|
||||
@@ -292,5 +292,103 @@ func (r *GormWalletTransactionRepository) ListByUserIdWithFiltersAndProductName(
|
||||
}
|
||||
}
|
||||
|
||||
return productNameMap, transactions, total, nil
|
||||
}
|
||||
|
||||
// ListWithFiltersAndProductName 管理端:根据条件筛选所有钱包交易记录(包含产品名称)
|
||||
func (r *GormWalletTransactionRepository) ListWithFiltersAndProductName(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (map[string]string, []*entities.WalletTransaction, int64, error) {
|
||||
var transactionsWithProduct []*WalletTransactionWithProduct
|
||||
var total int64
|
||||
|
||||
// 构建基础查询条件
|
||||
whereCondition := "1=1"
|
||||
whereArgs := []interface{}{}
|
||||
|
||||
// 应用筛选条件
|
||||
if filters != nil {
|
||||
// 用户ID筛选
|
||||
if userId, ok := filters["user_id"].(string); ok && userId != "" {
|
||||
whereCondition += " AND wt.user_id = ?"
|
||||
whereArgs = append(whereArgs, userId)
|
||||
}
|
||||
|
||||
// 时间范围筛选
|
||||
if startTime, ok := filters["start_time"].(time.Time); ok {
|
||||
whereCondition += " AND wt.created_at >= ?"
|
||||
whereArgs = append(whereArgs, startTime)
|
||||
}
|
||||
if endTime, ok := filters["end_time"].(time.Time); ok {
|
||||
whereCondition += " AND wt.created_at <= ?"
|
||||
whereArgs = append(whereArgs, endTime)
|
||||
}
|
||||
|
||||
// 交易ID筛选
|
||||
if transactionId, ok := filters["transaction_id"].(string); ok && transactionId != "" {
|
||||
whereCondition += " AND wt.transaction_id LIKE ?"
|
||||
whereArgs = append(whereArgs, "%"+transactionId+"%")
|
||||
}
|
||||
|
||||
// 产品名称筛选
|
||||
if productName, ok := filters["product_name"].(string); ok && productName != "" {
|
||||
whereCondition += " AND p.name LIKE ?"
|
||||
whereArgs = append(whereArgs, "%"+productName+"%")
|
||||
}
|
||||
|
||||
// 金额范围筛选
|
||||
if minAmount, ok := filters["min_amount"].(string); ok && minAmount != "" {
|
||||
whereCondition += " AND wt.amount >= ?"
|
||||
whereArgs = append(whereArgs, minAmount)
|
||||
}
|
||||
if maxAmount, ok := filters["max_amount"].(string); ok && maxAmount != "" {
|
||||
whereCondition += " AND wt.amount <= ?"
|
||||
whereArgs = append(whereArgs, maxAmount)
|
||||
}
|
||||
}
|
||||
|
||||
// 构建JOIN查询
|
||||
query := r.GetDB(ctx).Table("wallet_transactions wt").
|
||||
Select("wt.*, p.name as product_name").
|
||||
Joins("LEFT JOIN product p ON wt.product_id = p.id").
|
||||
Where(whereCondition, whereArgs...)
|
||||
|
||||
// 获取总数
|
||||
var count int64
|
||||
err := query.Count(&count).Error
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
total = count
|
||||
|
||||
// 应用排序和分页
|
||||
if options.Sort != "" {
|
||||
query = query.Order("wt." + options.Sort + " " + options.Order)
|
||||
} else {
|
||||
query = query.Order("wt.created_at DESC")
|
||||
}
|
||||
|
||||
if options.Page > 0 && options.PageSize > 0 {
|
||||
offset := (options.Page - 1) * options.PageSize
|
||||
query = query.Offset(offset).Limit(options.PageSize)
|
||||
}
|
||||
|
||||
// 执行查询
|
||||
err = query.Find(&transactionsWithProduct).Error
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
|
||||
// 转换为entities.WalletTransaction并构建产品名称映射
|
||||
var transactions []*entities.WalletTransaction
|
||||
productNameMap := make(map[string]string)
|
||||
|
||||
for _, t := range transactionsWithProduct {
|
||||
transaction := t.WalletTransaction
|
||||
transactions = append(transactions, &transaction)
|
||||
// 构建产品ID到产品名称的映射
|
||||
if t.ProductName != "" {
|
||||
productNameMap[transaction.ProductID] = t.ProductName
|
||||
}
|
||||
}
|
||||
|
||||
return productNameMap, transactions, total, nil
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/domains/finance/entities"
|
||||
"tyapi-server/internal/domains/finance/repositories"
|
||||
"tyapi-server/internal/domains/finance/value_objects"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// GormInvoiceApplicationRepository 发票申请仓储的GORM实现
|
||||
type GormInvoiceApplicationRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewGormInvoiceApplicationRepository 创建发票申请仓储
|
||||
func NewGormInvoiceApplicationRepository(db *gorm.DB) repositories.InvoiceApplicationRepository {
|
||||
return &GormInvoiceApplicationRepository{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// Create 创建发票申请
|
||||
func (r *GormInvoiceApplicationRepository) Create(ctx context.Context, application *entities.InvoiceApplication) error {
|
||||
return r.db.WithContext(ctx).Create(application).Error
|
||||
}
|
||||
|
||||
// Update 更新发票申请
|
||||
func (r *GormInvoiceApplicationRepository) Update(ctx context.Context, application *entities.InvoiceApplication) error {
|
||||
return r.db.WithContext(ctx).Save(application).Error
|
||||
}
|
||||
|
||||
// Save 保存发票申请
|
||||
func (r *GormInvoiceApplicationRepository) Save(ctx context.Context, application *entities.InvoiceApplication) error {
|
||||
return r.db.WithContext(ctx).Save(application).Error
|
||||
}
|
||||
|
||||
// FindByID 根据ID查找发票申请
|
||||
func (r *GormInvoiceApplicationRepository) FindByID(ctx context.Context, id string) (*entities.InvoiceApplication, error) {
|
||||
var application entities.InvoiceApplication
|
||||
err := r.db.WithContext(ctx).Where("id = ?", id).First(&application).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &application, nil
|
||||
}
|
||||
|
||||
// FindByUserID 根据用户ID查找发票申请列表
|
||||
func (r *GormInvoiceApplicationRepository) FindByUserID(ctx context.Context, userID string, page, pageSize int) ([]*entities.InvoiceApplication, int64, error) {
|
||||
var applications []*entities.InvoiceApplication
|
||||
var total int64
|
||||
|
||||
// 获取总数
|
||||
err := r.db.WithContext(ctx).Model(&entities.InvoiceApplication{}).Where("user_id = ?", userID).Count(&total).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 获取分页数据
|
||||
offset := (page - 1) * pageSize
|
||||
err = r.db.WithContext(ctx).Where("user_id = ?", userID).
|
||||
Order("created_at DESC").
|
||||
Offset(offset).
|
||||
Limit(pageSize).
|
||||
Find(&applications).Error
|
||||
|
||||
return applications, total, err
|
||||
}
|
||||
|
||||
// FindPendingApplications 查找待处理的发票申请
|
||||
func (r *GormInvoiceApplicationRepository) FindPendingApplications(ctx context.Context, page, pageSize int) ([]*entities.InvoiceApplication, int64, error) {
|
||||
var applications []*entities.InvoiceApplication
|
||||
var total int64
|
||||
|
||||
// 获取总数
|
||||
err := r.db.WithContext(ctx).Model(&entities.InvoiceApplication{}).
|
||||
Where("status = ?", entities.ApplicationStatusPending).
|
||||
Count(&total).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 获取分页数据
|
||||
offset := (page - 1) * pageSize
|
||||
err = r.db.WithContext(ctx).
|
||||
Where("status = ?", entities.ApplicationStatusPending).
|
||||
Order("created_at ASC").
|
||||
Offset(offset).
|
||||
Limit(pageSize).
|
||||
Find(&applications).Error
|
||||
|
||||
return applications, total, err
|
||||
}
|
||||
|
||||
// FindByUserIDAndStatus 根据用户ID和状态查找发票申请
|
||||
func (r *GormInvoiceApplicationRepository) FindByUserIDAndStatus(ctx context.Context, userID string, status entities.ApplicationStatus, page, pageSize int) ([]*entities.InvoiceApplication, int64, error) {
|
||||
var applications []*entities.InvoiceApplication
|
||||
var total int64
|
||||
|
||||
query := r.db.WithContext(ctx).Model(&entities.InvoiceApplication{}).Where("user_id = ?", userID)
|
||||
if status != "" {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
err := query.Count(&total).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 获取分页数据
|
||||
offset := (page - 1) * pageSize
|
||||
err = query.Order("created_at DESC").
|
||||
Offset(offset).
|
||||
Limit(pageSize).
|
||||
Find(&applications).Error
|
||||
|
||||
return applications, total, err
|
||||
}
|
||||
|
||||
// FindByUserIDAndStatusWithTimeRange 根据用户ID、状态和时间范围查找发票申请列表
|
||||
func (r *GormInvoiceApplicationRepository) FindByUserIDAndStatusWithTimeRange(ctx context.Context, userID string, status entities.ApplicationStatus, startTime, endTime *time.Time, page, pageSize int) ([]*entities.InvoiceApplication, int64, error) {
|
||||
var applications []*entities.InvoiceApplication
|
||||
var total int64
|
||||
|
||||
query := r.db.WithContext(ctx).Model(&entities.InvoiceApplication{}).Where("user_id = ?", userID)
|
||||
|
||||
// 添加状态筛选
|
||||
if status != "" {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
|
||||
// 添加时间范围筛选
|
||||
if startTime != nil {
|
||||
query = query.Where("created_at >= ?", startTime)
|
||||
}
|
||||
if endTime != nil {
|
||||
query = query.Where("created_at <= ?", endTime)
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
err := query.Count(&total).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 获取分页数据
|
||||
offset := (page - 1) * pageSize
|
||||
err = query.Order("created_at DESC").
|
||||
Offset(offset).
|
||||
Limit(pageSize).
|
||||
Find(&applications).Error
|
||||
|
||||
return applications, total, err
|
||||
}
|
||||
|
||||
// FindByStatus 根据状态查找发票申请
|
||||
func (r *GormInvoiceApplicationRepository) FindByStatus(ctx context.Context, status entities.ApplicationStatus) ([]*entities.InvoiceApplication, error) {
|
||||
var applications []*entities.InvoiceApplication
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("status = ?", status).
|
||||
Order("created_at DESC").
|
||||
Find(&applications).Error
|
||||
return applications, err
|
||||
}
|
||||
|
||||
// GetUserInvoiceInfo 获取用户发票信息
|
||||
|
||||
|
||||
|
||||
|
||||
// GetUserTotalInvoicedAmount 获取用户已开票总金额
|
||||
func (r *GormInvoiceApplicationRepository) GetUserTotalInvoicedAmount(ctx context.Context, userID string) (string, error) {
|
||||
var total string
|
||||
err := r.db.WithContext(ctx).
|
||||
Model(&entities.InvoiceApplication{}).
|
||||
Select("COALESCE(SUM(CAST(amount AS DECIMAL(10,2))), '0')").
|
||||
Where("user_id = ? AND status = ?", userID, entities.ApplicationStatusCompleted).
|
||||
Scan(&total).Error
|
||||
|
||||
return total, err
|
||||
}
|
||||
|
||||
// GetUserTotalAppliedAmount 获取用户申请开票总金额
|
||||
func (r *GormInvoiceApplicationRepository) GetUserTotalAppliedAmount(ctx context.Context, userID string) (string, error) {
|
||||
var total string
|
||||
err := r.db.WithContext(ctx).
|
||||
Model(&entities.InvoiceApplication{}).
|
||||
Select("COALESCE(SUM(CAST(amount AS DECIMAL(10,2))), '0')").
|
||||
Where("user_id = ?", userID).
|
||||
Scan(&total).Error
|
||||
|
||||
return total, err
|
||||
}
|
||||
|
||||
// FindByUserIDAndInvoiceType 根据用户ID和发票类型查找申请
|
||||
func (r *GormInvoiceApplicationRepository) FindByUserIDAndInvoiceType(ctx context.Context, userID string, invoiceType value_objects.InvoiceType, page, pageSize int) ([]*entities.InvoiceApplication, int64, error) {
|
||||
var applications []*entities.InvoiceApplication
|
||||
var total int64
|
||||
|
||||
query := r.db.WithContext(ctx).Model(&entities.InvoiceApplication{}).Where("user_id = ? AND invoice_type = ?", userID, invoiceType)
|
||||
|
||||
// 获取总数
|
||||
err := query.Count(&total).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 获取分页数据
|
||||
offset := (page - 1) * pageSize
|
||||
err = query.Order("created_at DESC").
|
||||
Offset(offset).
|
||||
Limit(pageSize).
|
||||
Find(&applications).Error
|
||||
|
||||
return applications, total, err
|
||||
}
|
||||
|
||||
// FindByDateRange 根据日期范围查找申请
|
||||
func (r *GormInvoiceApplicationRepository) FindByDateRange(ctx context.Context, startDate, endDate string, page, pageSize int) ([]*entities.InvoiceApplication, int64, error) {
|
||||
var applications []*entities.InvoiceApplication
|
||||
var total int64
|
||||
|
||||
query := r.db.WithContext(ctx).Model(&entities.InvoiceApplication{})
|
||||
if startDate != "" {
|
||||
query = query.Where("DATE(created_at) >= ?", startDate)
|
||||
}
|
||||
if endDate != "" {
|
||||
query = query.Where("DATE(created_at) <= ?", endDate)
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
err := query.Count(&total).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 获取分页数据
|
||||
offset := (page - 1) * pageSize
|
||||
err = query.Order("created_at DESC").
|
||||
Offset(offset).
|
||||
Limit(pageSize).
|
||||
Find(&applications).Error
|
||||
|
||||
return applications, total, err
|
||||
}
|
||||
|
||||
// SearchApplications 搜索发票申请
|
||||
func (r *GormInvoiceApplicationRepository) SearchApplications(ctx context.Context, keyword string, page, pageSize int) ([]*entities.InvoiceApplication, int64, error) {
|
||||
var applications []*entities.InvoiceApplication
|
||||
var total int64
|
||||
|
||||
query := r.db.WithContext(ctx).Model(&entities.InvoiceApplication{}).
|
||||
Where("company_name LIKE ? OR email LIKE ? OR tax_number LIKE ?",
|
||||
fmt.Sprintf("%%%s%%", keyword),
|
||||
fmt.Sprintf("%%%s%%", keyword),
|
||||
fmt.Sprintf("%%%s%%", keyword))
|
||||
|
||||
// 获取总数
|
||||
err := query.Count(&total).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 获取分页数据
|
||||
offset := (page - 1) * pageSize
|
||||
err = query.Order("created_at DESC").
|
||||
Offset(offset).
|
||||
Limit(pageSize).
|
||||
Find(&applications).Error
|
||||
|
||||
return applications, total, err
|
||||
}
|
||||
|
||||
// FindByStatusWithTimeRange 根据状态和时间范围查找发票申请
|
||||
func (r *GormInvoiceApplicationRepository) FindByStatusWithTimeRange(ctx context.Context, status entities.ApplicationStatus, startTime, endTime *time.Time, page, pageSize int) ([]*entities.InvoiceApplication, int64, error) {
|
||||
var applications []*entities.InvoiceApplication
|
||||
var total int64
|
||||
|
||||
query := r.db.WithContext(ctx).Model(&entities.InvoiceApplication{}).Where("status = ?", status)
|
||||
|
||||
// 添加时间范围筛选
|
||||
if startTime != nil {
|
||||
query = query.Where("created_at >= ?", startTime)
|
||||
}
|
||||
if endTime != nil {
|
||||
query = query.Where("created_at <= ?", endTime)
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
err := query.Count(&total).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 获取分页数据
|
||||
offset := (page - 1) * pageSize
|
||||
err = query.Order("created_at DESC").
|
||||
Offset(offset).
|
||||
Limit(pageSize).
|
||||
Find(&applications).Error
|
||||
|
||||
return applications, total, err
|
||||
}
|
||||
|
||||
// FindAllWithTimeRange 根据时间范围查找所有发票申请
|
||||
func (r *GormInvoiceApplicationRepository) FindAllWithTimeRange(ctx context.Context, startTime, endTime *time.Time, page, pageSize int) ([]*entities.InvoiceApplication, int64, error) {
|
||||
var applications []*entities.InvoiceApplication
|
||||
var total int64
|
||||
|
||||
query := r.db.WithContext(ctx).Model(&entities.InvoiceApplication{})
|
||||
|
||||
// 添加时间范围筛选
|
||||
if startTime != nil {
|
||||
query = query.Where("created_at >= ?", startTime)
|
||||
}
|
||||
if endTime != nil {
|
||||
query = query.Where("created_at <= ?", endTime)
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
err := query.Count(&total).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 获取分页数据
|
||||
offset := (page - 1) * pageSize
|
||||
err = query.Order("created_at DESC").
|
||||
Offset(offset).
|
||||
Limit(pageSize).
|
||||
Find(&applications).Error
|
||||
|
||||
return applications, total, err
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"tyapi-server/internal/domains/finance/entities"
|
||||
"tyapi-server/internal/domains/finance/repositories"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// GormUserInvoiceInfoRepository 用户开票信息仓储的GORM实现
|
||||
type GormUserInvoiceInfoRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewGormUserInvoiceInfoRepository 创建用户开票信息仓储
|
||||
func NewGormUserInvoiceInfoRepository(db *gorm.DB) repositories.UserInvoiceInfoRepository {
|
||||
return &GormUserInvoiceInfoRepository{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// Create 创建用户开票信息
|
||||
func (r *GormUserInvoiceInfoRepository) Create(ctx context.Context, info *entities.UserInvoiceInfo) error {
|
||||
return r.db.WithContext(ctx).Create(info).Error
|
||||
}
|
||||
|
||||
// Update 更新用户开票信息
|
||||
func (r *GormUserInvoiceInfoRepository) Update(ctx context.Context, info *entities.UserInvoiceInfo) error {
|
||||
return r.db.WithContext(ctx).Save(info).Error
|
||||
}
|
||||
|
||||
// Save 保存用户开票信息(创建或更新)
|
||||
func (r *GormUserInvoiceInfoRepository) Save(ctx context.Context, info *entities.UserInvoiceInfo) error {
|
||||
return r.db.WithContext(ctx).Save(info).Error
|
||||
}
|
||||
|
||||
// FindByUserID 根据用户ID查找开票信息
|
||||
func (r *GormUserInvoiceInfoRepository) FindByUserID(ctx context.Context, userID string) (*entities.UserInvoiceInfo, error) {
|
||||
var info entities.UserInvoiceInfo
|
||||
err := r.db.WithContext(ctx).Where("user_id = ?", userID).First(&info).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
// FindByID 根据ID查找开票信息
|
||||
func (r *GormUserInvoiceInfoRepository) FindByID(ctx context.Context, id string) (*entities.UserInvoiceInfo, error) {
|
||||
var info entities.UserInvoiceInfo
|
||||
err := r.db.WithContext(ctx).Where("id = ?", id).First(&info).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
// Delete 删除用户开票信息
|
||||
func (r *GormUserInvoiceInfoRepository) Delete(ctx context.Context, userID string) error {
|
||||
return r.db.WithContext(ctx).Where("user_id = ?", userID).Delete(&entities.UserInvoiceInfo{}).Error
|
||||
}
|
||||
|
||||
// Exists 检查用户开票信息是否存在
|
||||
func (r *GormUserInvoiceInfoRepository) Exists(ctx context.Context, userID string) (bool, error) {
|
||||
var count int64
|
||||
err := r.db.WithContext(ctx).Model(&entities.UserInvoiceInfo{}).Where("user_id = ?", userID).Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"tyapi-server/internal/shared/database"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -113,13 +114,39 @@ func (r *GormSubscriptionRepository) ListSubscriptions(ctx context.Context, quer
|
||||
|
||||
// 应用筛选条件
|
||||
if query.UserID != "" {
|
||||
dbQuery = dbQuery.Where("user_id = ?", query.UserID)
|
||||
dbQuery = dbQuery.Where("subscription.user_id = ?", query.UserID)
|
||||
}
|
||||
// 这里筛选的是关联的Product实体里的name或code字段,只有当keyword匹配关联Product的name或code时才返回
|
||||
|
||||
// 关键词搜索(产品名称或编码)
|
||||
if query.Keyword != "" {
|
||||
dbQuery = dbQuery.Joins("LEFT JOIN product ON product.id = subscription.product_id").
|
||||
Where("product.name LIKE ? OR product.code LIKE ?", "%"+query.Keyword+"%", "%"+query.Keyword+"%")
|
||||
}
|
||||
|
||||
// 产品名称筛选
|
||||
if query.ProductName != "" {
|
||||
dbQuery = dbQuery.Joins("LEFT JOIN product ON product.id = subscription.product_id").
|
||||
Where("product.name LIKE ?", "%"+query.ProductName+"%")
|
||||
}
|
||||
|
||||
// 企业名称筛选(需要关联用户和企业信息)
|
||||
if query.CompanyName != "" {
|
||||
dbQuery = dbQuery.Joins("LEFT JOIN users ON users.id = subscription.user_id").
|
||||
Joins("LEFT JOIN enterprise_infos ON enterprise_infos.user_id = users.id").
|
||||
Where("enterprise_infos.company_name LIKE ?", "%"+query.CompanyName+"%")
|
||||
}
|
||||
|
||||
// 时间范围筛选
|
||||
if query.StartTime != "" {
|
||||
if t, err := time.Parse("2006-01-02 15:04:05", query.StartTime); err == nil {
|
||||
dbQuery = dbQuery.Where("subscription.created_at >= ?", t)
|
||||
}
|
||||
}
|
||||
if query.EndTime != "" {
|
||||
if t, err := time.Parse("2006-01-02 15:04:05", query.EndTime); err == nil {
|
||||
dbQuery = dbQuery.Where("subscription.created_at <= ?", t)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
if err := dbQuery.Count(&total).Error; err != nil {
|
||||
@@ -136,7 +163,7 @@ func (r *GormSubscriptionRepository) ListSubscriptions(ctx context.Context, quer
|
||||
}
|
||||
dbQuery = dbQuery.Order(order)
|
||||
} else {
|
||||
dbQuery = dbQuery.Order("created_at DESC")
|
||||
dbQuery = dbQuery.Order("subscription.created_at DESC")
|
||||
}
|
||||
|
||||
// 应用分页
|
||||
@@ -173,13 +200,23 @@ func (r *GormSubscriptionRepository) CountByUser(ctx context.Context, userID str
|
||||
return count, err
|
||||
}
|
||||
|
||||
// CountByProduct 统计产品订阅数量
|
||||
// CountByProduct 统计产品的订阅数量
|
||||
func (r *GormSubscriptionRepository) CountByProduct(ctx context.Context, productID string) (int64, error) {
|
||||
var count int64
|
||||
err := r.GetDB(ctx).WithContext(ctx).Model(&entities.Subscription{}).Where("product_id = ?", productID).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// GetTotalRevenue 获取总收入
|
||||
func (r *GormSubscriptionRepository) GetTotalRevenue(ctx context.Context) (float64, error) {
|
||||
var total decimal.Decimal
|
||||
err := r.GetDB(ctx).WithContext(ctx).Model(&entities.Subscription{}).Select("COALESCE(SUM(price), 0)").Scan(&total).Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return total.InexactFloat64(), nil
|
||||
}
|
||||
|
||||
// 基础Repository接口方法
|
||||
|
||||
// Count 返回订阅总数
|
||||
|
||||
230
internal/infrastructure/events/invoice_event_handler.go
Normal file
230
internal/infrastructure/events/invoice_event_handler.go
Normal file
@@ -0,0 +1,230 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/domains/finance/events"
|
||||
"tyapi-server/internal/infrastructure/external/email"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// InvoiceEventHandler 发票事件处理器
|
||||
type InvoiceEventHandler struct {
|
||||
logger *zap.Logger
|
||||
emailService *email.QQEmailService
|
||||
name string
|
||||
eventTypes []string
|
||||
isAsync bool
|
||||
}
|
||||
|
||||
// NewInvoiceEventHandler 创建发票事件处理器
|
||||
func NewInvoiceEventHandler(logger *zap.Logger, emailService *email.QQEmailService) *InvoiceEventHandler {
|
||||
return &InvoiceEventHandler{
|
||||
logger: logger,
|
||||
emailService: emailService,
|
||||
name: "invoice-event-handler",
|
||||
eventTypes: []string{
|
||||
"InvoiceApplicationCreated",
|
||||
"InvoiceApplicationApproved",
|
||||
"InvoiceApplicationRejected",
|
||||
"InvoiceFileUploaded",
|
||||
},
|
||||
isAsync: true,
|
||||
}
|
||||
}
|
||||
|
||||
// GetName 获取处理器名称
|
||||
func (h *InvoiceEventHandler) GetName() string {
|
||||
return h.name
|
||||
}
|
||||
|
||||
// GetEventTypes 获取支持的事件类型
|
||||
func (h *InvoiceEventHandler) GetEventTypes() []string {
|
||||
return h.eventTypes
|
||||
}
|
||||
|
||||
// IsAsync 是否为异步处理器
|
||||
func (h *InvoiceEventHandler) IsAsync() bool {
|
||||
return h.isAsync
|
||||
}
|
||||
|
||||
// GetRetryConfig 获取重试配置
|
||||
func (h *InvoiceEventHandler) GetRetryConfig() interfaces.RetryConfig {
|
||||
return interfaces.RetryConfig{
|
||||
MaxRetries: 3,
|
||||
RetryDelay: 5 * time.Second,
|
||||
BackoffFactor: 2.0,
|
||||
MaxDelay: 30 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle 处理事件
|
||||
func (h *InvoiceEventHandler) Handle(ctx context.Context, event interfaces.Event) error {
|
||||
h.logger.Info("🔄 开始处理发票事件",
|
||||
zap.String("event_type", event.GetType()),
|
||||
zap.String("event_id", event.GetID()),
|
||||
zap.String("aggregate_id", event.GetAggregateID()),
|
||||
zap.String("handler_name", h.GetName()),
|
||||
zap.Time("event_timestamp", event.GetTimestamp()),
|
||||
)
|
||||
|
||||
switch event.GetType() {
|
||||
case "InvoiceApplicationCreated":
|
||||
h.logger.Info("📝 处理发票申请创建事件")
|
||||
return h.handleInvoiceApplicationCreated(ctx, event)
|
||||
case "InvoiceApplicationApproved":
|
||||
h.logger.Info("✅ 处理发票申请通过事件")
|
||||
return h.handleInvoiceApplicationApproved(ctx, event)
|
||||
case "InvoiceApplicationRejected":
|
||||
h.logger.Info("❌ 处理发票申请拒绝事件")
|
||||
return h.handleInvoiceApplicationRejected(ctx, event)
|
||||
case "InvoiceFileUploaded":
|
||||
h.logger.Info("📎 处理发票文件上传事件")
|
||||
return h.handleInvoiceFileUploaded(ctx, event)
|
||||
default:
|
||||
h.logger.Warn("⚠️ 未知的发票事件类型", zap.String("event_type", event.GetType()))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// handleInvoiceApplicationCreated 处理发票申请创建事件
|
||||
func (h *InvoiceEventHandler) handleInvoiceApplicationCreated(ctx context.Context, event interfaces.Event) error {
|
||||
h.logger.Info("发票申请已创建",
|
||||
zap.String("application_id", event.GetAggregateID()),
|
||||
)
|
||||
|
||||
// 这里可以发送通知给管理员,告知有新的发票申请
|
||||
// 暂时只记录日志
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleInvoiceApplicationApproved 处理发票申请通过事件
|
||||
func (h *InvoiceEventHandler) handleInvoiceApplicationApproved(ctx context.Context, event interfaces.Event) error {
|
||||
h.logger.Info("发票申请已通过",
|
||||
zap.String("application_id", event.GetAggregateID()),
|
||||
)
|
||||
|
||||
// 这里可以发送通知给用户,告知发票申请已通过
|
||||
// 暂时只记录日志
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleInvoiceApplicationRejected 处理发票申请拒绝事件
|
||||
func (h *InvoiceEventHandler) handleInvoiceApplicationRejected(ctx context.Context, event interfaces.Event) error {
|
||||
h.logger.Info("发票申请被拒绝",
|
||||
zap.String("application_id", event.GetAggregateID()),
|
||||
)
|
||||
|
||||
// 这里可以发送邮件通知用户,告知发票申请被拒绝
|
||||
// 暂时只记录日志
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleInvoiceFileUploaded 处理发票文件上传事件
|
||||
func (h *InvoiceEventHandler) handleInvoiceFileUploaded(ctx context.Context, event interfaces.Event) error {
|
||||
h.logger.Info("📎 发票文件已上传事件开始处理",
|
||||
zap.String("invoice_id", event.GetAggregateID()),
|
||||
zap.String("event_id", event.GetID()),
|
||||
)
|
||||
|
||||
// 解析事件数据
|
||||
payload := event.GetPayload()
|
||||
if payload == nil {
|
||||
h.logger.Error("❌ 事件数据为空")
|
||||
return fmt.Errorf("事件数据为空")
|
||||
}
|
||||
|
||||
h.logger.Info("📋 事件数据解析开始",
|
||||
zap.Any("payload_type", fmt.Sprintf("%T", payload)),
|
||||
)
|
||||
|
||||
// 将payload转换为JSON,然后解析为InvoiceFileUploadedEvent
|
||||
payloadBytes, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
h.logger.Error("❌ 序列化事件数据失败", zap.Error(err))
|
||||
return fmt.Errorf("序列化事件数据失败: %w", err)
|
||||
}
|
||||
|
||||
h.logger.Info("📄 事件数据序列化成功",
|
||||
zap.String("payload_json", string(payloadBytes)),
|
||||
)
|
||||
|
||||
var fileUploadedEvent events.InvoiceFileUploadedEvent
|
||||
err = json.Unmarshal(payloadBytes, &fileUploadedEvent)
|
||||
if err != nil {
|
||||
h.logger.Error("❌ 解析发票文件上传事件失败", zap.Error(err))
|
||||
return fmt.Errorf("解析发票文件上传事件失败: %w", err)
|
||||
}
|
||||
|
||||
h.logger.Info("✅ 事件数据解析成功",
|
||||
zap.String("invoice_id", fileUploadedEvent.InvoiceID),
|
||||
zap.String("user_id", fileUploadedEvent.UserID),
|
||||
zap.String("receiving_email", fileUploadedEvent.ReceivingEmail),
|
||||
zap.String("file_name", fileUploadedEvent.FileName),
|
||||
zap.String("file_url", fileUploadedEvent.FileURL),
|
||||
zap.String("company_name", fileUploadedEvent.CompanyName),
|
||||
zap.String("amount", fileUploadedEvent.Amount.String()),
|
||||
zap.String("invoice_type", string(fileUploadedEvent.InvoiceType)),
|
||||
)
|
||||
|
||||
// 发送发票邮件给用户
|
||||
return h.sendInvoiceEmail(ctx, &fileUploadedEvent)
|
||||
}
|
||||
|
||||
// sendInvoiceEmail 发送发票邮件
|
||||
func (h *InvoiceEventHandler) sendInvoiceEmail(ctx context.Context, event *events.InvoiceFileUploadedEvent) error {
|
||||
h.logger.Info("📧 开始发送发票邮件",
|
||||
zap.String("invoice_id", event.InvoiceID),
|
||||
zap.String("user_id", event.UserID),
|
||||
zap.String("receiving_email", event.ReceivingEmail),
|
||||
zap.String("file_name", event.FileName),
|
||||
zap.String("file_url", event.FileURL),
|
||||
)
|
||||
|
||||
// 构建邮件数据
|
||||
emailData := &email.InvoiceEmailData{
|
||||
CompanyName: event.CompanyName,
|
||||
Amount: event.Amount.String(),
|
||||
InvoiceType: event.InvoiceType.GetDisplayName(),
|
||||
FileURL: event.FileURL,
|
||||
FileName: event.FileName,
|
||||
ReceivingEmail: event.ReceivingEmail,
|
||||
ApprovedAt: event.UploadedAt.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
|
||||
h.logger.Info("📋 邮件数据构建完成",
|
||||
zap.String("company_name", emailData.CompanyName),
|
||||
zap.String("amount", emailData.Amount),
|
||||
zap.String("invoice_type", emailData.InvoiceType),
|
||||
zap.String("file_url", emailData.FileURL),
|
||||
zap.String("file_name", emailData.FileName),
|
||||
zap.String("receiving_email", emailData.ReceivingEmail),
|
||||
zap.String("approved_at", emailData.ApprovedAt),
|
||||
)
|
||||
|
||||
// 发送邮件
|
||||
h.logger.Info("🚀 开始调用邮件服务发送邮件")
|
||||
err := h.emailService.SendInvoiceEmail(ctx, emailData)
|
||||
if err != nil {
|
||||
h.logger.Error("❌ 发送发票邮件失败",
|
||||
zap.String("invoice_id", event.InvoiceID),
|
||||
zap.String("receiving_email", event.ReceivingEmail),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("发送发票邮件失败: %w", err)
|
||||
}
|
||||
|
||||
h.logger.Info("✅ 发票邮件发送成功",
|
||||
zap.String("invoice_id", event.InvoiceID),
|
||||
zap.String("receiving_email", event.ReceivingEmail),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
115
internal/infrastructure/events/invoice_event_publisher.go
Normal file
115
internal/infrastructure/events/invoice_event_publisher.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/domains/finance/events"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// InvoiceEventPublisher 发票事件发布器实现
|
||||
type InvoiceEventPublisher struct {
|
||||
logger *zap.Logger
|
||||
eventBus interfaces.EventBus
|
||||
}
|
||||
|
||||
// NewInvoiceEventPublisher 创建发票事件发布器
|
||||
func NewInvoiceEventPublisher(logger *zap.Logger, eventBus interfaces.EventBus) *InvoiceEventPublisher {
|
||||
return &InvoiceEventPublisher{
|
||||
logger: logger,
|
||||
eventBus: eventBus,
|
||||
}
|
||||
}
|
||||
|
||||
// PublishInvoiceApplicationCreated 发布发票申请创建事件
|
||||
func (p *InvoiceEventPublisher) PublishInvoiceApplicationCreated(ctx context.Context, event *events.InvoiceApplicationCreatedEvent) error {
|
||||
p.logger.Info("发布发票申请创建事件",
|
||||
zap.String("application_id", event.ApplicationID),
|
||||
zap.String("user_id", event.UserID),
|
||||
zap.String("invoice_type", string(event.InvoiceType)),
|
||||
zap.String("amount", event.Amount.String()),
|
||||
zap.String("company_name", event.CompanyName),
|
||||
zap.String("receiving_email", event.ReceivingEmail),
|
||||
)
|
||||
|
||||
// TODO: 实现实际的事件发布逻辑
|
||||
// 例如:发送到消息队列、调用外部服务等
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PublishInvoiceApplicationApproved 发布发票申请通过事件
|
||||
func (p *InvoiceEventPublisher) PublishInvoiceApplicationApproved(ctx context.Context, event *events.InvoiceApplicationApprovedEvent) error {
|
||||
p.logger.Info("发布发票申请通过事件",
|
||||
zap.String("application_id", event.ApplicationID),
|
||||
zap.String("user_id", event.UserID),
|
||||
zap.String("amount", event.Amount.String()),
|
||||
zap.String("receiving_email", event.ReceivingEmail),
|
||||
zap.Time("approved_at", event.ApprovedAt),
|
||||
)
|
||||
|
||||
// TODO: 实现实际的事件发布逻辑
|
||||
// 例如:发送邮件通知用户、更新统计数据等
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PublishInvoiceApplicationRejected 发布发票申请拒绝事件
|
||||
func (p *InvoiceEventPublisher) PublishInvoiceApplicationRejected(ctx context.Context, event *events.InvoiceApplicationRejectedEvent) error {
|
||||
p.logger.Info("发布发票申请拒绝事件",
|
||||
zap.String("application_id", event.ApplicationID),
|
||||
zap.String("user_id", event.UserID),
|
||||
zap.String("reason", event.Reason),
|
||||
zap.String("receiving_email", event.ReceivingEmail),
|
||||
zap.Time("rejected_at", event.RejectedAt),
|
||||
)
|
||||
|
||||
// TODO: 实现实际的事件发布逻辑
|
||||
// 例如:发送邮件通知用户、记录拒绝原因等
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PublishInvoiceFileUploaded 发布发票文件上传事件
|
||||
func (p *InvoiceEventPublisher) PublishInvoiceFileUploaded(ctx context.Context, event *events.InvoiceFileUploadedEvent) error {
|
||||
p.logger.Info("📤 开始发布发票文件上传事件",
|
||||
zap.String("invoice_id", event.InvoiceID),
|
||||
zap.String("user_id", event.UserID),
|
||||
zap.String("file_id", event.FileID),
|
||||
zap.String("file_name", event.FileName),
|
||||
zap.String("file_url", event.FileURL),
|
||||
zap.String("receiving_email", event.ReceivingEmail),
|
||||
zap.Time("uploaded_at", event.UploadedAt),
|
||||
)
|
||||
|
||||
// 发布到事件总线
|
||||
if p.eventBus != nil {
|
||||
p.logger.Info("🚀 准备发布事件到事件总线",
|
||||
zap.String("event_type", event.GetType()),
|
||||
zap.String("event_id", event.GetID()),
|
||||
)
|
||||
|
||||
if err := p.eventBus.Publish(ctx, event); err != nil {
|
||||
p.logger.Error("❌ 发布发票文件上传事件到事件总线失败",
|
||||
zap.String("invoice_id", event.InvoiceID),
|
||||
zap.String("event_type", event.GetType()),
|
||||
zap.String("event_id", event.GetID()),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
p.logger.Info("✅ 发票文件上传事件已发布到事件总线",
|
||||
zap.String("invoice_id", event.InvoiceID),
|
||||
zap.String("event_type", event.GetType()),
|
||||
zap.String("event_id", event.GetID()),
|
||||
)
|
||||
} else {
|
||||
p.logger.Warn("⚠️ 事件总线未初始化,无法发布事件",
|
||||
zap.String("invoice_id", event.InvoiceID),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
712
internal/infrastructure/external/email/qq_email_service.go
vendored
Normal file
712
internal/infrastructure/external/email/qq_email_service.go
vendored
Normal file
@@ -0,0 +1,712 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net"
|
||||
"net/smtp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/config"
|
||||
)
|
||||
|
||||
// QQEmailService QQ邮箱服务
|
||||
type QQEmailService struct {
|
||||
config config.EmailConfig
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// EmailData 邮件数据
|
||||
type EmailData struct {
|
||||
To string `json:"to"`
|
||||
Subject string `json:"subject"`
|
||||
Content string `json:"content"`
|
||||
Data map[string]interface{} `json:"data"`
|
||||
}
|
||||
|
||||
// InvoiceEmailData 发票邮件数据
|
||||
type InvoiceEmailData struct {
|
||||
CompanyName string `json:"company_name"`
|
||||
Amount string `json:"amount"`
|
||||
InvoiceType string `json:"invoice_type"`
|
||||
FileURL string `json:"file_url"`
|
||||
FileName string `json:"file_name"`
|
||||
ReceivingEmail string `json:"receiving_email"`
|
||||
ApprovedAt string `json:"approved_at"`
|
||||
}
|
||||
|
||||
// NewQQEmailService 创建QQ邮箱服务
|
||||
func NewQQEmailService(config config.EmailConfig, logger *zap.Logger) *QQEmailService {
|
||||
return &QQEmailService{
|
||||
config: config,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// SendEmail 发送邮件
|
||||
func (s *QQEmailService) SendEmail(ctx context.Context, data *EmailData) error {
|
||||
s.logger.Info("开始发送邮件",
|
||||
zap.String("to", data.To),
|
||||
zap.String("subject", data.Subject),
|
||||
)
|
||||
|
||||
// 构建邮件内容
|
||||
message := s.buildEmailMessage(data)
|
||||
|
||||
// 发送邮件
|
||||
err := s.sendSMTP(data.To, data.Subject, message)
|
||||
if err != nil {
|
||||
s.logger.Error("发送邮件失败",
|
||||
zap.String("to", data.To),
|
||||
zap.String("subject", data.Subject),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("发送邮件失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("邮件发送成功",
|
||||
zap.String("to", data.To),
|
||||
zap.String("subject", data.Subject),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendInvoiceEmail 发送发票邮件
|
||||
func (s *QQEmailService) SendInvoiceEmail(ctx context.Context, data *InvoiceEmailData) error {
|
||||
s.logger.Info("开始发送发票邮件",
|
||||
zap.String("to", data.ReceivingEmail),
|
||||
zap.String("company_name", data.CompanyName),
|
||||
zap.String("amount", data.Amount),
|
||||
)
|
||||
|
||||
// 构建邮件内容
|
||||
subject := "您的发票已开具成功"
|
||||
content := s.buildInvoiceEmailContent(data)
|
||||
|
||||
emailData := &EmailData{
|
||||
To: data.ReceivingEmail,
|
||||
Subject: subject,
|
||||
Content: content,
|
||||
Data: map[string]interface{}{
|
||||
"company_name": data.CompanyName,
|
||||
"amount": data.Amount,
|
||||
"invoice_type": data.InvoiceType,
|
||||
"file_url": data.FileURL,
|
||||
"file_name": data.FileName,
|
||||
"approved_at": data.ApprovedAt,
|
||||
},
|
||||
}
|
||||
|
||||
return s.SendEmail(ctx, emailData)
|
||||
}
|
||||
|
||||
// buildEmailMessage 构建邮件消息
|
||||
func (s *QQEmailService) buildEmailMessage(data *EmailData) string {
|
||||
headers := make(map[string]string)
|
||||
headers["From"] = s.config.FromEmail
|
||||
headers["To"] = data.To
|
||||
headers["Subject"] = data.Subject
|
||||
headers["MIME-Version"] = "1.0"
|
||||
headers["Content-Type"] = "text/html; charset=UTF-8"
|
||||
|
||||
var message strings.Builder
|
||||
for key, value := range headers {
|
||||
message.WriteString(fmt.Sprintf("%s: %s\r\n", key, value))
|
||||
}
|
||||
message.WriteString("\r\n")
|
||||
message.WriteString(data.Content)
|
||||
|
||||
return message.String()
|
||||
}
|
||||
|
||||
// buildInvoiceEmailContent 构建发票邮件内容
|
||||
func (s *QQEmailService) buildInvoiceEmailContent(data *InvoiceEmailData) string {
|
||||
htmlTemplate := `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>发票开具成功通知</title>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', 'Microsoft YaHei', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #2d3748;
|
||||
background: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 650px;
|
||||
margin: 0 auto;
|
||||
background: #ffffff;
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 50px 40px 40px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: radial-gradient(circle, rgba(255,255,255,0.08) 0%, transparent 70%);
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0px) rotate(0deg); }
|
||||
50% { transform: translateY(-20px) rotate(180deg); }
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.greeting {
|
||||
padding: 40px 40px 20px;
|
||||
text-align: center;
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #ffffff 100%);
|
||||
}
|
||||
|
||||
.greeting p {
|
||||
font-size: 16px;
|
||||
color: #4a5568;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.access-section {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin: 0 20px 30px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.access-section::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
|
||||
animation: shimmer 8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0%, 100% { transform: translateX(-100%) translateY(-100%) rotate(0deg); }
|
||||
50% { transform: translateX(100%) translateY(100%) rotate(180deg); }
|
||||
}
|
||||
|
||||
.access-section h3 {
|
||||
color: white;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.access-section p {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-bottom: 25px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.access-btn {
|
||||
display: inline-block;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
color: white;
|
||||
padding: 16px 32px;
|
||||
text-decoration: none;
|
||||
border-radius: 50px;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
backdrop-filter: blur(10px);
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.access-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.15);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.info-section {
|
||||
padding: 0 40px 40px;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #ffffff 100%);
|
||||
padding: 24px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid #e2e8f0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.info-item:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 25px -5px rgba(102, 126, 234, 0.1);
|
||||
border-color: #cbd5e0;
|
||||
}
|
||||
|
||||
.info-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 0 2px 2px 0;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-weight: 600;
|
||||
color: #718096;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.8px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #2d3748;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.notes-section {
|
||||
background: linear-gradient(135deg, #f0fff4 0%, #ffffff 100%);
|
||||
padding: 30px;
|
||||
border-radius: 16px;
|
||||
margin: 30px 0;
|
||||
border: 1px solid #c6f6d5;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.notes-section::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
|
||||
border-radius: 0 2px 2px 0;
|
||||
}
|
||||
|
||||
.notes-section h4 {
|
||||
color: #2f855a;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.notes-section h4::before {
|
||||
content: '📋';
|
||||
margin-right: 8px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.notes-section ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.notes-section li {
|
||||
color: #4a5568;
|
||||
margin-bottom: 10px;
|
||||
padding-left: 24px;
|
||||
position: relative;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.notes-section li::before {
|
||||
content: '✓';
|
||||
color: #48bb78;
|
||||
font-weight: bold;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
background: linear-gradient(135deg, #2d3748 0%, #1a202c 100%);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
padding: 35px 40px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.footer p {
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.footer-divider {
|
||||
width: 60px;
|
||||
height: 2px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
margin: 20px auto;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.container {
|
||||
margin: 10px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 40px 30px 30px;
|
||||
}
|
||||
|
||||
.greeting {
|
||||
padding: 30px 30px 20px;
|
||||
}
|
||||
|
||||
.access-section {
|
||||
margin: 0 15px 25px;
|
||||
padding: 30px 25px;
|
||||
}
|
||||
|
||||
.info-section {
|
||||
padding: 0 30px 30px;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 30px 30px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<div class="success-icon">✓</div>
|
||||
<h1>发票已开具完成</h1>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="greeting">
|
||||
<p>尊敬的用户,您好!</p>
|
||||
<p>您的发票申请已审核通过,发票已成功开具。</p>
|
||||
</div>
|
||||
|
||||
<div class="access-section">
|
||||
<h3>📄 发票访问链接</h3>
|
||||
<p>您的发票已准备就绪,请点击下方按钮访问查看页面</p>
|
||||
<a href="{{.FileURL}}" class="access-btn" target="_blank">
|
||||
🔗 访问发票页面
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="info-section">
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<span class="info-label">公司名称</span>
|
||||
<span class="info-value">{{.CompanyName}}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<span class="info-label">发票金额</span>
|
||||
<span class="info-value">¥{{.Amount}}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<span class="info-label">发票类型</span>
|
||||
<span class="info-value">{{.InvoiceType}}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<span class="info-label">开具时间</span>
|
||||
<span class="info-value">{{.ApprovedAt}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="notes-section">
|
||||
<h4>注意事项</h4>
|
||||
<ul>
|
||||
<li>访问页面后可在页面内下载发票文件</li>
|
||||
<li>请妥善保管发票文件,建议打印存档</li>
|
||||
<li>如有疑问,请回到我们平台进行下载</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>此邮件由系统自动发送,请勿回复</p>
|
||||
<div class="footer-divider"></div>
|
||||
<p>天远数据 API 服务平台</p>
|
||||
<p>发送时间:{{.CurrentTime}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
// 解析模板
|
||||
tmpl, err := template.New("invoice_email").Parse(htmlTemplate)
|
||||
if err != nil {
|
||||
s.logger.Error("解析邮件模板失败", zap.Error(err))
|
||||
return s.buildSimpleInvoiceEmail(data)
|
||||
}
|
||||
|
||||
// 准备模板数据
|
||||
templateData := struct {
|
||||
CompanyName string
|
||||
Amount string
|
||||
InvoiceType string
|
||||
FileURL string
|
||||
FileName string
|
||||
ApprovedAt string
|
||||
CurrentTime string
|
||||
Domain string
|
||||
}{
|
||||
CompanyName: data.CompanyName,
|
||||
Amount: data.Amount,
|
||||
InvoiceType: data.InvoiceType,
|
||||
FileURL: data.FileURL,
|
||||
FileName: data.FileName,
|
||||
ApprovedAt: data.ApprovedAt,
|
||||
CurrentTime: time.Now().Format("2006-01-02 15:04:05"),
|
||||
Domain: s.config.Domain,
|
||||
}
|
||||
|
||||
// 执行模板
|
||||
var content strings.Builder
|
||||
err = tmpl.Execute(&content, templateData)
|
||||
if err != nil {
|
||||
s.logger.Error("执行邮件模板失败", zap.Error(err))
|
||||
return s.buildSimpleInvoiceEmail(data)
|
||||
}
|
||||
|
||||
return content.String()
|
||||
}
|
||||
|
||||
// buildSimpleInvoiceEmail 构建简单的发票邮件内容(备用方案)
|
||||
func (s *QQEmailService) buildSimpleInvoiceEmail(data *InvoiceEmailData) string {
|
||||
return fmt.Sprintf(`
|
||||
发票开具成功通知
|
||||
|
||||
尊敬的用户,您好!
|
||||
|
||||
您的发票申请已审核通过,发票已成功开具。
|
||||
|
||||
发票信息:
|
||||
- 公司名称:%s
|
||||
- 发票金额:¥%s
|
||||
- 发票类型:%s
|
||||
- 开具时间:%s
|
||||
|
||||
发票文件下载链接:%s
|
||||
文件名:%s
|
||||
|
||||
如有疑问,请访问控制台查看详细信息:https://%s
|
||||
|
||||
天远数据 API 服务平台
|
||||
%s
|
||||
`, data.CompanyName, data.Amount, data.InvoiceType, data.ApprovedAt, data.FileURL, data.FileName, s.config.Domain, time.Now().Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
|
||||
// sendSMTP 通过SMTP发送邮件
|
||||
func (s *QQEmailService) sendSMTP(to, subject, message string) error {
|
||||
// 构建认证信息
|
||||
auth := smtp.PlainAuth("", s.config.Username, s.config.Password, s.config.Host)
|
||||
|
||||
// 构建收件人列表
|
||||
toList := []string{to}
|
||||
|
||||
// 发送邮件
|
||||
if s.config.UseSSL {
|
||||
// QQ邮箱587端口使用STARTTLS,465端口使用直接SSL
|
||||
if s.config.Port == 587 {
|
||||
// 使用STARTTLS (587端口)
|
||||
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", s.config.Host, s.config.Port))
|
||||
if err != nil {
|
||||
return fmt.Errorf("连接SMTP服务器失败: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client, err := smtp.NewClient(conn, s.config.Host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建SMTP客户端失败: %w", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
// 启用STARTTLS
|
||||
if err = client.StartTLS(&tls.Config{
|
||||
ServerName: s.config.Host,
|
||||
InsecureSkipVerify: false,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("启用STARTTLS失败: %w", err)
|
||||
}
|
||||
|
||||
// 认证
|
||||
if err = client.Auth(auth); err != nil {
|
||||
return fmt.Errorf("SMTP认证失败: %w", err)
|
||||
}
|
||||
|
||||
// 设置发件人
|
||||
if err = client.Mail(s.config.FromEmail); err != nil {
|
||||
return fmt.Errorf("设置发件人失败: %w", err)
|
||||
}
|
||||
|
||||
// 设置收件人
|
||||
for _, recipient := range toList {
|
||||
if err = client.Rcpt(recipient); err != nil {
|
||||
return fmt.Errorf("设置收件人失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 发送邮件内容
|
||||
writer, err := client.Data()
|
||||
if err != nil {
|
||||
return fmt.Errorf("准备发送邮件内容失败: %w", err)
|
||||
}
|
||||
defer writer.Close()
|
||||
|
||||
_, err = writer.Write([]byte(message))
|
||||
if err != nil {
|
||||
return fmt.Errorf("发送邮件内容失败: %w", err)
|
||||
}
|
||||
} else {
|
||||
// 使用直接SSL连接 (465端口)
|
||||
tlsConfig := &tls.Config{
|
||||
ServerName: s.config.Host,
|
||||
InsecureSkipVerify: false,
|
||||
}
|
||||
|
||||
conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", s.config.Host, s.config.Port), tlsConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("连接SMTP服务器失败: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client, err := smtp.NewClient(conn, s.config.Host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建SMTP客户端失败: %w", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
// 认证
|
||||
if err = client.Auth(auth); err != nil {
|
||||
return fmt.Errorf("SMTP认证失败: %w", err)
|
||||
}
|
||||
|
||||
// 设置发件人
|
||||
if err = client.Mail(s.config.FromEmail); err != nil {
|
||||
return fmt.Errorf("设置发件人失败: %w", err)
|
||||
}
|
||||
|
||||
// 设置收件人
|
||||
for _, recipient := range toList {
|
||||
if err = client.Rcpt(recipient); err != nil {
|
||||
return fmt.Errorf("设置收件人失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 发送邮件内容
|
||||
writer, err := client.Data()
|
||||
if err != nil {
|
||||
return fmt.Errorf("准备发送邮件内容失败: %w", err)
|
||||
}
|
||||
defer writer.Close()
|
||||
|
||||
_, err = writer.Write([]byte(message))
|
||||
if err != nil {
|
||||
return fmt.Errorf("发送邮件内容失败: %w", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 使用普通连接
|
||||
err := smtp.SendMail(
|
||||
fmt.Sprintf("%s:%d", s.config.Host, s.config.Port),
|
||||
auth,
|
||||
s.config.FromEmail,
|
||||
toList,
|
||||
[]byte(message),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("发送邮件失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -279,3 +280,56 @@ func (s *QiNiuStorageService) UploadFromReader(ctx context.Context, reader io.Re
|
||||
|
||||
return s.UploadFile(ctx, fileBytes, fileName)
|
||||
}
|
||||
|
||||
// DownloadFile 从七牛云下载文件
|
||||
func (s *QiNiuStorageService) DownloadFile(ctx context.Context, fileURL string) ([]byte, error) {
|
||||
s.logger.Info("开始从七牛云下载文件", zap.String("file_url", fileURL))
|
||||
|
||||
// 创建HTTP客户端
|
||||
client := &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
// 创建请求
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", fileURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建请求失败: %w", err)
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
s.logger.Error("下载文件失败",
|
||||
zap.String("file_url", fileURL),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, fmt.Errorf("下载文件失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 检查响应状态
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
s.logger.Error("下载文件失败,状态码异常",
|
||||
zap.String("file_url", fileURL),
|
||||
zap.Int("status_code", resp.StatusCode),
|
||||
)
|
||||
return nil, fmt.Errorf("下载文件失败,状态码: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
fileContent, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
s.logger.Error("读取文件内容失败",
|
||||
zap.String("file_url", fileURL),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, fmt.Errorf("读取文件内容失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("文件下载成功",
|
||||
zap.String("file_url", fileURL),
|
||||
zap.Int("file_size", len(fileContent)),
|
||||
)
|
||||
|
||||
return fileContent, nil
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ func (h *ApiHandler) AddWhiteListIP(c *gin.Context) {
|
||||
}
|
||||
|
||||
var req dto.WhiteListRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
if err := h.validator.BindAndValidate(c, &req); err != nil {
|
||||
h.responseBuilder.BadRequest(c, "请求参数错误")
|
||||
return
|
||||
}
|
||||
@@ -311,6 +311,86 @@ func (h *ApiHandler) GetUserApiCalls(c *gin.Context) {
|
||||
h.responseBuilder.Success(c, result, "获取API调用记录成功")
|
||||
}
|
||||
|
||||
// GetAdminApiCalls 获取管理端API调用记录
|
||||
// @Summary 获取管理端API调用记录
|
||||
// @Description 管理员获取API调用记录,支持筛选和分页
|
||||
// @Tags API管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param page_size query int false "每页数量" default(10)
|
||||
// @Param user_id query string false "用户ID"
|
||||
// @Param transaction_id query string false "交易ID"
|
||||
// @Param product_name query string false "产品名称"
|
||||
// @Param status query string false "状态"
|
||||
// @Param start_time query string false "开始时间" format(date-time)
|
||||
// @Param end_time query string false "结束时间" format(date-time)
|
||||
// @Param sort_by query string false "排序字段"
|
||||
// @Param sort_order query string false "排序方向" Enums(asc, desc)
|
||||
// @Success 200 {object} dto.ApiCallListResponse "获取API调用记录成功"
|
||||
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /api/v1/admin/api-calls [get]
|
||||
func (h *ApiHandler) GetAdminApiCalls(c *gin.Context) {
|
||||
// 解析查询参数
|
||||
page := h.getIntQuery(c, "page", 1)
|
||||
pageSize := h.getIntQuery(c, "page_size", 10)
|
||||
|
||||
// 构建筛选条件
|
||||
filters := make(map[string]interface{})
|
||||
|
||||
// 用户ID筛选
|
||||
if userId := c.Query("user_id"); userId != "" {
|
||||
filters["user_id"] = userId
|
||||
}
|
||||
|
||||
// 时间范围筛选
|
||||
if startTime := c.Query("start_time"); startTime != "" {
|
||||
if t, err := time.Parse("2006-01-02 15:04:05", startTime); err == nil {
|
||||
filters["start_time"] = t
|
||||
}
|
||||
}
|
||||
if endTime := c.Query("end_time"); endTime != "" {
|
||||
if t, err := time.Parse("2006-01-02 15:04:05", endTime); err == nil {
|
||||
filters["end_time"] = t
|
||||
}
|
||||
}
|
||||
|
||||
// 交易ID筛选
|
||||
if transactionId := c.Query("transaction_id"); transactionId != "" {
|
||||
filters["transaction_id"] = transactionId
|
||||
}
|
||||
|
||||
// 产品名称筛选
|
||||
if productName := c.Query("product_name"); productName != "" {
|
||||
filters["product_name"] = productName
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if status := c.Query("status"); status != "" {
|
||||
filters["status"] = status
|
||||
}
|
||||
|
||||
// 构建分页选项
|
||||
options := interfaces.ListOptions{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
Sort: "created_at",
|
||||
Order: "desc",
|
||||
}
|
||||
|
||||
result, err := h.appService.GetAdminApiCalls(c.Request.Context(), filters, options)
|
||||
if err != nil {
|
||||
h.logger.Error("获取管理端API调用记录失败", zap.Error(err))
|
||||
h.responseBuilder.BadRequest(c, "获取API调用记录失败")
|
||||
return
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, result, "获取API调用记录成功")
|
||||
}
|
||||
|
||||
// getIntQuery 获取整数查询参数
|
||||
func (h *ApiHandler) getIntQuery(c *gin.Context, key string, defaultValue int) int {
|
||||
if value := c.Query(key); value != "" {
|
||||
|
||||
@@ -17,24 +17,30 @@ import (
|
||||
|
||||
// FinanceHandler 财务HTTP处理器
|
||||
type FinanceHandler struct {
|
||||
appService finance.FinanceApplicationService
|
||||
responseBuilder interfaces.ResponseBuilder
|
||||
validator interfaces.RequestValidator
|
||||
logger *zap.Logger
|
||||
appService finance.FinanceApplicationService
|
||||
invoiceAppService finance.InvoiceApplicationService
|
||||
adminInvoiceAppService finance.AdminInvoiceApplicationService
|
||||
responseBuilder interfaces.ResponseBuilder
|
||||
validator interfaces.RequestValidator
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewFinanceHandler 创建财务HTTP处理器
|
||||
func NewFinanceHandler(
|
||||
appService finance.FinanceApplicationService,
|
||||
invoiceAppService finance.InvoiceApplicationService,
|
||||
adminInvoiceAppService finance.AdminInvoiceApplicationService,
|
||||
responseBuilder interfaces.ResponseBuilder,
|
||||
validator interfaces.RequestValidator,
|
||||
logger *zap.Logger,
|
||||
) *FinanceHandler {
|
||||
return &FinanceHandler{
|
||||
appService: appService,
|
||||
responseBuilder: responseBuilder,
|
||||
validator: validator,
|
||||
logger: logger,
|
||||
appService: appService,
|
||||
invoiceAppService: invoiceAppService,
|
||||
adminInvoiceAppService: adminInvoiceAppService,
|
||||
responseBuilder: responseBuilder,
|
||||
validator: validator,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -554,3 +560,381 @@ func (h *FinanceHandler) GetAlipayOrderStatus(c *gin.Context) {
|
||||
|
||||
h.responseBuilder.Success(c, result, "获取订单状态成功")
|
||||
}
|
||||
|
||||
// ==================== 发票相关Handler方法 ====================
|
||||
|
||||
// ApplyInvoice 申请开票
|
||||
// @Summary 申请开票
|
||||
// @Description 用户申请开票
|
||||
// @Tags 发票管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body finance.ApplyInvoiceRequest true "申请开票请求"
|
||||
// @Success 200 {object} response.Response{data=finance.InvoiceApplicationResponse}
|
||||
// @Failure 400 {object} response.Response
|
||||
// @Failure 500 {object} response.Response
|
||||
// @Router /api/v1/invoices/apply [post]
|
||||
func (h *FinanceHandler) ApplyInvoice(c *gin.Context) {
|
||||
var req finance.ApplyInvoiceRequest
|
||||
if err := h.validator.BindAndValidate(c, &req); err != nil {
|
||||
h.responseBuilder.BadRequest(c, "请求参数错误", err)
|
||||
return
|
||||
}
|
||||
|
||||
userID := c.GetString("user_id") // 从JWT中获取用户ID
|
||||
if userID == "" {
|
||||
h.responseBuilder.Unauthorized(c, "用户未登录")
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.invoiceAppService.ApplyInvoice(c.Request.Context(), userID, req)
|
||||
if err != nil {
|
||||
h.responseBuilder.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, result, "申请开票成功")
|
||||
}
|
||||
|
||||
// GetUserInvoiceInfo 获取用户发票信息
|
||||
// @Summary 获取用户发票信息
|
||||
// @Description 获取用户的发票信息
|
||||
// @Tags 发票管理
|
||||
// @Produce json
|
||||
// @Success 200 {object} response.Response{data=finance.InvoiceInfoResponse}
|
||||
// @Failure 400 {object} response.Response
|
||||
// @Failure 500 {object} response.Response
|
||||
// @Router /api/v1/invoices/info [get]
|
||||
func (h *FinanceHandler) GetUserInvoiceInfo(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
h.responseBuilder.Unauthorized(c, "用户未登录")
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.invoiceAppService.GetUserInvoiceInfo(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
h.responseBuilder.InternalError(c, "获取发票信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, result, "获取发票信息成功")
|
||||
}
|
||||
|
||||
// UpdateUserInvoiceInfo 更新用户发票信息
|
||||
// @Summary 更新用户发票信息
|
||||
// @Description 更新用户的发票信息
|
||||
// @Tags 发票管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body finance.UpdateInvoiceInfoRequest true "更新发票信息请求"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Failure 400 {object} response.Response
|
||||
// @Failure 500 {object} response.Response
|
||||
// @Router /api/v1/invoices/info [put]
|
||||
func (h *FinanceHandler) UpdateUserInvoiceInfo(c *gin.Context) {
|
||||
var req finance.UpdateInvoiceInfoRequest
|
||||
if err := h.validator.BindAndValidate(c, &req); err != nil {
|
||||
h.responseBuilder.BadRequest(c, "请求参数错误", err)
|
||||
return
|
||||
}
|
||||
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
h.responseBuilder.Unauthorized(c, "用户未登录")
|
||||
return
|
||||
}
|
||||
|
||||
err := h.invoiceAppService.UpdateUserInvoiceInfo(c.Request.Context(), userID, req)
|
||||
if err != nil {
|
||||
h.responseBuilder.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, nil, "更新发票信息成功")
|
||||
}
|
||||
|
||||
// GetUserInvoiceRecords 获取用户开票记录
|
||||
// @Summary 获取用户开票记录
|
||||
// @Description 获取用户的开票记录列表
|
||||
// @Tags 发票管理
|
||||
// @Produce json
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param page_size query int false "每页数量" default(10)
|
||||
// @Param status query string false "状态筛选"
|
||||
// @Success 200 {object} response.Response{data=finance.InvoiceRecordsResponse}
|
||||
// @Failure 400 {object} response.Response
|
||||
// @Failure 500 {object} response.Response
|
||||
// @Router /api/v1/invoices/records [get]
|
||||
func (h *FinanceHandler) GetUserInvoiceRecords(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
h.responseBuilder.Unauthorized(c, "用户未登录")
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
|
||||
status := c.Query("status")
|
||||
startTime := c.Query("start_time")
|
||||
endTime := c.Query("end_time")
|
||||
|
||||
req := finance.GetInvoiceRecordsRequest{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
Status: status,
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
}
|
||||
|
||||
result, err := h.invoiceAppService.GetUserInvoiceRecords(c.Request.Context(), userID, req)
|
||||
if err != nil {
|
||||
h.responseBuilder.InternalError(c, "获取开票记录失败")
|
||||
return
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, result, "获取开票记录成功")
|
||||
}
|
||||
|
||||
// DownloadInvoiceFile 下载发票文件
|
||||
// @Summary 下载发票文件
|
||||
// @Description 下载指定发票的文件
|
||||
// @Tags 发票管理
|
||||
// @Produce application/octet-stream
|
||||
// @Param application_id path string true "申请ID"
|
||||
// @Success 200 {file} file
|
||||
// @Failure 400 {object} response.Response
|
||||
// @Failure 500 {object} response.Response
|
||||
// @Router /api/v1/invoices/{application_id}/download [get]
|
||||
func (h *FinanceHandler) DownloadInvoiceFile(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
h.responseBuilder.Unauthorized(c, "用户未登录")
|
||||
return
|
||||
}
|
||||
|
||||
applicationID := c.Param("application_id")
|
||||
if applicationID == "" {
|
||||
h.responseBuilder.BadRequest(c, "申请ID不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.invoiceAppService.DownloadInvoiceFile(c.Request.Context(), userID, applicationID)
|
||||
if err != nil {
|
||||
h.responseBuilder.InternalError(c, "下载发票文件失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
c.Header("Content-Type", "application/pdf")
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", result.FileName))
|
||||
c.Header("Content-Length", fmt.Sprintf("%d", len(result.FileContent)))
|
||||
|
||||
// 直接返回文件内容
|
||||
c.Data(http.StatusOK, "application/pdf", result.FileContent)
|
||||
}
|
||||
|
||||
// GetAvailableAmount 获取可开票金额
|
||||
// @Summary 获取可开票金额
|
||||
// @Description 获取用户当前可开票的金额
|
||||
// @Tags 发票管理
|
||||
// @Produce json
|
||||
// @Success 200 {object} response.Response{data=finance.AvailableAmountResponse}
|
||||
// @Failure 400 {object} response.Response
|
||||
// @Failure 500 {object} response.Response
|
||||
// @Router /api/v1/invoices/available-amount [get]
|
||||
func (h *FinanceHandler) GetAvailableAmount(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
h.responseBuilder.Unauthorized(c, "用户未登录")
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.invoiceAppService.GetAvailableAmount(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
h.responseBuilder.InternalError(c, "获取可开票金额失败")
|
||||
return
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, result, "获取可开票金额成功")
|
||||
}
|
||||
|
||||
// ==================== 管理员发票相关Handler方法 ====================
|
||||
|
||||
// GetPendingApplications 获取发票申请列表(支持筛选)
|
||||
// @Summary 获取发票申请列表
|
||||
// @Description 管理员获取发票申请列表,支持状态和时间范围筛选
|
||||
// @Tags 管理员-发票管理
|
||||
// @Produce json
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param page_size query int false "每页数量" default(10)
|
||||
// @Param status query string false "状态筛选:pending/completed/rejected"
|
||||
// @Param start_time query string false "开始时间 (格式: 2006-01-02 15:04:05)"
|
||||
// @Param end_time query string false "结束时间 (格式: 2006-01-02 15:04:05)"
|
||||
// @Success 200 {object} response.Response{data=finance.PendingApplicationsResponse}
|
||||
// @Failure 400 {object} response.Response
|
||||
// @Failure 500 {object} response.Response
|
||||
// @Router /api/v1/admin/invoices/pending [get]
|
||||
func (h *FinanceHandler) GetPendingApplications(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
|
||||
status := c.Query("status")
|
||||
startTime := c.Query("start_time")
|
||||
endTime := c.Query("end_time")
|
||||
|
||||
req := finance.GetPendingApplicationsRequest{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
Status: status,
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
}
|
||||
|
||||
result, err := h.adminInvoiceAppService.GetPendingApplications(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
h.responseBuilder.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, result, "获取发票申请列表成功")
|
||||
}
|
||||
|
||||
// ApproveInvoiceApplication 通过发票申请(上传发票)
|
||||
// @Summary 通过发票申请
|
||||
// @Description 管理员通过发票申请并上传发票文件
|
||||
// @Tags 管理员-发票管理
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param application_id path string true "申请ID"
|
||||
// @Param file formData file true "发票文件"
|
||||
// @Param admin_notes formData string false "管理员备注"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Failure 400 {object} response.Response
|
||||
// @Failure 500 {object} response.Response
|
||||
// @Router /api/v1/admin/invoices/{application_id}/approve [post]
|
||||
func (h *FinanceHandler) ApproveInvoiceApplication(c *gin.Context) {
|
||||
applicationID := c.Param("application_id")
|
||||
if applicationID == "" {
|
||||
h.responseBuilder.BadRequest(c, "申请ID不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取上传的文件
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
h.responseBuilder.BadRequest(c, "请选择要上传的发票文件")
|
||||
return
|
||||
}
|
||||
|
||||
// 打开文件
|
||||
fileHandle, err := file.Open()
|
||||
if err != nil {
|
||||
h.responseBuilder.InternalError(c, "文件打开失败")
|
||||
return
|
||||
}
|
||||
defer fileHandle.Close()
|
||||
|
||||
// 获取管理员备注
|
||||
adminNotes := c.PostForm("admin_notes")
|
||||
|
||||
req := finance.ApproveInvoiceRequest{
|
||||
AdminNotes: adminNotes,
|
||||
}
|
||||
|
||||
err = h.adminInvoiceAppService.ApproveInvoiceApplication(c.Request.Context(), applicationID, fileHandle, req)
|
||||
if err != nil {
|
||||
h.responseBuilder.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
|
||||
h.responseBuilder.Success(c, nil, "通过发票申请成功")
|
||||
}
|
||||
|
||||
// RejectInvoiceApplication 拒绝发票申请
|
||||
// @Summary 拒绝发票申请
|
||||
// @Description 管理员拒绝发票申请
|
||||
// @Tags 管理员-发票管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param application_id path string true "申请ID"
|
||||
// @Param request body finance.RejectInvoiceRequest true "拒绝申请请求"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Failure 400 {object} response.Response
|
||||
// @Failure 500 {object} response.Response
|
||||
// @Router /api/v1/admin/invoices/{application_id}/reject [post]
|
||||
func (h *FinanceHandler) RejectInvoiceApplication(c *gin.Context) {
|
||||
applicationID := c.Param("application_id")
|
||||
if applicationID == "" {
|
||||
h.responseBuilder.BadRequest(c, "申请ID不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
var req finance.RejectInvoiceRequest
|
||||
if err := h.validator.BindAndValidate(c, &req); err != nil {
|
||||
h.responseBuilder.BadRequest(c, "请求参数错误", err)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.adminInvoiceAppService.RejectInvoiceApplication(c.Request.Context(), applicationID, req)
|
||||
if err != nil {
|
||||
h.responseBuilder.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, nil, "拒绝发票申请成功")
|
||||
}
|
||||
|
||||
// AdminDownloadInvoiceFile 管理员下载发票文件
|
||||
// @Summary 管理员下载发票文件
|
||||
// @Description 管理员下载指定发票的文件
|
||||
// @Tags 管理员-发票管理
|
||||
// @Produce application/octet-stream
|
||||
// @Param application_id path string true "申请ID"
|
||||
// @Success 200 {file} file
|
||||
// @Failure 400 {object} response.Response
|
||||
// @Failure 500 {object} response.Response
|
||||
// @Router /api/v1/admin/invoices/{application_id}/download [get]
|
||||
func (h *FinanceHandler) AdminDownloadInvoiceFile(c *gin.Context) {
|
||||
applicationID := c.Param("application_id")
|
||||
if applicationID == "" {
|
||||
h.responseBuilder.BadRequest(c, "申请ID不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.adminInvoiceAppService.DownloadInvoiceFile(c.Request.Context(), applicationID)
|
||||
if err != nil {
|
||||
h.responseBuilder.InternalError(c, "下载发票文件失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
c.Header("Content-Type", "application/pdf")
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", result.FileName))
|
||||
c.Header("Content-Length", fmt.Sprintf("%d", len(result.FileContent)))
|
||||
|
||||
// 直接返回文件内容
|
||||
c.Data(http.StatusOK, "application/pdf", result.FileContent)
|
||||
}
|
||||
|
||||
// DebugEventSystem 调试事件系统状态
|
||||
// @Summary 调试事件系统状态
|
||||
// @Description 获取事件系统的调试信息
|
||||
// @Tags 调试
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /api/v1/debug/events [get]
|
||||
func (h *FinanceHandler) DebugEventSystem(c *gin.Context) {
|
||||
h.logger.Info("🔍 请求事件系统调试信息")
|
||||
|
||||
// 这里可以添加事件系统的状态信息
|
||||
// 暂时返回基本信息
|
||||
debugInfo := map[string]interface{}{
|
||||
"timestamp": time.Now().Format("2006-01-02 15:04:05"),
|
||||
"message": "事件系统调试端点已启用",
|
||||
"handler": "FinanceHandler",
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, debugInfo, "事件系统调试信息")
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ package handlers
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
"tyapi-server/internal/application/api"
|
||||
"tyapi-server/internal/application/finance"
|
||||
"tyapi-server/internal/application/product"
|
||||
"tyapi-server/internal/application/product/dto/commands"
|
||||
"tyapi-server/internal/application/product/dto/queries"
|
||||
@@ -18,6 +21,8 @@ type ProductAdminHandler struct {
|
||||
categoryAppService product.CategoryApplicationService
|
||||
subscriptionAppService product.SubscriptionApplicationService
|
||||
documentationAppService product.DocumentationApplicationServiceInterface
|
||||
apiAppService api.ApiApplicationService
|
||||
financeAppService finance.FinanceApplicationService
|
||||
responseBuilder interfaces.ResponseBuilder
|
||||
validator interfaces.RequestValidator
|
||||
logger *zap.Logger
|
||||
@@ -29,6 +34,8 @@ func NewProductAdminHandler(
|
||||
categoryAppService product.CategoryApplicationService,
|
||||
subscriptionAppService product.SubscriptionApplicationService,
|
||||
documentationAppService product.DocumentationApplicationServiceInterface,
|
||||
apiAppService api.ApiApplicationService,
|
||||
financeAppService finance.FinanceApplicationService,
|
||||
responseBuilder interfaces.ResponseBuilder,
|
||||
validator interfaces.RequestValidator,
|
||||
logger *zap.Logger,
|
||||
@@ -38,6 +45,8 @@ func NewProductAdminHandler(
|
||||
categoryAppService: categoryAppService,
|
||||
subscriptionAppService: subscriptionAppService,
|
||||
documentationAppService: documentationAppService,
|
||||
apiAppService: apiAppService,
|
||||
financeAppService: financeAppService,
|
||||
responseBuilder: responseBuilder,
|
||||
validator: validator,
|
||||
logger: logger,
|
||||
@@ -710,7 +719,13 @@ func (h *ProductAdminHandler) GetCategoryDetail(c *gin.Context) {
|
||||
// @Security Bearer
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param page_size query int false "每页数量" default(10)
|
||||
// @Param status query string false "订阅状态"
|
||||
// @Param keyword query string false "搜索关键词"
|
||||
// @Param company_name query string false "企业名称"
|
||||
// @Param product_name query string false "产品名称"
|
||||
// @Param start_time query string false "订阅开始时间" format(date-time)
|
||||
// @Param end_time query string false "订阅结束时间" format(date-time)
|
||||
// @Param sort_by query string false "排序字段"
|
||||
// @Param sort_order query string false "排序方向" Enums(asc, desc)
|
||||
// @Success 200 {object} responses.SubscriptionListResponse "获取订阅列表成功"
|
||||
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||
@@ -719,7 +734,7 @@ func (h *ProductAdminHandler) GetCategoryDetail(c *gin.Context) {
|
||||
func (h *ProductAdminHandler) ListSubscriptions(c *gin.Context) {
|
||||
var query queries.ListSubscriptionsQuery
|
||||
if err := c.ShouldBindQuery(&query); err != nil {
|
||||
h.responseBuilder.BadRequest(c, "请求参数错误")
|
||||
h.responseBuilder.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -734,6 +749,14 @@ func (h *ProductAdminHandler) ListSubscriptions(c *gin.Context) {
|
||||
query.PageSize = 100
|
||||
}
|
||||
|
||||
// 设置默认排序
|
||||
if query.SortBy == "" {
|
||||
query.SortBy = "created_at"
|
||||
}
|
||||
if query.SortOrder == "" {
|
||||
query.SortOrder = "desc"
|
||||
}
|
||||
|
||||
result, err := h.subscriptionAppService.ListSubscriptions(c.Request.Context(), &query)
|
||||
if err != nil {
|
||||
h.logger.Error("获取订阅列表失败", zap.Error(err))
|
||||
@@ -1053,3 +1076,251 @@ func (h *ProductAdminHandler) DeleteProductDocumentation(c *gin.Context) {
|
||||
|
||||
h.responseBuilder.Success(c, nil, "文档删除成功")
|
||||
}
|
||||
|
||||
// GetAdminApiCalls 获取管理端API调用记录
|
||||
// @Summary 获取管理端API调用记录
|
||||
// @Description 管理员获取API调用记录,支持筛选和分页
|
||||
// @Tags API管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param page_size query int false "每页数量" default(10)
|
||||
// @Param user_id query string false "用户ID"
|
||||
// @Param transaction_id query string false "交易ID"
|
||||
// @Param product_name query string false "产品名称"
|
||||
// @Param status query string false "状态"
|
||||
// @Param start_time query string false "开始时间" format(date-time)
|
||||
// @Param end_time query string false "结束时间" format(date-time)
|
||||
// @Param sort_by query string false "排序字段"
|
||||
// @Param sort_order query string false "排序方向" Enums(asc, desc)
|
||||
// @Success 200 {object} dto.ApiCallListResponse "获取API调用记录成功"
|
||||
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /api/v1/admin/api-calls [get]
|
||||
func (h *ProductAdminHandler) GetAdminApiCalls(c *gin.Context) {
|
||||
// 解析查询参数
|
||||
page := h.getIntQuery(c, "page", 1)
|
||||
pageSize := h.getIntQuery(c, "page_size", 10)
|
||||
|
||||
// 构建筛选条件
|
||||
filters := make(map[string]interface{})
|
||||
|
||||
// 用户ID筛选
|
||||
if userId := c.Query("user_id"); userId != "" {
|
||||
filters["user_id"] = userId
|
||||
}
|
||||
|
||||
// 时间范围筛选
|
||||
if startTime := c.Query("start_time"); startTime != "" {
|
||||
if t, err := time.Parse("2006-01-02 15:04:05", startTime); err == nil {
|
||||
filters["start_time"] = t
|
||||
}
|
||||
}
|
||||
if endTime := c.Query("end_time"); endTime != "" {
|
||||
if t, err := time.Parse("2006-01-02 15:04:05", endTime); err == nil {
|
||||
filters["end_time"] = t
|
||||
}
|
||||
}
|
||||
|
||||
// 交易ID筛选
|
||||
if transactionId := c.Query("transaction_id"); transactionId != "" {
|
||||
filters["transaction_id"] = transactionId
|
||||
}
|
||||
|
||||
// 产品名称筛选
|
||||
if productName := c.Query("product_name"); productName != "" {
|
||||
filters["product_name"] = productName
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if status := c.Query("status"); status != "" {
|
||||
filters["status"] = status
|
||||
}
|
||||
|
||||
// 构建分页选项
|
||||
options := interfaces.ListOptions{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
Sort: "created_at",
|
||||
Order: "desc",
|
||||
}
|
||||
|
||||
result, err := h.apiAppService.GetAdminApiCalls(c.Request.Context(), filters, options)
|
||||
if err != nil {
|
||||
h.logger.Error("获取管理端API调用记录失败", zap.Error(err))
|
||||
h.responseBuilder.BadRequest(c, "获取API调用记录失败")
|
||||
return
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, result, "获取API调用记录成功")
|
||||
}
|
||||
|
||||
// GetAdminWalletTransactions 获取管理端消费记录
|
||||
// @Summary 获取管理端消费记录
|
||||
// @Description 管理员获取消费记录,支持筛选和分页
|
||||
// @Tags 财务管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param page_size query int false "每页数量" default(10)
|
||||
// @Param user_id query string false "用户ID"
|
||||
// @Param transaction_id query string false "交易ID"
|
||||
// @Param product_name query string false "产品名称"
|
||||
// @Param min_amount query string false "最小金额"
|
||||
// @Param max_amount query string false "最大金额"
|
||||
// @Param start_time query string false "开始时间" format(date-time)
|
||||
// @Param end_time query string false "结束时间" format(date-time)
|
||||
// @Param sort_by query string false "排序字段"
|
||||
// @Param sort_order query string false "排序方向" Enums(asc, desc)
|
||||
// @Success 200 {object} dto.WalletTransactionListResponse "获取消费记录成功"
|
||||
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /api/v1/admin/wallet-transactions [get]
|
||||
func (h *ProductAdminHandler) GetAdminWalletTransactions(c *gin.Context) {
|
||||
// 解析查询参数
|
||||
page := h.getIntQuery(c, "page", 1)
|
||||
pageSize := h.getIntQuery(c, "page_size", 10)
|
||||
|
||||
// 构建筛选条件
|
||||
filters := make(map[string]interface{})
|
||||
|
||||
// 用户ID筛选
|
||||
if userId := c.Query("user_id"); userId != "" {
|
||||
filters["user_id"] = userId
|
||||
}
|
||||
|
||||
// 时间范围筛选
|
||||
if startTime := c.Query("start_time"); startTime != "" {
|
||||
if t, err := time.Parse("2006-01-02 15:04:05", startTime); err == nil {
|
||||
filters["start_time"] = t
|
||||
}
|
||||
}
|
||||
if endTime := c.Query("end_time"); endTime != "" {
|
||||
if t, err := time.Parse("2006-01-02 15:04:05", endTime); err == nil {
|
||||
filters["end_time"] = t
|
||||
}
|
||||
}
|
||||
|
||||
// 交易ID筛选
|
||||
if transactionId := c.Query("transaction_id"); transactionId != "" {
|
||||
filters["transaction_id"] = transactionId
|
||||
}
|
||||
|
||||
// 产品名称筛选
|
||||
if productName := c.Query("product_name"); productName != "" {
|
||||
filters["product_name"] = productName
|
||||
}
|
||||
|
||||
// 金额范围筛选
|
||||
if minAmount := c.Query("min_amount"); minAmount != "" {
|
||||
filters["min_amount"] = minAmount
|
||||
}
|
||||
if maxAmount := c.Query("max_amount"); maxAmount != "" {
|
||||
filters["max_amount"] = maxAmount
|
||||
}
|
||||
|
||||
// 构建分页选项
|
||||
options := interfaces.ListOptions{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
Sort: "created_at",
|
||||
Order: "desc",
|
||||
}
|
||||
|
||||
result, err := h.financeAppService.GetAdminWalletTransactions(c.Request.Context(), filters, options)
|
||||
if err != nil {
|
||||
h.logger.Error("获取管理端消费记录失败", zap.Error(err))
|
||||
h.responseBuilder.BadRequest(c, "获取消费记录失败")
|
||||
return
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, result, "获取消费记录成功")
|
||||
}
|
||||
|
||||
// GetAdminRechargeRecords 获取管理端充值记录
|
||||
// @Summary 获取管理端充值记录
|
||||
// @Description 管理员获取充值记录,支持筛选和分页
|
||||
// @Tags 财务管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param page_size query int false "每页数量" default(10)
|
||||
// @Param user_id query string false "用户ID"
|
||||
// @Param recharge_type query string false "充值类型" Enums(alipay, transfer, gift)
|
||||
// @Param status query string false "状态" Enums(pending, success, failed)
|
||||
// @Param min_amount query string false "最小金额"
|
||||
// @Param max_amount query string false "最大金额"
|
||||
// @Param start_time query string false "开始时间" format(date-time)
|
||||
// @Param end_time query string false "结束时间" format(date-time)
|
||||
// @Param sort_by query string false "排序字段"
|
||||
// @Param sort_order query string false "排序方向" Enums(asc, desc)
|
||||
// @Success 200 {object} dto.RechargeRecordListResponse "获取充值记录成功"
|
||||
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /api/v1/admin/recharge-records [get]
|
||||
func (h *ProductAdminHandler) GetAdminRechargeRecords(c *gin.Context) {
|
||||
// 解析查询参数
|
||||
page := h.getIntQuery(c, "page", 1)
|
||||
pageSize := h.getIntQuery(c, "page_size", 10)
|
||||
|
||||
// 构建筛选条件
|
||||
filters := make(map[string]interface{})
|
||||
|
||||
// 用户ID筛选
|
||||
if userId := c.Query("user_id"); userId != "" {
|
||||
filters["user_id"] = userId
|
||||
}
|
||||
|
||||
// 时间范围筛选
|
||||
if startTime := c.Query("start_time"); startTime != "" {
|
||||
if t, err := time.Parse("2006-01-02 15:04:05", startTime); err == nil {
|
||||
filters["start_time"] = t
|
||||
}
|
||||
}
|
||||
if endTime := c.Query("end_time"); endTime != "" {
|
||||
if t, err := time.Parse("2006-01-02 15:04:05", endTime); err == nil {
|
||||
filters["end_time"] = t
|
||||
}
|
||||
}
|
||||
|
||||
// 充值类型筛选
|
||||
if rechargeType := c.Query("recharge_type"); rechargeType != "" {
|
||||
filters["recharge_type"] = rechargeType
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if status := c.Query("status"); status != "" {
|
||||
filters["status"] = status
|
||||
}
|
||||
|
||||
// 金额范围筛选
|
||||
if minAmount := c.Query("min_amount"); minAmount != "" {
|
||||
filters["min_amount"] = minAmount
|
||||
}
|
||||
if maxAmount := c.Query("max_amount"); maxAmount != "" {
|
||||
filters["max_amount"] = maxAmount
|
||||
}
|
||||
|
||||
// 构建分页选项
|
||||
options := interfaces.ListOptions{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
Sort: "created_at",
|
||||
Order: "desc",
|
||||
}
|
||||
|
||||
result, err := h.financeAppService.GetAdminRechargeRecords(c.Request.Context(), filters, options)
|
||||
if err != nil {
|
||||
h.logger.Error("获取管理端充值记录失败", zap.Error(err))
|
||||
h.responseBuilder.BadRequest(c, "获取充值记录失败")
|
||||
return
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, result, "获取充值记录成功")
|
||||
}
|
||||
|
||||
@@ -415,7 +415,10 @@ func (h *ProductHandler) GetCategoryDetail(c *gin.Context) {
|
||||
// @Security Bearer
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param page_size query int false "每页数量" default(10)
|
||||
// @Param status query string false "订阅状态"
|
||||
// @Param keyword query string false "搜索关键词"
|
||||
// @Param product_name query string false "产品名称"
|
||||
// @Param start_time query string false "订阅开始时间" format(date-time)
|
||||
// @Param end_time query string false "订阅结束时间" format(date-time)
|
||||
// @Param sort_by query string false "排序字段"
|
||||
// @Param sort_order query string false "排序方向" Enums(asc, desc)
|
||||
// @Success 200 {object} responses.SubscriptionListResponse "获取订阅列表成功"
|
||||
@@ -432,7 +435,7 @@ func (h *ProductHandler) ListMySubscriptions(c *gin.Context) {
|
||||
|
||||
var query queries.ListSubscriptionsQuery
|
||||
if err := h.validator.ValidateQuery(c, &query); err != nil {
|
||||
return
|
||||
return
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
@@ -446,6 +449,17 @@ func (h *ProductHandler) ListMySubscriptions(c *gin.Context) {
|
||||
query.PageSize = 100
|
||||
}
|
||||
|
||||
// 设置默认排序
|
||||
if query.SortBy == "" {
|
||||
query.SortBy = "created_at"
|
||||
}
|
||||
if query.SortOrder == "" {
|
||||
query.SortOrder = "desc"
|
||||
}
|
||||
|
||||
// 用户端不支持企业名称筛选,清空该字段
|
||||
query.CompanyName = ""
|
||||
|
||||
result, err := h.subAppService.ListMySubscriptions(c.Request.Context(), userID, &query)
|
||||
if err != nil {
|
||||
h.logger.Error("获取我的订阅列表失败", zap.Error(err), zap.String("user_id", userID))
|
||||
@@ -521,6 +535,13 @@ func (h *ProductHandler) GetMySubscriptionDetail(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 验证订阅是否属于当前用户
|
||||
if result.UserID != userID {
|
||||
h.logger.Error("用户尝试访问不属于自己的订阅", zap.String("user_id", userID), zap.String("subscription_user_id", result.UserID), zap.String("subscription_id", subscriptionID))
|
||||
h.responseBuilder.Forbidden(c, "无权访问此订阅")
|
||||
return
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, result, "获取我的订阅详情成功")
|
||||
}
|
||||
|
||||
@@ -539,16 +560,33 @@ func (h *ProductHandler) GetMySubscriptionDetail(c *gin.Context) {
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /api/v1/my/subscriptions/{id}/usage [get]
|
||||
func (h *ProductHandler) GetMySubscriptionUsage(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
h.responseBuilder.Unauthorized(c, "用户未登录")
|
||||
return
|
||||
}
|
||||
|
||||
subscriptionID := c.Param("id")
|
||||
if subscriptionID == "" {
|
||||
h.responseBuilder.BadRequest(c, "订阅ID不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前用户ID
|
||||
userID := h.getCurrentUserID(c)
|
||||
if userID == "" {
|
||||
h.responseBuilder.Unauthorized(c, "用户未认证")
|
||||
// 先获取订阅信息以验证权限
|
||||
var query queries.GetSubscriptionQuery
|
||||
query.ID = subscriptionID
|
||||
|
||||
subscription, err := h.subAppService.GetSubscriptionByID(c.Request.Context(), &query)
|
||||
if err != nil {
|
||||
h.logger.Error("获取订阅信息失败", zap.Error(err), zap.String("user_id", userID), zap.String("subscription_id", subscriptionID))
|
||||
h.responseBuilder.NotFound(c, "订阅不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证订阅是否属于当前用户
|
||||
if subscription.UserID != userID {
|
||||
h.logger.Error("用户尝试访问不属于自己的订阅使用情况", zap.String("user_id", userID), zap.String("subscription_user_id", subscription.UserID), zap.String("subscription_id", subscriptionID))
|
||||
h.responseBuilder.Forbidden(c, "无权访问此订阅")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -322,6 +322,46 @@ func (h *UserHandler) ListUsers(c *gin.Context) {
|
||||
h.response.Success(c, resp, "获取用户列表成功")
|
||||
}
|
||||
|
||||
// GetUserDetail 管理员获取用户详情
|
||||
// @Summary 管理员获取用户详情
|
||||
// @Description 管理员获取指定用户的详细信息
|
||||
// @Tags 用户管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param user_id path string true "用户ID"
|
||||
// @Success 200 {object} responses.UserDetailResponse "用户详情"
|
||||
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||
// @Failure 403 {object} map[string]interface{} "权限不足"
|
||||
// @Failure 404 {object} map[string]interface{} "用户不存在"
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /api/v1/users/admin/{user_id} [get]
|
||||
func (h *UserHandler) GetUserDetail(c *gin.Context) {
|
||||
// 检查管理员权限
|
||||
userID := h.getCurrentUserID(c)
|
||||
if userID == "" {
|
||||
h.response.Unauthorized(c, "用户未登录")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取路径参数中的用户ID
|
||||
targetUserID := c.Param("user_id")
|
||||
if targetUserID == "" {
|
||||
h.response.BadRequest(c, "用户ID不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
// 调用应用服务
|
||||
resp, err := h.appService.GetUserDetail(c.Request.Context(), targetUserID)
|
||||
if err != nil {
|
||||
h.logger.Error("获取用户详情失败", zap.Error(err), zap.String("target_user_id", targetUserID))
|
||||
h.response.BadRequest(c, "获取用户详情失败")
|
||||
return
|
||||
}
|
||||
|
||||
h.response.Success(c, resp, "获取用户详情成功")
|
||||
}
|
||||
|
||||
// GetUserStats 管理员获取用户统计信息
|
||||
// @Summary 管理员获取用户统计信息
|
||||
// @Description 管理员获取用户统计信息,包括总用户数、活跃用户数、已认证用户数
|
||||
|
||||
@@ -58,6 +58,18 @@ func (r *FinanceRoutes) Register(router *sharedhttp.GinRouter) {
|
||||
}
|
||||
}
|
||||
|
||||
// 发票相关路由,需要用户认证
|
||||
invoiceGroup := engine.Group("/api/v1/invoices")
|
||||
invoiceGroup.Use(r.authMiddleware.Handle())
|
||||
{
|
||||
invoiceGroup.POST("/apply", r.financeHandler.ApplyInvoice) // 申请开票
|
||||
invoiceGroup.GET("/info", r.financeHandler.GetUserInvoiceInfo) // 获取用户发票信息
|
||||
invoiceGroup.PUT("/info", r.financeHandler.UpdateUserInvoiceInfo) // 更新用户发票信息
|
||||
invoiceGroup.GET("/records", r.financeHandler.GetUserInvoiceRecords) // 获取用户开票记录
|
||||
invoiceGroup.GET("/available-amount", r.financeHandler.GetAvailableAmount) // 获取可开票金额
|
||||
invoiceGroup.GET("/:application_id/download", r.financeHandler.DownloadInvoiceFile) // 下载发票文件
|
||||
}
|
||||
|
||||
// 管理员财务路由组
|
||||
adminFinanceGroup := engine.Group("/api/v1/admin/finance")
|
||||
adminFinanceGroup.Use(r.adminAuthMiddleware.Handle())
|
||||
@@ -67,5 +79,15 @@ func (r *FinanceRoutes) Register(router *sharedhttp.GinRouter) {
|
||||
adminFinanceGroup.GET("/recharge-records", r.financeHandler.GetAdminRechargeRecords) // 管理员充值记录分页
|
||||
}
|
||||
|
||||
// 管理员发票相关路由组
|
||||
adminInvoiceGroup := engine.Group("/api/v1/admin/invoices")
|
||||
adminInvoiceGroup.Use(r.adminAuthMiddleware.Handle())
|
||||
{
|
||||
adminInvoiceGroup.GET("/pending", r.financeHandler.GetPendingApplications) // 获取待处理申请列表
|
||||
adminInvoiceGroup.POST("/:application_id/approve", r.financeHandler.ApproveInvoiceApplication) // 通过发票申请
|
||||
adminInvoiceGroup.POST("/:application_id/reject", r.financeHandler.RejectInvoiceApplication) // 拒绝发票申请
|
||||
adminInvoiceGroup.GET("/:application_id/download", r.financeHandler.AdminDownloadInvoiceFile) // 下载发票文件
|
||||
}
|
||||
|
||||
r.logger.Info("财务路由注册完成")
|
||||
}
|
||||
|
||||
@@ -79,5 +79,23 @@ func (r *ProductAdminRoutes) Register(router *sharedhttp.GinRouter) {
|
||||
subscriptions.GET("/stats", r.handler.GetSubscriptionStats)
|
||||
subscriptions.PUT("/:id/price", r.handler.UpdateSubscriptionPrice)
|
||||
}
|
||||
|
||||
// API调用记录管理
|
||||
apiCalls := adminGroup.Group("/api-calls")
|
||||
{
|
||||
apiCalls.GET("", r.handler.GetAdminApiCalls)
|
||||
}
|
||||
|
||||
// 消费记录管理
|
||||
walletTransactions := adminGroup.Group("/wallet-transactions")
|
||||
{
|
||||
walletTransactions.GET("", r.handler.GetAdminWalletTransactions)
|
||||
}
|
||||
|
||||
// 充值记录管理
|
||||
rechargeRecords := adminGroup.Group("/recharge-records")
|
||||
{
|
||||
rechargeRecords.GET("", r.handler.GetAdminRechargeRecords)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ func (r *UserRoutes) Register(router *sharedhttp.GinRouter) {
|
||||
adminGroup.Use(r.adminAuthMiddleware.Handle())
|
||||
{
|
||||
adminGroup.GET("/list", r.handler.ListUsers) // 管理员查看用户列表
|
||||
adminGroup.GET("/:user_id", r.handler.GetUserDetail) // 管理员获取用户详情
|
||||
adminGroup.GET("/stats", r.handler.GetUserStats) // 管理员获取用户统计信息
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user