feat: 初始提交云印享付费打印服务后端
This commit is contained in:
125
cloudprint-backend/internal/grpc/interceptor/auth.go
Normal file
125
cloudprint-backend/internal/grpc/interceptor/auth.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package interceptor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudprint/cloudprint-backend/pkg/jwt"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
const (
|
||||
AuthorizationHeader = "authorization"
|
||||
BearerPrefix = "Bearer "
|
||||
UserIDKey = "user_id"
|
||||
UserTypeKey = "user_type"
|
||||
)
|
||||
|
||||
type AuthInterceptor struct {
|
||||
jwtManager *jwt.JWTManager
|
||||
}
|
||||
|
||||
func NewAuthInterceptor(jwtManager *jwt.JWTManager) *AuthInterceptor {
|
||||
return &AuthInterceptor{jwtManager: jwtManager}
|
||||
}
|
||||
|
||||
// UnaryServerInterceptor returns a unary server interceptor that handles authentication
|
||||
func (i *AuthInterceptor) UnaryServerInterceptor() grpc.UnaryServerInterceptor {
|
||||
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
|
||||
// Skip auth for certain methods
|
||||
if isPublicMethod(info.FullMethod) {
|
||||
return handler(ctx, req)
|
||||
}
|
||||
|
||||
claims, err := i.extractAndValidateToken(ctx)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Unauthenticated, "invalid token: %v", err)
|
||||
}
|
||||
|
||||
// Add user info to context
|
||||
ctx = context.WithValue(ctx, UserIDKey, claims.UserID)
|
||||
ctx = context.WithValue(ctx, UserTypeKey, claims.UserType)
|
||||
|
||||
return handler(ctx, req)
|
||||
}
|
||||
}
|
||||
|
||||
// StreamServerInterceptor returns a streaming server interceptor that handles authentication
|
||||
func (i *AuthInterceptor) StreamServerInterceptor() grpc.StreamServerInterceptor {
|
||||
return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
||||
// Skip auth for certain methods
|
||||
if isPublicMethod(info.FullMethod) {
|
||||
return handler(srv, ss)
|
||||
}
|
||||
|
||||
claims, err := i.extractAndValidateToken(ss.Context())
|
||||
if err != nil {
|
||||
return status.Errorf(codes.Unauthenticated, "invalid token: %v", err)
|
||||
}
|
||||
|
||||
// Add user info to context
|
||||
ctx := context.WithValue(ss.Context(), UserIDKey, claims.UserID)
|
||||
ctx = context.WithValue(ctx, UserTypeKey, claims.UserType)
|
||||
|
||||
wrapped := &serverStreamWrapper{
|
||||
ServerStream: ss,
|
||||
ctx: ctx,
|
||||
}
|
||||
|
||||
return handler(srv, wrapped)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *AuthInterceptor) extractAndValidateToken(ctx context.Context) (*jwt.Claims, error) {
|
||||
md, ok := metadata.FromIncomingContext(ctx)
|
||||
if !ok {
|
||||
return nil, status.Error(codes.Unauthenticated, "missing metadata")
|
||||
}
|
||||
|
||||
authHeader := md.Get(AuthorizationHeader)
|
||||
if len(authHeader) == 0 {
|
||||
return nil, status.Error(codes.Unauthenticated, "missing authorization header")
|
||||
}
|
||||
|
||||
tokenString := strings.TrimPrefix(authHeader[0], BearerPrefix)
|
||||
if tokenString == authHeader[0] {
|
||||
return nil, status.Error(codes.Unauthenticated, "invalid authorization format")
|
||||
}
|
||||
|
||||
return i.jwtManager.ValidateToken(tokenString)
|
||||
}
|
||||
|
||||
func isPublicMethod(method string) bool {
|
||||
publicMethods := map[string]bool{
|
||||
"/cloudprint.AuthService/Login": true,
|
||||
"/cloudprint.AuthService/RefreshToken": true,
|
||||
"/cloudprint.AdminService/Login": true,
|
||||
"/cloudprint.PayService/HandlePayCallback": true,
|
||||
}
|
||||
return publicMethods[method]
|
||||
}
|
||||
|
||||
// serverStreamWrapper wraps grpc.ServerStream to override Context method
|
||||
type serverStreamWrapper struct {
|
||||
grpc.ServerStream
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func (w *serverStreamWrapper) Context() context.Context {
|
||||
return w.ctx
|
||||
}
|
||||
|
||||
// GetUserIDFromContext extracts user ID from context
|
||||
func GetUserIDFromContext(ctx context.Context) (int32, bool) {
|
||||
userID, ok := ctx.Value(UserIDKey).(int32)
|
||||
return userID, ok
|
||||
}
|
||||
|
||||
// GetUserTypeFromContext extracts user type from context
|
||||
func GetUserTypeFromContext(ctx context.Context) (string, bool) {
|
||||
userType, ok := ctx.Value(UserTypeKey).(string)
|
||||
return userType, ok
|
||||
}
|
||||
92
cloudprint-backend/internal/grpc/server.go
Normal file
92
cloudprint-backend/internal/grpc/server.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/cloudprint/cloudprint-backend/internal/grpc/interceptor"
|
||||
pb "github.com/cloudprint/cloudprint-backend/proto/generated"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/reflection"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
grpcServer *grpc.Server
|
||||
port int
|
||||
}
|
||||
|
||||
type ServerOption func(*Server)
|
||||
|
||||
func WithPort(port int) ServerOption {
|
||||
return func(s *Server) {
|
||||
s.port = port
|
||||
}
|
||||
}
|
||||
|
||||
func NewServer(opts ...ServerOption) *Server {
|
||||
s := &Server{
|
||||
port: 8080,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(s)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Server) RegisterServices(grpcServer *grpc.Server) {
|
||||
// Services will be registered here
|
||||
// This is a placeholder for the actual service registration
|
||||
}
|
||||
|
||||
func (s *Server) Start() error {
|
||||
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", s.port))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to listen: %v", err)
|
||||
}
|
||||
|
||||
s.grpcServer = grpc.NewServer(
|
||||
grpc.UnaryInterceptor(interceptor.AuthInterceptor{}.UnaryServerInterceptor()),
|
||||
)
|
||||
|
||||
// Register reflection service for debugging
|
||||
reflection.Register(s.grpcServer)
|
||||
|
||||
if err := s.grpcServer.Serve(lis); err != nil {
|
||||
return fmt.Errorf("failed to serve: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) Stop() {
|
||||
if s.grpcServer != nil {
|
||||
s.grpcServer.GracefulStop()
|
||||
}
|
||||
}
|
||||
|
||||
// Register all gRPC services
|
||||
func RegisterServices(grpcServer *grpc.Server) {
|
||||
// AuthService
|
||||
// pb.RegisterAuthServiceServer(grpcServer, authServiceServer)
|
||||
|
||||
// FileService
|
||||
// pb.RegisterFileServiceServer(grpcServer, fileServiceServer)
|
||||
|
||||
// OrderService
|
||||
// pb.RegisterOrderServiceServer(grpcServer, orderServiceServer)
|
||||
|
||||
// PrintService
|
||||
// pb.RegisterPrintServiceServer(grpcServer, printServiceServer)
|
||||
|
||||
// LibraryService
|
||||
// pb.RegisterLibraryServiceServer(grpcServer, libraryServiceServer)
|
||||
|
||||
// PriceService
|
||||
// pb.RegisterPriceServiceServer(grpcServer, priceServiceServer)
|
||||
|
||||
// PayService
|
||||
// pb.RegisterPayServiceServer(grpcServer, payServiceServer)
|
||||
|
||||
// AdminService
|
||||
// pb.RegisterAdminServiceServer(grpcServer, adminServiceServer)
|
||||
}
|
||||
68
cloudprint-backend/internal/grpc/service/admin_service.go
Normal file
68
cloudprint-backend/internal/grpc/service/admin_service.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cloudprint/cloudprint-backend/internal/repository"
|
||||
"github.com/cloudprint/cloudprint-backend/internal/service"
|
||||
pb "github.com/cloudprint/cloudprint-backend/proto/generated"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
type AdminServiceServer struct {
|
||||
pb.UnimplementedAdminServiceServer
|
||||
authService *service.AuthService
|
||||
orderRepo *repository.OrderRepository
|
||||
printerRepo *repository.PrinterRepository
|
||||
}
|
||||
|
||||
func NewAdminServiceServer(
|
||||
authService *service.AuthService,
|
||||
orderRepo *repository.OrderRepository,
|
||||
printerRepo *repository.PrinterRepository,
|
||||
) *AdminServiceServer {
|
||||
return &AdminServiceServer{
|
||||
authService: authService,
|
||||
orderRepo: orderRepo,
|
||||
printerRepo: printerRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AdminServiceServer) Login(ctx context.Context, req *pb.AdminLoginRequest) (*pb.AdminLoginResponse, error) {
|
||||
result, err := s.authService.AdminLogin(ctx, req.Username, req.Password)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Unauthenticated, "login failed: %v", err)
|
||||
}
|
||||
|
||||
return &pb.AdminLoginResponse{
|
||||
AccessToken: result.AccessToken,
|
||||
Admin: &pb.Admin{
|
||||
Id: result.Admin.ID,
|
||||
Username: result.Admin.Username,
|
||||
Nickname: result.Admin.Nickname,
|
||||
Role: int32(result.Admin.Role),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *AdminServiceServer) GetStatistics(ctx context.Context, req *pb.GetStatisticsRequest) (*pb.Statistics, error) {
|
||||
totalUsers, totalOrders, todayOrders, todayRevenue, pendingOrders, err := s.orderRepo.GetStatistics(ctx)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "get statistics failed: %v", err)
|
||||
}
|
||||
|
||||
activePrinters, err := s.printerRepo.CountActive(ctx)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "get printer count failed: %v", err)
|
||||
}
|
||||
|
||||
return &pb.Statistics{
|
||||
TotalUsers: totalUsers,
|
||||
TotalOrders: totalOrders,
|
||||
TodayOrders: todayOrders,
|
||||
TodayRevenue: todayRevenue,
|
||||
PendingOrders: pendingOrders,
|
||||
ActivePrinters: int32(activePrinters),
|
||||
}, nil
|
||||
}
|
||||
63
cloudprint-backend/internal/grpc/service/auth_service.go
Normal file
63
cloudprint-backend/internal/grpc/service/auth_service.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cloudprint/cloudprint-backend/internal/service"
|
||||
pb "github.com/cloudprint/cloudprint-backend/proto/generated"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
type AuthServiceServer struct {
|
||||
pb.UnimplementedAuthServiceServer
|
||||
authService *service.AuthService
|
||||
}
|
||||
|
||||
func NewAuthServiceServer(authService *service.AuthService) *AuthServiceServer {
|
||||
return &AuthServiceServer{
|
||||
authService: authService,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AuthServiceServer) Login(ctx context.Context, req *pb.LoginRequest) (*pb.LoginResponse, error) {
|
||||
result, err := s.authService.Login(ctx, req.Code)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "login failed: %v", err)
|
||||
}
|
||||
|
||||
return &pb.LoginResponse{
|
||||
AccessToken: result.AccessToken,
|
||||
RefreshToken: result.RefreshToken,
|
||||
User: &pb.User{
|
||||
Id: result.User.ID,
|
||||
Openid: result.User.Openid,
|
||||
Nickname: result.User.Nickname,
|
||||
Avatar: result.User.Avatar,
|
||||
Balance: result.User.Balance,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *AuthServiceServer) RefreshToken(ctx context.Context, req *pb.RefreshRequest) (*pb.RefreshResponse, error) {
|
||||
newToken, err := s.authService.RefreshToken(ctx, req.RefreshToken)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "refresh token failed: %v", err)
|
||||
}
|
||||
|
||||
return &pb.RefreshResponse{
|
||||
AccessToken: newToken,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *AuthServiceServer) GetUserInfo(ctx context.Context, req *pb.GetUserInfoRequest) (*pb.User, error) {
|
||||
// In a real implementation, extract user ID from context
|
||||
// For now, this is a placeholder
|
||||
return nil, status.Errorf(codes.Unimplemented, "not implemented")
|
||||
}
|
||||
|
||||
func (s *AuthServiceServer) UpdateUser(ctx context.Context, req *pb.UpdateUserRequest) (*pb.User, error) {
|
||||
// In a real implementation, extract user ID from context
|
||||
// For now, this is a placeholder
|
||||
return nil, status.Errorf(codes.Unimplemented, "not implemented")
|
||||
}
|
||||
141
cloudprint-backend/internal/grpc/service/file_service.go
Normal file
141
cloudprint-backend/internal/grpc/service/file_service.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/cloudprint/cloudprint-backend/internal/service"
|
||||
pb "github.com/cloudprint/cloudprint-backend/proto/generated"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
type FileServiceServer struct {
|
||||
pb.UnimplementedFileServiceServer
|
||||
fileService *service.FileService
|
||||
}
|
||||
|
||||
func NewFileServiceServer(fileService *service.FileService) *FileServiceServer {
|
||||
return &FileServiceServer{
|
||||
fileService: fileService,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *FileServiceServer) Upload(stream pb.FileService_UploadServer) (*pb.UploadResponse, error) {
|
||||
var fileName, fileType, uploaderType string
|
||||
var uploaderID int32
|
||||
var totalSize int64
|
||||
var chunks [][]byte
|
||||
|
||||
for {
|
||||
req, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "receive chunk failed: %v", err)
|
||||
}
|
||||
|
||||
if fileName == "" {
|
||||
fileName = req.FileName
|
||||
fileType = req.FileType
|
||||
uploaderType = "user" // Extract from context in production
|
||||
uploaderID = 1 // Extract from context in production
|
||||
totalSize = req.FileSize
|
||||
}
|
||||
|
||||
chunks = append(chunks, req.Data)
|
||||
}
|
||||
|
||||
// Concatenate chunks
|
||||
var data []byte
|
||||
for _, chunk := range chunks {
|
||||
data = append(data, chunk...)
|
||||
}
|
||||
|
||||
file, err := s.fileService.Upload(stream.Context(), fileName, fileType, uploaderType, uploaderID, data)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "upload failed: %v", err)
|
||||
}
|
||||
|
||||
return &pb.UploadResponse{
|
||||
FileId: file.ID,
|
||||
FilePath: file.FilePath,
|
||||
FileUrl: file.FilePath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *FileServiceServer) GetFile(req *pb.GetFileRequest, stream pb.FileService_GetFileServer) error {
|
||||
file, err := s.fileService.GetFile(stream.Context(), req.FileId)
|
||||
if err != nil || file == nil {
|
||||
return status.Errorf(codes.NotFound, "file not found")
|
||||
}
|
||||
|
||||
reader, err := s.fileService.GetFileReader(stream.Context(), file)
|
||||
if err != nil {
|
||||
return status.Errorf(codes.Internal, "read file failed: %v", err)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
buf := make([]byte, 4096)
|
||||
for {
|
||||
n, err := reader.Read(buf)
|
||||
if n > 0 {
|
||||
if err := stream.Send(&pb.FileData{Data: buf[:n], IsLast: false}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return stream.Send(&pb.FileData{IsLast: true})
|
||||
}
|
||||
|
||||
func (s *FileServiceServer) DeleteFile(ctx context.Context, req *pb.DeleteFileRequest) (*pb.Empty, error) {
|
||||
err := s.fileService.DeleteFile(ctx, req.FileId, "user", 1)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "delete file failed: %v", err)
|
||||
}
|
||||
|
||||
return &pb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *FileServiceServer) AddWatermark(ctx context.Context, req *pb.WatermarkRequest) (*pb.WatermarkResponse, error) {
|
||||
file, err := s.fileService.AddWatermark(ctx, req.FileId, req.Text, req.Opacity, int(req.Position), int(req.FontSize))
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "add watermark failed: %v", err)
|
||||
}
|
||||
|
||||
return &pb.WatermarkResponse{
|
||||
FileId: file.ID,
|
||||
FilePath: file.FilePath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *FileServiceServer) GetFileList(ctx context.Context, req *pb.GetFileListRequest) (*pb.FileListResponse, error) {
|
||||
files, total, err := s.fileService.GetFileList(ctx, req.UploaderType, req.UploaderId, int(req.Page), int(req.PageSize))
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "get file list failed: %v", err)
|
||||
}
|
||||
|
||||
pbFiles := make([]*pb.FileInfo, 0, len(files))
|
||||
for _, f := range files {
|
||||
pbFiles = append(pbFiles, &pb.FileInfo{
|
||||
Id: f.ID,
|
||||
FileName: f.FileName,
|
||||
FilePath: f.FilePath,
|
||||
FileType: f.FileType,
|
||||
FileSize: f.FileSize,
|
||||
})
|
||||
}
|
||||
|
||||
return &pb.FileListResponse{
|
||||
Files: pbFiles,
|
||||
Total: int32(total),
|
||||
}, nil
|
||||
}
|
||||
91
cloudprint-backend/internal/grpc/service/library_service.go
Normal file
91
cloudprint-backend/internal/grpc/service/library_service.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cloudprint/cloudprint-backend/internal/repository"
|
||||
pb "github.com/cloudprint/cloudprint-backend/proto/generated"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
type LibraryServiceServer struct {
|
||||
pb.UnimplementedLibraryServiceServer
|
||||
libraryRepo *repository.LibraryRepository
|
||||
fileRepo *repository.FileRepository
|
||||
}
|
||||
|
||||
func NewLibraryServiceServer(libraryRepo *repository.LibraryRepository, fileRepo *repository.FileRepository) *LibraryServiceServer {
|
||||
return &LibraryServiceServer{
|
||||
libraryRepo: libraryRepo,
|
||||
fileRepo: fileRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LibraryServiceServer) GetCategories(ctx context.Context, req *pb.GetCategoriesRequest) (*pb.CategoryListResponse, error) {
|
||||
categories, err := s.libraryRepo.GetAllCategories(ctx)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "get categories failed: %v", err)
|
||||
}
|
||||
|
||||
pbCategories := make([]*pb.LibraryCategory, 0, len(categories))
|
||||
for _, cat := range categories {
|
||||
pbCategories = append(pbCategories, &pb.LibraryCategory{
|
||||
Id: cat.ID,
|
||||
Name: cat.Name,
|
||||
Icon: cat.Icon,
|
||||
SortOrder: int32(cat.SortOrder),
|
||||
})
|
||||
}
|
||||
|
||||
return &pb.CategoryListResponse{Categories: pbCategories}, nil
|
||||
}
|
||||
|
||||
func (s *LibraryServiceServer) GetFiles(ctx context.Context, req *pb.GetLibraryFilesRequest) (*pb.LibraryFileListResponse, error) {
|
||||
files, total, err := s.libraryRepo.GetFilesByCategory(ctx, req.CategoryId, int(req.Page), int(req.PageSize))
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "get files failed: %v", err)
|
||||
}
|
||||
|
||||
pbFiles := make([]*pb.LibraryFile, 0, len(files))
|
||||
for _, file := range files {
|
||||
pbFile := &pb.LibraryFile{
|
||||
Id: file.ID,
|
||||
CategoryId: file.CategoryID,
|
||||
FileId: file.FileID,
|
||||
Title: file.Title,
|
||||
Description: file.Description,
|
||||
PageCount: int32(file.PageCount),
|
||||
DownloadCount: int32(file.DownloadCount),
|
||||
}
|
||||
if file.File != nil {
|
||||
pbFile.File = &pb.FileInfo{
|
||||
Id: file.File.ID,
|
||||
FileName: file.File.FileName,
|
||||
FilePath: file.File.FilePath,
|
||||
FileType: file.File.FileType,
|
||||
FileSize: file.File.FileSize,
|
||||
}
|
||||
}
|
||||
pbFiles = append(pbFiles, pbFile)
|
||||
}
|
||||
|
||||
return &pb.LibraryFileListResponse{
|
||||
Files: pbFiles,
|
||||
Total: int32(total),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *LibraryServiceServer) UploadFile(stream pb.LibraryService_UploadFileServer) error {
|
||||
// Placeholder for streaming upload
|
||||
return status.Errorf(codes.Unimplemented, "streaming upload not implemented")
|
||||
}
|
||||
|
||||
func (s *LibraryServiceServer) DeleteFile(ctx context.Context, req *pb.DeleteLibraryFileRequest) (*pb.Empty, error) {
|
||||
err := s.libraryRepo.DeleteFile(ctx, req.LibraryFileId)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "delete file failed: %v", err)
|
||||
}
|
||||
|
||||
return &pb.Empty{}, nil
|
||||
}
|
||||
98
cloudprint-backend/internal/grpc/service/order_service.go
Normal file
98
cloudprint-backend/internal/grpc/service/order_service.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cloudprint/cloudprint-backend/internal/service"
|
||||
pb "github.com/cloudprint/cloudprint-backend/proto/generated"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
type OrderServiceServer struct {
|
||||
pb.UnimplementedOrderServiceServer
|
||||
orderService *service.OrderService
|
||||
}
|
||||
|
||||
func NewOrderServiceServer(orderService *service.OrderService) *OrderServiceServer {
|
||||
return &OrderServiceServer{
|
||||
orderService: orderService,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *OrderServiceServer) CreateOrder(ctx context.Context, req *pb.CreateOrderRequest) (*pb.CreateOrderResponse, error) {
|
||||
// Extract user ID from context (set by auth interceptor)
|
||||
userID := int32(1) // Placeholder
|
||||
|
||||
items := make([]*service.CreateOrderItem, 0, len(req.Items))
|
||||
for _, item := range req.Items {
|
||||
items = append(items, &service.CreateOrderItem{
|
||||
FileID: item.FileId,
|
||||
PrinterID: item.PrinterId,
|
||||
Copies: int(item.Copies),
|
||||
Color: item.Color,
|
||||
Duplex: item.Duplex,
|
||||
PageRange: item.PageRange,
|
||||
})
|
||||
}
|
||||
|
||||
order, err := s.orderService.CreateOrder(ctx, userID, items, req.Remark)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "create order failed: %v", err)
|
||||
}
|
||||
|
||||
return &pb.CreateOrderResponse{
|
||||
OrderNo: order.OrderNo,
|
||||
TotalAmount: order.TotalAmount,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *OrderServiceServer) GetOrder(ctx context.Context, req *pb.GetOrderRequest) (*pb.Order, error) {
|
||||
order, err := s.orderService.GetOrder(ctx, req.OrderNo)
|
||||
if err != nil || order == nil {
|
||||
return nil, status.Errorf(codes.NotFound, "order not found")
|
||||
}
|
||||
|
||||
return convertOrderToPB(order), nil
|
||||
}
|
||||
|
||||
func (s *OrderServiceServer) GetOrderList(ctx context.Context, req *pb.GetOrderListRequest) (*pb.OrderListResponse, error) {
|
||||
orders, total, err := s.orderService.GetOrderList(ctx, req.UserId, req.Status, int(req.Page), int(req.PageSize))
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "get order list failed: %v", err)
|
||||
}
|
||||
|
||||
pbOrders := make([]*pb.Order, 0, len(orders))
|
||||
for _, order := range orders {
|
||||
pbOrders = append(pbOrders, convertOrderToPB(order))
|
||||
}
|
||||
|
||||
return &pb.OrderListResponse{
|
||||
Orders: pbOrders,
|
||||
Total: int32(total),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *OrderServiceServer) UpdateOrderStatus(ctx context.Context, req *pb.UpdateOrderStatusRequest) (*pb.Empty, error) {
|
||||
err := s.orderService.UpdateOrderStatus(ctx, req.OrderNo, int8(req.Status))
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "update order status failed: %v", err)
|
||||
}
|
||||
|
||||
return &pb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *OrderServiceServer) CancelOrder(ctx context.Context, req *pb.CancelOrderRequest) (*pb.Empty, error) {
|
||||
err := s.orderService.CancelOrder(ctx, req.OrderNo)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "cancel order failed: %v", err)
|
||||
}
|
||||
|
||||
return &pb.Empty{}, nil
|
||||
}
|
||||
|
||||
func convertOrderToPB(order *service.Order) *pb.Order {
|
||||
// Note: This is a simplified conversion. In production, you would need to handle all fields properly.
|
||||
// The service.Order type here is the model, not the protobuf type.
|
||||
return nil // Placeholder
|
||||
}
|
||||
201
cloudprint-backend/internal/grpc/service/pay_service.go
Normal file
201
cloudprint-backend/internal/grpc/service/pay_service.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cloudprint/cloudprint-backend/internal/model"
|
||||
"github.com/cloudprint/cloudprint-backend/internal/repository"
|
||||
"github.com/cloudprint/cloudprint-backend/internal/service"
|
||||
"github.com/cloudprint/cloudprint-backend/pkg/pay"
|
||||
pb "github.com/cloudprint/cloudprint-backend/proto/generated"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
type PayServiceServer struct {
|
||||
pb.UnimplementedPayServiceServer
|
||||
orderService *service.OrderService
|
||||
paymentRepo *repository.PaymentRepository
|
||||
weChatPay *pay.WeChatPay
|
||||
userRepo *repository.UserRepository
|
||||
}
|
||||
|
||||
func NewPayServiceServer(
|
||||
orderService *service.OrderService,
|
||||
paymentRepo *repository.PaymentRepository,
|
||||
weChatPay *pay.WeChatPay,
|
||||
userRepo *repository.UserRepository,
|
||||
) *PayServiceServer {
|
||||
return &PayServiceServer{
|
||||
orderService: orderService,
|
||||
paymentRepo: paymentRepo,
|
||||
weChatPay: weChatPay,
|
||||
userRepo: userRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PayServiceServer) CreateWxPayOrder(ctx context.Context, req *pb.WxPayRequest) (*pb.WxPayResponse, error) {
|
||||
resp, err := s.weChatPay.UnifiedOrder(ctx, req.OrderNo, req.Amount, req.Description, req.Openid, req.ClientIp)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "create pay order failed: %v", err)
|
||||
}
|
||||
|
||||
if resp.ReturnCode != "SUCCESS" {
|
||||
return nil, status.Errorf(codes.Internal, "wechat pay error: %s - %s", resp.ErrCode, resp.ErrMsg)
|
||||
}
|
||||
|
||||
// Create payment record
|
||||
payment := &model.Payment{
|
||||
OrderNo: req.OrderNo,
|
||||
PayType: "wechat",
|
||||
PayStatus: model.PayStatusPaying,
|
||||
}
|
||||
if err := s.paymentRepo.Create(ctx, payment); err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "create payment record failed: %v", err)
|
||||
}
|
||||
|
||||
return &pb.WxPayResponse{
|
||||
PrepayId: resp.PrepayID,
|
||||
CodeUrl: resp.CodeURL,
|
||||
MwebUrl: resp.MWebURL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *PayServiceServer) QueryPayStatus(ctx context.Context, req *pb.QueryPayStatusRequest) (*pb.PayStatusResponse, error) {
|
||||
tradeState, _, err := s.weChatPay.QueryOrder(ctx, req.OrderNo)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "query pay status failed: %v", err)
|
||||
}
|
||||
|
||||
var payStatus int32
|
||||
switch tradeState {
|
||||
case "SUCCESS":
|
||||
payStatus = int32(model.PayStatusSuccess)
|
||||
case "USERPAYING", "PAYING":
|
||||
payStatus = int32(model.PayStatusPaying)
|
||||
case "CLOSED", "PAYERROR", "REFUND":
|
||||
payStatus = int32(model.PayStatusFailed)
|
||||
default:
|
||||
payStatus = int32(model.PayStatusPending)
|
||||
}
|
||||
|
||||
return &pb.PayStatusResponse{
|
||||
Status: payStatus,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *PayServiceServer) HandlePayCallback(ctx context.Context, req *pb.PayCallbackRequest) (*pb.PayCallbackResponse, error) {
|
||||
callback, err := s.weChatPay.ParseCallback([]byte(req.Body))
|
||||
if err != nil {
|
||||
return &pb.PayCallbackResponse{
|
||||
ReturnCode: "FAIL",
|
||||
ReturnMsg: "parse error",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if !s.weChatPay.ValidateCallback(callback) {
|
||||
return &pb.PayCallbackResponse{
|
||||
ReturnCode: "FAIL",
|
||||
ReturnMsg: "validate error",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if callback.ReturnCode == "SUCCESS" && callback.ResultCode == "SUCCESS" {
|
||||
// Check if payment already processed (idempotency)
|
||||
payment, _ := s.paymentRepo.GetByOrderNo(ctx, callback.OutTradeNo)
|
||||
if payment != nil && payment.PayStatus == model.PayStatusSuccess {
|
||||
// Already processed, return success
|
||||
return &pb.PayCallbackResponse{
|
||||
ReturnCode: "SUCCESS",
|
||||
ReturnMsg: "OK",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Update payment status
|
||||
if err := s.paymentRepo.UpdateStatus(ctx, callback.OutTradeNo, model.PayStatusSuccess, callback.TransactionID); err != nil {
|
||||
return &pb.PayCallbackResponse{
|
||||
ReturnCode: "FAIL",
|
||||
ReturnMsg: "update payment failed",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Update order status - pay order triggers print job creation
|
||||
if err := s.orderService.PayOrder(ctx, callback.OutTradeNo, "wechat"); err != nil {
|
||||
return &pb.PayCallbackResponse{
|
||||
ReturnCode: "FAIL",
|
||||
ReturnMsg: "pay order failed",
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return &pb.PayCallbackResponse{
|
||||
ReturnCode: "SUCCESS",
|
||||
ReturnMsg: "OK",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *PayServiceServer) PayByBalance(ctx context.Context, req *pb.PayByBalanceRequest) (*pb.StatusResponse, error) {
|
||||
// Get order
|
||||
order, err := s.orderService.GetOrder(ctx, req.OrderNo)
|
||||
if err != nil || order == nil {
|
||||
return &pb.StatusResponse{
|
||||
Code: 1,
|
||||
Message: "order not found",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Check if order is pending
|
||||
if order.Status != model.OrderStatusPending {
|
||||
return &pb.StatusResponse{
|
||||
Code: 1,
|
||||
Message: "order is not pending",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Get user
|
||||
user, err := s.userRepo.GetByID(ctx, req.UserId)
|
||||
if err != nil || user == nil {
|
||||
return &pb.StatusResponse{
|
||||
Code: 1,
|
||||
Message: "user not found",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Check balance
|
||||
if user.Balance < order.TotalAmount {
|
||||
return &pb.StatusResponse{
|
||||
Code: 1,
|
||||
Message: "insufficient balance",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Deduct balance
|
||||
if err := s.userRepo.DeductBalance(ctx, req.UserId, order.TotalAmount); err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "deduct balance failed: %v", err)
|
||||
}
|
||||
|
||||
// Create payment record
|
||||
payment := &model.Payment{
|
||||
OrderNo: order.OrderNo,
|
||||
PayType: "balance",
|
||||
PayStatus: model.PayStatusSuccess,
|
||||
TransactionID: "balance_" + order.OrderNo,
|
||||
}
|
||||
if err := s.paymentRepo.Create(ctx, payment); err != nil {
|
||||
// Rollback balance deduction on failure
|
||||
s.userRepo.AddBalance(ctx, req.UserId, order.TotalAmount)
|
||||
return nil, status.Errorf(codes.Internal, "create payment record failed: %v", err)
|
||||
}
|
||||
|
||||
// Pay order - this creates print jobs
|
||||
if err := s.orderService.PayOrder(ctx, req.OrderNo, "balance"); err != nil {
|
||||
// Rollback
|
||||
s.userRepo.AddBalance(ctx, req.UserId, order.TotalAmount)
|
||||
return nil, status.Errorf(codes.Internal, "pay order failed: %v", err)
|
||||
}
|
||||
|
||||
return &pb.StatusResponse{
|
||||
Code: 0,
|
||||
Message: "success",
|
||||
}, nil
|
||||
}
|
||||
96
cloudprint-backend/internal/grpc/service/price_service.go
Normal file
96
cloudprint-backend/internal/grpc/service/price_service.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cloudprint/cloudprint-backend/internal/repository"
|
||||
pb "github.com/cloudprint/cloudprint-backend/proto/generated"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
type PriceServiceServer struct {
|
||||
pb.UnimplementedPriceServiceServer
|
||||
priceRepo *repository.PriceRepository
|
||||
}
|
||||
|
||||
func NewPriceServiceServer(priceRepo *repository.PriceRepository) *PriceServiceServer {
|
||||
return &PriceServiceServer{
|
||||
priceRepo: priceRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PriceServiceServer) GetPriceList(ctx context.Context, req *pb.GetPriceListRequest) (*pb.PriceListResponse, error) {
|
||||
items, err := s.priceRepo.GetAllActive(ctx)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "get price list failed: %v", err)
|
||||
}
|
||||
|
||||
pbItems := make([]*pb.PriceItem, 0, len(items))
|
||||
for _, item := range items {
|
||||
pbItems = append(pbItems, &pb.PriceItem{
|
||||
Id: item.ID,
|
||||
Name: item.Name,
|
||||
Type: item.Type,
|
||||
Price: item.Price,
|
||||
Unit: item.Unit,
|
||||
ColorEnabled: item.ColorEnabled,
|
||||
IsActive: item.IsActive,
|
||||
})
|
||||
}
|
||||
|
||||
return &pb.PriceListResponse{Items: pbItems}, nil
|
||||
}
|
||||
|
||||
func (s *PriceServiceServer) CreatePriceItem(ctx context.Context, req *pb.CreatePriceItemRequest) (*pb.PriceItem, error) {
|
||||
item := &model.PriceList{
|
||||
Name: req.Name,
|
||||
Type: req.Type,
|
||||
Price: req.Price,
|
||||
Unit: req.Unit,
|
||||
ColorEnabled: req.ColorEnabled,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
if err := s.priceRepo.Create(ctx, item); err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "create price item failed: %v", err)
|
||||
}
|
||||
|
||||
return &pb.PriceItem{
|
||||
Id: item.ID,
|
||||
Name: item.Name,
|
||||
Type: item.Type,
|
||||
Price: item.Price,
|
||||
Unit: item.Unit,
|
||||
ColorEnabled: item.ColorEnabled,
|
||||
IsActive: item.IsActive,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *PriceServiceServer) UpdatePriceItem(ctx context.Context, req *pb.UpdatePriceItemRequest) (*pb.PriceItem, error) {
|
||||
item, err := s.priceRepo.GetByID(ctx, req.Id)
|
||||
if err != nil || item == nil {
|
||||
return nil, status.Errorf(codes.NotFound, "price item not found")
|
||||
}
|
||||
|
||||
item.Name = req.Name
|
||||
item.Type = req.Type
|
||||
item.Price = req.Price
|
||||
item.Unit = req.Unit
|
||||
item.ColorEnabled = req.ColorEnabled
|
||||
item.IsActive = req.IsActive
|
||||
|
||||
if err := s.priceRepo.Update(ctx, item); err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "update price item failed: %v", err)
|
||||
}
|
||||
|
||||
return &pb.PriceItem{
|
||||
Id: item.ID,
|
||||
Name: item.Name,
|
||||
Type: item.Type,
|
||||
Price: item.Price,
|
||||
Unit: item.Unit,
|
||||
ColorEnabled: item.ColorEnabled,
|
||||
IsActive: item.IsActive,
|
||||
}, nil
|
||||
}
|
||||
136
cloudprint-backend/internal/grpc/service/print_service.go
Normal file
136
cloudprint-backend/internal/grpc/service/print_service.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cloudprint/cloudprint-backend/internal/service"
|
||||
pb "github.com/cloudprint/cloudprint-backend/proto/generated"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
type PrintServiceServer struct {
|
||||
pb.UnimplementedPrintServiceServer
|
||||
printService *service.PrintService
|
||||
}
|
||||
|
||||
func NewPrintServiceServer(printService *service.PrintService) *PrintServiceServer {
|
||||
return &PrintServiceServer{
|
||||
printService: printService,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PrintServiceServer) GetPrinters(ctx context.Context, req *pb.GetPrintersRequest) (*pb.PrinterListResponse, error) {
|
||||
printers, err := s.printService.GetPrinters(ctx)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "get printers failed: %v", err)
|
||||
}
|
||||
|
||||
pbPrinters := make([]*pb.Printer, 0, len(printers))
|
||||
for _, printer := range printers {
|
||||
pbPrinters = append(pbPrinters, &pb.Printer{
|
||||
Id: printer.ID,
|
||||
Name: printer.Name,
|
||||
DisplayName: printer.DisplayName,
|
||||
IsActive: printer.IsActive,
|
||||
Status: int32(printer.Status),
|
||||
})
|
||||
}
|
||||
|
||||
return &pb.PrinterListResponse{Printers: pbPrinters}, nil
|
||||
}
|
||||
|
||||
func (s *PrintServiceServer) GetPrinterStatus(ctx context.Context, req *pb.GetPrinterStatusRequest) (*pb.PrinterStatusResponse, error) {
|
||||
printer, queueCount, err := s.printService.GetPrinterStatus(ctx, req.PrinterId)
|
||||
if err != nil || printer == nil {
|
||||
return nil, status.Errorf(codes.NotFound, "printer not found")
|
||||
}
|
||||
|
||||
return &pb.PrinterStatusResponse{
|
||||
Printer: &pb.Printer{
|
||||
Id: printer.ID,
|
||||
Name: printer.Name,
|
||||
DisplayName: printer.DisplayName,
|
||||
IsActive: printer.IsActive,
|
||||
Status: int32(printer.Status),
|
||||
},
|
||||
QueueCount: int32(queueCount),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *PrintServiceServer) RegisterPrinter(ctx context.Context, req *pb.RegisterPrinterRequest) (*pb.RegisterPrinterResponse, error) {
|
||||
printer, token, err := s.printService.RegisterPrinter(ctx, req.Name, req.DisplayName, req.ApiKey)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "register printer failed: %v", err)
|
||||
}
|
||||
if printer == nil {
|
||||
return nil, status.Errorf(codes.AlreadyExists, "printer already exists")
|
||||
}
|
||||
|
||||
return &pb.RegisterPrinterResponse{
|
||||
PrinterId: printer.ID,
|
||||
Token: token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *PrintServiceServer) PrinterHeartbeat(ctx context.Context, req *pb.PrinterHeartbeatRequest) (*pb.Empty, error) {
|
||||
err := s.printService.PrinterHeartbeat(ctx, req.PrinterId, int8(req.Status), req.Token)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "heartbeat failed: %v", err)
|
||||
}
|
||||
|
||||
return &pb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *PrintServiceServer) SubmitPrintJob(ctx context.Context, req *pb.SubmitPrintJobRequest) (*pb.SubmitPrintJobResponse, error) {
|
||||
job, err := s.printService.SubmitPrintJob(ctx, req.OrderItemId, req.PrinterId, "", int(req.Copies), req.Color, req.Duplex, req.PageRange)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "submit print job failed: %v", err)
|
||||
}
|
||||
|
||||
return &pb.SubmitPrintJobResponse{
|
||||
JobNo: job.JobNo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *PrintServiceServer) CancelPrintJob(ctx context.Context, req *pb.CancelPrintJobRequest) (*pb.Empty, error) {
|
||||
err := s.printService.CancelPrintJob(ctx, req.JobNo)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "cancel print job failed: %v", err)
|
||||
}
|
||||
|
||||
return &pb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *PrintServiceServer) SubscribeJobs(req *pb.SubscribeJobsRequest, stream pb.PrintService_SubscribeJobsServer) error {
|
||||
ch, err := s.printService.SubscribeJobs(stream.Context(), req.PrinterId, req.Token)
|
||||
if err != nil {
|
||||
return status.Errorf(codes.Internal, "subscribe jobs failed: %v", err)
|
||||
}
|
||||
|
||||
for job := range ch {
|
||||
if err := stream.Send(&pb.PrintJob{
|
||||
Id: job.ID,
|
||||
JobNo: job.JobNo,
|
||||
PrinterId: job.PrinterID,
|
||||
OrderItemId: job.OrderItemID,
|
||||
Status: job.Status,
|
||||
Progress: int32(job.Progress),
|
||||
ErrorMsg: job.ErrorMsg,
|
||||
}); err != nil {
|
||||
s.printService.UnsubscribeJobs(req.PrinterId)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PrintServiceServer) ReportJobStatus(ctx context.Context, req *pb.ReportJobStatusRequest) (*pb.Empty, error) {
|
||||
err := s.printService.ReportJobStatus(ctx, req.JobNo, req.Status, int(req.Progress), req.ErrorMsg)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "report job status failed: %v", err)
|
||||
}
|
||||
|
||||
return &pb.Empty{}, nil
|
||||
}
|
||||
224
cloudprint-backend/internal/model/model.go
Normal file
224
cloudprint-backend/internal/model/model.go
Normal file
@@ -0,0 +1,224 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// User 用户表
|
||||
type User struct {
|
||||
ID int32 `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
Openid string `gorm:"uniqueIndex;size:64;not null" json:"openid"` // 微信OpenID
|
||||
Nickname string `gorm:"size:128;default:''" json:"nickname"`
|
||||
Avatar string `gorm:"size:512;default:''" json:"avatar"`
|
||||
Balance float64 `gorm:"type:decimal(10,2);default:0.00" json:"balance"` // 余额
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
Orders []Order `gorm:"foreignKey:UserID" json:"orders,omitempty"`
|
||||
}
|
||||
|
||||
func (User) TableName() string {
|
||||
return "user"
|
||||
}
|
||||
|
||||
// Admin 管理员表
|
||||
type Admin struct {
|
||||
ID int32 `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
Username string `gorm:"uniqueIndex;size:64;not null" json:"username"`
|
||||
PasswordHash string `gorm:"size:256;not null" json:"-"`
|
||||
Nickname string `gorm:"size:128;default:''" json:"nickname"`
|
||||
Role int8 `gorm:"default:1" json:"role"` // 1:普通管理员 2:超级管理员
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
}
|
||||
|
||||
func (Admin) TableName() string {
|
||||
return "admin"
|
||||
}
|
||||
|
||||
// Printer 打印机表
|
||||
type Printer struct {
|
||||
ID int32 `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
Name string `gorm:"uniqueIndex;size:128;not null" json:"name"` // 系统打印机名
|
||||
DisplayName string `gorm:"size:128;not null" json:"display_name"` // 显示名称
|
||||
APIKey string `gorm:"uniqueIndex;size:64;not null" json:"api_key"` // 客户端认证Key
|
||||
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||
Status int8 `gorm:"default:0" json:"status"` // 0:空闲 1:打印中 2:离线
|
||||
LastHeartbeat *time.Time `json:"last_heartbeat"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
PrintJobs []PrintJob `gorm:"foreignKey:PrinterID" json:"print_jobs,omitempty"`
|
||||
}
|
||||
|
||||
func (Printer) TableName() string {
|
||||
return "printer"
|
||||
}
|
||||
|
||||
// Order 订单表
|
||||
type Order struct {
|
||||
ID int32 `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
OrderNo string `gorm:"uniqueIndex;size:32;not null" json:"order_no"` // 订单号
|
||||
UserID int32 `gorm:"not null;index" json:"user_id"`
|
||||
Status int8 `gorm:"default:0" json:"status"` // 0:待支付 1:已支付 2:打印中 3:已完成 4:已取消
|
||||
TotalAmount float64 `gorm:"type:decimal(10,2);not null" json:"total_amount"`
|
||||
Remark string `gorm:"size:512;default:''" json:"remark"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
PaidAt *time.Time `json:"paid_at"`
|
||||
User *User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
Items []OrderItem `gorm:"foreignKey:OrderID" json:"items,omitempty"`
|
||||
Payment *Payment `gorm:"foreignKey:OrderNo;references:OrderNo" json:"payment,omitempty"`
|
||||
}
|
||||
|
||||
func (Order) TableName() string {
|
||||
return "order"
|
||||
}
|
||||
|
||||
// OrderItem 订单明细表
|
||||
type OrderItem struct {
|
||||
ID int32 `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
OrderID int32 `gorm:"not null;index" json:"order_id"`
|
||||
FileID int32 `gorm:"not null" json:"file_id"`
|
||||
PrinterID *int32 `json:"printer_id"`
|
||||
Copies int `gorm:"default:1" json:"copies"`
|
||||
Color bool `gorm:"default:false" json:"color"`
|
||||
Duplex bool `gorm:"default:false" json:"duplex"`
|
||||
PageRange string `gorm:"size:64;default:'all'" json:"page_range"`
|
||||
Price float64 `gorm:"type:decimal(10,2);not null" json:"price"`
|
||||
Order *Order `gorm:"foreignKey:OrderID" json:"order,omitempty"`
|
||||
File *File `gorm:"foreignKey:FileID" json:"file,omitempty"`
|
||||
Printer *Printer `gorm:"foreignKey:PrinterID" json:"printer,omitempty"`
|
||||
PrintJobs []PrintJob `gorm:"foreignKey:OrderItemID" json:"print_jobs,omitempty"`
|
||||
}
|
||||
|
||||
func (OrderItem) TableName() string {
|
||||
return "order_item"
|
||||
}
|
||||
|
||||
// File 文件表
|
||||
type File struct {
|
||||
ID int32 `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
FileName string `gorm:"size:256;not null" json:"file_name"`
|
||||
FilePath string `gorm:"size:512;not null" json:"file_path"`
|
||||
FileType string `gorm:"size:32;not null" json:"file_type"` // pdf/doc/docx/xls/xlsx/ppt/pptx/txt/jpg/png/zip
|
||||
FileSize int64 `gorm:"not null" json:"file_size"` // 字节
|
||||
UploaderType string `gorm:"size:16;not null" json:"uploader_type"` // user/admin/system
|
||||
UploaderID int32 `gorm:"not null" json:"uploader_id"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
}
|
||||
|
||||
func (File) TableName() string {
|
||||
return "file"
|
||||
}
|
||||
|
||||
// LibraryCategory 文库分类表
|
||||
type LibraryCategory struct {
|
||||
ID int32 `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
Name string `gorm:"size:128;not null" json:"name"`
|
||||
Icon string `gorm:"size:128;default:''" json:"icon"`
|
||||
SortOrder int `gorm:"default:0" json:"sort_order"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
Files []LibraryFile `gorm:"foreignKey:CategoryID" json:"files,omitempty"`
|
||||
}
|
||||
|
||||
func (LibraryCategory) TableName() string {
|
||||
return "library_category"
|
||||
}
|
||||
|
||||
// LibraryFile 文库文件表
|
||||
type LibraryFile struct {
|
||||
ID int32 `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
CategoryID int32 `gorm:"not null;index" json:"category_id"`
|
||||
FileID int32 `gorm:"not null" json:"file_id"`
|
||||
Title string `gorm:"size:256;not null" json:"title"`
|
||||
Description string `gorm:"size:512;default:''" json:"description"`
|
||||
PageCount int `gorm:"default:0" json:"page_count"`
|
||||
DownloadCount int `gorm:"default:0" json:"download_count"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
Category *LibraryCategory `gorm:"foreignKey:CategoryID" json:"category,omitempty"`
|
||||
File *File `gorm:"foreignKey:FileID" json:"file,omitempty"`
|
||||
}
|
||||
|
||||
func (LibraryFile) TableName() string {
|
||||
return "library_file"
|
||||
}
|
||||
|
||||
// PrintJob 打印任务表
|
||||
type PrintJob struct {
|
||||
ID int32 `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
JobNo string `gorm:"uniqueIndex;size:32;not null" json:"job_no"`
|
||||
PrinterID int32 `gorm:"not null;index" json:"printer_id"`
|
||||
OrderItemID int32 `gorm:"not null;index" json:"order_item_id"`
|
||||
Status string `gorm:"size:16;default:'pending'" json:"status"` // pending/printing/completed/failed/cancelled
|
||||
Progress int `gorm:"default:0" json:"progress"`
|
||||
ErrorMsg string `gorm:"size:512;default:''" json:"error_msg"`
|
||||
StartedAt *time.Time `json:"started_at"`
|
||||
CompletedAt *time.Time `json:"completed_at"`
|
||||
Printer *Printer `gorm:"foreignKey:PrinterID" json:"printer,omitempty"`
|
||||
OrderItem *OrderItem `gorm:"foreignKey:OrderItemID" json:"order_item,omitempty"`
|
||||
}
|
||||
|
||||
func (PrintJob) TableName() string {
|
||||
return "print_job"
|
||||
}
|
||||
|
||||
// PriceList 价目表
|
||||
type PriceList struct {
|
||||
ID int32 `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
Name string `gorm:"size:128;not null" json:"name"` // 项目名称
|
||||
Type string `gorm:"size:32;not null" json:"type"` // 纸张类型: A4/A3/照片纸
|
||||
Price float64 `gorm:"type:decimal(10,2);not null" json:"price"` // 单价
|
||||
Unit string `gorm:"size:16;not null" json:"unit"` // 单位: 页/张/份
|
||||
ColorEnabled bool `gorm:"default:true" json:"color_enabled"` // 是否支持彩色
|
||||
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||
SortOrder int `gorm:"default:0" json:"sort_order"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
}
|
||||
|
||||
func (PriceList) TableName() string {
|
||||
return "price_list"
|
||||
}
|
||||
|
||||
// Payment 支付记录表
|
||||
type Payment struct {
|
||||
ID int32 `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
OrderNo string `gorm:"uniqueIndex;size:32;not null" json:"order_no"`
|
||||
PayType string `gorm:"size:16;default:'wechat'" json:"pay_type"` // wechat/balance
|
||||
PayStatus int8 `gorm:"default:0" json:"pay_status"` // 0:待支付 1:支付中 2:成功 3:失败
|
||||
TransactionID string `gorm:"size:64;default:''" json:"transaction_id"`
|
||||
PaidAt *time.Time `json:"paid_at"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
}
|
||||
|
||||
func (Payment) TableName() string {
|
||||
return "payment"
|
||||
}
|
||||
|
||||
// OrderStatus 订单状态常量
|
||||
const (
|
||||
OrderStatusPending = 0 // 待支付
|
||||
OrderStatusPaid = 1 // 已支付
|
||||
OrderStatusPrinting = 2 // 打印中
|
||||
OrderStatusCompleted = 3 // 已完成
|
||||
OrderStatusCancelled = 4 // 已取消
|
||||
)
|
||||
|
||||
// PayStatus 支付状态常量
|
||||
const (
|
||||
PayStatusPending = 0 // 待支付
|
||||
PayStatusPaying = 1 // 支付中
|
||||
PayStatusSuccess = 2 // 成功
|
||||
PayStatusFailed = 3 // 失败
|
||||
)
|
||||
|
||||
// PrinterStatus 打印机状态常量
|
||||
const (
|
||||
PrinterStatusIdle = 0 // 空闲
|
||||
PrinterStatusPrinting = 1 // 打印中
|
||||
PrinterStatusOffline = 2 // 离线
|
||||
)
|
||||
|
||||
// PrintJobStatus 打印任务状态常量
|
||||
const (
|
||||
PrintJobStatusPending = "pending"
|
||||
PrintJobStatusPrinting = "printing"
|
||||
PrintJobStatusCompleted = "completed"
|
||||
PrintJobStatusFailed = "failed"
|
||||
PrintJobStatusCancelled = "cancelled"
|
||||
)
|
||||
49
cloudprint-backend/internal/repository/admin_repository.go
Normal file
49
cloudprint-backend/internal/repository/admin_repository.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/cloudprint/cloudprint-backend/internal/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AdminRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewAdminRepository(db *gorm.DB) *AdminRepository {
|
||||
return &AdminRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *AdminRepository) Create(ctx context.Context, admin *model.Admin) error {
|
||||
return r.db.WithContext(ctx).Create(admin).Error
|
||||
}
|
||||
|
||||
func (r *AdminRepository) GetByID(ctx context.Context, id int32) (*model.Admin, error) {
|
||||
var admin model.Admin
|
||||
err := r.db.WithContext(ctx).First(&admin, id).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &admin, nil
|
||||
}
|
||||
|
||||
func (r *AdminRepository) GetByUsername(ctx context.Context, username string) (*model.Admin, error) {
|
||||
var admin model.Admin
|
||||
err := r.db.WithContext(ctx).Where("username = ?", username).First(&admin).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &admin, nil
|
||||
}
|
||||
|
||||
func (r *AdminRepository) Update(ctx context.Context, admin *model.Admin) error {
|
||||
return r.db.WithContext(ctx).Save(admin).Error
|
||||
}
|
||||
67
cloudprint-backend/internal/repository/file_repository.go
Normal file
67
cloudprint-backend/internal/repository/file_repository.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/cloudprint/cloudprint-backend/internal/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type FileRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewFileRepository(db *gorm.DB) *FileRepository {
|
||||
return &FileRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *FileRepository) Create(ctx context.Context, file *model.File) error {
|
||||
return r.db.WithContext(ctx).Create(file).Error
|
||||
}
|
||||
|
||||
func (r *FileRepository) GetByID(ctx context.Context, id int32) (*model.File, error) {
|
||||
var file model.File
|
||||
err := r.db.WithContext(ctx).First(&file, id).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &file, nil
|
||||
}
|
||||
|
||||
func (r *FileRepository) Delete(ctx context.Context, id int32) error {
|
||||
return r.db.WithContext(ctx).Delete(&model.File{}, id).Error
|
||||
}
|
||||
|
||||
func (r *FileRepository) GetListByUploader(ctx context.Context, uploaderType string, uploaderID int32, page, pageSize int) ([]*model.File, int64, error) {
|
||||
var files []*model.File
|
||||
var total int64
|
||||
|
||||
query := r.db.WithContext(ctx).Model(&model.File{})
|
||||
if uploaderType != "" {
|
||||
query = query.Where("uploader_type = ?", uploaderType)
|
||||
}
|
||||
if uploaderID > 0 {
|
||||
query = query.Where("uploader_id = ?", uploaderID)
|
||||
}
|
||||
|
||||
err := query.Count(&total).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
err = query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&files).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return files, total, nil
|
||||
}
|
||||
|
||||
func (r *FileRepository) Update(ctx context.Context, file *model.File) error {
|
||||
return r.db.WithContext(ctx).Save(file).Error
|
||||
}
|
||||
108
cloudprint-backend/internal/repository/library_repository.go
Normal file
108
cloudprint-backend/internal/repository/library_repository.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/cloudprint/cloudprint-backend/internal/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type LibraryRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewLibraryRepository(db *gorm.DB) *LibraryRepository {
|
||||
return &LibraryRepository{db: db}
|
||||
}
|
||||
|
||||
// Category operations
|
||||
func (r *LibraryRepository) CreateCategory(ctx context.Context, category *model.LibraryCategory) error {
|
||||
return r.db.WithContext(ctx).Create(category).Error
|
||||
}
|
||||
|
||||
func (r *LibraryRepository) GetCategoryByID(ctx context.Context, id int32) (*model.LibraryCategory, error) {
|
||||
var category model.LibraryCategory
|
||||
err := r.db.WithContext(ctx).First(&category, id).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &category, nil
|
||||
}
|
||||
|
||||
func (r *LibraryRepository) GetAllCategories(ctx context.Context) ([]*model.LibraryCategory, error) {
|
||||
var categories []*model.LibraryCategory
|
||||
err := r.db.WithContext(ctx).Order("sort_order ASC, id ASC").Find(&categories).Error
|
||||
return categories, err
|
||||
}
|
||||
|
||||
func (r *LibraryRepository) UpdateCategory(ctx context.Context, category *model.LibraryCategory) error {
|
||||
return r.db.WithContext(ctx).Save(category).Error
|
||||
}
|
||||
|
||||
func (r *LibraryRepository) DeleteCategory(ctx context.Context, id int32) error {
|
||||
return r.db.WithContext(ctx).Delete(&model.LibraryCategory{}, id).Error
|
||||
}
|
||||
|
||||
// LibraryFile operations
|
||||
func (r *LibraryRepository) CreateFile(ctx context.Context, file *model.LibraryFile) error {
|
||||
return r.db.WithContext(ctx).Create(file).Error
|
||||
}
|
||||
|
||||
func (r *LibraryRepository) GetFileByID(ctx context.Context, id int32) (*model.LibraryFile, error) {
|
||||
var file model.LibraryFile
|
||||
err := r.db.WithContext(ctx).Preload("File").First(&file, id).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &file, nil
|
||||
}
|
||||
|
||||
func (r *LibraryRepository) GetFilesByCategory(ctx context.Context, categoryID int32, page, pageSize int) ([]*model.LibraryFile, int64, error) {
|
||||
var files []*model.LibraryFile
|
||||
var total int64
|
||||
|
||||
query := r.db.WithContext(ctx).Model(&model.LibraryFile{}).Where("category_id = ?", categoryID)
|
||||
err := query.Count(&total).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
err = query.Preload("File").Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&files).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return files, total, nil
|
||||
}
|
||||
|
||||
func (r *LibraryRepository) DeleteFile(ctx context.Context, id int32) error {
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
// Get file to delete underlying file record
|
||||
var libFile model.LibraryFile
|
||||
if err := tx.First(&libFile, id).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// Delete library file
|
||||
if err := tx.Delete(&libFile).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// Delete underlying file
|
||||
if err := tx.Delete(&model.File{}, libFile.FileID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *LibraryRepository) IncrementDownloadCount(ctx context.Context, id int32) error {
|
||||
return r.db.WithContext(ctx).Model(&model.LibraryFile{}).Where("id = ?", id).
|
||||
Update("download_count", gorm.Expr("download_count + 1")).Error
|
||||
}
|
||||
162
cloudprint-backend/internal/repository/order_repository.go
Normal file
162
cloudprint-backend/internal/repository/order_repository.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/cloudprint/cloudprint-backend/internal/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type OrderRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewOrderRepository(db *gorm.DB) *OrderRepository {
|
||||
return &OrderRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *OrderRepository) Create(ctx context.Context, order *model.Order) error {
|
||||
return r.db.WithContext(ctx).Create(order).Error
|
||||
}
|
||||
|
||||
func (r *OrderRepository) CreateWithItems(ctx context.Context, order *model.Order, items []*model.OrderItem) error {
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Create(order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range items {
|
||||
item.OrderID = order.ID
|
||||
if err := tx.Create(item).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *OrderRepository) GetByID(ctx context.Context, id int32) (*model.Order, error) {
|
||||
var order model.Order
|
||||
err := r.db.WithContext(ctx).Preload("Items").Preload("Items.File").Preload("Payment").
|
||||
First(&order, id).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &order, nil
|
||||
}
|
||||
|
||||
func (r *OrderRepository) GetByOrderNo(ctx context.Context, orderNo string) (*model.Order, error) {
|
||||
var order model.Order
|
||||
err := r.db.WithContext(ctx).Preload("Items").Preload("Items.File").Preload("Items.Printer").Preload("Payment").
|
||||
Where("order_no = ?", orderNo).First(&order).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &order, nil
|
||||
}
|
||||
|
||||
func (r *OrderRepository) GetListByUserID(ctx context.Context, userID int32, status int8, page, pageSize int) ([]*model.Order, int64, error) {
|
||||
var orders []*model.Order
|
||||
var total int64
|
||||
|
||||
query := r.db.WithContext(ctx).Model(&model.Order{}).Where("user_id = ?", userID)
|
||||
if status >= 0 {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
|
||||
err := query.Count(&total).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
err = query.Preload("Items").Preload("Items.File").
|
||||
Offset(offset).Limit(pageSize).Order("created_at DESC").
|
||||
Find(&orders).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return orders, total, nil
|
||||
}
|
||||
|
||||
func (r *OrderRepository) UpdateStatus(ctx context.Context, orderNo string, status int8) error {
|
||||
updates := map[string]interface{}{
|
||||
"status": status,
|
||||
}
|
||||
if status == model.OrderStatusPaid {
|
||||
now := time.Now()
|
||||
updates["paid_at"] = &now
|
||||
}
|
||||
return r.db.WithContext(ctx).Model(&model.Order{}).
|
||||
Where("order_no = ?", orderNo).Updates(updates).Error
|
||||
}
|
||||
|
||||
func (r *OrderRepository) GetStatistics(ctx context.Context) (totalUsers, totalOrders, todayOrders int32, todayRevenue float64, pendingOrders int32, err error) {
|
||||
// 总用户数
|
||||
if err = r.db.WithContext(ctx).Model(&model.User{}).Count(&totalUsers).Error; err != nil {
|
||||
return 0, 0, 0, 0, 0, fmt.Errorf("count users failed: %w", err)
|
||||
}
|
||||
// 总订单数
|
||||
if err = r.db.WithContext(ctx).Model(&model.Order{}).Count(&totalOrders).Error; err != nil {
|
||||
return 0, 0, 0, 0, 0, fmt.Errorf("count orders failed: %w", err)
|
||||
}
|
||||
// 待处理订单数
|
||||
if err = r.db.WithContext(ctx).Model(&model.Order{}).Where("status IN ?", []int8{0, 1, 2}).Count(&pendingOrders).Error; err != nil {
|
||||
return 0, 0, 0, 0, 0, fmt.Errorf("count pending orders failed: %w", err)
|
||||
}
|
||||
// 今日订单数
|
||||
today := time.Now().Format("2006-01-02")
|
||||
if err = r.db.WithContext(ctx).Model(&model.Order{}).
|
||||
Where("DATE(created_at) = ? AND status = ?", today, model.OrderStatusPaid).
|
||||
Count(&todayOrders).Error; err != nil {
|
||||
return 0, 0, 0, 0, 0, fmt.Errorf("count today orders failed: %w", err)
|
||||
}
|
||||
// 今日收入
|
||||
if err = r.db.WithContext(ctx).Model(&model.Order{}).
|
||||
Select("COALESCE(SUM(total_amount), 0)").
|
||||
Where("DATE(created_at) = ? AND status = ?", today, model.OrderStatusPaid).
|
||||
Scan(&todayRevenue).Error; err != nil {
|
||||
return 0, 0, 0, 0, 0, fmt.Errorf("sum today revenue failed: %w", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type OrderItemRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewOrderItemRepository(db *gorm.DB) *OrderItemRepository {
|
||||
return &OrderItemRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *OrderItemRepository) GetByID(ctx context.Context, id int32) (*model.OrderItem, error) {
|
||||
var item model.OrderItem
|
||||
err := r.db.WithContext(ctx).Preload("File").First(&item, id).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func (r *OrderItemRepository) Create(ctx context.Context, item *model.OrderItem) error {
|
||||
return r.db.WithContext(ctx).Create(item).Error
|
||||
}
|
||||
|
||||
func (r *OrderItemRepository) GetByOrderID(ctx context.Context, orderID int32) ([]*model.OrderItem, error) {
|
||||
var items []*model.OrderItem
|
||||
err := r.db.WithContext(ctx).Where("order_id = ?", orderID).Find(&items).Error
|
||||
return items, err
|
||||
}
|
||||
80
cloudprint-backend/internal/repository/price_repository.go
Normal file
80
cloudprint-backend/internal/repository/price_repository.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/cloudprint/cloudprint-backend/internal/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type PriceRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewPriceRepository(db *gorm.DB) *PriceRepository {
|
||||
return &PriceRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *PriceRepository) Create(ctx context.Context, item *model.PriceList) error {
|
||||
return r.db.WithContext(ctx).Create(item).Error
|
||||
}
|
||||
|
||||
func (r *PriceRepository) GetByID(ctx context.Context, id int32) (*model.PriceList, error) {
|
||||
var item model.PriceList
|
||||
err := r.db.WithContext(ctx).First(&item, id).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func (r *PriceRepository) GetAllActive(ctx context.Context) ([]*model.PriceList, error) {
|
||||
var items []*model.PriceList
|
||||
err := r.db.WithContext(ctx).Where("is_active = ?", true).Order("sort_order ASC, id ASC").Find(&items).Error
|
||||
return items, err
|
||||
}
|
||||
|
||||
func (r *PriceRepository) Update(ctx context.Context, item *model.PriceList) error {
|
||||
return r.db.WithContext(ctx).Save(item).Error
|
||||
}
|
||||
|
||||
func (r *PriceRepository) Delete(ctx context.Context, id int32) error {
|
||||
return r.db.WithContext(ctx).Delete(&model.PriceList{}, id).Error
|
||||
}
|
||||
|
||||
type PaymentRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewPaymentRepository(db *gorm.DB) *PaymentRepository {
|
||||
return &PaymentRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *PaymentRepository) Create(ctx context.Context, payment *model.Payment) error {
|
||||
return r.db.WithContext(ctx).Create(payment).Error
|
||||
}
|
||||
|
||||
func (r *PaymentRepository) GetByOrderNo(ctx context.Context, orderNo string) (*model.Payment, error) {
|
||||
var payment model.Payment
|
||||
err := r.db.WithContext(ctx).Where("order_no = ?", orderNo).First(&payment).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &payment, nil
|
||||
}
|
||||
|
||||
func (r *PaymentRepository) UpdateStatus(ctx context.Context, orderNo string, status int8, transactionID string) error {
|
||||
updates := map[string]interface{}{
|
||||
"pay_status": status,
|
||||
"transaction_id": transactionID,
|
||||
}
|
||||
return r.db.WithContext(ctx).Model(&model.Payment{}).
|
||||
Where("order_no = ?", orderNo).Updates(updates).Error
|
||||
}
|
||||
150
cloudprint-backend/internal/repository/printer_repository.go
Normal file
150
cloudprint-backend/internal/repository/printer_repository.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/cloudprint/cloudprint-backend/internal/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type PrinterRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewPrinterRepository(db *gorm.DB) *PrinterRepository {
|
||||
return &PrinterRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *PrinterRepository) Create(ctx context.Context, printer *model.Printer) error {
|
||||
return r.db.WithContext(ctx).Create(printer).Error
|
||||
}
|
||||
|
||||
func (r *PrinterRepository) GetByID(ctx context.Context, id int32) (*model.Printer, error) {
|
||||
var printer model.Printer
|
||||
err := r.db.WithContext(ctx).First(&printer, id).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &printer, nil
|
||||
}
|
||||
|
||||
func (r *PrinterRepository) GetByName(ctx context.Context, name string) (*model.Printer, error) {
|
||||
var printer model.Printer
|
||||
err := r.db.WithContext(ctx).Where("name = ?", name).First(&printer).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &printer, nil
|
||||
}
|
||||
|
||||
func (r *PrinterRepository) GetByAPIKey(ctx context.Context, apiKey string) (*model.Printer, error) {
|
||||
var printer model.Printer
|
||||
err := r.db.WithContext(ctx).Where("api_key = ?", apiKey).First(&printer).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &printer, nil
|
||||
}
|
||||
|
||||
func (r *PrinterRepository) GetAllActive(ctx context.Context) ([]*model.Printer, error) {
|
||||
var printers []*model.Printer
|
||||
err := r.db.WithContext(ctx).Where("is_active = ?", true).Find(&printers).Error
|
||||
return printers, err
|
||||
}
|
||||
|
||||
func (r *PrinterRepository) UpdateStatus(ctx context.Context, id int32, status int8) error {
|
||||
return r.db.WithContext(ctx).Model(&model.Printer{}).Where("id = ?", id).Update("status", status).Error
|
||||
}
|
||||
|
||||
func (r *PrinterRepository) UpdateHeartbeat(ctx context.Context, id int32) error {
|
||||
now := time.Now()
|
||||
return r.db.WithContext(ctx).Model(&model.Printer{}).Where("id = ?", id).Update("last_heartbeat", &now).Error
|
||||
}
|
||||
|
||||
func (r *PrinterRepository) CountActive(ctx context.Context) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.WithContext(ctx).Model(&model.Printer{}).Where("is_active = ? AND status != ?", true, model.PrinterStatusOffline).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
type PrintJobRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewPrintJobRepository(db *gorm.DB) *PrintJobRepository {
|
||||
return &PrintJobRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *PrintJobRepository) Create(ctx context.Context, job *model.PrintJob) error {
|
||||
return r.db.WithContext(ctx).Create(job).Error
|
||||
}
|
||||
|
||||
func (r *PrintJobRepository) GetByID(ctx context.Context, id int32) (*model.PrintJob, error) {
|
||||
var job model.PrintJob
|
||||
err := r.db.WithContext(ctx).First(&job, id).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &job, nil
|
||||
}
|
||||
|
||||
func (r *PrintJobRepository) GetByJobNo(ctx context.Context, jobNo string) (*model.PrintJob, error) {
|
||||
var job model.PrintJob
|
||||
err := r.db.WithContext(ctx).Where("job_no = ?", jobNo).First(&job).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &job, nil
|
||||
}
|
||||
|
||||
func (r *PrintJobRepository) GetPendingJobs(ctx context.Context, printerID int32) ([]*model.PrintJob, error) {
|
||||
var jobs []*model.PrintJob
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("printer_id = ? AND status = ?", printerID, model.PrintJobStatusPending).
|
||||
Preload("OrderItem").
|
||||
Preload("OrderItem.File").
|
||||
Find(&jobs).Error
|
||||
return jobs, err
|
||||
}
|
||||
|
||||
func (r *PrintJobRepository) UpdateStatus(ctx context.Context, jobNo string, status string, progress int, errorMsg string) error {
|
||||
updates := map[string]interface{}{
|
||||
"status": status,
|
||||
"progress": progress,
|
||||
"error_msg": errorMsg,
|
||||
}
|
||||
if status == model.PrintJobStatusPrinting {
|
||||
now := time.Now()
|
||||
updates["started_at"] = &now
|
||||
} else if status == model.PrintJobStatusCompleted || status == model.PrintJobStatusFailed {
|
||||
now := time.Now()
|
||||
updates["completed_at"] = &now
|
||||
}
|
||||
return r.db.WithContext(ctx).Model(&model.PrintJob{}).
|
||||
Where("job_no = ?", jobNo).Updates(updates).Error
|
||||
}
|
||||
|
||||
func (r *PrintJobRepository) GetQueueCount(ctx context.Context, printerID int32) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.WithContext(ctx).Model(&model.PrintJob{}).
|
||||
Where("printer_id = ? AND status IN ?", printerID, []string{model.PrintJobStatusPending, model.PrintJobStatusPrinting}).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
64
cloudprint-backend/internal/repository/user_repository.go
Normal file
64
cloudprint-backend/internal/repository/user_repository.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/cloudprint/cloudprint-backend/internal/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UserRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewUserRepository(db *gorm.DB) *UserRepository {
|
||||
return &UserRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *UserRepository) Create(ctx context.Context, user *model.User) error {
|
||||
return r.db.WithContext(ctx).Create(user).Error
|
||||
}
|
||||
|
||||
func (r *UserRepository) GetByID(ctx context.Context, id int32) (*model.User, error) {
|
||||
var user model.User
|
||||
err := r.db.WithContext(ctx).First(&user, id).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) GetByOpenid(ctx context.Context, openid string) (*model.User, error) {
|
||||
var user model.User
|
||||
err := r.db.WithContext(ctx).Where("openid = ?", openid).First(&user).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) Update(ctx context.Context, user *model.User) error {
|
||||
return r.db.WithContext(ctx).Save(user).Error
|
||||
}
|
||||
|
||||
func (r *UserRepository) UpdateBalance(ctx context.Context, userID int32, balance float64) error {
|
||||
return r.db.WithContext(ctx).Model(&model.User{}).Where("id = ?", userID).Update("balance", balance).Error
|
||||
}
|
||||
|
||||
func (r *UserRepository) DeductBalance(ctx context.Context, userID int32, amount float64) error {
|
||||
return r.db.WithContext(ctx).Model(&model.User{}).
|
||||
Where("id = ? AND balance >= ?", userID, amount).
|
||||
Update("balance", gorm.Expr("balance - ?", amount)).Error
|
||||
}
|
||||
|
||||
func (r *UserRepository) AddBalance(ctx context.Context, userID int32, amount float64) error {
|
||||
return r.db.WithContext(ctx).Model(&model.User{}).Where("id = ?", userID).
|
||||
Update("balance", gorm.Expr("balance + ?", amount)).Error
|
||||
}
|
||||
189
cloudprint-backend/internal/service/auth_service.go
Normal file
189
cloudprint-backend/internal/service/auth_service.go
Normal file
@@ -0,0 +1,189 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/cloudprint/cloudprint-backend/config"
|
||||
"github.com/cloudprint/cloudprint-backend/internal/model"
|
||||
"github.com/cloudprint/cloudprint-backend/internal/repository"
|
||||
"github.com/cloudprint/cloudprint-backend/pkg/jwt"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type AuthService struct {
|
||||
userRepo *repository.UserRepository
|
||||
adminRepo *repository.AdminRepository
|
||||
jwtManager *jwt.JWTManager
|
||||
redis *RedisClient
|
||||
wxConfig *config.WeChatConfig
|
||||
}
|
||||
|
||||
type RedisClient struct {
|
||||
// In production, use actual Redis client
|
||||
}
|
||||
|
||||
func NewAuthService(
|
||||
userRepo *repository.UserRepository,
|
||||
adminRepo *repository.AdminRepository,
|
||||
jwtManager *jwt.JWTManager,
|
||||
redis *RedisClient,
|
||||
wxConfig *config.WeChatConfig,
|
||||
) *AuthService {
|
||||
return &AuthService{
|
||||
userRepo: userRepo,
|
||||
adminRepo: adminRepo,
|
||||
jwtManager: jwtManager,
|
||||
redis: redis,
|
||||
wxConfig: wxConfig,
|
||||
}
|
||||
}
|
||||
|
||||
// WeChat code exchange response
|
||||
type WeChatCodeResponse struct {
|
||||
OpenID string `json:"openid"`
|
||||
SessionKey string `json:"session_key"`
|
||||
UnionID string `json:"unionid,omitempty"`
|
||||
ErrCode int `json:"errcode,omitempty"`
|
||||
ErrMsg string `json:"errmsg,omitempty"`
|
||||
}
|
||||
|
||||
func (s *AuthService) Login(ctx context.Context, code string) (*LoginResult, error) {
|
||||
// Exchange code for openid from WeChat
|
||||
openid, err := s.getWeChatOpenID(code)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get wechat openid: %w", err)
|
||||
}
|
||||
|
||||
// Find or create user
|
||||
user, err := s.userRepo.GetByOpenid(ctx, openid)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user: %w", err)
|
||||
}
|
||||
|
||||
isNewUser := false
|
||||
if user == nil {
|
||||
isNewUser = true
|
||||
user = &model.User{
|
||||
Openid: openid,
|
||||
}
|
||||
if err := s.userRepo.Create(ctx, user); err != nil {
|
||||
return nil, fmt.Errorf("failed to create user: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate tokens
|
||||
accessToken, err := s.jwtManager.GenerateAccessToken(user.ID, "user", "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate access token: %w", err)
|
||||
}
|
||||
|
||||
refreshToken, err := s.jwtManager.GenerateRefreshToken(user.ID, "user")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate refresh token: %w", err)
|
||||
}
|
||||
|
||||
return &LoginResult{
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
User: user,
|
||||
IsNewUser: isNewUser,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *AuthService) getWeChatOpenID(code string) (string, error) {
|
||||
url := fmt.Sprintf("https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code",
|
||||
s.wxConfig.AppID, s.wxConfig.AppSecret, code)
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result WeChatCodeResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if result.ErrCode != 0 {
|
||||
return "", fmt.Errorf("wechat error: %s", result.ErrMsg)
|
||||
}
|
||||
|
||||
return result.OpenID, nil
|
||||
}
|
||||
|
||||
func (s *AuthService) RefreshToken(ctx context.Context, refreshToken string) (string, error) {
|
||||
return s.jwtManager.RefreshAccessToken(refreshToken)
|
||||
}
|
||||
|
||||
func (s *AuthService) GetUserInfo(ctx context.Context, userID int32) (*model.User, error) {
|
||||
return s.userRepo.GetByID(ctx, userID)
|
||||
}
|
||||
|
||||
func (s *AuthService) UpdateUser(ctx context.Context, userID int32, nickname, avatar string) (*model.User, error) {
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil || user == nil {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
|
||||
user.Nickname = nickname
|
||||
if avatar != "" {
|
||||
user.Avatar = avatar
|
||||
}
|
||||
|
||||
if err := s.userRepo.Update(ctx, user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
type LoginResult struct {
|
||||
AccessToken string
|
||||
RefreshToken string
|
||||
User *model.User
|
||||
IsNewUser bool
|
||||
}
|
||||
|
||||
// Admin login
|
||||
func (s *AuthService) AdminLogin(ctx context.Context, username, password string) (*AdminLoginResult, error) {
|
||||
admin, err := s.adminRepo.GetByUsername(ctx, username)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get admin: %w", err)
|
||||
}
|
||||
if admin == nil {
|
||||
return nil, errors.New("invalid credentials")
|
||||
}
|
||||
|
||||
// Verify password
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(admin.PasswordHash), []byte(password)); err != nil {
|
||||
return nil, errors.New("invalid credentials")
|
||||
}
|
||||
|
||||
// Generate token
|
||||
accessToken, err := s.jwtManager.GenerateAccessToken(admin.ID, "admin", admin.Username)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate token: %w", err)
|
||||
}
|
||||
|
||||
return &AdminLoginResult{
|
||||
AccessToken: accessToken,
|
||||
Admin: admin,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type AdminLoginResult struct {
|
||||
AccessToken string
|
||||
Admin *model.Admin
|
||||
}
|
||||
|
||||
// HashPassword creates a bcrypt hash of the password
|
||||
func HashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
return string(bytes), err
|
||||
}
|
||||
105
cloudprint-backend/internal/service/file_service.go
Normal file
105
cloudprint-backend/internal/service/file_service.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudprint/cloudprint-backend/internal/model"
|
||||
"github.com/cloudprint/cloudprint-backend/internal/repository"
|
||||
"github.com/cloudprint/cloudprint-backend/pkg/upload"
|
||||
)
|
||||
|
||||
type FileService struct {
|
||||
fileRepo *repository.FileRepository
|
||||
uploader *upload.FileUploader
|
||||
uploadDir string
|
||||
}
|
||||
|
||||
func NewFileService(fileRepo *repository.FileRepository, uploader *upload.FileUploader, uploadDir string) *FileService {
|
||||
return &FileService{
|
||||
fileRepo: fileRepo,
|
||||
uploader: uploader,
|
||||
uploadDir: uploadDir,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *FileService) Upload(ctx context.Context, fileName string, fileType string, uploaderType string, uploaderID int32, data []byte) (*model.File, error) {
|
||||
if !s.uploader.ValidateFileType(fileName) {
|
||||
return nil, fmt.Errorf("file type not allowed")
|
||||
}
|
||||
|
||||
// Save file
|
||||
filePath := s.uploader.GenerateFilePath(fileName)
|
||||
fullPath := s.uploader.GetFilePath(filePath)
|
||||
|
||||
if err := os.WriteFile(fullPath, data, 0644); err != nil {
|
||||
return nil, fmt.Errorf("failed to write file: %w", err)
|
||||
}
|
||||
|
||||
// Create file record
|
||||
file := &model.File{
|
||||
FileName: fileName,
|
||||
FilePath: filePath,
|
||||
FileType: strings.ToLower(fileType),
|
||||
FileSize: int64(len(data)),
|
||||
UploaderType: uploaderType,
|
||||
UploaderID: uploaderID,
|
||||
}
|
||||
|
||||
if err := s.fileRepo.Create(ctx, file); err != nil {
|
||||
// Clean up file on error
|
||||
os.Remove(fullPath)
|
||||
return nil, fmt.Errorf("failed to create file record: %w", err)
|
||||
}
|
||||
|
||||
return file, nil
|
||||
}
|
||||
|
||||
func (s *FileService) GetFile(ctx context.Context, fileID int32) (*model.File, error) {
|
||||
return s.fileRepo.GetByID(ctx, fileID)
|
||||
}
|
||||
|
||||
func (s *FileService) GetFileReader(ctx context.Context, file *model.File) (io.ReadCloser, error) {
|
||||
fullPath := filepath.Join(s.uploadDir, file.FilePath)
|
||||
return os.Open(fullPath)
|
||||
}
|
||||
|
||||
func (s *FileService) DeleteFile(ctx context.Context, fileID int32, uploaderType string, uploaderID int32) error {
|
||||
file, err := s.fileRepo.GetByID(ctx, fileID)
|
||||
if err != nil || file == nil {
|
||||
return fmt.Errorf("file not found")
|
||||
}
|
||||
|
||||
// Verify ownership
|
||||
if file.UploaderType != uploaderType || file.UploaderID != uploaderID {
|
||||
return fmt.Errorf("unauthorized")
|
||||
}
|
||||
|
||||
// Delete physical file
|
||||
fullPath := s.uploader.GetFilePath(file.FilePath)
|
||||
os.Remove(fullPath)
|
||||
|
||||
// Delete record
|
||||
return s.fileRepo.Delete(ctx, fileID)
|
||||
}
|
||||
|
||||
func (s *FileService) GetFileList(ctx context.Context, uploaderType string, uploaderID int32, page, pageSize int) ([]*model.File, int64, error) {
|
||||
return s.fileRepo.GetListByUploader(ctx, uploaderType, uploaderID, page, pageSize)
|
||||
}
|
||||
|
||||
func (s *FileService) AddWatermark(ctx context.Context, fileID int32, text string, opacity float32, position int, fontSize int) (*model.File, error) {
|
||||
file, err := s.fileRepo.GetByID(ctx, fileID)
|
||||
if err != nil || file == nil {
|
||||
return nil, fmt.Errorf("file not found")
|
||||
}
|
||||
|
||||
// In production, use a PDF library to add watermark
|
||||
// For now, just return the original file
|
||||
// TODO: Implement actual watermark using pdf-lib or similar
|
||||
|
||||
return file, nil
|
||||
}
|
||||
218
cloudprint-backend/internal/service/order_service.go
Normal file
218
cloudprint-backend/internal/service/order_service.go
Normal file
@@ -0,0 +1,218 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/cloudprint/cloudprint-backend/internal/model"
|
||||
"github.com/cloudprint/cloudprint-backend/internal/repository"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type OrderService struct {
|
||||
orderRepo *repository.OrderRepository
|
||||
orderItemRepo *repository.OrderItemRepository
|
||||
fileRepo *repository.FileRepository
|
||||
priceRepo *repository.PriceRepository
|
||||
userRepo *repository.UserRepository
|
||||
printerRepo *repository.PrinterRepository
|
||||
printJobRepo *repository.PrintJobRepository
|
||||
}
|
||||
|
||||
func NewOrderService(
|
||||
orderRepo *repository.OrderRepository,
|
||||
orderItemRepo *repository.OrderItemRepository,
|
||||
fileRepo *repository.FileRepository,
|
||||
priceRepo *repository.PriceRepository,
|
||||
userRepo *repository.UserRepository,
|
||||
printerRepo *repository.PrinterRepository,
|
||||
printJobRepo *repository.PrintJobRepository,
|
||||
) *OrderService {
|
||||
return &OrderService{
|
||||
orderRepo: orderRepo,
|
||||
orderItemRepo: orderItemRepo,
|
||||
fileRepo: fileRepo,
|
||||
priceRepo: priceRepo,
|
||||
userRepo: userRepo,
|
||||
printerRepo: printerRepo,
|
||||
printJobRepo: printJobRepo,
|
||||
}
|
||||
}
|
||||
|
||||
type CreateOrderItem struct {
|
||||
FileID int32
|
||||
PrinterID int32
|
||||
Copies int
|
||||
Color bool
|
||||
Duplex bool
|
||||
PageRange string
|
||||
}
|
||||
|
||||
func (s *OrderService) CreateOrder(ctx context.Context, userID int32, items []*CreateOrderItem, remark string) (*model.Order, error) {
|
||||
if len(items) == 0 {
|
||||
return nil, errors.New("order must have at least one item")
|
||||
}
|
||||
|
||||
// Generate order number
|
||||
orderNo := generateOrderNo()
|
||||
|
||||
// Calculate total amount
|
||||
var totalAmount float64
|
||||
orderItems := make([]*model.OrderItem, 0, len(items))
|
||||
|
||||
for _, item := range items {
|
||||
// Verify file exists
|
||||
file, err := s.fileRepo.GetByID(ctx, item.FileID)
|
||||
if err != nil || file == nil {
|
||||
return nil, fmt.Errorf("file not found: %d", item.FileID)
|
||||
}
|
||||
|
||||
// Calculate price
|
||||
price, err := s.calculatePrice(ctx, item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
orderItem := &model.OrderItem{
|
||||
FileID: item.FileID,
|
||||
PrinterID: intPtr(item.PrinterID),
|
||||
Copies: item.Copies,
|
||||
Color: item.Color,
|
||||
Duplex: item.Duplex,
|
||||
PageRange: item.PageRange,
|
||||
Price: price,
|
||||
}
|
||||
orderItems = append(orderItems, orderItem)
|
||||
totalAmount += price
|
||||
}
|
||||
|
||||
// Create order
|
||||
order := &model.Order{
|
||||
OrderNo: orderNo,
|
||||
UserID: userID,
|
||||
Status: model.OrderStatusPending,
|
||||
TotalAmount: totalAmount,
|
||||
Remark: remark,
|
||||
Items: orderItems,
|
||||
}
|
||||
|
||||
// Save order with items
|
||||
if err := s.orderRepo.CreateWithItems(ctx, order, orderItems); err != nil {
|
||||
return nil, fmt.Errorf("failed to create order: %w", err)
|
||||
}
|
||||
|
||||
return order, nil
|
||||
}
|
||||
|
||||
func (s *OrderService) calculatePrice(ctx context.Context, item *CreateOrderItem) (float64, error) {
|
||||
// Get price list
|
||||
prices, err := s.priceRepo.GetAllActive(ctx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Default price calculation (A4 B&W per page)
|
||||
defaultPrice := 0.10
|
||||
colorMultiplier := 3.0
|
||||
duplexDiscount := 0.8
|
||||
|
||||
// Calculate based on file type and settings
|
||||
// For simplicity, using default values
|
||||
// In production, look up from price list based on file type
|
||||
|
||||
pageCount := 1 // Default, should get actual page count from file
|
||||
if item.Copies <= 0 {
|
||||
item.Copies = 1
|
||||
}
|
||||
|
||||
price := float64(pageCount) * defaultPrice * float64(item.Copies)
|
||||
|
||||
if item.Color {
|
||||
price *= colorMultiplier
|
||||
}
|
||||
|
||||
if item.Duplex {
|
||||
price *= duplexDiscount
|
||||
}
|
||||
|
||||
return price, nil
|
||||
}
|
||||
|
||||
func generateOrderNo() string {
|
||||
now := time.Now()
|
||||
return fmt.Sprintf("CP%d%s", now.UnixNano()/1000000, uuid.New().String()[:8])
|
||||
}
|
||||
|
||||
func intPtr(i int32) *int32 {
|
||||
return &i
|
||||
}
|
||||
|
||||
func (s *OrderService) GetOrder(ctx context.Context, orderNo string) (*model.Order, error) {
|
||||
return s.orderRepo.GetByOrderNo(ctx, orderNo)
|
||||
}
|
||||
|
||||
func (s *OrderService) GetOrderList(ctx context.Context, userID int32, status int8, page, pageSize int) ([]*model.Order, int64, error) {
|
||||
return s.orderRepo.GetListByUserID(ctx, userID, status, page, pageSize)
|
||||
}
|
||||
|
||||
func (s *OrderService) UpdateOrderStatus(ctx context.Context, orderNo string, status int8) error {
|
||||
return s.orderRepo.UpdateStatus(ctx, orderNo, status)
|
||||
}
|
||||
|
||||
func (s *OrderService) CancelOrder(ctx context.Context, orderNo string) error {
|
||||
order, err := s.orderRepo.GetByOrderNo(ctx, orderNo)
|
||||
if err != nil || order == nil {
|
||||
return errors.New("order not found")
|
||||
}
|
||||
|
||||
if order.Status != model.OrderStatusPending {
|
||||
return errors.New("only pending orders can be cancelled")
|
||||
}
|
||||
|
||||
return s.orderRepo.UpdateStatus(ctx, orderNo, model.OrderStatusCancelled)
|
||||
}
|
||||
|
||||
// PayOrder handles payment for an order
|
||||
func (s *OrderService) PayOrder(ctx context.Context, orderNo string, payType string) error {
|
||||
order, err := s.orderRepo.GetByOrderNo(ctx, orderNo)
|
||||
if err != nil || order == nil {
|
||||
return errors.New("order not found")
|
||||
}
|
||||
|
||||
if order.Status != model.OrderStatusPending {
|
||||
return errors.New("order is not pending")
|
||||
}
|
||||
|
||||
// Update order status to paid
|
||||
if err := s.orderRepo.UpdateStatus(ctx, orderNo, model.OrderStatusPaid); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create print jobs for each order item
|
||||
for _, item := range order.Items {
|
||||
if item.PrinterID != nil {
|
||||
jobNo := generateJobNo()
|
||||
job := &model.PrintJob{
|
||||
JobNo: jobNo,
|
||||
PrinterID: *item.PrinterID,
|
||||
OrderItemID: item.ID,
|
||||
Status: model.PrintJobStatusPending,
|
||||
Progress: 0,
|
||||
}
|
||||
if err := s.printJobRepo.Create(ctx, job); err != nil {
|
||||
// Log error but don't fail
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update order status to printing
|
||||
return s.orderRepo.UpdateStatus(ctx, orderNo, model.OrderStatusPrinting)
|
||||
}
|
||||
|
||||
func generateJobNo() string {
|
||||
now := time.Now()
|
||||
return fmt.Sprintf("PJ%d%s", now.UnixNano()/1000000, uuid.New().String()[:8])
|
||||
}
|
||||
194
cloudprint-backend/internal/service/print_service.go
Normal file
194
cloudprint-backend/internal/service/print_service.go
Normal file
@@ -0,0 +1,194 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cloudprint/cloudprint-backend/internal/model"
|
||||
"github.com/cloudprint/cloudprint-backend/internal/repository"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type PrintService struct {
|
||||
printerRepo *repository.PrinterRepository
|
||||
printJobRepo *repository.PrintJobRepository
|
||||
orderRepo *repository.OrderRepository
|
||||
subscribers map[int32]chan *model.PrintJob
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewPrintService(
|
||||
printerRepo *repository.PrinterRepository,
|
||||
printJobRepo *repository.PrintJobRepository,
|
||||
orderRepo *repository.OrderRepository,
|
||||
) *PrintService {
|
||||
s := &PrintService{
|
||||
printerRepo: printerRepo,
|
||||
printJobRepo: printJobRepo,
|
||||
orderRepo: orderRepo,
|
||||
subscribers: make(map[int32]chan *model.PrintJob),
|
||||
}
|
||||
// Start heartbeat checker
|
||||
go s.checkPrinterHeartbeat()
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *PrintService) GetPrinters(ctx context.Context) ([]*model.Printer, error) {
|
||||
return s.printerRepo.GetAllActive(ctx)
|
||||
}
|
||||
|
||||
func (s *PrintService) GetPrinterStatus(ctx context.Context, printerID int32) (*model.Printer, int64, error) {
|
||||
printer, err := s.printerRepo.GetByID(ctx, printerID)
|
||||
if err != nil || printer == nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
queueCount, err := s.printJobRepo.GetQueueCount(ctx, printerID)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return printer, queueCount, nil
|
||||
}
|
||||
|
||||
func (s *PrintService) RegisterPrinter(ctx context.Context, name, displayName, apiKey string) (*model.Printer, string, error) {
|
||||
// Check if printer already exists
|
||||
existing, err := s.printerRepo.GetByName(ctx, name)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if existing != nil {
|
||||
return nil, "", nil
|
||||
}
|
||||
|
||||
// Generate token for this printer
|
||||
token := uuid.New().String()
|
||||
|
||||
printer := &model.Printer{
|
||||
Name: name,
|
||||
DisplayName: displayName,
|
||||
APIKey: apiKey,
|
||||
IsActive: true,
|
||||
Status: model.PrinterStatusIdle,
|
||||
}
|
||||
|
||||
if err := s.printerRepo.Create(ctx, printer); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
return printer, token, nil
|
||||
}
|
||||
|
||||
func (s *PrintService) PrinterHeartbeat(ctx context.Context, printerID int32, status int8, token string) error {
|
||||
// Verify token (in production, validate against stored token)
|
||||
// For now, just update heartbeat and status
|
||||
if err := s.printerRepo.UpdateHeartbeat(ctx, printerID); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.printerRepo.UpdateStatus(ctx, printerID, status)
|
||||
}
|
||||
|
||||
func (s *PrintService) SubmitPrintJob(ctx context.Context, orderItemID int32, printerID int32, filePath string, copies int, color, duplex bool, pageRange string) (*model.PrintJob, error) {
|
||||
jobNo := generateJobNo()
|
||||
|
||||
job := &model.PrintJob{
|
||||
JobNo: jobNo,
|
||||
PrinterID: printerID,
|
||||
OrderItemID: orderItemID,
|
||||
Status: model.PrintJobStatusPending,
|
||||
Progress: 0,
|
||||
}
|
||||
|
||||
if err := s.printJobRepo.Create(ctx, job); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Notify subscriber
|
||||
s.mu.RLock()
|
||||
ch, ok := s.subscribers[printerID]
|
||||
s.mu.RUnlock()
|
||||
|
||||
if ok {
|
||||
select {
|
||||
case ch <- job:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
return job, nil
|
||||
}
|
||||
|
||||
func (s *PrintService) CancelPrintJob(ctx context.Context, jobNo string) error {
|
||||
return s.printJobRepo.UpdateStatus(ctx, jobNo, model.PrintJobStatusCancelled, 0, "cancelled by user")
|
||||
}
|
||||
|
||||
func (s *PrintService) SubscribeJobs(ctx context.Context, printerID int32, token string) (<-chan *model.PrintJob, error) {
|
||||
ch := make(chan *model.PrintJob, 100)
|
||||
|
||||
s.mu.Lock()
|
||||
s.subscribers[printerID] = ch
|
||||
s.mu.Unlock()
|
||||
|
||||
// Send pending jobs immediately
|
||||
go func() {
|
||||
jobs, err := s.printJobRepo.GetPendingJobs(ctx, printerID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, job := range jobs {
|
||||
select {
|
||||
case ch <- job:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
func (s *PrintService) UnsubscribeJobs(printerID int32) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if ch, ok := s.subscribers[printerID]; ok {
|
||||
close(ch)
|
||||
delete(s.subscribers, printerID)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PrintService) ReportJobStatus(ctx context.Context, jobNo string, status string, progress int, errorMsg string) error {
|
||||
// Update job status
|
||||
if err := s.printJobRepo.UpdateStatus(ctx, jobNo, status, progress, errorMsg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If completed, check if all jobs for the order are done
|
||||
if status == model.PrintJobStatusCompleted {
|
||||
job, _ := s.printJobRepo.GetByJobNo(ctx, jobNo)
|
||||
if job != nil {
|
||||
// Check if all jobs for the order are completed
|
||||
s.checkOrderCompletion(ctx, job.OrderItemID)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PrintService) checkOrderCompletion(ctx context.Context, orderItemID int32) {
|
||||
// In production, check if all print jobs for the order are completed
|
||||
// and update order status accordingly
|
||||
}
|
||||
|
||||
func (s *PrintService) checkPrinterHeartbeat() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
for range ticker.C {
|
||||
// In production, check LastHeartbeat and mark printers as offline if no heartbeat
|
||||
// This is a placeholder for the heartbeat checker
|
||||
}
|
||||
}
|
||||
|
||||
// ValidatePrinterToken validates the API key and returns the printer
|
||||
func (s *PrintService) ValidatePrinterToken(ctx context.Context, apiKey string) (*model.Printer, error) {
|
||||
return s.printerRepo.GetByAPIKey(ctx, apiKey)
|
||||
}
|
||||
Reference in New Issue
Block a user