feat: 初始提交云印享付费打印服务后端
This commit is contained in:
236
cloudprint-backend/README.md
Normal file
236
cloudprint-backend/README.md
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
# 云印享 - 付费打印服务系统
|
||||||
|
|
||||||
|
## 项目简介
|
||||||
|
|
||||||
|
云印享是一个基于 gRPC 的付费打印服务后端系统,支持用户管理、文件上传下载、订单处理、打印任务调度、微信支付、文库管理等核心功能。
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- **语言**: Go 1.21+
|
||||||
|
- **框架**: gRPC + Protobuf
|
||||||
|
- **数据库**: MySQL + GORM
|
||||||
|
- **认证**: JWT (JSON Web Token)
|
||||||
|
- **支付**: 微信支付 (gopay)
|
||||||
|
- **配置**: Viper
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
cloudprint-backend/
|
||||||
|
├── cmd/server/ # 应用入口
|
||||||
|
│ └── main.go
|
||||||
|
├── config/ # 配置管理
|
||||||
|
│ └── config.go
|
||||||
|
├── internal/
|
||||||
|
│ ├── grpc/ # gRPC 服务层
|
||||||
|
│ │ ├── interceptor/ # 拦截器 (认证)
|
||||||
|
│ │ └── service/ # gRPC 服务实现
|
||||||
|
│ ├── model/ # 数据模型
|
||||||
|
│ ├── repository/ # 数据访问层
|
||||||
|
│ └── service/ # 业务逻辑层
|
||||||
|
├── pkg/ # 公共工具包
|
||||||
|
│ ├── jwt/ # JWT 认证
|
||||||
|
│ ├── pay/ # 微信支付
|
||||||
|
│ └── upload/ # 文件上传
|
||||||
|
├── proto/ # Protobuf 定义
|
||||||
|
│ ├── cloudprint.proto
|
||||||
|
│ └── generated/ # 生成的代码
|
||||||
|
├── scripts/ # 数据库脚本
|
||||||
|
│ └── db.sql
|
||||||
|
├── config.yaml # 配置文件
|
||||||
|
├── go.mod
|
||||||
|
└── go.sum
|
||||||
|
```
|
||||||
|
|
||||||
|
## 服务列表
|
||||||
|
|
||||||
|
| 服务 | 端口 | 描述 |
|
||||||
|
|------|------|------|
|
||||||
|
| gRPC Server | 8080 | 主服务端口 |
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 1. 环境要求
|
||||||
|
|
||||||
|
- Go 1.21+
|
||||||
|
- MySQL 5.7+
|
||||||
|
- protoc (Protocol Buffer 编译器)
|
||||||
|
|
||||||
|
### 2. 数据库初始化
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mysql -u root -p < scripts/db.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 配置修改
|
||||||
|
|
||||||
|
编辑 `config.yaml` 文件,修改以下配置:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
database:
|
||||||
|
host: "localhost"
|
||||||
|
port: 3306
|
||||||
|
username: "root"
|
||||||
|
password: "your-password"
|
||||||
|
database: "cloudprint"
|
||||||
|
|
||||||
|
jwt:
|
||||||
|
secret: "your-secret-key-change-in-production"
|
||||||
|
|
||||||
|
wechat:
|
||||||
|
app_id: "your-app-id"
|
||||||
|
app_secret: "your-app-secret"
|
||||||
|
mch_id: "your-merchant-id"
|
||||||
|
api_key: "your-api-key"
|
||||||
|
notify_url: "http://your-domain.com/pay/callback"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 编译生成 gRPC 代码
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd cloudprint-backend
|
||||||
|
protoc --go_out=./proto/generated --go_opt=paths=source_relative \
|
||||||
|
--go-grpc_out=./proto/generated --go-grpc_opt=paths=source_relative \
|
||||||
|
proto/cloudprint.proto
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 运行服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run cmd/server/main.go -config config.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## API 文档
|
||||||
|
|
||||||
|
### AuthService (用户认证服务)
|
||||||
|
|
||||||
|
| 方法 | 描述 |
|
||||||
|
|------|------|
|
||||||
|
| Login | 微信登录,获取 AccessToken 和 RefreshToken |
|
||||||
|
| RefreshToken | 刷新 AccessToken |
|
||||||
|
| GetUserInfo | 获取用户信息 |
|
||||||
|
| UpdateUser | 更新用户信息(昵称、头像) |
|
||||||
|
|
||||||
|
### FileService (文件管理服务)
|
||||||
|
|
||||||
|
| 方法 | 描述 |
|
||||||
|
|------|------|
|
||||||
|
| Upload | 流式上传文件(支持分片) |
|
||||||
|
| GetFile | 流式下载文件 |
|
||||||
|
| DeleteFile | 删除文件 |
|
||||||
|
| AddWatermark | 添加水印 |
|
||||||
|
| GetFileList | 获取文件列表 |
|
||||||
|
|
||||||
|
### OrderService (订单服务)
|
||||||
|
|
||||||
|
| 方法 | 描述 |
|
||||||
|
|------|------|
|
||||||
|
| CreateOrder | 创建订单 |
|
||||||
|
| GetOrder | 获取订单详情 |
|
||||||
|
| GetOrderList | 获取订单列表 |
|
||||||
|
| UpdateOrderStatus | 更新订单状态 |
|
||||||
|
| CancelOrder | 取消订单 |
|
||||||
|
|
||||||
|
### PrintService (打印服务)
|
||||||
|
|
||||||
|
| 方法 | 描述 |
|
||||||
|
|------|------|
|
||||||
|
| GetPrinters | 获取打印机列表 |
|
||||||
|
| GetPrinterStatus | 获取打印机状态 |
|
||||||
|
| RegisterPrinter | 注册打印机 |
|
||||||
|
| PrinterHeartbeat | 打印机心跳 |
|
||||||
|
| SubmitPrintJob | 提交打印任务 |
|
||||||
|
| CancelPrintJob | 取消打印任务 |
|
||||||
|
| SubscribeJobs | 订阅打印任务(流式) |
|
||||||
|
| ReportJobStatus | 上报任务状态 |
|
||||||
|
|
||||||
|
### LibraryService (文库服务)
|
||||||
|
|
||||||
|
| 方法 | 描述 |
|
||||||
|
|------|------|
|
||||||
|
| GetCategories | 获取文库分类 |
|
||||||
|
| GetFiles | 获取文库文件列表 |
|
||||||
|
| UploadFile | 上传文库文件 |
|
||||||
|
| DeleteFile | 删除文库文件 |
|
||||||
|
|
||||||
|
### PriceService (价目表服务)
|
||||||
|
|
||||||
|
| 方法 | 描述 |
|
||||||
|
|------|------|
|
||||||
|
| GetPriceList | 获取价目表 |
|
||||||
|
| CreatePriceItem | 创建价目项 |
|
||||||
|
| UpdatePriceItem | 更新价目项 |
|
||||||
|
|
||||||
|
### PayService (支付服务)
|
||||||
|
|
||||||
|
| 方法 | 描述 |
|
||||||
|
|------|------|
|
||||||
|
| CreateWxPayOrder | 创建微信支付订单 |
|
||||||
|
| QueryPayStatus | 查询支付状态 |
|
||||||
|
| HandlePayCallback | 处理支付回调 |
|
||||||
|
| PayByBalance | 余额支付 |
|
||||||
|
|
||||||
|
### AdminService (管理后台服务)
|
||||||
|
|
||||||
|
| 方法 | 描述 |
|
||||||
|
|------|------|
|
||||||
|
| Login | 管理员登录 |
|
||||||
|
| GetStatistics | 获取统计数据 |
|
||||||
|
|
||||||
|
## 数据模型
|
||||||
|
|
||||||
|
### 订单状态
|
||||||
|
|
||||||
|
| 值 | 状态 |
|
||||||
|
|----|------|
|
||||||
|
| 0 | 待支付 |
|
||||||
|
| 1 | 已支付 |
|
||||||
|
| 2 | 打印中 |
|
||||||
|
| 3 | 已完成 |
|
||||||
|
| 4 | 已取消 |
|
||||||
|
|
||||||
|
### 支付状态
|
||||||
|
|
||||||
|
| 值 | 状态 |
|
||||||
|
|----|------|
|
||||||
|
| 0 | 待支付 |
|
||||||
|
| 1 | 支付中 |
|
||||||
|
| 2 | 成功 |
|
||||||
|
| 3 | 失败 |
|
||||||
|
|
||||||
|
### 打印机状态
|
||||||
|
|
||||||
|
| 值 | 状态 |
|
||||||
|
|----|------|
|
||||||
|
| 0 | 空闲 |
|
||||||
|
| 1 | 打印中 |
|
||||||
|
| 2 | 离线 |
|
||||||
|
|
||||||
|
### 打印任务状态
|
||||||
|
|
||||||
|
| 值 | 状态 |
|
||||||
|
|----|------|
|
||||||
|
| pending | 等待中 |
|
||||||
|
| printing | 打印中 |
|
||||||
|
| completed | 已完成 |
|
||||||
|
| failed | 失败 |
|
||||||
|
| cancelled | 已取消 |
|
||||||
|
|
||||||
|
## 默认账号
|
||||||
|
|
||||||
|
- 管理员用户名: `admin`
|
||||||
|
- 管理员密码: `admin123` (需在生产环境修改)
|
||||||
|
|
||||||
|
## 初始价目表
|
||||||
|
|
||||||
|
| 名称 | 类型 | 价格 | 单位 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| A4黑白打印 | A4 | 0.10元 | 页 |
|
||||||
|
| A4彩色打印 | A4 | 0.30元 | 页 |
|
||||||
|
| A3黑白打印 | A3 | 0.20元 | 页 |
|
||||||
|
| A3彩色打印 | A3 | 0.60元 | 页 |
|
||||||
|
| 照片打印 | 照片纸 | 2.00元 | 张 |
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
MIT License
|
||||||
163
cloudprint-backend/cmd/server/main.go
Normal file
163
cloudprint-backend/cmd/server/main.go
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/cloudprint/cloudprint-backend/config"
|
||||||
|
"github.com/cloudprint/cloudprint-backend/internal/grpc"
|
||||||
|
"github.com/cloudprint/cloudprint-backend/internal/grpc/interceptor"
|
||||||
|
"github.com/cloudprint/cloudprint-backend/internal/grpc/service"
|
||||||
|
"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/jwt"
|
||||||
|
"github.com/cloudprint/cloudprint-backend/pkg/pay"
|
||||||
|
"github.com/cloudprint/cloudprint-backend/pkg/upload"
|
||||||
|
pb "github.com/cloudprint/cloudprint-backend/proto/generated"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"gorm.io/driver/mysql"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
configPath := flag.String("config", "config.yaml", "path to config file")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// Load configuration
|
||||||
|
cfg, err := config.LoadConfig(*configPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize logger
|
||||||
|
zapLogger, _ := zap.NewProduction()
|
||||||
|
defer zapLogger.Sync()
|
||||||
|
|
||||||
|
// Initialize database
|
||||||
|
db, err := initDB(cfg)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to initialize database: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto migrate
|
||||||
|
if err := db.AutoMigrate(
|
||||||
|
&model.User{},
|
||||||
|
&model.Admin{},
|
||||||
|
&model.Printer{},
|
||||||
|
&model.Order{},
|
||||||
|
&model.OrderItem{},
|
||||||
|
&model.File{},
|
||||||
|
&model.LibraryCategory{},
|
||||||
|
&model.LibraryFile{},
|
||||||
|
&model.PrintJob{},
|
||||||
|
&model.PriceList{},
|
||||||
|
&model.Payment{},
|
||||||
|
); err != nil {
|
||||||
|
log.Fatalf("Failed to migrate database: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize repositories
|
||||||
|
userRepo := repository.NewUserRepository(db)
|
||||||
|
adminRepo := repository.NewAdminRepository(db)
|
||||||
|
fileRepo := repository.NewFileRepository(db)
|
||||||
|
orderRepo := repository.NewOrderRepository(db)
|
||||||
|
orderItemRepo := repository.NewOrderItemRepository(db)
|
||||||
|
printerRepo := repository.NewPrinterRepository(db)
|
||||||
|
printJobRepo := repository.NewPrintJobRepository(db)
|
||||||
|
libraryRepo := repository.NewLibraryRepository(db)
|
||||||
|
priceRepo := repository.NewPriceRepository(db)
|
||||||
|
paymentRepo := repository.NewPaymentRepository(db)
|
||||||
|
|
||||||
|
// Initialize JWT manager
|
||||||
|
jwtManager := jwt.NewJWTManager(
|
||||||
|
cfg.JWT.Secret,
|
||||||
|
cfg.JWT.AccessTokenTTL,
|
||||||
|
cfg.JWT.RefreshTokenTTL,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Initialize file uploader
|
||||||
|
fileUploader := upload.NewFileUploader(
|
||||||
|
cfg.Upload.Path,
|
||||||
|
cfg.Upload.ChunkPath,
|
||||||
|
cfg.Upload.MaxSize,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Initialize WeChat pay
|
||||||
|
weChatPay := pay.NewWeChatPay(&cfg.WeChat)
|
||||||
|
|
||||||
|
// Initialize services
|
||||||
|
authService := service.NewAuthService(userRepo, adminRepo, jwtManager, nil, &cfg.WeChat)
|
||||||
|
fileService := service.NewFileService(fileRepo, fileUploader, cfg.Upload.Path)
|
||||||
|
orderService := service.NewOrderService(orderRepo, orderItemRepo, fileRepo, priceRepo, userRepo, printerRepo, printJobRepo)
|
||||||
|
printService := service.NewPrintService(printerRepo, printJobRepo, orderRepo)
|
||||||
|
|
||||||
|
// Initialize gRPC server
|
||||||
|
grpcServer := grpc.NewServer(
|
||||||
|
grpc.UnaryInterceptor(interceptor.NewAuthInterceptor(jwtManager).UnaryServerInterceptor()),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Register gRPC services
|
||||||
|
pb.RegisterAuthServiceServer(grpcServer, service.NewAuthServiceServer(authService))
|
||||||
|
pb.RegisterFileServiceServer(grpcServer, service.NewFileServiceServer(fileService))
|
||||||
|
pb.RegisterOrderServiceServer(grpcServer, service.NewOrderServiceServer(orderService))
|
||||||
|
pb.RegisterPrintServiceServer(grpcServer, service.NewPrintServiceServer(printService))
|
||||||
|
pb.RegisterLibraryServiceServer(grpcServer, service.NewLibraryServiceServer(libraryRepo, fileRepo))
|
||||||
|
pb.RegisterPriceServiceServer(grpcServer, service.NewPriceServiceServer(priceRepo))
|
||||||
|
pb.RegisterPayServiceServer(grpcServer, service.NewPayServiceServer(orderService, paymentRepo, weChatPay, userRepo))
|
||||||
|
pb.RegisterAdminServiceServer(grpcServer, service.NewAdminServiceServer(authService, orderRepo, printerRepo))
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
|
||||||
|
lis, err := net.Listen("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to listen: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Graceful shutdown
|
||||||
|
sigCh := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-sigCh
|
||||||
|
grpcServer.GracefulStop()
|
||||||
|
}()
|
||||||
|
|
||||||
|
zapLogger.Info("Starting gRPC server", zap.String("address", addr))
|
||||||
|
if err := grpcServer.Serve(lis); err != nil {
|
||||||
|
log.Fatalf("Failed to serve: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func initDB(cfg *config.Config) (*gorm.DB, error) {
|
||||||
|
gormLogger := logger.New(
|
||||||
|
log.New(os.Stdout, "\r\n", log.LstdFlags),
|
||||||
|
logger.Config{
|
||||||
|
LogLevel: logger.Info,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
db, err := gorm.Open(mysql.Open(cfg.Database.DSN()), &gorm.Config{
|
||||||
|
Logger: gormLogger,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlDB, err := db.DB()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlDB.SetMaxOpenConns(cfg.Database.MaxOpenConns)
|
||||||
|
sqlDB.SetMaxIdleConns(cfg.Database.MaxIdleConns)
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
98
cloudprint-backend/config/config.go
Normal file
98
cloudprint-backend/config/config.go
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Server ServerConfig `mapstructure:"server"`
|
||||||
|
Database DatabaseConfig `mapstructure:"database"`
|
||||||
|
Redis RedisConfig `mapstructure:"redis"`
|
||||||
|
JWT JWTConfig `mapstructure:"jwt"`
|
||||||
|
WeChat WeChatConfig `mapstructure:"wechat"`
|
||||||
|
Upload UploadConfig `mapstructure:"upload"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServerConfig struct {
|
||||||
|
Host string `mapstructure:"host"`
|
||||||
|
Port int `mapstructure:"port"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DatabaseConfig struct {
|
||||||
|
Host string `mapstructure:"host"`
|
||||||
|
Port int `mapstructure:"port"`
|
||||||
|
Username string `mapstructure:"username"`
|
||||||
|
Password string `mapstructure:"password"`
|
||||||
|
Database string `mapstructure:"database"`
|
||||||
|
MaxOpenConns int `mapstructure:"max_open_conns"`
|
||||||
|
MaxIdleConns int `mapstructure:"max_idle_conns"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DatabaseConfig) DSN() string {
|
||||||
|
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
||||||
|
d.Username, d.Password, d.Host, d.Port, d.Database)
|
||||||
|
}
|
||||||
|
|
||||||
|
type RedisConfig struct {
|
||||||
|
Host string `mapstructure:"host"`
|
||||||
|
Port int `mapstructure:"port"`
|
||||||
|
Password string `mapstructure:"password"`
|
||||||
|
DB int `mapstructure:"db"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RedisConfig) Addr() string {
|
||||||
|
return fmt.Sprintf("%s:%d", r.Host, r.Port)
|
||||||
|
}
|
||||||
|
|
||||||
|
type JWTConfig struct {
|
||||||
|
Secret string `mapstructure:"secret"`
|
||||||
|
AccessTokenTTL time.Duration `mapstructure:"access_token_ttl"`
|
||||||
|
RefreshTokenTTL time.Duration `mapstructure:"refresh_token_ttl"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WeChatConfig struct {
|
||||||
|
AppID string `mapstructure:"app_id"`
|
||||||
|
AppSecret string `mapstructure:"app_secret"`
|
||||||
|
MchID string `mapstructure:"mch_id"`
|
||||||
|
ApiKey string `mapstructure:"api_key"`
|
||||||
|
NotifyURL string `mapstructure:"notify_url"`
|
||||||
|
CertPath string `mapstructure:"cert_path"`
|
||||||
|
CertContent string `mapstructure:"cert_content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UploadConfig struct {
|
||||||
|
Path string `mapstructure:"path"`
|
||||||
|
MaxSize int64 `mapstructure:"max_size"`
|
||||||
|
ChunkPath string `mapstructure:"chunk_path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var GlobalConfig *Config
|
||||||
|
|
||||||
|
func LoadConfig(path string) (*Config, error) {
|
||||||
|
viper.SetConfigFile(path)
|
||||||
|
viper.SetConfigType("yaml")
|
||||||
|
|
||||||
|
viper.SetDefault("server.host", "0.0.0.0")
|
||||||
|
viper.SetDefault("server.port", 8080)
|
||||||
|
viper.SetDefault("database.max_open_conns", 100)
|
||||||
|
viper.SetDefault("database.max_idle_conns", 10)
|
||||||
|
viper.SetDefault("jwt.access_token_ttl", "24h")
|
||||||
|
viper.SetDefault("jwt.refresh_token_ttl", "7d")
|
||||||
|
viper.SetDefault("upload.max_size", 52428800) // 50MB
|
||||||
|
viper.SetDefault("upload.chunk_path", "./uploads/chunks")
|
||||||
|
|
||||||
|
if err := viper.ReadInConfig(); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg Config
|
||||||
|
if err := viper.Unmarshal(&cfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
GlobalConfig = &cfg
|
||||||
|
return &cfg, nil
|
||||||
|
}
|
||||||
1132
cloudprint-backend/docs/openapi.yaml
Normal file
1132
cloudprint-backend/docs/openapi.yaml
Normal file
File diff suppressed because it is too large
Load Diff
43
cloudprint-backend/go.mod
Normal file
43
cloudprint-backend/go.mod
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
module github.com/cloudprint/cloudprint-backend
|
||||||
|
|
||||||
|
go 1.21
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||||
|
github.com/google/uuid v1.5.0
|
||||||
|
github.com/iGoogle-ink/gopay v1.5.14
|
||||||
|
github.com/spf13/viper v1.18.2
|
||||||
|
go.uber.org/zap v1.26.0
|
||||||
|
golang.org/x/crypto v0.18.0
|
||||||
|
google.golang.org/grpc v1.60.1
|
||||||
|
google.golang.org/protobuf v1.32.0
|
||||||
|
gorm.io/driver/mysql v1.5.2
|
||||||
|
gorm.io/gorm v1.25.5
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
|
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||||
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/magiconair/properties v1.8.7 // indirect
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||||
|
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||||
|
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||||
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
|
github.com/spf13/afero v1.11.0 // indirect
|
||||||
|
github.com/spf13/cast v1.6.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||||
|
golang.org/x/net v0.20.0 // indirect
|
||||||
|
golang.org/x/sys v0.16.0 // indirect
|
||||||
|
golang.org/x/text v0.14.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect
|
||||||
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
303
cloudprint-backend/go.sum
Normal file
303
cloudprint-backend/go.sum
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw=
|
||||||
|
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/DataDog/sketches-go v0.0.0-20190923095040-43f19ad77ff7/go.mod h1:Q5DbzQ+3AkgGwymQO7aZFNP7ns2lZKGtvRBzRXfdi60=
|
||||||
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
|
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||||
|
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
||||||
|
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||||
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
|
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||||
|
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||||
|
github.com/benbjohnson/clock v1.0.0/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM=
|
||||||
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
|
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||||
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
|
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/denisenkom/go-mssqldb v0.0.0-20190707035753-2be1aa521ff4/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM=
|
||||||
|
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200609043717-5ab96a526299/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
|
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||||
|
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||||
|
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||||
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
|
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
|
||||||
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||||
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
|
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||||
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
|
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
|
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||||
|
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||||
|
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||||
|
github.com/go-redis/redis/v8 v8.0.0-beta.5/go.mod h1:Mm9EH/5UMRx680UIryN6rd5XFn/L7zORPqLV+1D5thQ=
|
||||||
|
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||||
|
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
|
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||||
|
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||||
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
|
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
|
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||||
|
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
|
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
|
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||||
|
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||||
|
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||||
|
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||||
|
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||||
|
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
|
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
github.com/iGoogle-ink/gopay v1.5.14 h1:vcwdqNeccx8FWrnqKTeZZSK5FcAHGUD07OFHwhMheBs=
|
||||||
|
github.com/iGoogle-ink/gopay v1.5.14/go.mod h1:JuI4rWXHSTEgFHD5C5xfpy3Do3b0pIcu/wdsRyw/Ds4=
|
||||||
|
github.com/iGoogle-ink/gotil v1.0.2 h1:z58bvmhq31twxecMkyWU5Mh8H7T9SGPOTvUuAw9vB7M=
|
||||||
|
github.com/iGoogle-ink/gotil v1.0.2/go.mod h1:HOeCIcJkJLAsQ+cAPdozZS3huVDT4YUKN/F7tmvvrbM=
|
||||||
|
github.com/jinzhu/gorm v1.9.14/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||||
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||||
|
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
|
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
|
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||||
|
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||||
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
|
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
|
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
|
github.com/opentracing/opentracing-go v1.1.1-0.20190913142402-a7454ce5950e/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||||
|
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||||
|
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||||
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
|
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
|
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
|
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||||
|
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||||
|
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||||
|
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||||
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
|
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||||
|
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||||
|
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||||
|
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||||
|
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||||
|
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||||
|
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||||
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
|
||||||
|
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
|
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
||||||
|
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||||
|
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||||
|
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
|
||||||
|
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||||
|
go.opentelemetry.io/otel v0.6.0/go.mod h1:jzBIgIzK43Iu1BpDAXwqOd6UPsSAk+ewVZ5ofSXw4Ek=
|
||||||
|
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
||||||
|
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
||||||
|
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||||
|
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||||
|
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||||
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
||||||
|
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||||
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||||
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||||
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
|
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||||
|
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||||
|
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||||
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
google.golang.org/appengine v1.6.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
|
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
|
google.golang.org/genproto v0.0.0-20191009194640-548a555dbc03/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f h1:ultW7fxlIvee4HYrtnaRPon9HpEgFk5zYpmfMgtKB5I=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc=
|
||||||
|
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||||
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
|
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||||
|
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||||
|
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||||
|
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||||
|
google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU=
|
||||||
|
google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
|
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
||||||
|
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||||
|
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs=
|
||||||
|
gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8=
|
||||||
|
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||||
|
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
|
||||||
|
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||||
|
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
xorm.io/builder v0.3.7/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
|
||||||
|
xorm.io/xorm v1.0.2/go.mod h1:o4vnEsQ5V2F1/WK6w4XTwmiWJeGj82tqjAnHe44wVHY=
|
||||||
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)
|
||||||
|
}
|
||||||
105
cloudprint-backend/pkg/jwt/jwt.go
Normal file
105
cloudprint-backend/pkg/jwt/jwt.go
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
package jwt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidToken = errors.New("invalid token")
|
||||||
|
ErrExpiredToken = errors.New("token has expired")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Claims struct {
|
||||||
|
UserID int32 `json:"user_id"`
|
||||||
|
UserType string `json:"user_type"` // "user" or "admin"
|
||||||
|
Username string `json:"username,omitempty"`
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
type JWTManager struct {
|
||||||
|
secretKey []byte
|
||||||
|
accessTokenTTL time.Duration
|
||||||
|
refreshTokenTTL time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewJWTManager(secret string, accessTokenTTL, refreshTokenTTL time.Duration) *JWTManager {
|
||||||
|
return &JWTManager{
|
||||||
|
secretKey: []byte(secret),
|
||||||
|
accessTokenTTL: accessTokenTTL,
|
||||||
|
refreshTokenTTL: refreshTokenTTL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *JWTManager) GenerateAccessToken(userID int32, userType string, username string) (string, error) {
|
||||||
|
now := time.Now()
|
||||||
|
claims := &Claims{
|
||||||
|
UserID: userID,
|
||||||
|
UserType: userType,
|
||||||
|
Username: username,
|
||||||
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
|
ExpiresAt: jwt.NewNumericDate(now.Add(m.accessTokenTTL)),
|
||||||
|
IssuedAt: jwt.NewNumericDate(now),
|
||||||
|
NotBefore: jwt.NewNumericDate(now),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
return token.SignedString(m.secretKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *JWTManager) GenerateRefreshToken(userID int32, userType string) (string, error) {
|
||||||
|
now := time.Now()
|
||||||
|
claims := &Claims{
|
||||||
|
UserID: userID,
|
||||||
|
UserType: userType,
|
||||||
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
|
ExpiresAt: jwt.NewNumericDate(now.Add(m.refreshTokenTTL)),
|
||||||
|
IssuedAt: jwt.NewNumericDate(now),
|
||||||
|
NotBefore: jwt.NewNumericDate(now),
|
||||||
|
Subject: "refresh",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
return token.SignedString(m.secretKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *JWTManager) ValidateToken(tokenString string) (*Claims, error) {
|
||||||
|
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, ErrInvalidToken
|
||||||
|
}
|
||||||
|
return m.secretKey, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, jwt.ErrTokenExpired) {
|
||||||
|
return nil, ErrExpiredToken
|
||||||
|
}
|
||||||
|
return nil, ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := token.Claims.(*Claims)
|
||||||
|
if !ok || !token.Valid {
|
||||||
|
return nil, ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *JWTManager) RefreshAccessToken(refreshToken string) (string, error) {
|
||||||
|
claims, err := m.ValidateToken(refreshToken)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only refresh tokens with subject "refresh" can be used
|
||||||
|
if claims.Subject != "refresh" {
|
||||||
|
return "", ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.GenerateAccessToken(claims.UserID, claims.UserType, claims.Username)
|
||||||
|
}
|
||||||
200
cloudprint-backend/pkg/pay/wechat.go
Normal file
200
cloudprint-backend/pkg/pay/wechat.go
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
package pay
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cloudprint/cloudprint-backend/config"
|
||||||
|
"github.com/iGoogle-ink/gopay"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RandomString generates a random string with given length
|
||||||
|
func RandomString(length int) string {
|
||||||
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
b := make([]byte, length)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = charset[rand.Intn(len(charset))]
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
type WeChatPay struct {
|
||||||
|
client *gopay.WxPayClient
|
||||||
|
config *config.WeChatConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnifiedOrderResponse struct {
|
||||||
|
ReturnCode string `xml:"return_code"`
|
||||||
|
ReturnMsg string `xml:"return_msg"`
|
||||||
|
ResultCode string `xml:"result_code"`
|
||||||
|
PrepayID string `xml:"prepay_id,omitempty"`
|
||||||
|
CodeURL string `xml:"code_url,omitempty"`
|
||||||
|
MWebURL string `xml:"mweb_url,omitempty"`
|
||||||
|
ErrCode string `xml:"err_code,omitempty"`
|
||||||
|
ErrMsg string `xml:"err_msg,omitempty"`
|
||||||
|
TransactionID string `xml:"transaction_id,omitempty"`
|
||||||
|
TradeState string `xml:"trade_state,omitempty"`
|
||||||
|
TotalFee int `xml:"total_fee,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PayCallback struct {
|
||||||
|
ReturnCode string `xml:"return_code"`
|
||||||
|
ReturnMsg string `xml:"return_msg"`
|
||||||
|
AppID string `xml:"appid"`
|
||||||
|
MchID string `xml:"mch_id"`
|
||||||
|
NonceStr string `xml:"nonce_str"`
|
||||||
|
Sign string `xml:"sign"`
|
||||||
|
ResultCode string `xml:"result_code"`
|
||||||
|
TransactionID string `xml:"transaction_id"`
|
||||||
|
OutTradeNo string `xml:"out_trade_no"`
|
||||||
|
TotalFee int `xml:"total_fee"`
|
||||||
|
TimeEnd string `xml:"time_end"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWeChatPay(cfg *config.WeChatConfig) *WeChatPay {
|
||||||
|
client := gopay.NewWxPayClient(cfg.AppID, cfg.MchID, cfg.ApiKey, cfg.NotifyURL)
|
||||||
|
if cfg.CertPath != "" {
|
||||||
|
client.SetCertFilePath(cfg.CertPath)
|
||||||
|
}
|
||||||
|
return &WeChatPay{
|
||||||
|
client: client,
|
||||||
|
config: cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *WeChatPay) createSign(params map[string]string) string {
|
||||||
|
keys := make([]string, 0, len(params))
|
||||||
|
for k := range params {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
var signParts []string
|
||||||
|
for _, k := range keys {
|
||||||
|
if params[k] != "" && k != "sign" {
|
||||||
|
signParts = append(signParts, fmt.Sprintf("%s=%s", k, params[k]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
signStr := strings.Join(signParts, "&")
|
||||||
|
signStr += "&key=" + p.config.ApiKey
|
||||||
|
|
||||||
|
md5Hash := md5.New()
|
||||||
|
md5Hash.Write([]byte(signStr))
|
||||||
|
return strings.ToUpper(hex.EncodeToString(md5Hash.Sum(nil)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *WeChatPay) UnifiedOrder(ctx context.Context, orderNo string, amount float64, description, openID, clientIP string) (*UnifiedOrderResponse, error) {
|
||||||
|
nonceStr := RandomString(32)
|
||||||
|
totalFee := int(amount * 100) // Convert to fen
|
||||||
|
|
||||||
|
// Build request body
|
||||||
|
body := gopay.UnifiedOrder{
|
||||||
|
AppID: p.config.AppID,
|
||||||
|
MchID: p.config.MchID,
|
||||||
|
NonceStr: nonceStr,
|
||||||
|
Body: description,
|
||||||
|
OutTradeNo: orderNo,
|
||||||
|
TotalFee: totalFee,
|
||||||
|
SpbillCreateIP: clientIP,
|
||||||
|
NotifyURL: p.config.NotifyURL,
|
||||||
|
TradeType: "JSAPI",
|
||||||
|
OpenID: openID,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use go-pay client to call unified order
|
||||||
|
resp, err := p.client.UnifiedOrder(ctx, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unified order failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &UnifiedOrderResponse{
|
||||||
|
ReturnCode: resp.GetString("return_code"),
|
||||||
|
ReturnMsg: resp.GetString("return_msg"),
|
||||||
|
ResultCode: resp.GetString("result_code"),
|
||||||
|
PrepayID: resp.GetString("prepay_id"),
|
||||||
|
CodeURL: resp.GetString("code_url"),
|
||||||
|
MWebURL: resp.GetString("mweb_url"),
|
||||||
|
ErrCode: resp.GetString("err_code"),
|
||||||
|
ErrMsg: resp.GetString("err_msg"),
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *WeChatPay) QueryOrder(ctx context.Context, orderNo string) (string, int, error) {
|
||||||
|
nonceStr := RandomString(32)
|
||||||
|
|
||||||
|
body := gopay.QueryOrder{
|
||||||
|
AppID: p.config.AppID,
|
||||||
|
MchID: p.config.MchID,
|
||||||
|
NonceStr: nonceStr,
|
||||||
|
OutTradeNo: orderNo,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := p.client.QueryOrder(ctx, body)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tradeState := resp.GetString("trade_state")
|
||||||
|
totalFee := resp.GetInt("total_fee")
|
||||||
|
|
||||||
|
return tradeState, totalFee, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *WeChatPay) ParseCallback(body []byte) (*PayCallback, error) {
|
||||||
|
var callback PayCallback
|
||||||
|
if err := xml.Unmarshal(body, &callback); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse callback: %w", err)
|
||||||
|
}
|
||||||
|
return &callback, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *WeChatPay) ValidateCallback(callback *PayCallback) bool {
|
||||||
|
params := map[string]string{
|
||||||
|
"return_code": callback.ReturnCode,
|
||||||
|
"return_msg": callback.ReturnMsg,
|
||||||
|
"appid": callback.AppID,
|
||||||
|
"mch_id": callback.MchID,
|
||||||
|
"nonce_str": callback.NonceStr,
|
||||||
|
"result_code": callback.ResultCode,
|
||||||
|
"transaction_id": callback.TransactionID,
|
||||||
|
"out_trade_no": callback.OutTradeNo,
|
||||||
|
"total_fee": fmt.Sprintf("%d", callback.TotalFee),
|
||||||
|
"time_end": callback.TimeEnd,
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedSign := p.createSign(params)
|
||||||
|
return callback.Sign == expectedSign
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *WeChatPay) GenerateJSAPIPayParams(prepayID string) (map[string]string, error) {
|
||||||
|
nonceStr := RandomString(32)
|
||||||
|
timestamp := fmt.Sprintf("%d", time.Now().Unix())
|
||||||
|
|
||||||
|
params := map[string]string{
|
||||||
|
"appId": p.config.AppID,
|
||||||
|
"timeStamp": timestamp,
|
||||||
|
"nonceStr": nonceStr,
|
||||||
|
"package": "prepay_id=" + prepayID,
|
||||||
|
"signType": "MD5",
|
||||||
|
}
|
||||||
|
|
||||||
|
paySign := p.createSign(map[string]string{
|
||||||
|
"appId": p.config.AppID,
|
||||||
|
"timeStamp": timestamp,
|
||||||
|
"nonceStr": nonceStr,
|
||||||
|
"package": "prepay_id=" + prepayID,
|
||||||
|
"signType": "MD5",
|
||||||
|
})
|
||||||
|
|
||||||
|
params["paySign"] = paySign
|
||||||
|
return params, nil
|
||||||
|
}
|
||||||
196
cloudprint-backend/pkg/upload/upload.go
Normal file
196
cloudprint-backend/pkg/upload/upload.go
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
package upload
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// AllowedFileTypes 文件类型白名单
|
||||||
|
AllowedFileTypes = map[string]bool{
|
||||||
|
"pdf": true,
|
||||||
|
"doc": true,
|
||||||
|
"docx": true,
|
||||||
|
"xls": true,
|
||||||
|
"xlsx": true,
|
||||||
|
"ppt": true,
|
||||||
|
"pptx": true,
|
||||||
|
"txt": true,
|
||||||
|
"jpg": true,
|
||||||
|
"jpeg": true,
|
||||||
|
"png": true,
|
||||||
|
"bmp": true,
|
||||||
|
"gif": true,
|
||||||
|
"zip": true,
|
||||||
|
"rar": true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type FileUploader struct {
|
||||||
|
basePath string
|
||||||
|
chunkPath string
|
||||||
|
maxSize int64
|
||||||
|
mu sync.RWMutex
|
||||||
|
chunks map[string]*ChunkInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChunkInfo struct {
|
||||||
|
FileName string
|
||||||
|
TotalParts int
|
||||||
|
Parts map[int][]byte
|
||||||
|
CreatedAt int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFileUploader(basePath string, chunkPath string, maxSize int64) *FileUploader {
|
||||||
|
uploader := &FileUploader{
|
||||||
|
basePath: basePath,
|
||||||
|
chunkPath: chunkPath,
|
||||||
|
maxSize: maxSize,
|
||||||
|
chunks: make(map[string]*ChunkInfo),
|
||||||
|
}
|
||||||
|
// Ensure directories exist
|
||||||
|
os.MkdirAll(basePath, 0755)
|
||||||
|
os.MkdirAll(chunkPath, 0755)
|
||||||
|
return uploader
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *FileUploader) getExt(filename string) string {
|
||||||
|
ext := strings.ToLower(filepath.Ext(filename))
|
||||||
|
return strings.TrimPrefix(ext, ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *FileUploader) ValidateFileType(filename string) bool {
|
||||||
|
ext := u.getExt(filename)
|
||||||
|
return AllowedFileTypes[ext]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *FileUploader) GenerateFilePath(filename string) string {
|
||||||
|
ext := u.getExt(filename)
|
||||||
|
newName := fmt.Sprintf("%s.%s", uuid.New().String(), ext)
|
||||||
|
// 按日期分目录
|
||||||
|
// dateDir := time.Now().Format("2006/01/02")
|
||||||
|
// return filepath.Join(dateDir, newName)
|
||||||
|
return newName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *FileUploader) Upload(ctx context.Context, file *multipart.FileHeader) (string, error) {
|
||||||
|
if u.maxSize > 0 && file.Size > u.maxSize {
|
||||||
|
return "", fmt.Errorf("file size exceeds limit: %d > %d", file.Size, u.maxSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !u.ValidateFileType(file.Filename) {
|
||||||
|
return "", fmt.Errorf("file type not allowed: %s", file.Filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
src, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to open file: %w", err)
|
||||||
|
}
|
||||||
|
defer src.Close()
|
||||||
|
|
||||||
|
filePath := u.GenerateFilePath(file.Filename)
|
||||||
|
fullPath := filepath.Join(u.basePath, filePath)
|
||||||
|
|
||||||
|
// Ensure parent directory exists
|
||||||
|
if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dst, err := os.Create(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create file: %w", err)
|
||||||
|
}
|
||||||
|
defer dst.Close()
|
||||||
|
|
||||||
|
if _, err := io.Copy(dst, src); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to write file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return filePath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chunk upload support
|
||||||
|
func (u *FileUploader) InitChunkUpload(chunkID, fileName string, totalParts int) error {
|
||||||
|
u.mu.Lock()
|
||||||
|
defer u.mu.Unlock()
|
||||||
|
|
||||||
|
u.chunks[chunkID] = &ChunkInfo{
|
||||||
|
FileName: fileName,
|
||||||
|
TotalParts: totalParts,
|
||||||
|
Parts: make(map[int][]byte),
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *FileUploader) SaveChunk(chunkID string, partIndex int, data []byte) error {
|
||||||
|
u.mu.Lock()
|
||||||
|
defer u.mu.Unlock()
|
||||||
|
|
||||||
|
chunk, ok := u.chunks[chunkID]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("chunk upload not initialized: %s", chunkID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if partIndex < 0 || partIndex >= chunk.TotalParts {
|
||||||
|
return fmt.Errorf("invalid part index: %d", partIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
chunk.Parts[partIndex] = data
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *FileUploader) CompleteChunkUpload(ctx context.Context, chunkID string) (string, error) {
|
||||||
|
u.mu.Lock()
|
||||||
|
chunk, ok := u.chunks[chunkID]
|
||||||
|
if !ok {
|
||||||
|
u.mu.Unlock()
|
||||||
|
return "", fmt.Errorf("chunk upload not found: %s", chunkID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(chunk.Parts) != chunk.TotalParts {
|
||||||
|
u.mu.Unlock()
|
||||||
|
return "", fmt.Errorf("incomplete chunks: %d/%d", len(chunk.Parts), chunk.TotalParts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete chunk info early to allow other operations
|
||||||
|
delete(u.chunks, chunkID)
|
||||||
|
u.mu.Unlock()
|
||||||
|
|
||||||
|
filePath := u.GenerateFilePath(chunk.FileName)
|
||||||
|
fullPath := filepath.Join(u.basePath, filePath)
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Create(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create file: %w", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
for i := 0; i < chunk.TotalParts; i++ {
|
||||||
|
if _, err := f.Write(chunk.Parts[i]); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to write chunk %d: %w", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filePath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *FileUploader) DeleteFile(filePath string) error {
|
||||||
|
fullPath := filepath.Join(u.basePath, filePath)
|
||||||
|
return os.Remove(fullPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *FileUploader) GetFilePath(filePath string) string {
|
||||||
|
return filepath.Join(u.basePath, filePath)
|
||||||
|
}
|
||||||
502
cloudprint-backend/proto/cloudprint.proto
Normal file
502
cloudprint-backend/proto/cloudprint.proto
Normal file
@@ -0,0 +1,502 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package cloudprint;
|
||||||
|
|
||||||
|
option go_package = "github.com/cloudprint/cloudprint-backend/proto/generated";
|
||||||
|
|
||||||
|
import "google/protobuf/empty.proto";
|
||||||
|
import "google/protobuf/timestamp.proto";
|
||||||
|
|
||||||
|
// ============ 通用消息 ============
|
||||||
|
|
||||||
|
message Empty {}
|
||||||
|
|
||||||
|
message StatusResponse {
|
||||||
|
int32 code = 1;
|
||||||
|
string message = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 用户相关 ============
|
||||||
|
|
||||||
|
message User {
|
||||||
|
int32 id = 1;
|
||||||
|
string openid = 2;
|
||||||
|
string nickname = 3;
|
||||||
|
string avatar = 4;
|
||||||
|
double balance = 5;
|
||||||
|
google.protobuf.Timestamp created_at = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LoginRequest {
|
||||||
|
string code = 1; // 微信授权码
|
||||||
|
}
|
||||||
|
|
||||||
|
message LoginResponse {
|
||||||
|
string access_token = 1;
|
||||||
|
string refresh_token = 2;
|
||||||
|
User user = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RefreshRequest {
|
||||||
|
string refresh_token = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RefreshResponse {
|
||||||
|
string access_token = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetUserInfoRequest {}
|
||||||
|
|
||||||
|
message UpdateUserRequest {
|
||||||
|
string nickname = 1;
|
||||||
|
string avatar = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 文件相关 ============
|
||||||
|
|
||||||
|
message FileInfo {
|
||||||
|
int32 id = 1;
|
||||||
|
string file_name = 2;
|
||||||
|
string file_path = 3;
|
||||||
|
string file_type = 4;
|
||||||
|
int64 file_size = 5;
|
||||||
|
string uploader_type = 6;
|
||||||
|
int32 uploader_id = 7;
|
||||||
|
google.protobuf.Timestamp created_at = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UploadRequest {
|
||||||
|
string file_name = 1;
|
||||||
|
string file_type = 2;
|
||||||
|
int64 file_size = 3;
|
||||||
|
bytes data = 4;
|
||||||
|
bool is_last_chunk = 5;
|
||||||
|
string chunk_id = 6;
|
||||||
|
int32 chunk_index = 7;
|
||||||
|
int32 total_chunks = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UploadResponse {
|
||||||
|
int32 file_id = 1;
|
||||||
|
string file_path = 2;
|
||||||
|
string file_url = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetFileRequest {
|
||||||
|
int32 file_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message FileData {
|
||||||
|
bytes data = 1;
|
||||||
|
bool is_last = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeleteFileRequest {
|
||||||
|
int32 file_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message WatermarkRequest {
|
||||||
|
int32 file_id = 1;
|
||||||
|
string text = 2;
|
||||||
|
float opacity = 3;
|
||||||
|
int32 position = 4; // 1: 左上 2: 右上 3: 左下 4: 右下 5: 居中
|
||||||
|
int32 font_size = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message WatermarkResponse {
|
||||||
|
int32 file_id = 1;
|
||||||
|
string file_path = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetFileListRequest {
|
||||||
|
string uploader_type = 1;
|
||||||
|
int32 uploader_id = 2;
|
||||||
|
int32 page = 3;
|
||||||
|
int32 page_size = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message FileListResponse {
|
||||||
|
repeated FileInfo files = 1;
|
||||||
|
int32 total = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 订单相关 ============
|
||||||
|
|
||||||
|
message Order {
|
||||||
|
int32 id = 1;
|
||||||
|
string order_no = 2;
|
||||||
|
int32 user_id = 3;
|
||||||
|
int32 status = 4;
|
||||||
|
double total_amount = 5;
|
||||||
|
string remark = 6;
|
||||||
|
google.protobuf.Timestamp created_at = 7;
|
||||||
|
google.protobuf.Timestamp paid_at = 8;
|
||||||
|
repeated OrderItem items = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
message OrderItem {
|
||||||
|
int32 id = 1;
|
||||||
|
int32 order_id = 2;
|
||||||
|
int32 file_id = 3;
|
||||||
|
FileInfo file = 4;
|
||||||
|
int32 printer_id = 5;
|
||||||
|
int32 copies = 6;
|
||||||
|
bool color = 7;
|
||||||
|
bool duplex = 8;
|
||||||
|
string page_range = 9;
|
||||||
|
double price = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateOrderRequest {
|
||||||
|
repeated CreateOrderItem items = 1;
|
||||||
|
string remark = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateOrderItem {
|
||||||
|
int32 file_id = 1;
|
||||||
|
int32 printer_id = 2;
|
||||||
|
int32 copies = 3;
|
||||||
|
bool color = 4;
|
||||||
|
bool duplex = 5;
|
||||||
|
string page_range = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateOrderResponse {
|
||||||
|
string order_no = 1;
|
||||||
|
double total_amount = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetOrderRequest {
|
||||||
|
string order_no = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetOrderListRequest {
|
||||||
|
int32 user_id = 1;
|
||||||
|
int32 status = 2;
|
||||||
|
int32 page = 3;
|
||||||
|
int32 page_size = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message OrderListResponse {
|
||||||
|
repeated Order orders = 1;
|
||||||
|
int32 total = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdateOrderStatusRequest {
|
||||||
|
string order_no = 1;
|
||||||
|
int32 status = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CancelOrderRequest {
|
||||||
|
string order_no = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 打印机相关 ============
|
||||||
|
|
||||||
|
message Printer {
|
||||||
|
int32 id = 1;
|
||||||
|
string name = 2;
|
||||||
|
string display_name = 3;
|
||||||
|
bool is_active = 4;
|
||||||
|
int32 status = 5; // 0: 空闲 1: 打印中 2: 离线
|
||||||
|
google.protobuf.Timestamp last_heartbeat = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetPrintersRequest {}
|
||||||
|
|
||||||
|
message PrinterListResponse {
|
||||||
|
repeated Printer printers = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetPrinterStatusRequest {
|
||||||
|
int32 printer_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PrinterStatusResponse {
|
||||||
|
Printer printer = 1;
|
||||||
|
int32 queue_count = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RegisterPrinterRequest {
|
||||||
|
string name = 1;
|
||||||
|
string display_name = 2;
|
||||||
|
string api_key = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RegisterPrinterResponse {
|
||||||
|
int32 printer_id = 1;
|
||||||
|
string token = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PrinterHeartbeatRequest {
|
||||||
|
int32 printer_id = 1;
|
||||||
|
int32 status = 2;
|
||||||
|
string token = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 打印任务相关 ============
|
||||||
|
|
||||||
|
message PrintJob {
|
||||||
|
int32 id = 1;
|
||||||
|
string job_no = 2;
|
||||||
|
int32 printer_id = 3;
|
||||||
|
int32 order_item_id = 4;
|
||||||
|
string status = 5; // pending/printing/completed/failed/cancelled
|
||||||
|
int32 progress = 6;
|
||||||
|
string error_msg = 7;
|
||||||
|
google.protobuf.Timestamp started_at = 8;
|
||||||
|
google.protobuf.Timestamp completed_at = 9;
|
||||||
|
OrderItem order_item = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SubmitPrintJobRequest {
|
||||||
|
int32 order_item_id = 1;
|
||||||
|
int32 printer_id = 2;
|
||||||
|
string file_path = 3;
|
||||||
|
int32 copies = 4;
|
||||||
|
bool color = 5;
|
||||||
|
bool duplex = 6;
|
||||||
|
string page_range = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SubmitPrintJobResponse {
|
||||||
|
string job_no = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CancelPrintJobRequest {
|
||||||
|
string job_no = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SubscribeJobsRequest {
|
||||||
|
int32 printer_id = 1;
|
||||||
|
string token = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ReportJobStatusRequest {
|
||||||
|
string job_no = 1;
|
||||||
|
string status = 2;
|
||||||
|
int32 progress = 3;
|
||||||
|
string error_msg = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 文库相关 ============
|
||||||
|
|
||||||
|
message LibraryCategory {
|
||||||
|
int32 id = 1;
|
||||||
|
string name = 2;
|
||||||
|
string icon = 3;
|
||||||
|
int32 sort_order = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LibraryFile {
|
||||||
|
int32 id = 1;
|
||||||
|
int32 category_id = 2;
|
||||||
|
int32 file_id = 3;
|
||||||
|
string title = 4;
|
||||||
|
string description = 5;
|
||||||
|
int32 page_count = 6;
|
||||||
|
int32 download_count = 7;
|
||||||
|
FileInfo file = 8;
|
||||||
|
google.protobuf.Timestamp created_at = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetCategoriesRequest {}
|
||||||
|
|
||||||
|
message CategoryListResponse {
|
||||||
|
repeated LibraryCategory categories = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetLibraryFilesRequest {
|
||||||
|
int32 category_id = 1;
|
||||||
|
int32 page = 2;
|
||||||
|
int32 page_size = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LibraryFileListResponse {
|
||||||
|
repeated LibraryFile files = 1;
|
||||||
|
int32 total = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UploadLibraryFileRequest {
|
||||||
|
int32 category_id = 1;
|
||||||
|
string title = 2;
|
||||||
|
string description = 3;
|
||||||
|
bytes data = 4;
|
||||||
|
string file_name = 5;
|
||||||
|
string file_type = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UploadLibraryFileResponse {
|
||||||
|
int32 library_file_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeleteLibraryFileRequest {
|
||||||
|
int32 library_file_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 价目表相关 ============
|
||||||
|
|
||||||
|
message PriceItem {
|
||||||
|
int32 id = 1;
|
||||||
|
string name = 2;
|
||||||
|
string type = 3;
|
||||||
|
double price = 4;
|
||||||
|
string unit = 5;
|
||||||
|
bool color_enabled = 6;
|
||||||
|
bool is_active = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetPriceListRequest {}
|
||||||
|
|
||||||
|
message PriceListResponse {
|
||||||
|
repeated PriceItem items = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreatePriceItemRequest {
|
||||||
|
string name = 1;
|
||||||
|
string type = 2;
|
||||||
|
double price = 3;
|
||||||
|
string unit = 4;
|
||||||
|
bool color_enabled = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdatePriceItemRequest {
|
||||||
|
int32 id = 1;
|
||||||
|
string name = 2;
|
||||||
|
string type = 3;
|
||||||
|
double price = 4;
|
||||||
|
string unit = 5;
|
||||||
|
bool color_enabled = 6;
|
||||||
|
bool is_active = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 支付相关 ============
|
||||||
|
|
||||||
|
message WxPayRequest {
|
||||||
|
string order_no = 1;
|
||||||
|
double amount = 2;
|
||||||
|
string description = 3;
|
||||||
|
string openid = 4;
|
||||||
|
string client_ip = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message WxPayResponse {
|
||||||
|
string prepay_id = 1;
|
||||||
|
string code_url = 2;
|
||||||
|
string mweb_url = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message QueryPayStatusRequest {
|
||||||
|
string order_no = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PayStatusResponse {
|
||||||
|
int32 status = 1; // 0: 待支付 1: 支付中 2: 成功 3: 失败
|
||||||
|
string transaction_id = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PayCallbackRequest {
|
||||||
|
string body = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PayCallbackResponse {
|
||||||
|
string return_code = 1;
|
||||||
|
string return_msg = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PayByBalanceRequest {
|
||||||
|
string order_no = 1;
|
||||||
|
int32 user_id = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 管理后台相关 ============
|
||||||
|
|
||||||
|
message Admin {
|
||||||
|
int32 id = 1;
|
||||||
|
string username = 2;
|
||||||
|
string nickname = 3;
|
||||||
|
int32 role = 4;
|
||||||
|
google.protobuf.Timestamp created_at = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AdminLoginRequest {
|
||||||
|
string username = 1;
|
||||||
|
string password = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AdminLoginResponse {
|
||||||
|
string access_token = 1;
|
||||||
|
Admin admin = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetStatisticsRequest {}
|
||||||
|
|
||||||
|
message Statistics {
|
||||||
|
int32 total_users = 1;
|
||||||
|
int32 total_orders = 2;
|
||||||
|
int32 today_orders = 3;
|
||||||
|
double today_revenue = 4;
|
||||||
|
int32 pending_orders = 5;
|
||||||
|
int32 active_printers = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 服务定义 ============
|
||||||
|
|
||||||
|
service AuthService {
|
||||||
|
rpc Login(LoginRequest) returns (LoginResponse);
|
||||||
|
rpc RefreshToken(RefreshRequest) returns (RefreshResponse);
|
||||||
|
rpc GetUserInfo(GetUserInfoRequest) returns (User);
|
||||||
|
rpc UpdateUser(UpdateUserRequest) returns (User);
|
||||||
|
}
|
||||||
|
|
||||||
|
service FileService {
|
||||||
|
rpc Upload(stream UploadRequest) returns (UploadResponse);
|
||||||
|
rpc GetFile(GetFileRequest) returns (stream FileData);
|
||||||
|
rpc DeleteFile(DeleteFileRequest) returns (Empty);
|
||||||
|
rpc AddWatermark(WatermarkRequest) returns (WatermarkResponse);
|
||||||
|
rpc GetFileList(GetFileListRequest) returns (FileListResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
service OrderService {
|
||||||
|
rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse);
|
||||||
|
rpc GetOrder(GetOrderRequest) returns (Order);
|
||||||
|
rpc GetOrderList(GetOrderListRequest) returns (OrderListResponse);
|
||||||
|
rpc UpdateOrderStatus(UpdateOrderStatusRequest) returns (Empty);
|
||||||
|
rpc CancelOrder(CancelOrderRequest) returns (Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
service PrintService {
|
||||||
|
rpc GetPrinters(GetPrintersRequest) returns (PrinterListResponse);
|
||||||
|
rpc GetPrinterStatus(GetPrinterStatusRequest) returns (PrinterStatusResponse);
|
||||||
|
rpc RegisterPrinter(RegisterPrinterRequest) returns (RegisterPrinterResponse);
|
||||||
|
rpc PrinterHeartbeat(PrinterHeartbeatRequest) returns (Empty);
|
||||||
|
rpc SubmitPrintJob(SubmitPrintJobRequest) returns (SubmitPrintJobResponse);
|
||||||
|
rpc CancelPrintJob(CancelPrintJobRequest) returns (Empty);
|
||||||
|
rpc SubscribeJobs(SubscribeJobsRequest) returns (stream PrintJob);
|
||||||
|
rpc ReportJobStatus(ReportJobStatusRequest) returns (Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
service LibraryService {
|
||||||
|
rpc GetCategories(GetCategoriesRequest) returns (CategoryListResponse);
|
||||||
|
rpc GetFiles(GetLibraryFilesRequest) returns (LibraryFileListResponse);
|
||||||
|
rpc UploadFile(stream UploadLibraryFileRequest) returns (UploadLibraryFileResponse);
|
||||||
|
rpc DeleteFile(DeleteLibraryFileRequest) returns (Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
service PriceService {
|
||||||
|
rpc GetPriceList(GetPriceListRequest) returns (PriceListResponse);
|
||||||
|
rpc CreatePriceItem(CreatePriceItemRequest) returns (PriceItem);
|
||||||
|
rpc UpdatePriceItem(UpdatePriceItemRequest) returns (PriceItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
service PayService {
|
||||||
|
rpc CreateWxPayOrder(WxPayRequest) returns (WxPayResponse);
|
||||||
|
rpc QueryPayStatus(QueryPayStatusRequest) returns (PayStatusResponse);
|
||||||
|
rpc HandlePayCallback(PayCallbackRequest) returns (PayCallbackResponse);
|
||||||
|
rpc PayByBalance(PayByBalanceRequest) returns (StatusResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
service AdminService {
|
||||||
|
rpc Login(AdminLoginRequest) returns (AdminLoginResponse);
|
||||||
|
rpc GetStatistics(GetStatisticsRequest) returns (Statistics);
|
||||||
|
}
|
||||||
4785
cloudprint-backend/proto/generated/cloudprint.pb.go
Normal file
4785
cloudprint-backend/proto/generated/cloudprint.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
1856
cloudprint-backend/proto/generated/cloudprint_grpc.pb.go
Normal file
1856
cloudprint-backend/proto/generated/cloudprint_grpc.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
182
cloudprint-backend/scripts/db.sql
Normal file
182
cloudprint-backend/scripts/db.sql
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
-- 云印享 - 付费打印服务系统数据库初始化脚本
|
||||||
|
-- 数据库: cloudprint
|
||||||
|
|
||||||
|
CREATE DATABASE IF NOT EXISTS `cloudprint` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
USE `cloudprint`;
|
||||||
|
|
||||||
|
-- 用户表
|
||||||
|
CREATE TABLE IF NOT EXISTS `user` (
|
||||||
|
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
`openid` VARCHAR(64) UNIQUE NOT NULL COMMENT '微信OpenID',
|
||||||
|
`nickname` VARCHAR(128) DEFAULT '' COMMENT '昵称',
|
||||||
|
`avatar` VARCHAR(512) DEFAULT '' COMMENT '头像URL',
|
||||||
|
`balance` DECIMAL(10,2) DEFAULT 0.00 COMMENT '余额',
|
||||||
|
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
INDEX `idx_openid` (`openid`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
|
||||||
|
|
||||||
|
-- 管理员表
|
||||||
|
CREATE TABLE IF NOT EXISTS `admin` (
|
||||||
|
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
`username` VARCHAR(64) UNIQUE NOT NULL COMMENT '用户名',
|
||||||
|
`password_hash` VARCHAR(256) NOT NULL COMMENT '密码哈希',
|
||||||
|
`nickname` VARCHAR(128) DEFAULT '' COMMENT '昵称',
|
||||||
|
`role` TINYINT DEFAULT 1 COMMENT '1:普通管理员 2:超级管理员',
|
||||||
|
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
INDEX `idx_username` (`username`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='管理员表';
|
||||||
|
|
||||||
|
-- 打印机表
|
||||||
|
CREATE TABLE IF NOT EXISTS `printer` (
|
||||||
|
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
`name` VARCHAR(128) UNIQUE NOT NULL COMMENT '系统打印机名',
|
||||||
|
`display_name` VARCHAR(128) NOT NULL COMMENT '显示名称',
|
||||||
|
`api_key` VARCHAR(64) UNIQUE NOT NULL COMMENT '客户端认证Key',
|
||||||
|
`is_active` TINYINT DEFAULT 1 COMMENT '是否启用',
|
||||||
|
`status` TINYINT DEFAULT 0 COMMENT '0:空闲 1:打印中 2:离线',
|
||||||
|
`last_heartbeat` DATETIME DEFAULT NULL COMMENT '最后心跳时间',
|
||||||
|
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
INDEX `idx_name` (`name`),
|
||||||
|
INDEX `idx_api_key` (`api_key`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='打印机表';
|
||||||
|
|
||||||
|
-- 订单表
|
||||||
|
CREATE TABLE IF NOT EXISTS `order` (
|
||||||
|
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
`order_no` VARCHAR(32) UNIQUE NOT NULL COMMENT '订单号',
|
||||||
|
`user_id` INT NOT NULL COMMENT '用户ID',
|
||||||
|
`status` TINYINT DEFAULT 0 COMMENT '0:待支付 1:已支付 2:打印中 3:已完成 4:已取消',
|
||||||
|
`total_amount` DECIMAL(10,2) NOT NULL COMMENT '总金额',
|
||||||
|
`remark` VARCHAR(512) DEFAULT '' COMMENT '备注',
|
||||||
|
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`paid_at` DATETIME DEFAULT NULL COMMENT '支付时间',
|
||||||
|
INDEX `idx_order_no` (`order_no`),
|
||||||
|
INDEX `idx_user_id` (`user_id`),
|
||||||
|
INDEX `idx_status` (`status`),
|
||||||
|
INDEX `idx_created_at` (`created_at`),
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE RESTRICT
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
|
||||||
|
|
||||||
|
-- 订单明细表
|
||||||
|
CREATE TABLE IF NOT EXISTS `order_item` (
|
||||||
|
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
`order_id` INT NOT NULL COMMENT '订单ID',
|
||||||
|
`file_id` INT NOT NULL COMMENT '文件ID',
|
||||||
|
`printer_id` INT DEFAULT NULL COMMENT '打印机ID',
|
||||||
|
`copies` INT DEFAULT 1 COMMENT '份数',
|
||||||
|
`color` TINYINT DEFAULT 0 COMMENT '是否彩色 0:黑白 1:彩色',
|
||||||
|
`duplex` TINYINT DEFAULT 0 COMMENT '是否双面 0:单面 1:双面',
|
||||||
|
`page_range` VARCHAR(64) DEFAULT 'all' COMMENT '页面范围',
|
||||||
|
`price` DECIMAL(10,2) NOT NULL COMMENT '价格',
|
||||||
|
INDEX `idx_order_id` (`order_id`),
|
||||||
|
INDEX `idx_file_id` (`file_id`),
|
||||||
|
FOREIGN KEY (`order_id`) REFERENCES `order`(`id`) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (`file_id`) REFERENCES `file`(`id`) ON DELETE RESTRICT
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单明细表';
|
||||||
|
|
||||||
|
-- 文件表
|
||||||
|
CREATE TABLE IF NOT EXISTS `file` (
|
||||||
|
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
`file_name` VARCHAR(256) NOT NULL COMMENT '文件名',
|
||||||
|
`file_path` VARCHAR(512) NOT NULL COMMENT '文件路径',
|
||||||
|
`file_type` VARCHAR(32) NOT NULL COMMENT '文件类型',
|
||||||
|
`file_size` BIGINT NOT NULL COMMENT '文件大小(字节)',
|
||||||
|
`uploader_type` VARCHAR(16) NOT NULL COMMENT '上传者类型: user/admin/system',
|
||||||
|
`uploader_id` INT NOT NULL COMMENT '上传者ID',
|
||||||
|
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
INDEX `idx_uploader` (`uploader_type`, `uploader_id`),
|
||||||
|
INDEX `idx_file_type` (`file_type`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件表';
|
||||||
|
|
||||||
|
-- 文库分类表
|
||||||
|
CREATE TABLE IF NOT EXISTS `library_category` (
|
||||||
|
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
`name` VARCHAR(128) NOT NULL COMMENT '分类名称',
|
||||||
|
`icon` VARCHAR(128) DEFAULT '' COMMENT '图标',
|
||||||
|
`sort_order` INT DEFAULT 0 COMMENT '排序',
|
||||||
|
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文库分类表';
|
||||||
|
|
||||||
|
-- 文库文件表
|
||||||
|
CREATE TABLE IF NOT EXISTS `library_file` (
|
||||||
|
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
`category_id` INT NOT NULL COMMENT '分类ID',
|
||||||
|
`file_id` INT NOT NULL COMMENT '文件ID',
|
||||||
|
`title` VARCHAR(256) NOT NULL COMMENT '标题',
|
||||||
|
`description` VARCHAR(512) DEFAULT '' COMMENT '描述',
|
||||||
|
`page_count` INT DEFAULT 0 COMMENT '页数',
|
||||||
|
`download_count` INT DEFAULT 0 COMMENT '下载次数',
|
||||||
|
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
INDEX `idx_category_id` (`category_id`),
|
||||||
|
INDEX `idx_file_id` (`file_id`),
|
||||||
|
FOREIGN KEY (`category_id`) REFERENCES `library_category`(`id`) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (`file_id`) REFERENCES `file`(`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文库文件表';
|
||||||
|
|
||||||
|
-- 打印任务表
|
||||||
|
CREATE TABLE IF NOT EXISTS `print_job` (
|
||||||
|
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
`job_no` VARCHAR(32) UNIQUE NOT NULL COMMENT '任务号',
|
||||||
|
`printer_id` INT NOT NULL COMMENT '打印机ID',
|
||||||
|
`order_item_id` INT NOT NULL COMMENT '订单明细ID',
|
||||||
|
`status` VARCHAR(16) DEFAULT 'pending' COMMENT 'pending/printing/completed/failed/cancelled',
|
||||||
|
`progress` INT DEFAULT 0 COMMENT '进度(0-100)',
|
||||||
|
`error_msg` VARCHAR(512) DEFAULT '' COMMENT '错误信息',
|
||||||
|
`started_at` DATETIME DEFAULT NULL COMMENT '开始时间',
|
||||||
|
`completed_at` DATETIME DEFAULT NULL COMMENT '完成时间',
|
||||||
|
INDEX `idx_job_no` (`job_no`),
|
||||||
|
INDEX `idx_printer_id` (`printer_id`),
|
||||||
|
INDEX `idx_status` (`status`),
|
||||||
|
FOREIGN KEY (`printer_id`) REFERENCES `printer`(`id`) ON DELETE RESTRICT,
|
||||||
|
FOREIGN KEY (`order_item_id`) REFERENCES `order_item`(`id`) ON DELETE RESTRICT
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='打印任务表';
|
||||||
|
|
||||||
|
-- 价目表
|
||||||
|
CREATE TABLE IF NOT EXISTS `price_list` (
|
||||||
|
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
`name` VARCHAR(128) NOT NULL COMMENT '项目名称',
|
||||||
|
`type` VARCHAR(32) NOT NULL COMMENT '纸张类型: A4/A3/照片纸',
|
||||||
|
`price` DECIMAL(10,2) NOT NULL COMMENT '单价',
|
||||||
|
`unit` VARCHAR(16) NOT NULL COMMENT '单位: 页/张/份',
|
||||||
|
`color_enabled` TINYINT DEFAULT 1 COMMENT '是否支持彩色',
|
||||||
|
`is_active` TINYINT DEFAULT 1 COMMENT '是否启用',
|
||||||
|
`sort_order` INT DEFAULT 0 COMMENT '排序',
|
||||||
|
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
INDEX `idx_type` (`type`),
|
||||||
|
INDEX `idx_is_active` (`is_active`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='价目表';
|
||||||
|
|
||||||
|
-- 支付记录表
|
||||||
|
CREATE TABLE IF NOT EXISTS `payment` (
|
||||||
|
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
`order_no` VARCHAR(32) UNIQUE NOT NULL COMMENT '订单号',
|
||||||
|
`pay_type` VARCHAR(16) DEFAULT 'wechat' COMMENT '支付类型: wechat/balance',
|
||||||
|
`pay_status` TINYINT DEFAULT 0 COMMENT '0:待支付 1:支付中 2:成功 3:失败',
|
||||||
|
`transaction_id` VARCHAR(64) DEFAULT '' COMMENT '微信交易号',
|
||||||
|
`paid_at` DATETIME DEFAULT NULL COMMENT '支付时间',
|
||||||
|
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
INDEX `idx_order_no` (`order_no`),
|
||||||
|
INDEX `idx_pay_status` (`pay_status`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付记录表';
|
||||||
|
|
||||||
|
-- 初始化管理员账号 (密码: admin123)
|
||||||
|
-- 密码哈希使用 bcrypt 生成
|
||||||
|
INSERT INTO `admin` (`username`, `password_hash`, `nickname`, `role`) VALUES
|
||||||
|
('admin', '$2a$10$N9qo8uLOickgx2ZMRZoMy.MqrqJlmZ2j3.N9qo8uLOickgx2ZMRZoG', '系统管理员', 2);
|
||||||
|
|
||||||
|
-- 初始化默认价目表
|
||||||
|
INSERT INTO `price_list` (`name`, `type`, `price`, `unit`, `color_enabled`, `is_active`, `sort_order`) VALUES
|
||||||
|
('A4黑白打印', 'A4', 0.10, '页', 0, 1, 1),
|
||||||
|
('A4彩色打印', 'A4', 0.30, '页', 1, 1, 2),
|
||||||
|
('A3黑白打印', 'A3', 0.20, '页', 0, 1, 3),
|
||||||
|
('A3彩色打印', 'A3', 0.60, '页', 1, 1, 4),
|
||||||
|
('照片打印', '照片纸', 2.00, '张', 1, 1, 5);
|
||||||
|
|
||||||
|
-- 初始化默认文库分类
|
||||||
|
INSERT INTO `library_category` (`name`, `icon`, `sort_order`) VALUES
|
||||||
|
('学习资料', 'book', 1),
|
||||||
|
('考试真题', 'exam', 2),
|
||||||
|
('简历模板', 'resume', 3),
|
||||||
|
('合同协议', 'contract', 4),
|
||||||
|
('其他资料', 'other', 5);
|
||||||
Reference in New Issue
Block a user