feat: 初始提交云印享付费打印服务后端
This commit is contained in:
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