Files
user-system/internal/api/handler/export_handler.go
long-agent 8095307d82 fix: P0/P1 security and quality fixes
P0-01: Add ESCAPE clause to LIKE queries in operation_log.go and device.go
P0-02: Add atomic Increment to L1Cache and L2Cache interfaces
P0-07: Add TOTP verification step after password login
P1-01: Sanitize error messages in error.go middleware
P1-03: Remove err.Error() from export error messages
P1-04: Add error return to CountByResultSince in login_log.go
P1-05: Add transactional DeleteCascade to RoleRepository
P1-06: Add PasswordChangedAt tracking for JWT token invalidation
P1-07: Wrap theme SetDefault in database transaction
P1-08: Use config values for database pool parameters
P1-09: Add rows.Err() checks in social_account_repo.go
P1-10: Validate sortOrder with map in user.go ORDER BY
P1-11: Add GORM tags to Announcement struct
P1-15: Add pageSize upper limit (100) to device and log handlers
2026-04-18 15:33:12 +08:00

151 lines
4.3 KiB
Go

package handler
import (
"io"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/user-management-system/internal/service"
)
// ExportHandler handles user export/import requests
type ExportHandler struct {
exportService *service.ExportService
}
// NewExportHandler creates a new ExportHandler
func NewExportHandler(exportService *service.ExportService) *ExportHandler {
return &ExportHandler{exportService: exportService}
}
// ExportUsers 导出用户
// @Summary 导出用户数据
// @Description 导出用户数据为 CSV 或 Excel 格式
// @Tags 数据导入导出
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param format query string false "导出格式" default(csv) Enums(csv, excel)
// @Param fields query string false "导出字段,逗号分隔"
// @Param keyword query string false "关键词过滤"
// @Param status query int false "用户状态过滤"
// @Success 200 {file} file "用户数据文件"
// @Failure 401 {object} Response "未认证"
// @Failure 500 {object} Response "服务器错误"
// @Router /api/v1/exports/users [get]
func (h *ExportHandler) ExportUsers(c *gin.Context) {
format := c.DefaultQuery("format", "csv")
fieldsStr := c.Query("fields")
keyword := c.Query("keyword")
statusStr := c.Query("status")
var fields []string
if fieldsStr != "" {
fields = strings.Split(fieldsStr, ",")
}
var status *int
if statusStr != "" {
s, err := strconvAtoi(statusStr)
if err == nil {
status = &s
}
}
req := &service.ExportUsersRequest{
Format: format,
Fields: fields,
Keyword: keyword,
Status: status,
}
data, filename, contentType, err := h.exportService.ExportUsers(c.Request.Context(), req)
if err != nil {
// 安全修复:不泄露内部错误详情
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "导出失败"})
return
}
c.Header("Content-Type", contentType)
c.Header("Content-Disposition", "attachment; filename="+filename)
c.Data(http.StatusOK, contentType, data)
}
// ImportUsers 导入用户
// @Summary 导入用户数据
// @Description 从 CSV 或 Excel 文件导入用户数据
// @Tags 数据导入导出
// @Accept multipart/form-data
// @Produce json
// @Security BearerAuth
// @Param file formData file true "导入文件"
// @Param format query string false "文件格式" default(csv) Enums(csv, excel)
// @Success 200 {object} Response "导入结果"
// @Failure 400 {object} Response "请求参数错误"
// @Failure 401 {object} Response "未认证"
// @Failure 500 {object} Response "服务器错误"
// @Router /api/v1/exports/users [post]
func (h *ExportHandler) ImportUsers(c *gin.Context) {
file, _, err := c.Request.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "请上传文件"})
return
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "读取文件失败"})
return
}
format := c.DefaultQuery("format", "csv")
successCount, failCount, errs := h.exportService.ImportUsers(c.Request.Context(), data, format)
c.JSON(http.StatusOK, gin.H{
"code": 0,
"data": gin.H{
"success_count": successCount,
"fail_count": failCount,
"errors": errs,
},
})
}
// GetImportTemplate 获取导入模板
// @Summary 获取用户导入模板
// @Description 下载用户批量导入的 CSV 或 Excel 模板
// @Tags 数据导入导出
// @Produce json
// @Security BearerAuth
// @Param format query string false "模板格式" default(csv) Enums(csv, excel)
// @Success 200 {file} file "导入模板文件"
// @Failure 401 {object} Response "未认证"
// @Failure 500 {object} Response "服务器错误"
// @Router /api/v1/exports/template [get]
func (h *ExportHandler) GetImportTemplate(c *gin.Context) {
format := c.DefaultQuery("format", "csv")
data, filename, contentType, err := h.exportService.GetImportTemplateByFormat(format)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "获取模板失败"})
return
}
c.Header("Content-Type", contentType)
c.Header("Content-Disposition", "attachment; filename="+filename)
c.Data(http.StatusOK, contentType, data)
}
func strconvAtoi(s string) (int, error) {
var n int
for _, c := range s {
if c < '0' || c > '9' {
return 0, nil
}
n = n*10 + int(c-'0')
}
return n, nil
}