mirror of
https://github.com/skyle1995/NetworkAuth.git
synced 2026-05-25 02:24:05 +08:00
更新底层架构
This commit is contained in:
@@ -3,8 +3,6 @@ package utils
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
@@ -15,7 +13,10 @@ import (
|
||||
// name: Cookie名称
|
||||
// value: Cookie值
|
||||
// maxAge: 过期时间(秒),0表示会话Cookie,-1表示立即过期
|
||||
func CreateSecureCookie(name, value string, maxAge int) *http.Cookie {
|
||||
// domain: Cookie域名
|
||||
// secure: 是否只在HTTPS下发送
|
||||
// sameSiteStr: SameSite属性(Strict/Lax/None)
|
||||
func CreateSecureCookie(name, value string, maxAge int, domain string, secure bool, sameSiteStr string) *http.Cookie {
|
||||
cookie := &http.Cookie{
|
||||
Name: name,
|
||||
Value: value,
|
||||
@@ -24,14 +25,13 @@ func CreateSecureCookie(name, value string, maxAge int) *http.Cookie {
|
||||
MaxAge: maxAge,
|
||||
}
|
||||
|
||||
// 从配置读取安全设置
|
||||
if viper.GetBool("security.cookie.secure") {
|
||||
// 设置安全属性
|
||||
if secure {
|
||||
cookie.Secure = true
|
||||
}
|
||||
|
||||
// 设置SameSite属性
|
||||
sameSite := viper.GetString("security.cookie.same_site")
|
||||
switch sameSite {
|
||||
switch sameSiteStr {
|
||||
case "Strict":
|
||||
cookie.SameSite = http.SameSiteStrictMode
|
||||
case "Lax":
|
||||
@@ -44,8 +44,7 @@ func CreateSecureCookie(name, value string, maxAge int) *http.Cookie {
|
||||
cookie.SameSite = http.SameSiteStrictMode
|
||||
}
|
||||
|
||||
// 设置Domain(如果配置了)
|
||||
domain := viper.GetString("security.cookie.domain")
|
||||
// 设置Domain
|
||||
if domain != "" {
|
||||
cookie.Domain = domain
|
||||
}
|
||||
@@ -62,24 +61,11 @@ func CreateSecureCookie(name, value string, maxAge int) *http.Cookie {
|
||||
}
|
||||
|
||||
// CreateSessionCookie 创建会话Cookie(浏览器关闭时过期)
|
||||
func CreateSessionCookie(name, value string) *http.Cookie {
|
||||
return CreateSecureCookie(name, value, 0)
|
||||
func CreateSessionCookie(name, value string, domain string, secure bool, sameSiteStr string) *http.Cookie {
|
||||
return CreateSecureCookie(name, value, 0, domain, secure, sameSiteStr)
|
||||
}
|
||||
|
||||
// CreateExpiredCookie 创建立即过期的Cookie(用于清理)
|
||||
func CreateExpiredCookie(name string) *http.Cookie {
|
||||
return CreateSecureCookie(name, "", -1)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 配置函数
|
||||
// ============================================================================
|
||||
|
||||
// GetDefaultCookieMaxAge 获取默认Cookie过期时间
|
||||
func GetDefaultCookieMaxAge() int {
|
||||
maxAge := viper.GetInt("security.cookie.max_age")
|
||||
if maxAge <= 0 {
|
||||
return 86400 // 默认24小时
|
||||
}
|
||||
return maxAge
|
||||
func CreateExpiredCookie(name string, domain string) *http.Cookie {
|
||||
return CreateSecureCookie(name, "", -1, domain, false, "Lax")
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
@@ -38,28 +37,26 @@ var cryptoManager = &CryptoManager{}
|
||||
// 私有函数
|
||||
// ============================================================================
|
||||
|
||||
// initCrypto 初始化加密管理器
|
||||
// 缓存密钥和GCM实例,避免重复创建
|
||||
func (cm *CryptoManager) initCrypto() error {
|
||||
cm.mutex.Lock()
|
||||
defer cm.mutex.Unlock()
|
||||
// InitEncryption 初始化加密管理器
|
||||
// 必须在应用启动时调用,传入加密密钥
|
||||
func InitEncryption(secret string) error {
|
||||
cryptoManager.mutex.Lock()
|
||||
defer cryptoManager.mutex.Unlock()
|
||||
|
||||
if cm.inited {
|
||||
if cryptoManager.inited {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 从配置中获取密钥
|
||||
secret := viper.GetString("encryption_key")
|
||||
if secret == "" {
|
||||
secret = "default-secret"
|
||||
return errors.New("加密密钥不能为空")
|
||||
}
|
||||
|
||||
// 生成AES密钥
|
||||
sum := sha256.Sum256([]byte(secret))
|
||||
cm.key = sum[:]
|
||||
cryptoManager.key = sum[:]
|
||||
|
||||
// 创建AES cipher
|
||||
block, err := aes.NewCipher(cm.key)
|
||||
block, err := aes.NewCipher(cryptoManager.key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -70,8 +67,19 @@ func (cm *CryptoManager) initCrypto() error {
|
||||
return err
|
||||
}
|
||||
|
||||
cm.gcm = gcm
|
||||
cm.inited = true
|
||||
cryptoManager.gcm = gcm
|
||||
cryptoManager.inited = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// initCrypto 检查加密管理器是否已初始化
|
||||
func (cm *CryptoManager) initCrypto() error {
|
||||
cm.mutex.RLock()
|
||||
defer cm.mutex.RUnlock()
|
||||
|
||||
if !cm.inited {
|
||||
return errors.New("加密管理器未初始化,请先调用InitEncryption")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -101,110 +101,6 @@ func ConfigureConnectionPool(db *gorm.DB, config *DatabaseConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// PingDatabase 检查数据库连接健康状态
|
||||
// 使用指定的超时时间ping数据库以验证连接是否正常
|
||||
func PingDatabase(db *gorm.DB, timeout time.Duration) error {
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取底层数据库连接失败: %w", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
return sqlDB.PingContext(ctx)
|
||||
}
|
||||
|
||||
// GetConnectionStats 获取数据库连接池统计信息
|
||||
// 返回当前数据库连接池的详细统计数据,包括连接数、等待时间等
|
||||
func GetConnectionStats(db *gorm.DB) (*sql.DBStats, error) {
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取底层数据库连接失败: %w", err)
|
||||
}
|
||||
|
||||
stats := sqlDB.Stats()
|
||||
return &stats, nil
|
||||
}
|
||||
|
||||
// LogConnectionStats 记录数据库连接池统计信息
|
||||
// 获取并记录数据库连接池的统计信息到日志中,用于监控和调试
|
||||
func LogConnectionStats(db *gorm.DB) {
|
||||
stats, err := GetConnectionStats(db)
|
||||
if err != nil {
|
||||
LogError("获取数据库连接池统计信息失败", err, nil)
|
||||
return
|
||||
}
|
||||
|
||||
LogInfo("数据库连接池统计", map[string]interface{}{
|
||||
"open_connections": stats.OpenConnections,
|
||||
"in_use": stats.InUse,
|
||||
"idle": stats.Idle,
|
||||
"wait_count": stats.WaitCount,
|
||||
"wait_duration": stats.WaitDuration,
|
||||
"max_idle_closed": stats.MaxIdleClosed,
|
||||
"max_idle_time_closed": stats.MaxIdleTimeClosed,
|
||||
"max_lifetime_closed": stats.MaxLifetimeClosed,
|
||||
})
|
||||
}
|
||||
|
||||
// StartHealthCheck 启动数据库健康检查
|
||||
// 启动一个后台goroutine定期检查数据库连接健康状态
|
||||
// 只在健康检查失败时输出错误日志,正常情况下不输出日志
|
||||
func StartHealthCheck(db *gorm.DB, config *DatabaseConfig) {
|
||||
go func() {
|
||||
ticker := time.NewTicker(config.HealthCheckInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
if err := PingDatabase(db, config.PingTimeout); err != nil {
|
||||
// 只在健康检查失败时输出错误日志
|
||||
LogError("数据库健康检查失败", err, map[string]interface{}{
|
||||
"ping_timeout": config.PingTimeout,
|
||||
})
|
||||
}
|
||||
|
||||
// 记录连接池统计信息(仅在调试模式下)
|
||||
if logrus.GetLevel() == logrus.DebugLevel {
|
||||
LogConnectionStats(db)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// LogInfo("数据库健康检查已启动", map[string]interface{}{
|
||||
// "check_interval": config.HealthCheckInterval,
|
||||
// "ping_timeout": config.PingTimeout,
|
||||
// })
|
||||
}
|
||||
|
||||
// ValidateDatabaseConfig 验证数据库配置参数
|
||||
// 检查数据库配置参数的有效性,确保所有参数都在合理范围内
|
||||
func ValidateDatabaseConfig(config *DatabaseConfig) error {
|
||||
if config.MaxIdleConns < 0 {
|
||||
return fmt.Errorf("最大空闲连接数不能为负数: %d", config.MaxIdleConns)
|
||||
}
|
||||
if config.MaxOpenConns < 0 {
|
||||
return fmt.Errorf("最大打开连接数不能为负数: %d", config.MaxOpenConns)
|
||||
}
|
||||
if config.MaxIdleConns > config.MaxOpenConns && config.MaxOpenConns > 0 {
|
||||
return fmt.Errorf("最大空闲连接数(%d)不能大于最大打开连接数(%d)", config.MaxIdleConns, config.MaxOpenConns)
|
||||
}
|
||||
if config.ConnMaxLifetime < 0 {
|
||||
return fmt.Errorf("连接最大生存时间不能为负数: %v", config.ConnMaxLifetime)
|
||||
}
|
||||
if config.ConnMaxIdleTime < 0 {
|
||||
return fmt.Errorf("连接最大空闲时间不能为负数: %v", config.ConnMaxIdleTime)
|
||||
}
|
||||
if config.PingTimeout <= 0 {
|
||||
return fmt.Errorf("Ping超时时间必须大于0: %v", config.PingTimeout)
|
||||
}
|
||||
if config.HealthCheckInterval <= 0 {
|
||||
return fmt.Errorf("健康检查间隔必须大于0: %v", config.HealthCheckInterval)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 全局变量
|
||||
// ============================================================================
|
||||
@@ -222,11 +118,24 @@ var (
|
||||
// Redis函数
|
||||
// ============================================================================
|
||||
|
||||
// RedisLogger 自定义Redis日志记录器
|
||||
// 仅在Debug级别输出Redis内部日志
|
||||
type RedisLogger struct{}
|
||||
|
||||
func (l *RedisLogger) Printf(ctx context.Context, format string, v ...interface{}) {
|
||||
if logrus.GetLevel() >= logrus.DebugLevel {
|
||||
logrus.Debugf(format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
// InitRedis 初始化Redis客户端(仅在配置存在时尝试连接)
|
||||
// - 从 viper 读取 security.redis.* 配置
|
||||
// - 从 viper 读取 redis.* 配置
|
||||
// - 如果连接失败,则标记为不可用,不影响主流程
|
||||
func InitRedis() {
|
||||
redisOnce.Do(func() {
|
||||
// 设置自定义日志记录器,避免在Info级别输出大量连接错误日志
|
||||
redis.SetLogger(&RedisLogger{})
|
||||
|
||||
host := viper.GetString("redis.host")
|
||||
port := viper.GetInt("redis.port")
|
||||
if host == "" || port == 0 {
|
||||
@@ -341,3 +250,107 @@ func RedisDel(ctx context.Context, keys ...string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PingDatabase 检查数据库连接健康状态
|
||||
// 使用指定的超时时间ping数据库以验证连接是否正常
|
||||
func PingDatabase(db *gorm.DB, timeout time.Duration) error {
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取底层数据库连接失败: %w", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
return sqlDB.PingContext(ctx)
|
||||
}
|
||||
|
||||
// GetConnectionStats 获取数据库连接池统计信息
|
||||
// 返回当前数据库连接池的详细统计数据,包括连接数、等待时间等
|
||||
func GetConnectionStats(db *gorm.DB) (*sql.DBStats, error) {
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取底层数据库连接失败: %w", err)
|
||||
}
|
||||
|
||||
stats := sqlDB.Stats()
|
||||
return &stats, nil
|
||||
}
|
||||
|
||||
// LogConnectionStats 记录数据库连接池统计信息
|
||||
// 获取并记录数据库连接池的统计信息到日志中,用于监控和调试
|
||||
func LogConnectionStats(db *gorm.DB) {
|
||||
stats, err := GetConnectionStats(db)
|
||||
if err != nil {
|
||||
LogError("获取数据库连接池统计信息失败", err, nil)
|
||||
return
|
||||
}
|
||||
|
||||
LogInfo("数据库连接池统计", map[string]interface{}{
|
||||
"open_connections": stats.OpenConnections,
|
||||
"in_use": stats.InUse,
|
||||
"idle": stats.Idle,
|
||||
"wait_count": stats.WaitCount,
|
||||
"wait_duration": stats.WaitDuration,
|
||||
"max_idle_closed": stats.MaxIdleClosed,
|
||||
"max_idle_time_closed": stats.MaxIdleTimeClosed,
|
||||
"max_lifetime_closed": stats.MaxLifetimeClosed,
|
||||
})
|
||||
}
|
||||
|
||||
// StartHealthCheck 启动数据库健康检查
|
||||
// 启动一个后台goroutine定期检查数据库连接健康状态
|
||||
// 只在健康检查失败时输出错误日志,正常情况下不输出日志
|
||||
func StartHealthCheck(db *gorm.DB, config *DatabaseConfig) {
|
||||
go func() {
|
||||
ticker := time.NewTicker(config.HealthCheckInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
if err := PingDatabase(db, config.PingTimeout); err != nil {
|
||||
// 只在健康检查失败时输出错误日志
|
||||
LogError("数据库健康检查失败", err, map[string]interface{}{
|
||||
"ping_timeout": config.PingTimeout,
|
||||
})
|
||||
}
|
||||
|
||||
// 记录连接池统计信息(仅在调试模式下)
|
||||
if logrus.GetLevel() == logrus.DebugLevel {
|
||||
LogConnectionStats(db)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// LogInfo("数据库健康检查已启动", map[string]interface{}{
|
||||
// "check_interval": config.HealthCheckInterval,
|
||||
// "ping_timeout": config.PingTimeout,
|
||||
// })
|
||||
}
|
||||
|
||||
// ValidateDatabaseConfig 验证数据库配置参数
|
||||
// 检查数据库配置参数的有效性,确保所有参数都在合理范围内
|
||||
func ValidateDatabaseConfig(config *DatabaseConfig) error {
|
||||
if config.MaxIdleConns < 0 {
|
||||
return fmt.Errorf("最大空闲连接数不能为负数: %d", config.MaxIdleConns)
|
||||
}
|
||||
if config.MaxOpenConns < 0 {
|
||||
return fmt.Errorf("最大打开连接数不能为负数: %d", config.MaxOpenConns)
|
||||
}
|
||||
if config.MaxIdleConns > config.MaxOpenConns && config.MaxOpenConns > 0 {
|
||||
return fmt.Errorf("最大空闲连接数(%d)不能大于最大打开连接数(%d)", config.MaxIdleConns, config.MaxOpenConns)
|
||||
}
|
||||
if config.ConnMaxLifetime < 0 {
|
||||
return fmt.Errorf("连接最大生存时间不能为负数: %v", config.ConnMaxLifetime)
|
||||
}
|
||||
if config.ConnMaxIdleTime < 0 {
|
||||
return fmt.Errorf("连接最大空闲时间不能为负数: %v", config.ConnMaxIdleTime)
|
||||
}
|
||||
if config.PingTimeout <= 0 {
|
||||
return fmt.Errorf("Ping超时时间必须大于0: %v", config.PingTimeout)
|
||||
}
|
||||
if config.HealthCheckInterval <= 0 {
|
||||
return fmt.Errorf("健康检查间隔必须大于0: %v", config.HealthCheckInterval)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
121
utils/excel/excel.go
Normal file
121
utils/excel/excel.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package excel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
// ExportData 导出数据配置
|
||||
type ExportData struct {
|
||||
Headers []string // 表头显示名称列表
|
||||
Fields []string // 对应结构体字段名或Map键名
|
||||
Data interface{} // 数据源(必须是切片类型)
|
||||
Sheet string // 工作表名称,默认为 Sheet1
|
||||
}
|
||||
|
||||
// Export 生成Excel文件
|
||||
func Export(config ExportData) (*excelize.File, error) {
|
||||
f := excelize.NewFile()
|
||||
sheet := config.Sheet
|
||||
if sheet == "" {
|
||||
sheet = "Sheet1"
|
||||
}
|
||||
|
||||
// 如果不是默认Sheet1,则创建新Sheet
|
||||
index, err := f.NewSheet(sheet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 设置表头
|
||||
for i, header := range config.Headers {
|
||||
cell, _ := excelize.CoordinatesToCellName(i+1, 1)
|
||||
f.SetCellValue(sheet, cell, header)
|
||||
}
|
||||
|
||||
// 设置表头样式(加粗、背景色)
|
||||
style, _ := f.NewStyle(&excelize.Style{
|
||||
Font: &excelize.Font{Bold: true},
|
||||
Fill: excelize.Fill{Type: "pattern", Color: []string{"#E0E0E0"}, Pattern: 1},
|
||||
})
|
||||
f.SetRowStyle(sheet, 1, 1, style)
|
||||
|
||||
// 处理数据
|
||||
val := reflect.ValueOf(config.Data)
|
||||
if val.Kind() != reflect.Slice {
|
||||
return nil, fmt.Errorf("data must be a slice")
|
||||
}
|
||||
|
||||
for i := 0; i < val.Len(); i++ {
|
||||
item := val.Index(i)
|
||||
// 如果是指针,获取其指向的值
|
||||
if item.Kind() == reflect.Ptr {
|
||||
item = item.Elem()
|
||||
}
|
||||
|
||||
rowNum := i + 2 // 从第2行开始(第1行是表头)
|
||||
|
||||
for j, field := range config.Fields {
|
||||
cellName, _ := excelize.CoordinatesToCellName(j+1, rowNum)
|
||||
var cellValue interface{}
|
||||
|
||||
// 尝试从结构体或Map中获取值
|
||||
if item.Kind() == reflect.Struct {
|
||||
fieldVal := item.FieldByName(field)
|
||||
if fieldVal.IsValid() {
|
||||
cellValue = fieldVal.Interface()
|
||||
}
|
||||
} else if item.Kind() == reflect.Map {
|
||||
key := reflect.ValueOf(field)
|
||||
mapVal := item.MapIndex(key)
|
||||
if mapVal.IsValid() {
|
||||
cellValue = mapVal.Interface()
|
||||
}
|
||||
}
|
||||
|
||||
// 特殊类型处理
|
||||
switch v := cellValue.(type) {
|
||||
case time.Time:
|
||||
if !v.IsZero() {
|
||||
f.SetCellValue(sheet, cellName, v.Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
case *time.Time:
|
||||
if v != nil && !v.IsZero() {
|
||||
f.SetCellValue(sheet, cellName, v.Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
default:
|
||||
f.SetCellValue(sheet, cellName, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
f.SetActiveSheet(index)
|
||||
// 如果创建了新Sheet且名字不叫Sheet1,删除默认的Sheet1
|
||||
if sheet != "Sheet1" {
|
||||
f.DeleteSheet("Sheet1")
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// Parse 读取Excel文件内容,返回二维字符串数组
|
||||
func Parse(r io.Reader) ([][]string, error) {
|
||||
f, err := excelize.OpenReader(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// 获取第一个工作表
|
||||
sheet := f.GetSheetName(0)
|
||||
rows, err := f.GetRows(sheet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rows, nil
|
||||
}
|
||||
@@ -5,51 +5,40 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// 全局变量
|
||||
// ============================================================================
|
||||
|
||||
// serverStartTime 记录进程启动时间(近似服务器启动时间)
|
||||
var serverStartTime = time.Now()
|
||||
|
||||
// ============================================================================
|
||||
// 公共函数
|
||||
// ============================================================================
|
||||
|
||||
// GetServerStartTime 获取服务器启动时间
|
||||
// 返回: 服务器启动的时间戳
|
||||
// - 返回进程初始化时记录的时间点
|
||||
func GetServerStartTime() time.Time {
|
||||
return serverStartTime
|
||||
}
|
||||
|
||||
// GetServerUptime 获取服务器运行时长
|
||||
// 返回: 从服务器启动到现在的时间间隔
|
||||
// GetServerUptime 获取服务器已运行时长
|
||||
// - 通过当前时间与启动时间差计算
|
||||
func GetServerUptime() time.Duration {
|
||||
return time.Since(serverStartTime)
|
||||
}
|
||||
|
||||
// GetServerUptimeString 获取服务器运行时长的字符串表示
|
||||
// 返回: 格式化的运行时长字符串(中文单位)
|
||||
// GetServerUptimeString 获取格式化的服务器运行时长字符串
|
||||
// - 返回不带小数点的时长格式,如 "1h23m45s"
|
||||
func GetServerUptimeString() string {
|
||||
duration := time.Since(serverStartTime)
|
||||
|
||||
// 获取总秒数并转换为整数
|
||||
totalSeconds := int(duration.Seconds())
|
||||
|
||||
// 计算天、小时、分钟、秒
|
||||
days := totalSeconds / 86400
|
||||
hours := (totalSeconds % 86400) / 3600
|
||||
// 计算小时、分钟、秒
|
||||
hours := totalSeconds / 3600
|
||||
minutes := (totalSeconds % 3600) / 60
|
||||
seconds := totalSeconds % 60
|
||||
|
||||
// 根据时长长度选择合适的格式
|
||||
if days > 0 {
|
||||
return fmt.Sprintf("%d天%d小时%d分钟", days, hours, minutes)
|
||||
} else if hours > 0 {
|
||||
return fmt.Sprintf("%d小时%d分钟%d秒", hours, minutes, seconds)
|
||||
if hours > 0 {
|
||||
return fmt.Sprintf("%dh%dm%ds", hours, minutes, seconds)
|
||||
} else if minutes > 0 {
|
||||
return fmt.Sprintf("%d分钟%d秒", minutes, seconds)
|
||||
return fmt.Sprintf("%dm%ds", minutes, seconds)
|
||||
} else {
|
||||
return fmt.Sprintf("%d秒", seconds)
|
||||
return fmt.Sprintf("%ds", seconds)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user