Files
backend/cloudprint-backend/cmd/server/main.go

467 lines
14 KiB
Go
Raw Normal View History

package main
import (
2026-04-28 20:26:52 +08:00
"context"
"encoding/json"
"flag"
"fmt"
"log"
"net"
2026-04-28 20:26:52 +08:00
"net/http"
"os"
"os/signal"
"syscall"
"github.com/cloudprint/cloudprint-backend/config"
"github.com/cloudprint/cloudprint-backend/internal/grpc/interceptor"
2026-04-28 20:26:52 +08:00
grpcService "github.com/cloudprint/cloudprint-backend/internal/grpc/service"
"github.com/cloudprint/cloudprint-backend/internal/repository"
"github.com/cloudprint/cloudprint-backend/internal/service"
"github.com/cloudprint/cloudprint-backend/pkg/jwt"
"github.com/cloudprint/cloudprint-backend/pkg/pay"
"github.com/cloudprint/cloudprint-backend/pkg/upload"
pb "github.com/cloudprint/cloudprint-backend/proto/generated"
"go.uber.org/zap"
"google.golang.org/grpc"
2026-04-28 20:26:52 +08:00
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
func main() {
configPath := flag.String("config", "config.yaml", "path to config file")
flag.Parse()
// Load configuration
cfg, err := config.LoadConfig(*configPath)
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
// Initialize logger
zapLogger, _ := zap.NewProduction()
defer zapLogger.Sync()
// Initialize database
db, err := initDB(cfg)
if err != nil {
log.Fatalf("Failed to initialize database: %v", err)
}
// Initialize repositories
userRepo := repository.NewUserRepository(db)
adminRepo := repository.NewAdminRepository(db)
fileRepo := repository.NewFileRepository(db)
orderRepo := repository.NewOrderRepository(db)
orderItemRepo := repository.NewOrderItemRepository(db)
printerRepo := repository.NewPrinterRepository(db)
printJobRepo := repository.NewPrintJobRepository(db)
libraryRepo := repository.NewLibraryRepository(db)
priceRepo := repository.NewPriceRepository(db)
paymentRepo := repository.NewPaymentRepository(db)
// Initialize JWT manager
jwtManager := jwt.NewJWTManager(
cfg.JWT.Secret,
cfg.JWT.AccessTokenTTL,
cfg.JWT.RefreshTokenTTL,
)
// Initialize file uploader
fileUploader := upload.NewFileUploader(
cfg.Upload.Path,
cfg.Upload.ChunkPath,
cfg.Upload.MaxSize,
)
// Initialize WeChat pay
weChatPay := pay.NewWeChatPay(&cfg.WeChat)
// Initialize services
authService := service.NewAuthService(userRepo, adminRepo, jwtManager, nil, &cfg.WeChat)
fileService := service.NewFileService(fileRepo, fileUploader, cfg.Upload.Path)
orderService := service.NewOrderService(orderRepo, orderItemRepo, fileRepo, priceRepo, userRepo, printerRepo, printJobRepo)
printService := service.NewPrintService(printerRepo, printJobRepo, orderRepo)
// Initialize gRPC server
grpcServer := grpc.NewServer(
grpc.UnaryInterceptor(interceptor.NewAuthInterceptor(jwtManager).UnaryServerInterceptor()),
)
// Register gRPC services
2026-04-28 20:26:52 +08:00
pb.RegisterAuthServiceServer(grpcServer, grpcService.NewAuthServiceServer(authService))
pb.RegisterFileServiceServer(grpcServer, grpcService.NewFileServiceServer(fileService))
pb.RegisterOrderServiceServer(grpcServer, grpcService.NewOrderServiceServer(orderService))
pb.RegisterPrintServiceServer(grpcServer, grpcService.NewPrintServiceServer(printService))
pb.RegisterLibraryServiceServer(grpcServer, grpcService.NewLibraryServiceServer(libraryRepo, fileRepo))
pb.RegisterPriceServiceServer(grpcServer, grpcService.NewPriceServiceServer(priceRepo))
pb.RegisterPayServiceServer(grpcServer, grpcService.NewPayServiceServer(orderService, paymentRepo, weChatPay, userRepo))
pb.RegisterAdminServiceServer(grpcServer, grpcService.NewAdminServiceServer(authService, orderRepo, printerRepo))
// Initialize HTTP gateway
gateway := newHTTPGateway(grpcServer, jwtManager)
// Start server
addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
lis, err := net.Listen("tcp", addr)
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
// Graceful shutdown
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
2026-04-28 20:26:52 +08:00
// HTTP server for gateway
httpServer := &http.Server{
Handler: gateway,
}
// Start HTTP server in goroutine
go func() {
2026-04-28 20:26:52 +08:00
zapLogger.Info("Starting HTTP gateway", zap.String("address", addr))
if err := httpServer.Serve(lis); err != nil && err != http.ErrServerClosed {
log.Fatalf("Failed to serve HTTP: %v", err)
}
}()
2026-04-28 20:26:52 +08:00
// Wait for shutdown signal
<-sigCh
zapLogger.Info("Shutting down servers...")
// Graceful stop gRPC server
grpcServer.GracefulStop()
// Shutdown HTTP server
if err := httpServer.Close(); err != nil {
zapLogger.Error("Failed to close HTTP server", zap.Error(err))
}
}
func initDB(cfg *config.Config) (*gorm.DB, error) {
gormLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{
LogLevel: logger.Info,
},
)
2026-04-28 20:26:52 +08:00
db, err := gorm.Open(postgres.Open(cfg.Database.DSN()), &gorm.Config{
Logger: gormLogger,
})
if err != nil {
return nil, err
}
sqlDB, err := db.DB()
if err != nil {
return nil, err
}
sqlDB.SetMaxOpenConns(cfg.Database.MaxOpenConns)
sqlDB.SetMaxIdleConns(cfg.Database.MaxIdleConns)
return db, nil
}
2026-04-28 20:26:52 +08:00
// HTTP Gateway
type httpGateway struct {
grpcServer *grpc.Server
jwtManager *jwt.JWTManager
mux *http.ServeMux
}
func newHTTPGateway(grpcServer *grpc.Server, jwtManager *jwt.JWTManager) *httpGateway {
g := &httpGateway{
grpcServer: grpcServer,
jwtManager: jwtManager,
mux: http.NewServeMux(),
}
g.registerHandlers()
return g
}
func (g *httpGateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
g.mux.ServeHTTP(w, r)
}
func (g *httpGateway) registerHandlers() {
// Health check
g.mux.HandleFunc("/health", g.healthHandler)
// Auth endpoints
g.mux.HandleFunc("/api/v1/auth/login", g.handleLogin)
g.mux.HandleFunc("/api/v1/auth/refresh", g.handleRefreshToken)
g.mux.HandleFunc("/api/v1/auth/userinfo", g.handleGetUserInfo)
// File endpoints
g.mux.HandleFunc("/api/v1/file/upload", g.handleFileUpload)
g.mux.HandleFunc("/api/v1/file/", g.handleFileOps)
// Order endpoints
g.mux.HandleFunc("/api/v1/order/create", g.handleCreateOrder)
g.mux.HandleFunc("/api/v1/order/", g.handleOrderOps)
// Print endpoints
g.mux.HandleFunc("/api/v1/print/printers", g.handleGetPrinters)
g.mux.HandleFunc("/api/v1/print/submit", g.handleSubmitPrintJob)
// Library endpoints
g.mux.HandleFunc("/api/v1/library/categories", g.handleGetCategories)
g.mux.HandleFunc("/api/v1/library/files", g.handleGetLibraryFiles)
// Price endpoints
g.mux.HandleFunc("/api/v1/price/list", g.handleGetPriceList)
// Pay endpoints
g.mux.HandleFunc("/api/v1/pay/wxpay", g.handleCreateWxPayOrder)
g.mux.HandleFunc("/api/v1/pay/callback", g.handlePayCallback)
g.mux.HandleFunc("/api/v1/pay/balance", g.handlePayByBalance)
// Admin endpoints
g.mux.HandleFunc("/api/v1/admin/login", g.handleAdminLogin)
g.mux.HandleFunc("/api/v1/admin/stats", g.handleGetStatistics)
}
func (g *httpGateway) healthHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"status":"ok"}`))
}
func (g *httpGateway) writeJSON(w http.ResponseWriter, statusCode int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(data)
}
func (g *httpGateway) handleError(w http.ResponseWriter, err error) {
st, ok := status.FromError(err)
if !ok {
g.writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
code := http.StatusInternalServerError
switch st.Code() {
case codes.OK:
code = http.StatusOK
case codes.InvalidArgument:
code = http.StatusBadRequest
case codes.NotFound:
code = http.StatusNotFound
case codes.Unauthenticated:
code = http.StatusUnauthorized
case codes.PermissionDenied:
code = http.StatusForbidden
}
g.writeJSON(w, code, map[string]string{"error": st.Message()})
}
func (g *httpGateway) parseToken(r *http.Request) (context.Context, error) {
token := r.Header.Get("Authorization")
if token == "" {
return nil, status.Error(codes.Unauthenticated, "missing token")
}
if len(token) > 7 && token[:7] == "Bearer " {
token = token[7:]
}
claims, err := g.jwtManager.ValidateToken(token)
if err != nil {
return nil, status.Error(codes.Unauthenticated, "invalid token")
}
return metadata.NewOutgoingContext(r.Context(), metadata.Pairs(
"user_id", fmt.Sprintf("%d", claims.UserID),
"user_type", claims.UserType,
)), nil
}
func (g *httpGateway) handleLogin(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req pb.LoginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// gRPC call would go here - returning placeholder for now
g.writeJSON(w, http.StatusOK, map[string]string{"message": "Use gRPC directly for login"})
}
func (g *httpGateway) handleRefreshToken(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
g.writeJSON(w, http.StatusOK, map[string]string{"message": "not implemented"})
}
func (g *httpGateway) handleGetUserInfo(w http.ResponseWriter, r *http.Request) {
_, err := g.parseToken(r)
if err != nil {
g.handleError(w, err)
return
}
g.writeJSON(w, http.StatusOK, map[string]string{"message": "not implemented"})
}
func (g *httpGateway) handleFileUpload(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
_, err := g.parseToken(r)
if err != nil {
g.handleError(w, err)
return
}
g.writeJSON(w, http.StatusOK, map[string]string{"message": "not implemented"})
}
func (g *httpGateway) handleFileOps(w http.ResponseWriter, r *http.Request) {
_, err := g.parseToken(r)
if err != nil {
g.handleError(w, err)
return
}
switch r.Method {
case http.MethodGet:
g.writeJSON(w, http.StatusOK, map[string]string{"message": "not implemented"})
case http.MethodDelete:
g.writeJSON(w, http.StatusOK, map[string]string{"message": "not implemented"})
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func (g *httpGateway) handleCreateOrder(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
_, err := g.parseToken(r)
if err != nil {
g.handleError(w, err)
return
}
g.writeJSON(w, http.StatusOK, map[string]string{"message": "not implemented"})
}
func (g *httpGateway) handleOrderOps(w http.ResponseWriter, r *http.Request) {
_, err := g.parseToken(r)
if err != nil {
g.handleError(w, err)
return
}
switch r.Method {
case http.MethodGet:
g.writeJSON(w, http.StatusOK, map[string]string{"message": "not implemented"})
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func (g *httpGateway) handleGetPrinters(w http.ResponseWriter, r *http.Request) {
_, err := g.parseToken(r)
if err != nil {
g.handleError(w, err)
return
}
g.writeJSON(w, http.StatusOK, map[string]string{"message": "not implemented"})
}
func (g *httpGateway) handleSubmitPrintJob(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
_, err := g.parseToken(r)
if err != nil {
g.handleError(w, err)
return
}
g.writeJSON(w, http.StatusOK, map[string]string{"message": "not implemented"})
}
func (g *httpGateway) handleGetCategories(w http.ResponseWriter, r *http.Request) {
_, err := g.parseToken(r)
if err != nil {
g.handleError(w, err)
return
}
g.writeJSON(w, http.StatusOK, map[string]string{"message": "not implemented"})
}
func (g *httpGateway) handleGetLibraryFiles(w http.ResponseWriter, r *http.Request) {
_, err := g.parseToken(r)
if err != nil {
g.handleError(w, err)
return
}
g.writeJSON(w, http.StatusOK, map[string]string{"message": "not implemented"})
}
func (g *httpGateway) handleGetPriceList(w http.ResponseWriter, r *http.Request) {
_, err := g.parseToken(r)
if err != nil {
g.handleError(w, err)
return
}
g.writeJSON(w, http.StatusOK, map[string]string{"message": "not implemented"})
}
func (g *httpGateway) handleCreateWxPayOrder(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
_, err := g.parseToken(r)
if err != nil {
g.handleError(w, err)
return
}
g.writeJSON(w, http.StatusOK, map[string]string{"message": "not implemented"})
}
func (g *httpGateway) handlePayCallback(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
g.writeJSON(w, http.StatusOK, map[string]string{"message": "not implemented"})
}
func (g *httpGateway) handlePayByBalance(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
_, err := g.parseToken(r)
if err != nil {
g.handleError(w, err)
return
}
g.writeJSON(w, http.StatusOK, map[string]string{"message": "not implemented"})
}
func (g *httpGateway) handleAdminLogin(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
g.writeJSON(w, http.StatusOK, map[string]string{"message": "not implemented"})
}
func (g *httpGateway) handleGetStatistics(w http.ResponseWriter, r *http.Request) {
_, err := g.parseToken(r)
if err != nil {
g.handleError(w, err)
return
}
g.writeJSON(w, http.StatusOK, map[string]string{"message": "not implemented"})
}