├── api
├── v1
│ ├── ws
│ │ ├── enter.go
│ │ └── msg
│ │ │ ├── msg.go
│ │ │ ├── write.go
│ │ │ └── reader.go
│ ├── enter.go
│ └── common.go
├── response
│ ├── response.go
│ ├── product.go
│ ├── common.go
│ └── func.go
└── request
│ ├── request.go
│ ├── product.go
│ ├── category.go
│ ├── account.go
│ ├── common.go
│ └── user.go
├── util
├── response.go
├── hash.go
├── redisTool
│ └── redis.go
├── rand
│ └── rand.go
├── data.go
├── string.go
├── jwtTool
│ └── jwt.go
├── dataTool
│ ├── slice.go
│ └── map.go
├── timeTool
│ └── timeFunc.go
└── fileTool
│ └── file.go
├── service
├── log
│ ├── enter.go
│ └── logService.go
├── common
│ └── enter.go
├── template
│ ├── error.go
│ ├── rankService.go
│ └── enter.go
├── user
│ ├── enter.go
│ └── userFriendService.go
├── category
│ ├── error.go
│ ├── enter.go
│ └── task.go
├── transaction
│ ├── enter.go
│ ├── event.go
│ └── task.go
├── product
│ ├── bill
│ │ ├── error.go
│ │ ├── aliPay.go
│ │ ├── billInfo.go
│ │ └── weChatPay.go
│ ├── enter.go
│ ├── productService.go
│ └── productBillimportService.go
├── thirdparty
│ ├── enter.go
│ ├── task.go
│ ├── email
│ │ └── enter.go
│ ├── ai.go
│ └── email.go
├── account
│ └── enter.go
└── enter.go
├── test
├── initialize
│ ├── main_test.go
│ └── initialize.go
├── service
│ ├── user
│ │ ├── enter.go
│ │ └── base_test.go
│ ├── transaction
│ │ ├── enter.go
│ │ └── base_test.go
│ ├── account
│ │ └── enter.go
│ ├── common
│ │ └── common_test.go
│ └── thirdparty
│ │ └── ai_test.go
├── info
│ └── info.go
├── util
│ ├── get.go
│ ├── query.go
│ ├── enter.go
│ └── build.go
├── getModel.go
└── enter.go
├── .editcofiging
├── .editorconfig
├── global
├── lock
│ ├── key.go
│ ├── redis.go
│ ├── lock.go
│ └── mysql.go
├── nats
│ ├── dlq.go
│ ├── manager
│ │ ├── error.go
│ │ └── enter.go
│ ├── enter.go
│ ├── event.go
│ └── outbox.go
├── struct.go
├── cus
│ ├── key.go
│ ├── time.go
│ └── context.go
├── global.go
├── cron
│ ├── enter.go
│ └── scheduler.go
├── db
│ ├── db.go
│ └── db_test.go
├── error.go
└── constant
│ └── constant.go
├── .dockerignore
├── docker
├── redis.conf
├── Dockerfile
├── Dockerfile.build
├── Dockerfile.test
├── Dockerfile.debug
└── mysql.cnf
├── model
├── account
│ ├── error.go
│ ├── enter.go
│ └── accountMappingModel.go
├── common
│ ├── baseModel.go
│ ├── func.go
│ └── query
│ │ └── func.go
├── category
│ ├── enter.go
│ ├── categoryFatherModel.go
│ └── categoryModel.go
├── enter.go
├── user
│ ├── enter.go
│ ├── userLogModel.go
│ ├── transactionShareConfigModel.go
│ └── userModel.go
├── transaction
│ ├── enter.go
│ └── statisticModel.go
├── log
│ ├── accountLogMappingModel.go
│ ├── accountLogDao.go
│ ├── accountMappingModel.go
│ └── enter.go
└── product
│ ├── productBillModel.go
│ ├── productModel.go
│ ├── enter.go
│ ├── productBillHeaderModel.go
│ ├── productTransactionCategoryMappingModel.go
│ ├── productTransactionCategoryModel.go
│ └── productDao.go
├── script
├── enter.go
├── user
│ └── create
│ │ └── main.go
├── user.go
└── transaction
│ └── statistic
│ └── main.go
├── router
├── v1
│ ├── enter.go
│ ├── common.go
│ ├── transactionImport.go
│ ├── category.go
│ ├── transaction.go
│ ├── user.go
│ └── account.go
├── router.go
├── engine
│ └── engin.go
├── websocket
│ └── websocket.go
├── group
│ └── group.go
└── middleware
│ └── middleware.go
├── initialize
├── scheduler.go
├── system.go
├── captcha.go
├── thirdparty.go
├── nats.go
├── logger.go
├── mysql.go
├── redis.go
├── initialize.go
└── database
│ └── enter.go
├── docs
├── updateDocs.sh
└── beforeDocsMake
│ └── renameModel
│ └── main.go
├── .gitignore
├── .github
├── workflows
│ ├── push-tag.yml
│ ├── CI.yml
│ ├── develop-CI.yml
│ ├── common-create-release.yml
│ └── common-push-image.yml
└── config.yaml
├── .gitattributes
├── data
├── template
│ └── email
│ │ ├── registerSuccess.html
│ │ ├── updatePassword.html
│ │ └── captcha.html
└── database
│ └── product.sql
├── config.yaml
├── main.go
└── docker-compose.yaml
/api/v1/ws/enter.go:
--------------------------------------------------------------------------------
1 | package ws
2 |
--------------------------------------------------------------------------------
/util/response.go:
--------------------------------------------------------------------------------
1 | package util
2 |
--------------------------------------------------------------------------------
/service/log/enter.go:
--------------------------------------------------------------------------------
1 | package logService
2 |
--------------------------------------------------------------------------------
/api/response/response.go:
--------------------------------------------------------------------------------
1 | package response
2 |
--------------------------------------------------------------------------------
/test/initialize/main_test.go:
--------------------------------------------------------------------------------
1 | package initialize
2 |
--------------------------------------------------------------------------------
/.editcofiging:
--------------------------------------------------------------------------------
1 | [*.go]
2 | indent_style = tab
3 | indent_size = 4
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.go]
2 | indent_style = tab
3 | indent_size = 4
--------------------------------------------------------------------------------
/global/lock/key.go:
--------------------------------------------------------------------------------
1 | package lock
2 |
3 | type Key string
4 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | local_test
2 | docker/runtime
3 | docker/log
4 | docker/data
--------------------------------------------------------------------------------
/api/request/request.go:
--------------------------------------------------------------------------------
1 | package request
2 |
3 | type GetOne struct {
4 | Id int `json:"id" binding:"required"`
5 | }
6 |
--------------------------------------------------------------------------------
/api/request/product.go:
--------------------------------------------------------------------------------
1 | package request
2 |
3 | type ProductMappingTransactionCategory struct {
4 | CategoryId uint
5 | }
6 |
--------------------------------------------------------------------------------
/service/common/enter.go:
--------------------------------------------------------------------------------
1 | package commonService
2 |
3 | type Group struct {
4 | common
5 | }
6 |
7 | var GroupApp = new(Group)
8 |
--------------------------------------------------------------------------------
/service/template/error.go:
--------------------------------------------------------------------------------
1 | package templateService
2 |
3 | import "errors"
4 |
5 | var ErrNotBelongTemplate = errors.New("不属于模板")
6 |
--------------------------------------------------------------------------------
/docker/redis.conf:
--------------------------------------------------------------------------------
1 |
2 | # log
3 | logfile "redis.log"
4 |
5 | # AOF
6 | appendonly yes
7 | appendfsync everysec
8 | appendfilename "appendonly.aof"
--------------------------------------------------------------------------------
/service/user/enter.go:
--------------------------------------------------------------------------------
1 | package userService
2 |
3 | type Group struct {
4 | User
5 | Friend Friend
6 | }
7 |
8 | var GroupApp = new(Group)
9 |
--------------------------------------------------------------------------------
/model/account/error.go:
--------------------------------------------------------------------------------
1 | package accountModel
2 |
3 | import "github.com/pkg/errors"
4 |
5 | var (
6 | ErrAccountType = errors.New("account type")
7 | )
8 |
--------------------------------------------------------------------------------
/service/category/error.go:
--------------------------------------------------------------------------------
1 | package categoryService
2 |
3 | import "errors"
4 |
5 | var (
6 | ErrExistTransaction = errors.New("exist transaction")
7 | )
8 |
--------------------------------------------------------------------------------
/script/enter.go:
--------------------------------------------------------------------------------
1 | package script
2 |
3 | import "github.com/ZiRunHua/LeapLedger/service"
4 |
5 | var (
6 | userService = service.GroupApp.UserServiceGroup
7 | )
8 |
--------------------------------------------------------------------------------
/router/v1/enter.go:
--------------------------------------------------------------------------------
1 | package v1
2 |
3 | import (
4 | v1 "github.com/ZiRunHua/LeapLedger/api/v1"
5 | )
6 |
7 | var (
8 | apiApp = v1.ApiGroupApp
9 | publicApi = apiApp.PublicApi
10 | )
11 |
--------------------------------------------------------------------------------
/global/nats/dlq.go:
--------------------------------------------------------------------------------
1 | package nats
2 |
3 | import "context"
4 |
5 | func RepublishDieMsg(batch int, ctx context.Context) error {
6 | _, err := dlqManage.RepublishBatch(batch, ctx)
7 | return err
8 | }
9 |
--------------------------------------------------------------------------------
/model/common/baseModel.go:
--------------------------------------------------------------------------------
1 | package commonModel
2 |
3 | type Model interface {
4 | modelInterface()
5 | }
6 |
7 | type BaseModel struct {
8 | }
9 |
10 | func (base *BaseModel) modelInterface() {
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/service/transaction/enter.go:
--------------------------------------------------------------------------------
1 | package transactionService
2 |
3 | type Group struct {
4 | Transaction
5 | Timing Timing
6 | }
7 |
8 | var (
9 | GroupApp = new(Group)
10 |
11 | server = &Transaction{}
12 | )
13 |
--------------------------------------------------------------------------------
/global/nats/manager/error.go:
--------------------------------------------------------------------------------
1 | package manager
2 |
3 | import "errors"
4 |
5 | var (
6 | ErrMsgHandlerNotExist = errors.New("msg handler not exist")
7 |
8 | ErrStreamNotExist = errors.New("stream not exist")
9 | )
10 |
--------------------------------------------------------------------------------
/service/product/bill/error.go:
--------------------------------------------------------------------------------
1 | package bill
2 |
3 | import "errors"
4 |
5 | var (
6 | ErrCategoryCannotRead = errors.New("交易类型不可读取")
7 | ErrCategoryReadFail = errors.New("读取交易类型失败")
8 | ErrCategoryMappingNotExist = errors.New("不存在关联类型")
9 | )
10 |
--------------------------------------------------------------------------------
/util/hash.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "crypto/sha256"
5 | "encoding/hex"
6 | )
7 |
8 | func ClientPasswordHash(email, password string) string {
9 | hash := sha256.Sum256([]byte(email + password))
10 | return hex.EncodeToString(hash[:])
11 | }
12 |
--------------------------------------------------------------------------------
/test/service/user/enter.go:
--------------------------------------------------------------------------------
1 | package transaction
2 |
3 | import (
4 | _ "github.com/ZiRunHua/LeapLedger/test/initialize"
5 | )
6 | import (
7 | _service "github.com/ZiRunHua/LeapLedger/service"
8 | )
9 |
10 | var (
11 | service = _service.GroupApp.UserServiceGroup
12 | )
13 |
--------------------------------------------------------------------------------
/service/product/enter.go:
--------------------------------------------------------------------------------
1 | package productService
2 |
3 | import transactionService "github.com/ZiRunHua/LeapLedger/service/transaction"
4 |
5 | type Group struct {
6 | Product
7 | }
8 |
9 | var GroupApp = new(Group)
10 |
11 | var transactionServer = transactionService.GroupApp
12 |
--------------------------------------------------------------------------------
/service/thirdparty/enter.go:
--------------------------------------------------------------------------------
1 | package thirdpartyService
2 |
3 | import (
4 | "github.com/ZiRunHua/LeapLedger/service/thirdparty/email"
5 | )
6 |
7 | type Group struct {
8 | Ai aiServer
9 | }
10 |
11 | var (
12 | GroupApp = new(Group)
13 | emailServer = email.Service
14 | )
15 |
--------------------------------------------------------------------------------
/initialize/scheduler.go:
--------------------------------------------------------------------------------
1 | package initialize
2 |
3 | import (
4 | "github.com/go-co-op/gocron"
5 | "time"
6 | )
7 |
8 | type _scheduler struct {
9 | }
10 |
11 | func (m *_scheduler) do() error {
12 | Scheduler = gocron.NewScheduler(time.Local)
13 | Scheduler.StartAsync()
14 | return nil
15 | }
16 |
--------------------------------------------------------------------------------
/initialize/system.go:
--------------------------------------------------------------------------------
1 | package initialize
2 |
3 | type _system struct {
4 | Addr int `yaml:"Addr"`
5 | RouterPrefix string `yaml:"RouterPrefix"`
6 | LockMode string `yaml:"LockMode"`
7 |
8 | JwtKey string `yaml:"JwtKey"`
9 | ClientSignKey string `yaml:"ClientSignKey"`
10 | }
11 |
--------------------------------------------------------------------------------
/service/category/enter.go:
--------------------------------------------------------------------------------
1 | package categoryService
2 |
3 | import (
4 | _thirdpartyService "github.com/ZiRunHua/LeapLedger/service/thirdparty"
5 | )
6 |
7 | type Group struct {
8 | Category
9 | Task _task
10 | }
11 |
12 | var GroupApp = new(Group)
13 |
14 | var aiService = _thirdpartyService.GroupApp.Ai
15 |
--------------------------------------------------------------------------------
/model/category/enter.go:
--------------------------------------------------------------------------------
1 | package categoryModel
2 |
3 | import (
4 | "github.com/ZiRunHua/LeapLedger/global/db"
5 | )
6 |
7 | func init() {
8 | tables := []interface{}{
9 | Category{}, Mapping{}, Father{},
10 | }
11 | err := db.InitDb.AutoMigrate(tables...)
12 | if err != nil {
13 | panic(err)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/docs/updateDocs.sh:
--------------------------------------------------------------------------------
1 | #!/sh
2 | echo go install github.com/swaggo/swag/cmd/swag@latest
3 | go install github.com/swaggo/swag/cmd/swag@latest
4 | #echo go run /go/LeapLedger/docs/beforeDocsMake/renameModel/main.go
5 | #go run /go/LeapLedger/docs/beforeDocsMake/renameModel/main.go
6 | echo swag init -p pascalcase
7 | swag init -p pascalcase
--------------------------------------------------------------------------------
/router/v1/common.go:
--------------------------------------------------------------------------------
1 | package v1
2 |
3 | import (
4 | "github.com/ZiRunHua/LeapLedger/router/group"
5 | )
6 |
7 | func init() {
8 | // base path: /public
9 | router := group.Public
10 | {
11 | router.GET("/captcha", publicApi.Captcha)
12 | router.POST("/captcha/email/send", publicApi.SendEmailCaptcha)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/global/struct.go:
--------------------------------------------------------------------------------
1 | package global
2 |
3 | import "time"
4 |
5 | type AmountCount struct {
6 | Amount int64
7 | Count int64
8 | }
9 |
10 | type IEStatistic struct {
11 | Income AmountCount
12 | Expense AmountCount
13 | }
14 |
15 | type IEStatisticWithTime struct {
16 | IEStatistic
17 | StartTime time.Time
18 | EndTime time.Time
19 | }
20 |
--------------------------------------------------------------------------------
/api/v1/ws/msg/msg.go:
--------------------------------------------------------------------------------
1 | // Package msg is the middle layer of client websocket communication
2 | // This package is used to standardize websocket message formats with clients
3 | package msg
4 |
5 | import "errors"
6 |
7 | type MsgType string
8 | type MsgHandler func([]byte) error
9 |
10 | var ErrMsgHandleNotExist = errors.New("msg handel not exist")
11 |
--------------------------------------------------------------------------------
/docker/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.23-alpine
2 |
3 | ENV GO111MODULE=on
4 | ENV GOPROXY=https://goproxy.io,direct
5 | ENV TZ=Asia/Shanghai
6 |
7 | WORKDIR /go/LeapLedger
8 |
9 | COPY go.mod go.mod
10 | COPY go.sum go.sum
11 | RUN go mod download
12 |
13 | COPY . .
14 |
15 | RUN go build -o leapledger
16 |
17 | EXPOSE 8080
18 |
19 | CMD ["/bin/sh", "-c", "./leapledger"]
--------------------------------------------------------------------------------
/global/cus/key.go:
--------------------------------------------------------------------------------
1 | package cus
2 |
3 | type Key string
4 |
5 | const Claims Key = "claims"
6 |
7 | const UserId Key = "userId"
8 | const User Key = "user"
9 |
10 | const Account Key = "account"
11 | const AccountUser Key = "accountUser"
12 | const AccountId Key = "accountId"
13 |
14 | const Db Key = "Db"
15 | const Tx Key = "Tx"
16 | const TxCommit Key = "TxCommit"
17 |
--------------------------------------------------------------------------------
/model/enter.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | _ "github.com/ZiRunHua/LeapLedger/model/account"
5 | _ "github.com/ZiRunHua/LeapLedger/model/category"
6 | _ "github.com/ZiRunHua/LeapLedger/model/log"
7 | _ "github.com/ZiRunHua/LeapLedger/model/product"
8 | _ "github.com/ZiRunHua/LeapLedger/model/transaction"
9 | _ "github.com/ZiRunHua/LeapLedger/model/user"
10 | )
11 |
--------------------------------------------------------------------------------
/service/account/enter.go:
--------------------------------------------------------------------------------
1 | package accountService
2 |
3 | import (
4 | log "github.com/ZiRunHua/LeapLedger/service/log"
5 | userService "github.com/ZiRunHua/LeapLedger/service/user"
6 | )
7 |
8 | var GroupApp = &Group{}
9 |
10 | type Group struct {
11 | base
12 | Share share
13 | }
14 |
15 | var (
16 | logServer = log.Log
17 | userServer = userService.GroupApp
18 | )
19 |
--------------------------------------------------------------------------------
/test/info/info.go:
--------------------------------------------------------------------------------
1 | package info
2 |
3 | import "encoding/json"
4 |
5 | var (
6 | Data Info
7 | )
8 |
9 | type Info struct {
10 | UserId uint
11 | Email string
12 | AccountId uint
13 | Token string
14 | }
15 |
16 | func (i *Info) ToString() string {
17 | b, err := json.Marshal(i)
18 | if err != nil {
19 | panic(err)
20 | }
21 | return string(b)
22 | }
23 |
--------------------------------------------------------------------------------
/test/service/transaction/enter.go:
--------------------------------------------------------------------------------
1 | package transaction
2 |
3 | import (
4 | _ "github.com/ZiRunHua/LeapLedger/test/initialize"
5 | testUtil "github.com/ZiRunHua/LeapLedger/test/util"
6 | )
7 | import (
8 | _service "github.com/ZiRunHua/LeapLedger/service"
9 | )
10 |
11 | var (
12 | get = &testUtil.Get{}
13 |
14 | service = _service.GroupApp.TransactionServiceGroup
15 | )
16 |
--------------------------------------------------------------------------------
/util/redisTool/redis.go:
--------------------------------------------------------------------------------
1 | package redisTool
2 |
3 | import (
4 | "context"
5 | "github.com/ZiRunHua/LeapLedger/global"
6 | "strconv"
7 | )
8 |
9 | var (
10 | rdb = global.GvaRdb
11 | )
12 |
13 | func GetInt(key string, ctx context.Context) (value int, err error) {
14 | str, err := rdb.Get(ctx, key).Result()
15 | if err != nil {
16 | return
17 | }
18 | return strconv.Atoi(str)
19 | }
20 |
--------------------------------------------------------------------------------
/model/user/enter.go:
--------------------------------------------------------------------------------
1 | package userModel
2 |
3 | import "github.com/ZiRunHua/LeapLedger/global/db"
4 |
5 | func init() {
6 | tables := []interface{}{
7 | User{}, UserClientWeb{}, UserClientAndroid{}, UserClientIos{}, Tour{},
8 | Friend{}, FriendInvitation{},
9 | TransactionShareConfig{},
10 | Log{},
11 | }
12 | err := db.InitDb.AutoMigrate(tables...)
13 | if err != nil {
14 | panic(err)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 | *.idea
8 |
9 | # Test binary, built with `go test -c`
10 | local_test
11 | /log
12 | *.log
13 |
14 | runtime
15 |
16 | docker/*
17 | !docker/redis.conf
18 | !docker/mysql.cnf
19 |
20 | !docker/Dockerfile
21 | !docker/Dockerfile.build
22 | !docker/Dockerfile.debug
23 | !docker/Dockerfile.test
24 |
25 | docker-compose.override.yaml
26 |
27 | production_config.yaml
--------------------------------------------------------------------------------
/api/v1/ws/msg/write.go:
--------------------------------------------------------------------------------
1 | package msg
2 |
3 | import (
4 | "github.com/gorilla/websocket"
5 | )
6 |
7 | type Msg struct {
8 | Type MsgType
9 | Data any
10 | }
11 |
12 | func Send[T any](conn *websocket.Conn, t MsgType, data T) error {
13 | return conn.WriteJSON(Msg{Type: t, Data: data})
14 | }
15 |
16 | const msgTypeErr MsgType = "error"
17 |
18 | func SendError(conn *websocket.Conn, err error) error {
19 | return conn.WriteJSON(Msg{Type: msgTypeErr, Data: err.Error()})
20 | }
21 |
--------------------------------------------------------------------------------
/test/util/get.go:
--------------------------------------------------------------------------------
1 | package tUtil
2 |
3 | import (
4 | "math/rand"
5 |
6 | categoryModel "github.com/ZiRunHua/LeapLedger/model/category"
7 | transactionModel "github.com/ZiRunHua/LeapLedger/model/transaction"
8 | )
9 |
10 | type Get struct {
11 | }
12 |
13 | func (g *Get) Category() categoryModel.Category {
14 | return testCategoryList[rand.Intn(len(testCategoryList))]
15 | }
16 |
17 | func (g *Get) TransInfo() transactionModel.Info {
18 | return build.TransInfo(testUser, g.Category())
19 | }
20 |
--------------------------------------------------------------------------------
/test/util/query.go:
--------------------------------------------------------------------------------
1 | package tUtil
2 |
3 | import (
4 | "github.com/ZiRunHua/LeapLedger/global/constant"
5 | "github.com/ZiRunHua/LeapLedger/global/db"
6 | categoryModel "github.com/ZiRunHua/LeapLedger/model/category"
7 | )
8 |
9 | type Query struct {
10 | }
11 |
12 | func (q *Query) Category(accountID uint, ie constant.IncomeExpense) (category categoryModel.Category, err error) {
13 | err = db.Db.Where("account_id = ? AND income_expense = ?", accountID, ie).First(&category).Error
14 | return
15 | }
16 |
--------------------------------------------------------------------------------
/model/transaction/enter.go:
--------------------------------------------------------------------------------
1 | package transactionModel
2 |
3 | import "github.com/ZiRunHua/LeapLedger/global/db"
4 |
5 | func init() {
6 | tables := []interface{}{
7 | Transaction{}, Mapping{},
8 | ExpenseAccountStatistic{}, ExpenseAccountUserStatistic{}, ExpenseCategoryStatistic{},
9 | IncomeAccountStatistic{}, IncomeAccountUserStatistic{}, IncomeCategoryStatistic{},
10 | Timing{}, TimingExec{},
11 | }
12 | err := db.InitDb.AutoMigrate(tables...)
13 | if err != nil {
14 | panic(err)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/initialize/captcha.go:
--------------------------------------------------------------------------------
1 | package initialize
2 |
3 | type _captcha struct {
4 | KeyLong int `yaml:"KeyLong"`
5 | ImgWidth int `yaml:"ImgWidth"`
6 | ImgHeight int `yaml:"ImgHeight"`
7 | OpenCaptcha int `yaml:"OpenCaptcha"` // 防爆破验证码开启此数,0代表每次登录都需要验证码,其他数字代表错误密码此数,如3代表错误三次后出现验证码
8 | OpenCaptchaTimeOut int `yaml:"OpenCaptchaTimeOut"` // 防爆破验证码超时时间,单位:s(秒)
9 | EmailCaptcha int `yaml:"EmailCaptcha"`
10 | EmailCaptchaTimeOut int `yaml:"EmailCaptchaTimeOut"`
11 | }
12 |
--------------------------------------------------------------------------------
/docker/Dockerfile.build:
--------------------------------------------------------------------------------
1 | FROM golang:1.23-alpine AS builder
2 |
3 | ENV GO111MODULE=on
4 | ENV GOPROXY=https://goproxy.io,direct
5 | ENV TZ=Asia/Shanghai
6 |
7 | WORKDIR /go/LeapLedger
8 |
9 | COPY go.mod go.mod
10 | COPY go.sum go.sum
11 | RUN go mod download
12 |
13 | COPY . .
14 |
15 | RUN go build -o leapledger
16 |
17 | FROM alpine:3.20.3
18 |
19 | ENV TZ=Asia/Shanghai
20 |
21 | WORKDIR /go/LeapLedger
22 |
23 | COPY --from=builder /go/LeapLedger /go/LeapLedger
24 |
25 | EXPOSE 8080
26 |
27 | CMD ["./leapledger"]
--------------------------------------------------------------------------------
/model/common/func.go:
--------------------------------------------------------------------------------
1 | package commonModel
2 |
3 | import (
4 | "github.com/ZiRunHua/LeapLedger/global"
5 | "github.com/pkg/errors"
6 | "gorm.io/gorm"
7 | )
8 |
9 | func ExistOfModel(model Model, query interface{}, args ...interface{}) (bool, error) {
10 | err := global.GvaDb.Where(query, args...).Take(model).Error
11 | if errors.Is(err, gorm.ErrRecordNotFound) {
12 | return false, nil
13 | } else if err == nil {
14 | return true, nil
15 | } else {
16 | return false, errors.Wrap(err, "exist")
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/util/rand/rand.go:
--------------------------------------------------------------------------------
1 | package rand
2 |
3 | import (
4 | mathRand "math/rand"
5 | "time"
6 | )
7 |
8 | const charSet string = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
9 |
10 | func String(length int) string {
11 | mathRand.New(mathRand.NewSource(time.Now().UnixNano()))
12 | var result string
13 | for i := 0; i < length; i++ {
14 | randomIndex := mathRand.Intn(len(charSet))
15 | result += string(charSet[randomIndex])
16 | }
17 | return result
18 | }
19 | func Int(max int) int {
20 | return mathRand.Intn(max)
21 | }
22 |
--------------------------------------------------------------------------------
/.github/workflows/push-tag.yml:
--------------------------------------------------------------------------------
1 | name: Push Tag
2 |
3 | on:
4 | push:
5 | tags:
6 | - '*'
7 |
8 | jobs:
9 | create-release:
10 | uses: ./.github/workflows/common-create-release.yml
11 | with:
12 | draft: true
13 |
14 | push-image:
15 | needs: create-release
16 | uses: ./.github/workflows/common-push-image.yml
17 | with:
18 | tag: ${{ github.ref_name }}
19 | secrets:
20 | DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }}
21 | DOCKER_HUB_ACCESS_TOKEN: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
--------------------------------------------------------------------------------
/test/service/account/enter.go:
--------------------------------------------------------------------------------
1 | package transaction
2 |
3 | import (
4 | _ "github.com/ZiRunHua/LeapLedger/test/initialize"
5 | testUtil "github.com/ZiRunHua/LeapLedger/test/util"
6 | )
7 | import (
8 | _service "github.com/ZiRunHua/LeapLedger/service"
9 | )
10 |
11 | var (
12 | build = &testUtil.Build{}
13 | query = &testUtil.Query{}
14 | service = _service.GroupApp.AccountServiceGroup
15 | categoryService = _service.GroupApp.CategoryServiceGroup
16 | transService = _service.GroupApp.TransactionServiceGroup
17 | )
18 |
--------------------------------------------------------------------------------
/model/log/accountLogMappingModel.go:
--------------------------------------------------------------------------------
1 | package logModel
2 |
3 | import (
4 | "gorm.io/gorm"
5 | "time"
6 | )
7 |
8 | type AccountLogMapping struct {
9 | ID uint `gorm:"primarykey"`
10 | AccountId uint `gorm:"index:idx_account_id"`
11 | LogTable string
12 | LogId uint
13 | CreatedAt time.Time `gorm:"type:TIMESTAMP"`
14 | UpdatedAt time.Time `gorm:"type:TIMESTAMP"`
15 | DeletedAt gorm.DeletedAt `gorm:"index;type:TIMESTAMP"`
16 | }
17 |
18 | func (a *AccountLogMapping) TableName() string {
19 | return "account_log_mapping"
20 | }
21 |
--------------------------------------------------------------------------------
/docker/Dockerfile.test:
--------------------------------------------------------------------------------
1 | FROM golang:1.23-alpine
2 |
3 | ENV GO111MODULE=on
4 | ENV GOPROXY=https://goproxy.io,direct
5 | ENV TZ=Asia/Shanghai
6 |
7 | WORKDIR /go/LeapLedger
8 |
9 | #RUN sed -i 's#https\?://dl-cdn.alpinelinux.org/alpine#https://mirrors.tuna.tsinghua.edu.cn/alpine#g' /etc/apk/repositories
10 | RUN apk update && apk add build-base && apk add gcc
11 |
12 | COPY go.mod go.mod
13 | COPY go.sum go.sum
14 | RUN go mod download
15 |
16 | RUN go install gotest.tools/gotestsum@latest
17 |
18 | COPY . .
19 |
20 | CMD ["gotestsum", "--junitfile", "docs/junit.xml"]
--------------------------------------------------------------------------------
/docker/Dockerfile.debug:
--------------------------------------------------------------------------------
1 | FROM golang:1.23-alpine
2 |
3 | # 设置环境变量
4 | ENV GO111MODULE=on
5 | ENV GOPROXY=https://goproxy.io,direct
6 | ENV TZ=Asia/Shanghai
7 |
8 | # 设置工作目录
9 | WORKDIR /go/LeapLedger
10 |
11 | # 拷贝依赖文件并下载
12 | COPY go.mod go.mod
13 | COPY go.sum go.sum
14 | RUN go mod download
15 |
16 | RUN go install github.com/go-delve/delve/cmd/dlv@v1.23
17 |
18 | # 拷贝源代码
19 | COPY . .
20 |
21 | # 构建Go应用程序
22 | RUN go build -gcflags="-N -l" -o leapledger .
23 |
24 | # 声明服务端口
25 | EXPOSE 8080 2345
26 | # 指定容器启动命令
27 | CMD ["dlv", "--listen=:2345", "--headless=true", "--api-version=2", "exec", "./leapledger"]
--------------------------------------------------------------------------------
/test/service/common/common_test.go:
--------------------------------------------------------------------------------
1 | package thirdparty
2 |
3 | import (
4 | _ "github.com/ZiRunHua/LeapLedger/test/initialize"
5 | )
6 | import (
7 | _commonService "github.com/ZiRunHua/LeapLedger/service/common"
8 | "testing"
9 | )
10 |
11 | var commonServer = _commonService.GroupApp
12 |
13 | func TestJwt(t *testing.T) {
14 | claims := commonServer.MakeCustomClaims(1)
15 | token, err := commonServer.GenerateJWT(claims)
16 | if err != nil {
17 | t.Error(err)
18 | }
19 | claims, err = commonServer.ParseToken(token)
20 | if err != nil {
21 | t.Error(err)
22 | }
23 |
24 | t.Log(claims)
25 | }
26 |
--------------------------------------------------------------------------------
/initialize/thirdparty.go:
--------------------------------------------------------------------------------
1 | package initialize
2 |
3 | type _thirdParty struct {
4 | WeCom _weCom `yaml:"WeCom"`
5 | Ai _ai `yaml:"Ai"`
6 | }
7 |
8 | type _weCom struct {
9 | CorpId string `yaml:"CorpId"`
10 | CorpSecret string `yaml:"CorpSecret"`
11 | }
12 |
13 | type _ai struct {
14 | Host string `yaml:"Host"`
15 | Port string `yaml:"Port"`
16 | MinSimilarity float32 `yaml:"MinSimilarity"`
17 | }
18 |
19 | func (a _ai) GetPortalSite() string {
20 | return "http://" + a.Host + ":" + a.Port
21 | }
22 | func (a _ai) IsOpen() bool {
23 | return a.Host != "" && a.Port != ""
24 | }
25 |
--------------------------------------------------------------------------------
/.github/workflows/CI.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | branches: [ "main" ]
7 |
8 | jobs:
9 | test-and-update-docs:
10 | uses: ./.github/workflows/common-test-and-update-docs.yml
11 | with:
12 | ref: ${{ github.ref }}
13 | secrets:
14 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
15 |
16 | push-image:
17 | needs: test-and-update-docs
18 | uses: ./.github/workflows/common-push-image.yml
19 | with:
20 | tag: latest
21 | secrets:
22 | DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }}
23 | DOCKER_HUB_ACCESS_TOKEN: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
--------------------------------------------------------------------------------
/global/global.go:
--------------------------------------------------------------------------------
1 | package global
2 |
3 | import (
4 | "github.com/ZiRunHua/LeapLedger/initialize"
5 | "go.uber.org/zap"
6 | )
7 |
8 | var (
9 | GvaDb = initialize.Db
10 | GvaRdb = initialize.Rdb
11 | Config = initialize.Config
12 | Cache = initialize.Cache
13 | )
14 |
15 | var (
16 | RequestLogger *zap.Logger
17 | ErrorLogger *zap.Logger
18 | PanicLogger *zap.Logger
19 | )
20 |
21 | func init() {
22 | GvaDb = initialize.Db
23 | GvaRdb = initialize.Rdb
24 | Config = initialize.Config
25 | Cache = initialize.Cache
26 |
27 | RequestLogger = initialize.RequestLogger
28 | ErrorLogger = initialize.ErrorLogger
29 | PanicLogger = initialize.PanicLogger
30 | }
31 |
--------------------------------------------------------------------------------
/model/log/accountLogDao.go:
--------------------------------------------------------------------------------
1 | package logModel
2 |
3 | import (
4 | "github.com/ZiRunHua/LeapLedger/global"
5 | "gorm.io/gorm"
6 | )
7 |
8 | type AccountLogDao struct {
9 | db *gorm.DB
10 | }
11 |
12 | func NewDao(db ...*gorm.DB) *AccountLogDao {
13 | if len(db) > 0 {
14 | return &AccountLogDao{db: db[0]}
15 | }
16 | return &AccountLogDao{global.GvaDb}
17 | }
18 |
19 | func (d *AccountLogDao) RecordAccountLogMapping(logModel AccountLogger) (AccountLogMapping, error) {
20 | log := AccountLogMapping{
21 | AccountId: logModel.GetAccountId(),
22 | LogTable: logModel.TableName(),
23 | LogId: logModel.GetId(),
24 | }
25 | return log, d.db.Create(&log).Error
26 | }
27 |
--------------------------------------------------------------------------------
/.github/workflows/develop-CI.yml:
--------------------------------------------------------------------------------
1 | name: Develop CI
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | branches: [ "develop" ]
7 |
8 | jobs:
9 | test-and-update-docs:
10 | uses: ./.github/workflows/common-test-and-update-docs.yml
11 | with:
12 | ref: ${{ github.ref }}
13 | secrets:
14 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
15 |
16 | push-image:
17 | needs: test-and-update-docs
18 | uses: ./.github/workflows/common-push-image.yml
19 | with:
20 | tag: ${{ github.ref_name }}
21 | secrets:
22 | DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }}
23 | DOCKER_HUB_ACCESS_TOKEN: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
--------------------------------------------------------------------------------
/api/response/product.go:
--------------------------------------------------------------------------------
1 | package response
2 |
3 | import (
4 | "github.com/ZiRunHua/LeapLedger/global/constant"
5 | )
6 |
7 | type ProductOne struct {
8 | UniqueKey string
9 | Name string
10 | }
11 |
12 | type ProductList struct {
13 | List []ProductOne
14 | }
15 |
16 | type ProductTransactionCategory struct {
17 | Id uint
18 | Name string
19 | IncomeExpense constant.IncomeExpense
20 | }
21 |
22 | type ProductTransactionCategoryList struct {
23 | List []ProductTransactionCategory
24 | }
25 |
26 | type ProductMappingTree struct {
27 | Tree []ProductMappingTreeFather
28 | }
29 |
30 | type ProductMappingTreeFather struct {
31 | FatherId uint
32 | Children []uint
33 | }
34 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.go whitespace=fix
2 |
3 | # Auto detect text files and perform LF normalization
4 | * text=auto
5 |
6 | # Always perform LF normalization on Go source files and related text files
7 | *.go text
8 | *.md text
9 | *.yaml text
10 | *.yml text
11 | *.json text
12 | *.sh text
13 | *.txt text
14 | *.xml text
15 |
16 | # Make sure that these Windows files always have CRLF line endings in checkout
17 | *.bat text eol=crlf
18 | *.ps1 text eol=crlf
19 |
20 | # Never perform LF normalization on these binary files
21 | *.zip binary
22 | *.tar binary
23 | *.gz binary
24 | *.bz2 binary
25 | *.7z binary
26 | *.rar binary
27 | *.xls binary
28 | *.xlsx binary
--------------------------------------------------------------------------------
/data/template/email/registerSuccess.html:
--------------------------------------------------------------------------------
1 |
2 |
亲爱的 [username]
3 |
我们很高兴通知你,你的注册已成功完成。现在,你可以开始尽情享受我们的应用了。
4 |
祝你愉快的使用体验!
5 |
最诚挚的问候。
6 |
起飞咯!
7 |
--------------------------------------------------------------------------------
/data/template/email/updatePassword.html:
--------------------------------------------------------------------------------
1 |
2 |
亲爱的 [username]
3 |
我们注意到您的账户密码已经被修改。为了确保您的账户的安全性和保护您的个人信息。如果您不是执行此操作的人,请立即联系我们的客户支持团队以报告此问题。
4 |
祝你愉快的使用体验!
5 |
最诚挚的问候。
6 |
起飞咯!
7 |
--------------------------------------------------------------------------------
/model/product/productBillModel.go:
--------------------------------------------------------------------------------
1 | package productModel
2 |
3 | import (
4 | "github.com/ZiRunHua/LeapLedger/global/constant"
5 | commonModel "github.com/ZiRunHua/LeapLedger/model/common"
6 | queryFunc "github.com/ZiRunHua/LeapLedger/model/common/query"
7 | )
8 |
9 | type Bill struct {
10 | ProductKey Key `gorm:"primary_key;"`
11 | Encoding constant.Encoding
12 | StartRow int
13 | DateFormat string `gorm:"default:2006-01-02 15:04:05;"`
14 | commonModel.BaseModel
15 | }
16 |
17 | func (b *Bill) TableName() string {
18 | return "product_bill"
19 | }
20 |
21 | func (b *Bill) IsEmpty() bool {
22 | return b.ProductKey == ""
23 | }
24 |
25 | func (b *Bill) SelectByPrimaryKey(key string) (*Bill, error) {
26 | return queryFunc.FirstByField[*Bill]("product_key", key)
27 | }
28 |
--------------------------------------------------------------------------------
/global/cus/time.go:
--------------------------------------------------------------------------------
1 | package cus
2 |
3 | import (
4 | "gorm.io/gorm"
5 | "gorm.io/gorm/schema"
6 | "time"
7 | )
8 |
9 | type Time time.Time
10 |
11 | // GormDataType returns the common data type in Gorm for CustomTime
12 | func (Time) GormDataType() string {
13 | return "timestamp"
14 | }
15 |
16 | // GormDBDataType returns the specific data type for a given dialect
17 | func (Time) GormDBDataType(db *gorm.DB, field *schema.Field) string {
18 | return "TIMESTAMP"
19 | }
20 |
21 | type DeletedAt gorm.DeletedAt
22 |
23 | func (DeletedAt) GormDataType() string {
24 | return "timestamp"
25 | }
26 |
27 | // GormDBDataType returns the specific data type for a given dialect
28 | func (DeletedAt) GormDBDataType(db *gorm.DB, field *schema.Field) string {
29 | return "TIMESTAMP"
30 | }
31 |
--------------------------------------------------------------------------------
/.github/workflows/common-create-release.yml:
--------------------------------------------------------------------------------
1 | name: Create Release
2 |
3 | on:
4 | workflow_call:
5 | inputs:
6 | draft:
7 | required: true
8 | type: boolean
9 |
10 | jobs:
11 | create-release:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Checkout code
15 | uses: actions/checkout@v4
16 |
17 | - name: Create GitHub Release
18 | id: create_release
19 | uses: actions/create-release@v1
20 | env:
21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
22 | with:
23 | tag_name: ${{ github.ref_name }}
24 | release_name: "Release ${{ github.ref_name }}"
25 | body: |
26 | Release notes for version ${{ github.ref_name }}.
27 | draft: ${{ inputs.draft }}
28 | prerelease: false
--------------------------------------------------------------------------------
/model/log/accountMappingModel.go:
--------------------------------------------------------------------------------
1 | package logModel
2 |
3 | import (
4 | "gorm.io/gorm"
5 | )
6 |
7 | type AccountMappingLogData struct {
8 | MainId uint
9 | RelatedId uint
10 | }
11 |
12 | func (m *AccountMappingLogData) Record(baseLog BaseAccountLog, tx *gorm.DB) (AccountLogger, error) {
13 | log := AccountMappingLog{BaseAccountLog: baseLog, Data: *m}
14 | return &log, tx.Create(&log).Error
15 | }
16 |
17 | type AccountMappingLog struct {
18 | BaseAccountLog `gorm:"embedded"`
19 | Data AccountMappingLogData `gorm:"embedded;embeddedPrefix:data_"`
20 | }
21 |
22 | func (a *AccountMappingLog) TableName() string {
23 | return "log_account_mapping"
24 | }
25 |
26 | func (a *AccountMappingLog) RecordMapping(tx *gorm.DB) (AccountLogMapping, error) {
27 | return NewDao(tx).RecordAccountLogMapping(a)
28 | }
29 |
--------------------------------------------------------------------------------
/global/cron/enter.go:
--------------------------------------------------------------------------------
1 | package cron
2 |
3 | import (
4 | "context"
5 | "path/filepath"
6 |
7 | "github.com/ZiRunHua/LeapLedger/global"
8 | "github.com/ZiRunHua/LeapLedger/global/constant"
9 | "github.com/ZiRunHua/LeapLedger/global/nats"
10 | "github.com/ZiRunHua/LeapLedger/initialize"
11 | "go.uber.org/zap"
12 | )
13 |
14 | var (
15 | logPath = filepath.Join(constant.LogPath, "cron.log")
16 | logger *zap.Logger
17 |
18 | Scheduler = initialize.Scheduler
19 | )
20 |
21 | func init() {
22 | var err error
23 | logger, err = global.Config.Logger.New(logPath)
24 | if err != nil {
25 | panic(err)
26 | }
27 | _, err = Scheduler.Every(30).Minute().Do(
28 | MakeJobFunc(
29 | func() error {
30 | return nats.RepublishDieMsg(50, context.TODO())
31 | },
32 | ),
33 | )
34 | if err != nil {
35 | panic(err)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/model/account/enter.go:
--------------------------------------------------------------------------------
1 | package accountModel
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "github.com/ZiRunHua/LeapLedger/global"
7 | "github.com/ZiRunHua/LeapLedger/global/cus"
8 | "github.com/ZiRunHua/LeapLedger/global/db"
9 | )
10 |
11 | func init() {
12 | ctx := cus.WithDb(context.TODO(), db.InitDb)
13 | tables := []interface{}{
14 | Account{}, Mapping{},
15 | User{}, UserConfig{}, UserInvitation{},
16 | }
17 | err := ctx.GetDb().AutoMigrate(tables...)
18 | if err != nil {
19 | panic(err)
20 | }
21 | err = NewDao(ctx.GetDb()).initRedis()
22 | if err != nil {
23 | panic(err)
24 | }
25 | }
26 |
27 | var (
28 | rdb = global.GvaRdb
29 | rdbKey redisKey
30 | )
31 |
32 | type redisKey struct {
33 | }
34 |
35 | func (r *redisKey) getLocation(id uint) string {
36 | return fmt.Sprintf("account:%d:location", id)
37 | }
38 |
--------------------------------------------------------------------------------
/test/service/thirdparty/ai_test.go:
--------------------------------------------------------------------------------
1 | package thirdparty
2 |
3 | import (
4 | _ "github.com/ZiRunHua/LeapLedger/test/initialize"
5 | )
6 | import (
7 | "context"
8 | _thirdpartyService "github.com/ZiRunHua/LeapLedger/service/thirdparty"
9 | "testing"
10 | )
11 |
12 | var aiService = _thirdpartyService.GroupApp.Ai
13 |
14 | func TestChineseSimilarityMatching(t *testing.T) {
15 | sourceData := []string{
16 | "餐饮美食", "酒店旅游", "运动户外", "美容美发", "生活服务", "爱车养车",
17 | "母婴亲子", "服饰装扮", "日用百货", "文化休闲", "数码电器", "教育培训",
18 | "家居家装", "宠物", "商业服务", "医疗健康", "其他",
19 | }
20 |
21 | targetData := []string{
22 | "住房", "交通", "购物", "通讯", "餐饮", "杂项",
23 | "医疗", "旅游", "文化休闲", "其他",
24 | }
25 | result, err := aiService.BatchChineseSimilarityMatching(sourceData, targetData, context.TODO())
26 | if err != nil {
27 | t.Error(err)
28 | }
29 | t.Log(result)
30 | }
31 |
--------------------------------------------------------------------------------
/data/template/email/captcha.html:
--------------------------------------------------------------------------------
1 |
2 |
你好!
3 |
您正在[Action],我们需要验证你的电子邮件地址。
4 |
你的验证码是:
5 |
[Captcha]
6 |
验证码将在 [ExpirationTime] 后过期,请尽快完成验证。如果你没有进行此[Action]请求,请忽略此邮件。
7 |
祝愉快注册!
8 |
--------------------------------------------------------------------------------
/service/category/task.go:
--------------------------------------------------------------------------------
1 | package categoryService
2 |
3 | import (
4 | "github.com/ZiRunHua/LeapLedger/global/nats"
5 | accountModel "github.com/ZiRunHua/LeapLedger/model/account"
6 | categoryModel "github.com/ZiRunHua/LeapLedger/model/category"
7 | )
8 |
9 | type _task struct{}
10 |
11 | func init() {
12 | nats.SubscribeTaskWithPayloadAndProcessInTransaction[accountModel.Mapping](
13 | nats.TaskMappingCategoryToAccountMapping,
14 | GroupApp.MappingCategoryToAccountMapping,
15 | )
16 |
17 | nats.SubscribeTaskWithPayload[categoryModel.Category](
18 | nats.TaskUpdateCategoryMapping,
19 | GroupApp.UpdateCategoryMapping,
20 | )
21 | }
22 |
23 | func (t *_task) MappingCategoryToAccountMapping(mappingAccount accountModel.Mapping) error {
24 | _ = nats.PublishTaskWithPayload[accountModel.Mapping](nats.TaskMappingCategoryToAccountMapping, mappingAccount)
25 | return nil
26 | }
27 |
--------------------------------------------------------------------------------
/service/log/logService.go:
--------------------------------------------------------------------------------
1 | package logService
2 |
3 | import (
4 | logModel "github.com/ZiRunHua/LeapLedger/model/log"
5 | "gorm.io/gorm"
6 | )
7 |
8 | type log struct{}
9 |
10 | var Log = new(log)
11 |
12 | type _log interface {
13 | RecordAccountLog(provider logModel.AccountLogDataProvider, baseInfo logModel.BaseAccountLog, tx *gorm.DB) (
14 | AccountLog logModel.AccountLogger, logMapping logModel.AccountLogMapping, err error,
15 | )
16 | }
17 |
18 | func (logSvc *log) RecordAccountLog(
19 | provider logModel.AccountLogDataProvider,
20 | baseInfo logModel.BaseAccountLog,
21 | tx *gorm.DB,
22 | ) (AccountLog logModel.AccountLogger, logMapping logModel.AccountLogMapping, err error) {
23 | data := provider.GetLogDataModel()
24 | AccountLog, err = data.Record(baseInfo, tx)
25 | if err != nil {
26 | return
27 | }
28 | logMapping, err = AccountLog.RecordMapping(tx)
29 | return
30 | }
31 |
--------------------------------------------------------------------------------
/test/service/user/base_test.go:
--------------------------------------------------------------------------------
1 | package transaction
2 |
3 | import (
4 | "context"
5 | "time"
6 |
7 | "github.com/ZiRunHua/LeapLedger/global/constant"
8 | "github.com/ZiRunHua/LeapLedger/global/db"
9 | "github.com/ZiRunHua/LeapLedger/global/nats"
10 | accountModel "github.com/ZiRunHua/LeapLedger/model/account"
11 | _ "github.com/ZiRunHua/LeapLedger/test/initialize"
12 | "github.com/google/uuid"
13 | )
14 | import (
15 | "testing"
16 | )
17 |
18 | func TestTourCreate(t *testing.T) {
19 | t.Parallel()
20 | if !nats.PublishTask(nats.TaskCreateTourist) {
21 | t.Fail()
22 | }
23 | time.Sleep(time.Second * 10)
24 | user, err := service.EnableTourist(uuid.NewString(), constant.Android, context.TODO())
25 | if err != nil {
26 | t.Fatal(err)
27 | }
28 | var account accountModel.Account
29 | err = db.Db.Where("user_id = ?", user.ID).First(&account).Error
30 | if err != nil {
31 | t.Fatal(err)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/router/v1/transactionImport.go:
--------------------------------------------------------------------------------
1 | package v1
2 |
3 | import (
4 | "github.com/ZiRunHua/LeapLedger/router/group"
5 | "github.com/ZiRunHua/LeapLedger/router/websocket"
6 | )
7 |
8 | func init() {
9 | // base path: /product
10 | router := group.Private.Group("product")
11 | // base path: /account/{accountId}/product
12 | accountRouter := group.Account.Group("product")
13 | editRouter := group.AccountCreator.Group("product")
14 | baseApi := apiApp.ProductApi
15 | {
16 | router.GET("/list", baseApi.GetList)
17 | router.GET("/:key/transCategory", baseApi.GetTransactionCategory)
18 | accountRouter.GET("/:key/transCategory/mapping/tree", baseApi.GetMappingTree)
19 | editRouter.POST("/transCategory/:id/mapping", baseApi.MappingTransactionCategory)
20 | editRouter.DELETE("/transCategory/:id/mapping", baseApi.DeleteTransactionCategoryMapping)
21 | editRouter.GET("/:key/bill/import", websocket.Use(baseApi.ImportProductBill))
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/util/data.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "errors"
5 | "strings"
6 | )
7 |
8 | type dataFunc interface {
9 | utilDataFunc()
10 | CopyNotEmptyStringOptional(originData *string, targetData *string) error
11 | }
12 |
13 | type data struct{}
14 |
15 | var Data data
16 |
17 | func (d *data) utilDataFunc() {}
18 |
19 | var (
20 | ErrDataIsEmpty = errors.New("数据不可为空")
21 | )
22 |
23 | func (d *data) NotEmptyString(originData *string) error {
24 | if originData != nil {
25 | *originData = strings.TrimSpace(*originData)
26 | if *originData == "" {
27 | return ErrDataIsEmpty
28 | }
29 | }
30 | return ErrDataIsEmpty
31 | }
32 |
33 | func (d *data) CopyNotEmptyStringOptional(originData *string, targetData *string) error {
34 | if originData != nil {
35 | *originData = strings.TrimSpace(*originData)
36 | if *originData == "" {
37 | return ErrDataIsEmpty
38 | }
39 | *targetData = *originData
40 | }
41 | return nil
42 | }
43 |
--------------------------------------------------------------------------------
/.github/workflows/common-push-image.yml:
--------------------------------------------------------------------------------
1 | name: Push Image
2 |
3 | on:
4 | workflow_call:
5 | inputs:
6 | tag:
7 | required: true
8 | type: string
9 |
10 | secrets:
11 | DOCKER_HUB_USERNAME:
12 | required: true
13 | DOCKER_HUB_ACCESS_TOKEN:
14 | required: true
15 |
16 | jobs:
17 | push-image:
18 | runs-on: ubuntu-latest
19 | steps:
20 | - name: Checkout code
21 | uses: actions/checkout@v4
22 |
23 | - name: Login to GitHub Container Registry
24 | uses: docker/login-action@v3
25 | with:
26 | username: ${{ secrets.DOCKER_HUB_USERNAME }}
27 | password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
28 |
29 | - name: Build and push Docker image
30 | uses: docker/build-push-action@v6
31 | with:
32 | context: .
33 | file: docker/Dockerfile.build
34 | push: true
35 | tags: xiaozirun/leap-ledger:${{ inputs.tag }}
--------------------------------------------------------------------------------
/router/router.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | _ "github.com/ZiRunHua/LeapLedger/docs"
5 | "github.com/ZiRunHua/LeapLedger/global"
6 | "github.com/ZiRunHua/LeapLedger/global/constant"
7 | routerEngine "github.com/ZiRunHua/LeapLedger/router/engine"
8 | "github.com/ZiRunHua/LeapLedger/router/group"
9 | _ "github.com/ZiRunHua/LeapLedger/router/v1"
10 | "github.com/gin-gonic/gin"
11 | "github.com/swaggo/files"
12 | "github.com/swaggo/gin-swagger"
13 | "net/http"
14 | )
15 |
16 | var Engine = routerEngine.Engine
17 |
18 | func init() {
19 | // health
20 | group.Public.GET(
21 | "/health", func(c *gin.Context) {
22 | c.JSON(http.StatusOK, "ok")
23 | },
24 | )
25 | if global.Config.Mode == constant.Debug {
26 | group.Public.GET(
27 | "/swagger/*any", ginSwagger.WrapHandler(
28 | swaggerFiles.Handler, func(config *ginSwagger.Config) {
29 | config.DocExpansion = "none"
30 | config.DeepLinking = true
31 | },
32 | ),
33 | )
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/model/category/categoryFatherModel.go:
--------------------------------------------------------------------------------
1 | package categoryModel
2 |
3 | import (
4 | "github.com/ZiRunHua/LeapLedger/global"
5 | "github.com/ZiRunHua/LeapLedger/global/constant"
6 | commonModel "github.com/ZiRunHua/LeapLedger/model/common"
7 | "time"
8 | )
9 |
10 | type Father struct {
11 | ID uint `gorm:"primary_key"`
12 | AccountId uint `gorm:"index;comment:'账本ID'"`
13 | IncomeExpense constant.IncomeExpense `gorm:"comment:'收支类型'"`
14 | Name string `gorm:"size:128;comment:'名称'"`
15 | Previous uint `gorm:"comment:'前一位'"`
16 | OrderUpdatedAt time.Time `gorm:"type:TIMESTAMP"`
17 | CreatedAt time.Time `gorm:"type:TIMESTAMP"`
18 | commonModel.BaseModel
19 | }
20 |
21 | func (f *Father) TableName() string {
22 | return "category_father"
23 | }
24 |
25 | func (f *Father) SelectById(id uint) error {
26 | return global.GvaDb.First(f, id).Error
27 | }
28 |
--------------------------------------------------------------------------------
/model/product/productModel.go:
--------------------------------------------------------------------------------
1 | package productModel
2 |
3 | import (
4 | "github.com/ZiRunHua/LeapLedger/global"
5 | commonModel "github.com/ZiRunHua/LeapLedger/model/common"
6 | )
7 |
8 | type Product struct {
9 | Key Key `gorm:"primary_key"`
10 | Name string `gorm:"comment:'名称'"`
11 | Hide uint8 `gorm:"default:0;comment:'隐藏标识'"`
12 | Weight int `gorm:"default:0;comment:'权重'"`
13 | commonModel.BaseModel
14 | }
15 |
16 | type Key string
17 |
18 | const AliPay, WeChatPay Key = "AliPay", "WeChatPay"
19 |
20 | func (p *Product) TableName() string {
21 | return "product"
22 | }
23 |
24 | func (p *Product) IsEmpty() bool {
25 | return p == nil || p.Key == ""
26 | }
27 |
28 | func (p *Product) SelectByKey(key Key) (result Product, err error) {
29 | err = global.GvaDb.Where("key = ?", key).First(&result).Error
30 | return
31 | }
32 |
33 | func (p *Product) GetBill() (*Bill, error) {
34 | bill := &Bill{}
35 | return bill.SelectByPrimaryKey(string(p.Key))
36 | }
37 |
--------------------------------------------------------------------------------
/global/nats/enter.go:
--------------------------------------------------------------------------------
1 | package nats
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "github.com/ZiRunHua/LeapLedger/global/db"
7 | "github.com/ZiRunHua/LeapLedger/global/nats/manager"
8 | )
9 |
10 | type PayloadType interface{}
11 |
12 | type handler[Data any] func(Data, context.Context) error
13 |
14 | var (
15 | taskManage = manager.TaskManage
16 | eventManage = manager.EventManage
17 | dlqManage = manager.DlqManage
18 | )
19 |
20 | func init() {
21 | err := db.InitDb.AutoMigrate(&outbox{})
22 | if err != nil {
23 | panic(err)
24 | }
25 | SubscribeTaskWithPayload(
26 | TaskOutbox, outboxService.getHandleTransaction(outboxTypeTask),
27 | )
28 | SubscribeEvent(
29 | EventOutbox, "outbox", outboxService.getHandleTransaction(outboxTypeEvent),
30 | )
31 | }
32 |
33 | func fromJson[T PayloadType](jsonStr []byte, data *T) error {
34 | if len(jsonStr) != 0 {
35 | if err := json.Unmarshal(jsonStr, &data); err != nil {
36 | return err
37 | }
38 | }
39 | return nil
40 | }
41 |
--------------------------------------------------------------------------------
/router/engine/engin.go:
--------------------------------------------------------------------------------
1 | package engine
2 |
3 | import (
4 | "fmt"
5 | "github.com/ZiRunHua/LeapLedger/global"
6 | "github.com/ZiRunHua/LeapLedger/global/constant"
7 | "github.com/ZiRunHua/LeapLedger/router/middleware"
8 | "github.com/gin-gonic/gin"
9 | "time"
10 | )
11 |
12 | var Engine *gin.Engine
13 |
14 | func init() {
15 | Engine = gin.New()
16 | Engine.Use(
17 | gin.LoggerWithConfig(
18 | gin.LoggerConfig{
19 | Formatter: func(params gin.LogFormatterParams) string {
20 | return fmt.Sprintf(
21 | "[GIN] %s | %s | %s | %d | %s | %s | %s\n",
22 | params.TimeStamp.Format(time.RFC3339),
23 | params.Method,
24 | params.Path,
25 | params.StatusCode,
26 | params.Latency,
27 | params.ClientIP,
28 | params.ErrorMessage,
29 | )
30 | },
31 | },
32 | ),
33 | gin.CustomRecovery(middleware.Recovery),
34 | )
35 | if global.Config.Mode == constant.Debug {
36 | Engine.Use(middleware.RequestLogger(global.RequestLogger))
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/service/thirdparty/task.go:
--------------------------------------------------------------------------------
1 | package thirdpartyService
2 |
3 | import (
4 | "context"
5 | "errors"
6 |
7 | "github.com/ZiRunHua/LeapLedger/global"
8 | "github.com/ZiRunHua/LeapLedger/global/nats"
9 | userModel "github.com/ZiRunHua/LeapLedger/model/user"
10 | )
11 |
12 | func init() {
13 | nats.SubscribeTaskWithPayload(
14 | nats.TaskSendCaptchaEmail, func(t nats.PayloadSendCaptchaEmail, ctx context.Context) error {
15 | err := GroupApp.sendCaptchaEmail(t.Email, t.Action)
16 | if errors.Is(err, global.ErrServiceClosed) {
17 | return nil
18 | }
19 | return err
20 | },
21 | )
22 | nats.SubscribeTaskWithPayload(
23 | nats.TaskSendNotificationEmail, func(t nats.PayloadSendNotificationEmail, ctx context.Context) error {
24 | user, err := userModel.NewDao().SelectById(t.UserId)
25 | if err != nil {
26 | return err
27 | }
28 | err = GroupApp.sendNotificationEmail(user, t.Notification)
29 | if errors.Is(err, global.ErrServiceClosed) {
30 | return nil
31 | }
32 | return err
33 | },
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/service/template/rankService.go:
--------------------------------------------------------------------------------
1 | package templateService
2 |
3 | import (
4 | "context"
5 | accountModel "github.com/ZiRunHua/LeapLedger/model/account"
6 | tmplRank "github.com/ZiRunHua/LeapLedger/service/template/rank"
7 | "strconv"
8 | "time"
9 | )
10 |
11 | var rank tmplRank.Rank[rankMember]
12 |
13 | func initRank() {
14 | list, err := TemplateApp.GetList()
15 | if err != nil {
16 | panic(err)
17 | }
18 | members := make([]rankMember, len(list), len(list))
19 | for i, account := range list {
20 | members[i] = newRankMember(account)
21 | }
22 | rank = tmplRank.NewRank[rankMember]("tmplAccount", members, time.Hour*24)
23 | err = rank.Init(context.TODO())
24 | if err != nil {
25 | panic(err)
26 | }
27 | }
28 |
29 | type rankMember struct {
30 | tmplRank.Member
31 | key string
32 | id uint
33 | }
34 |
35 | func newRankMember(account accountModel.Account) rankMember {
36 | return rankMember{id: account.ID, key: strconv.Itoa(int(account.ID))}
37 | }
38 |
39 | func (rm rankMember) String() string {
40 | return rm.key
41 | }
42 |
--------------------------------------------------------------------------------
/util/string.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "bytes"
5 | "regexp"
6 | )
7 |
8 | type str struct{}
9 |
10 | var Str str
11 |
12 | func (s *str) MaskEmail(email string) string {
13 | re := regexp.MustCompile(`([^@]+)@(.+)`)
14 | matches := re.FindStringSubmatch(email)
15 |
16 | if len(matches) == 3 {
17 | username := matches[1]
18 | domain := matches[2]
19 |
20 | maskedUsername := s.MaskString(username, 3)
21 |
22 | maskedEmail := maskedUsername + "@" + domain
23 | return maskedEmail
24 | }
25 |
26 | return email
27 | }
28 |
29 | func (s *str) MaskString(input string, visibleChars int) string {
30 | if len(input) <= visibleChars {
31 | return input
32 | }
33 |
34 | maskedChars := len(input) - visibleChars
35 | maskedString := input[:visibleChars] + s.RepeatChar('*', maskedChars)
36 | return maskedString
37 | }
38 |
39 | func (s *str) RepeatChar(char byte, count int) string {
40 | return s.StringOf(char, count)
41 | }
42 |
43 | func (s *str) StringOf(char byte, count int) string {
44 | return string(bytes.Repeat([]byte{char}, count))
45 | }
46 |
--------------------------------------------------------------------------------
/service/enter.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | accountService "github.com/ZiRunHua/LeapLedger/service/account"
5 | categoryService "github.com/ZiRunHua/LeapLedger/service/category"
6 | commonService "github.com/ZiRunHua/LeapLedger/service/common"
7 | productService "github.com/ZiRunHua/LeapLedger/service/product"
8 | templateService "github.com/ZiRunHua/LeapLedger/service/template"
9 | thirdpartyService "github.com/ZiRunHua/LeapLedger/service/thirdparty"
10 | transactionService "github.com/ZiRunHua/LeapLedger/service/transaction"
11 | userService "github.com/ZiRunHua/LeapLedger/service/user"
12 | )
13 |
14 | var GroupApp = new(Group)
15 |
16 | type Group struct {
17 | CommonServiceGroup commonService.Group
18 | CategoryServiceGroup categoryService.Group
19 | AccountServiceGroup accountService.Group
20 | TransactionServiceGroup transactionService.Group
21 | UserServiceGroup userService.Group
22 | ProductServiceGroup productService.Group
23 | TemplateServiceGroup templateService.Group
24 | ThirdpartyServiceGroup thirdpartyService.Group
25 | }
26 |
--------------------------------------------------------------------------------
/script/user/create/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | userModel "github.com/ZiRunHua/LeapLedger/model/user"
8 | userService "github.com/ZiRunHua/LeapLedger/service/user"
9 | "github.com/ZiRunHua/LeapLedger/util"
10 | )
11 |
12 | func main() {
13 | email := GetInput("email:")
14 | password := GetInput("password:")
15 | username := GetInput("username:")
16 | create(email, password, username)
17 | }
18 | func create(email, password, username string) {
19 | addData := userModel.AddData{
20 | Email: email,
21 | Password: util.ClientPasswordHash(email, password),
22 | Username: username,
23 | }
24 | user, err := userService.GroupApp.Register(
25 | addData, context.Background(),
26 | *userService.GroupApp.NewRegisterOption().WithSendEmail(false),
27 | )
28 | if err != nil {
29 | panic(err)
30 | }
31 | fmt.Println("create success:", user.Email, user.Username, password)
32 | }
33 |
34 | func GetInput(tip string) (userInput string) {
35 | fmt.Println(tip)
36 | _, err := fmt.Scanln(&userInput)
37 | if err != nil {
38 | return ""
39 | }
40 | return
41 | }
42 |
--------------------------------------------------------------------------------
/model/product/enter.go:
--------------------------------------------------------------------------------
1 | package productModel
2 |
3 | import (
4 | "context"
5 | "os"
6 | "path/filepath"
7 |
8 | "github.com/ZiRunHua/LeapLedger/global/constant"
9 | "github.com/ZiRunHua/LeapLedger/global/cus"
10 | "github.com/ZiRunHua/LeapLedger/global/db"
11 | "github.com/ZiRunHua/LeapLedger/util/fileTool"
12 | "gorm.io/gorm"
13 | "gorm.io/gorm/logger"
14 | )
15 |
16 | var initSqlFile = filepath.Clean(constant.DataPath + "/database/product.sql")
17 |
18 | func init() {
19 | // table
20 | tables := []interface{}{
21 | Product{}, BillHeader{}, Bill{},
22 | TransactionCategory{}, TransactionCategoryMapping{},
23 | }
24 | err := db.InitDb.AutoMigrate(tables...)
25 | if err != nil {
26 | panic(err)
27 | }
28 | // table data
29 | sqlFile, err := os.Open(initSqlFile)
30 | if err != nil {
31 | panic(err)
32 | }
33 | err = db.Transaction(
34 | context.Background(), func(ctx *cus.TxContext) error {
35 | tx := ctx.GetDb()
36 | tx = tx.Session(&gorm.Session{Logger: tx.Logger.LogMode(logger.Silent)})
37 | return fileTool.ExecSqlFile(sqlFile, tx)
38 | },
39 | )
40 | if err != nil {
41 | panic(err)
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/util/jwtTool/jwt.go:
--------------------------------------------------------------------------------
1 | package jwtTool
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "github.com/golang-jwt/jwt/v5"
7 | )
8 |
9 | var (
10 | TokenExpired error = errors.New("Token is expired")
11 | TokenNotValidYet error = errors.New("Token not active yet")
12 | TokenMalformed error = errors.New("That's not even a token")
13 | TokenInvalid error = errors.New("Couldn't handle this token:")
14 | SignKey string = "test"
15 | )
16 |
17 | func CreateToken(claims jwt.RegisteredClaims, key []byte) (string, error) {
18 | return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(key)
19 | }
20 |
21 | func ParseToken(tokenStr string, key []byte) (claims jwt.RegisteredClaims, err error) {
22 | token, err := jwt.ParseWithClaims(
23 | tokenStr, &claims, func(token *jwt.Token) (interface{}, error) {
24 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
25 | return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
26 | }
27 | return key, nil
28 | },
29 | )
30 | if err != nil {
31 | return
32 | }
33 | if !token.Valid {
34 | err = errors.New("parse token fail")
35 | }
36 | return
37 | }
38 |
--------------------------------------------------------------------------------
/router/v1/category.go:
--------------------------------------------------------------------------------
1 | package v1
2 |
3 | import (
4 | "github.com/ZiRunHua/LeapLedger/router/group"
5 | )
6 |
7 | func init() {
8 | // base path: /account/{accountId}/category
9 | readRouter := group.Account.Group("category")
10 | editRouter := group.AccountCreator.Group("category")
11 | editMappingRouter := group.AccountOwnEditor.Group("category")
12 | baseApi := apiApp.CategoryApi
13 | {
14 | editRouter.POST("", baseApi.CreateOne)
15 | editRouter.PUT("/:id/move", baseApi.MoveCategory)
16 | editRouter.PUT("/:id", baseApi.Update)
17 | editRouter.DELETE("/:id", baseApi.Delete)
18 | readRouter.GET("/tree", baseApi.GetTree)
19 | readRouter.GET("/list", baseApi.GetList)
20 | }
21 | {
22 | editRouter.POST("/father", baseApi.CreateOneFather)
23 | editRouter.PUT("/father/:id/move", baseApi.MoveFather)
24 | editRouter.PUT("/father/:id", baseApi.UpdateFather)
25 | editRouter.DELETE("/father/:id", baseApi.DeleteFather)
26 | }
27 | {
28 | editMappingRouter.POST("/:id/mapping", baseApi.MappingCategory)
29 | editMappingRouter.DELETE("/:id/mapping", baseApi.DeleteCategoryMapping)
30 | editMappingRouter.GET("/mapping/tree", baseApi.GetMappingTree)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/router/websocket/websocket.go:
--------------------------------------------------------------------------------
1 | package websocket
2 |
3 | import (
4 | "github.com/ZiRunHua/LeapLedger/global"
5 | "go.uber.org/zap"
6 | "net"
7 | "time"
8 |
9 | "github.com/gin-gonic/gin"
10 | "github.com/gorilla/websocket"
11 | )
12 |
13 | var upgrader = websocket.Upgrader{
14 | ReadBufferSize: 1024,
15 | WriteBufferSize: 1024,
16 | }
17 |
18 | func Use(handler func(conn *websocket.Conn, ctx *gin.Context) error) gin.HandlerFunc {
19 | return func(ctx *gin.Context) {
20 | conn, err := upgrader.Upgrade(ctx.Writer, ctx.Request, nil)
21 | if err != nil {
22 | panic(err)
23 | }
24 | conn.SetPingHandler(
25 | func(message string) error {
26 | err := conn.WriteControl(websocket.PongMessage, []byte(message), time.Now().Add(1))
27 | if err == websocket.ErrCloseSent {
28 | return nil
29 | } else if e, ok := err.(net.Error); ok && e.Temporary() {
30 | return nil
31 | }
32 | return err
33 | },
34 | )
35 | conn.SetPongHandler(nil)
36 | conn.SetCloseHandler(nil)
37 | defer conn.Close()
38 | err = handler(conn, ctx)
39 | if err != nil {
40 | global.ErrorLogger.Error("websocket err", zap.Error(err))
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/initialize/nats.go:
--------------------------------------------------------------------------------
1 | package initialize
2 |
3 | import (
4 | "github.com/ZiRunHua/LeapLedger/global/constant"
5 | "github.com/nats-io/nats.go"
6 | )
7 |
8 | type _nats struct {
9 | ServerUrl string `yaml:"ServerUrl"`
10 | Subjects []constant.Subject `yaml:"Subjects"`
11 | subjectMap map[constant.Subject]struct{}
12 | IsConsumerServer bool
13 | }
14 |
15 | // NatsDb is used to record and retry failure messages
16 | // Enabled in consumer server
17 |
18 | func (n *_nats) do() error {
19 | err := n.init()
20 | if err != nil {
21 | return err
22 | }
23 | Nats, err = nats.Connect(n.ServerUrl)
24 | if err != nil {
25 | return err
26 | }
27 | return err
28 | }
29 |
30 | func (n *_nats) init() error {
31 | n.subjectMap = make(map[constant.Subject]struct{})
32 | for _, subject := range n.Subjects {
33 | n.subjectMap[subject] = struct{}{}
34 | }
35 | n.IsConsumerServer = len(n.subjectMap) > 0
36 | return nil
37 | }
38 |
39 | const allTask constant.Subject = "all"
40 |
41 | func (n *_nats) CanSubscribe(subj constant.Subject) bool {
42 | if _, ok := n.subjectMap[allTask]; ok {
43 | return true
44 | }
45 | _, ok := n.subjectMap[subj]
46 | return ok
47 | }
48 |
--------------------------------------------------------------------------------
/api/request/category.go:
--------------------------------------------------------------------------------
1 | package request
2 |
3 | import (
4 | "github.com/ZiRunHua/LeapLedger/global/constant"
5 | )
6 |
7 | type CategoryOne struct {
8 | Id uint
9 | Name string
10 | Icon string
11 | FatherId uint
12 | IncomeExpense IncomeExpense
13 | }
14 |
15 | type CategoryCreateOne struct {
16 | Name string `binding:"required"`
17 | Icon string `binding:"required"`
18 | FatherId uint `binding:"required"`
19 | }
20 |
21 | type CategoryUpdateOne struct {
22 | Name *string
23 | Icon *string
24 | }
25 |
26 | type CategoryCreateOneFather struct {
27 | Name string
28 | IncomeExpense constant.IncomeExpense
29 | }
30 |
31 | type CategoryMove struct {
32 | Previous *uint
33 | FatherId *uint
34 | }
35 |
36 | type CategoryMoveFather struct {
37 | Previous *uint
38 | }
39 |
40 | type CategoryGetTree struct {
41 | IncomeExpense *constant.IncomeExpense
42 | }
43 |
44 | type CategoryGetList struct {
45 | IncomeExpense *constant.IncomeExpense `binding:"omitempty"`
46 | }
47 |
48 | type CategoryMapping struct {
49 | ChildCategoryId uint
50 | }
51 |
52 | type CategoryGetMappingTree struct {
53 | MappingAccountId uint `binding:"required"`
54 | }
55 |
--------------------------------------------------------------------------------
/model/common/query/func.go:
--------------------------------------------------------------------------------
1 | package query
2 |
3 | import (
4 | "github.com/ZiRunHua/LeapLedger/global"
5 | commonModel "github.com/ZiRunHua/LeapLedger/model/common"
6 |
7 | "github.com/pkg/errors"
8 | "gorm.io/gorm"
9 | )
10 |
11 | func FirstByPrimaryKey[T commonModel.Model](id interface{}) (T, error) {
12 | var result T
13 | err := global.GvaDb.First(&result, id).Error
14 | return result, err
15 | }
16 |
17 | func FirstByField[T commonModel.Model](field string, value interface{}) (T, error) {
18 | var result T
19 | err := global.GvaDb.Where(map[string]interface{}{field: value}).First(&result).Error
20 | return result, err
21 | }
22 |
23 | func Exist[T commonModel.Model](query interface{}, args ...interface{}) (bool, error) {
24 | var result T
25 | err := global.GvaDb.Where(query, args...).First(&result).Error
26 | if errors.Is(err, gorm.ErrRecordNotFound) {
27 | return false, nil
28 | } else if err == nil {
29 | return true, nil
30 | } else {
31 | return false, errors.Wrap(err, "exist")
32 | }
33 | }
34 |
35 | func GetAmountCountStatistic(query *gorm.DB) (result global.AmountCount, err error) {
36 | err = query.Select("COUNT(*) as Count,SUM(amount) as Amount").Scan(&result).Error
37 | return
38 | }
39 |
--------------------------------------------------------------------------------
/router/v1/transaction.go:
--------------------------------------------------------------------------------
1 | package v1
2 |
3 | import (
4 | "github.com/ZiRunHua/LeapLedger/router/group"
5 | )
6 |
7 | func init() {
8 | // base path: /account/{accountId}/transaction
9 | readRouter := group.AccountReader.Group("transaction")
10 | editRouter := group.AccountOwnEditor.Group("transaction")
11 | baseApi := apiApp.TransactionApi
12 | {
13 | readRouter.GET("/:id", baseApi.GetOne)
14 | editRouter.POST("", baseApi.CreateOne)
15 | editRouter.PUT("/:id", baseApi.Update)
16 | editRouter.DELETE("/:id", baseApi.Delete)
17 |
18 | readRouter.GET("/list", baseApi.GetList)
19 | readRouter.GET("/total", baseApi.GetTotal)
20 | readRouter.GET("/month/statistic", baseApi.GetMonthStatistic)
21 | readRouter.GET("/day/statistic", baseApi.GetDayStatistic)
22 | readRouter.GET("/category/amount/rank", baseApi.GetCategoryAmountRank)
23 | readRouter.GET("/amount/rank", baseApi.GetAmountRank)
24 | // timing
25 | editRouter.GET("/timing/list", baseApi.GetTimingList)
26 | editRouter.POST("/timing", baseApi.CreateTiming)
27 | editRouter.PUT("/timing/:id", baseApi.UpdateTiming)
28 | editRouter.DELETE("/timing/:id", baseApi.DeleteTiming)
29 | editRouter.PUT("/timing/:id/:operate", baseApi.HandleTiming)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/test/getModel.go:
--------------------------------------------------------------------------------
1 | package test
2 |
3 | import (
4 | categoryModel "github.com/ZiRunHua/LeapLedger/model/category"
5 | transactionModel "github.com/ZiRunHua/LeapLedger/model/transaction"
6 | "github.com/ZiRunHua/LeapLedger/util/rand"
7 | "time"
8 | )
9 |
10 | func getCategory() categoryModel.Category {
11 | return ExpenseCategoryList[0]
12 | }
13 | func NewTransaction() transactionModel.Transaction {
14 | return transactionModel.Transaction{
15 | Info: NewTransInfo(),
16 | }
17 | }
18 |
19 | func NewTransTime() transactionModel.Timing {
20 | transInfo := NewTransInfo()
21 | return transactionModel.Timing{
22 | AccountId: transInfo.AccountId,
23 | UserId: transInfo.UserId,
24 | TransInfo: transInfo,
25 | Type: transactionModel.EveryDay,
26 | OffsetDays: 1,
27 | NextTime: transInfo.TradeTime,
28 | Close: false,
29 | }
30 | }
31 |
32 | func NewTransInfo() transactionModel.Info {
33 | category := getCategory()
34 | return transactionModel.Info{
35 | UserId: User.ID,
36 | AccountId: Account.ID,
37 | CategoryId: category.ID,
38 | IncomeExpense: category.IncomeExpense,
39 | Amount: rand.Int(1000),
40 | Remark: "test",
41 | TradeTime: time.Now(),
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/service/transaction/event.go:
--------------------------------------------------------------------------------
1 | package transactionService
2 |
3 | import (
4 | "context"
5 | "github.com/ZiRunHua/LeapLedger/global/nats"
6 | transactionModel "github.com/ZiRunHua/LeapLedger/model/transaction"
7 | )
8 |
9 | func init() {
10 | // statistic update
11 | nats.BindTaskToEventAndMakePayload(
12 | nats.EventTransactionCreate, nats.TaskStatisticUpdate,
13 | func(eventData transactionModel.Transaction) (transactionModel.StatisticData, error) {
14 | return eventData.GetStatisticData(true), nil
15 | },
16 | )
17 |
18 | nats.SubscribeEvent(
19 | nats.EventTransactionUpdate, "update_statistic_after_transaction_update",
20 | func(eventData nats.EventTransactionUpdatePayload, ctx context.Context) error {
21 | err := GroupApp.Transaction.updateStatistic(eventData.OldTrans.GetStatisticData(false), ctx)
22 | if err != nil {
23 | return err
24 | }
25 | return GroupApp.Transaction.updateStatistic(eventData.NewTrans.GetStatisticData(true), ctx)
26 | },
27 | )
28 |
29 | nats.BindTaskToEventAndMakePayload(
30 | nats.EventTransactionDelete, nats.TaskStatisticUpdate,
31 | func(eventData transactionModel.Transaction) (transactionModel.StatisticData, error) {
32 | return eventData.GetStatisticData(false), nil
33 | },
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/global/db/db.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "context"
5 | "github.com/ZiRunHua/LeapLedger/global/cus"
6 | "github.com/ZiRunHua/LeapLedger/initialize"
7 | "gorm.io/gorm"
8 | "gorm.io/gorm/logger"
9 | )
10 |
11 | var (
12 | Db = initialize.Db
13 | InitDb = Db.Session(&gorm.Session{Logger: Db.Logger.LogMode(logger.Silent)})
14 | Context *cus.DbContext
15 | )
16 |
17 | func init() {
18 | Context = cus.WithDb(context.Background(), Db)
19 | }
20 |
21 | func Get(ctx context.Context) *gorm.DB {
22 | value := ctx.Value(cus.Db)
23 | if value == nil {
24 | return Db
25 | }
26 | return ctx.Value(cus.Db).(*gorm.DB)
27 | }
28 |
29 | type TxFunc func(ctx *cus.TxContext) error
30 |
31 | func Transaction(parent context.Context, fc TxFunc) error {
32 | ctx := cus.WithTxCommitContext(parent)
33 | err := Get(ctx).Transaction(
34 | func(tx *gorm.DB) error {
35 | return fc(cus.WithTx(ctx, tx))
36 | },
37 | )
38 | if err == nil {
39 | ctx.ExecCallback()
40 | }
41 | return err
42 | }
43 |
44 | // AddCommitCallback
45 | // Transaction needs to be called before using this method
46 | func AddCommitCallback(parent context.Context, callbacks ...cus.TxCommitCallback) error {
47 | return parent.Value(cus.TxCommit).(*cus.TxCommitContext).AddCallback(callbacks...)
48 | }
49 |
--------------------------------------------------------------------------------
/service/thirdparty/email/enter.go:
--------------------------------------------------------------------------------
1 | package email
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/ZiRunHua/LeapLedger/global/cron"
7 | )
8 |
9 | type emailService interface {
10 | init()
11 | Send(emails []string, subject string, contest string) error
12 | getToken() error
13 | }
14 |
15 | // 初始化
16 | var Service emailService
17 | var ServiceStatus bool = false
18 |
19 | func init() {
20 | Service = &WeCom{}
21 | Service.init()
22 | }
23 |
24 | // token过期
25 | var tokenExpiredError = &_tokenExpiredError{}
26 |
27 | type _tokenExpiredError struct {
28 | }
29 |
30 | func (e *_tokenExpiredError) Error() string {
31 | return "邮箱服务Token已过期"
32 | }
33 |
34 | // 第三方响应错误
35 | type thirdPartyResponseError struct {
36 | StatusCode int // 第三方响应的HTTP状态码
37 | ErrorCode int // 第三方响应的错误码
38 | Message string // 错误消息
39 | }
40 |
41 | func (e *thirdPartyResponseError) Error() string {
42 | return fmt.Sprintf("第三方响应错误,状态码:%d,错误码:%d,消息:%s", e.StatusCode, e.ErrorCode, e.Message)
43 | }
44 |
45 | func init() {
46 | _, err := cron.Scheduler.Every(30).Minute().Do(
47 | cron.MakeJobFunc(
48 | func() error {
49 | if false == ServiceStatus {
50 | return nil
51 | }
52 | return Service.getToken()
53 | },
54 | ),
55 | )
56 | if err != nil {
57 | panic(err)
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/test/util/enter.go:
--------------------------------------------------------------------------------
1 | package tUtil
2 |
3 | import (
4 | "github.com/ZiRunHua/LeapLedger/global/constant"
5 | "github.com/ZiRunHua/LeapLedger/global/db"
6 | accountModel "github.com/ZiRunHua/LeapLedger/model/account"
7 | categoryModel "github.com/ZiRunHua/LeapLedger/model/category"
8 | userModel "github.com/ZiRunHua/LeapLedger/model/user"
9 | _service "github.com/ZiRunHua/LeapLedger/service"
10 | "github.com/ZiRunHua/LeapLedger/test/initialize"
11 | )
12 |
13 | var (
14 | userService = _service.GroupApp.UserServiceGroup
15 | templateService = _service.GroupApp.TemplateServiceGroup
16 |
17 | testUser userModel.User
18 | testAccount accountModel.Account
19 | testCategoryList []categoryModel.Category
20 | testInfo = initialize.Info
21 | )
22 |
23 | func init() {
24 | var err error
25 | testUser, err = userModel.NewDao().SelectById(testInfo.UserId)
26 | if err != nil {
27 | panic(err)
28 | }
29 | userInfo, err := testUser.GetUserClient(constant.Web, db.Db)
30 | if err != nil {
31 | panic(err)
32 | }
33 | testAccount, err = accountModel.NewDao().SelectById(userInfo.CurrentAccountId)
34 | if err != nil {
35 | panic(err)
36 | }
37 | testCategoryList, err = categoryModel.NewDao().GetListByAccount(testAccount, nil)
38 | if err != nil {
39 | panic(err)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/test/initialize/initialize.go:
--------------------------------------------------------------------------------
1 | package initialize
2 |
3 | import (
4 | "github.com/ZiRunHua/LeapLedger/global/constant"
5 | "github.com/ZiRunHua/LeapLedger/global/db"
6 | _ "github.com/ZiRunHua/LeapLedger/global/nats"
7 | "github.com/ZiRunHua/LeapLedger/global/nats/manager"
8 | _ "github.com/ZiRunHua/LeapLedger/initialize/database"
9 | "github.com/ZiRunHua/LeapLedger/test/info"
10 | )
11 | import (
12 | accountModel "github.com/ZiRunHua/LeapLedger/model/account"
13 | categoryModel "github.com/ZiRunHua/LeapLedger/model/category"
14 | userModel "github.com/ZiRunHua/LeapLedger/model/user"
15 | )
16 |
17 | var (
18 | User userModel.User
19 | Account accountModel.Account
20 | ExpenseCategoryList []categoryModel.Category
21 | )
22 |
23 | var (
24 | Info = info.Data
25 | )
26 |
27 | func init() {
28 | manager.UpdateTestBackOff()
29 | var err error
30 | User, err = userModel.NewDao().SelectById(Info.UserId)
31 | if err != nil {
32 | panic(err)
33 | }
34 | userInfo, err := User.GetUserClient(constant.Web, db.Db)
35 | if err != nil {
36 | panic(err)
37 | }
38 | Account, err = accountModel.NewDao().SelectById(userInfo.CurrentAccountId)
39 | if err != nil {
40 | panic(err)
41 | }
42 | ie := constant.Expense
43 | ExpenseCategoryList, err = categoryModel.NewDao().GetListByAccount(Account, &ie)
44 | if err != nil {
45 | panic(err)
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/global/lock/redis.go:
--------------------------------------------------------------------------------
1 | package lock
2 |
3 | import (
4 | "context"
5 | "github.com/ZiRunHua/LeapLedger/initialize"
6 | "github.com/go-redis/redis/v8"
7 | "github.com/google/uuid"
8 | "time"
9 | )
10 |
11 | var (
12 | rdb = initialize.LockRdb
13 | )
14 |
15 | type RedisLock struct {
16 | client *redis.Client
17 | key string
18 | value string
19 | expiration time.Duration
20 | }
21 |
22 | func newRedisLock(client *redis.Client, key string, expiration time.Duration) *RedisLock {
23 | return &RedisLock{
24 | client: client,
25 | key: key,
26 | value: uuid.New().String(),
27 | expiration: expiration,
28 | }
29 | }
30 |
31 | func (rl *RedisLock) Lock(ctx context.Context) error {
32 | success, err := rl.client.SetNX(ctx, rl.key, rl.value, rl.expiration).Result()
33 | if err != nil {
34 | return err
35 | }
36 | if false == success {
37 | return ErrLockOccupied
38 | }
39 | return nil
40 | }
41 |
42 | func (rl *RedisLock) Release(ctx context.Context) error {
43 | script := `
44 | if redis.call("GET", KEYS[1]) == ARGV[1] then
45 | return redis.call("DEL", KEYS[1])
46 | else
47 | return 0
48 | end
49 | `
50 | result, err := rl.client.Eval(ctx, script, []string{rl.key}, rl.value).Int()
51 | if err != nil {
52 | return err
53 | } else if result == 0 {
54 | return ErrLockNotExist
55 | }
56 | return nil
57 | }
58 |
--------------------------------------------------------------------------------
/api/v1/enter.go:
--------------------------------------------------------------------------------
1 | package v1
2 |
3 | import (
4 | "github.com/ZiRunHua/LeapLedger/api/response"
5 | apiUtil "github.com/ZiRunHua/LeapLedger/api/util"
6 | "github.com/ZiRunHua/LeapLedger/service"
7 | "github.com/gin-gonic/gin"
8 | )
9 |
10 | type PublicApi struct {
11 | }
12 |
13 | type ApiGroup struct {
14 | AccountApi
15 | CategoryApi
16 | UserApi
17 | TransactionApi
18 | PublicApi
19 | ProductApi
20 | }
21 |
22 | var (
23 | ApiGroupApp = new(ApiGroup)
24 | )
25 |
26 | // 服务
27 | var (
28 | commonService = service.GroupApp.CommonServiceGroup
29 | )
30 | var (
31 | userService = service.GroupApp.UserServiceGroup
32 | accountService = service.GroupApp.AccountServiceGroup
33 | categoryService = service.GroupApp.CategoryServiceGroup
34 | transactionService = service.GroupApp.TransactionServiceGroup
35 | productService = service.GroupApp.ProductServiceGroup
36 | templateService = service.GroupApp.TemplateServiceGroup
37 | )
38 |
39 | // 工具
40 | var contextFunc = apiUtil.ContextFunc
41 | var checkFunc = apiUtil.CheckFunc
42 |
43 | func handelError(err error, ctx *gin.Context) bool {
44 | if err != nil {
45 | response.FailToError(ctx, err)
46 | return true
47 | }
48 | return false
49 | }
50 |
51 | func responseError(err error, ctx *gin.Context) bool {
52 | if err != nil {
53 | response.FailToError(ctx, err)
54 | return true
55 | }
56 | return false
57 | }
58 |
--------------------------------------------------------------------------------
/model/log/enter.go:
--------------------------------------------------------------------------------
1 | package logModel
2 |
3 | import (
4 | "github.com/ZiRunHua/LeapLedger/global/constant"
5 | "github.com/ZiRunHua/LeapLedger/global/db"
6 | "gorm.io/gorm"
7 | )
8 |
9 | func init() {
10 | tables := []interface{}{
11 | AccountMappingLog{}, AccountLogMapping{},
12 | }
13 | err := db.InitDb.AutoMigrate(tables...)
14 | if err != nil {
15 | panic(err)
16 | }
17 | }
18 |
19 | /*账本*/
20 | type AccountLog[T AccountLogDataRecordable] struct {
21 | BaseAccountLog `gorm:"embedded"`
22 | Data T `gorm:"type:json"`
23 | }
24 |
25 | type BaseAccountLog struct {
26 | ID uint `gorm:"primarykey"`
27 | UserId uint `gorm:"index:idx_user_id;not null"`
28 | AccountId uint `gorm:"index:idx_account_id;not null"`
29 | Operation constant.LogOperation `gorm:"not null"`
30 | }
31 |
32 | func (b *BaseAccountLog) GetId() uint {
33 | return b.ID
34 | }
35 | func (b *BaseAccountLog) GetAccountId() uint {
36 | return b.AccountId
37 | }
38 |
39 | type AccountLogger interface {
40 | TableName() string
41 | GetId() uint
42 | GetAccountId() uint
43 | RecordMapping(tx *gorm.DB) (AccountLogMapping, error)
44 | }
45 |
46 | type AccountLogDataRecordable interface {
47 | Record(baseLog BaseAccountLog, tx *gorm.DB) (AccountLogger, error)
48 | }
49 |
50 | type AccountLogDataProvider interface {
51 | GetLogDataModel() AccountLogDataRecordable
52 | }
53 |
--------------------------------------------------------------------------------
/model/user/userLogModel.go:
--------------------------------------------------------------------------------
1 | package userModel
2 |
3 | import (
4 | "github.com/ZiRunHua/LeapLedger/global"
5 | "github.com/ZiRunHua/LeapLedger/global/constant"
6 | commonModel "github.com/ZiRunHua/LeapLedger/model/common"
7 | "gorm.io/gorm"
8 | "time"
9 | )
10 |
11 | type Log struct {
12 | ID uint `gorm:"primarykey"`
13 | UserId uint `gorm:"comment:用户id;not null"`
14 | Action constant.UserAction `gorm:"comment:操作;not null;size:32"`
15 | Remark string `gorm:"comment:备注;not null;size:255"`
16 | CreatedAt time.Time `gorm:"type:TIMESTAMP"`
17 | UpdatedAt time.Time `gorm:"type:TIMESTAMP"`
18 | DeletedAt gorm.DeletedAt `gorm:"index;type:TIMESTAMP"`
19 | commonModel.BaseModel
20 | }
21 |
22 | func (l *Log) TableName() string {
23 | return "user_log"
24 | }
25 |
26 | func (l *Log) IsEmpty() bool {
27 | return l.ID == 0
28 | }
29 |
30 | type LogDao struct {
31 | db *gorm.DB
32 | }
33 |
34 | func NewLogDao(db *gorm.DB) *LogDao {
35 | if db == nil {
36 | db = global.GvaDb
37 | }
38 | return &LogDao{db}
39 | }
40 |
41 | type LogAddData struct {
42 | Action constant.UserAction
43 | Remark string
44 | }
45 |
46 | func (l *LogDao) Add(user User, data *LogAddData) (*Log, error) {
47 | log := &Log{
48 | UserId: user.ID,
49 | Action: data.Action,
50 | Remark: data.Remark,
51 | }
52 | err := l.db.Create(&log).Error
53 | return log, err
54 | }
55 |
--------------------------------------------------------------------------------
/docker/mysql.cnf:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
2 | #
3 | # This program is free software; you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License, version 2.0,
5 | # as published by the Free Software Foundation.
6 | #
7 | # This program is also distributed with certain software (including
8 | # but not limited to OpenSSL) that is licensed under separate terms,
9 | # as designated in a particular file or component or in included license
10 | # documentation. The authors of MySQL hereby grant you an additional
11 | # permission to link the program and your derivative works with the
12 | # separately licensed software that they have included with MySQL.
13 | #
14 | # This program is distributed in the hope that it will be useful,
15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | # GNU General Public License, version 2.0, for more details.
18 | #
19 | # You should have received a copy of the GNU General Public License
20 | # along with this program; if not, write to the Free Software
21 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
22 |
23 | #
24 | # The MySQL Client configuration file.
25 | #
26 | # For explanations see
27 | # http://dev.mysql.com/doc/mysql/en/server-system-variables.html
28 |
29 | [mysqld]
30 | transaction-isolation = READ-COMMITTED
--------------------------------------------------------------------------------
/global/lock/lock.go:
--------------------------------------------------------------------------------
1 | package lock
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "github.com/ZiRunHua/LeapLedger/global"
7 | "time"
8 | )
9 |
10 | var (
11 | currentMode mode
12 |
13 | New func(key Key) Lock
14 | NewWithDuration func(key Key, duration time.Duration) Lock
15 |
16 | ErrLockNotExist = errors.New("lock not exist")
17 | ErrLockOccupied = errors.New("lock occupied")
18 | )
19 |
20 | type mode string
21 |
22 | const (
23 | mysqlMode mode = "mysql"
24 | redisMode mode = "redis"
25 | )
26 |
27 | type Lock interface {
28 | Lock(context.Context) error
29 | Release(context.Context) error
30 | }
31 |
32 | func init() {
33 | currentMode = mode(global.Config.System.LockMode)
34 | updatePublicFunc()
35 | }
36 | func updatePublicFunc() {
37 | switch currentMode {
38 | case mysqlMode:
39 | mdb = global.GvaDb
40 | err := mdb.AutoMigrate(&lockTable{})
41 | if err != nil {
42 | panic(err)
43 | }
44 | New = func(key Key) Lock {
45 | return newMysqlLock(mdb, string(key), time.Second*30)
46 | }
47 | NewWithDuration = func(key Key, duration time.Duration) Lock {
48 | return newMysqlLock(mdb, string(key), duration)
49 | }
50 | return
51 | case redisMode:
52 | rdb = global.GvaRdb
53 | if rdb == nil {
54 | panic("initialize.LockRdb is nil")
55 | }
56 | New = func(key Key) Lock {
57 | return newRedisLock(rdb, string(key), time.Second*30)
58 | }
59 | NewWithDuration = func(key Key, duration time.Duration) Lock {
60 | return newRedisLock(rdb, string(key), duration)
61 | }
62 | return
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/.github/config.yaml:
--------------------------------------------------------------------------------
1 | # 服务初始化的配置文件 执行代码详见 ./initialize/initialize.go
2 | # This is the service initialization configuration file execution code see ./initialize/initialize.go
3 |
4 | # can set `debug` or `production`
5 | # `debug` mode can print mysql and redis logs and `/public/swagger/index.html` api
6 | Mode: debug
7 |
8 | System:
9 | Addr: 8080
10 | RouterPrefix: ""
11 | JwtKey: ""
12 | # Some apis use symmetric signature keys that can be configured to improve security
13 | ClientSignKey: ""
14 |
15 | Redis:
16 | Addr: "localhost:6379"
17 | Password: ""
18 | Db: 0
19 | LockDb: 1
20 |
21 | Mysql:
22 | Path: "localhost"
23 | Port: "3306"
24 | Config: "parseTime=True&loc=Local"
25 | DbName: "leap_ledger"
26 | Username: "root"
27 | Password: ""
28 |
29 | Nats:
30 | ServerUrl: localhost:4222
31 | # This is the topic that the consumer server needs to subscribe to, such as createTourist, statisticUpdate, transactionSync.
32 | # subjects see ./global/nats/nats.go
33 | Subjects: [all]
34 |
35 | Captcha:
36 | KeyLong: 6
37 | ImgWidth: 180
38 | ImgHeight: 50
39 | OpenCaptcha: 0
40 | OpenCaptchaTimeout: 3600
41 | EmailCaptcha: 0
42 | EmailCaptchaTimeOut: 3600
43 |
44 | ThirdParty:
45 | # WeCom used to send domain email
46 | WeCom:
47 | CorpId: ""
48 | CorpSecret: ""
49 | # Ai is used to obtain Chinese similarity, which is used to match transaction types
50 | # https://huggingface.co/google-bert/bert-base-chinese
51 | Ai:
52 | Host: "" # leap-ledger-ai-server
53 | Port: "" # 5000
54 | MinSimilarity: 0.85
--------------------------------------------------------------------------------
/global/lock/mysql.go:
--------------------------------------------------------------------------------
1 | package lock
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "time"
7 |
8 | "github.com/google/uuid"
9 | "gorm.io/gorm"
10 | )
11 |
12 | var (
13 | mdb *gorm.DB
14 | )
15 |
16 | type lockTable struct {
17 | Key string `gorm:"primarykey"`
18 | Value string
19 | Expiration int64
20 | }
21 |
22 | func (lt *lockTable) TableName() string { return "core_lock" }
23 |
24 | func newMysqlLock(client *gorm.DB, key string, expiration time.Duration) *MysqlLock {
25 | return &MysqlLock{
26 | client: client,
27 | key: key,
28 | value: uuid.New().String(),
29 | expiration: expiration,
30 | }
31 | }
32 |
33 | type MysqlLock struct {
34 | client *gorm.DB
35 | key string
36 | value string
37 | expiration time.Duration
38 | }
39 |
40 | func (ml *MysqlLock) Lock(ctx context.Context) error {
41 | err := ml.client.WithContext(ctx).Create(
42 | &lockTable{
43 | Key: ml.key,
44 | Value: ml.value,
45 | Expiration: time.Now().Add(ml.expiration).Unix(),
46 | },
47 | ).Error
48 | if err != nil {
49 | if errors.Is(err, gorm.ErrDuplicatedKey) {
50 | return ErrLockOccupied
51 | }
52 | return err
53 | }
54 | return nil
55 | }
56 |
57 | func (ml *MysqlLock) Release(ctx context.Context) error {
58 | result := ml.client.WithContext(ctx).Where("`key` = ? AND `value` = ?", ml.key, ml.value).Delete(&lockTable{})
59 | err := result.Error
60 | if err != nil {
61 | return err
62 | }
63 | if result.RowsAffected == 0 {
64 | return ErrLockNotExist
65 | }
66 | return nil
67 | }
68 |
--------------------------------------------------------------------------------
/service/product/productService.go:
--------------------------------------------------------------------------------
1 | package productService
2 |
3 | import (
4 | "context"
5 | "github.com/ZiRunHua/LeapLedger/global"
6 | "github.com/ZiRunHua/LeapLedger/global/db"
7 | categoryModel "github.com/ZiRunHua/LeapLedger/model/category"
8 | productModel "github.com/ZiRunHua/LeapLedger/model/product"
9 | "github.com/pkg/errors"
10 | )
11 |
12 | type Product struct {
13 | }
14 |
15 | func (proService *Product) MappingTransactionCategory(
16 | category categoryModel.Category, productTransCat productModel.TransactionCategory, ctx context.Context,
17 | ) (*productModel.TransactionCategoryMapping, error) {
18 | if category.IncomeExpense != productTransCat.IncomeExpense {
19 | return nil, errors.Wrap(global.ErrInvalidParameter, "")
20 | }
21 | mapping := &productModel.TransactionCategoryMapping{
22 | AccountId: category.AccountId,
23 | CategoryId: category.ID,
24 | PtcId: productTransCat.ID,
25 | ProductKey: productTransCat.ProductKey,
26 | }
27 | err := db.Get(ctx).Model(mapping).Create(mapping).Error
28 | return mapping, err
29 | }
30 |
31 | func (proService *Product) DeleteMappingTransactionCategory(
32 | category categoryModel.Category, productTransCat productModel.TransactionCategory, ctx context.Context,
33 | ) error {
34 | if category.IncomeExpense != productTransCat.IncomeExpense {
35 | return errors.Wrap(global.ErrInvalidParameter, "")
36 | }
37 | err := db.Get(ctx).Where(
38 | "category_id = ? AND ptc_id = ?", category.ID, productTransCat.ID,
39 | ).Delete(&productModel.TransactionCategoryMapping{}).Error
40 | return err
41 | }
42 |
--------------------------------------------------------------------------------
/config.yaml:
--------------------------------------------------------------------------------
1 | # 服务初始化的配置文件 执行代码详见 ./initialize/initialize.go
2 | # This is the service initialization configuration file execution code see ./initialize/initialize.go
3 |
4 | # can set `debug` or `production`
5 | # `debug` mode can print mysql and redis logs and `/public/swagger/index.html` api
6 | Mode: debug
7 |
8 | System:
9 | Addr: 8080
10 | RouterPrefix: ""
11 | JwtKey: ""
12 | # Some apis use symmetric signature keys that can be configured to improve security
13 | ClientSignKey: ""
14 |
15 | Redis:
16 | Addr: "leap-ledger-redis:6379"
17 | Password: ""
18 | Db: 0
19 | LockDb: 1
20 |
21 | Mysql:
22 | Path: "leap-ledger-mysql"
23 | Port: "3306"
24 | Config: "parseTime=True&loc=Local"
25 | DbName: "leap_ledger"
26 | Username: "root"
27 | Password: ""
28 |
29 | Nats:
30 | ServerUrl: leap-ledger-nats:4222
31 | # This is the topic that the consumer server needs to subscribe to, such as createTourist, statisticUpdate, transactionSync.
32 | # subjects see ./global/nats/nats.go
33 | Subjects: [all]
34 |
35 | Captcha:
36 | KeyLong: 6
37 | ImgWidth: 180
38 | ImgHeight: 50
39 | OpenCaptcha: 0
40 | OpenCaptchaTimeout: 3600
41 | EmailCaptcha: 0
42 | EmailCaptchaTimeOut: 3600
43 |
44 | ThirdParty:
45 | # WeCom used to send domain email
46 | WeCom:
47 | CorpId: ""
48 | CorpSecret: ""
49 | # Ai is used to obtain Chinese similarity, which is used to match transaction types
50 | # https://huggingface.co/google-bert/bert-base-chinese
51 | Ai:
52 | Host: "" # leap-ledger-ai-server
53 | Port: "" # 5000
54 | MinSimilarity: 0.85
--------------------------------------------------------------------------------
/model/product/productBillHeaderModel.go:
--------------------------------------------------------------------------------
1 | package productModel
2 |
3 | import (
4 | "github.com/ZiRunHua/LeapLedger/global"
5 | commonModel "github.com/ZiRunHua/LeapLedger/model/common"
6 | )
7 |
8 | type BillHeader struct {
9 | ID uint
10 | ProductKey string `gorm:"not null;uniqueIndex:product_header_type,priority:1"`
11 | Name string
12 | Type BillHeaderType `gorm:"not null;uniqueIndex:product_header_type,priority:2"`
13 | commonModel.BaseModel
14 | }
15 | type BillHeaderType string
16 |
17 | const (
18 | TransTime BillHeaderType = "trans_time"
19 | TransCategory BillHeaderType = "trans_category"
20 | Remark BillHeaderType = "remark"
21 | IncomeExpense BillHeaderType = "income_expense"
22 | Amount BillHeaderType = "amount"
23 | OrderNumber BillHeaderType = "order_number"
24 | TransStatus BillHeaderType = "trans_status"
25 | )
26 |
27 | func (b *BillHeader) TableName() string {
28 | return "product_bill_header"
29 | }
30 |
31 | func (b *BillHeader) IsEmpty() bool {
32 | return b.ID == 0
33 | }
34 |
35 | func (tc *BillHeader) GetNameMap(productKey string) (
36 | NameMap map[string]BillHeader, err error,
37 | ) {
38 | var billHeader BillHeader
39 | rows, err := global.GvaDb.Model(&billHeader).Where(
40 | "product_key = ? ", productKey,
41 | ).Rows()
42 | defer rows.Close()
43 | if err != nil {
44 | return
45 | }
46 | NameMap = map[string]BillHeader{}
47 | for rows.Next() {
48 | err = global.GvaDb.ScanRows(rows, &billHeader)
49 | if err != nil {
50 | return
51 | }
52 | NameMap[billHeader.Name] = billHeader
53 | }
54 | return
55 | }
56 |
--------------------------------------------------------------------------------
/initialize/logger.go:
--------------------------------------------------------------------------------
1 | package initialize
2 |
3 | import (
4 | "os"
5 | "path/filepath"
6 |
7 | "github.com/ZiRunHua/LeapLedger/global/constant"
8 | "go.uber.org/zap"
9 | "go.uber.org/zap/zapcore"
10 | )
11 |
12 | type _logger struct {
13 | encoder zapcore.Encoder
14 | }
15 |
16 | var (
17 | _requestLogPath = filepath.Join(constant.RootDir, "log", "request.log")
18 | _errorLogPath = filepath.Join(constant.RootDir, "log", "error.log")
19 | _panicLogPath = filepath.Join(constant.RootDir, "log", "panic.log")
20 | )
21 |
22 | func (l *_logger) do() error {
23 | l.setEncoder()
24 | var err error
25 | if RequestLogger, err = l.New(_requestLogPath); err != nil {
26 | return err
27 | }
28 | if ErrorLogger, err = l.New(_errorLogPath); err != nil {
29 | return err
30 | }
31 | if PanicLogger, err = l.New(_panicLogPath); err != nil {
32 | return err
33 | }
34 | return nil
35 | }
36 |
37 | func (l *_logger) setEncoder() {
38 | encoderConfig := zap.NewProductionEncoderConfig()
39 | encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
40 | encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
41 | l.encoder = zapcore.NewConsoleEncoder(encoderConfig)
42 | }
43 |
44 | func (l *_logger) New(path string) (*zap.Logger, error) {
45 | err := os.MkdirAll(filepath.Dir(path), os.ModePerm)
46 | if err != nil {
47 | return nil, err
48 | }
49 | file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
50 | if err != nil {
51 | return nil, err
52 | }
53 | writeSyncer := zapcore.AddSync(file)
54 | core := zapcore.NewCore(l.encoder, writeSyncer, zapcore.DebugLevel)
55 | return zap.New(core), nil
56 | }
57 |
--------------------------------------------------------------------------------
/test/enter.go:
--------------------------------------------------------------------------------
1 | package test
2 |
3 | import (
4 | categoryModel "github.com/ZiRunHua/LeapLedger/model/category"
5 | "github.com/ZiRunHua/LeapLedger/test/initialize"
6 | )
7 |
8 | var (
9 | User = initialize.User
10 | Account = initialize.Account
11 | ExpenseCategoryList []categoryModel.Category
12 | )
13 |
14 | func init() {
15 | ExpenseCategoryList = initialize.ExpenseCategoryList
16 | }
17 |
18 | var Timezones = []string{
19 | "UTC",
20 | "America/New_York",
21 | "Europe/London",
22 | "Asia/Tokyo",
23 | "Australia/Sydney",
24 | "Europe/Paris",
25 | "Asia/Shanghai",
26 | "America/Los_Angeles",
27 | "Europe/Berlin",
28 | "Asia/Kolkata",
29 | "America/Chicago",
30 | "Europe/Moscow",
31 | "Asia/Dubai",
32 | "America/Denver",
33 | "Europe/Madrid",
34 | "Asia/Singapore",
35 | "America/Phoenix",
36 | "Europe/Rome",
37 | "Asia/Hong_Kong",
38 | "America/Anchorage",
39 | "Europe/Athens",
40 | "Asia/Seoul",
41 | "America/Halifax",
42 | "Europe/Stockholm",
43 | "Asia/Bangkok",
44 | "America/St_Johns",
45 | "Europe/Helsinki",
46 | "Asia/Jakarta",
47 | "America/Sao_Paulo",
48 | "Europe/Warsaw",
49 | "Asia/Kuala_Lumpur",
50 | "America/Argentina/Buenos_Aires",
51 | "Europe/Istanbul",
52 | "Asia/Manila",
53 | "America/Mexico_City",
54 | "Europe/Brussels",
55 | "Asia/Taipei",
56 | "America/Toronto",
57 | "Europe/Vienna",
58 | "Asia/Riyadh",
59 | "America/Caracas",
60 | "Europe/Zurich",
61 | "Asia/Baghdad",
62 | "America/Lima",
63 | "Europe/Copenhagen",
64 | "Asia/Tehran",
65 | "America/Fort_Nelson",
66 | "America/Hermosillo",
67 | "America/Chicago",
68 | "America/Mexico_City",
69 | }
70 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/ZiRunHua/LeapLedger/initialize"
5 | _ "github.com/ZiRunHua/LeapLedger/initialize/database"
6 | "github.com/ZiRunHua/LeapLedger/router"
7 | )
8 | import (
9 | "context"
10 | "fmt"
11 | "log"
12 | "net/http"
13 | "os"
14 | "os/signal"
15 | "syscall"
16 | "time"
17 | // Import "time/tzdata" for loading time zone data,
18 | // so in order to make the binary files can be run independently,
19 | // or in need of extra "$GOROOT/lib/time/zoneinfo.Zip" file, see time.LoadLocation
20 | _ "time/tzdata"
21 | )
22 |
23 | var httpServer *http.Server
24 |
25 | // @title LeapLedger API
26 | // @version 1.0
27 |
28 | // @contact.name ZiRunHua
29 |
30 | // @license.name AGPL 3.0
31 | // @license.url https://www.gnu.org/licenses/agpl-3.0.html
32 |
33 | // @host localhost:8080
34 |
35 | // @securityDefinitions.jwt Bearer
36 | // @in header
37 | // @name Authorization
38 | func main() {
39 | httpServer = &http.Server{
40 | Addr: fmt.Sprintf(":%d", initialize.Config.System.Addr),
41 | Handler: router.Engine,
42 | ReadTimeout: 5 * time.Second,
43 | WriteTimeout: 5 * time.Second,
44 | MaxHeaderBytes: 1 << 20,
45 | }
46 | err := httpServer.ListenAndServe()
47 | if err != nil {
48 | panic(err)
49 | }
50 | shutDown()
51 | }
52 |
53 | func shutDown() {
54 | quit := make(chan os.Signal, 1)
55 | signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
56 | <-quit
57 | log.Println("Shutting down server...")
58 |
59 | if err := httpServer.Shutdown(context.TODO()); err != nil {
60 | log.Fatal("Server forced to shutdown:", err)
61 | }
62 |
63 | log.Println("Server exiting")
64 | }
65 |
--------------------------------------------------------------------------------
/router/group/group.go:
--------------------------------------------------------------------------------
1 | package group
2 |
3 | import (
4 | "github.com/ZiRunHua/LeapLedger/global"
5 | "github.com/ZiRunHua/LeapLedger/global/cus"
6 | accountModel "github.com/ZiRunHua/LeapLedger/model/account"
7 | routerEngine "github.com/ZiRunHua/LeapLedger/router/engine"
8 | "github.com/ZiRunHua/LeapLedger/router/middleware"
9 | "github.com/gin-gonic/gin"
10 | )
11 |
12 | var engine = routerEngine.Engine
13 | var (
14 | Public, Private *gin.RouterGroup
15 | Account *gin.RouterGroup
16 |
17 | NoTourist *gin.RouterGroup
18 |
19 | AccountReader *gin.RouterGroup
20 | AccountOwnEditor *gin.RouterGroup
21 | AccountAdministrator *gin.RouterGroup
22 | AccountCreator *gin.RouterGroup
23 | )
24 |
25 | const accountWithIdPrefixPath = "/account/:" + string(cus.AccountId)
26 |
27 | func init() {
28 | Public = engine.Group(global.Config.System.RouterPrefix + "/public")
29 | Private = engine.Group(global.Config.System.RouterPrefix, middleware.JWTAuth())
30 |
31 | NoTourist = Private.Group("")
32 | NoTourist.Use(middleware.NoTourist())
33 | // account router
34 | Account = Private.Group(accountWithIdPrefixPath)
35 | // account role
36 | AccountReader = createAccountRoleGroup(accountModel.UserPermissionReader)
37 | AccountOwnEditor = createAccountRoleGroup(accountModel.UserPermissionOwnEditor)
38 | AccountAdministrator = createAccountRoleGroup(accountModel.UserPermissionAdministrator)
39 | AccountCreator = createAccountRoleGroup(accountModel.UserPermissionCreator)
40 | }
41 |
42 | func createAccountRoleGroup(permission accountModel.UserPermission) *gin.RouterGroup {
43 | group := Account.Group("", middleware.AccountAuth(permission))
44 | return group
45 | }
46 |
--------------------------------------------------------------------------------
/script/user.go:
--------------------------------------------------------------------------------
1 | package script
2 |
3 | import (
4 | "context"
5 | accountModel "github.com/ZiRunHua/LeapLedger/model/account"
6 | userModel "github.com/ZiRunHua/LeapLedger/model/user"
7 | "github.com/ZiRunHua/LeapLedger/util"
8 | "github.com/ZiRunHua/LeapLedger/util/rand"
9 | "gorm.io/gorm"
10 | )
11 |
12 | type _user struct {
13 | }
14 |
15 | var User = _user{}
16 |
17 | // Create("template@gmail.com","1999123456","template")
18 | func (u *_user) Create(email, password, username string, ctx context.Context) (userModel.User, error) {
19 | addData := userModel.AddData{
20 | Email: email,
21 | Password: util.ClientPasswordHash(email, password),
22 | Username: username,
23 | }
24 | return userService.Register(addData, ctx, *userService.NewRegisterOption().WithSendEmail(false))
25 | }
26 |
27 | func (u *_user) CreateTourist(ctx context.Context) (user userModel.User, err error) {
28 | email := rand.String(12)
29 | password := rand.String(8)
30 | username := rand.String(8)
31 | addData := userModel.AddData{
32 | Email: email,
33 | Password: util.ClientPasswordHash(email, password),
34 | Username: username,
35 | }
36 | option := userService.NewRegisterOption().WithTour(true)
37 | user, err = userService.Register(addData, ctx, *option)
38 | if err != nil {
39 | return
40 | }
41 | return
42 | }
43 |
44 | func (u *_user) ChangeCurrantAccount(accountUser accountModel.User, db *gorm.DB) (err error) {
45 | for _, client := range userModel.GetClients() {
46 | err = db.Model(&client).Where("user_id = ?", accountUser.UserId).Update(
47 | "current_account_id", accountUser.AccountId,
48 | ).Error
49 | if err != nil {
50 | return
51 | }
52 | }
53 | return
54 | }
55 |
--------------------------------------------------------------------------------
/global/error.go:
--------------------------------------------------------------------------------
1 | package global
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/pkg/errors"
7 | )
8 |
9 | var (
10 | ErrInvalidParameter = errors.New("invalid parameter")
11 | ErrNoPermission = errors.New("无权限")
12 |
13 | ErrFrequentOperation = errors.New("操作太频繁,稍后再试")
14 | ErrDeviceNotSupported = errors.New("当前设备不支持")
15 |
16 | ErrTooManyTourists = errors.New("游客过多稍后再试")
17 | )
18 |
19 | // 数据校验
20 | var (
21 | ErrTimeFrameIsTooLong = errors.New("时间范围过长")
22 |
23 | ErrAccountId = errors.New("error accountId")
24 | )
25 |
26 | type errDataIsEmpty struct {
27 | Field string
28 | }
29 |
30 | func (e *errDataIsEmpty) Error() string {
31 | return e.Field + "数据不可为空"
32 | }
33 |
34 | func NewErrDataIsEmpty(param string) error {
35 | return &errDataIsEmpty{
36 | Field: param,
37 | }
38 | }
39 |
40 | var ErrOperationTooFrequent = errors.New("操作过于频繁,请稍后再试!")
41 | var ErrVerifyEmailCaptchaFail = errors.New("校验邮箱验证码失败!")
42 | var ErrServiceClosed = errors.New("服务未开启!")
43 | var ErrTouristHaveNoRight = errors.New("游客无权操作!")
44 |
45 | // 对应constant.UserAction
46 | var ErrUnsupportedUserAction = errors.New("暂不支持该操作")
47 |
48 | // 用户
49 | var ErrSameAsTheOldPassword = errors.New("新旧密码相同")
50 |
51 | // 账本
52 | var ErrAccountType = errors.New("账本类型不允许该操作")
53 |
54 | // 交易类型
55 | var ErrCategoryNameEmpty = errors.New("名称不可为空")
56 | var ErrCategorySameName = errors.New("类型名称相同")
57 |
58 | func NewErrThirdpartyApi(name, msg string) error {
59 | return &errThirdpartyApi{Name: name, Msg: msg}
60 | }
61 |
62 | type errThirdpartyApi struct {
63 | Name, Msg string
64 | }
65 |
66 | func (eta *errThirdpartyApi) Error() string {
67 | return fmt.Sprintf("第三方%s接口服务错误:%s", eta.Name, eta.Msg)
68 | }
69 |
--------------------------------------------------------------------------------
/router/v1/user.go:
--------------------------------------------------------------------------------
1 | package v1
2 |
3 | import (
4 | "github.com/ZiRunHua/LeapLedger/router/group"
5 | )
6 |
7 | func init() {
8 | // base path: /user or /public/user
9 | router := group.Private.Group("user")
10 | publicRouter := group.Public.Group("user")
11 | noTouristRouter := group.NoTourist.Group("user")
12 | baseApi := apiApp.UserApi
13 | {
14 | // public
15 | publicRouter.POST("/login", publicApi.Login)
16 | publicRouter.POST("/register", publicApi.Register)
17 | publicRouter.PUT("/password", publicApi.UpdatePassword)
18 | publicRouter.POST("/tour", publicApi.TourRequest)
19 | // current user
20 | noTouristRouter.POST("/current/captcha/email/send", baseApi.SendCaptchaEmail)
21 | router.POST("/token/refresh", baseApi.RefreshToken)
22 | router.PUT("/client/current/account", baseApi.SetCurrentAccount)
23 | router.PUT("/client/current/share/account", baseApi.SetCurrentShareAccount)
24 | noTouristRouter.PUT("/current/password", baseApi.UpdatePassword)
25 | noTouristRouter.PUT("/current", baseApi.UpdateInfo)
26 | router.GET("/home", baseApi.Home)
27 | // all user
28 | router.GET("/search", baseApi.SearchUser)
29 | // config
30 | router.GET("/transaction/share/config", baseApi.GetTransactionShareConfig)
31 | router.PUT("/transaction/share/config", baseApi.UpdateTransactionShareConfig)
32 | // friend
33 | router.GET("/friend/list", baseApi.GetFriendList)
34 | router.POST("/friend/invitation", baseApi.CreateFriendInvitation)
35 | router.PUT("/friend/invitation/:id/accept", baseApi.AcceptFriendInvitation)
36 | router.PUT("/friend/invitation/:id/refuse", baseApi.RefuseFriendInvitation)
37 | router.GET("/friend/invitation", baseApi.GetFriendInvitationList)
38 |
39 | router.GET("/account/invitation/list", baseApi.GetAccountInvitationList)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/model/product/productTransactionCategoryMappingModel.go:
--------------------------------------------------------------------------------
1 | package productModel
2 |
3 | import (
4 | "database/sql"
5 | "github.com/ZiRunHua/LeapLedger/global"
6 | accountModel "github.com/ZiRunHua/LeapLedger/model/account"
7 | commonModel "github.com/ZiRunHua/LeapLedger/model/common"
8 | "time"
9 | )
10 |
11 | type TransactionCategoryMapping struct {
12 | AccountId uint `gorm:"uniqueIndex:account_ptc_mapping,priority:1"`
13 | CategoryId uint `gorm:"uniqueIndex:category_ptc_mapping,priority:1"`
14 | PtcId uint `gorm:"uniqueIndex:account_ptc_mapping,priority:2;uniqueIndex:category_ptc_mapping,priority:2"`
15 | ProductKey string
16 | CreatedAt time.Time `gorm:"type:TIMESTAMP"`
17 | UpdatedAt time.Time `gorm:"type:TIMESTAMP"`
18 | commonModel.BaseModel
19 | }
20 |
21 | func (tcm *TransactionCategoryMapping) TableName() string {
22 | return "product_transaction_category_mapping"
23 | }
24 |
25 | func (tcm *TransactionCategoryMapping) IsEmpty() bool {
26 | return tcm == nil || tcm.AccountId == 0
27 | }
28 |
29 | func (tcm *TransactionCategoryMapping) GetPtcIdMapping(
30 | account *accountModel.Account, productKey Key,
31 | ) (result map[uint]TransactionCategoryMapping, err error) {
32 | db := global.GvaDb
33 | rows, err := db.Model(&TransactionCategoryMapping{}).Where(
34 | "account_id = ? AND product_key = ?", account.ID, productKey,
35 | ).Rows()
36 | defer func(rows *sql.Rows) {
37 | if err != nil {
38 | _ = rows.Close()
39 | } else {
40 | err = rows.Close()
41 | }
42 | }(rows)
43 | if err != nil {
44 | return
45 | }
46 | row, result := TransactionCategoryMapping{}, map[uint]TransactionCategoryMapping{}
47 | for rows.Next() {
48 | err = db.ScanRows(rows, &row)
49 | if err != nil {
50 | return
51 | }
52 | result[row.PtcId] = row
53 | }
54 | return
55 | }
56 |
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: '3.3'
2 | services:
3 | leap-ledger-server:
4 | container_name: leap-ledger-server
5 | image: xiaozirun/leap-ledger:${SERVICE_VERSION:-latest}
6 | build:
7 | context: .
8 | dockerfile: docker/Dockerfile.build
9 | ports:
10 | - "8080:8080"
11 | - "2345:2345"
12 | volumes:
13 | - ./log:/go/LeapLedger/log
14 | - ./docs:/go/LeapLedger/docs
15 | networks:
16 | - leap-ledger-network
17 | depends_on:
18 | - leap-ledger-mysql
19 | - leap-ledger-nats
20 | - leap-ledger-redis
21 | leap-ledger-mysql:
22 | container_name: leap-ledger-mysql
23 | image: mysql:8.0.30
24 | environment:
25 | MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
26 | MYSQL_DATABASE: leap_ledger
27 | ports:
28 | - "3306:3306"
29 | volumes:
30 | - ./docker/data/mysql:/var/lib/mysql
31 | - ./docker/mysql.cnf:/etc/mysql/conf.d/mysql.cnf
32 | command: >
33 | bash -c "
34 | chmod 644 /etc/mysql/conf.d/*.cnf
35 | && /entrypoint.sh mysqld
36 | "
37 | networks:
38 | - leap-ledger-network
39 | leap-ledger-redis:
40 | container_name: leap-ledger-redis
41 | privileged: true
42 | image: redis:latest
43 | ports:
44 | - "6379:6379"
45 | volumes:
46 | - ./docker/redis.conf:/redis.conf
47 | - ./docker/data/redis:/data
48 | networks:
49 | - leap-ledger-network
50 | command: ["redis-server", "/redis.conf"]
51 | leap-ledger-nats:
52 | container_name: leap-ledger-nats
53 | image: nats:latest
54 | command: -js -sd /data -http_port 8222
55 | ports:
56 | - "4222:4222"
57 | - "8222:8222"
58 | volumes:
59 | - ./docker/data/nats:/data
60 | networks:
61 | - leap-ledger-network
62 | networks:
63 | leap-ledger-network:
--------------------------------------------------------------------------------
/model/account/accountMappingModel.go:
--------------------------------------------------------------------------------
1 | package accountModel
2 |
3 | import (
4 | logModel "github.com/ZiRunHua/LeapLedger/model/log"
5 | "gorm.io/gorm"
6 | "time"
7 | )
8 |
9 | type Mapping struct {
10 | ID uint `gorm:"primarykey"`
11 | MainId uint `gorm:"not null;uniqueIndex:idx_mapping,priority:1"`
12 | RelatedId uint `gorm:"not null;uniqueIndex:idx_mapping,priority:2"`
13 | CreatedAt time.Time `gorm:"type:TIMESTAMP"`
14 | UpdatedAt time.Time `gorm:"type:TIMESTAMP"`
15 | }
16 |
17 | func (m *Mapping) TableName() string {
18 | return "account_mapping"
19 | }
20 |
21 | func (m *Mapping) GetMainAccount(db *gorm.DB) (result Account, err error) {
22 | err = db.First(&result, m.MainId).Error
23 | return
24 | }
25 |
26 | func (m *Mapping) GetRelatedAccount(db *gorm.DB) (result Account, err error) {
27 | err = db.First(&result, m.RelatedId).Error
28 | return
29 | }
30 |
31 | func (m *Mapping) GetLogDataModel() logModel.AccountLogDataRecordable {
32 | result := &logModel.AccountMappingLogData{
33 | MainId: m.MainId,
34 | RelatedId: m.RelatedId,
35 | }
36 | return result
37 | }
38 |
39 | type MappingCondition struct {
40 | mainId *uint
41 | relatedId *uint
42 | }
43 |
44 | func NewMappingCondition() *MappingCondition {
45 | return &MappingCondition{}
46 | }
47 |
48 | func (mc *MappingCondition) addConditionToQuery(db *gorm.DB) *gorm.DB {
49 | if mc.mainId != nil {
50 | db = db.Where("main_id = ?", mc.mainId)
51 | }
52 | if mc.relatedId != nil {
53 | db = db.Where("related_id = ?", mc.relatedId)
54 | }
55 | return db
56 | }
57 |
58 | func (mc *MappingCondition) WithMainId(mainId uint) *MappingCondition {
59 | mc.mainId = &mainId
60 | return mc
61 | }
62 |
63 | func (mc *MappingCondition) WithRelatedId(relatedId uint) *MappingCondition {
64 | mc.relatedId = &relatedId
65 | return mc
66 | }
67 |
--------------------------------------------------------------------------------
/model/user/transactionShareConfigModel.go:
--------------------------------------------------------------------------------
1 | package userModel
2 |
3 | import (
4 | "github.com/ZiRunHua/LeapLedger/global"
5 | "gorm.io/gorm"
6 | "time"
7 | )
8 |
9 | type TransactionShareConfig struct {
10 | ID uint `gorm:"primarykey"`
11 | UserId uint `gorm:"comment:'用户ID';unique"`
12 | DisplayFlags Flag `gorm:"comment:'展示字段标志'"`
13 | CreatedAt time.Time `gorm:"type:TIMESTAMP"`
14 | UpdatedAt time.Time `gorm:"type:TIMESTAMP"`
15 | DeletedAt gorm.DeletedAt `gorm:"index;type:TIMESTAMP"`
16 | }
17 |
18 | type Flag uint
19 |
20 | const (
21 | FLAG_AMOUNT Flag = 1 << iota
22 | FLAG_CATEGORY
23 | FLAG_TRADE_TIME
24 | FLAG_ACCOUNT
25 | FLAG_CREATE_TIME
26 | FLAG_UPDATE_TIME
27 | FLAG_REMARK
28 | )
29 | const DISPLAY_FLAGS_DEFAULT = FLAG_AMOUNT + FLAG_CATEGORY + FLAG_TRADE_TIME + FLAG_ACCOUNT + FLAG_REMARK
30 |
31 | func (u *TransactionShareConfig) TableName() string {
32 | return "user_transaction_share_config"
33 | }
34 |
35 | func (u *TransactionShareConfig) SelectByUserId(userId uint) error {
36 | u.UserId = userId
37 | u.DisplayFlags = DISPLAY_FLAGS_DEFAULT
38 | return global.GvaDb.Where("user_id = ?", userId).FirstOrCreate(&u).Error
39 | }
40 |
41 | func (u *TransactionShareConfig) OpenDisplayFlag(flag Flag, db *gorm.DB) error {
42 | where := db.Where("user_id = ?", u.UserId)
43 | return where.Model(&u).Update("display_flags", gorm.Expr("display_flags | ?", flag)).Error
44 | }
45 |
46 | func (u *TransactionShareConfig) ClosedDisplayFlag(flag Flag, db *gorm.DB) error {
47 | where := db.Where("user_id = ? AND display_flags & ? >0", u.UserId, flag)
48 | return where.Model(&u).Update("display_flags", gorm.Expr("display_flags ^ ?", flag)).Error
49 | }
50 |
51 | func (u *TransactionShareConfig) GetFlagStatus(flag Flag) bool {
52 | return u.DisplayFlags&flag > 0
53 | }
54 |
--------------------------------------------------------------------------------
/script/transaction/statistic/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/ZiRunHua/LeapLedger/global"
5 | "github.com/ZiRunHua/LeapLedger/global/constant"
6 | commonModel "github.com/ZiRunHua/LeapLedger/model/common"
7 |
8 | _ "github.com/ZiRunHua/LeapLedger/initialize"
9 | transactionModel "github.com/ZiRunHua/LeapLedger/model/transaction"
10 | "gorm.io/gorm"
11 | )
12 |
13 | func main() {
14 | correctStatistic()
15 | }
16 | func correctStatistic() {
17 | tables := []commonModel.Model{
18 | &transactionModel.ExpenseCategoryStatistic{}, &transactionModel.ExpenseAccountStatistic{},
19 | &transactionModel.ExpenseAccountUserStatistic{}, &transactionModel.IncomeCategoryStatistic{},
20 | &transactionModel.IncomeAccountStatistic{},
21 | &transactionModel.IncomeAccountUserStatistic{},
22 | }
23 | for _, table := range tables {
24 | global.GvaDb.Delete(table, "count >= ?", 0)
25 | }
26 | err := global.GvaDb.Transaction(
27 | func(tx *gorm.DB) error {
28 | return transAccumulate(tx)
29 | },
30 | )
31 | if err != nil {
32 | panic(err.Error())
33 | }
34 | }
35 |
36 | func transAccumulate(tx *gorm.DB) error {
37 | var list []transactionModel.Transaction
38 | err := tx.Model(&transactionModel.Transaction{}).Find(&list).Error
39 | if err != nil {
40 | return err
41 | }
42 |
43 | for _, trans := range list {
44 | if trans.IncomeExpense == constant.Expense {
45 | err = transactionModel.ExpenseAccumulate(
46 | trans.TradeTime, trans.AccountId, trans.UserId, trans.CategoryId, trans.Amount, 1, tx,
47 | )
48 | if err != nil {
49 | return err
50 | }
51 | } else {
52 | err = transactionModel.IncomeAccumulate(
53 | trans.TradeTime, trans.AccountId, trans.UserId, trans.CategoryId, trans.Amount, 1, tx,
54 | )
55 | if err != nil {
56 | return err
57 | }
58 | }
59 | }
60 | return nil
61 | }
62 |
--------------------------------------------------------------------------------
/global/db/db_test.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "context"
5 | "github.com/ZiRunHua/LeapLedger/global/cus"
6 | "gorm.io/gorm"
7 | "gorm.io/gorm/logger"
8 | "testing"
9 | )
10 |
11 | func init() {
12 | Db = Db.Session(&gorm.Session{Logger: Db.Logger.LogMode(logger.Silent)})
13 | }
14 |
15 | func callback() {
16 | sum := 0
17 | for i := 0; i < 10; i++ {
18 | sum += i * i
19 | }
20 | }
21 |
22 | func Benchmark_Gorm_ThreeTransaction(b *testing.B) {
23 | for i := 0; i < b.N; i++ {
24 | _ = Db.Transaction(
25 | func(tx *gorm.DB) error {
26 | return tx.Transaction(
27 | func(tx *gorm.DB) error {
28 | return tx.Transaction(
29 | func(tx *gorm.DB) error {
30 | callback()
31 | callback()
32 | return nil
33 | },
34 | )
35 | },
36 | )
37 | },
38 | )
39 | }
40 | }
41 |
42 | func Benchmark_Cus_ThreeTransaction(b *testing.B) {
43 | for i := 0; i < b.N; i++ {
44 | _ = Transaction(
45 | context.Background(), func(ctx *cus.TxContext) error {
46 | return Transaction(
47 | ctx, func(ctx *cus.TxContext) error {
48 | return Transaction(
49 | ctx, func(ctx *cus.TxContext) error {
50 | _ = AddCommitCallback(ctx, callback, callback)
51 | return nil
52 | },
53 | )
54 | },
55 | )
56 | },
57 | )
58 | }
59 | }
60 | func Benchmark_Gorm_Transaction(b *testing.B) {
61 | for i := 0; i < b.N; i++ {
62 | _ = Db.Transaction(
63 | func(tx *gorm.DB) error {
64 | callback()
65 | callback()
66 | return nil
67 | },
68 | )
69 | }
70 | }
71 |
72 | func Benchmark_Cus_Transaction(b *testing.B) {
73 | for i := 0; i < b.N; i++ {
74 | _ = Transaction(
75 | context.Background(), func(ctx *cus.TxContext) error {
76 | _ = AddCommitCallback(ctx, callback, callback)
77 | return nil
78 | },
79 | )
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/service/product/productBillimportService.go:
--------------------------------------------------------------------------------
1 | package productService
2 |
3 | import (
4 | "context"
5 | "io"
6 |
7 | "github.com/ZiRunHua/LeapLedger/global/db"
8 | accountModel "github.com/ZiRunHua/LeapLedger/model/account"
9 | productModel "github.com/ZiRunHua/LeapLedger/model/product"
10 | transactionModel "github.com/ZiRunHua/LeapLedger/model/transaction"
11 | "github.com/ZiRunHua/LeapLedger/service/product/bill"
12 | "github.com/ZiRunHua/LeapLedger/util/fileTool"
13 | )
14 |
15 | type BillFile struct {
16 | fileName string
17 | fileReader io.Reader
18 | }
19 |
20 | func (bf *BillFile) GetRowReader() (func(yield func([]string) bool), error) {
21 | return fileTool.NewRowReader(
22 | bf.fileReader,
23 | fileTool.GetFileSuffix(bf.fileName),
24 | )
25 | }
26 |
27 | func (proService *Product) GetNewBillFile(fileName string, fileReader io.Reader) BillFile {
28 | return BillFile{fileName: fileName, fileReader: fileReader}
29 | }
30 |
31 | func (proService *Product) ProcessesBill(
32 | file BillFile, product productModel.Product, accountUser accountModel.User,
33 | handler func(transInfo transactionModel.Info, err error) error, ctx context.Context,
34 | ) error {
35 | rowReader, err := file.GetRowReader()
36 | if err != nil {
37 | return err
38 | }
39 | account, err := accountModel.NewDao(db.Get(ctx)).SelectById(accountUser.AccountId)
40 | transReader, err := bill.NewReader(account, product, ctx)
41 | if err != nil {
42 | return err
43 | }
44 |
45 | var (
46 | transInfo transactionModel.Info
47 | ignore bool
48 | )
49 | for row := range rowReader {
50 | transInfo, ignore, err = transReader.ReaderTrans(row, ctx)
51 | if ignore {
52 | continue
53 | }
54 | transInfo.AccountId, transInfo.UserId = accountUser.AccountId, accountUser.UserId
55 | err = handler(transInfo, err)
56 | if err != nil {
57 | return err
58 | }
59 | }
60 | return nil
61 | }
62 |
--------------------------------------------------------------------------------
/test/util/build.go:
--------------------------------------------------------------------------------
1 | package tUtil
2 |
3 | import (
4 | "context"
5 | "time"
6 |
7 | "github.com/ZiRunHua/LeapLedger/global/constant"
8 | "github.com/ZiRunHua/LeapLedger/global/cus"
9 | "github.com/ZiRunHua/LeapLedger/global/db"
10 | accountModel "github.com/ZiRunHua/LeapLedger/model/account"
11 | categoryModel "github.com/ZiRunHua/LeapLedger/model/category"
12 | transactionModel "github.com/ZiRunHua/LeapLedger/model/transaction"
13 | userModel "github.com/ZiRunHua/LeapLedger/model/user"
14 | "github.com/ZiRunHua/LeapLedger/util/rand"
15 | )
16 |
17 | type Build struct {
18 | }
19 |
20 | var build = &Build{}
21 |
22 | func (n *Build) User() (user userModel.User, err error) {
23 | user, err = userService.CreateTourist(context.TODO())
24 | if err != nil {
25 | return
26 | }
27 | tour, err := userModel.NewDao().SelectTour(user.ID)
28 | if err != nil {
29 | return
30 | }
31 | err = db.Transaction(
32 | context.TODO(), func(ctx *cus.TxContext) error {
33 | return tour.Use(ctx.GetDb())
34 | },
35 | )
36 | return user, err
37 | }
38 |
39 | func (n *Build) Account(user userModel.User, t accountModel.Type) (
40 | account accountModel.Account, aUser accountModel.User,
41 | err error,
42 | ) {
43 | accountTmpl := templateService.NewAccountTmpl()
44 | err = accountTmpl.ReadFromJson(constant.ExampleAccountJsonPath)
45 | if err != nil {
46 | return
47 | }
48 | accountTmpl.Type = t
49 | return templateService.CreateAccountByTemplate(accountTmpl, user, context.TODO())
50 | }
51 |
52 | func (n *Build) TransInfo(user userModel.User, category categoryModel.Category) transactionModel.Info {
53 | return transactionModel.Info{
54 | UserId: user.ID,
55 | AccountId: category.AccountId,
56 | CategoryId: category.ID,
57 | IncomeExpense: category.IncomeExpense,
58 | Amount: rand.Int(1000),
59 | Remark: "test",
60 | TradeTime: time.Now(),
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/service/user/userFriendService.go:
--------------------------------------------------------------------------------
1 | package userService
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "github.com/ZiRunHua/LeapLedger/global/cus"
7 | "github.com/ZiRunHua/LeapLedger/global/db"
8 | userModel "github.com/ZiRunHua/LeapLedger/model/user"
9 | "gorm.io/gorm"
10 | )
11 |
12 | type Friend struct{}
13 |
14 | func (f *Friend) CreateInvitation(
15 | inviter userModel.User, invitee userModel.User, ctx context.Context,
16 | ) (invitation userModel.FriendInvitation, err error) {
17 | return invitation, db.Transaction(
18 | ctx, func(ctx *cus.TxContext) (err error) {
19 | tx := ctx.GetDb()
20 | dao := userModel.NewDao(tx)
21 | invitation, err = dao.CreateFriendInvitation(inviter.ID, invitee.ID)
22 | if false == errors.Is(err, gorm.ErrDuplicatedKey) {
23 | return
24 | }
25 | // 处理重复键
26 | invitation, err = dao.SelectFriendInvitation(inviter.ID, invitee.ID, true)
27 | if err != nil {
28 | return
29 | }
30 | var isRealFriend bool
31 | isRealFriend, err = dao.IsRealFriend(inviter.ID, invitee.ID)
32 | if isRealFriend || err != nil {
33 | return
34 | }
35 | if invitation.Status == userModel.InvitationStatsOfWaiting {
36 | return
37 | }
38 | return invitation.UpdateStatus(userModel.InvitationStatsOfWaiting, tx)
39 | },
40 | )
41 | }
42 |
43 | func (f *Friend) AcceptInvitation(Invitation *userModel.FriendInvitation, ctx context.Context) (
44 | inviterFriend userModel.Friend, inviteeFriend userModel.Friend, err error,
45 | ) {
46 | err = db.Transaction(
47 | ctx, func(ctx *cus.TxContext) error {
48 | inviterFriend, inviteeFriend, err = Invitation.Accept(ctx.GetDb())
49 | return err
50 | },
51 | )
52 | return
53 | }
54 |
55 | func (f *Friend) RefuseInvitation(Invitation *userModel.FriendInvitation, ctx context.Context) error {
56 | return db.Transaction(
57 | ctx, func(ctx *cus.TxContext) error {
58 | return Invitation.Refuse(ctx.GetDb())
59 | },
60 | )
61 | }
62 |
--------------------------------------------------------------------------------
/api/request/account.go:
--------------------------------------------------------------------------------
1 | package request
2 |
3 | import accountModel "github.com/ZiRunHua/LeapLedger/model/account"
4 |
5 | // AccountCreateOne 账本新建
6 | type AccountCreateOne struct {
7 | Name string `binding:"required"`
8 | Icon string `binding:"required"`
9 | Location string `binding:"required"`
10 | Type accountModel.Type `binding:"required"`
11 | }
12 |
13 | // AccountUpdateOne 账本修改
14 | type AccountUpdateOne struct {
15 | Name *string
16 | Icon *string
17 | Type accountModel.Type `binding:"required"`
18 | }
19 |
20 | // AccountTransCategoryInit 账本交易类型初始话化
21 | type AccountTransCategoryInit struct {
22 | TemplateId uint
23 | }
24 |
25 | // AccountMapping 账本关联
26 | type AccountMapping struct {
27 | AccountId uint
28 | }
29 |
30 | // UpdateAccountMapping 账本关联
31 | type UpdateAccountMapping struct {
32 | RelatedAccountId uint
33 | }
34 |
35 | // AccountCreateOneUserInvitation 账本邀请建立
36 | type AccountCreateOneUserInvitation struct {
37 | Invitee uint `binding:"required"`
38 | Role *accountModel.UserRole `binding:"omitempty"`
39 | }
40 |
41 | // AccountGetUserInvitationList 账本邀请列表
42 | type AccountGetUserInvitationList struct {
43 | AccountId uint `binding:"required"`
44 | Invitee *uint `binding:"omitempty"`
45 | Role *accountModel.UserRole `binding:"omitempty"`
46 | PageData
47 | }
48 |
49 | // AccountGetUserInfo 账本用户信息获取
50 | type AccountGetUserInfo struct {
51 | Types []InfoType
52 | }
53 |
54 | type AccountInfo struct {
55 | Types *[]InfoType `binding:"omitempty"`
56 | }
57 |
58 | type AccountUpdateUser struct {
59 | Role accountModel.UserRole `binding:"required"`
60 | }
61 |
62 | func (a *AccountUpdateUser) GetUpdateData() accountModel.UserUpdateData {
63 | return accountModel.UserUpdateData{
64 | Permission: a.Role.ToUserPermission(),
65 | }
66 | }
67 |
68 | type AccountUserConfigFlagUpdate struct {
69 | Status bool
70 | }
71 |
--------------------------------------------------------------------------------
/service/transaction/task.go:
--------------------------------------------------------------------------------
1 | package transactionService
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "strings"
7 | "time"
8 |
9 | "github.com/ZiRunHua/LeapLedger/global/cron"
10 | "github.com/ZiRunHua/LeapLedger/global/nats"
11 | )
12 |
13 | type _task struct{}
14 |
15 | func init() {
16 | // update statistic
17 | nats.SubscribeTaskWithPayload(nats.TaskStatisticUpdate, GroupApp.Transaction.updateStatistic)
18 | // sync trans
19 | nats.SubscribeTaskWithPayloadAndProcessInTransaction(
20 | nats.TaskTransactionSync, GroupApp.Transaction.SyncToMappingAccount,
21 | )
22 | // timing
23 | var moments []string
24 | for i := 0; i < 24; i++ {
25 | moments = append(moments, fmt.Sprintf("%02d:00", i))
26 | }
27 | _, err := cron.Scheduler.Every(1).Day().At(strings.Join(moments, ";")).Do(
28 | cron.PublishTaskWithMakePayload(
29 | nats.TaskTransactionTimingTaskAssign, func() (taskTransactionTimingTaskAssign, error) {
30 | now := time.Now()
31 | return taskTransactionTimingTaskAssign{
32 | Deadline: time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, time.Local),
33 | TaskSize: 50,
34 | }, nil
35 | },
36 | ),
37 | )
38 | if err != nil {
39 | panic(err)
40 | }
41 |
42 | nats.SubscribeTaskWithPayloadAndProcessInTransaction(
43 | nats.TaskTransactionTimingTaskAssign, func(assign taskTransactionTimingTaskAssign, ctx context.Context) error {
44 | return GroupApp.Timing.Exec.GenerateAndPublishTasks(assign.Deadline, assign.TaskSize, ctx)
45 | },
46 | )
47 |
48 | nats.SubscribeTaskWithPayloadAndProcessInTransaction(
49 | nats.TaskTransactionTimingExec, func(execTask transactionTimingExecTask, ctx context.Context) error {
50 | return GroupApp.Timing.Exec.ProcessWaitExecByStartId(execTask.StartId, execTask.Size, ctx)
51 | },
52 | )
53 | }
54 |
55 | type taskTransactionTimingTaskAssign struct {
56 | Deadline time.Time
57 | TaskSize int
58 | }
59 |
60 | type transactionTimingExecTask struct {
61 | StartId uint
62 | Size int
63 | }
64 |
--------------------------------------------------------------------------------
/router/v1/account.go:
--------------------------------------------------------------------------------
1 | package v1
2 |
3 | import (
4 | "github.com/ZiRunHua/LeapLedger/router/group"
5 | )
6 |
7 | func init() {
8 | // base path: /account/{accountId}
9 | router := group.Private.Group("account")
10 | baseApi := apiApp.AccountApi
11 | {
12 | router.POST("", baseApi.CreateOne)
13 | group.AccountAdministrator.PUT("", baseApi.Update)
14 | group.AccountAdministrator.DELETE("", baseApi.Delete)
15 | router.GET("/list", baseApi.GetList)
16 | router.GET("/list/:type", baseApi.GetListByType)
17 | group.Account.GET("", baseApi.GetOne)
18 | group.Account.GET("/info/:type", baseApi.GetInfo)
19 | group.Account.GET("/info", baseApi.GetInfo)
20 | // 模板
21 | router.GET("/template/list", baseApi.GetAccountTemplateList)
22 | router.POST("/form/template/:id", baseApi.CreateOneByTemplate)
23 | group.AccountCreator.POST("/transaction/category/init", baseApi.InitCategoryByTemplate)
24 | // 共享
25 | group.AccountCreator.PUT("/user/:id", baseApi.UpdateUser)
26 | group.Account.GET("/user/list", baseApi.GetUserList)
27 | group.Account.GET("/user/:id/info", baseApi.GetUserInfo)
28 | router.GET("/user/invitation/list", baseApi.GetUserInvitationList)
29 | group.AccountOwnEditor.POST("/user/invitation", baseApi.CreateAccountUserInvitation)
30 | router.PUT("/user/invitation/:id/accept", baseApi.AcceptAccountUserInvitation)
31 | router.PUT("/user/invitation/:id/refuse", baseApi.RefuseAccountUserInvitation)
32 | // 账本关联
33 | group.AccountOwnEditor.GET("/mapping", baseApi.GetAccountMapping)
34 | group.AccountOwnEditor.DELETE("/mapping/:id", baseApi.DeleteAccountMapping)
35 | group.Account.GET("/mapping/list", baseApi.GetAccountMappingList)
36 | group.AccountOwnEditor.POST("/mapping", baseApi.CreateAccountMapping)
37 | group.AccountOwnEditor.PUT("/mapping/:id", baseApi.UpdateAccountMapping)
38 | // 账本用户配置
39 | group.AccountOwnEditor.GET("/user/config", baseApi.GetUserConfig)
40 | group.AccountOwnEditor.PUT("/user/config/flag/:flag", baseApi.UpdateUserConfigFlag)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/global/cus/context.go:
--------------------------------------------------------------------------------
1 | package cus
2 |
3 | import (
4 | "context"
5 | "gorm.io/gorm"
6 | )
7 |
8 | func WithDb(parent context.Context, db *gorm.DB) *DbContext {
9 | return &DbContext{Context: parent, db: db}
10 | }
11 |
12 | type DbContext struct {
13 | context.Context
14 | db *gorm.DB
15 | }
16 |
17 | func (dc *DbContext) Value(key any) any {
18 | if key == Db {
19 | return dc.db
20 | }
21 | return dc.Context.Value(key)
22 | }
23 | func (dc *DbContext) GetDb() *gorm.DB {
24 | return dc.db
25 | }
26 |
27 | func WithTx(parent context.Context, tx *gorm.DB) *TxContext {
28 | return &TxContext{Context: parent, tx: tx}
29 | }
30 |
31 | type TxContext struct {
32 | context.Context
33 | tx *gorm.DB
34 | }
35 |
36 | func (tc *TxContext) Value(key any) any {
37 | if key == Db || key == Tx {
38 | return tc.tx
39 | }
40 | return tc.Context.Value(key)
41 | }
42 |
43 | func (tc *TxContext) GetDb() *gorm.DB {
44 | return tc.tx
45 | }
46 |
47 | func WithTxCommitContext(parent context.Context) *TxCommitContext {
48 | return &TxCommitContext{Context: parent}
49 | }
50 |
51 | type TxCommitCallback func()
52 |
53 | type TxCommitContext struct {
54 | context.Context
55 | callbacks []TxCommitCallback
56 | }
57 |
58 | func (t *TxCommitContext) Value(key any) any {
59 | if key == TxCommit {
60 | return t
61 | }
62 | return t.Context.Value(key)
63 | }
64 |
65 | func (t *TxCommitContext) AddCallback(callback ...TxCommitCallback) error {
66 | t.callbacks = append(t.callbacks, callback...)
67 | return nil
68 | }
69 |
70 | func (t *TxCommitContext) ExecCallback() {
71 | if len(t.callbacks) == 0 {
72 | return
73 | }
74 | parent := t.Context.Value(TxCommit)
75 | if parent != nil {
76 | // The parent transaction decides to commit last, so the callback is handed over to the parent transaction
77 | err := parent.(*TxCommitContext).AddCallback(t.callbacks...)
78 | if err != nil {
79 | panic(err)
80 | }
81 | return
82 | }
83 | for _, callback := range t.callbacks {
84 | callback()
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/initialize/mysql.go:
--------------------------------------------------------------------------------
1 | package initialize
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/ZiRunHua/LeapLedger/global/constant"
7 |
8 | _ "github.com/go-sql-driver/mysql"
9 | "gorm.io/driver/mysql"
10 | "gorm.io/gorm"
11 | "gorm.io/gorm/logger"
12 | "gorm.io/gorm/schema"
13 | )
14 |
15 | type _mysql struct {
16 | Path string `yaml:"Path"`
17 | Port string `yaml:"Port"`
18 | Config string `yaml:"Config"`
19 | DbName string `yaml:"DbName"`
20 | Username string `yaml:"Username"`
21 | Password string `yaml:"Password"`
22 | }
23 |
24 | func (m *_mysql) dsn() string {
25 | return m.Username + ":" + m.Password + "@tcp(" + m.Path + ":" + m.Port + ")/" + m.DbName + "?" + m.Config
26 | }
27 |
28 | func (m *_mysql) do() error {
29 | var err error
30 | mysqlConfig := mysql.Config{
31 | DSN: m.dsn(), // DSN data source name
32 | DefaultStringSize: 191, // string 类型字段的默认长度
33 | SkipInitializeWithVersion: false, //
34 | }
35 | var db *gorm.DB
36 | db, err = reconnection[*gorm.DB](
37 | func() (*gorm.DB, error) {
38 | return gorm.Open(mysql.New(mysqlConfig), m.gormConfig())
39 | }, 10,
40 | )
41 |
42 | if err != nil {
43 | return err
44 | }
45 | sqlDb, _ := db.DB()
46 | sqlDb.SetMaxIdleConns(50)
47 | sqlDb.SetMaxOpenConns(50)
48 | sqlDb.SetConnMaxLifetime(5 * time.Minute)
49 | db.InstanceSet("gorm:table_options", "ENGINE=InnoDB")
50 | Db = db
51 | return nil
52 | }
53 |
54 | func (m *_mysql) gormConfig() *gorm.Config {
55 | config := &gorm.Config{
56 | SkipDefaultTransaction: true,
57 | NamingStrategy: schema.NamingStrategy{
58 | SingularTable: true,
59 | },
60 | DisableForeignKeyConstraintWhenMigrating: true,
61 | TranslateError: true,
62 | }
63 | switch Config.Mode {
64 | case constant.Debug:
65 | config.Logger = logger.Default.LogMode(logger.Info)
66 | case constant.Production:
67 | config.Logger = logger.Default.LogMode(logger.Silent)
68 | default:
69 | panic("error Mode")
70 | }
71 | return config
72 | }
73 |
--------------------------------------------------------------------------------
/global/nats/manager/enter.go:
--------------------------------------------------------------------------------
1 | package manager
2 |
3 | import (
4 | "path/filepath"
5 |
6 | "github.com/ZiRunHua/LeapLedger/global"
7 | "github.com/ZiRunHua/LeapLedger/global/constant"
8 | "github.com/ZiRunHua/LeapLedger/initialize"
9 | "github.com/nats-io/nats.go/jetstream"
10 | "go.uber.org/zap"
11 | )
12 |
13 | var (
14 | natsConn = initialize.Nats
15 | js jetstream.JetStream
16 | )
17 |
18 | var (
19 | taskManage *taskManager
20 | eventManage *eventManager
21 | dlqManage *dlqManager
22 |
23 | TaskManage TaskManager
24 | EventManage EventManager
25 | DlqManage DlqManager
26 | )
27 |
28 | var natsLogPath = filepath.Join(constant.LogPath, "nats")
29 |
30 | var (
31 | taskLogger *zap.Logger
32 | eventLogger *zap.Logger
33 | dlqLogger *zap.Logger
34 | )
35 |
36 | func init() {
37 | var err error
38 | js, err = jetstream.New(natsConn)
39 | if err != nil {
40 | panic(err)
41 | }
42 | taskLogger, err = global.Config.Logger.New(natsTaskLogPath)
43 | if err != nil {
44 | panic(err)
45 | }
46 | eventLogger, err = global.Config.Logger.New(natsEventLogPath)
47 | if err != nil {
48 | panic(err)
49 | }
50 | dlqLogger, err = global.Config.Logger.New(dlqLogPath)
51 | if err != nil {
52 | panic(err)
53 | }
54 |
55 | if taskManage != nil {
56 | taskManage = &taskManager{taskMsgHandler: taskManage.taskMsgHandler}
57 | } else {
58 | taskManage = &taskManager{}
59 | }
60 | TaskManage = taskManage
61 | err = taskManage.init(js, taskLogger)
62 | if err != nil {
63 | panic(err)
64 | }
65 |
66 | if eventManage != nil {
67 | eventManage = &eventManager{eventMsgHandler: eventManage.eventMsgHandler}
68 | } else {
69 | eventManage = &eventManager{}
70 | }
71 | EventManage = eventManage
72 | err = eventManage.init(js, taskManage, eventLogger)
73 | if err != nil {
74 | panic(err)
75 | }
76 |
77 | dlqManage = &dlqManager{}
78 | DlqManage = dlqManage
79 | err = dlqManage.init(js, []dlqRegisterStream{taskManage, eventManage}, dlqLogger)
80 | if err != nil {
81 | panic(err)
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/api/response/common.go:
--------------------------------------------------------------------------------
1 | package response
2 |
3 | import (
4 | "github.com/ZiRunHua/LeapLedger/global/db"
5 | accountModel "github.com/ZiRunHua/LeapLedger/model/account"
6 | userModel "github.com/ZiRunHua/LeapLedger/model/user"
7 | "github.com/ZiRunHua/LeapLedger/util/dataTool"
8 | "time"
9 | )
10 |
11 | type CommonCaptcha struct {
12 | CaptchaId string
13 | PicBase64 string
14 | CaptchaLength int
15 | OpenCaptcha bool
16 | }
17 |
18 | type Id struct {
19 | Id uint
20 | }
21 |
22 | type CreateResponse struct {
23 | Id uint
24 | CreatedAt time.Time
25 | UpdatedAt time.Time
26 | }
27 |
28 | type Token struct {
29 | Token string
30 | TokenExpirationTime time.Time
31 | }
32 |
33 | type TwoLevelTree struct {
34 | Tree []Father
35 | }
36 |
37 | type Father struct {
38 | NameId
39 | Children []NameId
40 | }
41 |
42 | type NameId struct {
43 | Id uint
44 | Name string
45 | }
46 |
47 | type NameValue struct {
48 | Name string
49 | Value int
50 | }
51 |
52 | type PageData struct {
53 | page int
54 | limit int
55 | count int
56 | }
57 |
58 | type ExpirationTime struct {
59 | ExpirationTime int
60 | }
61 |
62 | type List[T any] struct {
63 | List []T
64 | }
65 |
66 | func getUsernameMap(ids []uint) (map[uint]string, error) {
67 | var nameList dataTool.Slice[uint, struct {
68 | ID uint
69 | Username string
70 | }]
71 | err := db.Db.Model(&userModel.User{}).Where("id IN (?)", ids).Find(&nameList).Error
72 | if err != nil {
73 | return nil, err
74 | }
75 | result := make(map[uint]string)
76 | for _, s := range nameList {
77 | result[s.ID] = s.Username
78 | }
79 | return result, nil
80 | }
81 |
82 | func getAccountNameMap(ids []uint) (map[uint]string, error) {
83 | var nameList dataTool.Slice[uint, struct {
84 | ID uint
85 | Name string
86 | }]
87 | err := db.Db.Model(&accountModel.Account{}).Where("id IN (?)", ids).Find(&nameList).Error
88 | if err != nil {
89 | return nil, err
90 | }
91 | result := make(map[uint]string)
92 | for _, s := range nameList {
93 | result[s.ID] = s.Name
94 | }
95 | return result, nil
96 | }
97 |
--------------------------------------------------------------------------------
/api/request/common.go:
--------------------------------------------------------------------------------
1 | package request
2 |
3 | import (
4 | "github.com/ZiRunHua/LeapLedger/global"
5 | "github.com/ZiRunHua/LeapLedger/global/constant"
6 | "github.com/pkg/errors"
7 | "time"
8 | )
9 |
10 | type IncomeExpense struct {
11 | IncomeExpense constant.IncomeExpense `json:"Income_expense"`
12 | }
13 | type Name struct {
14 | Name string
15 | }
16 | type Id struct {
17 | Id uint
18 | }
19 |
20 | type PageData struct {
21 | Offset int `binding:"gte=0"`
22 | Limit int `binding:"gt=0"`
23 | }
24 |
25 | type PicCaptcha struct {
26 | Captcha string `binding:"required"`
27 | CaptchaId string `binding:"required"`
28 | }
29 |
30 | type CommonSendEmailCaptcha struct {
31 | Email string `binding:"required,email"`
32 | Type constant.UserAction `binding:"required,oneof=register forgetPassword"`
33 | PicCaptcha
34 | }
35 |
36 | type TimeFrame struct {
37 | StartTime time.Time
38 | EndTime time.Time
39 | }
40 |
41 | func (t *TimeFrame) CheckTimeFrame() error {
42 | if t.EndTime.Before(t.StartTime) {
43 | return errors.New("时间范围错误")
44 | }
45 | if t.StartTime.AddDate(2, 2, 2).Before(t.EndTime) {
46 | return global.ErrTimeFrameIsTooLong
47 | }
48 | return nil
49 | }
50 |
51 | // 格式化日时间 将StartTime置为当日第一秒 endTime置为当日最后一秒
52 | func (t *TimeFrame) FormatDayTime() (startTime time.Time, endTime time.Time) {
53 | year, month, day := t.StartTime.Date()
54 | startTime = time.Date(year, month, day, 0, 0, 0, 0, t.StartTime.Location())
55 | year, month, day = t.EndTime.Date()
56 | endTime = time.Date(year, month, day, 23, 59, 59, 0, t.EndTime.Location())
57 | return
58 | }
59 |
60 | func (t *TimeFrame) SetLocal(l *time.Location) {
61 | t.StartTime, t.EndTime = t.StartTime.In(l), t.EndTime.In(l)
62 | }
63 |
64 | func (t *TimeFrame) ToUTC() {
65 | t.StartTime, t.EndTime = t.StartTime.UTC(), t.EndTime.UTC()
66 | }
67 |
68 | // 信息类型
69 | type InfoType string
70 |
71 | // 今日交易统计
72 | var TodayTransTotal InfoType = "todayTransTotal"
73 |
74 | // 本月交易统计
75 | var CurrentMonthTransTotal InfoType = "currentMonthTransTotal"
76 |
77 | // 最近交易数据
78 | var RecentTrans InfoType = "recentTrans"
79 |
80 | type AccountId struct {
81 | AccountId uint
82 | }
83 |
--------------------------------------------------------------------------------
/util/dataTool/slice.go:
--------------------------------------------------------------------------------
1 | package dataTool
2 |
3 | // Slice的拓展方法
4 | type Slice[K comparable, V any] []V
5 |
6 | // ToMap slice转map
7 | /**
8 | 示例:
9 | var userList dataTools.Slice[uint, User]
10 | userList = []User{
11 | {ID: 1, Name: "test1", Password: "test2"},
12 | {ID: 2, Name: "test2", Password: "test3"},
13 | {ID: 3, Name: "test3", Password: "test3"},
14 | } // 或者 global.GvaDb.Limit(5).Find(&userList)
15 | userMap := userList.ToMap(func(user User) uint {
16 | return user.ID
17 | })
18 | fmt.Println(userMap) //将得到 map[1:{1 test1 test2} 2:{2 test2 test3}]
19 | */
20 | func (s Slice[K, V]) ToMap(getKey func(V) K) (result map[K]V) {
21 | result = make(map[K]V)
22 | for _, v := range s {
23 | result[getKey(v)] = v
24 | }
25 | return
26 | }
27 |
28 | // ExtractValues 提取slice中的值
29 | /**
30 | 示例:
31 | var userList dataTools.Slice[uint, User]
32 | userList = []User{
33 | {ID: 1, Name: "test1", Password: "test2"},
34 | {ID: 2, Name: "test2", Password: "test3"},
35 | } // 或者 global.GvaDb.Limit(2).Find(&userList)
36 | userIds := userList.ExtractValues(func(user User) uint {
37 | return user.ID
38 | })
39 | fmt.Println(userIds) //将得到 [1 2]
40 | */
41 | func (s Slice[K, V]) ExtractValues(getVal func(V) K) (result []K) {
42 | result = make([]K, len(s), len(s))
43 | for i, v := range s {
44 | result[i] = getVal(v)
45 | }
46 | return
47 | }
48 |
49 | func (s *Slice[K, V]) Reverse() {
50 | n := len(*s)
51 | for i := 0; i < n/2; i++ {
52 | (*s)[i], (*s)[n-1-i] = (*s)[n-1-i], (*s)[i]
53 | }
54 | }
55 |
56 | func (s Slice[K, V]) CopyReverse() Slice[K, V] {
57 | list := make(Slice[K, V], len(s), len(s))
58 | copy(list, s)
59 | list.Reverse()
60 | return list
61 | }
62 |
63 | func Reverse[V any](s []V) {
64 | for i := 0; i < len(s)/2; i++ {
65 | s[i], s[len(s)-1-i] = s[len(s)-1-i], s[i]
66 | }
67 | }
68 |
69 | func CopyReverse[V any](s []V) []V {
70 | list := make([]V, len(s), len(s))
71 | copy(list, s)
72 | Reverse(list)
73 | return list
74 | }
75 |
76 | func ToMap[Slice ~[]V, V any, K comparable](s Slice, getKey func(V) K) (result map[K]V) {
77 | result = make(map[K]V)
78 | for _, v := range s {
79 | result[getKey(v)] = v
80 | }
81 | return
82 | }
83 |
--------------------------------------------------------------------------------
/api/v1/common.go:
--------------------------------------------------------------------------------
1 | package v1
2 |
3 | import (
4 | "github.com/ZiRunHua/LeapLedger/api/request"
5 | "github.com/ZiRunHua/LeapLedger/api/response"
6 | "github.com/ZiRunHua/LeapLedger/global"
7 | "github.com/ZiRunHua/LeapLedger/global/nats"
8 | "github.com/gin-gonic/gin"
9 | "github.com/mojocn/base64Captcha"
10 | "github.com/pkg/errors"
11 | )
12 |
13 | type CommonApi struct {
14 | }
15 |
16 | var captchaStore = base64Captcha.DefaultMemStore
17 |
18 | // Captcha
19 | //
20 | // @Tags Common
21 | // @Produce json
22 | // @Success 200 {object} response.Data{Data=response.CommonCaptcha}
23 | // @Router /public/captcha [get]
24 | func (p *PublicApi) Captcha(c *gin.Context) {
25 | driver := base64Captcha.NewDriverDigit(
26 | global.Config.Captcha.ImgHeight, global.Config.Captcha.ImgWidth, global.Config.Captcha.KeyLong, 0.7,
27 | 80,
28 | )
29 | cp := base64Captcha.NewCaptcha(driver, captchaStore)
30 | id, b64s, _, err := cp.Generate()
31 | if err != nil {
32 | response.FailWithMessage("验证码获取失败", c)
33 | return
34 | }
35 | response.OkWithDetailed(
36 | response.CommonCaptcha{
37 | CaptchaId: id,
38 | PicBase64: b64s,
39 | CaptchaLength: global.Config.Captcha.KeyLong,
40 | }, "验证码获取成功", c,
41 | )
42 | }
43 |
44 | // SendEmailCaptcha
45 | //
46 | // @Tags Common
47 | // @Accept json
48 | // @Produce json
49 | // @Param body body request.CommonSendEmailCaptcha true "data"
50 | // @Success 200 {object} response.Data{Data=response.ExpirationTime}
51 | // @Router /public/captcha/email/send [post]
52 | func (p *PublicApi) SendEmailCaptcha(ctx *gin.Context) {
53 | var requestData request.CommonSendEmailCaptcha
54 | if err := ctx.ShouldBindJSON(&requestData); err != nil {
55 | response.FailToParameter(ctx, err)
56 | return
57 | }
58 |
59 | if false == captchaStore.Verify(requestData.CaptchaId, requestData.Captcha, true) {
60 | response.FailWithMessage("验证码错误", ctx)
61 | return
62 | }
63 |
64 | isSuccess := nats.PublishTaskWithPayload(
65 | nats.TaskSendCaptchaEmail, nats.PayloadSendCaptchaEmail{
66 | Email: requestData.Email, Action: requestData.Type,
67 | },
68 | )
69 | if !isSuccess {
70 | response.FailToError(ctx, errors.New("发送失败"))
71 | }
72 | response.OkWithData(response.ExpirationTime{ExpirationTime: global.Config.Captcha.EmailCaptchaTimeOut}, ctx)
73 | }
74 |
--------------------------------------------------------------------------------
/docs/beforeDocsMake/renameModel/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bufio"
5 | "go/ast"
6 | "go/parser"
7 | "go/token"
8 | "io/fs"
9 | "os"
10 | "path/filepath"
11 | "strings"
12 |
13 | "github.com/ZiRunHua/LeapLedger/global/constant"
14 | )
15 |
16 | func main() {
17 | handleDir(filepath.Clean(constant.RootDir + "/api/request/"))
18 | handleDir(filepath.Clean(constant.RootDir + "/api/response/"))
19 | }
20 | func handleDir(path string) {
21 | _ = filepath.Walk(
22 | path, func(path string, info fs.FileInfo, err error) error {
23 | if info.IsDir() {
24 | return nil
25 | }
26 | handleFile(path)
27 | return nil
28 | },
29 | )
30 | return
31 | }
32 | func handleFile(path string) {
33 | file, err := os.OpenFile(path, os.O_RDWR, 0666)
34 | defer file.Close()
35 | if err != nil {
36 | panic(err)
37 | }
38 | reader := bufio.NewReader(file)
39 | var lines []string
40 | for {
41 | line, err := reader.ReadString('\n')
42 | if err != nil {
43 | if err.Error() == "EOF" {
44 | break
45 | }
46 | panic(err)
47 | }
48 | lines = append(lines, line)
49 | }
50 | fileSet := token.NewFileSet()
51 | astFile, err := parser.ParseFile(fileSet, path, nil, parser.AllErrors)
52 | if err != nil {
53 | panic(err)
54 | }
55 |
56 | for _, decl := range astFile.Decls {
57 | genDecl, ok := decl.(*ast.GenDecl)
58 | if !ok {
59 | continue
60 | }
61 | if genDecl.Tok != token.TYPE {
62 | continue
63 | }
64 | for _, spec := range genDecl.Specs {
65 | structSpec, ok := spec.(*ast.TypeSpec)
66 | if !ok {
67 | continue
68 | }
69 | structType, ok := structSpec.Type.(*ast.StructType)
70 | if !ok {
71 | continue
72 | }
73 | lineNumber := fileSet.Position(structType.End()).Line - 1
74 | if !strings.Contains(lines[lineNumber], "// @name") {
75 | lines[lineNumber] = strings.TrimSpace(lines[lineNumber]) + " // @name " + structSpec.Name.Name + "\n"
76 | }
77 | }
78 | }
79 | file, err = os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0666)
80 | if err != nil {
81 | panic(err)
82 | }
83 | defer file.Close()
84 | writer := bufio.NewWriter(file)
85 | for _, line := range lines {
86 | _, err = writer.WriteString(line)
87 | if err != nil {
88 | panic(err)
89 | }
90 | }
91 | err = writer.Flush()
92 | if err != nil {
93 | panic(err)
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/global/cron/scheduler.go:
--------------------------------------------------------------------------------
1 | package cron
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "github.com/ZiRunHua/LeapLedger/global/lock"
7 | "github.com/ZiRunHua/LeapLedger/global/nats"
8 | "go.uber.org/zap"
9 | "runtime/debug"
10 | )
11 |
12 | const lockKeyPrefix = "cron:"
13 | const publishLockKeyPrefix = lockKeyPrefix + "publish:"
14 | const taskPublishLockKeyPrefix = publishLockKeyPrefix + "task:"
15 |
16 | func PublishTask(task nats.Task) func() {
17 | return MakeOnceJob(
18 | lock.Key(taskPublishLockKeyPrefix+string(task)),
19 | func() error {
20 | isSuccess := nats.PublishTask(task)
21 | if !isSuccess {
22 | return errors.New("publish fail")
23 | }
24 | return nil
25 | },
26 | )
27 | }
28 |
29 | func PublishTaskWithPayload[T nats.PayloadType](task nats.Task, payload T) func() {
30 | return MakeOnceJob(
31 | lock.Key(taskPublishLockKeyPrefix+string(task)),
32 | func() error {
33 | isSuccess := nats.PublishTaskWithPayload[T](task, payload)
34 | if !isSuccess {
35 | return errors.New("publish fail")
36 | }
37 | return nil
38 | },
39 | )
40 | }
41 |
42 | func PublishTaskWithMakePayload[T nats.PayloadType](task nats.Task, makePayload func() (T, error)) func() {
43 | return MakeOnceJob(
44 | lock.Key(taskPublishLockKeyPrefix+string(task)),
45 | func() error {
46 | payload, err := makePayload()
47 | if err != nil {
48 | return err
49 | }
50 | isSuccess := nats.PublishTaskWithPayload[T](task, payload)
51 | if !isSuccess {
52 | return errors.New("publish fail")
53 | }
54 | return nil
55 | },
56 | )
57 | }
58 |
59 | func MakeOnceJob(key lock.Key, f func() error) func() {
60 | return MakeJobFunc(
61 | func() error {
62 | l := lock.New(key)
63 | err := l.Lock(context.Background())
64 | if err != nil {
65 | if errors.Is(err, lock.ErrLockOccupied) {
66 | return nil
67 | }
68 | return err
69 | }
70 | defer l.Release(context.Background())
71 | return f()
72 | },
73 | )
74 | }
75 |
76 | func MakeJobFunc(f func() error) func() {
77 | return func() {
78 | defer func() {
79 | r := recover()
80 | if r != nil {
81 | logger.Error("job exec panic", zap.Any("panic", r), zap.Stack(string(debug.Stack())))
82 | }
83 | }()
84 | err := f()
85 | if err != nil {
86 | logger.Error("job exec error", zap.Error(err))
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/model/transaction/statisticModel.go:
--------------------------------------------------------------------------------
1 | package transactionModel
2 |
3 | import (
4 | commonModel "github.com/ZiRunHua/LeapLedger/model/common"
5 | "gorm.io/gorm"
6 | "time"
7 | )
8 |
9 | type statisticModel interface {
10 | GetUpdatesValue(amount, count int) map[string]interface{}
11 | GetDate(tradeTime time.Time) time.Time
12 | TableName() string
13 | }
14 |
15 | type Statistic struct {
16 | Date time.Time `gorm:"primaryKey;type:TIMESTAMP"`
17 | Amount int
18 | Count int
19 | commonModel.BaseModel
20 | }
21 |
22 | func (s *Statistic) GetUpdatesValue(amount, count int) map[string]interface{} {
23 | return map[string]interface{}{
24 | "amount": gorm.Expr("amount + ?", amount),
25 | "count": gorm.Expr("count + ?", count),
26 | }
27 | }
28 |
29 | func (s *Statistic) GetDate(t time.Time) time.Time {
30 | return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
31 | }
32 |
33 | func ExpenseAccumulate(
34 | tradeTime time.Time, accountId uint, userId uint, categoryId uint, amount int, count int, tx *gorm.DB,
35 | ) error {
36 | var err error
37 | var accountSta ExpenseAccountStatistic
38 | err = accountSta.Accumulate(tradeTime, accountId, amount, count, tx)
39 | if err != nil {
40 | return err
41 | }
42 | var accountUserSta ExpenseAccountUserStatistic
43 | err = accountUserSta.Accumulate(tradeTime, accountId, userId, categoryId, amount, count, tx)
44 | if err != nil {
45 | return err
46 | }
47 | var categorySta ExpenseCategoryStatistic
48 | err = categorySta.Accumulate(tradeTime, accountId, categoryId, amount, count, tx)
49 | if err != nil {
50 | return err
51 | }
52 | return err
53 | }
54 |
55 | func IncomeAccumulate(
56 | tradeTime time.Time, accountId uint, userId uint, categoryId uint, amount int, count int, tx *gorm.DB,
57 | ) error {
58 | var err error
59 | var accountSta IncomeAccountStatistic
60 | err = accountSta.Accumulate(tradeTime, accountId, amount, count, tx)
61 | if err != nil {
62 | return err
63 | }
64 | var accountUserSta IncomeAccountUserStatistic
65 | err = accountUserSta.Accumulate(tradeTime, accountId, userId, categoryId, amount, count, tx)
66 | if err != nil {
67 | return err
68 | }
69 | var categorySta IncomeCategoryStatistic
70 | err = categorySta.Accumulate(tradeTime, accountId, categoryId, amount, count, tx)
71 | if err != nil {
72 | return err
73 | }
74 | return err
75 | }
76 |
--------------------------------------------------------------------------------
/model/product/productTransactionCategoryModel.go:
--------------------------------------------------------------------------------
1 | package productModel
2 |
3 | import (
4 | "github.com/ZiRunHua/LeapLedger/global"
5 | "github.com/ZiRunHua/LeapLedger/global/constant"
6 | commonModel "github.com/ZiRunHua/LeapLedger/model/common"
7 | )
8 |
9 | type TransactionCategory struct {
10 | ID uint `gorm:"primary_key"`
11 | ProductKey string `gorm:"uniqueIndex:unique_name,priority:1"`
12 | IncomeExpense constant.IncomeExpense `gorm:"size:8;comment:'收支类型';uniqueIndex:unique_name,priority:2"`
13 | Name string `gorm:"size:64;uniqueIndex:unique_name,priority:3"`
14 | commonModel.BaseModel
15 | }
16 |
17 | func (tc *TransactionCategory) TableName() string {
18 | return "product_transaction_category"
19 | }
20 |
21 | func (tc *TransactionCategory) IsEmpty() bool {
22 | return tc == nil || tc.ID == 0
23 | }
24 |
25 | func (tc *TransactionCategory) SelectById(id uint) error {
26 | return global.GvaDb.First(tc, id).Error
27 | }
28 |
29 | func (tc *TransactionCategory) GetMap(productKey Key) (map[uint]TransactionCategory, error) {
30 | transCategoryMap := make(map[uint]TransactionCategory)
31 | var prodTransCategory TransactionCategory
32 | rows, err := global.GvaDb.Model(&prodTransCategory).Where(
33 | "product_key = ? ", productKey,
34 | ).Rows()
35 | if err != nil {
36 | return nil, err
37 | }
38 | for rows.Next() {
39 | err = global.GvaDb.ScanRows(rows, &prodTransCategory)
40 | if err != nil {
41 | return nil, err
42 | }
43 | transCategoryMap[prodTransCategory.ID] = prodTransCategory
44 | }
45 | return transCategoryMap, nil
46 | }
47 |
48 | func (tc *TransactionCategory) GetIncomeExpenseAndNameMap(productKey Key) (
49 | result map[constant.IncomeExpense]map[string]TransactionCategory, err error,
50 | ) {
51 | var prodTransCategory TransactionCategory
52 | rows, err := global.GvaDb.Model(&prodTransCategory).Where(
53 | "product_key = ? ", productKey,
54 | ).Rows()
55 | if err != nil {
56 | return
57 | }
58 | result = map[constant.IncomeExpense]map[string]TransactionCategory{}
59 | for rows.Next() {
60 | err = global.GvaDb.ScanRows(rows, &prodTransCategory)
61 | if err != nil {
62 | return
63 | }
64 | if _, exist := result[prodTransCategory.IncomeExpense]; exist == false {
65 | result[prodTransCategory.IncomeExpense] = map[string]TransactionCategory{prodTransCategory.Name: prodTransCategory}
66 | } else {
67 | result[prodTransCategory.IncomeExpense][prodTransCategory.Name] = prodTransCategory
68 | }
69 | }
70 | return
71 | }
72 |
--------------------------------------------------------------------------------
/initialize/redis.go:
--------------------------------------------------------------------------------
1 | package initialize
2 |
3 | import (
4 | "context"
5 | "log"
6 | "time"
7 |
8 | "github.com/ZiRunHua/LeapLedger/global/constant"
9 | "github.com/ZiRunHua/LeapLedger/util"
10 | "github.com/go-redis/redis/v8"
11 | )
12 |
13 | type _redis struct {
14 | Addr string `yaml:"Addr"`
15 | Password string `yaml:"Password"`
16 | Db int `yaml:"Db"`
17 | LockDb int `yaml:"LockDb"`
18 | }
19 |
20 | var Rdb, LockRdb *redis.Client
21 |
22 | func (r *_redis) do() error {
23 | if len(r.Addr) == 0 {
24 | return nil
25 | }
26 | var err error
27 | Rdb, err = r.getNewRedisClient("", r.Db)
28 | if err != nil {
29 | return err
30 | }
31 | LockRdb, err = r.getNewRedisClient("lock", r.LockDb)
32 | if err != nil {
33 | return err
34 | }
35 | Cache = &util.RedisCache{DB: r.Db, Addr: r.Addr, Password: r.Password}
36 | return Cache.Init()
37 | }
38 |
39 | func (r *_redis) getNewRedisClient(name string, dbNum int) (*redis.Client, error) {
40 | connect := func() (*redis.Client, error) {
41 | db := redis.NewClient(&redis.Options{Addr: r.Addr, Password: r.Password, DB: dbNum})
42 | ctx, cancel := context.WithTimeout(context.TODO(), time.Second*3)
43 | defer cancel()
44 | return db, db.Ping(ctx).Err()
45 | }
46 | db, err := reconnection[*redis.Client](connect, 3)
47 | if err != nil {
48 | return db, err
49 | }
50 | if Config.Mode == constant.Debug {
51 | db.AddHook(&RedisHook{name: name})
52 | }
53 | return db, err
54 | }
55 |
56 | type RedisHook struct {
57 | name string
58 | }
59 |
60 | func (rh RedisHook) BeforeProcess(ctx context.Context, cmd redis.Cmder) (context.Context, error) {
61 | if len(rh.name) == 0 {
62 | log.Printf("exec => <%s>\n", cmd)
63 | } else {
64 | log.Printf("%s exec => <%s>\n", rh.name, cmd)
65 | }
66 | return ctx, nil
67 | }
68 |
69 | func (rh RedisHook) AfterProcess(_ context.Context, cmd redis.Cmder) error {
70 | if len(rh.name) == 0 {
71 | log.Printf("finish => <%s>\n", cmd)
72 | } else {
73 | log.Printf("%s finish => <%s>\n", rh.name, cmd)
74 | }
75 | return nil
76 | }
77 |
78 | func (rh RedisHook) BeforeProcessPipeline(ctx context.Context, cmds []redis.Cmder) (context.Context, error) {
79 | if len(rh.name) == 0 {
80 | log.Printf("pipeline exec => %v\n", cmds)
81 | } else {
82 | log.Printf("%s pipeline exec => %v\n", rh.name, cmds)
83 | }
84 | return ctx, nil
85 | }
86 |
87 | func (rh RedisHook) AfterProcessPipeline(_ context.Context, cmds []redis.Cmder) error {
88 | if len(rh.name) == 0 {
89 | log.Printf("pipeline finish => %v\n", cmds)
90 | } else {
91 | log.Printf("%s pipeline finish => %v\n", rh.name, cmds)
92 | }
93 | return nil
94 | }
95 |
--------------------------------------------------------------------------------
/model/product/productDao.go:
--------------------------------------------------------------------------------
1 | package productModel
2 |
3 | import (
4 | "github.com/ZiRunHua/LeapLedger/global"
5 | "github.com/ZiRunHua/LeapLedger/global/constant"
6 | "gorm.io/gorm"
7 | )
8 |
9 | type ProductDao struct {
10 | db *gorm.DB
11 | }
12 |
13 | func NewDao(db ...*gorm.DB) *ProductDao {
14 | if len(db) > 0 {
15 | return &ProductDao{db: db[0]}
16 | }
17 | return &ProductDao{global.GvaDb}
18 | }
19 | func (pd *ProductDao) SelectByKey(key Key) (result Product, err error) {
20 | return result, pd.db.Where("`key` = ?", key).First(&result).Error
21 | }
22 |
23 | func (pd *ProductDao) SelectBillByKey(key Key) (result Bill, err error) {
24 | return result, pd.db.Where("`product_key` = ?", key).First(&result).Error
25 | }
26 | func (pd *ProductDao) SelectCategoryByName(
27 | key Key, ie constant.IncomeExpense, name string,
28 | ) (result TransactionCategory, err error) {
29 | err = pd.db.Where("product_key = ? AND income_expense = ? AND name = ?", key, ie, name).First(&result).Error
30 | return
31 | }
32 |
33 | func (pd *ProductDao) SelectAllCategoryMappingByCategoryId(categoryId uint) (
34 | result []TransactionCategoryMapping, err error,
35 | ) {
36 | err = pd.db.Where("category_id = ?", categoryId).Find(&result).Error
37 | return
38 | }
39 | func (pd *ProductDao) GetPtcIdMapping(accountId uint, productKey Key) (
40 | result map[uint]TransactionCategoryMapping, err error,
41 | ) {
42 | var list []TransactionCategoryMapping
43 | err = pd.db.Where("account_id = ? AND product_key = ?", accountId, productKey).Find(&list).Error
44 | if err != nil {
45 | return
46 | }
47 | result = make(map[uint]TransactionCategoryMapping)
48 | for _, mapping := range list {
49 | result[mapping.PtcId] = mapping
50 | }
51 | return
52 | }
53 |
54 | func (pd *ProductDao) GetIncomeExpenseAndNameMap(productKey Key) (
55 | result map[constant.IncomeExpense]map[string]TransactionCategory, err error,
56 | ) {
57 | var list []TransactionCategory
58 | err = pd.db.Where("product_key = ? ", productKey).Find(&list).Error
59 | if err != nil {
60 | return
61 | }
62 | result = make(map[constant.IncomeExpense]map[string]TransactionCategory)
63 | for _, category := range list {
64 | if _, exist := result[category.IncomeExpense]; exist == false {
65 | result[category.IncomeExpense] = map[string]TransactionCategory{category.Name: category}
66 | } else {
67 | result[category.IncomeExpense][category.Name] = category
68 | }
69 | }
70 | return
71 | }
72 | func (pd *ProductDao) GetBillHeaderList(productKey Key) (list []BillHeader, err error) {
73 | err = pd.db.Where("product_key = ? ", productKey).Order("id ASC").Find(&list).Error
74 | return
75 | }
76 |
--------------------------------------------------------------------------------
/api/v1/ws/msg/reader.go:
--------------------------------------------------------------------------------
1 | package msg
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "io"
7 | "sync"
8 |
9 | "github.com/ZiRunHua/LeapLedger/util/fileTool"
10 | "github.com/gorilla/websocket"
11 | )
12 |
13 | func NewReader() Reader {
14 | return &reader{MsgHandle: make(map[MsgType]MsgHandler)}
15 | }
16 |
17 | func RegisterHandle[T any](reader Reader, msgType MsgType, handler func(data T) error) {
18 | reader.registerHandle(
19 | msgType, func(bytes []byte) error {
20 | var data T
21 | err := json.Unmarshal(bytes, &data)
22 | if err != nil {
23 | return err
24 | }
25 | return handler(data)
26 | },
27 | )
28 | }
29 |
30 | func ForReadAndHandleJsonMsg(reader Reader, conn *websocket.Conn) error {
31 | var err error
32 | for {
33 | err = reader.readJsonMsgToHandle(conn)
34 | if err != nil {
35 | return err
36 | }
37 | }
38 | }
39 |
40 | func ReadBytes(reader Reader, conn *websocket.Conn) ([]byte, error) {
41 | return reader.readBytes(conn)
42 | }
43 |
44 | func ReadFile(reader Reader, conn *websocket.Conn) (io.Reader, error) {
45 | return reader.readFile(conn)
46 | }
47 |
48 | type readMsg struct {
49 | Type MsgType
50 | Data json.RawMessage
51 | }
52 |
53 | type Reader interface {
54 | registerHandle(msgType MsgType, handler MsgHandler)
55 | readJsonMsgToHandle(conn *websocket.Conn) error
56 | readBytes(conn *websocket.Conn) ([]byte, error)
57 | readFile(conn *websocket.Conn) (io.Reader, error)
58 | }
59 |
60 | type reader struct {
61 | Reader
62 | MsgHandle map[MsgType]MsgHandler
63 | lock sync.Mutex
64 | }
65 |
66 | func (mr *reader) registerHandle(msgType MsgType, handler MsgHandler) {
67 | mr.lock.Lock()
68 | defer mr.lock.Unlock()
69 | mr.MsgHandle[msgType] = handler
70 | }
71 |
72 | func (mr *reader) readJsonMsgToHandle(conn *websocket.Conn) error {
73 | var msg readMsg
74 | _, str, err := conn.ReadMessage()
75 | if err != nil {
76 | return err
77 | }
78 | err = json.Unmarshal(str, &msg)
79 | if err != nil {
80 | return err
81 | }
82 | handle, exist := mr.MsgHandle[msg.Type]
83 | if !exist {
84 | return ErrMsgHandleNotExist
85 | }
86 | return handle(msg.Data)
87 | }
88 |
89 | func (mr *reader) readBytes(conn *websocket.Conn) ([]byte, error) {
90 | _, str, err := conn.ReadMessage()
91 | return str, err
92 | }
93 | func (mr *reader) readFile(conn *websocket.Conn) (io.Reader, error) {
94 | _, fileData, err := conn.ReadMessage()
95 | if err != nil {
96 | return nil, err
97 | }
98 | r := bytes.NewBuffer(fileData)
99 | return fileTool.GetUTF8Reader(r, fileData)
100 | }
101 |
--------------------------------------------------------------------------------
/test/service/transaction/base_test.go:
--------------------------------------------------------------------------------
1 | package transaction
2 |
3 | import (
4 | "context"
5 |
6 | _ "github.com/ZiRunHua/LeapLedger/test/initialize"
7 | )
8 | import (
9 | "reflect"
10 | "testing"
11 | "time"
12 |
13 | "github.com/ZiRunHua/LeapLedger/global"
14 | "github.com/ZiRunHua/LeapLedger/global/constant"
15 | "github.com/ZiRunHua/LeapLedger/global/nats"
16 | accountModel "github.com/ZiRunHua/LeapLedger/model/account"
17 | transactionModel "github.com/ZiRunHua/LeapLedger/model/transaction"
18 | "github.com/ZiRunHua/LeapLedger/test"
19 | )
20 |
21 | func TestCreate(t *testing.T) {
22 | transInfo := test.NewTransInfo()
23 | user, err := accountModel.NewDao().SelectUser(transInfo.AccountId, transInfo.UserId)
24 | if err != nil {
25 | t.Fatal(err)
26 | }
27 | builder := transactionModel.NewStatisticConditionBuilder(transInfo.AccountId)
28 | builder.WithUserIds([]uint{transInfo.UserId}).WithCategoryIds([]uint{transInfo.CategoryId})
29 | builder.WithDate(transInfo.TradeTime, transInfo.TradeTime)
30 | total, err := transactionModel.NewDao().GetIeStatisticByCondition(&transInfo.IncomeExpense, *builder.Build(), nil)
31 | if err != nil {
32 | t.Fatal(err)
33 | }
34 | var trans transactionModel.Transaction
35 | createOption, err := service.NewOptionFormConfig(transInfo, context.TODO())
36 | if err != nil {
37 | t.Fatal(err)
38 | }
39 | trans, err = service.Create(transInfo, user, transactionModel.RecordTypeOfManual, createOption, context.TODO())
40 | if err != nil {
41 | t.Fatal(err)
42 | }
43 | time.Sleep(time.Second * 10)
44 | newTotal, err := transactionModel.NewDao().GetIeStatisticByCondition(
45 | &transInfo.IncomeExpense, *builder.Build(), nil,
46 | )
47 | if err != nil {
48 | t.Fatal(err)
49 | }
50 |
51 | if transInfo.IncomeExpense == constant.Income {
52 | total.Income.Amount += int64(trans.Amount)
53 | total.Income.Count++
54 | } else {
55 | total.Expense.Amount += int64(trans.Amount)
56 | total.Expense.Count++
57 | }
58 | if !reflect.DeepEqual(total, newTotal) {
59 | t.Fatal("total not equal", total, newTotal)
60 | } else {
61 | t.Log("pass", total, newTotal)
62 | }
63 | }
64 |
65 | func TestAll(t *testing.T) {
66 | var transaction transactionModel.Transaction
67 | rows, err := global.GvaDb.Model(&transactionModel.Transaction{}).Rows()
68 | defer func() {
69 | err = rows.Close()
70 | if err != nil {
71 | t.Fatal(err)
72 | }
73 | }()
74 | if err != nil {
75 | t.Fatal(err)
76 | }
77 | for rows.Next() {
78 | err = global.GvaDb.ScanRows(rows, &transaction)
79 | if err != nil {
80 | t.Fatal(err)
81 | }
82 | nats.PublishTaskWithPayload(nats.TaskStatisticUpdate, transaction.GetStatisticData(true))
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/initialize/initialize.go:
--------------------------------------------------------------------------------
1 | package initialize
2 |
3 | import (
4 | "context"
5 | "os"
6 | "path/filepath"
7 | "time"
8 |
9 | "github.com/ZiRunHua/LeapLedger/global/constant"
10 | "github.com/ZiRunHua/LeapLedger/util"
11 |
12 | "github.com/go-co-op/gocron"
13 | "github.com/nats-io/nats.go"
14 | "go.uber.org/zap"
15 | "golang.org/x/sync/errgroup"
16 | "gopkg.in/yaml.v3"
17 | "gorm.io/gorm"
18 | )
19 |
20 | type _config struct {
21 | Mode constant.ServerMode `yaml:"Mode"`
22 | Redis _redis `yaml:"Redis"`
23 | Mysql _mysql `yaml:"Mysql"`
24 | Nats _nats `yaml:"Nats"`
25 | Scheduler _scheduler `yaml:"Scheduler"`
26 | Logger _logger `yaml:"Logger"`
27 | System _system `yaml:"System"`
28 | Captcha _captcha `yaml:"Captcha"`
29 | ThirdParty _thirdParty `yaml:"ThirdParty"`
30 | }
31 |
32 | var (
33 | Config *_config
34 | Cache util.Cache
35 | Db *gorm.DB
36 | Nats *nats.Conn
37 | Scheduler *gocron.Scheduler
38 | RequestLogger *zap.Logger
39 | ErrorLogger *zap.Logger
40 | PanicLogger *zap.Logger
41 | )
42 |
43 | func init() {
44 | var err error
45 | Config = &_config{
46 | Redis: _redis{}, Mysql: _mysql{}, Logger: _logger{}, System: _system{}, Captcha: _captcha{}, Nats: _nats{},
47 | ThirdParty: _thirdParty{WeCom: _weCom{}},
48 | }
49 |
50 | if err = initConfig(); err != nil {
51 | panic(err)
52 | }
53 |
54 | group, _ := errgroup.WithContext(context.TODO())
55 | group.Go(Config.Logger.do)
56 | group.Go(Config.Mysql.do)
57 | group.Go(Config.Redis.do)
58 | group.Go(Config.Nats.do)
59 | group.Go(Config.Scheduler.do)
60 | if err = group.Wait(); err != nil {
61 | panic(err)
62 | }
63 | }
64 |
65 | func initConfig() error {
66 | configFileName := os.Getenv("CONFIG_FILE_NAME")
67 | if len(configFileName) == 0 {
68 | configFileName = "config.yaml"
69 | }
70 | configPath := filepath.Join(constant.RootDir, configFileName)
71 | yamlFile, err := os.ReadFile(configPath)
72 | if err != nil {
73 | return err
74 | }
75 | err = yaml.Unmarshal(yamlFile, Config)
76 | if err != nil {
77 | return err
78 | }
79 | return setConfigDefault()
80 | }
81 | func setConfigDefault() error {
82 | if Config.System.LockMode == "" {
83 | Config.System.LockMode = "redis"
84 | }
85 | return nil
86 | }
87 | func reconnection[T any](connect func() (T, error), retryTimes int) (result T, err error) {
88 | defer func() {
89 | if err != nil && retryTimes > 0 {
90 | time.Sleep(time.Second * 3)
91 | result, err = reconnection[T](connect, retryTimes-1)
92 | }
93 | }()
94 | result, err = connect()
95 | return
96 | }
97 |
--------------------------------------------------------------------------------
/model/user/userModel.go:
--------------------------------------------------------------------------------
1 | package userModel
2 |
3 | import (
4 | "errors"
5 | "github.com/ZiRunHua/LeapLedger/global"
6 | "github.com/ZiRunHua/LeapLedger/global/constant"
7 | commonModel "github.com/ZiRunHua/LeapLedger/model/common"
8 | "gorm.io/gorm"
9 | "strconv"
10 | "time"
11 | )
12 |
13 | type User struct {
14 | ID uint `gorm:"primarykey"`
15 | Username string `gorm:"type:varchar(128);comment:'用户名'"`
16 | Password string `gorm:"type:char(64);comment:'密码'"`
17 | Email string `gorm:"type:varchar(64);comment:'邮箱';unique"`
18 | CreatedAt time.Time `gorm:"type:TIMESTAMP"`
19 | UpdatedAt time.Time `gorm:"type:TIMESTAMP"`
20 | DeletedAt gorm.DeletedAt `gorm:"index;type:TIMESTAMP"`
21 | commonModel.BaseModel
22 | }
23 |
24 | type UserInfo struct {
25 | ID uint
26 | Username string
27 | Email string
28 | }
29 |
30 | func (u *User) SelectById(id uint, selects ...interface{}) error {
31 | query := global.GvaDb.Where("id = ?", id)
32 | if len(selects) > 0 {
33 | query = query.Select(selects[0], selects[1:]...)
34 | }
35 | return query.First(u).Error
36 | }
37 |
38 | func (u *User) GetUserClient(client constant.Client, db *gorm.DB) (clientInfo UserClientBaseInfo, err error) {
39 | var data UserClientBaseInfo
40 | err = db.Model(GetUserClientModel(client)).Where("user_id = ?", u.ID).First(&data).Error
41 | return data, err
42 | }
43 |
44 | func (u *User) IsTourist(db *gorm.DB) (bool, error) {
45 | _, err := NewDao(db).SelectTour(u.ID)
46 | if err != nil {
47 | if errors.Is(err, gorm.ErrRecordNotFound) {
48 | return false, nil
49 | }
50 | return false, err
51 | }
52 | return true, nil
53 | }
54 |
55 | func (u *User) ModifyAsTourist(db *gorm.DB) error {
56 | return db.Model(u).Updates(
57 | map[string]interface{}{
58 | "username": "游玩家",
59 | "email": "player" + strconv.Itoa(int(u.ID)),
60 | },
61 | ).Error
62 | }
63 |
64 | func (u *User) GetTransactionShareConfig() (TransactionShareConfig, error) {
65 | data := TransactionShareConfig{}
66 | return data, data.SelectByUserId(u.ID)
67 | }
68 |
69 | type Tour struct {
70 | UserId uint `gorm:"primary"`
71 | Status bool
72 | CreatedAt time.Time `gorm:"type:TIMESTAMP"`
73 | UpdatedAt time.Time `gorm:"type:TIMESTAMP"`
74 | DeletedAt gorm.DeletedAt `gorm:"index;type:TIMESTAMP"`
75 | commonModel.BaseModel
76 | }
77 |
78 | func (u *Tour) TableName() string {
79 | return "user_tour"
80 | }
81 | func (t *Tour) GetUser(db *gorm.DB) (user User, err error) {
82 | err = db.First(&user, t.UserId).Error
83 | return user, err
84 | }
85 | func (t *Tour) Use(db *gorm.DB) error {
86 | if t.Status == true {
87 | return errors.New("tourist used")
88 | }
89 | return db.Model(t).Where("user_id = ?", t.UserId).Update("status", true).Error
90 | }
91 |
--------------------------------------------------------------------------------
/util/timeTool/timeFunc.go:
--------------------------------------------------------------------------------
1 | package timeTool
2 |
3 | import "time"
4 |
5 | func SplitMonths(startDate, endDate time.Time) [][2]time.Time {
6 | var months [][2]time.Time
7 | current := startDate
8 | for !current.Equal(endDate) {
9 | current = GetLastSecondOfMonth(startDate)
10 | if current.After(endDate) {
11 | current = endDate
12 | }
13 | months = append(months, [2]time.Time{startDate, current})
14 | startDate = current.Add(time.Second)
15 | }
16 | return months
17 | }
18 |
19 | func SplitDays(startDate, endDate time.Time) []time.Time {
20 | startDate = time.Date(startDate.Year(), startDate.Month(), startDate.Day(), 0, 0, 0, 0, startDate.Location())
21 | var days []time.Time
22 | current := startDate
23 | for !current.After(endDate) {
24 | days = append(days, current)
25 | current = current.AddDate(0, 0, 1)
26 | }
27 | return days
28 | }
29 | func GetFirstSecondOfDay(date time.Time) time.Time {
30 | year, month, day := date.Date()
31 | return time.Date(year, month, day, 0, 0, 0, 0, date.Location())
32 | }
33 |
34 | func GetFirstSecondOfMonth(date time.Time) time.Time {
35 | year, month, _ := date.Date()
36 | return time.Date(year, month, 1, 0, 0, 0, 0, date.Location())
37 | }
38 |
39 | func GetLastSecondOfMonth(date time.Time) time.Time {
40 | year, month, _ := date.Date()
41 | nextMonth := time.Date(year, month+1, 1, 0, 0, 0, 0, date.Location())
42 | lastSecond := nextMonth.Add(-time.Second)
43 | return lastSecond
44 | }
45 |
46 | // 获取本周一的第一秒
47 | func GetFirstSecondOfWeek(currentTime time.Time) time.Time {
48 | weekday := currentTime.Weekday()
49 | daysToMonday := time.Duration(0)
50 | if weekday != time.Monday {
51 | daysToMonday = time.Duration(weekday - time.Monday)
52 | if weekday < time.Monday {
53 | daysToMonday += 7
54 | }
55 | }
56 |
57 | monday := currentTime.Add(-daysToMonday * 24 * time.Hour)
58 | monday = time.Date(monday.Year(), monday.Month(), monday.Day(), 0, 0, 0, 0, currentTime.Location())
59 | return monday
60 | }
61 |
62 | func GetLastSecondOfWeek(t time.Time) time.Time {
63 | weekday := t.Weekday()
64 | daysToSunday := (time.Sunday - weekday) % 7
65 | if daysToSunday < 0 {
66 | daysToSunday += 7
67 | }
68 | sunday := t.AddDate(0, 0, int(daysToSunday))
69 | sunday = time.Date(sunday.Year(), sunday.Month(), sunday.Day(), 23, 59, 59, 0, sunday.Location())
70 | return sunday
71 | }
72 |
73 | // 获取今年的第一秒
74 | func GetFirstSecondOfYear(currentTime time.Time) time.Time {
75 | return time.Date(currentTime.Year(), time.January, 1, 0, 0, 0, 0, currentTime.Location())
76 | }
77 |
78 | func GetLastSecondOfYear(currentTime time.Time) time.Time {
79 | return time.Date(currentTime.Year(), time.December, 31, 23, 59, 59, 0, currentTime.Location())
80 | }
81 |
82 | func ToDay(t time.Time) time.Time {
83 | return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
84 | }
85 |
--------------------------------------------------------------------------------
/api/response/func.go:
--------------------------------------------------------------------------------
1 | package response
2 |
3 | import (
4 | "fmt"
5 | "github.com/ZiRunHua/LeapLedger/global"
6 | "github.com/gin-gonic/gin"
7 | "github.com/pkg/errors"
8 | "gorm.io/gorm"
9 | )
10 |
11 | type Data struct {
12 | Data interface{}
13 | Msg string `example:"success"`
14 | } // @name Response
15 |
16 | type NoContent struct {
17 | Data interface{}
18 | Msg string
19 | } // @name NoContent
20 |
21 | func ResponseAndAbort(status int, data interface{}, msg string, ctx *gin.Context) {
22 | ctx.AbortWithStatusJSON(
23 | status, Data{
24 | data,
25 | msg,
26 | },
27 | )
28 | }
29 |
30 | func Response(status int, data interface{}, msg string, ctx *gin.Context) {
31 | ctx.JSON(
32 | status, Data{
33 | data,
34 | msg,
35 | },
36 | )
37 | }
38 |
39 | func Ok(ctx *gin.Context) {
40 | Response(204, map[string]interface{}{}, "操作成功", ctx)
41 | }
42 |
43 | func OkWithMessage(message string, ctx *gin.Context) {
44 | Response(200, map[string]interface{}{}, message, ctx)
45 | }
46 |
47 | func OkWithData(data interface{}, ctx *gin.Context) {
48 | Response(200, data, "查询成功", ctx)
49 | }
50 |
51 | func OkWithDetailed(data interface{}, message string, ctx *gin.Context) {
52 | Response(200, data, message, ctx)
53 | }
54 |
55 | func Fail(ctx *gin.Context) {
56 | ResponseAndAbort(500, map[string]interface{}{}, "服务器睡了(这年龄你睡得着!)", ctx)
57 | }
58 | func FailToParameter(ctx *gin.Context, err error) {
59 | ResponseAndAbort(400, map[string]interface{}{}, "参数错误"+err.Error(), ctx)
60 | }
61 |
62 | func FailToError(ctx *gin.Context, err error) {
63 | logError(ctx, err)
64 | msg := err.Error()
65 | if errors.Is(err, gorm.ErrRecordNotFound) {
66 | msg = "数据未找到"
67 | }
68 | ResponseAndAbort(500, map[string]interface{}{}, msg, ctx)
69 | }
70 |
71 | func FailWithMessage(message string, ctx *gin.Context) {
72 | ResponseAndAbort(500, map[string]interface{}{}, message, ctx)
73 | }
74 |
75 | func FrequentOperation(ctx *gin.Context) {
76 | ResponseAndAbort(500, map[string]interface{}{}, "请勿频繁操作,请稍后再试!", ctx)
77 | }
78 |
79 | func FailWithDetailed(data interface{}, message string, ctx *gin.Context) {
80 | ResponseAndAbort(500, data, message, ctx)
81 | }
82 |
83 | func Forbidden(ctx *gin.Context) {
84 | ResponseAndAbort(403, map[string]interface{}{}, "无权访问", ctx)
85 | }
86 |
87 | func TokenExpired(ctx *gin.Context) {
88 | ResponseAndAbort(401, map[string]interface{}{}, "token expired", ctx)
89 | }
90 |
91 | func logError(ctx *gin.Context, err error) {
92 | reqMethod := ctx.Request.Method
93 | reqPath := ctx.Request.URL.Path
94 | clientIP := ctx.ClientIP()
95 | global.ErrorLogger.Error(
96 | fmt.Sprintf(
97 | "%s %s %s error: %+v\n",
98 | reqMethod,
99 | reqPath,
100 | clientIP,
101 | err,
102 | ),
103 | )
104 | }
105 |
--------------------------------------------------------------------------------
/util/dataTool/map.go:
--------------------------------------------------------------------------------
1 | package dataTool
2 |
3 | import (
4 | "sync"
5 | )
6 |
7 | type Map[K comparable, V any] interface {
8 | Load(K) (V, bool)
9 | Store(K, V)
10 | LoadOrStore(K, V) (actual V, loaded bool)
11 | Delete(K)
12 | Range(func(K, V) (shouldContinue bool))
13 | }
14 |
15 | // SyncMap is type-safe sync.Map
16 | type SyncMap[K comparable, V any] struct {
17 | Map sync.Map
18 | }
19 |
20 | func NewSyncMap[K comparable, V any]() *SyncMap[K, V] {
21 | return &SyncMap[K, V]{}
22 | }
23 | func (t *SyncMap[K, V]) Load(k K) (V, bool) {
24 | value, ok := t.Map.Load(k)
25 | if value != nil {
26 | return value.(V), ok
27 | }
28 | var v V
29 | return v, ok
30 | }
31 |
32 | func (t *SyncMap[K, V]) Store(k K, v V) {
33 | t.Map.Store(k, v)
34 | }
35 |
36 | func (t *SyncMap[K, V]) LoadOrStore(k K, v V) (actual V, loaded bool) {
37 | value, ok := t.Map.LoadOrStore(k, v)
38 | return value.(V), ok
39 | }
40 |
41 | func (t *SyncMap[K, V]) CompareAndSwap(k K, old, new V) (swapped bool) {
42 | return t.Map.CompareAndSwap(k, old, new)
43 | }
44 |
45 | func (t *SyncMap[K, V]) CompareAndDelete(old, new V) (deleted bool) {
46 | return t.Map.CompareAndDelete(old, new)
47 | }
48 |
49 | func (t *SyncMap[K, V]) Delete(key K) {
50 | t.Map.Delete(key)
51 | }
52 |
53 | func (t *SyncMap[K, V]) Range(f func(key K, value V) bool) {
54 | t.Map.Range(
55 | func(key, value any) bool {
56 | return f(key.(K), value.(V))
57 | },
58 | )
59 | }
60 |
61 | type RWMutexMap[K comparable, V any] struct {
62 | mu sync.RWMutex
63 | m map[K]V
64 | }
65 |
66 | func NewRWMutexMap[K comparable, V any]() *RWMutexMap[K, V] {
67 | return &RWMutexMap[K, V]{
68 | m: make(map[K]V),
69 | }
70 | }
71 |
72 | func (sm *RWMutexMap[K, V]) Load(key K) (value V, ok bool) {
73 | sm.mu.RLock()
74 | defer sm.mu.RUnlock()
75 | value, ok = sm.m[key]
76 | return
77 | }
78 |
79 | func (sm *RWMutexMap[K, V]) Store(key K, value V) {
80 | sm.mu.Lock()
81 | defer sm.mu.Unlock()
82 | sm.m[key] = value
83 | }
84 |
85 | func (sm *RWMutexMap[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) {
86 | sm.mu.Lock()
87 | defer sm.mu.Unlock()
88 |
89 | if actual, loaded = sm.m[key]; loaded {
90 | return actual, true
91 | }
92 |
93 | sm.m[key] = value
94 | return value, false
95 | }
96 |
97 | func (sm *RWMutexMap[K, V]) Delete(key K) {
98 | sm.mu.Lock()
99 | defer sm.mu.Unlock()
100 | delete(sm.m, key)
101 | }
102 |
103 | func (sm *RWMutexMap[K, V]) Range(f func(key K, value V) (shouldContinue bool)) {
104 | sm.mu.RLock()
105 | defer sm.mu.RUnlock()
106 |
107 | for k, v := range sm.m {
108 | if !f(k, v) {
109 | break
110 | }
111 | }
112 | }
113 |
114 | func (sm *RWMutexMap[K, V]) Len() int {
115 | sm.mu.RLock()
116 | defer sm.mu.RUnlock()
117 | return len(sm.m)
118 | }
119 |
--------------------------------------------------------------------------------
/global/nats/event.go:
--------------------------------------------------------------------------------
1 | package nats
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "errors"
7 |
8 | "github.com/ZiRunHua/LeapLedger/global/cus"
9 | "github.com/ZiRunHua/LeapLedger/global/db"
10 | "github.com/ZiRunHua/LeapLedger/global/nats/manager"
11 | transactionModel "github.com/ZiRunHua/LeapLedger/model/transaction"
12 | )
13 |
14 | type Event = manager.Event
15 |
16 | const EventOutbox Event = "outbox"
17 | const EventTransactionCreate Event = "transaction_create_event"
18 | const EventTransactionUpdate Event = "transaction_update_event"
19 |
20 | type EventTransactionUpdatePayload struct {
21 | OldTrans, NewTrans transactionModel.Transaction
22 | }
23 |
24 | const EventTransactionDelete Event = "transaction_delete_event"
25 |
26 | func PublishEvent(event Event) (isSuccess bool) {
27 | return eventManage.Publish(event, []byte{})
28 | }
29 |
30 | func PublishEventWithPayload[T PayloadType](event Event, fetchTaskData T) (isSuccess bool) {
31 | str, err := json.Marshal(&fetchTaskData)
32 | if err != nil {
33 | return false
34 | }
35 | return eventManage.Publish(event, str)
36 | }
37 |
38 | func SubscribeEvent[T PayloadType](event Event, name string, handleTransaction handler[T]) {
39 | eventManage.SubscribeToNewConsumer(
40 | event, name, func(payload []byte) error {
41 | var data T
42 | if err := fromJson(payload, &data); err != nil {
43 | return err
44 | }
45 | return db.Transaction(
46 | context.TODO(), func(ctx *cus.TxContext) error {
47 | return handleTransaction(data, ctx)
48 | },
49 | )
50 | },
51 | )
52 | }
53 |
54 | func BindTaskToEvent(event Event, triggerTask Task) {
55 | eventManage.Subscribe(
56 | event, triggerTask,
57 | func(eventData []byte) ([]byte, error) {
58 | return eventData, nil
59 | },
60 | )
61 | }
62 |
63 | func BindTaskToEventAndMakePayload[T PayloadType, TriggerTaskDataType PayloadType](
64 | event Event, triggerTask Task, fetchTaskData func(eventData T) (TriggerTaskDataType, error),
65 | ) {
66 | eventManage.Subscribe(
67 | event, triggerTask, func(eventData []byte) ([]byte, error) {
68 | var data T
69 | if err := fromJson(eventData, &data); err != nil {
70 | return nil, err
71 | }
72 | taskData, err := fetchTaskData(data)
73 | if err != nil {
74 | return nil, err
75 | }
76 | return json.Marshal(taskData)
77 | },
78 | )
79 | }
80 |
81 | func PublishEventToOutboxWithPayload[T PayloadType](ctx context.Context, event Event, payload T) error {
82 | if event == EventOutbox {
83 | return errors.New("cannot be TaskOutbox")
84 | }
85 | bytes, err := json.Marshal(payload)
86 | if err != nil {
87 | return err
88 | }
89 | id, err := outboxService.sendToOutbox(db.Get(ctx), outboxTypeEvent, string(event), bytes)
90 | if err != nil {
91 | return err
92 | }
93 | return db.AddCommitCallback(ctx, func() { PublishEventWithPayload(EventOutbox, id) })
94 | }
95 |
--------------------------------------------------------------------------------
/data/database/product.sql:
--------------------------------------------------------------------------------
1 | REPLACE INTO `product` (`key`, `name`, `hide`, `weight`)
2 | VALUES ('AliPay', '支付宝', 0, 0),
3 | ('WeChatPay', '微信支付', 0, 0);
4 |
5 | REPLACE INTO `product_bill` (`product_key`, `encoding`, `start_row`, `date_format`)
6 | VALUES ('AliPay', 'GBK', 23, '2006-01-02 15:04:05'),
7 | ('WeChatPay', 'UTF-8', 17, '2006-01-02 15:04:05');
8 |
9 | REPLACE INTO `product_bill_header` (`name`, `type`, `product_key`)
10 | VALUES ('交易时间', 'trans_time', 'AliPay'),
11 | ('交易分类', 'trans_category', 'AliPay'),
12 | ('商品说明', 'remark', 'AliPay'),
13 | ('收/支', 'income_expense', 'AliPay'),
14 | ('金额', 'amount', 'AliPay'),
15 | ('交易订单号', 'order_number', 'AliPay'),
16 | ('交易状态', 'trans_status', 'AliPay'),
17 | ('交易时间', 'trans_time', 'WeChatPay'),
18 | ('交易类型', 'trans_category', 'WeChatPay'),
19 | ('商品', 'remark', 'WeChatPay'),
20 | ('收/支', 'income_expense', 'WeChatPay'),
21 | ('金额(元)', 'amount', 'WeChatPay'),
22 | ('交易单号', 'order_number', 'WeChatPay'),
23 | ('当前状态', 'trans_status', 'WeChatPay');
24 |
25 | REPLACE INTO `product_transaction_category` (`id`, `product_key`, `income_expense`, `name`)
26 | VALUES (1, 'AliPay', 'expense', '餐饮美食'),
27 | (2, 'AliPay', 'expense', '服饰装扮'),
28 | (3, 'AliPay', 'expense', '日用百货'),
29 | (4, 'AliPay', 'expense', '家居家装'),
30 | (5, 'AliPay', 'expense', '数码电器'),
31 | (6, 'AliPay', 'expense', '运动户外'),
32 | (7, 'AliPay', 'expense', '美容美发'),
33 | (8, 'AliPay', 'expense', '母婴亲子'),
34 | (9, 'AliPay', 'expense', '宠物'),
35 | (10, 'AliPay', 'expense', '交通出行'),
36 | (11, 'AliPay', 'expense', '爱车养车'),
37 | (12, 'AliPay', 'expense', '住房物业'),
38 | (13, 'AliPay', 'expense', '酒店旅游'),
39 | (14, 'AliPay', 'expense', '文化休闲'),
40 | (15, 'AliPay', 'expense', '教育培训'),
41 | (16, 'AliPay', 'expense', '医疗健康'),
42 | (17, 'AliPay', 'expense', '生活服务'),
43 | (18, 'AliPay', 'expense', '公共服务'),
44 | (19, 'AliPay', 'expense', '商业服务'),
45 | (20, 'AliPay', 'expense', '公益捐赠'),
46 | (21, 'AliPay', 'expense', '互助保障'),
47 | (22, 'AliPay', 'expense', '投资理财'),
48 | (23, 'AliPay', 'expense', '保险'),
49 | (24, 'AliPay', 'expense', '信用借还'),
50 | (25, 'AliPay', 'expense', '充值缴费'),
51 | (26, 'AliPay', 'expense', '转账红包'),
52 | (27, 'AliPay', 'expense', '亲友代付'),
53 | (28, 'AliPay', 'expense', '账户存取'),
54 | (29, 'AliPay', 'expense', '退款'),
55 | (30, 'AliPay', 'expense', '其他'),
56 | (31, 'AliPay', 'income', '收入'),
57 | (32, 'AliPay', 'income', '转账红包'),
58 | (33, 'AliPay', 'income', '账户存取'),
59 | (34, 'AliPay', 'income', '退款'),
60 | (35, 'AliPay', 'income', '其他'),
61 | (36, 'WeChatPay', 'expense', '商户消费'),
62 | (37, 'WeChatPay', 'expense', '扫二维码付款'),
63 | (38, 'WeChatPay', 'income', '二维码收款');
64 |
--------------------------------------------------------------------------------
/initialize/database/enter.go:
--------------------------------------------------------------------------------
1 | package database
2 |
3 | import (
4 | "fmt"
5 |
6 | _ "github.com/ZiRunHua/LeapLedger/model"
7 | "github.com/pkg/errors"
8 | )
9 |
10 | import (
11 | userModel "github.com/ZiRunHua/LeapLedger/model/user"
12 | "github.com/ZiRunHua/LeapLedger/script"
13 | "github.com/ZiRunHua/LeapLedger/service"
14 | "github.com/ZiRunHua/LeapLedger/util"
15 |
16 | _templateService "github.com/ZiRunHua/LeapLedger/service/template"
17 |
18 | "context"
19 |
20 | "github.com/ZiRunHua/LeapLedger/global/cus"
21 | "github.com/ZiRunHua/LeapLedger/global/db"
22 | testInfo "github.com/ZiRunHua/LeapLedger/test/info"
23 | "gorm.io/gorm"
24 | )
25 |
26 | var (
27 | userService = service.GroupApp.UserServiceGroup
28 |
29 | commonService = service.GroupApp.CommonServiceGroup
30 |
31 | templateService = service.GroupApp.TemplateServiceGroup
32 |
33 | testUserPassword = _templateService.TmplUserPassword
34 | )
35 |
36 | func init() {
37 | var err error
38 | ctx := cus.WithDb(context.Background(), db.InitDb)
39 | // init tourist User
40 | err = db.Transaction(ctx, initTourist)
41 | if err != nil {
42 | panic(err)
43 | }
44 | // init test User
45 | err = db.Transaction(ctx, initTestUser)
46 | if err != nil {
47 | panic(err)
48 | }
49 | }
50 |
51 | func initTestUser(ctx *cus.TxContext) (err error) {
52 | tx := db.Get(ctx)
53 | var user userModel.User
54 | user, err = script.User.CreateTourist(ctx)
55 | if err != nil {
56 | return
57 | }
58 | var tourist userModel.Tour
59 | tourist, err = userModel.NewDao(tx).SelectTour(user.ID)
60 | if err != nil {
61 | return
62 | }
63 | err = tourist.Use(tx)
64 | if err != nil {
65 | return
66 | }
67 | err = userService.UpdatePassword(user, util.ClientPasswordHash(user.Email, testUserPassword), ctx)
68 | if err != nil {
69 | return
70 | }
71 | account, _, err := templateService.CreateExampleAccount(user, ctx)
72 | if err != nil {
73 | return
74 | }
75 | token, err := commonService.GenerateJWT(commonService.MakeCustomClaims(user.ID))
76 | if err != nil {
77 | return
78 | }
79 | testInfo.Data = testInfo.Info{
80 | UserId: user.ID,
81 | Email: user.Email,
82 | AccountId: account.ID,
83 | Token: token,
84 | }
85 | fmt.Println("test user", testInfo.Data)
86 | return
87 | }
88 |
89 | func initTourist(ctx *cus.TxContext) error {
90 | tx := db.Get(ctx)
91 | _, err := userModel.NewDao(tx).SelectByUnusedTour()
92 | if err == nil {
93 | return nil
94 | } else if !errors.Is(err, gorm.ErrRecordNotFound) {
95 | return err
96 | }
97 | user, err := script.User.CreateTourist(ctx)
98 | if err != nil {
99 | return err
100 | }
101 | _, accountUser, err := templateService.CreateExampleAccount(user, ctx)
102 | if err != nil {
103 | return err
104 | }
105 | err = script.User.ChangeCurrantAccount(accountUser, tx)
106 | if err != nil {
107 | return err
108 | }
109 | return err
110 | }
111 |
--------------------------------------------------------------------------------
/api/request/user.go:
--------------------------------------------------------------------------------
1 | package request
2 |
3 | import (
4 | "crypto/hmac"
5 | "crypto/sha256"
6 | "encoding/hex"
7 | "errors"
8 | "strings"
9 |
10 | "github.com/ZiRunHua/LeapLedger/global"
11 | "github.com/ZiRunHua/LeapLedger/global/constant"
12 | userModel "github.com/ZiRunHua/LeapLedger/model/user"
13 | )
14 |
15 | type UserLogin struct {
16 | Email string `binding:"required"`
17 | Password string `binding:"required"`
18 | PicCaptcha
19 | }
20 |
21 | type UserRegister struct {
22 | Username string `binding:"required"`
23 | Password string `binding:"required"`
24 | Email string `binding:"required,email"`
25 | Captcha string `binding:"required"`
26 | }
27 |
28 | type UserForgetPassword struct {
29 | Email string `binding:"required,email"`
30 | Password string `binding:"required"`
31 | Captcha string `binding:"required"`
32 | }
33 |
34 | type UserUpdatePassword struct {
35 | Password string `binding:"required"`
36 | Captcha string `binding:"required"`
37 | }
38 |
39 | type UserUpdateInfo struct {
40 | Username string `binding:"required"`
41 | }
42 |
43 | type UserSearch struct {
44 | Id *uint `binding:"omitempty"`
45 | Username string `binding:"required"`
46 | PageData
47 | }
48 |
49 | type UserSendEmail struct {
50 | PicCaptcha
51 | Type constant.UserAction `binding:"required,oneof=updatePassword"`
52 | }
53 |
54 | type UserHome struct {
55 | AccountId uint
56 | }
57 |
58 | type TourApply struct {
59 | DeviceNumber string
60 | Key string
61 | Sign string
62 | }
63 |
64 | func (t *TourApply) CheckSign() bool {
65 | h := hmac.New(sha256.New, []byte(global.Config.System.ClientSignKey))
66 | h.Write([]byte(t.DeviceNumber + t.Key))
67 | s := hex.EncodeToString(h.Sum(nil))
68 | return strings.Compare(t.Sign, s) == 0
69 | }
70 |
71 | type TransactionShareConfigName string
72 |
73 | const (
74 | FLAG_ACCOUNT TransactionShareConfigName = "account"
75 | FLAG_CREATE_TIME TransactionShareConfigName = "createTime"
76 | FLAG_UPDATE_TIME TransactionShareConfigName = "updateTime"
77 | FLAG_REMARK TransactionShareConfigName = "remark"
78 | )
79 |
80 | type UserTransactionShareConfigUpdate struct {
81 | Flag TransactionShareConfigName
82 | Status bool
83 | }
84 |
85 | func GetFlagByFlagName(name TransactionShareConfigName) (userModel.Flag, error) {
86 | switch name {
87 | case FLAG_ACCOUNT:
88 | return userModel.FLAG_ACCOUNT, nil
89 | case FLAG_CREATE_TIME:
90 | return userModel.FLAG_CREATE_TIME, nil
91 | case FLAG_UPDATE_TIME:
92 | return userModel.FLAG_UPDATE_TIME, nil
93 | case FLAG_REMARK:
94 | return userModel.FLAG_REMARK, nil
95 | }
96 | return 0, errors.New("flag参数错误")
97 | }
98 |
99 | type UserCreateFriendInvitation struct {
100 | Invitee uint
101 | }
102 |
103 | type UserGetFriendInvitation struct {
104 | IsInvite bool
105 | }
106 |
107 | type UserGetAccountInvitationList struct {
108 | PageData
109 | }
110 |
--------------------------------------------------------------------------------
/model/category/categoryModel.go:
--------------------------------------------------------------------------------
1 | package categoryModel
2 |
3 | import (
4 | "github.com/ZiRunHua/LeapLedger/global"
5 | "github.com/ZiRunHua/LeapLedger/global/constant"
6 | accountModel "github.com/ZiRunHua/LeapLedger/model/account"
7 | commonModel "github.com/ZiRunHua/LeapLedger/model/common"
8 | "gorm.io/gorm"
9 | "time"
10 | )
11 |
12 | type Category struct {
13 | ID uint `gorm:"comment:'主键';primary_key;" `
14 | AccountId uint `gorm:"comment:'账本ID';uniqueIndex:unique_name,priority:1"`
15 | FatherId uint `gorm:"comment:'category_father表ID';index" `
16 | IncomeExpense constant.IncomeExpense `gorm:"comment:'收支类型'"`
17 | Name string `gorm:"comment:'名称';size:128;uniqueIndex:unique_name,priority:2"`
18 | Icon string `gorm:"comment:'图标';size:64"`
19 | Previous uint `gorm:"comment:'前一位'"`
20 | OrderUpdatedAt time.Time `gorm:"comment:'顺序更新时间';not null;default:now();type:TIMESTAMP;"`
21 | CreatedAt time.Time `gorm:"type:TIMESTAMP"`
22 | UpdatedAt time.Time `gorm:"type:TIMESTAMP"`
23 | DeletedAt gorm.DeletedAt `gorm:"index;type:TIMESTAMP"`
24 | commonModel.BaseModel
25 | }
26 |
27 | func (c *Category) SelectById(id uint) error {
28 | return global.GvaDb.First(c, id).Error
29 | }
30 |
31 | func (c *Category) GetFather() (father Father, err error) {
32 | err = global.GvaDb.First(&father, c.FatherId).Error
33 | return
34 | }
35 |
36 | func (c *Category) GetAccount() (result accountModel.Account, err error) {
37 | err = result.SelectById(c.AccountId)
38 | return
39 | }
40 |
41 | func (c *Category) CheckName(_ *gorm.DB) error {
42 | if c.Name == "" {
43 | return global.NewErrDataIsEmpty("交易类型名称")
44 | }
45 | return nil
46 | }
47 |
48 | type Condition struct {
49 | account accountModel.Account
50 | ie *constant.IncomeExpense
51 | }
52 |
53 | func (c *Condition) buildWhere(db *gorm.DB) *gorm.DB {
54 | if c.ie == nil {
55 | return db.Where("account_id = ?", c.account.ID)
56 | }
57 | return db.Where("account_id = ? AND income_expense = ?", c.account.ID, c.ie)
58 | }
59 |
60 | // Mapping
61 | // ParentAccountId - ChildCategoryId unique
62 | // ParentCategoryId - ChildCategoryId unique
63 | // ChildAccountId - ParentCategoryId unique
64 | type Mapping struct {
65 | ID uint `gorm:"primarykey"`
66 | ParentAccountId uint `gorm:"comment:'父账本ID';uniqueIndex:idx_mapping,priority:2"`
67 | ChildAccountId uint `gorm:"comment:'子账本ID';" `
68 | ParentCategoryId uint `gorm:"comment:'父收支类型ID';index"`
69 | ChildCategoryId uint `gorm:"comment:'子收支类型ID';uniqueIndex:idx_mapping,priority:1"`
70 | CreatedAt time.Time `gorm:"type:TIMESTAMP"`
71 | UpdatedAt time.Time `gorm:"type:TIMESTAMP"`
72 | DeletedAt gorm.DeletedAt `gorm:"index;type:TIMESTAMP"`
73 | commonModel.BaseModel
74 | }
75 |
76 | func (p *Mapping) TableName() string {
77 | return "category_mapping"
78 | }
79 |
--------------------------------------------------------------------------------
/router/middleware/middleware.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "bytes"
5 | "github.com/ZiRunHua/LeapLedger/api/response"
6 | apiUtil "github.com/ZiRunHua/LeapLedger/api/util"
7 | "github.com/ZiRunHua/LeapLedger/global"
8 | accountModel "github.com/ZiRunHua/LeapLedger/model/account"
9 | commonService "github.com/ZiRunHua/LeapLedger/service/common"
10 | "github.com/gin-gonic/gin"
11 | "go.uber.org/zap"
12 | "io"
13 | "strconv"
14 | "time"
15 | )
16 |
17 | func NoTourist() gin.HandlerFunc {
18 | return func(ctx *gin.Context) {
19 | user, err := apiUtil.ContextFunc.GetUser(ctx)
20 | if err != nil {
21 | response.FailToError(ctx, err)
22 | return
23 | }
24 | isTourist, err := user.IsTourist(global.GvaDb)
25 | if err != nil {
26 | response.FailToError(ctx, err)
27 | return
28 | }
29 | if isTourist {
30 | response.FailToError(ctx, global.ErrTouristHaveNoRight)
31 | return
32 | }
33 | ctx.Next()
34 | }
35 | }
36 |
37 | func JWTAuth() gin.HandlerFunc {
38 | return func(ctx *gin.Context) {
39 | claims, err := commonService.GroupApp.ParseToken(apiUtil.ContextFunc.GetToken(ctx))
40 | if err != nil {
41 | response.TokenExpired(ctx)
42 | return
43 | }
44 | apiUtil.ContextFunc.SetClaims(claims, ctx)
45 | id, err := strconv.Atoi(claims.ID)
46 | if err != nil {
47 | response.TokenExpired(ctx)
48 | return
49 | }
50 | apiUtil.ContextFunc.SetUserId(uint(id), ctx)
51 | ctx.Next()
52 | }
53 | }
54 |
55 | func Recovery(ctx *gin.Context, err any) {
56 | body, _ := io.ReadAll(ctx.Request.Body)
57 | global.PanicLogger.Error(
58 | "[Recovery from panic]",
59 | zap.Any("error", err),
60 | zap.String("method", ctx.Request.Method),
61 | zap.String("url", ctx.Request.RequestURI),
62 | zap.Any("body", body),
63 | )
64 | response.Fail(ctx)
65 | }
66 |
67 | func RequestLogger(logger *zap.Logger) gin.HandlerFunc {
68 | return func(c *gin.Context) {
69 | start := time.Now()
70 | path := c.Request.URL.Path
71 | query := c.Request.URL.RawQuery
72 | bodyBytes, err := io.ReadAll(c.Request.Body)
73 | c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
74 | c.Next()
75 | var body string
76 | if err != nil {
77 | body = ""
78 | } else {
79 | body = string(bodyBytes)
80 | }
81 | cost := time.Since(start)
82 | logger.Info(
83 | path,
84 | zap.Int("status", c.Writer.Status()),
85 | zap.String("method", c.Request.Method),
86 | zap.String("path", path),
87 | zap.String("query", query),
88 | zap.String("body", body),
89 | zap.String("ip", c.ClientIP()),
90 | zap.String("user-agent", c.Request.UserAgent()),
91 | zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()),
92 | zap.Duration("cost", cost),
93 | )
94 | }
95 | }
96 | func AccountAuth(permission accountModel.UserPermission) gin.HandlerFunc {
97 | return func(ctx *gin.Context) {
98 | if !apiUtil.ContextFunc.CheckAccountPermissionFromParam(permission, ctx) {
99 | return
100 | }
101 | ctx.Next()
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/global/nats/outbox.go:
--------------------------------------------------------------------------------
1 | package nats
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "runtime/debug"
7 |
8 | "github.com/ZiRunHua/LeapLedger/global/cus"
9 | "github.com/ZiRunHua/LeapLedger/global/db"
10 | "github.com/ZiRunHua/LeapLedger/global/nats/manager"
11 | "gorm.io/gorm"
12 | "gorm.io/gorm/clause"
13 | )
14 |
15 | type outbox struct {
16 | Id uint
17 | ExecType outboxType
18 | Type string
19 | Payload []byte `gorm:"type:TEXT"`
20 | FailErr string `gorm:"type:TEXT"`
21 | gorm.Model
22 | }
23 | type outboxType string
24 |
25 | const outboxTypeTask outboxType = "task"
26 | const outboxTypeEvent outboxType = "event"
27 |
28 | func (o *outbox) TableName() string {
29 | return "core_outbox"
30 | }
31 | func (o *outbox) fail(db *gorm.DB, errStr string) error {
32 | return db.Model(o).Update("fail_err", errStr).Error
33 | }
34 | func (o *outbox) completeReceipt(db *gorm.DB) error {
35 | return db.Delete(o).Error
36 | }
37 |
38 | var outboxService outboxServer
39 |
40 | type outboxServer struct{}
41 |
42 | func (oServer *outboxServer) sendToOutbox(db *gorm.DB, execType outboxType, t string, data []byte) (uint, error) {
43 | o := &outbox{ExecType: execType, Type: t, Payload: data}
44 | err := db.Create(o).Error
45 | if err != nil {
46 | return 0, err
47 | }
48 | return o.Id, nil
49 | }
50 |
51 | func (oServer *outboxServer) getOutboxAndLockById(db *gorm.DB, id uint) (outbox, error) {
52 | var o outbox
53 | err := db.Clauses(clause.Locking{Strength: "UPDATE", Options: clause.LockingOptionsNoWait}).First(&o, id).Error
54 | return o, err
55 | }
56 |
57 | func (oServer *outboxServer) getHandleTransaction(t outboxType) handler[uint] {
58 | var msgHandler func(execType string, payload []byte) error
59 | switch t {
60 | case outboxTypeTask:
61 | msgHandler = func(execType string, payload []byte) error {
62 | h, err := taskManage.GetMessageHandler(manager.Task(execType))
63 | if err != nil {
64 | return err
65 | }
66 | return h(payload)
67 | }
68 | case outboxTypeEvent:
69 | msgHandler = func(execType string, payload []byte) error {
70 | if eventManage.Publish(manager.Event(execType), payload) {
71 | return nil
72 | }
73 | return errors.New("fail publish event")
74 | }
75 | }
76 | return func(id uint, ctx context.Context) error {
77 | var msgHandleErr error
78 | err := db.Transaction(
79 | ctx, func(ctx *cus.TxContext) error {
80 | tx := ctx.GetDb()
81 | o, err := oServer.getOutboxAndLockById(tx, id)
82 | if err != nil {
83 | if errors.Is(err, gorm.ErrRecordNotFound) {
84 | return nil
85 | }
86 | return err
87 | }
88 | defer func() {
89 | r := recover()
90 | if r != nil {
91 | _ = o.fail(tx, string(debug.Stack()))
92 | }
93 | }()
94 | msgHandleErr = msgHandler(o.Type, o.Payload)
95 | if msgHandleErr != nil {
96 | return o.fail(tx, msgHandleErr.Error())
97 | }
98 | return o.completeReceipt(tx)
99 | },
100 | )
101 | if err != nil {
102 | return err
103 | }
104 | return msgHandleErr
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/service/template/enter.go:
--------------------------------------------------------------------------------
1 | package templateService
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "path/filepath"
7 |
8 | "github.com/ZiRunHua/LeapLedger/global"
9 | "github.com/ZiRunHua/LeapLedger/global/constant"
10 | "github.com/ZiRunHua/LeapLedger/global/cus"
11 | "github.com/ZiRunHua/LeapLedger/global/db"
12 | "github.com/ZiRunHua/LeapLedger/global/nats"
13 | userModel "github.com/ZiRunHua/LeapLedger/model/user"
14 | _accountService "github.com/ZiRunHua/LeapLedger/service/account"
15 | _categoryService "github.com/ZiRunHua/LeapLedger/service/category"
16 | _productService "github.com/ZiRunHua/LeapLedger/service/product"
17 | _userService "github.com/ZiRunHua/LeapLedger/service/user"
18 | "github.com/ZiRunHua/LeapLedger/util"
19 | "github.com/ZiRunHua/LeapLedger/util/rand"
20 | "go.uber.org/zap"
21 | "gorm.io/gorm"
22 | )
23 |
24 | type Group struct {
25 | template
26 | }
27 |
28 | var (
29 | GroupApp = &Group{}
30 | errorLog *zap.Logger
31 | )
32 |
33 | var TmplUserId uint = 1
34 |
35 | const (
36 | TmplUserEmail = "template@gmail.com"
37 | TmplUserName = "template"
38 | )
39 |
40 | var TmplUserPassword = rand.String(15)
41 |
42 | var (
43 | templateService = GroupApp
44 | userService = _userService.GroupApp
45 | accountService = _accountService.GroupApp
46 | categoryService = _categoryService.GroupApp
47 | productService = _productService.GroupApp
48 | )
49 |
50 | func init() {
51 | var err error
52 | logPath := filepath.Clean(constant.LogPath + "/service/template/error.log")
53 | if errorLog, err = global.Config.Logger.New(logPath); err != nil {
54 | panic(err)
55 | }
56 |
57 | nats.SubscribeTaskWithPayloadAndProcessInTransaction(
58 | nats.TaskCreateTourist, func(t []byte, ctx context.Context) error {
59 | user, err := userService.CreateTourist(ctx)
60 | if err != nil {
61 | return err
62 | }
63 | _, _, err = templateService.CreateExampleAccount(user, ctx)
64 | return err
65 | },
66 | )
67 | ctx := cus.WithDb(context.Background(), db.InitDb)
68 | // init template User
69 | err = db.Transaction(ctx, initTemplateUser)
70 | if err != nil {
71 | panic(err)
72 | }
73 | initRank()
74 | }
75 |
76 | func initTemplateUser(ctx *cus.TxContext) (err error) {
77 | tx := db.Get(ctx)
78 | var user userModel.User
79 | // find user
80 | err = tx.First(&user, TmplUserId).Error
81 | if err == nil {
82 | return
83 | } else if !errors.Is(err, gorm.ErrRecordNotFound) {
84 | return
85 | }
86 | // create user
87 | option := userService.NewRegisterOption()
88 | option.WithSendEmail(false)
89 | user, err = userService.Register(
90 | userModel.AddData{
91 | Email: TmplUserEmail,
92 | Password: util.ClientPasswordHash(TmplUserEmail, TmplUserPassword),
93 | Username: TmplUserName,
94 | }, ctx,
95 | *option,
96 | )
97 | if err != nil {
98 | return
99 | }
100 | if user.ID != TmplUserId {
101 | TmplUserId = user.ID
102 | }
103 | // create account
104 | _, _, err = templateService.CreateExampleAccount(user, ctx)
105 | if err != nil {
106 | return
107 | }
108 | SetTmplUser(user)
109 | return
110 | }
111 |
112 | func SetTmplUser(user userModel.User) {
113 | TmplUserId = user.ID
114 | }
115 |
--------------------------------------------------------------------------------
/util/fileTool/file.go:
--------------------------------------------------------------------------------
1 | package fileTool
2 |
3 | import (
4 | "encoding/csv"
5 | "errors"
6 | "io"
7 | "path"
8 | "strings"
9 |
10 | "github.com/saintfish/chardet"
11 | "github.com/xuri/excelize/v2"
12 | "golang.org/x/text/encoding/charmap"
13 | "golang.org/x/text/encoding/simplifiedchinese"
14 | "golang.org/x/text/transform"
15 | "gorm.io/gorm"
16 | )
17 |
18 | func GetUTF8Reader(reader io.Reader, checkBytes []byte) (io.Reader, error) {
19 | encoding, err := DetectEncoding(checkBytes)
20 | if err != nil {
21 | return reader, err
22 | }
23 | switch encoding {
24 | case "ISO-8859-1":
25 | reader = transform.NewReader(reader, charmap.ISO8859_1.NewDecoder())
26 | case "GB-18030":
27 | reader = transform.NewReader(reader, simplifiedchinese.GBK.NewDecoder())
28 | }
29 | return reader, nil
30 | }
31 |
32 | func DetectEncoding(data []byte) (string, error) {
33 | detector := chardet.NewTextDetector()
34 | result, err := detector.DetectBest(data)
35 | if err != nil {
36 | return "", err
37 | }
38 | return result.Charset, nil
39 | }
40 |
41 | func GetFileSuffix(filename string) string {
42 | return path.Ext(filename)
43 | }
44 |
45 | func NewRowReader(reader io.Reader, suffix string) (func(yield func([]string) bool), error) {
46 | switch suffix {
47 | case ".csv":
48 | return IteratorsHandleCSVReader(reader)
49 | case ".excel":
50 | return IteratorsHandleEXCELReader(reader)
51 | default:
52 | return nil, errors.New("不支持该文件类型")
53 | }
54 | }
55 |
56 | func IteratorsHandleCSVReader(reader io.Reader) (func(yield func([]string) bool), error) {
57 | return func(yield func([]string) bool) {
58 | csvReader := csv.NewReader(reader)
59 | for {
60 | row, err := csvReader.Read()
61 | if err == io.EOF {
62 | return
63 | }
64 | if err != nil && !errors.Is(err, csv.ErrFieldCount) {
65 | panic(err)
66 | }
67 | if !yield(row) {
68 | return
69 | }
70 | }
71 | }, nil
72 | }
73 |
74 | // 迭代器处理EXCEL 会跳过空行
75 | func IteratorsHandleEXCELReader(reader io.Reader) (func(yield func([]string) bool), error) {
76 | file, err := excelize.OpenReader(reader)
77 | if err != nil {
78 | return nil, err
79 | }
80 | rows, err := file.Rows(file.GetSheetName(1))
81 | if err != nil {
82 | return nil, err
83 | }
84 | return func(yield func([]string) bool) {
85 | defer func() {
86 | err = rows.Close()
87 | if err != nil {
88 | panic(err)
89 | }
90 | }()
91 | var row []string
92 | var err error
93 | for rows.Next() {
94 | row, err = rows.Columns()
95 | if err != nil {
96 | return
97 | }
98 | if len(row) == 0 {
99 | continue
100 | }
101 | if !yield(row) {
102 | return
103 | }
104 | }
105 | }, nil
106 | }
107 |
108 | func ExecSqlFile(reader io.Reader, db *gorm.DB) error {
109 | sqlBytes, err := io.ReadAll(reader)
110 | if err != nil {
111 | return err
112 | }
113 |
114 | sqlStatements := strings.Split(string(sqlBytes), ";")
115 | for _, stmt := range sqlStatements {
116 | trimmedStmt := strings.TrimSpace(stmt)
117 | if len(trimmedStmt) > 0 {
118 | if err = db.Exec(trimmedStmt).Error; err != nil {
119 | return err
120 | }
121 | }
122 | }
123 | return nil
124 | }
125 |
--------------------------------------------------------------------------------
/service/product/bill/aliPay.go:
--------------------------------------------------------------------------------
1 | package bill
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "strconv"
7 | "strings"
8 | "time"
9 |
10 | "github.com/ZiRunHua/LeapLedger/global/constant"
11 | transactionModel "github.com/ZiRunHua/LeapLedger/model/transaction"
12 | "github.com/araddon/dateparse"
13 | )
14 |
15 | type AliPayReader struct {
16 | aliPayTransactionReader
17 | }
18 |
19 | type aliPayTransactionReader interface {
20 | TransactionReader
21 | checkOrderStatus() bool
22 | setTransCategory() error
23 | setAmount() error
24 | setRemark()
25 | setTradeTime() error
26 | }
27 |
28 | func (r *AliPayReader) readTransaction(t *ReaderTemplate) (bool, error) {
29 | t.currentTransaction = transactionModel.Info{}
30 | if !r.checkOrderStatus(t) {
31 | return true, nil
32 | }
33 | r.setRemark(t)
34 | var execErr error
35 | err := r.setTransCategory(t)
36 | if err != nil {
37 | if errors.Is(err, ErrCategoryCannotRead) {
38 | return true, nil
39 | }
40 | execErr = err
41 | }
42 | err = r.setAmount(t)
43 | if err != nil {
44 | execErr = err
45 | }
46 | err = r.setTradeTime(t)
47 | if err != nil {
48 | execErr = errors.New("读取交易时间错误:" + err.Error())
49 | }
50 | return false, execErr
51 | }
52 |
53 | func (r *AliPayReader) checkOrderStatus(t *ReaderTemplate) bool {
54 | status := strings.TrimSpace(t.currentRow[t.transDataMapping.TransStatus])
55 | if status != "交易成功" {
56 | return false
57 | }
58 | return true
59 | }
60 |
61 | func (r *AliPayReader) setTransCategory(t *ReaderTemplate) error {
62 | incomeExpenseStr := strings.TrimSpace(t.currentRow[t.transDataMapping.IncomeExpense])
63 | var incomeExpense constant.IncomeExpense
64 | if incomeExpenseStr == "收入" {
65 | incomeExpense = constant.Income
66 | } else if incomeExpenseStr == "支出" {
67 | incomeExpense = constant.Expense
68 | } else {
69 | return ErrCategoryCannotRead
70 | }
71 | name := strings.TrimSpace(t.currentRow[t.transDataMapping.TransCategory])
72 | ptc, exist := t.ptcMapping[incomeExpense][name]
73 | if exist == false {
74 | return ErrCategoryReadFail
75 | }
76 | mapping, exist := t.ptcIdToMapping[ptc.ID]
77 | if exist == false {
78 | return fmt.Errorf("类型\"%s\"%v", name, ErrCategoryMappingNotExist)
79 | }
80 | t.currentTransaction.IncomeExpense = incomeExpense
81 | t.currentTransaction.CategoryId = mapping.CategoryId
82 | return nil
83 | }
84 |
85 | func (r *AliPayReader) setAmount(t *ReaderTemplate) error {
86 | amountFloat, err := strconv.ParseFloat(t.currentRow[t.transDataMapping.Amount], 64)
87 | if err != nil {
88 | return err
89 | }
90 | t.currentTransaction.Amount = int(amountFloat) * 100
91 | return nil
92 | }
93 |
94 | func (r *AliPayReader) setRemark(t *ReaderTemplate) {
95 | t.currentTransaction.Remark = strings.TrimSpace(t.currentRow[t.transDataMapping.Remark])
96 | }
97 |
98 | func (r *AliPayReader) setTradeTime(t *ReaderTemplate) error {
99 | layout, err := dateparse.ParseFormat(t.currentRow[t.transDataMapping.TradeTime])
100 | if err != nil {
101 | return err
102 | }
103 | t.currentTransaction.TradeTime, err = time.ParseInLocation(
104 | layout,
105 | t.currentRow[t.transDataMapping.TradeTime], t.location,
106 | )
107 | return err
108 | }
109 |
--------------------------------------------------------------------------------
/service/product/bill/billInfo.go:
--------------------------------------------------------------------------------
1 | package bill
2 |
3 | import (
4 | "context"
5 | "github.com/ZiRunHua/LeapLedger/global/constant"
6 | "github.com/ZiRunHua/LeapLedger/global/db"
7 | accountModel "github.com/ZiRunHua/LeapLedger/model/account"
8 | productModel "github.com/ZiRunHua/LeapLedger/model/product"
9 | "github.com/ZiRunHua/LeapLedger/util/dataTool"
10 | "github.com/pkg/errors"
11 | "strings"
12 | "time"
13 | )
14 |
15 | type BillInfo struct {
16 | info productModel.Bill
17 | location *time.Location
18 | billHeaders []productModel.BillHeader
19 | ptcMapping map[constant.IncomeExpense]map[string]productModel.TransactionCategory
20 | transDataMapping transactionDataColumnMapping
21 | ptcIdToMapping map[uint]productModel.TransactionCategoryMapping
22 | status billStatus
23 | }
24 |
25 | type billStatus string
26 |
27 | const statusOfReadInHead, statusOfReadInTransaction billStatus = "read head", "read transaction"
28 |
29 | func (b *BillInfo) init(bill productModel.Bill, account accountModel.Account, ctx context.Context) error {
30 | b.info, b.location, b.status = bill, account.GetTimeLocation(), statusOfReadInHead
31 | dao := productModel.NewDao(db.Get(ctx))
32 | var err error
33 | b.ptcIdToMapping, err = dao.GetPtcIdMapping(account.ID, bill.ProductKey)
34 | if err != nil {
35 | return err
36 | }
37 | b.ptcMapping, err = dao.GetIncomeExpenseAndNameMap(bill.ProductKey)
38 | if err != nil {
39 | return err
40 | }
41 | b.billHeaders, err = productModel.NewDao(db.Get(ctx)).GetBillHeaderList(b.info.ProductKey)
42 | if err != nil {
43 | return err
44 | }
45 | if len(b.billHeaders) == 0 {
46 | panic("The bill header is not configured")
47 | }
48 | return nil
49 | }
50 |
51 | func (b *BillInfo) setTransDataMapping(header []string, _ context.Context) error {
52 | headerMappedToPtc := dataTool.ToMap(
53 | b.billHeaders, func(v productModel.BillHeader) string {
54 | return v.Name
55 | },
56 | )
57 | headerTypeMappedToColumn := map[productModel.BillHeaderType]int{}
58 | for index, name := range header {
59 | name = strings.TrimSpace(name)
60 | if _, exist := headerMappedToPtc[name]; exist == true {
61 | headerTypeMappedToColumn[headerMappedToPtc[name].Type] = index
62 | }
63 | }
64 |
65 | needHeader := []productModel.BillHeaderType{
66 | productModel.TransCategory, productModel.IncomeExpense, productModel.Amount, productModel.Remark,
67 | productModel.TransTime, productModel.OrderNumber, productModel.TransStatus,
68 | }
69 | for i := range needHeader {
70 | if _, exist := headerTypeMappedToColumn[needHeader[i]]; exist == false {
71 | return errors.Wrap(errors.New(string(needHeader[i]+"数据缺失")), "setTransMapping")
72 | }
73 | }
74 | b.transDataMapping = transactionDataColumnMapping{
75 | OrderNumber: headerTypeMappedToColumn[productModel.OrderNumber],
76 | TransCategory: headerTypeMappedToColumn[productModel.TransCategory],
77 | IncomeExpense: headerTypeMappedToColumn[productModel.IncomeExpense],
78 | Amount: headerTypeMappedToColumn[productModel.Amount],
79 | Remark: headerTypeMappedToColumn[productModel.Remark],
80 | TradeTime: headerTypeMappedToColumn[productModel.TransTime],
81 | TransStatus: headerTypeMappedToColumn[productModel.TransStatus],
82 | }
83 | return nil
84 | }
85 |
--------------------------------------------------------------------------------
/service/thirdparty/ai.go:
--------------------------------------------------------------------------------
1 | package thirdpartyService
2 |
3 | import (
4 | "context"
5 | "strings"
6 |
7 | "github.com/ZiRunHua/LeapLedger/global"
8 | "github.com/go-resty/resty/v2"
9 | )
10 |
11 | const AI_SERVER_NAME = "AI"
12 | const API_SIMILARITY_MATCHING = "/similarity/matching"
13 |
14 | type aiServer struct{}
15 |
16 | func (as *aiServer) getBaseUrl() string {
17 | return global.Config.ThirdParty.Ai.GetPortalSite()
18 | }
19 |
20 | func (as *aiServer) IsOpen() bool {
21 | return global.Config.ThirdParty.Ai.IsOpen()
22 | }
23 |
24 | func (as *aiServer) ChineseSimilarityMatching(sourceStr string, targetList []string, ctx context.Context) (
25 | target string, err error,
26 | ) {
27 | if false == as.IsOpen() {
28 | for _, targetStr := range targetList {
29 | if strings.Compare(sourceStr, targetStr) == 0 {
30 | return targetStr, nil
31 | }
32 | }
33 | return target, nil
34 | }
35 | responseData, err := as.requestChineseSimilarity([]string{sourceStr}, targetList, ctx)
36 | if err != nil {
37 | return
38 | }
39 | minSimilarity := global.Config.ThirdParty.Ai.MinSimilarity
40 | if len(responseData) > 0 && responseData[0].Similarity >= minSimilarity {
41 | return responseData[0].Target, nil
42 | }
43 | return
44 | }
45 |
46 | func (as *aiServer) BatchChineseSimilarityMatching(sourceList, targetList []string, ctx context.Context) (
47 | map[string]string, error,
48 | ) {
49 | if false == as.IsOpen() {
50 | targetNameMap := make(map[string]struct{})
51 | for _, targetStr := range targetList {
52 | targetNameMap[targetStr] = struct{}{}
53 | }
54 | result := make(map[string]string)
55 | for _, sourceStr := range sourceList {
56 | if _, exist := targetNameMap[sourceStr]; !exist {
57 | continue
58 | }
59 | result[sourceStr] = sourceStr
60 | }
61 | return result, nil
62 | }
63 | responseData, err := as.requestChineseSimilarity(sourceList, targetList, ctx)
64 | if err != nil {
65 | return nil, err
66 | }
67 |
68 | minSimilarity, result := global.Config.ThirdParty.Ai.MinSimilarity, make(map[string]string)
69 | for _, item := range responseData {
70 | if item.Similarity >= minSimilarity {
71 | result[item.Source] = item.Target
72 | }
73 | }
74 | return result, nil
75 | }
76 |
77 | type chineseSimilarityResponse []struct {
78 | Source, Target string
79 | Similarity float32
80 | }
81 |
82 | func (as *aiServer) requestChineseSimilarity(
83 | SourceList, TargetList []string,
84 | ctx context.Context) (chineseSimilarityResponse, error) {
85 | var response struct {
86 | aiApiResponse
87 | Data chineseSimilarityResponse
88 | }
89 | _, err := resty.New().R().SetContext(ctx).SetBody(
90 | map[string]interface{}{
91 | "SourceData": SourceList, "TargetData": TargetList,
92 | },
93 | ).SetResult(&response).Post(as.getBaseUrl() + API_SIMILARITY_MATCHING)
94 |
95 | if err != nil {
96 | return nil, err
97 | }
98 | if false == response.isSuccess() {
99 | return nil, global.NewErrThirdpartyApi(AI_SERVER_NAME, response.Msg)
100 | }
101 | return response.Data, nil
102 | }
103 |
104 | type aiApiResponse struct {
105 | Code int
106 | Msg string
107 | }
108 |
109 | func (a *aiApiResponse) isSuccess() bool { return a.Code == 200 }
110 |
--------------------------------------------------------------------------------
/service/thirdparty/email.go:
--------------------------------------------------------------------------------
1 | package thirdpartyService
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "os"
7 | "path/filepath"
8 | "time"
9 |
10 | "github.com/ZiRunHua/LeapLedger/global"
11 | "github.com/ZiRunHua/LeapLedger/global/constant"
12 | userModel "github.com/ZiRunHua/LeapLedger/model/user"
13 | commonService "github.com/ZiRunHua/LeapLedger/service/common"
14 | "github.com/ZiRunHua/LeapLedger/util/rand"
15 | "github.com/pkg/errors"
16 | )
17 |
18 | var emailTemplate map[constant.Notification][]byte
19 | var emailTemplateFilePath = map[constant.Notification]string{
20 | constant.NotificationOfCaptcha: filepath.Clean("/template/email/captcha.html"),
21 | constant.NotificationOfRegistrationSuccess: filepath.Clean("/template/email/registerSuccess.html"),
22 | constant.NotificationOfUpdatePassword: filepath.Clean("/template/email/updatePassword.html"),
23 | }
24 |
25 | func init() {
26 | emailTemplate = make(map[constant.Notification][]byte, len(emailTemplateFilePath))
27 | var err error
28 | for notification, path := range emailTemplateFilePath {
29 | if emailTemplate[notification], err = os.ReadFile(filepath.Clean(constant.DataPath + path)); err != nil {
30 | panic(err)
31 | }
32 | }
33 | }
34 |
35 | func (g *Group) sendCaptchaEmail(email string, action constant.UserAction) error {
36 | captcha := rand.String(6)
37 | expirationTime := time.Second * time.Duration(global.Config.Captcha.EmailCaptchaTimeOut)
38 | err := commonService.Common.SetEmailCaptchaCache(email, captcha, expirationTime)
39 | if err != nil {
40 | return err
41 | }
42 | minutes := int(expirationTime.Minutes())
43 | content := bytes.Replace(emailTemplate[constant.NotificationOfCaptcha], []byte("[Captcha]"), []byte(captcha), 1)
44 | content = bytes.Replace(content, []byte("[ExpirationTime]"), []byte(fmt.Sprintf("%d分钟", minutes)), 1)
45 | var actionName string
46 | switch action {
47 | case constant.Register:
48 | actionName = "注册"
49 | case constant.ForgetPassword:
50 | actionName = "忘记密码"
51 | case constant.UpdatePassword:
52 | actionName = "修改密码"
53 | default:
54 | return errors.Wrap(global.ErrUnsupportedUserAction, "发送邮箱验证码")
55 | }
56 | content = bytes.Replace(content, []byte("[Action]"), []byte(actionName), 2)
57 | return emailServer.Send([]string{email}, actionName+"验证码", string(content))
58 | }
59 |
60 | func (g *Group) sendRegisterSuccessEmail(user userModel.User) error {
61 | content := bytes.Replace(
62 | emailTemplate[constant.NotificationOfRegistrationSuccess], []byte("[username]"), []byte(user.Username), 1,
63 | )
64 | return emailServer.Send([]string{user.Email}, "注册成功", string(content))
65 | }
66 |
67 | func (g *Group) sendUpdatePasswordEmail(user userModel.User) error {
68 | content := bytes.Replace(
69 | emailTemplate[constant.NotificationOfUpdatePassword], []byte("[username]"), []byte(user.Username), 1,
70 | )
71 | return emailServer.Send([]string{user.Email}, "修改密码", string(content))
72 | }
73 |
74 | func (g *Group) sendNotificationEmail(user userModel.User, notification constant.Notification) error {
75 | switch notification {
76 | case constant.NotificationOfUpdatePassword:
77 | return g.sendUpdatePasswordEmail(user)
78 | case constant.NotificationOfRegistrationSuccess:
79 | return g.sendRegisterSuccessEmail(user)
80 | default:
81 | return errors.New("不支持该类型邮箱通知")
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/global/constant/constant.go:
--------------------------------------------------------------------------------
1 | package constant
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "os/exec"
7 | "path/filepath"
8 | "strings"
9 | )
10 |
11 | type ServerMode string
12 |
13 | const Debug, Production ServerMode = "debug", "production"
14 |
15 | var (
16 | RootDir = getRootDir()
17 | LogPath = filepath.Join(RootDir, "log")
18 | DataPath = filepath.Join(RootDir, "data")
19 | ExampleAccountJsonPath = filepath.Clean(DataPath + "/template/account/example.json")
20 | )
21 |
22 | // IncomeExpense 收支类型
23 | type IncomeExpense string // @name IncomeExpense `example:"expense" enums:"income,expense" swaggertype:"string"`
24 |
25 | const (
26 | Income IncomeExpense = "income"
27 | Expense IncomeExpense = "expense"
28 | )
29 |
30 | func (ie *IncomeExpense) QueryIncome() bool {
31 | return ie == nil || *ie == Income
32 | }
33 |
34 | func (ie *IncomeExpense) QueryExpense() bool {
35 | return ie == nil || *ie == Expense
36 | }
37 |
38 | // Client 客户端
39 | type Client string
40 |
41 | var ClientList = []Client{Web, Android, Ios}
42 |
43 | const (
44 | Web Client = "web"
45 | Android Client = "android"
46 | Ios Client = "ios"
47 | )
48 |
49 | type Encoding string
50 |
51 | const (
52 | GBK Encoding = "GBK"
53 | UTF8 Encoding = "UTF8"
54 | )
55 |
56 | type UserAction string
57 |
58 | const (
59 | Login UserAction = "login"
60 | Register UserAction = "register"
61 | ForgetPassword UserAction = "forgetPassword"
62 | UpdatePassword UserAction = "updatePassword"
63 | )
64 |
65 | type CacheTab string
66 |
67 | const (
68 | LoginFailCount CacheTab = "loginFailCount"
69 | EmailCaptcha CacheTab = "emailCaptcha"
70 | CaptchaEmailErrorCount CacheTab = "captchaEmailErrorCount"
71 | )
72 |
73 | type Notification int
74 |
75 | const (
76 | NotificationOfCaptcha Notification = iota
77 | NotificationOfRegistrationSuccess Notification = iota
78 | NotificationOfUpdatePassword Notification = iota
79 | )
80 |
81 | type LogOperation string
82 |
83 | const (
84 | LogOperationOfAdd LogOperation = "add"
85 | LogOperationOfUpdate LogOperation = "update"
86 | LogOperationOfDelete LogOperation = "delete"
87 | )
88 |
89 | // nats
90 |
91 | type Subject string
92 |
93 | func getRootDir() string {
94 | // `os.Getwd()` is avoided here because, during tests, the working directory is set to the test file’s directory.
95 | // This command retrieves the module's root directory instead.
96 | // Source of `go list` usage: https://stackoverflow.com/a/75943840/23658318
97 | rootDir, err := exec.Command("go", "list", "-m", "-f", "{{.Dir}}").Output()
98 | if err == nil {
99 | return strings.TrimSpace(string(rootDir))
100 | }
101 | // If `go list` fails, it may indicate the absence of a Go environment.
102 | // In such cases, this suggests we are not in a test environment, so fall back to `os.Getwd()` to set `RootDir`.
103 | workDir, err := os.Getwd()
104 | if err != nil {
105 | panic(err)
106 | }
107 | // Validate that the directory exists
108 | _, err = os.Stat(workDir)
109 | if err != nil {
110 | if os.IsNotExist(err) {
111 | panic(fmt.Sprintf("Path:%s does not exists", workDir))
112 | }
113 | panic(err)
114 | }
115 | return workDir
116 | }
117 |
--------------------------------------------------------------------------------
/service/product/bill/weChatPay.go:
--------------------------------------------------------------------------------
1 | package bill
2 |
3 | import (
4 | "errors"
5 | "strconv"
6 | "strings"
7 | "time"
8 |
9 | "github.com/ZiRunHua/LeapLedger/global/constant"
10 | transactionModel "github.com/ZiRunHua/LeapLedger/model/transaction"
11 |
12 | "github.com/araddon/dateparse"
13 | )
14 |
15 | type WeChatPayReader struct {
16 | weChatPayTransactionReader
17 | }
18 |
19 | type weChatPayTransactionReader interface {
20 | TransactionReader
21 | checkOrderStatus() bool
22 | setTransCategory() error
23 | setAmount() error
24 | setRemark()
25 | setTradeTime() error
26 | }
27 |
28 | func (r *WeChatPayReader) readTransaction(t *ReaderTemplate) (bool, error) {
29 | t.currentTransaction = transactionModel.Info{}
30 | if !r.checkOrderStatus(t) {
31 | return true, nil
32 | }
33 | r.setRemark(t)
34 | var execErr error
35 | err := r.setTransCategory(t)
36 | if err != nil {
37 | if errors.Is(err, ErrCategoryCannotRead) {
38 | return true, nil
39 | }
40 | execErr = err
41 | }
42 | err = r.setAmount(t)
43 | if err != nil {
44 | execErr = err
45 | }
46 | err = r.setTradeTime(t)
47 | if err != nil {
48 | execErr = errors.New("读取交易时间错误:" + err.Error())
49 | }
50 | return false, execErr
51 | }
52 |
53 | func (r *WeChatPayReader) checkOrderStatus(t *ReaderTemplate) bool {
54 | status := strings.TrimSpace(t.currentRow[t.transDataMapping.TransStatus])
55 | if status != "支付成功" && status != "已转账" && status != "已收钱" {
56 | return false
57 | }
58 | return true
59 | }
60 |
61 | func (r *WeChatPayReader) setTransCategory(t *ReaderTemplate) error {
62 | incomeExpenseStr := strings.TrimSpace(t.currentRow[t.transDataMapping.IncomeExpense])
63 | var incomeExpense constant.IncomeExpense
64 | if incomeExpenseStr == "收入" {
65 | incomeExpense = constant.Income
66 | } else if incomeExpenseStr == "支出" {
67 | incomeExpense = constant.Expense
68 | } else {
69 | return ErrCategoryCannotRead
70 | }
71 | name := strings.TrimSpace(t.currentRow[t.transDataMapping.TransCategory])
72 | ptc, exist := t.ptcMapping[incomeExpense][name]
73 | if exist == false {
74 | return ErrCategoryReadFail
75 | }
76 | mapping, exist := t.ptcIdToMapping[ptc.ID]
77 | if exist == false {
78 | return ErrCategoryMappingNotExist
79 | }
80 | t.currentTransaction.IncomeExpense = incomeExpense
81 | t.currentTransaction.CategoryId = mapping.CategoryId
82 | return nil
83 | }
84 |
85 | func (r *WeChatPayReader) setAmount(t *ReaderTemplate) error {
86 | amountStr := strings.TrimLeft(t.currentRow[t.transDataMapping.Amount], "¥")
87 | amountFloat, err := strconv.ParseFloat(amountStr, 64)
88 | if err != nil {
89 | return err
90 | } else {
91 | t.currentTransaction.Amount = int(amountFloat) * 100
92 | }
93 | return nil
94 | }
95 |
96 | func (r *WeChatPayReader) setRemark(t *ReaderTemplate) {
97 | t.currentTransaction.Remark = strings.TrimSpace(t.currentRow[t.transDataMapping.Remark])
98 | }
99 |
100 | func (r *WeChatPayReader) setTradeTime(t *ReaderTemplate) error {
101 | layout, err := dateparse.ParseFormat(t.currentRow[t.transDataMapping.TradeTime])
102 | if err != nil {
103 | return err
104 | }
105 | t.currentTransaction.TradeTime, err = time.ParseInLocation(
106 | layout,
107 | t.currentRow[t.transDataMapping.TradeTime], t.location,
108 | )
109 | return err
110 | }
111 |
--------------------------------------------------------------------------------