feat: xiaji-go v1.0.0 - 智能记账机器人
- Telegram Bot + QQ Bot (WebSocket) 双平台支持 - 150+ 预设分类关键词,jieba 智能分词 - Web 管理后台(记录查看/删除/CSV导出) - 金额精确存储(分/int64) - 版本信息嵌入(编译时注入) - Docker 支持 - 优雅关闭(context + signal)
This commit is contained in:
7
.dockerignore
Normal file
7
.dockerignore
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
*.db
|
||||||
|
config.yaml
|
||||||
|
xiaji-go
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
README.md
|
||||||
|
docker-compose.yml
|
||||||
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# 二进制文件
|
||||||
|
xiaji-go
|
||||||
|
xiaji-go-linux-amd64
|
||||||
|
|
||||||
|
# 数据库
|
||||||
|
*.db
|
||||||
|
|
||||||
|
# 配置文件(含敏感信息)
|
||||||
|
config.yaml
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Go
|
||||||
|
vendor/
|
||||||
52
Dockerfile
Normal file
52
Dockerfile
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# ============ 构建阶段 ============
|
||||||
|
FROM golang:1.22-bookworm AS builder
|
||||||
|
|
||||||
|
ARG VERSION=dev
|
||||||
|
ARG GIT_COMMIT=unknown
|
||||||
|
ARG BUILD_TIME=unknown
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# 安装 gojieba 依赖
|
||||||
|
RUN apt-get update && apt-get install -y gcc g++ cmake && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# 先复制依赖文件,利用缓存
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
# 复制源码
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# 编译
|
||||||
|
RUN CGO_ENABLED=1 go build \
|
||||||
|
-ldflags "-X xiaji-go/version.Version=${VERSION} \
|
||||||
|
-X xiaji-go/version.GitCommit=${GIT_COMMIT} \
|
||||||
|
-X xiaji-go/version.BuildTime=${BUILD_TIME}" \
|
||||||
|
-o xiaji-go cmd/main.go
|
||||||
|
|
||||||
|
# ============ 运行阶段 ============
|
||||||
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y ca-certificates tzdata && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
ENV TZ=Asia/Shanghai
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 从构建阶段复制
|
||||||
|
COPY --from=builder /build/xiaji-go .
|
||||||
|
COPY --from=builder /build/templates/ ./templates/
|
||||||
|
COPY --from=builder /build/config.yaml.example ./config.yaml.example
|
||||||
|
|
||||||
|
# gojieba 词典文件
|
||||||
|
COPY --from=builder /root/go/pkg/mod/github.com/yanyiwu/gojieba@v1.3.0/dict/ /app/dict/
|
||||||
|
|
||||||
|
# 数据目录
|
||||||
|
VOLUME ["/app/data"]
|
||||||
|
|
||||||
|
EXPOSE 9521
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
|
CMD curl -f http://localhost:9521/health || exit 1
|
||||||
|
|
||||||
|
ENTRYPOINT ["./xiaji-go"]
|
||||||
51
Makefile
Normal file
51
Makefile
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
APP_NAME := xiaji-go
|
||||||
|
VERSION := 1.0.0
|
||||||
|
GIT_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
||||||
|
BUILD_TIME := $(shell date -u '+%Y-%m-%dT%H:%M:%SZ')
|
||||||
|
LDFLAGS := -X xiaji-go/version.Version=$(VERSION) \
|
||||||
|
-X xiaji-go/version.GitCommit=$(GIT_COMMIT) \
|
||||||
|
-X xiaji-go/version.BuildTime=$(BUILD_TIME)
|
||||||
|
|
||||||
|
DOCKER_IMAGE := ouaone/xiaji-go
|
||||||
|
DOCKER_TAG := $(VERSION)
|
||||||
|
|
||||||
|
.PHONY: build clean run docker docker-push release help
|
||||||
|
|
||||||
|
## build: 编译二进制文件
|
||||||
|
build:
|
||||||
|
go build -ldflags "$(LDFLAGS)" -o $(APP_NAME) cmd/main.go
|
||||||
|
|
||||||
|
## build-linux: 交叉编译 Linux amd64
|
||||||
|
build-linux:
|
||||||
|
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o $(APP_NAME)-linux-amd64 cmd/main.go
|
||||||
|
|
||||||
|
## clean: 清理编译产物
|
||||||
|
clean:
|
||||||
|
rm -f $(APP_NAME) $(APP_NAME)-linux-amd64
|
||||||
|
|
||||||
|
## run: 编译并运行
|
||||||
|
run: build
|
||||||
|
./$(APP_NAME)
|
||||||
|
|
||||||
|
## docker: 构建 Docker 镜像
|
||||||
|
docker:
|
||||||
|
docker build -t $(DOCKER_IMAGE):$(DOCKER_TAG) -t $(DOCKER_IMAGE):latest \
|
||||||
|
--build-arg VERSION=$(VERSION) \
|
||||||
|
--build-arg GIT_COMMIT=$(GIT_COMMIT) \
|
||||||
|
--build-arg BUILD_TIME=$(BUILD_TIME) .
|
||||||
|
|
||||||
|
## docker-push: 推送 Docker 镜像
|
||||||
|
docker-push:
|
||||||
|
docker push $(DOCKER_IMAGE):$(DOCKER_TAG)
|
||||||
|
docker push $(DOCKER_IMAGE):latest
|
||||||
|
|
||||||
|
## version: 显示版本信息
|
||||||
|
version:
|
||||||
|
@echo "$(APP_NAME) v$(VERSION) ($(GIT_COMMIT))"
|
||||||
|
|
||||||
|
## help: 显示帮助
|
||||||
|
help:
|
||||||
|
@echo "$(APP_NAME) v$(VERSION)"
|
||||||
|
@echo ""
|
||||||
|
@echo "可用目标:"
|
||||||
|
@grep -E '^## ' Makefile | sed 's/^## / /'
|
||||||
175
README.md
Normal file
175
README.md
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
# 🦞 虾记 Xiaji-Go
|
||||||
|
|
||||||
|
一个支持 **Telegram Bot** 和 **QQ Bot** 的智能记账机器人,带 Web 管理后台。
|
||||||
|
|
||||||
|
## ✨ 功能特性
|
||||||
|
|
||||||
|
- **多平台支持**:同时接入 Telegram Bot 和 QQ Bot(WebSocket 模式)
|
||||||
|
- **智能记账**:发送自然语言自动识别金额和消费分类(基于 jieba 分词)
|
||||||
|
- **150+ 预设分类关键词**:餐饮、交通、购物、饮品、水果、住房、医疗、娱乐等
|
||||||
|
- **Web 管理后台**:响应式设计,支持记录查看、删除、CSV 导出
|
||||||
|
- **金额精确**:使用分(int64)存储,避免浮点精度问题
|
||||||
|
- **优雅关闭**:支持 SIGTERM/SIGINT 信号,干净退出
|
||||||
|
|
||||||
|
## 📦 快速开始
|
||||||
|
|
||||||
|
### 二进制运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 复制配置文件
|
||||||
|
cp config.yaml.example config.yaml
|
||||||
|
|
||||||
|
# 2. 编辑配置(填入 Bot Token 等信息)
|
||||||
|
vi config.yaml
|
||||||
|
|
||||||
|
# 3. 运行
|
||||||
|
./xiaji-go
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker 运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 准备配置文件
|
||||||
|
cp config.yaml.example config.yaml
|
||||||
|
vi config.yaml
|
||||||
|
|
||||||
|
# 2. 使用 docker-compose
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# 或直接 docker run
|
||||||
|
docker run -d \
|
||||||
|
--name xiaji-go \
|
||||||
|
-p 9521:9521 \
|
||||||
|
-v $(pwd)/config.yaml:/app/config.yaml:ro \
|
||||||
|
-v $(pwd)/data:/app/data \
|
||||||
|
ouaone/xiaji-go:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### 从源码编译
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 需要 Go 1.22+、GCC(gojieba 依赖 CGO)
|
||||||
|
make build
|
||||||
|
|
||||||
|
# 交叉编译 Linux
|
||||||
|
make build-linux
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚙️ 配置说明
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
server:
|
||||||
|
port: 9521 # Web 后台端口
|
||||||
|
key: "your-secret-key" # 会话密钥
|
||||||
|
|
||||||
|
database:
|
||||||
|
path: "./xiaji.db" # SQLite 数据库路径
|
||||||
|
|
||||||
|
admin:
|
||||||
|
username: "admin" # Web 后台用户名
|
||||||
|
password: "your_password" # Web 后台密码
|
||||||
|
|
||||||
|
telegram:
|
||||||
|
enabled: true # 是否启用 Telegram Bot
|
||||||
|
token: "YOUR_BOT_TOKEN" # BotFather 获取的 Token
|
||||||
|
|
||||||
|
qqbot:
|
||||||
|
enabled: false # 是否启用 QQ Bot
|
||||||
|
appid: "YOUR_APPID" # QQ 开放平台 AppID
|
||||||
|
secret: "YOUR_SECRET" # QQ 开放平台 AppSecret
|
||||||
|
```
|
||||||
|
|
||||||
|
## 💬 使用方式
|
||||||
|
|
||||||
|
### Telegram Bot 命令
|
||||||
|
|
||||||
|
| 命令 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `/start` | 欢迎信息 |
|
||||||
|
| `/help` | 使用帮助 |
|
||||||
|
| `/list` | 最近 10 条记录 |
|
||||||
|
| `/today` | 今日消费汇总 |
|
||||||
|
| 直接发消息 | 自动记账(如"午饭 25元") |
|
||||||
|
|
||||||
|
### QQ Bot 关键词
|
||||||
|
|
||||||
|
| 关键词 | 说明 |
|
||||||
|
|--------|------|
|
||||||
|
| `帮助` / `菜单` / `功能` | 显示帮助 |
|
||||||
|
| `查看` / `记录` / `列表` | 最近 10 条记录 |
|
||||||
|
| `今日` / `今天` | 今日消费汇总 |
|
||||||
|
| 直接发消息 | 自动记账 |
|
||||||
|
|
||||||
|
### 记账格式
|
||||||
|
|
||||||
|
支持多种自然语言格式:
|
||||||
|
|
||||||
|
```
|
||||||
|
午饭 25元
|
||||||
|
打车 ¥30
|
||||||
|
买咖啡15块
|
||||||
|
车费10元
|
||||||
|
超市买水果38.5
|
||||||
|
```
|
||||||
|
|
||||||
|
### 消费分类
|
||||||
|
|
||||||
|
系统预设了 150+ 个关键词,自动匹配以下分类:
|
||||||
|
|
||||||
|
| 分类 | 示例关键词 |
|
||||||
|
|------|-----------|
|
||||||
|
| 🍜 餐饮 | 早饭、午饭、晚饭、外卖、火锅、面条 |
|
||||||
|
| 🚗 交通 | 打车、车费、地铁、公交、加油 |
|
||||||
|
| 🛒 购物 | 买、淘宝、超市、衣服、手机 |
|
||||||
|
| ☕ 饮品 | 咖啡、奶茶、星巴克、可乐 |
|
||||||
|
| 🍎 水果 | 水果、苹果、香蕉、西瓜 |
|
||||||
|
| 🍪 零食 | 零食、面包、蛋糕、甜品 |
|
||||||
|
| 🏠 住房 | 房租、水电、物业、宽带 |
|
||||||
|
| 📱 通讯 | 话费、流量、充值 |
|
||||||
|
| 💊 医疗 | 看病、药、医院、挂号 |
|
||||||
|
| 🎮 娱乐 | 电影、游戏、健身、旅游 |
|
||||||
|
| 📚 教育 | 书、课、培训、学费 |
|
||||||
|
| 🚬 烟酒 | 烟、白酒、红酒 |
|
||||||
|
|
||||||
|
## 🌐 Web 管理后台
|
||||||
|
|
||||||
|
访问 `http://localhost:9521`:
|
||||||
|
|
||||||
|
- 📊 今日支出 / 本月支出 / 总记录数统计
|
||||||
|
- 📋 最近 50 条记录列表
|
||||||
|
- 🔍 按分类筛选
|
||||||
|
- 🗑️ 删除记录
|
||||||
|
- 📥 CSV 导出(Excel 兼容 BOM 编码)
|
||||||
|
|
||||||
|
## 🏗️ 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
xiaji-go/
|
||||||
|
├── cmd/main.go # 入口文件
|
||||||
|
├── config/config.go # 配置加载与验证
|
||||||
|
├── version/version.go # 版本信息
|
||||||
|
├── models/models.go # 数据模型与分类关键词
|
||||||
|
├── internal/
|
||||||
|
│ ├── bot/telegram.go # Telegram Bot
|
||||||
|
│ ├── qq/qq.go # QQ Bot (WebSocket)
|
||||||
|
│ ├── service/finance.go # 记账核心服务(jieba 分词)
|
||||||
|
│ └── web/server.go # Web 管理后台
|
||||||
|
├── templates/index.html # Web 前端页面
|
||||||
|
├── config.yaml.example # 配置示例
|
||||||
|
├── Dockerfile # Docker 镜像
|
||||||
|
├── docker-compose.yml # Docker Compose
|
||||||
|
└── Makefile # 构建脚本
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 技术栈
|
||||||
|
|
||||||
|
- **语言**: Go 1.22
|
||||||
|
- **Web 框架**: Gin
|
||||||
|
- **数据库**: SQLite (GORM)
|
||||||
|
- **分词**: gojieba (结巴分词 Go 版)
|
||||||
|
- **Telegram SDK**: go-telegram-bot-api v5
|
||||||
|
- **QQ SDK**: tencent-connect/botgo v0.2.1 (WebSocket 模式)
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
MIT
|
||||||
102
cmd/main.go
Normal file
102
cmd/main.go
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"xiaji-go/config"
|
||||||
|
"xiaji-go/internal/bot"
|
||||||
|
"xiaji-go/internal/qq"
|
||||||
|
"xiaji-go/internal/service"
|
||||||
|
"xiaji-go/internal/web"
|
||||||
|
"xiaji-go/models"
|
||||||
|
"xiaji-go/version"
|
||||||
|
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// 版本信息
|
||||||
|
if len(os.Args) > 1 && (os.Args[1] == "-v" || os.Args[1] == "--version" || os.Args[1] == "version") {
|
||||||
|
fmt.Println(version.Info())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("🦞 %s", version.Info())
|
||||||
|
|
||||||
|
// 1. 加载配置
|
||||||
|
cfgPath := "config.yaml"
|
||||||
|
if len(os.Args) > 1 {
|
||||||
|
cfgPath = os.Args[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := config.LoadConfig(cfgPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("无法加载配置: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 初始化数据库
|
||||||
|
db, err := gorm.Open(sqlite.Open(cfg.Database.Path), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("无法连接数据库: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 自动迁移表结构
|
||||||
|
if err := models.Migrate(db); err != nil {
|
||||||
|
log.Fatalf("数据库迁移失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 初始化核心服务
|
||||||
|
finance := service.NewFinanceService(db)
|
||||||
|
defer finance.Close()
|
||||||
|
|
||||||
|
// 全局 context,用于优雅退出
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// 5. 启动 Telegram Bot
|
||||||
|
if cfg.Telegram.Enabled {
|
||||||
|
tgBot, err := bot.NewTGBot(cfg.Telegram.Token, finance)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("⚠️ TG Bot 启动失败: %v", err)
|
||||||
|
} else {
|
||||||
|
go tgBot.Start(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 启动 QQ Bot
|
||||||
|
if cfg.QQBot.Enabled {
|
||||||
|
qqBot := qq.NewQQBot(cfg.QQBot.AppID, cfg.QQBot.Secret, finance)
|
||||||
|
go qqBot.Start(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 启动 Web 后台
|
||||||
|
webServer := web.NewWebServer(db, cfg.Server.Port, cfg.Admin.Username, cfg.Admin.Password)
|
||||||
|
go webServer.Start()
|
||||||
|
|
||||||
|
// 8. 优雅关闭
|
||||||
|
log.Println("🦞 Xiaji-Go 已全面启动")
|
||||||
|
sig := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-sig
|
||||||
|
|
||||||
|
log.Println("⏳ 正在关闭服务...")
|
||||||
|
cancel() // 通知所有 goroutine 停止
|
||||||
|
|
||||||
|
// 等待一点时间让 goroutine 退出
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
|
// 关闭数据库连接
|
||||||
|
sqlDB, err := db.DB()
|
||||||
|
if err == nil {
|
||||||
|
sqlDB.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("👋 Xiaji-Go 已关闭")
|
||||||
|
}
|
||||||
19
config.yaml.example
Normal file
19
config.yaml.example
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
server:
|
||||||
|
port: 9521
|
||||||
|
key: "your-secret-key-change-me"
|
||||||
|
|
||||||
|
database:
|
||||||
|
path: "./xiaji.db"
|
||||||
|
|
||||||
|
admin:
|
||||||
|
username: "admin"
|
||||||
|
password: "your_password"
|
||||||
|
|
||||||
|
telegram:
|
||||||
|
enabled: true
|
||||||
|
token: "YOUR_TELEGRAM_BOT_TOKEN"
|
||||||
|
|
||||||
|
qqbot:
|
||||||
|
enabled: false
|
||||||
|
appid: "YOUR_QQ_BOT_APPID"
|
||||||
|
secret: "YOUR_QQ_BOT_SECRET"
|
||||||
75
config/config.go
Normal file
75
config/config.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Server struct {
|
||||||
|
Port int `yaml:"port"`
|
||||||
|
Key string `yaml:"key"`
|
||||||
|
} `yaml:"server"`
|
||||||
|
Database struct {
|
||||||
|
Path string `yaml:"path"`
|
||||||
|
} `yaml:"database"`
|
||||||
|
Telegram struct {
|
||||||
|
Enabled bool `yaml:"enabled"`
|
||||||
|
Token string `yaml:"token"`
|
||||||
|
} `yaml:"telegram"`
|
||||||
|
QQBot struct {
|
||||||
|
Enabled bool `yaml:"enabled"`
|
||||||
|
AppID string `yaml:"appid"`
|
||||||
|
Secret string `yaml:"secret"`
|
||||||
|
} `yaml:"qqbot"`
|
||||||
|
Admin struct {
|
||||||
|
Username string `yaml:"username"`
|
||||||
|
Password string `yaml:"password"`
|
||||||
|
} `yaml:"admin"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadConfig(path string) (*Config, error) {
|
||||||
|
cfg := &Config{}
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("打开配置文件失败: %w", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
d := yaml.NewDecoder(file)
|
||||||
|
if err := d.Decode(&cfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("解析配置文件失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cfg.Validate(); err != nil {
|
||||||
|
return nil, fmt.Errorf("配置验证失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) Validate() error {
|
||||||
|
if c.Database.Path == "" {
|
||||||
|
return fmt.Errorf("database.path 不能为空")
|
||||||
|
}
|
||||||
|
if c.Server.Port <= 0 || c.Server.Port > 65535 {
|
||||||
|
return fmt.Errorf("server.port 必须在 1-65535 之间,当前值: %d", c.Server.Port)
|
||||||
|
}
|
||||||
|
if c.Server.Key == "" {
|
||||||
|
return fmt.Errorf("server.key 不能为空(用于会话签名)")
|
||||||
|
}
|
||||||
|
if c.Admin.Username == "" || c.Admin.Password == "" {
|
||||||
|
return fmt.Errorf("admin.username 和 admin.password 不能为空")
|
||||||
|
}
|
||||||
|
if c.Telegram.Enabled && c.Telegram.Token == "" {
|
||||||
|
return fmt.Errorf("telegram 已启用但 token 为空")
|
||||||
|
}
|
||||||
|
if c.QQBot.Enabled {
|
||||||
|
if c.QQBot.AppID == "" || c.QQBot.Secret == "" {
|
||||||
|
return fmt.Errorf("qqbot 已启用但 appid 或 secret 为空")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
14
docker-compose.yml
Normal file
14
docker-compose.yml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
xiaji-go:
|
||||||
|
image: ouaone/xiaji-go:latest
|
||||||
|
container_name: xiaji-go
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "9521:9521"
|
||||||
|
volumes:
|
||||||
|
- ./config.yaml:/app/config.yaml:ro
|
||||||
|
- ./data:/app/data
|
||||||
|
environment:
|
||||||
|
- TZ=Asia/Shanghai
|
||||||
49
go.mod
Normal file
49
go.mod
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
module xiaji-go
|
||||||
|
|
||||||
|
go 1.22
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gin-gonic/gin v1.9.1
|
||||||
|
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
|
||||||
|
github.com/tencent-connect/botgo v0.2.1
|
||||||
|
github.com/yanyiwu/gojieba v1.3.0
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
gorm.io/driver/sqlite v1.5.4
|
||||||
|
gorm.io/gorm v1.25.5
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/bytedance/sonic v1.9.1 // indirect
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||||
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||||
|
github.com/go-resty/resty/v2 v2.6.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/gorilla/websocket v1.4.2 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||||
|
github.com/leodido/go-urn v1.2.4 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.17 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||||
|
github.com/tidwall/gjson v1.9.3 // indirect
|
||||||
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
|
github.com/tidwall/pretty v1.2.0 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||||
|
golang.org/x/arch v0.3.0 // indirect
|
||||||
|
golang.org/x/crypto v0.16.0 // indirect
|
||||||
|
golang.org/x/net v0.19.0 // indirect
|
||||||
|
golang.org/x/oauth2 v0.23.0 // indirect
|
||||||
|
golang.org/x/sync v0.1.0 // indirect
|
||||||
|
golang.org/x/sys v0.15.0 // indirect
|
||||||
|
golang.org/x/text v0.14.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.30.0 // indirect
|
||||||
|
)
|
||||||
236
go.sum
Normal file
236
go.sum
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
||||||
|
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||||
|
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||||
|
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||||
|
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||||
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
|
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||||
|
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
||||||
|
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||||
|
github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w=
|
||||||
|
github.com/go-resty/resty/v2 v2.6.0 h1:joIR5PNLM2EFqqESUjCMGXrWmXNHEU9CEiK813oKYS4=
|
||||||
|
github.com/go-resty/resty/v2 v2.6.0/go.mod h1:PwvJS6hvaPkjtjNg9ph+VrSD92bi5Zq73w/BIH7cC3Q=
|
||||||
|
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||||
|
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
|
||||||
|
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
|
||||||
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||||
|
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||||
|
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||||
|
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||||
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||||
|
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||||
|
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||||
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||||
|
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||||
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||||
|
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||||
|
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||||
|
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||||
|
github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||||
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||||
|
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/tencent-connect/botgo v0.2.1 h1:+BrTt9Zh+awL28GWC4g5Na3nQaGRWb0N5IctS8WqBCk=
|
||||||
|
github.com/tencent-connect/botgo v0.2.1/go.mod h1:oO1sG9ybhXNickvt+CVym5khwQ+uKhTR+IhTqEfOVsI=
|
||||||
|
github.com/tidwall/gjson v1.9.3 h1:hqzS9wAHMO+KVBBkLxYdkEeeFHuqr95GfClRLKlgK0E=
|
||||||
|
github.com/tidwall/gjson v1.9.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
|
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||||
|
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||||
|
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
|
github.com/yanyiwu/gojieba v1.3.0 h1:6VeaPOR+MawnImdeSvWNr7rP4tvUfnGlEKaoBnR33Ds=
|
||||||
|
github.com/yanyiwu/gojieba v1.3.0/go.mod h1:54wkP7sMJ6bklf7yPl6F+JG71dzVUU1WigZbR47nGdY=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
|
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||||
|
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
|
||||||
|
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
|
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
|
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||||
|
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||||
|
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
|
||||||
|
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||||
|
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
|
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||||
|
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||||
|
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||||
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
|
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||||
|
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gorm.io/driver/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0=
|
||||||
|
gorm.io/driver/sqlite v1.5.4/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
|
||||||
|
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
|
||||||
|
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||||
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||||
122
internal/bot/telegram.go
Normal file
122
internal/bot/telegram.go
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
package bot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"xiaji-go/internal/service"
|
||||||
|
|
||||||
|
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TGBot struct {
|
||||||
|
api *tgbotapi.BotAPI
|
||||||
|
finance *service.FinanceService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTGBot(token string, finance *service.FinanceService) (*TGBot, error) {
|
||||||
|
bot, err := tgbotapi.NewBotAPI(token)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &TGBot{api: bot, finance: finance}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *TGBot) Start(ctx context.Context) {
|
||||||
|
u := tgbotapi.NewUpdate(0)
|
||||||
|
u.Timeout = 60
|
||||||
|
updates := b.api.GetUpdatesChan(u)
|
||||||
|
|
||||||
|
log.Println("🚀 Telegram Bot 已启动")
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
log.Println("⏳ Telegram Bot 正在停止...")
|
||||||
|
b.api.StopReceivingUpdates()
|
||||||
|
return
|
||||||
|
case update, ok := <-updates:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if update.Message == nil || update.Message.Text == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b.handleMessage(update.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *TGBot) handleMessage(msg *tgbotapi.Message) {
|
||||||
|
text := msg.Text
|
||||||
|
chatID := msg.Chat.ID
|
||||||
|
userID := msg.From.ID
|
||||||
|
|
||||||
|
var reply string
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case text == "/start":
|
||||||
|
reply = "🦞 欢迎使用虾记记账!\n\n直接发送消费描述即可记账,例如:\n• 午饭 25元\n• 打车 ¥30\n• 买咖啡15块\n\n命令:\n/list - 查看最近记录\n/help - 帮助"
|
||||||
|
|
||||||
|
case text == "/help":
|
||||||
|
reply = "📖 使用说明:\n\n直接发送带金额的文本即可自动记账。\n系统会自动识别金额和消费分类。\n\n支持格式:\n• 午饭 25元\n• ¥30 打车\n• 买水果15块\n\n命令:\n/list - 最近10条记录\n/today - 今日汇总\n/start - 欢迎信息"
|
||||||
|
|
||||||
|
case text == "/today":
|
||||||
|
today := time.Now().Format("2006-01-02")
|
||||||
|
items, err := b.finance.GetTransactionsByDate(userID, today)
|
||||||
|
if err != nil {
|
||||||
|
reply = "❌ 查询失败"
|
||||||
|
} else if len(items) == 0 {
|
||||||
|
reply = fmt.Sprintf("📭 %s 暂无消费记录", today)
|
||||||
|
} else {
|
||||||
|
var sb strings.Builder
|
||||||
|
var total int64
|
||||||
|
sb.WriteString(fmt.Sprintf("📊 今日(%s)消费:\n\n", today))
|
||||||
|
for _, item := range items {
|
||||||
|
sb.WriteString(fmt.Sprintf("• %s:%.2f元\n", item.Category, item.AmountYuan()))
|
||||||
|
total += item.Amount
|
||||||
|
}
|
||||||
|
sb.WriteString(fmt.Sprintf("\n💰 共 %d 笔,合计 %.2f 元", len(items), float64(total)/100.0))
|
||||||
|
reply = sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
case text == "/list":
|
||||||
|
items, err := b.finance.GetTransactions(userID, 10)
|
||||||
|
if err != nil {
|
||||||
|
reply = "❌ 查询失败"
|
||||||
|
} else if len(items) == 0 {
|
||||||
|
reply = "📭 暂无记录"
|
||||||
|
} else {
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString("📋 最近记录:\n\n")
|
||||||
|
for _, item := range items {
|
||||||
|
sb.WriteString(fmt.Sprintf("• [%s] %s:%.2f元\n", item.Date, item.Category, item.AmountYuan()))
|
||||||
|
}
|
||||||
|
reply = sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
case strings.HasPrefix(text, "/"):
|
||||||
|
reply = "❓ 未知命令,输入 /help 查看帮助"
|
||||||
|
|
||||||
|
default:
|
||||||
|
// 记账逻辑
|
||||||
|
amount, category, err := b.finance.AddTransaction(userID, text)
|
||||||
|
if err != nil {
|
||||||
|
reply = "❌ 记账失败,请稍后重试"
|
||||||
|
log.Printf("记账失败 user=%d: %v", userID, err)
|
||||||
|
} else if amount == 0 {
|
||||||
|
reply = "📍 没看到金额,这笔花了多少钱?"
|
||||||
|
} else {
|
||||||
|
amountYuan := float64(amount) / 100.0
|
||||||
|
reply = fmt.Sprintf("✅ 已记入【%s】:%.2f元\n📝 备注:%s", category, amountYuan, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m := tgbotapi.NewMessage(chatID, reply)
|
||||||
|
if _, err := b.api.Send(m); err != nil {
|
||||||
|
log.Printf("发送消息失败 chat=%d: %v", chatID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
212
internal/qq/qq.go
Normal file
212
internal/qq/qq.go
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
package qq
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"hash/fnv"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"xiaji-go/internal/service"
|
||||||
|
|
||||||
|
"github.com/tencent-connect/botgo"
|
||||||
|
"github.com/tencent-connect/botgo/dto"
|
||||||
|
"github.com/tencent-connect/botgo/dto/message"
|
||||||
|
"github.com/tencent-connect/botgo/event"
|
||||||
|
"github.com/tencent-connect/botgo/openapi"
|
||||||
|
"github.com/tencent-connect/botgo/token"
|
||||||
|
)
|
||||||
|
|
||||||
|
type QQBot struct {
|
||||||
|
api openapi.OpenAPI
|
||||||
|
finance *service.FinanceService
|
||||||
|
credentials *token.QQBotCredentials
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewQQBot(appID string, secret string, finance *service.FinanceService) *QQBot {
|
||||||
|
return &QQBot{
|
||||||
|
finance: finance,
|
||||||
|
credentials: &token.QQBotCredentials{
|
||||||
|
AppID: appID,
|
||||||
|
AppSecret: secret,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// hashUserID 将 QQ 的字符串用户标识转为 int64
|
||||||
|
func hashUserID(authorID string) int64 {
|
||||||
|
h := fnv.New64a()
|
||||||
|
h.Write([]byte(authorID))
|
||||||
|
return int64(h.Sum64())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *QQBot) Start(ctx context.Context) {
|
||||||
|
// 创建 token source 并启动自动刷新
|
||||||
|
tokenSource := token.NewQQBotTokenSource(b.credentials)
|
||||||
|
if err := token.StartRefreshAccessToken(ctx, tokenSource); err != nil {
|
||||||
|
log.Printf("❌ QQ Bot Token 刷新失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化 OpenAPI
|
||||||
|
b.api = botgo.NewOpenAPI(b.credentials.AppID, tokenSource).WithTimeout(5 * time.Second)
|
||||||
|
|
||||||
|
// 注册事件处理器
|
||||||
|
_ = event.RegisterHandlers(
|
||||||
|
b.groupATMessageHandler(),
|
||||||
|
b.c2cMessageHandler(),
|
||||||
|
b.channelATMessageHandler(),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 获取 WebSocket 接入信息
|
||||||
|
wsInfo, err := b.api.WS(ctx, nil, "")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("❌ QQ Bot 获取 WS 信息失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置 intents: 群聊和C2C (1<<25) + 公域消息 (1<<30)
|
||||||
|
intent := dto.Intent(1<<25 | 1<<30)
|
||||||
|
|
||||||
|
log.Printf("🚀 QQ Bot 已启动 (WebSocket, shards=%d)", wsInfo.Shards)
|
||||||
|
|
||||||
|
// 启动 session manager (阻塞)
|
||||||
|
if err := botgo.NewSessionManager().Start(wsInfo, tokenSource, &intent); err != nil {
|
||||||
|
log.Printf("❌ QQ Bot WebSocket 断开: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isCommand 判断是否匹配命令关键词
|
||||||
|
func isCommand(text string, keywords ...string) bool {
|
||||||
|
for _, kw := range keywords {
|
||||||
|
if text == kw {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// processAndReply 通用记账处理
|
||||||
|
func (b *QQBot) processAndReply(userID string, content string) string {
|
||||||
|
uid := hashUserID(userID)
|
||||||
|
text := strings.TrimSpace(message.ETLInput(content))
|
||||||
|
if text == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
today := time.Now().Format("2006-01-02")
|
||||||
|
|
||||||
|
// 命令处理
|
||||||
|
switch {
|
||||||
|
case isCommand(text, "帮助", "help", "/help", "/start", "菜单", "功能"):
|
||||||
|
return "🦞 虾记记账\n\n" +
|
||||||
|
"直接发送消费描述即可记账:\n" +
|
||||||
|
"• 午饭 25元\n" +
|
||||||
|
"• 打车 ¥30\n" +
|
||||||
|
"• 买咖啡15块\n\n" +
|
||||||
|
"📋 命令列表:\n" +
|
||||||
|
"• 记录/查看 — 最近10条\n" +
|
||||||
|
"• 今日/今天 — 今日汇总\n" +
|
||||||
|
"• 帮助 — 本帮助信息"
|
||||||
|
|
||||||
|
case isCommand(text, "查看", "记录", "列表", "list", "/list", "最近"):
|
||||||
|
items, err := b.finance.GetTransactions(uid, 10)
|
||||||
|
if err != nil {
|
||||||
|
return "❌ 查询失败"
|
||||||
|
}
|
||||||
|
if len(items) == 0 {
|
||||||
|
return "📭 暂无记录"
|
||||||
|
}
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString("📋 最近记录:\n\n")
|
||||||
|
for _, item := range items {
|
||||||
|
sb.WriteString(fmt.Sprintf("• [%s] %s:%.2f元\n", item.Date, item.Category, item.AmountYuan()))
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
|
||||||
|
case isCommand(text, "今日", "今天", "today"):
|
||||||
|
items, err := b.finance.GetTransactionsByDate(uid, today)
|
||||||
|
if err != nil {
|
||||||
|
return "❌ 查询失败"
|
||||||
|
}
|
||||||
|
if len(items) == 0 {
|
||||||
|
return fmt.Sprintf("📭 %s 暂无消费记录", today)
|
||||||
|
}
|
||||||
|
var sb strings.Builder
|
||||||
|
var total int64
|
||||||
|
sb.WriteString(fmt.Sprintf("📊 今日(%s)消费:\n\n", today))
|
||||||
|
for _, item := range items {
|
||||||
|
sb.WriteString(fmt.Sprintf("• %s:%.2f元\n", item.Category, item.AmountYuan()))
|
||||||
|
total += item.Amount
|
||||||
|
}
|
||||||
|
sb.WriteString(fmt.Sprintf("\n💰 共 %d 笔,合计 %.2f 元", len(items), float64(total)/100.0))
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
amount, category, err := b.finance.AddTransaction(uid, text)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("QQ记账失败 user=%s: %v", userID, err)
|
||||||
|
return "❌ 记账失败,请稍后重试"
|
||||||
|
}
|
||||||
|
if amount == 0 {
|
||||||
|
return "📍 没看到金额,这笔花了多少钱?"
|
||||||
|
}
|
||||||
|
|
||||||
|
amountYuan := float64(amount) / 100.0
|
||||||
|
return fmt.Sprintf("✅ 已记入【%s】:%.2f元\n📝 备注:%s", category, amountYuan, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// channelATMessageHandler 频道@机器人消息
|
||||||
|
func (b *QQBot) channelATMessageHandler() event.ATMessageEventHandler {
|
||||||
|
return func(ev *dto.WSPayload, data *dto.WSATMessageData) error {
|
||||||
|
reply := b.processAndReply(data.Author.ID, data.Content)
|
||||||
|
if reply == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_, err := b.api.PostMessage(context.Background(), data.ChannelID, &dto.MessageToCreate{
|
||||||
|
MsgID: data.ID,
|
||||||
|
Content: reply,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("QQ频道消息发送失败: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// groupATMessageHandler 群@机器人消息
|
||||||
|
func (b *QQBot) groupATMessageHandler() event.GroupATMessageEventHandler {
|
||||||
|
return func(ev *dto.WSPayload, data *dto.WSGroupATMessageData) error {
|
||||||
|
reply := b.processAndReply(data.Author.ID, data.Content)
|
||||||
|
if reply == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_, err := b.api.PostGroupMessage(context.Background(), data.GroupID, dto.MessageToCreate{
|
||||||
|
MsgID: data.ID,
|
||||||
|
Content: reply,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("QQ群消息发送失败: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// c2cMessageHandler C2C 私聊消息
|
||||||
|
func (b *QQBot) c2cMessageHandler() event.C2CMessageEventHandler {
|
||||||
|
return func(ev *dto.WSPayload, data *dto.WSC2CMessageData) error {
|
||||||
|
reply := b.processAndReply(data.Author.ID, data.Content)
|
||||||
|
if reply == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_, err := b.api.PostC2CMessage(context.Background(), data.Author.ID, dto.MessageToCreate{
|
||||||
|
MsgID: data.ID,
|
||||||
|
Content: reply,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("QQ私聊消息发送失败: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
116
internal/service/finance.go
Normal file
116
internal/service/finance.go
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"xiaji-go/models"
|
||||||
|
|
||||||
|
"github.com/yanyiwu/gojieba"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FinanceService struct {
|
||||||
|
db *gorm.DB
|
||||||
|
jieba *gojieba.Jieba
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFinanceService(db *gorm.DB) *FinanceService {
|
||||||
|
return &FinanceService{
|
||||||
|
db: db,
|
||||||
|
jieba: gojieba.NewJieba(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *FinanceService) Close() {
|
||||||
|
s.jieba.Free()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseText 从自然语言文本中提取金额(分)和分类
|
||||||
|
func (s *FinanceService) ParseText(text string) (int64, string) {
|
||||||
|
// 1. 提取金额 — 优先匹配带单位的,如 "15.5元"、"¥30"、"20块"
|
||||||
|
amountPatterns := []*regexp.Regexp{
|
||||||
|
regexp.MustCompile(`[¥¥]\s*(\d+\.?\d*)`),
|
||||||
|
regexp.MustCompile(`(\d+\.?\d*)\s*[元块]`),
|
||||||
|
}
|
||||||
|
|
||||||
|
var amountStr string
|
||||||
|
for _, re := range amountPatterns {
|
||||||
|
m := re.FindStringSubmatch(text)
|
||||||
|
if len(m) > 1 {
|
||||||
|
amountStr = m[1]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兜底:取最后一个独立数字
|
||||||
|
if amountStr == "" {
|
||||||
|
re := regexp.MustCompile(`(\d+\.?\d*)`)
|
||||||
|
matches := re.FindAllStringSubmatch(text, -1)
|
||||||
|
if len(matches) > 0 {
|
||||||
|
amountStr = matches[len(matches)-1][1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if amountStr == "" {
|
||||||
|
return 0, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
amountFloat, err := strconv.ParseFloat(amountStr, 64)
|
||||||
|
if err != nil || amountFloat <= 0 {
|
||||||
|
return 0, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转为分
|
||||||
|
amountCents := int64(math.Round(amountFloat * 100))
|
||||||
|
|
||||||
|
// 2. 提取分类(Jieba 分词 + 数据库匹配)
|
||||||
|
words := s.jieba.Cut(text, true)
|
||||||
|
category := "其他"
|
||||||
|
|
||||||
|
for _, word := range words {
|
||||||
|
var ck models.CategoryKeyword
|
||||||
|
if err := s.db.Where("keyword = ?", word).First(&ck).Error; err == nil {
|
||||||
|
category = ck.Category
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return amountCents, category
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddTransaction 解析文本并创建一条交易记录
|
||||||
|
func (s *FinanceService) AddTransaction(userID int64, text string) (int64, string, error) {
|
||||||
|
amount, category := s.ParseText(text)
|
||||||
|
if amount == 0 {
|
||||||
|
return 0, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tx := models.Transaction{
|
||||||
|
UserID: userID,
|
||||||
|
Amount: amount,
|
||||||
|
Category: category,
|
||||||
|
Note: text,
|
||||||
|
Date: time.Now().Format("2006-01-02"),
|
||||||
|
}
|
||||||
|
|
||||||
|
return amount, category, s.db.Create(&tx).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTransactions 获取用户的交易记录
|
||||||
|
func (s *FinanceService) GetTransactions(userID int64, limit int) ([]models.Transaction, error) {
|
||||||
|
var items []models.Transaction
|
||||||
|
err := s.db.Where("user_id = ? AND is_deleted = ?", userID, false).
|
||||||
|
Order("id desc").Limit(limit).Find(&items).Error
|
||||||
|
return items, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTransactionsByDate 获取用户指定日期的交易记录
|
||||||
|
func (s *FinanceService) GetTransactionsByDate(userID int64, date string) ([]models.Transaction, error) {
|
||||||
|
var items []models.Transaction
|
||||||
|
err := s.db.Where("user_id = ? AND date = ? AND is_deleted = ?", userID, date, false).
|
||||||
|
Order("id desc").Find(&items).Error
|
||||||
|
return items, err
|
||||||
|
}
|
||||||
116
internal/web/server.go
Normal file
116
internal/web/server.go
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"xiaji-go/models"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WebServer struct {
|
||||||
|
db *gorm.DB
|
||||||
|
port int
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWebServer(db *gorm.DB, port int, username, password string) *WebServer {
|
||||||
|
return &WebServer{db: db, port: port, username: username, password: password}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WebServer) Start() {
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
r := gin.Default()
|
||||||
|
r.LoadHTMLGlob("templates/*")
|
||||||
|
|
||||||
|
// 页面
|
||||||
|
r.GET("/", s.handleIndex)
|
||||||
|
r.GET("/api/records", s.handleRecords)
|
||||||
|
r.POST("/delete/:id", s.handleDelete)
|
||||||
|
r.GET("/export", s.handleExport)
|
||||||
|
|
||||||
|
// 健康检查
|
||||||
|
r.GET("/health", func(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||||
|
})
|
||||||
|
|
||||||
|
logAddr := fmt.Sprintf(":%d", s.port)
|
||||||
|
fmt.Printf("🌐 Web后台运行在 http://127.0.0.1%s\n", logAddr)
|
||||||
|
if err := r.Run(logAddr); err != nil {
|
||||||
|
fmt.Printf("❌ Web服务启动失败: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WebServer) handleIndex(c *gin.Context) {
|
||||||
|
c.HTML(http.StatusOK, "index.html", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WebServer) handleRecords(c *gin.Context) {
|
||||||
|
var items []models.Transaction
|
||||||
|
s.db.Where("is_deleted = ?", false).Order("id desc").Limit(50).Find(&items)
|
||||||
|
|
||||||
|
type txResponse struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
Amount float64 `json:"amount"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
Note string `json:"note"`
|
||||||
|
Date string `json:"date"`
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := make([]txResponse, len(items))
|
||||||
|
for i, item := range items {
|
||||||
|
resp[i] = txResponse{
|
||||||
|
ID: item.ID,
|
||||||
|
UserID: item.UserID,
|
||||||
|
Amount: item.AmountYuan(),
|
||||||
|
Category: item.Category,
|
||||||
|
Note: item.Note,
|
||||||
|
Date: item.Date,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WebServer) handleDelete(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := s.db.Model(&models.Transaction{}).Where("id = ? AND is_deleted = ?", id, false).Update("is_deleted", true)
|
||||||
|
if result.Error != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除失败"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "记录不存在或已删除"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"status": "success"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WebServer) handleExport(c *gin.Context) {
|
||||||
|
var items []models.Transaction
|
||||||
|
s.db.Where("is_deleted = ?", false).Order("date asc, id asc").Find(&items)
|
||||||
|
|
||||||
|
c.Header("Content-Type", "text/csv; charset=utf-8")
|
||||||
|
c.Header("Content-Disposition", "attachment; filename=transactions.csv")
|
||||||
|
|
||||||
|
// BOM for Excel
|
||||||
|
c.Writer.Write([]byte{0xEF, 0xBB, 0xBF})
|
||||||
|
c.Writer.WriteString("ID,日期,分类,金额(元),备注\n")
|
||||||
|
|
||||||
|
for _, item := range items {
|
||||||
|
line := fmt.Sprintf("%d,%s,%s,%.2f,\"%s\"\n", item.ID, item.Date, item.Category, item.AmountYuan(), item.Note)
|
||||||
|
c.Writer.WriteString(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
136
models/models.go
Normal file
136
models/models.go
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Transaction struct {
|
||||||
|
ID uint `gorm:"primaryKey" json:"id"`
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
Amount int64 `json:"amount"` // 金额,单位:分
|
||||||
|
Category string `gorm:"size:50" json:"category"`
|
||||||
|
Note string `json:"note"`
|
||||||
|
Date string `gorm:"size:20;index" json:"date"`
|
||||||
|
IsDeleted bool `gorm:"default:false" json:"is_deleted"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CategoryKeyword struct {
|
||||||
|
ID uint `gorm:"primaryKey"`
|
||||||
|
Keyword string `gorm:"uniqueIndex;size:50"`
|
||||||
|
Category string `gorm:"size:50"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AmountYuan 返回元为单位的金额(显示用)
|
||||||
|
func (t *Transaction) AmountYuan() float64 {
|
||||||
|
return float64(t.Amount) / 100.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate 自动迁移数据库表结构并初始化分类关键词
|
||||||
|
func Migrate(db *gorm.DB) error {
|
||||||
|
if err := db.AutoMigrate(&Transaction{}, &CategoryKeyword{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已有关键词数据
|
||||||
|
var count int64
|
||||||
|
db.Model(&CategoryKeyword{}).Count(&count)
|
||||||
|
if count > 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预设分类关键词
|
||||||
|
keywords := []CategoryKeyword{
|
||||||
|
// 餐饮
|
||||||
|
{Keyword: "早餐", Category: "餐饮"}, {Keyword: "午餐", Category: "餐饮"}, {Keyword: "晚餐", Category: "餐饮"},
|
||||||
|
{Keyword: "早饭", Category: "餐饮"}, {Keyword: "午饭", Category: "餐饮"}, {Keyword: "晚饭", Category: "餐饮"},
|
||||||
|
{Keyword: "吃饭", Category: "餐饮"}, {Keyword: "吃", Category: "餐饮"}, {Keyword: "饭", Category: "餐饮"},
|
||||||
|
{Keyword: "面", Category: "餐饮"}, {Keyword: "粉", Category: "餐饮"}, {Keyword: "粥", Category: "餐饮"},
|
||||||
|
{Keyword: "火锅", Category: "餐饮"}, {Keyword: "烧烤", Category: "餐饮"}, {Keyword: "烤肉", Category: "餐饮"},
|
||||||
|
{Keyword: "外卖", Category: "餐饮"}, {Keyword: "点餐", Category: "餐饮"}, {Keyword: "宵夜", Category: "餐饮"},
|
||||||
|
{Keyword: "夜宵", Category: "餐饮"}, {Keyword: "小吃", Category: "餐饮"}, {Keyword: "快餐", Category: "餐饮"},
|
||||||
|
{Keyword: "饺子", Category: "餐饮"}, {Keyword: "面条", Category: "餐饮"}, {Keyword: "米饭", Category: "餐饮"},
|
||||||
|
{Keyword: "菜", Category: "餐饮"}, {Keyword: "肉", Category: "餐饮"}, {Keyword: "鱼", Category: "餐饮"},
|
||||||
|
{Keyword: "鸡", Category: "餐饮"}, {Keyword: "蛋", Category: "餐饮"}, {Keyword: "汤", Category: "餐饮"},
|
||||||
|
{Keyword: "麻辣烫", Category: "餐饮"}, {Keyword: "炒饭", Category: "餐饮"}, {Keyword: "盖饭", Category: "餐饮"},
|
||||||
|
{Keyword: "包子", Category: "餐饮"}, {Keyword: "馒头", Category: "餐饮"}, {Keyword: "饼", Category: "餐饮"},
|
||||||
|
{Keyword: "食堂", Category: "餐饮"}, {Keyword: "餐厅", Category: "餐饮"}, {Keyword: "饭店", Category: "餐饮"},
|
||||||
|
{Keyword: "美团", Category: "餐饮"}, {Keyword: "饿了么", Category: "餐饮"},
|
||||||
|
|
||||||
|
// 交通
|
||||||
|
{Keyword: "打车", Category: "交通"}, {Keyword: "车费", Category: "交通"}, {Keyword: "出租车", Category: "交通"},
|
||||||
|
{Keyword: "滴滴", Category: "交通"}, {Keyword: "公交", Category: "交通"}, {Keyword: "地铁", Category: "交通"},
|
||||||
|
{Keyword: "高铁", Category: "交通"}, {Keyword: "火车", Category: "交通"}, {Keyword: "飞机", Category: "交通"},
|
||||||
|
{Keyword: "机票", Category: "交通"}, {Keyword: "车票", Category: "交通"}, {Keyword: "船票", Category: "交通"},
|
||||||
|
{Keyword: "加油", Category: "交通"}, {Keyword: "油费", Category: "交通"}, {Keyword: "停车", Category: "交通"},
|
||||||
|
{Keyword: "停车费", Category: "交通"}, {Keyword: "过路费", Category: "交通"}, {Keyword: "高速", Category: "交通"},
|
||||||
|
{Keyword: "骑车", Category: "交通"}, {Keyword: "单车", Category: "交通"}, {Keyword: "共享", Category: "交通"},
|
||||||
|
{Keyword: "顺风车", Category: "交通"}, {Keyword: "快车", Category: "交通"}, {Keyword: "专车", Category: "交通"},
|
||||||
|
{Keyword: "拼车", Category: "交通"}, {Keyword: "出行", Category: "交通"}, {Keyword: "通勤", Category: "交通"},
|
||||||
|
|
||||||
|
// 购物
|
||||||
|
{Keyword: "买", Category: "购物"}, {Keyword: "购物", Category: "购物"}, {Keyword: "淘宝", Category: "购物"},
|
||||||
|
{Keyword: "京东", Category: "购物"}, {Keyword: "拼多多", Category: "购物"}, {Keyword: "网购", Category: "购物"},
|
||||||
|
{Keyword: "超市", Category: "购物"}, {Keyword: "商场", Category: "购物"}, {Keyword: "衣服", Category: "购物"},
|
||||||
|
{Keyword: "鞋", Category: "购物"}, {Keyword: "裤子", Category: "购物"}, {Keyword: "裙子", Category: "购物"},
|
||||||
|
{Keyword: "包", Category: "购物"}, {Keyword: "手机", Category: "购物"}, {Keyword: "电脑", Category: "购物"},
|
||||||
|
{Keyword: "日用品", Category: "购物"}, {Keyword: "生活用品", Category: "购物"},
|
||||||
|
|
||||||
|
// 饮品
|
||||||
|
{Keyword: "咖啡", Category: "饮品"}, {Keyword: "奶茶", Category: "饮品"}, {Keyword: "茶", Category: "饮品"},
|
||||||
|
{Keyword: "饮料", Category: "饮品"}, {Keyword: "水", Category: "饮品"}, {Keyword: "果汁", Category: "饮品"},
|
||||||
|
{Keyword: "星巴克", Category: "饮品"}, {Keyword: "瑞幸", Category: "饮品"}, {Keyword: "喜茶", Category: "饮品"},
|
||||||
|
{Keyword: "蜜雪", Category: "饮品"}, {Keyword: "可乐", Category: "饮品"}, {Keyword: "啤酒", Category: "饮品"},
|
||||||
|
{Keyword: "酒", Category: "饮品"}, {Keyword: "牛奶", Category: "饮品"},
|
||||||
|
|
||||||
|
// 水果
|
||||||
|
{Keyword: "水果", Category: "水果"}, {Keyword: "苹果", Category: "水果"}, {Keyword: "香蕉", Category: "水果"},
|
||||||
|
{Keyword: "橘子", Category: "水果"}, {Keyword: "橙子", Category: "水果"}, {Keyword: "葡萄", Category: "水果"},
|
||||||
|
{Keyword: "西瓜", Category: "水果"}, {Keyword: "草莓", Category: "水果"}, {Keyword: "芒果", Category: "水果"},
|
||||||
|
|
||||||
|
// 零食
|
||||||
|
{Keyword: "零食", Category: "零食"}, {Keyword: "薯片", Category: "零食"}, {Keyword: "糖", Category: "零食"},
|
||||||
|
{Keyword: "巧克力", Category: "零食"}, {Keyword: "饼干", Category: "零食"}, {Keyword: "面包", Category: "零食"},
|
||||||
|
{Keyword: "蛋糕", Category: "零食"}, {Keyword: "甜品", Category: "零食"}, {Keyword: "甜点", Category: "零食"},
|
||||||
|
|
||||||
|
// 住房
|
||||||
|
{Keyword: "房租", Category: "住房"}, {Keyword: "租房", Category: "住房"}, {Keyword: "水电", Category: "住房"},
|
||||||
|
{Keyword: "电费", Category: "住房"}, {Keyword: "水费", Category: "住房"}, {Keyword: "燃气", Category: "住房"},
|
||||||
|
{Keyword: "物业", Category: "住房"}, {Keyword: "宽带", Category: "住房"}, {Keyword: "网费", Category: "住房"},
|
||||||
|
|
||||||
|
// 通讯
|
||||||
|
{Keyword: "话费", Category: "通讯"}, {Keyword: "流量", Category: "通讯"}, {Keyword: "充值", Category: "通讯"},
|
||||||
|
{Keyword: "手机费", Category: "通讯"},
|
||||||
|
|
||||||
|
// 医疗
|
||||||
|
{Keyword: "看病", Category: "医疗"}, {Keyword: "药", Category: "医疗"}, {Keyword: "医院", Category: "医疗"},
|
||||||
|
{Keyword: "挂号", Category: "医疗"}, {Keyword: "体检", Category: "医疗"}, {Keyword: "医疗", Category: "医疗"},
|
||||||
|
{Keyword: "门诊", Category: "医疗"}, {Keyword: "牙", Category: "医疗"},
|
||||||
|
|
||||||
|
// 娱乐
|
||||||
|
{Keyword: "电影", Category: "娱乐"}, {Keyword: "游戏", Category: "娱乐"}, {Keyword: "KTV", Category: "娱乐"},
|
||||||
|
{Keyword: "唱歌", Category: "娱乐"}, {Keyword: "旅游", Category: "娱乐"}, {Keyword: "景点", Category: "娱乐"},
|
||||||
|
{Keyword: "门票", Category: "娱乐"}, {Keyword: "健身", Category: "娱乐"}, {Keyword: "运动", Category: "娱乐"},
|
||||||
|
{Keyword: "会员", Category: "娱乐"}, {Keyword: "VIP", Category: "娱乐"},
|
||||||
|
|
||||||
|
// 教育
|
||||||
|
{Keyword: "书", Category: "教育"}, {Keyword: "课", Category: "教育"}, {Keyword: "培训", Category: "教育"},
|
||||||
|
{Keyword: "学费", Category: "教育"}, {Keyword: "考试", Category: "教育"}, {Keyword: "学习", Category: "教育"},
|
||||||
|
|
||||||
|
// 烟酒
|
||||||
|
{Keyword: "烟", Category: "烟酒"}, {Keyword: "香烟", Category: "烟酒"}, {Keyword: "白酒", Category: "烟酒"},
|
||||||
|
{Keyword: "红酒", Category: "烟酒"},
|
||||||
|
|
||||||
|
// 红包/转账
|
||||||
|
{Keyword: "红包", Category: "红包"}, {Keyword: "转账", Category: "转账"}, {Keyword: "借", Category: "转账"},
|
||||||
|
{Keyword: "还钱", Category: "转账"},
|
||||||
|
|
||||||
|
// 宠物
|
||||||
|
{Keyword: "猫粮", Category: "宠物"}, {Keyword: "狗粮", Category: "宠物"}, {Keyword: "宠物", Category: "宠物"},
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.CreateInBatches(keywords, 50).Error
|
||||||
|
}
|
||||||
193
templates/index.html
Normal file
193
templates/index.html
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>🦞 虾记记账</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #f0f2f5; color: #333; min-height: 100vh; }
|
||||||
|
|
||||||
|
.header { background: linear-gradient(135deg, #ff6b6b, #ee5a24); color: #fff; padding: 20px; text-align: center; position: sticky; top: 0; z-index: 100; box-shadow: 0 2px 10px rgba(0,0,0,.15); }
|
||||||
|
.header h1 { font-size: 24px; margin-bottom: 4px; }
|
||||||
|
.header .subtitle { font-size: 13px; opacity: .85; }
|
||||||
|
|
||||||
|
.stats { display: flex; gap: 10px; padding: 15px; overflow-x: auto; }
|
||||||
|
.stat-card { flex: 1; min-width: 120px; background: #fff; border-radius: 12px; padding: 15px; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,.06); }
|
||||||
|
.stat-card .num { font-size: 22px; font-weight: 700; color: #ee5a24; }
|
||||||
|
.stat-card .label { font-size: 12px; color: #999; margin-top: 4px; }
|
||||||
|
|
||||||
|
.toolbar { display: flex; gap: 10px; padding: 0 15px 10px; align-items: center; }
|
||||||
|
.toolbar a { padding: 8px 16px; background: #ee5a24; color: #fff; border-radius: 8px; text-decoration: none; font-size: 13px; white-space: nowrap; }
|
||||||
|
.toolbar a:hover { background: #d63031; }
|
||||||
|
.toolbar .spacer { flex: 1; }
|
||||||
|
.filter-select { padding: 8px 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 13px; background: #fff; }
|
||||||
|
|
||||||
|
.tx-list { padding: 0 15px 80px; }
|
||||||
|
.tx-card { background: #fff; border-radius: 12px; padding: 14px 16px; margin-bottom: 10px; box-shadow: 0 1px 4px rgba(0,0,0,.05); display: flex; align-items: center; gap: 12px; transition: transform .15s; }
|
||||||
|
.tx-card:active { transform: scale(.98); }
|
||||||
|
.tx-icon { width: 42px; height: 42px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 20px; flex-shrink: 0; }
|
||||||
|
.tx-info { flex: 1; min-width: 0; }
|
||||||
|
.tx-category { font-weight: 600; font-size: 15px; }
|
||||||
|
.tx-note { font-size: 12px; color: #999; margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.tx-right { text-align: right; flex-shrink: 0; }
|
||||||
|
.tx-amount { font-size: 17px; font-weight: 700; color: #e74c3c; }
|
||||||
|
.tx-date { font-size: 11px; color: #bbb; margin-top: 2px; }
|
||||||
|
.tx-actions { margin-top: 4px; }
|
||||||
|
.btn-del { background: none; border: 1px solid #e74c3c; color: #e74c3c; border-radius: 4px; padding: 2px 8px; font-size: 11px; cursor: pointer; }
|
||||||
|
.btn-del:hover { background: #e74c3c; color: #fff; }
|
||||||
|
|
||||||
|
.empty { text-align: center; padding: 60px 20px; color: #999; }
|
||||||
|
.empty .icon { font-size: 48px; margin-bottom: 10px; }
|
||||||
|
|
||||||
|
.cat-餐饮 { background: #fff3e0; }
|
||||||
|
.cat-交通 { background: #e3f2fd; }
|
||||||
|
.cat-购物 { background: #fce4ec; }
|
||||||
|
.cat-娱乐 { background: #f3e5f5; }
|
||||||
|
.cat-其他 { background: #f5f5f5; }
|
||||||
|
|
||||||
|
.modal { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.5); z-index: 200; align-items: center; justify-content: center; }
|
||||||
|
.modal.show { display: flex; }
|
||||||
|
.modal-body { background: #fff; border-radius: 16px; padding: 24px; width: 280px; text-align: center; }
|
||||||
|
.modal-body h3 { margin-bottom: 12px; }
|
||||||
|
.modal-body .btns { display: flex; gap: 10px; margin-top: 16px; }
|
||||||
|
.modal-body .btns button { flex: 1; padding: 10px; border: none; border-radius: 8px; font-size: 14px; cursor: pointer; }
|
||||||
|
.modal-body .btn-cancel { background: #f0f0f0; }
|
||||||
|
.modal-body .btn-confirm { background: #e74c3c; color: #fff; }
|
||||||
|
|
||||||
|
@media(min-width:600px) {
|
||||||
|
.tx-list { max-width: 600px; margin: 0 auto; }
|
||||||
|
.stats { max-width: 600px; margin: 0 auto; }
|
||||||
|
.toolbar { max-width: 600px; margin: 0 auto; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="header">
|
||||||
|
<h1>🦞 虾记记账</h1>
|
||||||
|
<div class="subtitle">Xiaji-Go 记账管理</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="num" id="todayTotal">0.00</div>
|
||||||
|
<div class="label">今日支出</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="num" id="monthTotal">0.00</div>
|
||||||
|
<div class="label">本月支出</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="num" id="txCount">0</div>
|
||||||
|
<div class="label">总记录数</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="toolbar">
|
||||||
|
<select class="filter-select" id="catFilter" onchange="filterList()">
|
||||||
|
<option value="">全部分类</option>
|
||||||
|
</select>
|
||||||
|
<div class="spacer"></div>
|
||||||
|
<a href="/export">📥 导出CSV</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tx-list" id="txList"></div>
|
||||||
|
|
||||||
|
<div class="modal" id="delModal">
|
||||||
|
<div class="modal-body">
|
||||||
|
<h3>确认删除?</h3>
|
||||||
|
<p id="delInfo" style="font-size:13px;color:#666;"></p>
|
||||||
|
<div class="btns">
|
||||||
|
<button class="btn-cancel" onclick="closeModal()">取消</button>
|
||||||
|
<button class="btn-confirm" onclick="confirmDelete()">删除</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const catIcons = { '餐饮':'🍜','交通':'🚗','购物':'🛒','娱乐':'🎮','住房':'🏠','通讯':'📱','医疗':'💊','教育':'📚','其他':'📦' };
|
||||||
|
let allData = [];
|
||||||
|
let deleteId = null;
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/records');
|
||||||
|
allData = await r.json();
|
||||||
|
if (!Array.isArray(allData)) allData = [];
|
||||||
|
renderStats();
|
||||||
|
renderList(allData);
|
||||||
|
populateFilter();
|
||||||
|
} catch(e) { console.error(e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderStats() {
|
||||||
|
const today = new Date().toISOString().slice(0,10);
|
||||||
|
const month = today.slice(0,7);
|
||||||
|
let todaySum = 0, monthSum = 0;
|
||||||
|
allData.forEach(tx => {
|
||||||
|
if (tx.date === today) todaySum += tx.amount;
|
||||||
|
if (tx.date && tx.date.startsWith(month)) monthSum += tx.amount;
|
||||||
|
});
|
||||||
|
document.getElementById('todayTotal').textContent = todaySum.toFixed(2);
|
||||||
|
document.getElementById('monthTotal').textContent = monthSum.toFixed(2);
|
||||||
|
document.getElementById('txCount').textContent = allData.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderList(data) {
|
||||||
|
const el = document.getElementById('txList');
|
||||||
|
if (!data.length) {
|
||||||
|
el.innerHTML = '<div class="empty"><div class="icon">📭</div>暂无记录<br><small>通过 Telegram/QQ 发送消息记账</small></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.innerHTML = data.map(tx => {
|
||||||
|
const icon = catIcons[tx.category] || '📦';
|
||||||
|
const catClass = 'cat-' + (tx.category || '其他');
|
||||||
|
return `<div class="tx-card">
|
||||||
|
<div class="tx-icon ${catClass}">${icon}</div>
|
||||||
|
<div class="tx-info">
|
||||||
|
<div class="tx-category">${tx.category || '其他'}</div>
|
||||||
|
<div class="tx-note" title="${esc(tx.note)}">${esc(tx.note)}</div>
|
||||||
|
</div>
|
||||||
|
<div class="tx-right">
|
||||||
|
<div class="tx-amount">-${tx.amount.toFixed(2)}</div>
|
||||||
|
<div class="tx-date">${tx.date}</div>
|
||||||
|
<div class="tx-actions"><button class="btn-del" onclick="showDelete(${tx.id},'${esc(tx.note)}',${tx.amount})">删除</button></div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function esc(s) { return String(s||'').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
|
||||||
|
|
||||||
|
function populateFilter() {
|
||||||
|
const cats = [...new Set(allData.map(t => t.category))].sort();
|
||||||
|
const sel = document.getElementById('catFilter');
|
||||||
|
const cur = sel.value;
|
||||||
|
sel.innerHTML = '<option value="">全部分类</option>' + cats.map(c => `<option value="${c}">${c}</option>`).join('');
|
||||||
|
sel.value = cur;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterList() {
|
||||||
|
const cat = document.getElementById('catFilter').value;
|
||||||
|
renderList(cat ? allData.filter(t => t.category === cat) : allData);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showDelete(id, note, amount) {
|
||||||
|
deleteId = id;
|
||||||
|
document.getElementById('delInfo').textContent = `${note} (${amount.toFixed(2)}元)`;
|
||||||
|
document.getElementById('delModal').classList.add('show');
|
||||||
|
}
|
||||||
|
function closeModal() { document.getElementById('delModal').classList.remove('show'); deleteId = null; }
|
||||||
|
async function confirmDelete() {
|
||||||
|
if (!deleteId) return;
|
||||||
|
await fetch('/delete/' + deleteId, { method: 'POST' });
|
||||||
|
closeModal();
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadData();
|
||||||
|
setInterval(loadData, 30000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
22
version/version.go
Normal file
22
version/version.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package version
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
Version = "dev"
|
||||||
|
GitCommit = "unknown"
|
||||||
|
BuildTime = "unknown"
|
||||||
|
GoVersion = runtime.Version()
|
||||||
|
)
|
||||||
|
|
||||||
|
func Info() string {
|
||||||
|
return fmt.Sprintf("Xiaji-Go %s (commit: %s, built: %s, %s %s/%s)",
|
||||||
|
Version, GitCommit, BuildTime, GoVersion, runtime.GOOS, runtime.GOARCH)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Short() string {
|
||||||
|
return fmt.Sprintf("Xiaji-Go %s", Version)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user