535 lines
15 KiB
Go
535 lines
15 KiB
Go
|
|
package service
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"bytes"
|
|||
|
|
"context"
|
|||
|
|
"encoding/csv"
|
|||
|
|
"fmt"
|
|||
|
|
"strings"
|
|||
|
|
"time"
|
|||
|
|
|
|||
|
|
"github.com/xuri/excelize/v2"
|
|||
|
|
|
|||
|
|
"github.com/user-management-system/internal/auth"
|
|||
|
|
"github.com/user-management-system/internal/domain"
|
|||
|
|
"github.com/user-management-system/internal/repository"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
const (
|
|||
|
|
ExportFormatCSV = "csv"
|
|||
|
|
ExportFormatXLSX = "xlsx"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// ExportUsersRequest defines the supported export filters and output options.
|
|||
|
|
type ExportUsersRequest struct {
|
|||
|
|
Format string
|
|||
|
|
Fields []string
|
|||
|
|
Keyword string
|
|||
|
|
Status *int
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type exportColumn struct {
|
|||
|
|
Key string
|
|||
|
|
Header string
|
|||
|
|
Value func(*domain.User) string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var defaultExportColumns = []exportColumn{
|
|||
|
|
{Key: "id", Header: "ID", Value: func(u *domain.User) string { return fmt.Sprintf("%d", u.ID) }},
|
|||
|
|
{Key: "username", Header: "用户名", Value: func(u *domain.User) string { return u.Username }},
|
|||
|
|
{Key: "email", Header: "邮箱", Value: func(u *domain.User) string { return domain.DerefStr(u.Email) }},
|
|||
|
|
{Key: "phone", Header: "手机号", Value: func(u *domain.User) string { return domain.DerefStr(u.Phone) }},
|
|||
|
|
{Key: "nickname", Header: "昵称", Value: func(u *domain.User) string { return u.Nickname }},
|
|||
|
|
{Key: "avatar", Header: "头像", Value: func(u *domain.User) string { return u.Avatar }},
|
|||
|
|
{Key: "gender", Header: "性别", Value: func(u *domain.User) string { return genderLabel(u.Gender) }},
|
|||
|
|
{Key: "status", Header: "状态", Value: func(u *domain.User) string { return userStatusLabel(u.Status) }},
|
|||
|
|
{Key: "region", Header: "地区", Value: func(u *domain.User) string { return u.Region }},
|
|||
|
|
{Key: "bio", Header: "个人简介", Value: func(u *domain.User) string { return u.Bio }},
|
|||
|
|
{Key: "totp_enabled", Header: "TOTP已启用", Value: func(u *domain.User) string { return boolLabel(u.TOTPEnabled) }},
|
|||
|
|
{Key: "last_login_time", Header: "最后登录时间", Value: func(u *domain.User) string { return timeLabel(u.LastLoginTime) }},
|
|||
|
|
{Key: "last_login_ip", Header: "最后登录IP", Value: func(u *domain.User) string { return u.LastLoginIP }},
|
|||
|
|
{Key: "created_at", Header: "注册时间", Value: func(u *domain.User) string { return u.CreatedAt.Format("2006-01-02 15:04:05") }},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ExportService 用户数据导入导出服务
|
|||
|
|
type ExportService struct {
|
|||
|
|
userRepo *repository.UserRepository
|
|||
|
|
roleRepo *repository.RoleRepository
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NewExportService 创建导入导出服务
|
|||
|
|
func NewExportService(
|
|||
|
|
userRepo *repository.UserRepository,
|
|||
|
|
roleRepo *repository.RoleRepository,
|
|||
|
|
) *ExportService {
|
|||
|
|
return &ExportService{
|
|||
|
|
userRepo: userRepo,
|
|||
|
|
roleRepo: roleRepo,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ExportUsers exports users as CSV or XLSX.
|
|||
|
|
func (s *ExportService) ExportUsers(ctx context.Context, req *ExportUsersRequest) ([]byte, string, string, error) {
|
|||
|
|
if req == nil {
|
|||
|
|
req = &ExportUsersRequest{}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
format, err := normalizeExportFormat(req.Format)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, "", "", err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
columns, err := resolveExportColumns(req.Fields)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, "", "", err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
users, err := s.listUsersForExport(ctx, req)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, "", "", err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
filename := fmt.Sprintf("users_%s.%s", time.Now().Format("20060102_150405"), format)
|
|||
|
|
switch format {
|
|||
|
|
case ExportFormatCSV:
|
|||
|
|
data, err := buildCSVExport(columns, users)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, "", "", err
|
|||
|
|
}
|
|||
|
|
return data, filename, "text/csv; charset=utf-8", nil
|
|||
|
|
case ExportFormatXLSX:
|
|||
|
|
data, err := buildXLSXExport(columns, users)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, "", "", err
|
|||
|
|
}
|
|||
|
|
return data, filename, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", nil
|
|||
|
|
default:
|
|||
|
|
return nil, "", "", fmt.Errorf("不支持的导出格式: %s", req.Format)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ExportUsersCSV keeps backward compatibility for callers that still expect CSV-only export.
|
|||
|
|
func (s *ExportService) ExportUsersCSV(ctx context.Context) ([]byte, string, error) {
|
|||
|
|
data, filename, _, err := s.ExportUsers(ctx, &ExportUsersRequest{Format: ExportFormatCSV})
|
|||
|
|
return data, filename, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ExportUsersXLSX exports users as Excel.
|
|||
|
|
func (s *ExportService) ExportUsersXLSX(ctx context.Context) ([]byte, string, error) {
|
|||
|
|
data, filename, _, err := s.ExportUsers(ctx, &ExportUsersRequest{Format: ExportFormatXLSX})
|
|||
|
|
return data, filename, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (s *ExportService) listUsersForExport(ctx context.Context, req *ExportUsersRequest) ([]*domain.User, error) {
|
|||
|
|
var allUsers []*domain.User
|
|||
|
|
offset := 0
|
|||
|
|
batchSize := 500
|
|||
|
|
|
|||
|
|
for {
|
|||
|
|
var (
|
|||
|
|
users []*domain.User
|
|||
|
|
total int64
|
|||
|
|
err error
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if req.Keyword != "" || req.Status != nil {
|
|||
|
|
filter := &repository.AdvancedFilter{
|
|||
|
|
Keyword: req.Keyword,
|
|||
|
|
Status: -1,
|
|||
|
|
SortBy: "created_at",
|
|||
|
|
SortOrder: "desc",
|
|||
|
|
Offset: offset,
|
|||
|
|
Limit: batchSize,
|
|||
|
|
}
|
|||
|
|
if req.Status != nil {
|
|||
|
|
filter.Status = *req.Status
|
|||
|
|
}
|
|||
|
|
users, total, err = s.userRepo.AdvancedSearch(ctx, filter)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("查询用户失败: %w", err)
|
|||
|
|
}
|
|||
|
|
allUsers = append(allUsers, users...)
|
|||
|
|
offset += len(users)
|
|||
|
|
if offset >= int(total) || len(users) == 0 {
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
users, _, err = s.userRepo.List(ctx, offset, batchSize)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("查询用户失败: %w", err)
|
|||
|
|
}
|
|||
|
|
allUsers = append(allUsers, users...)
|
|||
|
|
if len(users) < batchSize {
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
offset += batchSize
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return allUsers, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ImportUsers imports users from CSV or XLSX.
|
|||
|
|
func (s *ExportService) ImportUsers(ctx context.Context, data []byte, format string) (successCount, failCount int, errs []string) {
|
|||
|
|
normalized, err := normalizeExportFormat(format)
|
|||
|
|
if err != nil {
|
|||
|
|
return 0, 0, []string{err.Error()}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var records [][]string
|
|||
|
|
switch normalized {
|
|||
|
|
case ExportFormatCSV:
|
|||
|
|
records, err = parseCSVRecords(data)
|
|||
|
|
case ExportFormatXLSX:
|
|||
|
|
records, err = parseXLSXRecords(data)
|
|||
|
|
default:
|
|||
|
|
err = fmt.Errorf("不支持的导入格式: %s", format)
|
|||
|
|
}
|
|||
|
|
if err != nil {
|
|||
|
|
return 0, 0, []string{err.Error()}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return s.importUsersRecords(ctx, records)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ImportUsersCSV keeps backward compatibility for callers that still upload CSV.
|
|||
|
|
func (s *ExportService) ImportUsersCSV(ctx context.Context, data []byte) (successCount, failCount int, errs []string) {
|
|||
|
|
return s.ImportUsers(ctx, data, ExportFormatCSV)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ImportUsersXLSX imports users from Excel.
|
|||
|
|
func (s *ExportService) ImportUsersXLSX(ctx context.Context, data []byte) (successCount, failCount int, errs []string) {
|
|||
|
|
return s.ImportUsers(ctx, data, ExportFormatXLSX)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (s *ExportService) importUsersRecords(ctx context.Context, records [][]string) (successCount, failCount int, errs []string) {
|
|||
|
|
if len(records) < 2 {
|
|||
|
|
return 0, 0, []string{"导入文件为空或没有数据行"}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
headers := records[0]
|
|||
|
|
colIdx := buildColIndex(headers)
|
|||
|
|
getCol := func(row []string, name string) string {
|
|||
|
|
idx, ok := colIdx[name]
|
|||
|
|
if !ok || idx >= len(row) {
|
|||
|
|
return ""
|
|||
|
|
}
|
|||
|
|
return strings.TrimSpace(row[idx])
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for i, row := range records[1:] {
|
|||
|
|
lineNum := i + 2
|
|||
|
|
username := getCol(row, "用户名")
|
|||
|
|
password := getCol(row, "密码")
|
|||
|
|
|
|||
|
|
if username == "" || password == "" {
|
|||
|
|
failCount++
|
|||
|
|
errs = append(errs, fmt.Sprintf("第%d行:用户名和密码不能为空", lineNum))
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
exists, err := s.userRepo.ExistsByUsername(ctx, username)
|
|||
|
|
if err != nil {
|
|||
|
|
failCount++
|
|||
|
|
errs = append(errs, fmt.Sprintf("第%d行:检查用户名失败: %v", lineNum, err))
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
if exists {
|
|||
|
|
failCount++
|
|||
|
|
errs = append(errs, fmt.Sprintf("第%d行:用户名 '%s' 已存在", lineNum, username))
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
hashedPwd, err := hashPassword(password)
|
|||
|
|
if err != nil {
|
|||
|
|
failCount++
|
|||
|
|
errs = append(errs, fmt.Sprintf("第%d行:密码加密失败: %v", lineNum, err))
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
user := &domain.User{
|
|||
|
|
Username: username,
|
|||
|
|
Email: domain.StrPtr(getCol(row, "邮箱")),
|
|||
|
|
Phone: domain.StrPtr(getCol(row, "手机号")),
|
|||
|
|
Nickname: getCol(row, "昵称"),
|
|||
|
|
Password: hashedPwd,
|
|||
|
|
Region: getCol(row, "地区"),
|
|||
|
|
Bio: getCol(row, "个人简介"),
|
|||
|
|
Status: domain.UserStatusActive,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if err := s.userRepo.Create(ctx, user); err != nil {
|
|||
|
|
failCount++
|
|||
|
|
errs = append(errs, fmt.Sprintf("第%d行:创建用户失败: %v", lineNum, err))
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
successCount++
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return successCount, failCount, errs
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetImportTemplate keeps backward compatibility for callers that still expect CSV templates.
|
|||
|
|
func (s *ExportService) GetImportTemplate() ([]byte, string) {
|
|||
|
|
data, filename, _, _ := s.GetImportTemplateByFormat(ExportFormatCSV)
|
|||
|
|
return data, filename
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetImportTemplateByFormat returns a CSV or XLSX template for imports.
|
|||
|
|
func (s *ExportService) GetImportTemplateByFormat(format string) ([]byte, string, string, error) {
|
|||
|
|
normalized, err := normalizeExportFormat(format)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, "", "", err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
headers := []string{"用户名", "密码", "邮箱", "手机号", "昵称", "性别", "地区", "个人简介"}
|
|||
|
|
rows := [][]string{{
|
|||
|
|
"john_doe", "Password123!", "john@example.com", "13800138000",
|
|||
|
|
"约翰", "男", "北京", "这是个人简介",
|
|||
|
|
}}
|
|||
|
|
|
|||
|
|
switch normalized {
|
|||
|
|
case ExportFormatCSV:
|
|||
|
|
data, err := buildCSVRecords(headers, rows)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, "", "", err
|
|||
|
|
}
|
|||
|
|
return data, "user_import_template.csv", "text/csv; charset=utf-8", nil
|
|||
|
|
case ExportFormatXLSX:
|
|||
|
|
data, err := buildXLSXRecords(headers, rows)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, "", "", err
|
|||
|
|
}
|
|||
|
|
return data, "user_import_template.xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", nil
|
|||
|
|
default:
|
|||
|
|
return nil, "", "", fmt.Errorf("不支持的模板格式: %s", format)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func normalizeExportFormat(format string) (string, error) {
|
|||
|
|
normalized := strings.ToLower(strings.TrimSpace(format))
|
|||
|
|
if normalized == "" {
|
|||
|
|
normalized = ExportFormatCSV
|
|||
|
|
}
|
|||
|
|
switch normalized {
|
|||
|
|
case ExportFormatCSV, ExportFormatXLSX:
|
|||
|
|
return normalized, nil
|
|||
|
|
default:
|
|||
|
|
return "", fmt.Errorf("不支持的格式: %s", format)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func resolveExportColumns(fields []string) ([]exportColumn, error) {
|
|||
|
|
if len(fields) == 0 {
|
|||
|
|
return defaultExportColumns, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
columnMap := make(map[string]exportColumn, len(defaultExportColumns))
|
|||
|
|
for _, col := range defaultExportColumns {
|
|||
|
|
columnMap[col.Key] = col
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
selected := make([]exportColumn, 0, len(fields))
|
|||
|
|
seen := make(map[string]struct{}, len(fields))
|
|||
|
|
for _, field := range fields {
|
|||
|
|
key := strings.ToLower(strings.TrimSpace(field))
|
|||
|
|
if key == "" {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
if _, ok := seen[key]; ok {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
col, ok := columnMap[key]
|
|||
|
|
if !ok {
|
|||
|
|
return nil, fmt.Errorf("不支持的导出字段: %s", field)
|
|||
|
|
}
|
|||
|
|
selected = append(selected, col)
|
|||
|
|
seen[key] = struct{}{}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if len(selected) == 0 {
|
|||
|
|
return defaultExportColumns, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return selected, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func buildCSVExport(columns []exportColumn, users []*domain.User) ([]byte, error) {
|
|||
|
|
headers := make([]string, 0, len(columns))
|
|||
|
|
rows := make([][]string, 0, len(users))
|
|||
|
|
for _, col := range columns {
|
|||
|
|
headers = append(headers, col.Header)
|
|||
|
|
}
|
|||
|
|
for _, u := range users {
|
|||
|
|
row := make([]string, 0, len(columns))
|
|||
|
|
for _, col := range columns {
|
|||
|
|
row = append(row, col.Value(u))
|
|||
|
|
}
|
|||
|
|
rows = append(rows, row)
|
|||
|
|
}
|
|||
|
|
return buildCSVRecords(headers, rows)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func buildCSVRecords(headers []string, rows [][]string) ([]byte, error) {
|
|||
|
|
var buf bytes.Buffer
|
|||
|
|
buf.Write([]byte{0xEF, 0xBB, 0xBF})
|
|||
|
|
writer := csv.NewWriter(&buf)
|
|||
|
|
|
|||
|
|
if err := writer.Write(headers); err != nil {
|
|||
|
|
return nil, fmt.Errorf("写CSV表头失败: %w", err)
|
|||
|
|
}
|
|||
|
|
for _, row := range rows {
|
|||
|
|
if err := writer.Write(row); err != nil {
|
|||
|
|
return nil, fmt.Errorf("写CSV行失败: %w", err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
writer.Flush()
|
|||
|
|
if err := writer.Error(); err != nil {
|
|||
|
|
return nil, fmt.Errorf("CSV Flush 失败: %w", err)
|
|||
|
|
}
|
|||
|
|
return buf.Bytes(), nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func buildXLSXExport(columns []exportColumn, users []*domain.User) ([]byte, error) {
|
|||
|
|
headers := make([]string, 0, len(columns))
|
|||
|
|
rows := make([][]string, 0, len(users))
|
|||
|
|
for _, col := range columns {
|
|||
|
|
headers = append(headers, col.Header)
|
|||
|
|
}
|
|||
|
|
for _, u := range users {
|
|||
|
|
row := make([]string, 0, len(columns))
|
|||
|
|
for _, col := range columns {
|
|||
|
|
row = append(row, col.Value(u))
|
|||
|
|
}
|
|||
|
|
rows = append(rows, row)
|
|||
|
|
}
|
|||
|
|
return buildXLSXRecords(headers, rows)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func buildXLSXRecords(headers []string, rows [][]string) ([]byte, error) {
|
|||
|
|
file := excelize.NewFile()
|
|||
|
|
defer file.Close()
|
|||
|
|
|
|||
|
|
sheet := file.GetSheetName(file.GetActiveSheetIndex())
|
|||
|
|
if sheet == "" {
|
|||
|
|
sheet = "Sheet1"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for idx, header := range headers {
|
|||
|
|
cell, err := excelize.CoordinatesToCellName(idx+1, 1)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("生成表头单元格失败: %w", err)
|
|||
|
|
}
|
|||
|
|
if err := file.SetCellValue(sheet, cell, header); err != nil {
|
|||
|
|
return nil, fmt.Errorf("写入表头失败: %w", err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for rowIdx, row := range rows {
|
|||
|
|
for colIdx, value := range row {
|
|||
|
|
cell, err := excelize.CoordinatesToCellName(colIdx+1, rowIdx+2)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("生成数据单元格失败: %w", err)
|
|||
|
|
}
|
|||
|
|
if err := file.SetCellValue(sheet, cell, value); err != nil {
|
|||
|
|
return nil, fmt.Errorf("写入单元格失败: %w", err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var buf bytes.Buffer
|
|||
|
|
if _, err := file.WriteTo(&buf); err != nil {
|
|||
|
|
return nil, fmt.Errorf("生成Excel失败: %w", err)
|
|||
|
|
}
|
|||
|
|
return buf.Bytes(), nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func parseCSVRecords(data []byte) ([][]string, error) {
|
|||
|
|
if len(data) >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF {
|
|||
|
|
data = data[3:]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
reader := csv.NewReader(bytes.NewReader(data))
|
|||
|
|
records, err := reader.ReadAll()
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("CSV 解析失败: %w", err)
|
|||
|
|
}
|
|||
|
|
return records, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func parseXLSXRecords(data []byte) ([][]string, error) {
|
|||
|
|
file, err := excelize.OpenReader(bytes.NewReader(data))
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("Excel 解析失败: %w", err)
|
|||
|
|
}
|
|||
|
|
defer file.Close()
|
|||
|
|
|
|||
|
|
sheets := file.GetSheetList()
|
|||
|
|
if len(sheets) == 0 {
|
|||
|
|
return nil, fmt.Errorf("Excel 文件没有可用工作表")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
rows, err := file.GetRows(sheets[0])
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("读取Excel行失败: %w", err)
|
|||
|
|
}
|
|||
|
|
return rows, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---- 辅助函数 ----
|
|||
|
|
|
|||
|
|
func genderLabel(g domain.Gender) string {
|
|||
|
|
switch g {
|
|||
|
|
case domain.GenderMale:
|
|||
|
|
return "男"
|
|||
|
|
case domain.GenderFemale:
|
|||
|
|
return "女"
|
|||
|
|
default:
|
|||
|
|
return "未知"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func userStatusLabel(s domain.UserStatus) string {
|
|||
|
|
switch s {
|
|||
|
|
case domain.UserStatusActive:
|
|||
|
|
return "已激活"
|
|||
|
|
case domain.UserStatusInactive:
|
|||
|
|
return "未激活"
|
|||
|
|
case domain.UserStatusLocked:
|
|||
|
|
return "已锁定"
|
|||
|
|
case domain.UserStatusDisabled:
|
|||
|
|
return "已禁用"
|
|||
|
|
default:
|
|||
|
|
return "未知"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func boolLabel(b bool) string {
|
|||
|
|
if b {
|
|||
|
|
return "是"
|
|||
|
|
}
|
|||
|
|
return "否"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func timeLabel(t *time.Time) string {
|
|||
|
|
if t == nil {
|
|||
|
|
return ""
|
|||
|
|
}
|
|||
|
|
return t.Format("2006-01-02 15:04:05")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// buildColIndex 将表头列名映射到列索引
|
|||
|
|
func buildColIndex(headers []string) map[string]int {
|
|||
|
|
idx := make(map[string]int, len(headers))
|
|||
|
|
for i, h := range headers {
|
|||
|
|
idx[h] = i
|
|||
|
|
}
|
|||
|
|
return idx
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// hashPassword hashes imported passwords with the primary runtime algorithm.
|
|||
|
|
func hashPassword(password string) (string, error) {
|
|||
|
|
return auth.HashPassword(password)
|
|||
|
|
}
|