init
This commit is contained in:
@@ -0,0 +1,421 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user