Files
2026-01-16 15:49:34 +08:00

422 lines
15 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package admin
import (
"encoding/json"
"fmt"
"time"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/queue"
"github.com/goravel/framework/facades"
"github.com/spf13/cast"
apperrors "goravel/app/errors"
"goravel/app/http/helpers"
"goravel/app/http/response"
"goravel/app/http/trans"
"goravel/app/jobs"
"goravel/app/models"
"goravel/app/services"
"goravel/app/utils"
)
type PaymentController struct {
paymentService services.PaymentService
}
func NewPaymentController() *PaymentController {
return &PaymentController{
paymentService: services.NewPaymentService(),
}
}
// buildFilters 构建筛选条件(列表和导出共用)
func (r *PaymentController) buildFilters(ctx http.Context) (services.PaymentFilters, http.Response) {
paymentNo := ctx.Request().Input("payment_no", ctx.Request().Query("payment_no", ""))
orderNo := ctx.Request().Input("order_no", ctx.Request().Query("order_no", ""))
paymentMethodID := cast.ToUint(ctx.Request().Input("payment_method_id", ctx.Request().Query("payment_method_id", "0")))
userID := cast.ToUint(ctx.Request().Input("user_id", ctx.Request().Query("user_id", "0")))
status := ctx.Request().Input("status", ctx.Request().Query("status", ""))
orderBy := ctx.Request().Input("order_by", ctx.Request().Query("order_by", ""))
// 解析时间参数
startTimeStr := ctx.Request().Query("start_time", "")
if startTimeStr == "" {
startTimeStr = ctx.Request().Input("start_time", "")
}
endTimeStr := ctx.Request().Query("end_time", "")
if endTimeStr == "" {
endTimeStr = ctx.Request().Input("end_time", "")
}
var startTime, endTime time.Time
var err error
if startTimeStr != "" {
utcTimeStr := helpers.ConvertTimeToUTC(ctx, startTimeStr)
if utcTimeStr == "" {
return services.PaymentFilters{}, response.Error(ctx, http.StatusBadRequest, "invalid_start_time")
}
startTime, err = utils.ParseDateTime(utcTimeStr)
if err != nil {
return services.PaymentFilters{}, response.Error(ctx, http.StatusBadRequest, "invalid_start_time")
}
}
if endTimeStr != "" {
utcTimeStr := helpers.ConvertTimeToUTC(ctx, endTimeStr)
if utcTimeStr == "" {
return services.PaymentFilters{}, response.Error(ctx, http.StatusBadRequest, "invalid_end_time")
}
endTime, err = utils.ParseDateTime(utcTimeStr)
if err != nil {
return services.PaymentFilters{}, response.Error(ctx, http.StatusBadRequest, "invalid_end_time")
}
}
// 与列表保持一致:未传 start_time 时默认最近 7 天;未传 end_time 时默认当前时间
// 这样导出数据集与列表查询数据集一致,并避免扫到未建表的历史月份
if startTime.IsZero() {
startTime = time.Now().UTC().AddDate(0, 0, -7)
}
if endTime.IsZero() {
endTime = time.Now().UTC()
}
// 校验时间范围不超过 3 个月(与列表/导出一致)
if valid, err := utils.ValidateTimeRange(startTime, endTime); !valid {
// ValidateTimeRange 返回的是可翻译错误键,这里直接返回 key 交给前端处理
return services.PaymentFilters{}, response.Error(ctx, http.StatusBadRequest, err.Error())
}
return services.PaymentFilters{
PaymentNo: paymentNo,
OrderNo: orderNo,
PaymentMethodID: paymentMethodID,
UserID: userID,
Status: status,
StartTime: startTime,
EndTime: endTime,
OrderBy: orderBy,
}, nil
}
// Index 支付记录列表
// @Summary 获取支付记录列表
// @Description 分页获取支付记录列表,支持多条件筛选
// @Tags 支付管理
// @Accept json
// @Produce json
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(10)
// @Param payment_no query string false "支付单号(模糊搜索)"
// @Param order_no query string false "订单号(模糊搜索)"
// @Param payment_method_id query int false "支付方式ID"
// @Param user_id query int false "用户ID"
// @Param status query string false "支付状态(pending/paid/failed/cancelled"
// @Param start_time query string false "开始时间(格式:2006-01-02 15:04:05"
// @Param end_time query string false "结束时间(格式:2006-01-02 15:04:05"
// @Param order_by query string false "排序(格式:字段:asc/desc,如:created_at:desc"
// @Success 200 {object} map[string]any
// @Failure 400 {object} map[string]any "参数错误"
// @Failure 500 {object} map[string]any "服务器错误"
// @Router /api/admin/payments [get]
// @Security BearerAuth
func (r *PaymentController) Index(ctx http.Context) http.Response {
page := helpers.GetIntQuery(ctx, "page", 1)
pageSize := helpers.GetIntQuery(ctx, "page_size", 10)
filters, resp := r.buildFilters(ctx)
if resp != nil {
return resp
}
payments, total, err := r.paymentService.GetPayments(filters, page, pageSize)
if err != nil {
return response.ErrorWithLog(ctx, "payment", err, map[string]any{
"filters": filters,
})
}
// 转换响应数据
paymentList := make([]http.Json, len(payments))
for i, payment := range payments {
paymentJson := http.Json{
"id": payment.ID,
"payment_no": payment.PaymentNo,
"order_no": payment.OrderNo,
"payment_method_id": payment.PaymentMethodID,
"user_id": payment.UserID,
"amount": payment.Amount,
"status": payment.Status,
"third_party_no": payment.ThirdPartyNo,
"pay_time": r.formatPayTime(payment.PayTime),
"fail_reason": payment.FailReason,
"remark": payment.Remark,
"created_at": payment.CreatedAt,
"updated_at": payment.UpdatedAt,
}
// 添加支付方式信息
if payment.PaymentMethod.ID > 0 {
paymentJson["payment_method"] = http.Json{
"id": payment.PaymentMethod.ID,
"name": payment.PaymentMethod.Name,
"code": payment.PaymentMethod.Code,
"type": payment.PaymentMethod.Type,
}
}
paymentList[i] = paymentJson
}
return response.Success(ctx, http.Json{
"data": paymentList,
"total": total,
"page": page,
"page_size": pageSize,
})
}
// Show 支付记录详情
// @Summary 获取支付记录详情
// @Description 根据支付单号获取支付记录详细信息(分表后ID可能重复,使用支付单号查询)
// @Tags 支付管理
// @Accept json
// @Produce json
// @Param id path string true "支付单号"
// @Success 200 {object} map[string]any
// @Failure 400 {object} map[string]any "参数错误"
// @Failure 404 {object} map[string]any "支付记录不存在"
// @Failure 500 {object} map[string]any "服务器错误"
// @Router /api/admin/payments/{id} [get]
// @Security BearerAuth
func (r *PaymentController) Show(ctx http.Context) http.Response {
paymentNo := ctx.Request().Route("id") // 路由参数名保持兼容
if paymentNo == "" {
return response.Error(ctx, http.StatusBadRequest, "payment_no_required")
}
payment, err := r.paymentService.GetPaymentByPaymentNo(paymentNo)
if err != nil {
return response.Error(ctx, http.StatusNotFound, apperrors.ErrPaymentNotFound.Code)
}
paymentJson := http.Json{
"id": payment.ID,
"payment_no": payment.PaymentNo,
"order_no": payment.OrderNo,
"payment_method_id": payment.PaymentMethodID,
"user_id": payment.UserID,
"amount": payment.Amount,
"status": payment.Status,
"third_party_no": payment.ThirdPartyNo,
"pay_time": r.formatPayTime(payment.PayTime),
"fail_reason": payment.FailReason,
"remark": payment.Remark,
"created_at": payment.CreatedAt,
"updated_at": payment.UpdatedAt,
}
// 添加支付方式信息
if payment.PaymentMethod.ID > 0 {
paymentJson["payment_method"] = http.Json{
"id": payment.PaymentMethod.ID,
"name": payment.PaymentMethod.Name,
"code": payment.PaymentMethod.Code,
"type": payment.PaymentMethod.Type,
}
}
return response.Success(ctx, paymentJson)
}
// formatPayTime 格式化支付时间为字符串
func (r *PaymentController) formatPayTime(t *time.Time) string {
return utils.FormatDateTimePtr(t)
}
// Export 导出支付记录
// @Summary 导出支付记录
// @Description 异步导出支付记录为CSV文件
// @Tags 支付管理
// @Accept json
// @Produce json
// @Param payment_no query string false "支付单号"
// @Param order_no query string false "订单号"
// @Param payment_method_id query int false "支付方式ID"
// @Param user_id query int false "用户ID"
// @Param status query string false "支付状态"
// @Param start_time query string false "开始时间"
// @Param end_time query string false "结束时间"
// @Success 200 {object} map[string]any
// @Failure 400 {object} map[string]any "参数错误"
// @Failure 429 {object} map[string]any "导出任务正在进行中"
// @Failure 500 {object} map[string]any "服务器错误"
// @Router /api/admin/payments/export [post]
// @Security BearerAuth
func (r *PaymentController) Export(ctx http.Context) http.Response {
adminID, err := helpers.GetAdminIDFromContext(ctx)
if err != nil {
return response.Error(ctx, http.StatusUnauthorized, "unauthorized")
}
// 防重复点击
lockKey := fmt.Sprintf("export:payments:lock:%d", adminID)
lock := facades.Cache().Lock(lockKey, 10*time.Second)
if !lock.Get() {
return response.Error(ctx, http.StatusTooManyRequests, "export_in_progress")
}
// 构建筛选条件
filters, resp := r.buildFilters(ctx)
if resp != nil {
return resp
}
// 获取存储驱动配置
disk := utils.GetConfigValue("storage", "file_disk", "")
if disk == "" {
disk = utils.GetConfigValue("storage", "export_disk", "")
}
if disk == "" {
disk = "local"
}
exportRecord := models.Export{
AdminID: adminID,
Type: models.ExportTypePayments,
Status: models.ExportStatusProcessing,
Disk: disk,
Path: "",
}
if err := facades.Orm().Query().Create(&exportRecord); err != nil {
return response.ErrorWithLog(ctx, "export", err)
}
// 序列化筛选条件
filtersMap := map[string]any{
"payment_no": filters.PaymentNo,
"order_no": filters.OrderNo,
"payment_method_id": filters.PaymentMethodID,
"user_id": filters.UserID,
"status": filters.Status,
"order_by": filters.OrderBy,
}
if !filters.StartTime.IsZero() {
filtersMap["start_time"] = utils.FormatDateTime(filters.StartTime)
}
if !filters.EndTime.IsZero() {
filtersMap["end_time"] = utils.FormatDateTime(filters.EndTime)
}
lang := r.getCurrentLanguage(ctx)
timezone := helpers.GetCurrentTimezone(ctx)
exportArgsStruct := jobs.ExportPaymentsArgs{
ExportID: exportRecord.ID,
AdminID: adminID,
Filters: filtersMap,
Type: "payments",
Language: lang,
Timezone: timezone,
}
exportArgsJSON, err := json.Marshal(exportArgsStruct)
if err != nil {
facades.Log().Errorf("序列化导出参数失败: export_id=%d, error=%v", exportRecord.ID, err)
exportRecord.Status = models.ExportStatusFailed
exportRecord.ErrorMsg = err.Error()
facades.Orm().Query().Save(&exportRecord)
return response.ErrorWithLog(ctx, "export", err)
}
facades.Log().Infof("提交支付记录导出任务到队列: export_id=%d", exportRecord.ID)
exportArgs := []queue.Arg{
{
Type: "string",
Value: string(exportArgsJSON),
},
}
if err := facades.Queue().Job(&jobs.ExportPayments{}, exportArgs).OnQueue("long-running").Dispatch(); err != nil {
lock.Release()
facades.Log().Errorf("提交导出任务失败: export_id=%d, error=%v", exportRecord.ID, err)
exportRecord.Status = models.ExportStatusFailed
exportRecord.ErrorMsg = err.Error()
facades.Orm().Query().Save(&exportRecord)
return response.ErrorWithLog(ctx, "export", err)
}
facades.Log().Infof("支付记录导出任务已成功提交到队列: export_id=%d", exportRecord.ID)
return response.Success(ctx, http.Json{
"export_id": exportRecord.ID,
"message": trans.Get(ctx, "export_task_submitted"),
})
}
// GetExportStatus 查询导出状态
// @Summary 查询支付记录导出状态
// @Description 根据导出记录ID查询导出任务的状态
// @Tags 支付管理
// @Accept json
// @Produce json
// @Param id path int true "导出记录ID"
// @Success 200 {object} map[string]any
// @Failure 400 {object} map[string]any "参数错误"
// @Failure 404 {object} map[string]any "导出记录不存在"
// @Failure 500 {object} map[string]any "服务器错误"
// @Router /api/admin/payments/export/status/{id} [get]
// @Security BearerAuth
func (r *PaymentController) GetExportStatus(ctx http.Context) http.Response {
exportID := helpers.GetUintRoute(ctx, "id")
if exportID == 0 {
return response.Error(ctx, http.StatusBadRequest, "export_id_required")
}
var exportRecord models.Export
if err := facades.Orm().Query().Where("id", exportID).FirstOrFail(&exportRecord); err != nil {
return response.Error(ctx, http.StatusNotFound, "export_not_found")
}
result := http.Json{
"id": exportRecord.ID,
"status": exportRecord.Status,
"status_text": r.getExportStatusText(ctx, exportRecord.Status),
"path": exportRecord.Path,
"filename": exportRecord.Filename,
"size": exportRecord.Size,
"error_msg": exportRecord.ErrorMsg,
"created_at": exportRecord.CreatedAt,
"updated_at": exportRecord.UpdatedAt,
}
if exportRecord.Status == models.ExportStatusSuccess && exportRecord.Path != "" {
result["download_url"] = fmt.Sprintf("/api/admin/exports/%d/download", exportRecord.ID)
}
return response.Success(ctx, result)
}
// getCurrentLanguage 获取当前语言(使用通用工具函数)
func (r *PaymentController) getCurrentLanguage(ctx http.Context) string {
return utils.GetCurrentLanguage(ctx)
}
// getExportStatusText 获取导出状态文本
func (r *PaymentController) getExportStatusText(ctx http.Context, status uint8) string {
switch status {
case models.ExportStatusProcessing:
return trans.Get(ctx, "export_task_status_processing")
case models.ExportStatusSuccess:
return trans.Get(ctx, "export_task_status_success")
case models.ExportStatusFailed:
return trans.Get(ctx, "export_task_status_failed")
default:
return trans.Get(ctx, "export_task_status_unknown")
}
}