🎉 Initial Commit
Some checks failed
release-nightly / build-docker (push) Has been cancelled

This commit is contained in:
2025-01-25 18:33:11 +08:00
commit 4cf3742a75
25 changed files with 1703 additions and 0 deletions

24
pkg/internal/cache/store.go vendored Normal file
View File

@ -0,0 +1,24 @@
package cache
import (
"github.com/dgraph-io/ristretto"
"github.com/eko/gocache/lib/v4/store"
ristrettoCache "github.com/eko/gocache/store/ristretto/v4"
)
var S store.StoreInterface
func NewStore() error {
ristretto, err := ristretto.NewCache(&ristretto.Config{
NumCounters: 1000,
MaxCost: 100,
BufferItems: 64,
})
if err != nil {
return err
}
S = ristrettoCache.NewRistretto(ristretto)
return nil
}

View File

@ -0,0 +1,21 @@
package database
import (
"git.solsynth.dev/hypernet/wallet/pkg/internal/models"
"gorm.io/gorm"
)
var AutoMaintainRange = []any{
&models.Wallet{},
&models.Transaction{},
}
func RunMigration(source *gorm.DB) error {
if err := source.AutoMigrate(
AutoMaintainRange...,
); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,27 @@
package database
import (
"git.solsynth.dev/hypernet/nexus/pkg/nex/cruda"
"git.solsynth.dev/hypernet/wallet/pkg/internal/gap"
"github.com/rs/zerolog/log"
"github.com/samber/lo"
"github.com/spf13/viper"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var C *gorm.DB
func NewGorm() error {
var err error
dsn, err := cruda.NewCrudaConn(gap.Nx).AllocDatabase("wallet")
C, err = gorm.Open(postgres.Open(dsn), &gorm.Config{Logger: logger.New(&log.Logger, logger.Config{
Colorful: true,
IgnoreRecordNotFoundError: true,
LogLevel: lo.Ternary(viper.GetBool("debug.database"), logger.Info, logger.Silent),
})})
return err
}

View File

@ -0,0 +1,44 @@
package gap
import (
"fmt"
"strings"
"git.solsynth.dev/hypernet/nexus/pkg/nex"
"git.solsynth.dev/hypernet/nexus/pkg/proto"
"github.com/rs/zerolog/log"
"github.com/samber/lo"
"github.com/spf13/viper"
)
var Nx *nex.Conn
func InitializeToNexus() error {
grpcBind := strings.SplitN(viper.GetString("grpc_bind"), ":", 2)
httpBind := strings.SplitN(viper.GetString("bind"), ":", 2)
outboundIp, _ := nex.GetOutboundIP()
grpcOutbound := fmt.Sprintf("%s:%s", outboundIp, grpcBind[1])
httpOutbound := fmt.Sprintf("%s:%s", outboundIp, httpBind[1])
var err error
Nx, err = nex.NewNexusConn(viper.GetString("nexus_addr"), &proto.ServiceInfo{
Id: viper.GetString("id"),
Type: "wa",
Label: "Wallet",
GrpcAddr: grpcOutbound,
HttpAddr: lo.ToPtr("http://" + httpOutbound + "/api"),
})
if err == nil {
go func() {
err := Nx.RunRegistering()
if err != nil {
log.Error().Err(err).Msg("An error occurred while registering service...")
}
}()
}
return err
}

View File

@ -0,0 +1,26 @@
package grpc
import (
"context"
health "google.golang.org/grpc/health/grpc_health_v1"
"time"
)
func (v *Server) Check(ctx context.Context, request *health.HealthCheckRequest) (*health.HealthCheckResponse, error) {
return &health.HealthCheckResponse{
Status: health.HealthCheckResponse_SERVING,
}, nil
}
func (v *Server) Watch(request *health.HealthCheckRequest, server health.Health_WatchServer) error {
for {
if server.Send(&health.HealthCheckResponse{
Status: health.HealthCheckResponse_SERVING,
}) != nil {
break
}
time.Sleep(1000 * time.Millisecond)
}
return nil
}

View File

@ -0,0 +1,40 @@
package grpc
import (
"net"
"git.solsynth.dev/hypernet/nexus/pkg/proto"
"github.com/spf13/viper"
"google.golang.org/grpc"
health "google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/grpc/reflection"
)
type Server struct {
proto.UnimplementedDirectoryServiceServer
health.UnimplementedHealthServer
srv *grpc.Server
}
func NewGrpc() *Server {
server := &Server{
srv: grpc.NewServer(),
}
proto.RegisterDirectoryServiceServer(server.srv, server)
health.RegisterHealthServer(server.srv, server)
reflection.Register(server.srv)
return server
}
func (v *Server) Listen() error {
listener, err := net.Listen("tcp", viper.GetString("grpc_bind"))
if err != nil {
return err
}
return v.srv.Serve(listener)
}

View File

@ -0,0 +1,42 @@
package grpc
import (
"context"
"git.solsynth.dev/hypernet/nexus/pkg/nex"
"strconv"
"git.solsynth.dev/hypernet/nexus/pkg/proto"
"git.solsynth.dev/hypernet/wallet/pkg/internal/database"
)
func (v *Server) BroadcastEvent(ctx context.Context, in *proto.EventInfo) (*proto.EventResponse, error) {
switch in.GetEvent() {
case "deletion":
data := nex.DecodeMap(in.GetData())
resType, ok := data["type"].(string)
if !ok {
break
}
switch resType {
case "account":
id, ok := data["id"].(string)
if !ok {
break
}
numericId, err := strconv.Atoi(id)
if err != nil {
break
}
tx := database.C.Begin()
for _, model := range database.AutoMaintainRange {
switch model.(type) {
default:
tx.Delete(model, "account_id = ?", numericId)
}
}
tx.Commit()
}
}
return &proto.EventResponse{}, nil
}

5
pkg/internal/meta.go Normal file
View File

@ -0,0 +1,5 @@
package pkg
const (
AppVersion = "1.0.0"
)

View File

@ -0,0 +1,17 @@
package models
import (
"git.solsynth.dev/hypernet/nexus/pkg/nex/cruda"
"github.com/shopspring/decimal"
)
type Transaction struct {
cruda.BaseModel
Remark string `json:"remark"` // The usage of this transaction
Amount decimal.Decimal `json:"amount" type:"decimal(30,2);"`
Payer *Wallet `json:"payer"` // Who give the money
Payee *Wallet `json:"payee"` // Who get the money
PayerID *uint `json:"payer_id"` // Leave this field as nil means pay from the system
PayeeID *uint `json:"payee_id"` // Leave this field as nil means pay to the system
}

View File

@ -0,0 +1,14 @@
package models
import (
"git.solsynth.dev/hypernet/nexus/pkg/nex/cruda"
"github.com/shopspring/decimal"
)
type Wallet struct {
cruda.BaseModel
Transactions []Transaction `json:"transactions"`
Balance decimal.Decimal `json:"amount" sql:"type:decimal(30,2);"`
AccountID uint `json:"account_id"`
}

View File

@ -0,0 +1,20 @@
package api
import (
"github.com/gofiber/fiber/v2"
)
func MapAPIs(app *fiber.App, baseURL string) {
api := app.Group(baseURL).Name("API")
{
wallet := api.Group("/wallet").Name("Wallet API")
{
wallet.Get("/me", getMyWallet)
}
transaction := api.Group("/transaction").Name("Transaction API")
{
transaction.Get("/me", getTransaction)
}
}
}

View File

@ -0,0 +1,41 @@
package api
import (
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
"git.solsynth.dev/hypernet/wallet/pkg/internal/database"
"git.solsynth.dev/hypernet/wallet/pkg/internal/models"
"github.com/gofiber/fiber/v2"
)
func getTransaction(c *fiber.Ctx) error {
take := c.QueryInt("take", 0)
offset := c.QueryInt("offset", 0)
if err := sec.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(*sec.UserInfo)
var wallet models.Wallet
if err := database.C.Where("account_id = ?", user.ID).First(&wallet).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
var count int64
if err := database.C.Model(&models.Transaction{}).Where("payer_id = ? OR payee_id = ?", user.ID, user.ID).
Count(&count).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
var transactions []models.Transaction
if err := database.C.Where("payer_id = ? OR payee_id = ?", user.ID, user.ID).
Limit(take).Offset(offset).
Find(&transactions).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return c.JSON(fiber.Map{
"count": count,
"data": transactions,
})
}

View File

@ -0,0 +1,22 @@
package api
import (
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
"git.solsynth.dev/hypernet/wallet/pkg/internal/database"
"git.solsynth.dev/hypernet/wallet/pkg/internal/models"
"github.com/gofiber/fiber/v2"
)
func getMyWallet(c *fiber.Ctx) error {
if err := sec.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(*sec.UserInfo)
var wallet models.Wallet
if err := database.C.Where("account_id = ?", user.ID).First(&wallet).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
return c.JSON(wallet)
}

View File

@ -0,0 +1,18 @@
package exts
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
)
var validation = validator.New(validator.WithRequiredStructEnabled())
func BindAndValidate(c *fiber.Ctx, out any) error {
if err := c.BodyParser(out); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else if err := validation.Struct(out); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return nil
}

View File

@ -0,0 +1,71 @@
package server
import (
"strings"
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
"git.solsynth.dev/hypernet/wallet/pkg/internal/server/api"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/idempotency"
"github.com/gofiber/fiber/v2/middleware/logger"
jsoniter "github.com/json-iterator/go"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
)
var IReader *sec.InternalTokenReader
type App struct {
app *fiber.App
}
func NewServer() *App {
app := fiber.New(fiber.Config{
DisableStartupMessage: true,
EnableIPValidation: true,
ServerHeader: "Hypernet.Wallet",
AppName: "Hypernet.Wallet",
ProxyHeader: fiber.HeaderXForwardedFor,
JSONEncoder: jsoniter.ConfigCompatibleWithStandardLibrary.Marshal,
JSONDecoder: jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal,
BodyLimit: 512 * 1024 * 1024 * 1024, // 512 TiB
EnablePrintRoutes: viper.GetBool("debug.print_routes"),
})
app.Use(idempotency.New())
app.Use(cors.New(cors.Config{
AllowCredentials: true,
AllowMethods: strings.Join([]string{
fiber.MethodGet,
fiber.MethodPost,
fiber.MethodHead,
fiber.MethodOptions,
fiber.MethodPut,
fiber.MethodDelete,
fiber.MethodPatch,
}, ","),
AllowOriginsFunc: func(origin string) bool {
return true
},
}))
app.Use(logger.New(logger.Config{
Format: "${status} | ${latency} | ${method} ${path}\n",
Output: log.Logger,
}))
app.Use(sec.ContextMiddleware(IReader))
api.MapAPIs(app, "/api")
return &App{app}
}
func (v *App) Listen() {
if err := v.app.Listen(viper.GetString("bind")); err != nil {
log.Fatal().Err(err).Msg("An error occurred when starting server...")
}
}

View File

@ -0,0 +1,24 @@
package services
import (
database2 "git.solsynth.dev/hypernet/wallet/pkg/internal/database"
"time"
"github.com/rs/zerolog/log"
)
func DoAutoDatabaseCleanup() {
deadline := time.Now().Add(60 * time.Minute)
log.Debug().Time("deadline", deadline).Msg("Now cleaning up entire database...")
var count int64
for _, model := range database2.AutoMaintainRange {
tx := database2.C.Unscoped().Delete(model, "deleted_at >= ?", deadline)
if tx.Error != nil {
log.Error().Err(tx.Error).Msg("An error occurred when running auth context cleanup...")
}
count += tx.RowsAffected
}
log.Debug().Int64("affected", count).Msg("Clean up entire database accomplished.")
}