x
This commit is contained in:
148
pkg/config/config.go
Normal file
148
pkg/config/config.go
Normal file
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Copyright (c) 2023 shenjunzheng@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultConfigType = "json"
|
||||
)
|
||||
|
||||
var (
|
||||
// ConfigName holds the name of the configuration file.
|
||||
ConfigName = ""
|
||||
|
||||
// ConfigType specifies the type/format of the configuration file.
|
||||
ConfigType = ""
|
||||
|
||||
// ConfigPath denotes the path to the configuration file.
|
||||
ConfigPath = ""
|
||||
|
||||
// ERROR
|
||||
ErrInvalidDirectory = errors.New("invalid directory path")
|
||||
ErrMissingConfigName = errors.New("config name not specified")
|
||||
)
|
||||
|
||||
// Init initializes the configuration settings.
|
||||
// It sets up the name, type, and path for the configuration file.
|
||||
func Init(name, _type, path string) error {
|
||||
if len(name) == 0 {
|
||||
return ErrMissingConfigName
|
||||
}
|
||||
|
||||
if len(_type) == 0 {
|
||||
_type = DefaultConfigType
|
||||
}
|
||||
|
||||
var err error
|
||||
if len(path) == 0 {
|
||||
path, err = os.UserHomeDir()
|
||||
if err != nil {
|
||||
path = os.TempDir()
|
||||
}
|
||||
path += string(os.PathSeparator) + "." + name
|
||||
}
|
||||
if err := PrepareDir(path); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ConfigName = name
|
||||
ConfigType = _type
|
||||
ConfigPath = path
|
||||
return nil
|
||||
}
|
||||
|
||||
// Load loads the configuration from the previously initialized file.
|
||||
// It unmarshals the configuration into the provided conf interface.
|
||||
func Load(conf interface{}) error {
|
||||
viper.SetConfigName(ConfigName)
|
||||
viper.SetConfigType(ConfigType)
|
||||
viper.AddConfigPath(ConfigPath)
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
if err := viper.SafeWriteConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := viper.Unmarshal(conf); err != nil {
|
||||
return err
|
||||
}
|
||||
SetDefault(conf)
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadFile loads the configuration from a specified file.
|
||||
// It unmarshals the configuration into the provided conf interface.
|
||||
func LoadFile(file string, conf interface{}) error {
|
||||
viper.SetConfigFile(file)
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := viper.Unmarshal(conf); err != nil {
|
||||
return err
|
||||
}
|
||||
SetDefault(conf)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetConfig sets a configuration key to a specified value.
|
||||
// It also writes the updated configuration back to the file.
|
||||
func SetConfig(key string, value interface{}) error {
|
||||
viper.Set(key, value)
|
||||
if err := viper.WriteConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetConfig resets the configuration to empty.
|
||||
func ResetConfig() error {
|
||||
viper.Reset()
|
||||
viper.SetConfigName(ConfigName)
|
||||
viper.SetConfigType(ConfigType)
|
||||
viper.AddConfigPath(ConfigPath)
|
||||
return viper.WriteConfig()
|
||||
}
|
||||
|
||||
// GetConfig retrieves all configuration settings as a map.
|
||||
func GetConfig() map[string]interface{} {
|
||||
return viper.AllSettings()
|
||||
}
|
||||
|
||||
// PrepareDir ensures that the specified directory path exists.
|
||||
// If the directory does not exist, it attempts to create it.
|
||||
func PrepareDir(path string) error {
|
||||
stat, err := os.Stat(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(path, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
} else if !stat.IsDir() {
|
||||
log.Debugf("%s is not a directory", path)
|
||||
return ErrInvalidDirectory
|
||||
}
|
||||
return nil
|
||||
}
|
||||
251
pkg/config/default.go
Normal file
251
pkg/config/default.go
Normal file
@@ -0,0 +1,251 @@
|
||||
/*
|
||||
* Copyright (c) 2023 shenjunzheng@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// DefaultTag is the default tag used to identify default values for struct fields.
|
||||
var DefaultTag string
|
||||
|
||||
func init() {
|
||||
DefaultTag = "default"
|
||||
}
|
||||
|
||||
// SetDefaultTag updates the tag used to identify default values.
|
||||
func SetDefaultTag(tag string) {
|
||||
DefaultTag = tag
|
||||
}
|
||||
|
||||
// SetDefault sets the default values for a given interface{}.
|
||||
// The interface{} must be a pointer to a struct, bcs the default values are SET on the struct fields.
|
||||
func SetDefault(v interface{}) {
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
|
||||
val := reflect.ValueOf(v)
|
||||
if val.Kind() == reflect.Ptr {
|
||||
val = val.Elem()
|
||||
}
|
||||
|
||||
setDefault(val, "")
|
||||
}
|
||||
|
||||
// setDefault recursively sets default values based on the struct's tags.
|
||||
func setDefault(val reflect.Value, tag string) {
|
||||
|
||||
if !val.CanSet() {
|
||||
return
|
||||
}
|
||||
|
||||
switch val.Kind() {
|
||||
case reflect.Struct:
|
||||
handleStruct(val, tag)
|
||||
case reflect.Ptr:
|
||||
handlePtr(val, tag)
|
||||
case reflect.Interface:
|
||||
handleInterface(val, tag)
|
||||
case reflect.Map:
|
||||
handleMap(val, tag)
|
||||
case reflect.Slice, reflect.Array:
|
||||
handleSliceArray(val, tag)
|
||||
default:
|
||||
handleSimpleType(val, tag)
|
||||
}
|
||||
}
|
||||
|
||||
// handleSimpleType handles the assignment of default values for simple data types.
|
||||
func handleSimpleType(val reflect.Value, tag string) {
|
||||
|
||||
if len(tag) == 0 || !val.IsZero() || !val.CanSet() {
|
||||
return
|
||||
}
|
||||
|
||||
// Assign appropriate default values based on the data type.
|
||||
switch val.Kind() {
|
||||
case reflect.String:
|
||||
val.SetString(tag)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
if intValue, err := strconv.ParseInt(tag, 10, 64); err == nil {
|
||||
val.SetInt(intValue)
|
||||
}
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
if uintVal, err := strconv.ParseUint(tag, 10, 64); err == nil {
|
||||
val.SetUint(uintVal)
|
||||
}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
if floatValue, err := strconv.ParseFloat(tag, 64); err == nil {
|
||||
val.SetFloat(floatValue)
|
||||
}
|
||||
case reflect.Bool:
|
||||
if boolVal, err := strconv.ParseBool(tag); err == nil {
|
||||
val.SetBool(boolVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleStruct processes struct type fields and sets default values based on their tags.
|
||||
func handleStruct(val reflect.Value, tag string) {
|
||||
|
||||
if val.Kind() != reflect.Struct {
|
||||
return
|
||||
}
|
||||
|
||||
if !val.IsZero() || len(tag) == 0 {
|
||||
typ := val.Type()
|
||||
for i := 0; i < val.NumField(); i++ {
|
||||
field := val.Field(i)
|
||||
fieldType := typ.Field(i)
|
||||
setDefault(field, fieldType.Tag.Get(DefaultTag))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !val.CanSet() {
|
||||
return
|
||||
}
|
||||
|
||||
// If tag is provided, unmarshal the default value and set it.
|
||||
newStruct := reflect.New(val.Type()).Interface()
|
||||
if err := json.Unmarshal([]byte(tag), newStruct); err == nil {
|
||||
newStructVal := reflect.ValueOf(newStruct).Elem()
|
||||
for i := 0; i < newStructVal.NumField(); i++ {
|
||||
field := newStructVal.Field(i)
|
||||
fieldType := newStructVal.Type().Field(i)
|
||||
setDefault(field, fieldType.Tag.Get(DefaultTag))
|
||||
}
|
||||
val.Set(newStructVal)
|
||||
}
|
||||
}
|
||||
|
||||
// handleSliceArray sets default values for slice or array types.
|
||||
func handleSliceArray(val reflect.Value, tag string) {
|
||||
|
||||
if val.Kind() != reflect.Slice && val.Kind() != reflect.Array {
|
||||
return
|
||||
}
|
||||
|
||||
if !val.IsZero() || len(tag) == 0 {
|
||||
for i := 0; i < val.Len(); i++ {
|
||||
setDefault(val.Index(i), "")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !val.CanSet() {
|
||||
return
|
||||
}
|
||||
|
||||
// array 提前初始化子结构体,解决 default 长度小于 array 长度的问题
|
||||
if val.Kind() == reflect.Array {
|
||||
for i := 0; i < val.Len(); i++ {
|
||||
setDefault(val.Index(i), "")
|
||||
}
|
||||
}
|
||||
|
||||
// If tag is provided, unmarshal the default value and set it.
|
||||
newSlice := reflect.New(reflect.SliceOf(val.Type().Elem())).Interface()
|
||||
if err := json.Unmarshal([]byte(tag), newSlice); err == nil {
|
||||
sliceValue := reflect.ValueOf(newSlice).Elem()
|
||||
for j := 0; j < sliceValue.Len(); j++ {
|
||||
v := sliceValue.Index(j)
|
||||
setDefault(v, "")
|
||||
if val.Kind() == reflect.Array {
|
||||
if j >= val.Len() {
|
||||
return
|
||||
}
|
||||
val.Index(j).Set(v)
|
||||
} else {
|
||||
val.Set(reflect.Append(val, v))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleMap sets default values for map types.
|
||||
func handleMap(val reflect.Value, tag string) {
|
||||
|
||||
if val.Kind() != reflect.Map {
|
||||
return
|
||||
}
|
||||
|
||||
if !val.IsZero() || len(tag) == 0 {
|
||||
for _, key := range val.MapKeys() {
|
||||
setDefault(val.MapIndex(key), "")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !val.CanSet() {
|
||||
return
|
||||
}
|
||||
|
||||
// If tag is provided, unmarshal the default value and set it.
|
||||
newMap := reflect.New(val.Type()).Interface()
|
||||
if err := json.Unmarshal([]byte(tag), newMap); err == nil {
|
||||
newMapVal := reflect.ValueOf(newMap).Elem()
|
||||
for _, k := range newMapVal.MapKeys() {
|
||||
v := newMapVal.MapIndex(k)
|
||||
setDefault(v, "")
|
||||
}
|
||||
val.Set(newMapVal)
|
||||
}
|
||||
}
|
||||
|
||||
// handlePtr handles pointer types and sets default values based on their tags.
|
||||
func handlePtr(val reflect.Value, tag string) {
|
||||
|
||||
if val.Kind() != reflect.Ptr {
|
||||
return
|
||||
}
|
||||
|
||||
if !val.IsZero() || len(tag) == 0 || !val.CanSet() {
|
||||
return
|
||||
}
|
||||
|
||||
// If tag is provided, unmarshal the default value and set it.
|
||||
newPtr := reflect.New(val.Type()).Interface()
|
||||
if err := json.Unmarshal([]byte(tag), newPtr); err == nil {
|
||||
newPtrVal := reflect.ValueOf(newPtr).Elem()
|
||||
setDefault(newPtrVal, "")
|
||||
val.Set(newPtrVal)
|
||||
}
|
||||
}
|
||||
|
||||
// handleInterface processes interface types and sets default values based on their tags.
|
||||
// support pointer interface only, bcs the default values are SET on the struct fields.
|
||||
func handleInterface(val reflect.Value, tag string) {
|
||||
|
||||
if val.Kind() != reflect.Interface {
|
||||
return
|
||||
}
|
||||
|
||||
if val.IsNil() {
|
||||
return
|
||||
}
|
||||
|
||||
if val.Elem().Kind() != reflect.Ptr {
|
||||
return
|
||||
}
|
||||
|
||||
// Recursively set the default for the inner value of the interface.
|
||||
setDefault(reflect.ValueOf(val.Elem().Interface()).Elem(), tag)
|
||||
}
|
||||
25
pkg/dllver/version.go
Normal file
25
pkg/dllver/version.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package dllver
|
||||
|
||||
type Info struct {
|
||||
FilePath string `json:"file_path"`
|
||||
CompanyName string `json:"company_name"`
|
||||
FileDescription string `json:"file_description"`
|
||||
FileVersion string `json:"file_version"`
|
||||
FileMajorVersion int `json:"file_major_version"`
|
||||
LegalCopyright string `json:"legal_copyright"`
|
||||
ProductName string `json:"product_name"`
|
||||
ProductVersion string `json:"product_version"`
|
||||
}
|
||||
|
||||
func New(filePath string) (*Info, error) {
|
||||
i := &Info{
|
||||
FilePath: filePath,
|
||||
}
|
||||
|
||||
err := i.initialize()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return i, nil
|
||||
}
|
||||
7
pkg/dllver/version_others.go
Normal file
7
pkg/dllver/version_others.go
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build !windows
|
||||
|
||||
package dllver
|
||||
|
||||
func (i *Info) initialize() error {
|
||||
return nil
|
||||
}
|
||||
142
pkg/dllver/version_windows.go
Normal file
142
pkg/dllver/version_windows.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package dllver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var (
|
||||
modversion = syscall.NewLazyDLL("version.dll")
|
||||
procGetFileVersionInfoSize = modversion.NewProc("GetFileVersionInfoSizeW")
|
||||
procGetFileVersionInfo = modversion.NewProc("GetFileVersionInfoW")
|
||||
procVerQueryValue = modversion.NewProc("VerQueryValueW")
|
||||
)
|
||||
|
||||
// VS_FIXEDFILEINFO 结构体
|
||||
type VS_FIXEDFILEINFO struct {
|
||||
Signature uint32
|
||||
StrucVersion uint32
|
||||
FileVersionMS uint32
|
||||
FileVersionLS uint32
|
||||
ProductVersionMS uint32
|
||||
ProductVersionLS uint32
|
||||
FileFlagsMask uint32
|
||||
FileFlags uint32
|
||||
FileOS uint32
|
||||
FileType uint32
|
||||
FileSubtype uint32
|
||||
FileDateMS uint32
|
||||
FileDateLS uint32
|
||||
}
|
||||
|
||||
// initialize 初始化版本信息
|
||||
func (i *Info) initialize() error {
|
||||
// 转换路径为 UTF16
|
||||
pathPtr, err := syscall.UTF16PtrFromString(i.FilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 获取版本信息大小
|
||||
var handle uintptr
|
||||
size, _, err := procGetFileVersionInfoSize.Call(
|
||||
uintptr(unsafe.Pointer(pathPtr)),
|
||||
uintptr(unsafe.Pointer(&handle)),
|
||||
)
|
||||
if size == 0 {
|
||||
return fmt.Errorf("GetFileVersionInfoSize failed: %v", err)
|
||||
}
|
||||
|
||||
// 分配内存
|
||||
verInfo := make([]byte, size)
|
||||
ret, _, err := procGetFileVersionInfo.Call(
|
||||
uintptr(unsafe.Pointer(pathPtr)),
|
||||
0,
|
||||
size,
|
||||
uintptr(unsafe.Pointer(&verInfo[0])),
|
||||
)
|
||||
if ret == 0 {
|
||||
return fmt.Errorf("GetFileVersionInfo failed: %v", err)
|
||||
}
|
||||
|
||||
// 获取固定的文件信息
|
||||
var fixedFileInfo *VS_FIXEDFILEINFO
|
||||
var uLen uint32
|
||||
rootPtr, _ := syscall.UTF16PtrFromString("\\")
|
||||
ret, _, err = procVerQueryValue.Call(
|
||||
uintptr(unsafe.Pointer(&verInfo[0])),
|
||||
uintptr(unsafe.Pointer(rootPtr)),
|
||||
uintptr(unsafe.Pointer(&fixedFileInfo)),
|
||||
uintptr(unsafe.Pointer(&uLen)),
|
||||
)
|
||||
if ret == 0 {
|
||||
return fmt.Errorf("VerQueryValue failed: %v", err)
|
||||
}
|
||||
|
||||
// 解析文件版本
|
||||
i.FileVersion = fmt.Sprintf("%d.%d.%d.%d",
|
||||
(fixedFileInfo.FileVersionMS>>16)&0xffff,
|
||||
(fixedFileInfo.FileVersionMS>>0)&0xffff,
|
||||
(fixedFileInfo.FileVersionLS>>16)&0xffff,
|
||||
(fixedFileInfo.FileVersionLS>>0)&0xffff,
|
||||
)
|
||||
i.FileMajorVersion = int((fixedFileInfo.FileVersionMS >> 16) & 0xffff)
|
||||
|
||||
i.ProductVersion = fmt.Sprintf("%d.%d.%d.%d",
|
||||
(fixedFileInfo.ProductVersionMS>>16)&0xffff,
|
||||
(fixedFileInfo.ProductVersionMS>>0)&0xffff,
|
||||
(fixedFileInfo.ProductVersionLS>>16)&0xffff,
|
||||
(fixedFileInfo.ProductVersionLS>>0)&0xffff,
|
||||
)
|
||||
|
||||
// 获取翻译信息
|
||||
type langAndCodePage struct {
|
||||
language uint16
|
||||
codePage uint16
|
||||
}
|
||||
|
||||
var lpTranslate *langAndCodePage
|
||||
var cbTranslate uint32
|
||||
transPtr, _ := syscall.UTF16PtrFromString("\\VarFileInfo\\Translation")
|
||||
ret, _, _ = procVerQueryValue.Call(
|
||||
uintptr(unsafe.Pointer(&verInfo[0])),
|
||||
uintptr(unsafe.Pointer(transPtr)),
|
||||
uintptr(unsafe.Pointer(&lpTranslate)),
|
||||
uintptr(unsafe.Pointer(&cbTranslate)),
|
||||
)
|
||||
|
||||
if ret != 0 && cbTranslate > 0 {
|
||||
// 获取所有需要的字符串信息
|
||||
stringInfos := map[string]*string{
|
||||
"CompanyName": &i.CompanyName,
|
||||
"FileDescription": &i.FileDescription,
|
||||
"FileVersion": &i.FileVersion,
|
||||
"LegalCopyright": &i.LegalCopyright,
|
||||
"ProductName": &i.ProductName,
|
||||
"ProductVersion": &i.ProductVersion,
|
||||
}
|
||||
|
||||
for name, ptr := range stringInfos {
|
||||
subBlock := fmt.Sprintf("\\StringFileInfo\\%04x%04x\\%s",
|
||||
lpTranslate.language, lpTranslate.codePage, name)
|
||||
|
||||
subBlockPtr, _ := syscall.UTF16PtrFromString(subBlock)
|
||||
var buffer *uint16
|
||||
var bufLen uint32
|
||||
|
||||
ret, _, _ = procVerQueryValue.Call(
|
||||
uintptr(unsafe.Pointer(&verInfo[0])),
|
||||
uintptr(unsafe.Pointer(subBlockPtr)),
|
||||
uintptr(unsafe.Pointer(&buffer)),
|
||||
uintptr(unsafe.Pointer(&bufLen)),
|
||||
)
|
||||
|
||||
if ret != 0 && bufLen > 0 {
|
||||
*ptr = syscall.UTF16ToString((*[1 << 20]uint16)(unsafe.Pointer(buffer))[:bufLen:bufLen])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
142
pkg/model/chatroom.go
Normal file
142
pkg/model/chatroom.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/sjzar/chatlog/pkg/model/wxproto"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type ChatRoom struct {
|
||||
Name string `json:"name"`
|
||||
Owner string `json:"owner"`
|
||||
Users []ChatRoomUser `json:"users"`
|
||||
|
||||
// Extra From Contact
|
||||
Remark string `json:"remark"`
|
||||
NickName string `json:"nickName"`
|
||||
|
||||
User2DisplayName map[string]string `json:"-"`
|
||||
}
|
||||
|
||||
type ChatRoomUser struct {
|
||||
UserName string `json:"userName"`
|
||||
DisplayName string `json:"displayName"`
|
||||
}
|
||||
|
||||
// CREATE TABLE ChatRoom(
|
||||
// ChatRoomName TEXT PRIMARY KEY,
|
||||
// UserNameList TEXT,
|
||||
// DisplayNameList TEXT,
|
||||
// ChatRoomFlag int Default 0,
|
||||
// Owner INTEGER DEFAULT 0,
|
||||
// IsShowName INTEGER DEFAULT 0,
|
||||
// SelfDisplayName TEXT,
|
||||
// Reserved1 INTEGER DEFAULT 0,
|
||||
// Reserved2 TEXT,
|
||||
// Reserved3 INTEGER DEFAULT 0,
|
||||
// Reserved4 TEXT,
|
||||
// Reserved5 INTEGER DEFAULT 0,
|
||||
// Reserved6 TEXT,
|
||||
// RoomData BLOB,
|
||||
// Reserved7 INTEGER DEFAULT 0,
|
||||
// Reserved8 TEXT
|
||||
// )
|
||||
type ChatRoomV3 struct {
|
||||
ChatRoomName string `json:"ChatRoomName"`
|
||||
Reserved2 string `json:"Reserved2"` // Creator
|
||||
RoomData []byte `json:"RoomData"`
|
||||
|
||||
// // 非关键信息,暂时忽略
|
||||
// UserNameList string `json:"UserNameList"`
|
||||
// DisplayNameList string `json:"DisplayNameList"`
|
||||
// ChatRoomFlag int `json:"ChatRoomFlag"`
|
||||
// Owner int `json:"Owner"`
|
||||
// IsShowName int `json:"IsShowName"`
|
||||
// SelfDisplayName string `json:"SelfDisplayName"`
|
||||
// Reserved1 int `json:"Reserved1"`
|
||||
// Reserved3 int `json:"Reserved3"`
|
||||
// Reserved4 string `json:"Reserved4"`
|
||||
// Reserved5 int `json:"Reserved5"`
|
||||
// Reserved6 string `json:"Reserved6"`
|
||||
// Reserved7 int `json:"Reserved7"`
|
||||
// Reserved8 string `json:"Reserved8"`
|
||||
}
|
||||
|
||||
func (c *ChatRoomV3) Wrap() *ChatRoom {
|
||||
|
||||
var users []ChatRoomUser
|
||||
if len(c.RoomData) != 0 {
|
||||
users = ParseRoomData(c.RoomData)
|
||||
}
|
||||
|
||||
user2DisplayName := make(map[string]string, len(users))
|
||||
for _, user := range users {
|
||||
if user.DisplayName != "" {
|
||||
user2DisplayName[user.UserName] = user.DisplayName
|
||||
}
|
||||
}
|
||||
|
||||
return &ChatRoom{
|
||||
Name: c.ChatRoomName,
|
||||
Owner: c.Reserved2,
|
||||
Users: users,
|
||||
User2DisplayName: user2DisplayName,
|
||||
}
|
||||
}
|
||||
|
||||
// CREATE TABLE chat_room(
|
||||
// id INTEGER PRIMARY KEY,
|
||||
// username TEXT,
|
||||
// owner TEXT,
|
||||
// ext_buffer BLOB
|
||||
// )
|
||||
type ChatRoomV4 struct {
|
||||
ID int `json:"id"`
|
||||
UserName string `json:"username"`
|
||||
Owner string `json:"owner"`
|
||||
ExtBuffer []byte `json:"ext_buffer"`
|
||||
}
|
||||
|
||||
func (c *ChatRoomV4) Wrap() *ChatRoom {
|
||||
|
||||
var users []ChatRoomUser
|
||||
if len(c.ExtBuffer) != 0 {
|
||||
users = ParseRoomData(c.ExtBuffer)
|
||||
}
|
||||
|
||||
return &ChatRoom{
|
||||
Name: c.UserName,
|
||||
Owner: c.Owner,
|
||||
Users: users,
|
||||
}
|
||||
}
|
||||
|
||||
func ParseRoomData(b []byte) (users []ChatRoomUser) {
|
||||
var pbMsg wxproto.RoomData
|
||||
if err := proto.Unmarshal(b, &pbMsg); err != nil {
|
||||
return
|
||||
}
|
||||
if pbMsg.Users == nil {
|
||||
return
|
||||
}
|
||||
|
||||
users = make([]ChatRoomUser, 0, len(pbMsg.Users))
|
||||
for _, user := range pbMsg.Users {
|
||||
u := ChatRoomUser{UserName: user.UserName}
|
||||
if user.DisplayName != nil {
|
||||
u.DisplayName = *user.DisplayName
|
||||
}
|
||||
users = append(users, u)
|
||||
}
|
||||
return users
|
||||
}
|
||||
|
||||
func (c *ChatRoom) DisplayName() string {
|
||||
switch {
|
||||
case c.Remark != "":
|
||||
return c.Remark
|
||||
case c.NickName != "":
|
||||
return c.NickName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
158
pkg/model/contact.go
Normal file
158
pkg/model/contact.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package model
|
||||
|
||||
type Contact struct {
|
||||
UserName string `json:"userName"`
|
||||
Alias string `json:"alias"`
|
||||
Remark string `json:"remark"`
|
||||
NickName string `json:"nickName"`
|
||||
IsFriend bool `json:"isFriend"`
|
||||
}
|
||||
|
||||
// CREATE TABLE Contact(
|
||||
// UserName TEXT PRIMARY KEY ,
|
||||
// Alias TEXT,
|
||||
// EncryptUserName TEXT,
|
||||
// DelFlag INTEGER DEFAULT 0,
|
||||
// Type INTEGER DEFAULT 0,
|
||||
// VerifyFlag INTEGER DEFAULT 0,
|
||||
// Reserved1 INTEGER DEFAULT 0,
|
||||
// Reserved2 INTEGER DEFAULT 0,
|
||||
// Reserved3 TEXT,
|
||||
// Reserved4 TEXT,
|
||||
// Remark TEXT,
|
||||
// NickName TEXT,
|
||||
// LabelIDList TEXT,
|
||||
// DomainList TEXT,
|
||||
// ChatRoomType int,
|
||||
// PYInitial TEXT,
|
||||
// QuanPin TEXT,
|
||||
// RemarkPYInitial TEXT,
|
||||
// RemarkQuanPin TEXT,
|
||||
// BigHeadImgUrl TEXT,
|
||||
// SmallHeadImgUrl TEXT,
|
||||
// HeadImgMd5 TEXT,
|
||||
// ChatRoomNotify INTEGER DEFAULT 0,
|
||||
// Reserved5 INTEGER DEFAULT 0,
|
||||
// Reserved6 TEXT,
|
||||
// Reserved7 TEXT,
|
||||
// ExtraBuf BLOB,
|
||||
// Reserved8 INTEGER DEFAULT 0,
|
||||
// Reserved9 INTEGER DEFAULT 0,
|
||||
// Reserved10 TEXT,
|
||||
// Reserved11 TEXT
|
||||
// )
|
||||
type ContactV3 struct {
|
||||
UserName string `json:"UserName"`
|
||||
Alias string `json:"Alias"`
|
||||
Remark string `json:"Remark"`
|
||||
NickName string `json:"NickName"`
|
||||
Reserved1 int `json:"Reserved1"` // 1 自己好友或自己加入的群聊; 0 群聊成员(非好友)
|
||||
|
||||
// EncryptUserName string `json:"EncryptUserName"`
|
||||
// DelFlag int `json:"DelFlag"`
|
||||
// Type int `json:"Type"`
|
||||
// VerifyFlag int `json:"VerifyFlag"`
|
||||
// Reserved2 int `json:"Reserved2"`
|
||||
// Reserved3 string `json:"Reserved3"`
|
||||
// Reserved4 string `json:"Reserved4"`
|
||||
// LabelIDList string `json:"LabelIDList"`
|
||||
// DomainList string `json:"DomainList"`
|
||||
// ChatRoomType int `json:"ChatRoomType"`
|
||||
// PYInitial string `json:"PYInitial"`
|
||||
// QuanPin string `json:"QuanPin"`
|
||||
// RemarkPYInitial string `json:"RemarkPYInitial"`
|
||||
// RemarkQuanPin string `json:"RemarkQuanPin"`
|
||||
// BigHeadImgUrl string `json:"BigHeadImgUrl"`
|
||||
// SmallHeadImgUrl string `json:"SmallHeadImgUrl"`
|
||||
// HeadImgMd5 string `json:"HeadImgMd5"`
|
||||
// ChatRoomNotify int `json:"ChatRoomNotify"`
|
||||
// Reserved5 int `json:"Reserved5"`
|
||||
// Reserved6 string `json:"Reserved6"`
|
||||
// Reserved7 string `json:"Reserved7"`
|
||||
// ExtraBuf []byte `json:"ExtraBuf"`
|
||||
// Reserved8 int `json:"Reserved8"`
|
||||
// Reserved9 int `json:"Reserved9"`
|
||||
// Reserved10 string `json:"Reserved10"`
|
||||
// Reserved11 string `json:"Reserved11"`
|
||||
}
|
||||
|
||||
func (c *ContactV3) Wrap() *Contact {
|
||||
return &Contact{
|
||||
UserName: c.UserName,
|
||||
Alias: c.Alias,
|
||||
Remark: c.Remark,
|
||||
NickName: c.NickName,
|
||||
IsFriend: c.Reserved1 == 1,
|
||||
}
|
||||
}
|
||||
|
||||
// CREATE TABLE contact(
|
||||
// id INTEGER PRIMARY KEY,
|
||||
// username TEXT,
|
||||
// local_type INTEGER,
|
||||
// alias TEXT,
|
||||
// encrypt_username TEXT,
|
||||
// flag INTEGER,
|
||||
// delete_flag INTEGER,
|
||||
// verify_flag INTEGER,
|
||||
// remark TEXT,
|
||||
// remark_quan_pin TEXT,
|
||||
// remark_pin_yin_initial TEXT,
|
||||
// nick_name TEXT,
|
||||
// pin_yin_initial TEXT,
|
||||
// quan_pin TEXT,
|
||||
// big_head_url TEXT,
|
||||
// small_head_url TEXT,
|
||||
// head_img_md5 TEXT,
|
||||
// chat_room_notify INTEGER,
|
||||
// is_in_chat_room INTEGER,
|
||||
// description TEXT,
|
||||
// extra_buffer BLOB,
|
||||
// chat_room_type INTEGER
|
||||
// )
|
||||
type ContactV4 struct {
|
||||
UserName string `json:"username"`
|
||||
Alias string `json:"alias"`
|
||||
Remark string `json:"remark"`
|
||||
NickName string `json:"nick_name"`
|
||||
LocalType int `json:"local_type"` // 2 群聊; 3 群聊成员(非好友); 5,6 企业微信;
|
||||
|
||||
// ID int `json:"id"`
|
||||
|
||||
// EncryptUserName string `json:"encrypt_username"`
|
||||
// Flag int `json:"flag"`
|
||||
// DeleteFlag int `json:"delete_flag"`
|
||||
// VerifyFlag int `json:"verify_flag"`
|
||||
// RemarkQuanPin string `json:"remark_quan_pin"`
|
||||
// RemarkPinYinInitial string `json:"remark_pin_yin_initial"`
|
||||
// PinYinInitial string `json:"pin_yin_initial"`
|
||||
// QuanPin string `json:"quan_pin"`
|
||||
// BigHeadUrl string `json:"big_head_url"`
|
||||
// SmallHeadUrl string `json:"small_head_url"`
|
||||
// HeadImgMd5 string `json:"head_img_md5"`
|
||||
// ChatRoomNotify int `json:"chat_room_notify"`
|
||||
// IsInChatRoom int `json:"is_in_chat_room"`
|
||||
// Description string `json:"description"`
|
||||
// ExtraBuffer []byte `json:"extra_buffer"`
|
||||
// ChatRoomType int `json:"chat_room_type"`
|
||||
}
|
||||
|
||||
func (c *ContactV4) Wrap() *Contact {
|
||||
return &Contact{
|
||||
UserName: c.UserName,
|
||||
Alias: c.Alias,
|
||||
Remark: c.Remark,
|
||||
NickName: c.NickName,
|
||||
IsFriend: c.LocalType != 3,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Contact) DisplayName() string {
|
||||
switch {
|
||||
case c.Remark != "":
|
||||
return c.Remark
|
||||
case c.NickName != "":
|
||||
return c.NickName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
301
pkg/model/message.go
Normal file
301
pkg/model/message.go
Normal file
@@ -0,0 +1,301 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sjzar/chatlog/pkg/model/wxproto"
|
||||
"github.com/sjzar/chatlog/pkg/util"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const (
|
||||
// Source
|
||||
WeChatV3 = "wechatv3"
|
||||
WeChatV4 = "wechatv4"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Sequence int64 `json:"sequence"` // 消息序号,10位时间戳 + 3位序号
|
||||
CreateTime time.Time `json:"createTime"` // 消息创建时间,10位时间戳
|
||||
TalkerID int `json:"talkerID"` // 聊天对象,Name2ID 表序号,索引值
|
||||
Talker string `json:"talker"` // 聊天对象,微信 ID or 群 ID
|
||||
IsSender int `json:"isSender"` // 是否为发送消息,0 接收消息,1 发送消息
|
||||
Type int `json:"type"` // 消息类型
|
||||
SubType int `json:"subType"` // 消息子类型
|
||||
Content string `json:"content"` // 消息内容,文字聊天内容 或 XML
|
||||
CompressContent []byte `json:"compressContent"` // 非文字聊天内容,如图片、语音、视频等
|
||||
IsChatRoom bool `json:"isChatRoom"` // 是否为群聊消息
|
||||
ChatRoomSender string `json:"chatRoomSender"` // 群聊消息发送人
|
||||
|
||||
// Fill Info
|
||||
// 从联系人等信息中填充
|
||||
DisplayName string `json:"-"` // 显示名称
|
||||
CharRoomName string `json:"-"` // 群聊名称
|
||||
|
||||
Version string `json:"-"` // 消息版本,内部判断
|
||||
}
|
||||
|
||||
// CREATE TABLE MSG (
|
||||
// localId INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
// TalkerId INT DEFAULT 0,
|
||||
// MsgSvrID INT,
|
||||
// Type INT,
|
||||
// SubType INT,
|
||||
// IsSender INT,
|
||||
// CreateTime INT,
|
||||
// Sequence INT DEFAULT 0,
|
||||
// StatusEx INT DEFAULT 0,
|
||||
// FlagEx INT,
|
||||
// Status INT,
|
||||
// MsgServerSeq INT,
|
||||
// MsgSequence INT,
|
||||
// StrTalker TEXT,
|
||||
// StrContent TEXT,
|
||||
// DisplayContent TEXT,
|
||||
// Reserved0 INT DEFAULT 0,
|
||||
// Reserved1 INT DEFAULT 0,
|
||||
// Reserved2 INT DEFAULT 0,
|
||||
// Reserved3 INT DEFAULT 0,
|
||||
// Reserved4 TEXT,
|
||||
// Reserved5 TEXT,
|
||||
// Reserved6 TEXT,
|
||||
// CompressContent BLOB,
|
||||
// BytesExtra BLOB,
|
||||
// BytesTrans BLOB
|
||||
// )
|
||||
type MessageV3 struct {
|
||||
Sequence int64 `json:"Sequence"` // 消息序号,10位时间戳 + 3位序号
|
||||
CreateTime int64 `json:"CreateTime"` // 消息创建时间,10位时间戳
|
||||
TalkerID int `json:"TalkerId"` // 聊天对象,Name2ID 表序号,索引值
|
||||
StrTalker string `json:"StrTalker"` // 聊天对象,微信 ID or 群 ID
|
||||
IsSender int `json:"IsSender"` // 是否为发送消息,0 接收消息,1 发送消息
|
||||
Type int `json:"Type"` // 消息类型
|
||||
SubType int `json:"SubType"` // 消息子类型
|
||||
StrContent string `json:"StrContent"` // 消息内容,文字聊天内容 或 XML
|
||||
CompressContent []byte `json:"CompressContent"` // 非文字聊天内容,如图片、语音、视频等
|
||||
BytesExtra []byte `json:"BytesExtra"` // protobuf 额外数据,记录群聊发送人等信息
|
||||
|
||||
// 非关键信息,后续有需要再加入
|
||||
// LocalID int64 `json:"localId"`
|
||||
// MsgSvrID int64 `json:"MsgSvrID"`
|
||||
// StatusEx int `json:"StatusEx"`
|
||||
// FlagEx int `json:"FlagEx"`
|
||||
// Status int `json:"Status"`
|
||||
// MsgServerSeq int64 `json:"MsgServerSeq"`
|
||||
// MsgSequence int64 `json:"MsgSequence"`
|
||||
// DisplayContent string `json:"DisplayContent"`
|
||||
// Reserved0 int `json:"Reserved0"`
|
||||
// Reserved1 int `json:"Reserved1"`
|
||||
// Reserved2 int `json:"Reserved2"`
|
||||
// Reserved3 int `json:"Reserved3"`
|
||||
// Reserved4 string `json:"Reserved4"`
|
||||
// Reserved5 string `json:"Reserved5"`
|
||||
// Reserved6 string `json:"Reserved6"`
|
||||
// BytesTrans []byte `json:"BytesTrans"`
|
||||
}
|
||||
|
||||
func (m *MessageV3) Wrap() *Message {
|
||||
|
||||
isChatRoom := strings.HasSuffix(m.StrTalker, "@chatroom")
|
||||
|
||||
var chatRoomSender string
|
||||
if len(m.BytesExtra) != 0 && isChatRoom {
|
||||
chatRoomSender = ParseBytesExtra(m.BytesExtra)
|
||||
}
|
||||
|
||||
return &Message{
|
||||
Sequence: m.Sequence,
|
||||
CreateTime: time.Unix(m.CreateTime, 0),
|
||||
TalkerID: m.TalkerID,
|
||||
Talker: m.StrTalker,
|
||||
IsSender: m.IsSender,
|
||||
Type: m.Type,
|
||||
SubType: m.SubType,
|
||||
Content: m.StrContent,
|
||||
CompressContent: m.CompressContent,
|
||||
IsChatRoom: isChatRoom,
|
||||
ChatRoomSender: chatRoomSender,
|
||||
Version: WeChatV3,
|
||||
}
|
||||
}
|
||||
|
||||
// CREATE TABLE Msg_xxxxxxxxxxxx(
|
||||
// local_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
// server_id INTEGER,
|
||||
// local_type INTEGER,
|
||||
// sort_seq INTEGER,
|
||||
// real_sender_id INTEGER,
|
||||
// create_time INTEGER,
|
||||
// status INTEGER,
|
||||
// upload_status INTEGER,
|
||||
// download_status INTEGER,
|
||||
// server_seq INTEGER,
|
||||
// origin_source INTEGER,
|
||||
// source TEXT,
|
||||
// message_content TEXT,
|
||||
// compress_content TEXT,
|
||||
// packed_info_data BLOB,
|
||||
// WCDB_CT_message_content INTEGER DEFAULT NULL,
|
||||
// WCDB_CT_source INTEGER DEFAULT NULL
|
||||
// )
|
||||
type MessageV4 struct {
|
||||
SortSeq int64 `json:"sort_seq"` // 消息序号,10位时间戳 + 3位序号
|
||||
LocalType int `json:"local_type"` // 消息类型
|
||||
RealSenderID int `json:"real_sender_id"` // 发送人 ID,对应 Name2Id 表序号
|
||||
CreateTime int64 `json:"create_time"` // 消息创建时间,10位时间戳
|
||||
MessageContent []byte `json:"message_content"` // 消息内容,文字聊天内容 或 zstd 压缩内容
|
||||
PackedInfoData []byte `json:"packed_info_data"` // 额外数据,类似 proto,格式与 v3 有差异
|
||||
Status int `json:"status"` // 消息状态,2 是已发送,4 是已接收,可以用于判断 IsSender(猜测)
|
||||
|
||||
// 非关键信息,后续有需要再加入
|
||||
// LocalID int `json:"local_id"`
|
||||
// ServerID int64 `json:"server_id"`
|
||||
|
||||
// UploadStatus int `json:"upload_status"`
|
||||
// DownloadStatus int `json:"download_status"`
|
||||
// ServerSeq int `json:"server_seq"`
|
||||
// OriginSource int `json:"origin_source"`
|
||||
// Source string `json:"source"`
|
||||
// CompressContent string `json:"compress_content"`
|
||||
}
|
||||
|
||||
func (m *MessageV4) Wrap(id2Name map[int]string, isChatRoom bool) *Message {
|
||||
|
||||
_m := &Message{
|
||||
Sequence: m.SortSeq,
|
||||
CreateTime: time.Unix(m.CreateTime, 0),
|
||||
TalkerID: m.RealSenderID, // 依赖 Name2Id 表进行转换为 StrTalker
|
||||
CompressContent: m.PackedInfoData,
|
||||
Type: m.LocalType,
|
||||
Version: WeChatV4,
|
||||
}
|
||||
|
||||
if name, ok := id2Name[m.RealSenderID]; ok {
|
||||
_m.Talker = name
|
||||
}
|
||||
|
||||
if m.Status == 2 {
|
||||
_m.IsSender = 1
|
||||
}
|
||||
|
||||
if util.IsNormalString(m.MessageContent) {
|
||||
_m.Content = string(m.MessageContent)
|
||||
} else {
|
||||
_m.CompressContent = m.MessageContent
|
||||
}
|
||||
|
||||
if isChatRoom {
|
||||
_m.IsChatRoom = true
|
||||
split := strings.Split(_m.Content, "\n")
|
||||
if len(split) > 1 {
|
||||
_m.Content = split[1]
|
||||
_m.ChatRoomSender = strings.TrimSuffix(split[0], ":")
|
||||
}
|
||||
}
|
||||
|
||||
return _m
|
||||
}
|
||||
|
||||
// ParseBytesExtra 解析额外数据
|
||||
// 按需解析
|
||||
func ParseBytesExtra(b []byte) (chatRoomSender string) {
|
||||
var pbMsg wxproto.BytesExtra
|
||||
if err := proto.Unmarshal(b, &pbMsg); err != nil {
|
||||
return
|
||||
}
|
||||
if pbMsg.Items == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, item := range pbMsg.Items {
|
||||
if item.Type == 1 {
|
||||
return item.Value
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (m *Message) PlainText(showChatRoom bool) string {
|
||||
buf := strings.Builder{}
|
||||
|
||||
talker := m.Talker
|
||||
if m.IsSender == 1 {
|
||||
talker = "我"
|
||||
} else if m.IsChatRoom {
|
||||
talker = m.ChatRoomSender
|
||||
}
|
||||
if m.DisplayName != "" {
|
||||
buf.WriteString(m.DisplayName)
|
||||
buf.WriteString("(")
|
||||
buf.WriteString(talker)
|
||||
buf.WriteString(")")
|
||||
} else {
|
||||
buf.WriteString(talker)
|
||||
}
|
||||
buf.WriteString(" ")
|
||||
|
||||
if m.IsChatRoom && showChatRoom {
|
||||
buf.WriteString("[")
|
||||
if m.CharRoomName != "" {
|
||||
buf.WriteString(m.CharRoomName)
|
||||
buf.WriteString("(")
|
||||
buf.WriteString(m.Talker)
|
||||
buf.WriteString(")")
|
||||
} else {
|
||||
buf.WriteString(m.Talker)
|
||||
}
|
||||
buf.WriteString("] ")
|
||||
}
|
||||
|
||||
buf.WriteString(m.CreateTime.Format("2006-01-02 15:04:05"))
|
||||
buf.WriteString("\n")
|
||||
|
||||
switch m.Type {
|
||||
case 1:
|
||||
buf.WriteString(m.Content)
|
||||
case 3:
|
||||
buf.WriteString("[图片]")
|
||||
case 34:
|
||||
buf.WriteString("[语音]")
|
||||
case 43:
|
||||
buf.WriteString("[视频]")
|
||||
case 47:
|
||||
buf.WriteString("[动画表情]")
|
||||
case 49:
|
||||
switch m.SubType {
|
||||
case 6:
|
||||
buf.WriteString("[文件]")
|
||||
case 8:
|
||||
buf.WriteString("[GIF表情]")
|
||||
case 19:
|
||||
buf.WriteString("[合并转发]")
|
||||
case 33, 36:
|
||||
buf.WriteString("[小程序]")
|
||||
case 57:
|
||||
buf.WriteString("[引用]")
|
||||
case 63:
|
||||
buf.WriteString("[视频号]")
|
||||
case 87:
|
||||
buf.WriteString("[群公告]")
|
||||
case 2000:
|
||||
buf.WriteString("[转账]")
|
||||
case 2003:
|
||||
buf.WriteString("[红包封面]")
|
||||
default:
|
||||
buf.WriteString("[分享]")
|
||||
}
|
||||
case 50:
|
||||
buf.WriteString("[语音通话]")
|
||||
case 10000:
|
||||
buf.WriteString("[系统消息]")
|
||||
default:
|
||||
buf.WriteString(fmt.Sprintf("Type: %d Content: %s", m.Type, m.Content))
|
||||
}
|
||||
buf.WriteString("\n")
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
133
pkg/model/session.go
Normal file
133
pkg/model/session.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
UserName string `json:"userName"`
|
||||
NOrder int `json:"nOrder"`
|
||||
NickName string `json:"nickName"`
|
||||
Content string `json:"content"`
|
||||
NTime time.Time `json:"nTime"`
|
||||
}
|
||||
|
||||
// CREATE TABLE Session(
|
||||
// strUsrName TEXT PRIMARY KEY,
|
||||
// nOrder INT DEFAULT 0,
|
||||
// nUnReadCount INTEGER DEFAULT 0,
|
||||
// parentRef TEXT,
|
||||
// Reserved0 INTEGER DEFAULT 0,
|
||||
// Reserved1 TEXT,
|
||||
// strNickName TEXT,
|
||||
// nStatus INTEGER,
|
||||
// nIsSend INTEGER,
|
||||
// strContent TEXT,
|
||||
// nMsgType INTEGER,
|
||||
// nMsgLocalID INTEGER,
|
||||
// nMsgStatus INTEGER,
|
||||
// nTime INTEGER,
|
||||
// editContent TEXT,
|
||||
// othersAtMe INT,
|
||||
// Reserved2 INTEGER DEFAULT 0,
|
||||
// Reserved3 TEXT,
|
||||
// Reserved4 INTEGER DEFAULT 0,
|
||||
// Reserved5 TEXT,
|
||||
// bytesXml BLOB
|
||||
// )
|
||||
type SessionV3 struct {
|
||||
StrUsrName string `json:"strUsrName"`
|
||||
NOrder int `json:"nOrder"`
|
||||
StrNickName string `json:"strNickName"`
|
||||
StrContent string `json:"strContent"`
|
||||
NTime int64 `json:"nTime"`
|
||||
|
||||
// NUnReadCount int `json:"nUnReadCount"`
|
||||
// ParentRef string `json:"parentRef"`
|
||||
// Reserved0 int `json:"Reserved0"`
|
||||
// Reserved1 string `json:"Reserved1"`
|
||||
// NStatus int `json:"nStatus"`
|
||||
// NIsSend int `json:"nIsSend"`
|
||||
// NMsgType int `json:"nMsgType"`
|
||||
// NMsgLocalID int `json:"nMsgLocalID"`
|
||||
// NMsgStatus int `json:"nMsgStatus"`
|
||||
// EditContent string `json:"editContent"`
|
||||
// OthersAtMe int `json:"othersAtMe"`
|
||||
// Reserved2 int `json:"Reserved2"`
|
||||
// Reserved3 string `json:"Reserved3"`
|
||||
// Reserved4 int `json:"Reserved4"`
|
||||
// Reserved5 string `json:"Reserved5"`
|
||||
// BytesXml string `json:"bytesXml"`
|
||||
}
|
||||
|
||||
func (s *SessionV3) Wrap() *Session {
|
||||
return &Session{
|
||||
UserName: s.StrUsrName,
|
||||
NOrder: s.NOrder,
|
||||
NickName: s.StrNickName,
|
||||
Content: s.StrContent,
|
||||
NTime: time.Unix(int64(s.NTime), 0),
|
||||
}
|
||||
}
|
||||
|
||||
// 注意,v4 session 是独立数据库文件
|
||||
// CREATE TABLE SessionTable(
|
||||
// username TEXT PRIMARY KEY,
|
||||
// type INTEGER,
|
||||
// unread_count INTEGER,
|
||||
// unread_first_msg_srv_id INTEGER,
|
||||
// is_hidden INTEGER,
|
||||
// summary TEXT,
|
||||
// draft TEXT,
|
||||
// status INTEGER,
|
||||
// last_timestamp INTEGER,
|
||||
// sort_timestamp INTEGER,
|
||||
// last_clear_unread_timestamp INTEGER,
|
||||
// last_msg_locald_id INTEGER,
|
||||
// last_msg_type INTEGER,
|
||||
// last_msg_sub_type INTEGER,
|
||||
// last_msg_sender TEXT,
|
||||
// last_sender_display_name TEXT,
|
||||
// last_msg_ext_type INTEGER
|
||||
// )
|
||||
type SessionV4 struct {
|
||||
Username string `json:"username"`
|
||||
Summary string `json:"summary"`
|
||||
LastTimestamp int `json:"last_timestamp"`
|
||||
LastMsgSender string `json:"last_msg_sender"`
|
||||
LastSenderDisplayName string `json:"last_sender_display_name"`
|
||||
|
||||
// Type int `json:"type"`
|
||||
// UnreadCount int `json:"unread_count"`
|
||||
// UnreadFirstMsgSrvID int `json:"unread_first_msg_srv_id"`
|
||||
// IsHidden int `json:"is_hidden"`
|
||||
// Draft string `json:"draft"`
|
||||
// Status int `json:"status"`
|
||||
// SortTimestamp int `json:"sort_timestamp"`
|
||||
// LastClearUnreadTimestamp int `json:"last_clear_unread_timestamp"`
|
||||
// LastMsgLocaldID int `json:"last_msg_locald_id"`
|
||||
// LastMsgType int `json:"last_msg_type"`
|
||||
// LastMsgSubType int `json:"last_msg_sub_type"`
|
||||
// LastMsgExtType int `json:"last_msg_ext_type"`
|
||||
}
|
||||
|
||||
func (s *Session) PlainText(limit int) string {
|
||||
buf := strings.Builder{}
|
||||
buf.WriteString(s.NickName)
|
||||
buf.WriteString("(")
|
||||
buf.WriteString(s.UserName)
|
||||
buf.WriteString(") ")
|
||||
buf.WriteString(s.NTime.Format("2006-01-02 15:04:05"))
|
||||
buf.WriteString("\n")
|
||||
if limit > 0 {
|
||||
if len(s.Content) > limit {
|
||||
buf.WriteString(s.Content[:limit])
|
||||
buf.WriteString(" <...>")
|
||||
} else {
|
||||
buf.WriteString(s.Content)
|
||||
}
|
||||
}
|
||||
buf.WriteString("\n")
|
||||
return buf.String()
|
||||
}
|
||||
254
pkg/model/wxproto/bytesextra.pb.go
Normal file
254
pkg/model/wxproto/bytesextra.pb.go
Normal file
@@ -0,0 +1,254 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.5
|
||||
// protoc v5.29.3
|
||||
// source: bytesextra.proto
|
||||
|
||||
package wxproto
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type BytesExtraHeader struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Field1 int32 `protobuf:"varint,1,opt,name=field1,proto3" json:"field1,omitempty"`
|
||||
Field2 int32 `protobuf:"varint,2,opt,name=field2,proto3" json:"field2,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *BytesExtraHeader) Reset() {
|
||||
*x = BytesExtraHeader{}
|
||||
mi := &file_bytesextra_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *BytesExtraHeader) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*BytesExtraHeader) ProtoMessage() {}
|
||||
|
||||
func (x *BytesExtraHeader) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_bytesextra_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use BytesExtraHeader.ProtoReflect.Descriptor instead.
|
||||
func (*BytesExtraHeader) Descriptor() ([]byte, []int) {
|
||||
return file_bytesextra_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *BytesExtraHeader) GetField1() int32 {
|
||||
if x != nil {
|
||||
return x.Field1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *BytesExtraHeader) GetField2() int32 {
|
||||
if x != nil {
|
||||
return x.Field2
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type BytesExtraItem struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Type int32 `protobuf:"varint,1,opt,name=type,proto3" json:"type,omitempty"`
|
||||
Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *BytesExtraItem) Reset() {
|
||||
*x = BytesExtraItem{}
|
||||
mi := &file_bytesextra_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *BytesExtraItem) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*BytesExtraItem) ProtoMessage() {}
|
||||
|
||||
func (x *BytesExtraItem) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_bytesextra_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use BytesExtraItem.ProtoReflect.Descriptor instead.
|
||||
func (*BytesExtraItem) Descriptor() ([]byte, []int) {
|
||||
return file_bytesextra_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *BytesExtraItem) GetType() int32 {
|
||||
if x != nil {
|
||||
return x.Type
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *BytesExtraItem) GetValue() string {
|
||||
if x != nil {
|
||||
return x.Value
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type BytesExtra struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Header *BytesExtraHeader `protobuf:"bytes,1,opt,name=header,proto3" json:"header,omitempty"`
|
||||
Items []*BytesExtraItem `protobuf:"bytes,3,rep,name=items,proto3" json:"items,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *BytesExtra) Reset() {
|
||||
*x = BytesExtra{}
|
||||
mi := &file_bytesextra_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *BytesExtra) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*BytesExtra) ProtoMessage() {}
|
||||
|
||||
func (x *BytesExtra) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_bytesextra_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use BytesExtra.ProtoReflect.Descriptor instead.
|
||||
func (*BytesExtra) Descriptor() ([]byte, []int) {
|
||||
return file_bytesextra_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *BytesExtra) GetHeader() *BytesExtraHeader {
|
||||
if x != nil {
|
||||
return x.Header
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *BytesExtra) GetItems() []*BytesExtraItem {
|
||||
if x != nil {
|
||||
return x.Items
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_bytesextra_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_bytesextra_proto_rawDesc = string([]byte{
|
||||
0x0a, 0x10, 0x62, 0x79, 0x74, 0x65, 0x73, 0x65, 0x78, 0x74, 0x72, 0x61, 0x2e, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x12, 0x0c, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
|
||||
0x22, 0x42, 0x0a, 0x10, 0x42, 0x79, 0x74, 0x65, 0x73, 0x45, 0x78, 0x74, 0x72, 0x61, 0x48, 0x65,
|
||||
0x61, 0x64, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x18, 0x01,
|
||||
0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x12, 0x16, 0x0a, 0x06,
|
||||
0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x66, 0x69,
|
||||
0x65, 0x6c, 0x64, 0x32, 0x22, 0x3a, 0x0a, 0x0e, 0x42, 0x79, 0x74, 0x65, 0x73, 0x45, 0x78, 0x74,
|
||||
0x72, 0x61, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01,
|
||||
0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61,
|
||||
0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
|
||||
0x22, 0x78, 0x0a, 0x0a, 0x42, 0x79, 0x74, 0x65, 0x73, 0x45, 0x78, 0x74, 0x72, 0x61, 0x12, 0x36,
|
||||
0x0a, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e,
|
||||
0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x79,
|
||||
0x74, 0x65, 0x73, 0x45, 0x78, 0x74, 0x72, 0x61, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x52, 0x06,
|
||||
0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x32, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18,
|
||||
0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x79, 0x74, 0x65, 0x73, 0x45, 0x78, 0x74, 0x72, 0x61, 0x49,
|
||||
0x74, 0x65, 0x6d, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x42, 0x0b, 0x5a, 0x09, 0x2e, 0x3b,
|
||||
0x77, 0x78, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
})
|
||||
|
||||
var (
|
||||
file_bytesextra_proto_rawDescOnce sync.Once
|
||||
file_bytesextra_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_bytesextra_proto_rawDescGZIP() []byte {
|
||||
file_bytesextra_proto_rawDescOnce.Do(func() {
|
||||
file_bytesextra_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_bytesextra_proto_rawDesc), len(file_bytesextra_proto_rawDesc)))
|
||||
})
|
||||
return file_bytesextra_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_bytesextra_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
|
||||
var file_bytesextra_proto_goTypes = []any{
|
||||
(*BytesExtraHeader)(nil), // 0: app.protobuf.BytesExtraHeader
|
||||
(*BytesExtraItem)(nil), // 1: app.protobuf.BytesExtraItem
|
||||
(*BytesExtra)(nil), // 2: app.protobuf.BytesExtra
|
||||
}
|
||||
var file_bytesextra_proto_depIdxs = []int32{
|
||||
0, // 0: app.protobuf.BytesExtra.header:type_name -> app.protobuf.BytesExtraHeader
|
||||
1, // 1: app.protobuf.BytesExtra.items:type_name -> app.protobuf.BytesExtraItem
|
||||
2, // [2:2] is the sub-list for method output_type
|
||||
2, // [2:2] is the sub-list for method input_type
|
||||
2, // [2:2] is the sub-list for extension type_name
|
||||
2, // [2:2] is the sub-list for extension extendee
|
||||
0, // [0:2] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_bytesextra_proto_init() }
|
||||
func file_bytesextra_proto_init() {
|
||||
if File_bytesextra_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_bytesextra_proto_rawDesc), len(file_bytesextra_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 3,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_bytesextra_proto_goTypes,
|
||||
DependencyIndexes: file_bytesextra_proto_depIdxs,
|
||||
MessageInfos: file_bytesextra_proto_msgTypes,
|
||||
}.Build()
|
||||
File_bytesextra_proto = out.File
|
||||
file_bytesextra_proto_goTypes = nil
|
||||
file_bytesextra_proto_depIdxs = nil
|
||||
}
|
||||
18
pkg/model/wxproto/bytesextra.proto
Normal file
18
pkg/model/wxproto/bytesextra.proto
Normal file
@@ -0,0 +1,18 @@
|
||||
syntax = "proto3";
|
||||
package app.protobuf;
|
||||
option go_package=".;wxproto";
|
||||
|
||||
message BytesExtraHeader {
|
||||
int32 field1 = 1;
|
||||
int32 field2 = 2;
|
||||
}
|
||||
|
||||
message BytesExtraItem {
|
||||
int32 type = 1;
|
||||
string value = 2;
|
||||
}
|
||||
|
||||
message BytesExtra {
|
||||
BytesExtraHeader header = 1;
|
||||
repeated BytesExtraItem items = 3;
|
||||
}
|
||||
222
pkg/model/wxproto/roomdata.pb.go
Normal file
222
pkg/model/wxproto/roomdata.pb.go
Normal file
@@ -0,0 +1,222 @@
|
||||
// v3 & v4 通用,可能会有部分字段差异
|
||||
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.5
|
||||
// protoc v5.29.3
|
||||
// source: roomdata.proto
|
||||
|
||||
package wxproto
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type RoomData struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Users []*RoomDataUser `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty"`
|
||||
RoomCap *int32 `protobuf:"varint,5,opt,name=roomCap,proto3,oneof" json:"roomCap,omitempty"` // 只在第一份数据中出现,值为500
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RoomData) Reset() {
|
||||
*x = RoomData{}
|
||||
mi := &file_roomdata_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RoomData) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*RoomData) ProtoMessage() {}
|
||||
|
||||
func (x *RoomData) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_roomdata_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use RoomData.ProtoReflect.Descriptor instead.
|
||||
func (*RoomData) Descriptor() ([]byte, []int) {
|
||||
return file_roomdata_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *RoomData) GetUsers() []*RoomDataUser {
|
||||
if x != nil {
|
||||
return x.Users
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *RoomData) GetRoomCap() int32 {
|
||||
if x != nil && x.RoomCap != nil {
|
||||
return *x.RoomCap
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type RoomDataUser struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
UserName string `protobuf:"bytes,1,opt,name=userName,proto3" json:"userName,omitempty"` // 用户ID或名称
|
||||
DisplayName *string `protobuf:"bytes,2,opt,name=displayName,proto3,oneof" json:"displayName,omitempty"` // 显示名称,可能是UTF-8编码的中文,部分记录可能为空
|
||||
Status int32 `protobuf:"varint,3,opt,name=status,proto3" json:"status,omitempty"` // 状态码,值范围0-9
|
||||
Inviter *string `protobuf:"bytes,4,opt,name=inviter,proto3,oneof" json:"inviter,omitempty"` // 邀请人
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RoomDataUser) Reset() {
|
||||
*x = RoomDataUser{}
|
||||
mi := &file_roomdata_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RoomDataUser) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*RoomDataUser) ProtoMessage() {}
|
||||
|
||||
func (x *RoomDataUser) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_roomdata_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use RoomDataUser.ProtoReflect.Descriptor instead.
|
||||
func (*RoomDataUser) Descriptor() ([]byte, []int) {
|
||||
return file_roomdata_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *RoomDataUser) GetUserName() string {
|
||||
if x != nil {
|
||||
return x.UserName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *RoomDataUser) GetDisplayName() string {
|
||||
if x != nil && x.DisplayName != nil {
|
||||
return *x.DisplayName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *RoomDataUser) GetStatus() int32 {
|
||||
if x != nil {
|
||||
return x.Status
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RoomDataUser) GetInviter() string {
|
||||
if x != nil && x.Inviter != nil {
|
||||
return *x.Inviter
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_roomdata_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_roomdata_proto_rawDesc = string([]byte{
|
||||
0x0a, 0x0e, 0x72, 0x6f, 0x6f, 0x6d, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||
0x12, 0x0c, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x22, 0x67,
|
||||
0x0a, 0x08, 0x52, 0x6f, 0x6f, 0x6d, 0x44, 0x61, 0x74, 0x61, 0x12, 0x30, 0x0a, 0x05, 0x75, 0x73,
|
||||
0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x61, 0x70, 0x70, 0x2e,
|
||||
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x52, 0x6f, 0x6f, 0x6d, 0x44, 0x61, 0x74,
|
||||
0x61, 0x55, 0x73, 0x65, 0x72, 0x52, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x12, 0x1d, 0x0a, 0x07,
|
||||
0x72, 0x6f, 0x6f, 0x6d, 0x43, 0x61, 0x70, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52,
|
||||
0x07, 0x72, 0x6f, 0x6f, 0x6d, 0x43, 0x61, 0x70, 0x88, 0x01, 0x01, 0x42, 0x0a, 0x0a, 0x08, 0x5f,
|
||||
0x72, 0x6f, 0x6f, 0x6d, 0x43, 0x61, 0x70, 0x22, 0xa4, 0x01, 0x0a, 0x0c, 0x52, 0x6f, 0x6f, 0x6d,
|
||||
0x44, 0x61, 0x74, 0x61, 0x55, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72,
|
||||
0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72,
|
||||
0x4e, 0x61, 0x6d, 0x65, 0x12, 0x25, 0x0a, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e,
|
||||
0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0b, 0x64, 0x69, 0x73,
|
||||
0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x88, 0x01, 0x01, 0x12, 0x16, 0x0a, 0x06, 0x73,
|
||||
0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x73, 0x74, 0x61,
|
||||
0x74, 0x75, 0x73, 0x12, 0x1d, 0x0a, 0x07, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x72, 0x18, 0x04,
|
||||
0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x07, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x72, 0x88,
|
||||
0x01, 0x01, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61,
|
||||
0x6d, 0x65, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x72, 0x42, 0x0b,
|
||||
0x5a, 0x09, 0x2e, 0x3b, 0x77, 0x78, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x33,
|
||||
})
|
||||
|
||||
var (
|
||||
file_roomdata_proto_rawDescOnce sync.Once
|
||||
file_roomdata_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_roomdata_proto_rawDescGZIP() []byte {
|
||||
file_roomdata_proto_rawDescOnce.Do(func() {
|
||||
file_roomdata_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_roomdata_proto_rawDesc), len(file_roomdata_proto_rawDesc)))
|
||||
})
|
||||
return file_roomdata_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_roomdata_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||
var file_roomdata_proto_goTypes = []any{
|
||||
(*RoomData)(nil), // 0: app.protobuf.RoomData
|
||||
(*RoomDataUser)(nil), // 1: app.protobuf.RoomDataUser
|
||||
}
|
||||
var file_roomdata_proto_depIdxs = []int32{
|
||||
1, // 0: app.protobuf.RoomData.users:type_name -> app.protobuf.RoomDataUser
|
||||
1, // [1:1] is the sub-list for method output_type
|
||||
1, // [1:1] is the sub-list for method input_type
|
||||
1, // [1:1] is the sub-list for extension type_name
|
||||
1, // [1:1] is the sub-list for extension extendee
|
||||
0, // [0:1] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_roomdata_proto_init() }
|
||||
func file_roomdata_proto_init() {
|
||||
if File_roomdata_proto != nil {
|
||||
return
|
||||
}
|
||||
file_roomdata_proto_msgTypes[0].OneofWrappers = []any{}
|
||||
file_roomdata_proto_msgTypes[1].OneofWrappers = []any{}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_roomdata_proto_rawDesc), len(file_roomdata_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 2,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_roomdata_proto_goTypes,
|
||||
DependencyIndexes: file_roomdata_proto_depIdxs,
|
||||
MessageInfos: file_roomdata_proto_msgTypes,
|
||||
}.Build()
|
||||
File_roomdata_proto = out.File
|
||||
file_roomdata_proto_goTypes = nil
|
||||
file_roomdata_proto_depIdxs = nil
|
||||
}
|
||||
16
pkg/model/wxproto/roomdata.proto
Normal file
16
pkg/model/wxproto/roomdata.proto
Normal file
@@ -0,0 +1,16 @@
|
||||
// v3 & v4 通用,可能会有部分字段差异
|
||||
syntax = "proto3";
|
||||
package app.protobuf;
|
||||
option go_package=".;wxproto";
|
||||
|
||||
message RoomData {
|
||||
repeated RoomDataUser users = 1;
|
||||
optional int32 roomCap = 5; // 只在第一份数据中出现,值为500
|
||||
}
|
||||
|
||||
message RoomDataUser {
|
||||
string userName = 1; // 用户ID或名称
|
||||
optional string displayName = 2; // 显示名称,可能是UTF-8编码的中文,部分记录可能为空
|
||||
int32 status = 3; // 状态码,值范围0-9
|
||||
optional string inviter = 4; // 邀请人
|
||||
}
|
||||
135
pkg/util/os.go
Normal file
135
pkg/util/os.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// FindFilesWithPatterns 在指定目录下查找匹配多个正则表达式的文件
|
||||
// directory: 要搜索的目录路径
|
||||
// patterns: 正则表达式模式列表
|
||||
// recursive: 是否递归搜索子目录
|
||||
// 返回匹配的文件路径列表和可能的错误
|
||||
func FindFilesWithPatterns(directory string, pattern string, recursive bool) ([]string, error) {
|
||||
// 编译所有正则表达式
|
||||
re, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("无效的正则表达式 '%s': %v", pattern, err)
|
||||
}
|
||||
|
||||
// 检查目录是否存在
|
||||
dirInfo, err := os.Stat(directory)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("无法访问目录 '%s': %v", directory, err)
|
||||
}
|
||||
if !dirInfo.IsDir() {
|
||||
return nil, fmt.Errorf("'%s' 不是一个目录", directory)
|
||||
}
|
||||
|
||||
// 存储匹配的文件路径
|
||||
var matchedFiles []string
|
||||
|
||||
// 创建文件系统
|
||||
fsys := os.DirFS(directory)
|
||||
|
||||
// 遍历文件系统
|
||||
err = fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 如果是目录且不递归,则跳过子目录
|
||||
if d.IsDir() {
|
||||
if !recursive && path != "." {
|
||||
return fs.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查文件名是否匹配任何一个正则表达式
|
||||
if re.MatchString(d.Name()) {
|
||||
// 添加完整路径到结果列表
|
||||
fullPath := filepath.Join(directory, path)
|
||||
matchedFiles = append(matchedFiles, fullPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("遍历目录时出错: %v", err)
|
||||
}
|
||||
|
||||
return matchedFiles, nil
|
||||
}
|
||||
|
||||
func DefaultWorkDir(account string) string {
|
||||
if len(account) == 0 {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
return filepath.Join(os.ExpandEnv("${USERPROFILE}"), "Documents", "chatlog")
|
||||
case "darwin":
|
||||
return filepath.Join(os.ExpandEnv("${HOME}"), "Documents", "chatlog")
|
||||
default:
|
||||
return filepath.Join(os.ExpandEnv("${HOME}"), "chatlog")
|
||||
}
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
return filepath.Join(os.ExpandEnv("${USERPROFILE}"), "Documents", "chatlog", account)
|
||||
case "darwin":
|
||||
return filepath.Join(os.ExpandEnv("${HOME}"), "Documents", "chatlog", account)
|
||||
default:
|
||||
return filepath.Join(os.ExpandEnv("${HOME}"), "chatlog", account)
|
||||
}
|
||||
}
|
||||
|
||||
func GetDirSize(dir string) string {
|
||||
var size int64
|
||||
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err == nil {
|
||||
size += info.Size()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return ByteCountSI(size)
|
||||
}
|
||||
|
||||
func ByteCountSI(b int64) string {
|
||||
const unit = 1000
|
||||
if b < unit {
|
||||
return fmt.Sprintf("%d B", b)
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for n := b / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %cB",
|
||||
float64(b)/float64(div), "kMGTPE"[exp])
|
||||
}
|
||||
|
||||
// PrepareDir ensures that the specified directory path exists.
|
||||
// If the directory does not exist, it attempts to create it.
|
||||
func PrepareDir(path string) error {
|
||||
stat, err := os.Stat(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(path, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
} else if !stat.IsDir() {
|
||||
log.Debugf("%s is not a directory", path)
|
||||
return fmt.Errorf("%s is not a directory", path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
15
pkg/util/os_windows.go
Normal file
15
pkg/util/os_windows.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func Is64Bit(handle windows.Handle) (bool, error) {
|
||||
var is32Bit bool
|
||||
if err := windows.IsWow64Process(handle, &is32Bit); err != nil {
|
||||
return false, fmt.Errorf("检查进程位数失败: %w", err)
|
||||
}
|
||||
return !is32Bit, nil
|
||||
}
|
||||
34
pkg/util/strings.go
Normal file
34
pkg/util/strings.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
func IsNormalString(b []byte) bool {
|
||||
str := string(b)
|
||||
|
||||
// 检查是否为有效的 UTF-8
|
||||
if !utf8.ValidString(str) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查是否全部为可打印字符
|
||||
for _, r := range str {
|
||||
if !unicode.IsPrint(r) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func MustAnyToInt(v interface{}) int {
|
||||
str := fmt.Sprintf("%v", v)
|
||||
if i, err := strconv.Atoi(str); err == nil {
|
||||
return i
|
||||
}
|
||||
return 0
|
||||
}
|
||||
636
pkg/util/time.go
Normal file
636
pkg/util/time.go
Normal file
@@ -0,0 +1,636 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var zoneStr = time.Now().Format("-0700")
|
||||
|
||||
// 时间粒度常量
|
||||
type TimeGranularity int
|
||||
|
||||
const (
|
||||
GranularityUnknown TimeGranularity = iota // 未知粒度
|
||||
GranularitySecond // 精确到秒
|
||||
GranularityMinute // 精确到分钟
|
||||
GranularityHour // 精确到小时
|
||||
GranularityDay // 精确到天
|
||||
GranularityMonth // 精确到月
|
||||
GranularityQuarter // 精确到季度
|
||||
GranularityYear // 精确到年
|
||||
)
|
||||
|
||||
// timeOf 内部函数,解析各种格式的时间点,并返回时间粒度
|
||||
// 支持以下格式:
|
||||
// 1. 时间戳(秒): 1609459200 (GranularitySecond)
|
||||
// 2. 标准日期: 20060102, 2006-01-02 (GranularityDay)
|
||||
// 3. 带时间的日期: 20060102/15:04, 2006-01-02/15:04 (GranularityMinute)
|
||||
// 4. 完整时间: 20060102150405 (GranularitySecond)
|
||||
// 5. RFC3339: 2006-01-02T15:04:05Z07:00 (GranularitySecond)
|
||||
// 6. 相对时间: 5h-ago, 3d-ago, 1w-ago, 1m-ago, 1y-ago (根据单位确定粒度)
|
||||
// 7. 自然语言: now (GranularitySecond), today, yesterday (GranularityDay)
|
||||
// 8. 年份: 2006 (GranularityYear)
|
||||
// 9. 月份: 200601, 2006-01 (GranularityMonth)
|
||||
// 10. 季度: 2006Q1, 2006Q2, 2006Q3, 2006Q4 (GranularityQuarter)
|
||||
// 11. 年月日时分: 200601021504 (GranularityMinute)
|
||||
func timeOf(str string) (t time.Time, g TimeGranularity, ok bool) {
|
||||
if str == "" {
|
||||
return time.Time{}, GranularityUnknown, false
|
||||
}
|
||||
|
||||
str = strings.TrimSpace(str)
|
||||
|
||||
// 处理自然语言时间
|
||||
switch strings.ToLower(str) {
|
||||
case "now":
|
||||
return time.Now(), GranularitySecond, true
|
||||
case "today":
|
||||
now := time.Now()
|
||||
return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()), GranularityDay, true
|
||||
case "yesterday":
|
||||
now := time.Now().AddDate(0, 0, -1)
|
||||
return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()), GranularityDay, true
|
||||
case "this-week":
|
||||
now := time.Now()
|
||||
weekday := int(now.Weekday())
|
||||
if weekday == 0 { // 周日
|
||||
weekday = 7
|
||||
}
|
||||
// 本周一
|
||||
monday := now.AddDate(0, 0, -(weekday - 1))
|
||||
return time.Date(monday.Year(), monday.Month(), monday.Day(), 0, 0, 0, 0, now.Location()), GranularityDay, true
|
||||
case "last-week":
|
||||
now := time.Now()
|
||||
weekday := int(now.Weekday())
|
||||
if weekday == 0 { // 周日
|
||||
weekday = 7
|
||||
}
|
||||
// 上周一
|
||||
lastMonday := now.AddDate(0, 0, -(weekday-1)-7)
|
||||
return time.Date(lastMonday.Year(), lastMonday.Month(), lastMonday.Day(), 0, 0, 0, 0, now.Location()), GranularityDay, true
|
||||
case "this-month":
|
||||
now := time.Now()
|
||||
return time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()), GranularityMonth, true
|
||||
case "last-month":
|
||||
now := time.Now()
|
||||
return time.Date(now.Year(), now.Month()-1, 1, 0, 0, 0, 0, now.Location()), GranularityMonth, true
|
||||
case "this-year":
|
||||
now := time.Now()
|
||||
return time.Date(now.Year(), 1, 1, 0, 0, 0, 0, now.Location()), GranularityYear, true
|
||||
case "last-year":
|
||||
now := time.Now()
|
||||
return time.Date(now.Year()-1, 1, 1, 0, 0, 0, 0, now.Location()), GranularityYear, true
|
||||
case "all":
|
||||
// 返回零值时间
|
||||
return time.Time{}, GranularityYear, true
|
||||
}
|
||||
|
||||
// 处理相对时间: 5h-ago, 3d-ago, 1w-ago, 1m-ago, 1y-ago
|
||||
if strings.HasSuffix(str, "-ago") {
|
||||
str = strings.TrimSuffix(str, "-ago")
|
||||
|
||||
// 特殊处理 0d-ago 为当天开始
|
||||
if str == "0d" {
|
||||
now := time.Now()
|
||||
return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()), GranularityDay, true
|
||||
}
|
||||
|
||||
// 解析数字和单位
|
||||
re := regexp.MustCompile(`^(\d+)([hdwmy])$`)
|
||||
matches := re.FindStringSubmatch(str)
|
||||
if len(matches) == 3 {
|
||||
num, err := strconv.Atoi(matches[1])
|
||||
if err != nil {
|
||||
return time.Time{}, GranularityUnknown, false
|
||||
}
|
||||
|
||||
// 确保数字是正数
|
||||
if num <= 0 {
|
||||
// 对于0d-ago已经特殊处理,其他0或负数都是无效的
|
||||
if num == 0 && matches[2] != "d" {
|
||||
return time.Time{}, GranularityUnknown, false
|
||||
}
|
||||
return time.Time{}, GranularityUnknown, false
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
var resultTime time.Time
|
||||
var granularity TimeGranularity
|
||||
|
||||
switch matches[2] {
|
||||
case "h": // 小时
|
||||
resultTime = now.Add(-time.Duration(num) * time.Hour)
|
||||
granularity = GranularityHour
|
||||
case "d": // 天
|
||||
resultTime = now.AddDate(0, 0, -num)
|
||||
granularity = GranularityDay
|
||||
case "w": // 周
|
||||
resultTime = now.AddDate(0, 0, -num*7)
|
||||
granularity = GranularityDay
|
||||
case "m": // 月
|
||||
resultTime = now.AddDate(0, -num, 0)
|
||||
granularity = GranularityMonth
|
||||
case "y": // 年
|
||||
resultTime = now.AddDate(-num, 0, 0)
|
||||
granularity = GranularityYear
|
||||
default:
|
||||
return time.Time{}, GranularityUnknown, false
|
||||
}
|
||||
|
||||
return resultTime, granularity, true
|
||||
}
|
||||
|
||||
// 尝试标准 duration 解析
|
||||
dur, err := time.ParseDuration(str)
|
||||
if err == nil {
|
||||
// 根据duration单位确定粒度
|
||||
hours := dur.Hours()
|
||||
if hours < 1 {
|
||||
return time.Now().Add(-dur), GranularitySecond, true
|
||||
} else if hours < 24 {
|
||||
return time.Now().Add(-dur), GranularityHour, true
|
||||
} else {
|
||||
return time.Now().Add(-dur), GranularityDay, true
|
||||
}
|
||||
}
|
||||
|
||||
return time.Time{}, GranularityUnknown, false
|
||||
}
|
||||
|
||||
// 处理季度: 2006Q1, 2006Q2, 2006Q3, 2006Q4
|
||||
if matched, _ := regexp.MatchString(`^\d{4}Q[1-4]$`, str); matched {
|
||||
re := regexp.MustCompile(`^(\d{4})Q([1-4])$`)
|
||||
matches := re.FindStringSubmatch(str)
|
||||
if len(matches) == 3 {
|
||||
year, _ := strconv.Atoi(matches[1])
|
||||
quarter, _ := strconv.Atoi(matches[2])
|
||||
|
||||
// 验证年份范围
|
||||
if year < 1970 || year > 9999 {
|
||||
return time.Time{}, GranularityUnknown, false
|
||||
}
|
||||
|
||||
// 计算季度的开始月份
|
||||
startMonth := time.Month((quarter-1)*3 + 1)
|
||||
|
||||
return time.Date(year, startMonth, 1, 0, 0, 0, 0, time.Local), GranularityQuarter, true
|
||||
}
|
||||
}
|
||||
|
||||
// 处理年份: 2006
|
||||
if len(str) == 4 && isDigitsOnly(str) {
|
||||
year, err := strconv.Atoi(str)
|
||||
if err == nil && year >= 1970 && year <= 9999 {
|
||||
return time.Date(year, 1, 1, 0, 0, 0, 0, time.Local), GranularityYear, true
|
||||
}
|
||||
return time.Time{}, GranularityUnknown, false
|
||||
}
|
||||
|
||||
// 处理月份: 200601 或 2006-01
|
||||
if (len(str) == 6 && isDigitsOnly(str)) || (len(str) == 7 && strings.Count(str, "-") == 1) {
|
||||
var year, month int
|
||||
var err error
|
||||
|
||||
if len(str) == 6 && isDigitsOnly(str) {
|
||||
year, err = strconv.Atoi(str[0:4])
|
||||
if err != nil {
|
||||
return time.Time{}, GranularityUnknown, false
|
||||
}
|
||||
month, err = strconv.Atoi(str[4:6])
|
||||
if err != nil {
|
||||
return time.Time{}, GranularityUnknown, false
|
||||
}
|
||||
} else { // 2006-01
|
||||
parts := strings.Split(str, "-")
|
||||
if len(parts) != 2 {
|
||||
return time.Time{}, GranularityUnknown, false
|
||||
}
|
||||
year, err = strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return time.Time{}, GranularityUnknown, false
|
||||
}
|
||||
month, err = strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return time.Time{}, GranularityUnknown, false
|
||||
}
|
||||
}
|
||||
|
||||
if year < 1970 || year > 9999 || month < 1 || month > 12 {
|
||||
return time.Time{}, GranularityUnknown, false
|
||||
}
|
||||
|
||||
return time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.Local), GranularityMonth, true
|
||||
}
|
||||
|
||||
// 处理日期格式: 20060102 或 2006-01-02
|
||||
if len(str) == 8 && isDigitsOnly(str) {
|
||||
// 验证年月日
|
||||
year, _ := strconv.Atoi(str[0:4])
|
||||
month, _ := strconv.Atoi(str[4:6])
|
||||
day, _ := strconv.Atoi(str[6:8])
|
||||
|
||||
if year < 1970 || year > 9999 || month < 1 || month > 12 || day < 1 || day > 31 {
|
||||
return time.Time{}, GranularityUnknown, false
|
||||
}
|
||||
|
||||
// 进一步验证日期是否有效
|
||||
if !isValidDate(year, month, day) {
|
||||
return time.Time{}, GranularityUnknown, false
|
||||
}
|
||||
|
||||
// 直接构造时间
|
||||
result := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.Local)
|
||||
return result, GranularityDay, true
|
||||
} else if len(str) == 10 && strings.Count(str, "-") == 2 {
|
||||
// 验证年月日
|
||||
parts := strings.Split(str, "-")
|
||||
if len(parts) != 3 {
|
||||
return time.Time{}, GranularityUnknown, false
|
||||
}
|
||||
|
||||
year, err1 := strconv.Atoi(parts[0])
|
||||
month, err2 := strconv.Atoi(parts[1])
|
||||
day, err3 := strconv.Atoi(parts[2])
|
||||
|
||||
if err1 != nil || err2 != nil || err3 != nil {
|
||||
return time.Time{}, GranularityUnknown, false
|
||||
}
|
||||
|
||||
if year < 1970 || year > 9999 || month < 1 || month > 12 || day < 1 || day > 31 {
|
||||
return time.Time{}, GranularityUnknown, false
|
||||
}
|
||||
|
||||
// 进一步验证日期是否有效
|
||||
if !isValidDate(year, month, day) {
|
||||
return time.Time{}, GranularityUnknown, false
|
||||
}
|
||||
|
||||
// 直接构造时间
|
||||
result := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.Local)
|
||||
return result, GranularityDay, true
|
||||
}
|
||||
|
||||
// 处理年月日时分: 200601021504
|
||||
if len(str) == 12 && isDigitsOnly(str) {
|
||||
year, _ := strconv.Atoi(str[0:4])
|
||||
month, _ := strconv.Atoi(str[4:6])
|
||||
day, _ := strconv.Atoi(str[6:8])
|
||||
hour, _ := strconv.Atoi(str[8:10])
|
||||
minute, _ := strconv.Atoi(str[10:12])
|
||||
|
||||
if year < 1970 || year > 9999 || month < 1 || month > 12 || day < 1 || day > 31 ||
|
||||
hour < 0 || hour > 23 || minute < 0 || minute > 59 {
|
||||
return time.Time{}, GranularityUnknown, false
|
||||
}
|
||||
|
||||
// 进一步验证日期是否有效
|
||||
if !isValidDate(year, month, day) {
|
||||
return time.Time{}, GranularityUnknown, false
|
||||
}
|
||||
|
||||
// 直接构造时间
|
||||
result := time.Date(year, time.Month(month), day, hour, minute, 0, 0, time.Local)
|
||||
return result, GranularityMinute, true
|
||||
}
|
||||
|
||||
// 处理带时间的日期: 20060102/15:04 或 2006-01-02/15:04
|
||||
if strings.Contains(str, "/") {
|
||||
parts := strings.Split(str, "/")
|
||||
if len(parts) != 2 {
|
||||
return time.Time{}, GranularityUnknown, false
|
||||
}
|
||||
|
||||
datePart := parts[0]
|
||||
timePart := parts[1]
|
||||
|
||||
// 验证日期部分
|
||||
var year, month, day int
|
||||
var err1, err2, err3 error
|
||||
|
||||
if len(datePart) == 8 && isDigitsOnly(datePart) {
|
||||
year, err1 = strconv.Atoi(datePart[0:4])
|
||||
month, err2 = strconv.Atoi(datePart[4:6])
|
||||
day, err3 = strconv.Atoi(datePart[6:8])
|
||||
} else if len(datePart) == 10 && strings.Count(datePart, "-") == 2 {
|
||||
dateParts := strings.Split(datePart, "-")
|
||||
if len(dateParts) != 3 {
|
||||
return time.Time{}, GranularityUnknown, false
|
||||
}
|
||||
year, err1 = strconv.Atoi(dateParts[0])
|
||||
month, err2 = strconv.Atoi(dateParts[1])
|
||||
day, err3 = strconv.Atoi(dateParts[2])
|
||||
} else {
|
||||
return time.Time{}, GranularityUnknown, false
|
||||
}
|
||||
|
||||
if err1 != nil || err2 != nil || err3 != nil {
|
||||
return time.Time{}, GranularityUnknown, false
|
||||
}
|
||||
|
||||
if year < 1970 || year > 9999 || month < 1 || month > 12 || day < 1 || day > 31 {
|
||||
return time.Time{}, GranularityUnknown, false
|
||||
}
|
||||
|
||||
// 进一步验证日期是否有效
|
||||
if !isValidDate(year, month, day) {
|
||||
return time.Time{}, GranularityUnknown, false
|
||||
}
|
||||
|
||||
// 验证时间部分
|
||||
if !regexp.MustCompile(`^\d{2}:\d{2}$`).MatchString(timePart) {
|
||||
return time.Time{}, GranularityUnknown, false
|
||||
}
|
||||
|
||||
timeParts := strings.Split(timePart, ":")
|
||||
hour, err1 := strconv.Atoi(timeParts[0])
|
||||
minute, err2 := strconv.Atoi(timeParts[1])
|
||||
|
||||
if err1 != nil || err2 != nil {
|
||||
return time.Time{}, GranularityUnknown, false
|
||||
}
|
||||
|
||||
if hour < 0 || hour > 23 || minute < 0 || minute > 59 {
|
||||
return time.Time{}, GranularityUnknown, false
|
||||
}
|
||||
|
||||
// 直接构造时间
|
||||
result := time.Date(year, time.Month(month), day, hour, minute, 0, 0, time.Local)
|
||||
return result, GranularityMinute, true
|
||||
}
|
||||
|
||||
// 处理完整时间: 20060102150405
|
||||
if len(str) == 14 && isDigitsOnly(str) {
|
||||
year, _ := strconv.Atoi(str[0:4])
|
||||
month, _ := strconv.Atoi(str[4:6])
|
||||
day, _ := strconv.Atoi(str[6:8])
|
||||
hour, _ := strconv.Atoi(str[8:10])
|
||||
minute, _ := strconv.Atoi(str[10:12])
|
||||
second, _ := strconv.Atoi(str[12:14])
|
||||
|
||||
if year < 1970 || year > 9999 || month < 1 || month > 12 || day < 1 || day > 31 ||
|
||||
hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59 {
|
||||
return time.Time{}, GranularityUnknown, false
|
||||
}
|
||||
|
||||
// 进一步验证日期是否有效
|
||||
if !isValidDate(year, month, day) {
|
||||
return time.Time{}, GranularityUnknown, false
|
||||
}
|
||||
|
||||
// 直接构造时间
|
||||
result := time.Date(year, time.Month(month), day, hour, minute, second, 0, time.Local)
|
||||
return result, GranularitySecond, true
|
||||
}
|
||||
|
||||
// 处理时间戳(秒)
|
||||
if isDigitsOnly(str) {
|
||||
n, err := strconv.ParseInt(str, 10, 64)
|
||||
if err == nil {
|
||||
// 检查是否是合理的时间戳范围
|
||||
if n >= 1000000000 && n <= 253402300799 { // 2001年到2286年的秒级时间戳
|
||||
return time.Unix(n, 0), GranularitySecond, true
|
||||
}
|
||||
}
|
||||
return time.Time{}, GranularityUnknown, false
|
||||
}
|
||||
|
||||
// 处理 RFC3339: 2006-01-02T15:04:05Z07:00
|
||||
if strings.Contains(str, "T") && (strings.Contains(str, "Z") || strings.Contains(str, "+") || strings.Contains(str, "-")) {
|
||||
t, err := time.Parse(time.RFC3339, str)
|
||||
if err != nil {
|
||||
// 尝试不带秒的格式
|
||||
t, err = time.Parse("2006-01-02T15:04Z07:00", str)
|
||||
}
|
||||
if err == nil {
|
||||
return t, GranularitySecond, true
|
||||
}
|
||||
}
|
||||
|
||||
// 排除所有其他不支持的格式
|
||||
return time.Time{}, GranularityUnknown, false
|
||||
}
|
||||
|
||||
// TimeOf 解析各种格式的时间点
|
||||
// 支持以下格式:
|
||||
// 1. 时间戳(秒): 1609459200
|
||||
// 2. 标准日期: 20060102, 2006-01-02
|
||||
// 3. 带时间的日期: 20060102/15:04, 2006-01-02/15:04
|
||||
// 4. 完整时间: 20060102150405
|
||||
// 5. RFC3339: 2006-01-02T15:04:05Z07:00
|
||||
// 6. 相对时间: 5h-ago, 3d-ago, 1w-ago, 1m-ago, 1y-ago (小时、天、周、月、年)
|
||||
// 7. 自然语言: now, today, yesterday
|
||||
// 8. 年份: 2006
|
||||
// 9. 月份: 200601, 2006-01
|
||||
// 10. 季度: 2006Q1, 2006Q2, 2006Q3, 2006Q4
|
||||
// 11. 年月日时分: 200601021504
|
||||
func TimeOf(str string) (t time.Time, ok bool) {
|
||||
t, _, ok = timeOf(str)
|
||||
return
|
||||
}
|
||||
|
||||
// TimeRangeOf 解析各种格式的时间范围
|
||||
// 支持以下格式:
|
||||
// 1. 单个时间点: 根据时间粒度确定合适的时间范围
|
||||
// - 精确到秒/分钟/小时: 扩展为当天范围
|
||||
// - 精确到天: 当天 00:00:00 ~ 23:59:59
|
||||
// - 精确到月: 当月第一天 ~ 最后一天
|
||||
// - 精确到季度: 季度第一天 ~ 最后一天
|
||||
// - 精确到年: 当年第一天 ~ 最后一天
|
||||
//
|
||||
// 2. 时间区间: 2006-01-01~2006-01-31, 2006-01-01,2006-01-31, 2006-01-01 to 2006-01-31
|
||||
// 3. 相对时间: last-7d, last-30d, last-3m, last-1y (最近7天、30天、3个月、1年)
|
||||
// 4. 特定时间段: today, yesterday, this-week, last-week, this-month, last-month, this-year, last-year
|
||||
// 5. all: 表示所有时间
|
||||
func TimeRangeOf(str string) (start, end time.Time, ok bool) {
|
||||
if str == "" {
|
||||
return time.Time{}, time.Time{}, false
|
||||
}
|
||||
|
||||
str = strings.TrimSpace(str)
|
||||
|
||||
// 处理 all 特殊情况
|
||||
if strings.ToLower(str) == "all" {
|
||||
start = time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
end = time.Date(9999, 12, 31, 23, 59, 59, 999999999, time.UTC)
|
||||
return start, end, true
|
||||
}
|
||||
|
||||
// 处理相对时间范围: last-7d, last-30d, last-3m, last-1y
|
||||
if matched, _ := regexp.MatchString(`^last-\d+[dwmy]$`, str); matched {
|
||||
re := regexp.MustCompile(`^last-(\d+)([dwmy])$`)
|
||||
matches := re.FindStringSubmatch(str)
|
||||
if len(matches) == 3 {
|
||||
num, err := strconv.Atoi(matches[1])
|
||||
if err != nil || num <= 0 {
|
||||
return time.Time{}, time.Time{}, false
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
end = time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 999999999, now.Location())
|
||||
|
||||
switch matches[2] {
|
||||
case "d": // 天
|
||||
start = now.AddDate(0, 0, -num)
|
||||
start = time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, start.Location())
|
||||
return start, end, true
|
||||
case "w": // 周
|
||||
start = now.AddDate(0, 0, -num*7)
|
||||
start = time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, start.Location())
|
||||
return start, end, true
|
||||
case "m": // 月
|
||||
start = now.AddDate(0, -num, 0)
|
||||
start = time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, start.Location())
|
||||
return start, end, true
|
||||
case "y": // 年
|
||||
start = now.AddDate(-num, 0, 0)
|
||||
start = time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, start.Location())
|
||||
return start, end, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理时间区间: 2006-01-01~2006-01-31, 2006-01-01,2006-01-31, 2006-01-01 to 2006-01-31
|
||||
separators := []string{"~", ",", " to "}
|
||||
for _, sep := range separators {
|
||||
if strings.Contains(str, sep) {
|
||||
parts := strings.Split(str, sep)
|
||||
if len(parts) == 2 {
|
||||
startTime, startGran, startOk := timeOf(strings.TrimSpace(parts[0]))
|
||||
endTime, endGran, endOk := timeOf(strings.TrimSpace(parts[1]))
|
||||
|
||||
if startOk && endOk {
|
||||
// 根据粒度调整时间范围
|
||||
start = adjustStartTime(startTime, startGran)
|
||||
end = adjustEndTime(endTime, endGran)
|
||||
|
||||
// 确保开始时间早于结束时间
|
||||
if start.After(end) {
|
||||
// 正确交换开始和结束时间
|
||||
start, end = adjustStartTime(endTime, endGran), adjustEndTime(startTime, startGran)
|
||||
}
|
||||
|
||||
return start, end, true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理单个时间点,根据粒度确定合适的时间范围
|
||||
t, g, ok := timeOf(str)
|
||||
if ok {
|
||||
switch g {
|
||||
case GranularitySecond, GranularityMinute, GranularityHour:
|
||||
// 精确到秒/分钟/小时的时间点,扩展为当天范围
|
||||
start = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
|
||||
end = time.Date(t.Year(), t.Month(), t.Day(), 23, 59, 59, 999999999, t.Location())
|
||||
case GranularityDay:
|
||||
// 精确到天的时间点
|
||||
start = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
|
||||
end = time.Date(t.Year(), t.Month(), t.Day(), 23, 59, 59, 999999999, t.Location())
|
||||
case GranularityMonth:
|
||||
// 精确到月的时间点
|
||||
start = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location())
|
||||
end = time.Date(t.Year(), t.Month()+1, 0, 23, 59, 59, 999999999, t.Location())
|
||||
case GranularityQuarter:
|
||||
// 精确到季度的时间点
|
||||
quarter := (t.Month()-1)/3 + 1
|
||||
startMonth := time.Month((int(quarter)-1)*3 + 1)
|
||||
endMonth := startMonth + 2
|
||||
start = time.Date(t.Year(), startMonth, 1, 0, 0, 0, 0, t.Location())
|
||||
end = time.Date(t.Year(), endMonth+1, 0, 23, 59, 59, 999999999, t.Location())
|
||||
case GranularityYear:
|
||||
// 精确到年的时间点
|
||||
start = time.Date(t.Year(), 1, 1, 0, 0, 0, 0, t.Location())
|
||||
end = time.Date(t.Year(), 12, 31, 23, 59, 59, 999999999, t.Location())
|
||||
}
|
||||
return start, end, true
|
||||
}
|
||||
|
||||
return time.Time{}, time.Time{}, false
|
||||
}
|
||||
|
||||
// adjustStartTime 根据时间粒度调整开始时间
|
||||
func adjustStartTime(t time.Time, g TimeGranularity) time.Time {
|
||||
switch g {
|
||||
case GranularitySecond, GranularityMinute, GranularityHour:
|
||||
// 对于精确到秒/分钟/小时的时间,保持原样
|
||||
return t
|
||||
case GranularityDay:
|
||||
// 精确到天,设置为当天开始
|
||||
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
|
||||
case GranularityMonth:
|
||||
// 精确到月,设置为当月第一天
|
||||
return time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location())
|
||||
case GranularityQuarter:
|
||||
// 精确到季度,设置为季度第一天
|
||||
quarter := (t.Month()-1)/3 + 1
|
||||
startMonth := time.Month((int(quarter)-1)*3 + 1)
|
||||
return time.Date(t.Year(), startMonth, 1, 0, 0, 0, 0, t.Location())
|
||||
case GranularityYear:
|
||||
// 精确到年,设置为当年第一天
|
||||
return time.Date(t.Year(), 1, 1, 0, 0, 0, 0, t.Location())
|
||||
default:
|
||||
// 未知粒度,默认为当天开始
|
||||
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
|
||||
}
|
||||
}
|
||||
|
||||
// adjustEndTime 根据时间粒度调整结束时间
|
||||
func adjustEndTime(t time.Time, g TimeGranularity) time.Time {
|
||||
switch g {
|
||||
case GranularitySecond, GranularityMinute, GranularityHour:
|
||||
// 对于精确到秒/分钟/小时的时间,设置为当天结束
|
||||
return time.Date(t.Year(), t.Month(), t.Day(), 23, 59, 59, 999999999, t.Location())
|
||||
case GranularityDay:
|
||||
// 精确到天,设置为当天结束
|
||||
return time.Date(t.Year(), t.Month(), t.Day(), 23, 59, 59, 999999999, t.Location())
|
||||
case GranularityMonth:
|
||||
// 精确到月,设置为当月最后一天
|
||||
return time.Date(t.Year(), t.Month()+1, 0, 23, 59, 59, 999999999, t.Location())
|
||||
case GranularityQuarter:
|
||||
// 精确到季度,设置为季度最后一天
|
||||
quarter := (t.Month()-1)/3 + 1
|
||||
startMonth := time.Month((int(quarter)-1)*3 + 1)
|
||||
endMonth := startMonth + 2
|
||||
return time.Date(t.Year(), endMonth+1, 0, 23, 59, 59, 999999999, t.Location())
|
||||
case GranularityYear:
|
||||
// 精确到年,设置为当年最后一天
|
||||
return time.Date(t.Year(), 12, 31, 23, 59, 59, 999999999, t.Location())
|
||||
default:
|
||||
// 未知粒度,默认为当天结束
|
||||
return time.Date(t.Year(), t.Month(), t.Day(), 23, 59, 59, 999999999, t.Location())
|
||||
}
|
||||
}
|
||||
|
||||
// isDigitsOnly 检查字符串是否只包含数字
|
||||
func isDigitsOnly(s string) bool {
|
||||
for _, c := range s {
|
||||
if c < '0' || c > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return len(s) > 0
|
||||
}
|
||||
|
||||
// isValidDate 检查日期是否有效
|
||||
func isValidDate(year, month, day int) bool {
|
||||
// 检查月份的天数
|
||||
daysInMonth := 31
|
||||
|
||||
switch month {
|
||||
case 4, 6, 9, 11:
|
||||
daysInMonth = 30
|
||||
case 2:
|
||||
// 闰年判断
|
||||
if (year%4 == 0 && year%100 != 0) || year%400 == 0 {
|
||||
daysInMonth = 29
|
||||
} else {
|
||||
daysInMonth = 28
|
||||
}
|
||||
}
|
||||
|
||||
return day <= daysInMonth
|
||||
}
|
||||
1004
pkg/util/time_test.go
Normal file
1004
pkg/util/time_test.go
Normal file
File diff suppressed because it is too large
Load Diff
32
pkg/version/version.go
Normal file
32
pkg/version/version.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
Version = "(dev)"
|
||||
buildInfo = debug.BuildInfo{}
|
||||
)
|
||||
|
||||
func init() {
|
||||
if bi, ok := debug.ReadBuildInfo(); ok {
|
||||
buildInfo = *bi
|
||||
if len(bi.Main.Version) > 0 {
|
||||
Version = bi.Main.Version
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func GetMore(mod bool) string {
|
||||
if mod {
|
||||
mod := buildInfo.String()
|
||||
if len(mod) > 0 {
|
||||
return fmt.Sprintf("\t%s\n", strings.ReplaceAll(mod[:len(mod)-1], "\n", "\n\t"))
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("version %s %s %s/%s\n", Version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
Reference in New Issue
Block a user