feat: add project work orders
This commit is contained in:
@@ -0,0 +1,428 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.beifan.cn/trace-system/backend-go/models"
|
||||
"git.beifan.cn/trace-system/backend-go/services"
|
||||
)
|
||||
|
||||
// ProjectOrdersController 项目工单控制器
|
||||
type ProjectOrdersController struct {
|
||||
projectOrdersService services.ProjectOrdersService
|
||||
}
|
||||
|
||||
// NewProjectOrdersController 创建项目工单控制器实例
|
||||
func NewProjectOrdersController() *ProjectOrdersController {
|
||||
return &ProjectOrdersController{
|
||||
projectOrdersService: services.ProjectOrdersService{},
|
||||
}
|
||||
}
|
||||
|
||||
// Create 创建项目工单
|
||||
// @Summary 创建项目工单
|
||||
// @Description 创建一个新的项目工单并分配编号
|
||||
// @Tags 项目工单
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param data body models.CreateProjectOrderDTO true "工单数据"
|
||||
// @Success 200 {object} models.DataResponse
|
||||
// @Failure 400 {object} models.ErrorResponse
|
||||
// @Failure 401 {object} models.ErrorResponse
|
||||
// @Failure 500 {object} models.ErrorResponse
|
||||
// @Router /project-orders [post]
|
||||
func (c *ProjectOrdersController) Create(ctx *gin.Context) {
|
||||
userModel, ok := GetCurrentUser(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var dto models.CreateProjectOrderDTO
|
||||
if !BindJSON(ctx, &dto) {
|
||||
return
|
||||
}
|
||||
|
||||
order, err := c.projectOrdersService.Create(dto, userModel.ID)
|
||||
if err != nil {
|
||||
ErrorResponse(ctx, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(ctx, "项目工单创建成功", gin.H{
|
||||
"order": order,
|
||||
})
|
||||
}
|
||||
|
||||
// FindAll 获取项目工单列表
|
||||
// @Summary 获取项目工单列表
|
||||
// @Description 支持分页、搜索、按状态/服务类型/技术员筛选
|
||||
// @Tags 项目工单
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param page query int false "页码"
|
||||
// @Param limit query int false "每页数量"
|
||||
// @Param search query string false "搜索关键词"
|
||||
// @Param workOrderStatus query string false "工单状态"
|
||||
// @Param serviceType query string false "服务类型"
|
||||
// @Param technicianId query int false "技术员 ID"
|
||||
// @Param mine query bool false "仅查看自己负责的工单"
|
||||
// @Success 200 {object} models.PaginationResponse
|
||||
// @Failure 401 {object} models.ErrorResponse
|
||||
// @Failure 500 {object} models.ErrorResponse
|
||||
// @Router /project-orders [get]
|
||||
func (c *ProjectOrdersController) FindAll(ctx *gin.Context) {
|
||||
userModel, ok := GetCurrentUser(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1"))
|
||||
limit, _ := strconv.Atoi(ctx.DefaultQuery("limit", "20"))
|
||||
search := ctx.DefaultQuery("search", "")
|
||||
workOrderStatus := ctx.DefaultQuery("workOrderStatus", "")
|
||||
serviceType := ctx.DefaultQuery("serviceType", "")
|
||||
|
||||
var technicianID *uint
|
||||
if tidStr := ctx.Query("technicianId"); tidStr != "" {
|
||||
if tid, err := strconv.ParseUint(tidStr, 10, 32); err == nil {
|
||||
t := uint(tid)
|
||||
technicianID = &t
|
||||
}
|
||||
}
|
||||
// 非管理员默认只看自己的工单(除非显式指定 technicianId)
|
||||
if userModel.Role != "admin" && technicianID == nil {
|
||||
technicianID = &userModel.ID
|
||||
}
|
||||
// mine=true 强制只看自己的
|
||||
if ctx.Query("mine") == "true" {
|
||||
technicianID = &userModel.ID
|
||||
}
|
||||
|
||||
orders, total, totalPages, err := c.projectOrdersService.FindAll(page, limit, search, workOrderStatus, serviceType, technicianID)
|
||||
if err != nil {
|
||||
ErrorResponse(ctx, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(ctx, "获取项目工单列表成功", gin.H{
|
||||
"data": orders,
|
||||
"pagination": gin.H{
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"total": total,
|
||||
"totalPages": totalPages,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// FindOne 获取单个项目工单详情
|
||||
// @Summary 获取项目工单详情
|
||||
// @Tags 项目工单
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param serialNumber path string true "工单号"
|
||||
// @Success 200 {object} models.DataResponse
|
||||
// @Failure 401 {object} models.ErrorResponse
|
||||
// @Failure 404 {object} models.ErrorResponse
|
||||
// @Router /project-orders/{serialNumber} [get]
|
||||
func (c *ProjectOrdersController) FindOne(ctx *gin.Context) {
|
||||
serialNumber := ctx.Param("serialNumber")
|
||||
|
||||
order, err := c.projectOrdersService.FindOne(serialNumber)
|
||||
if err != nil {
|
||||
ErrorResponse(ctx, http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(ctx, "查询成功", gin.H{
|
||||
"order": order,
|
||||
})
|
||||
}
|
||||
|
||||
// Update 更新项目工单信息
|
||||
// @Summary 更新项目工单
|
||||
// @Tags 项目工单
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param serialNumber path string true "工单号"
|
||||
// @Param data body models.UpdateProjectOrderDTO true "更新数据"
|
||||
// @Success 200 {object} models.DataResponse
|
||||
// @Failure 400 {object} models.ErrorResponse
|
||||
// @Failure 401 {object} models.ErrorResponse
|
||||
// @Failure 403 {object} models.ErrorResponse
|
||||
// @Router /project-orders/{serialNumber} [patch]
|
||||
func (c *ProjectOrdersController) Update(ctx *gin.Context) {
|
||||
userModel, ok := GetCurrentUser(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
serialNumber := ctx.Param("serialNumber")
|
||||
var dto models.UpdateProjectOrderDTO
|
||||
if !BindJSON(ctx, &dto) {
|
||||
return
|
||||
}
|
||||
|
||||
order, err := c.projectOrdersService.Update(serialNumber, dto, userModel)
|
||||
if err != nil {
|
||||
ErrorResponse(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(ctx, "工单更新成功", gin.H{
|
||||
"order": order,
|
||||
})
|
||||
}
|
||||
|
||||
// SubmitCompletion 技术员提交完成资料
|
||||
// @Summary 提交完成确认
|
||||
// @Description 技术员填写处理结果后提交,工单进入"待完成确认"状态
|
||||
// @Tags 项目工单
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param serialNumber path string true "工单号"
|
||||
// @Param data body models.SubmitProjectCompletionDTO true "处理结果"
|
||||
// @Success 200 {object} models.DataResponse
|
||||
// @Failure 400 {object} models.ErrorResponse
|
||||
// @Failure 401 {object} models.ErrorResponse
|
||||
// @Router /project-orders/{serialNumber}/submit [post]
|
||||
func (c *ProjectOrdersController) SubmitCompletion(ctx *gin.Context) {
|
||||
userModel, ok := GetCurrentUser(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
serialNumber := ctx.Param("serialNumber")
|
||||
var dto models.SubmitProjectCompletionDTO
|
||||
if !BindJSON(ctx, &dto) {
|
||||
return
|
||||
}
|
||||
|
||||
order, err := c.projectOrdersService.SubmitCompletion(serialNumber, dto, userModel)
|
||||
if err != nil {
|
||||
ErrorResponse(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(ctx, "已提交完成确认", gin.H{
|
||||
"order": order,
|
||||
})
|
||||
}
|
||||
|
||||
// GenerateQRCode 生成项目工单二维码
|
||||
// @Summary 生成项目工单二维码
|
||||
// @Tags 项目工单
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param serialNumber path string true "工单号"
|
||||
// @Param data body models.QRCodeDTO false "二维码参数"
|
||||
// @Success 200 {object} models.QRCodeResponse
|
||||
// @Failure 400 {object} models.ErrorResponse
|
||||
// @Failure 401 {object} models.ErrorResponse
|
||||
// @Router /project-orders/{serialNumber}/qrcode [post]
|
||||
func (c *ProjectOrdersController) GenerateQRCode(ctx *gin.Context) {
|
||||
serialNumber := ctx.Param("serialNumber")
|
||||
|
||||
var qrCodeData models.QRCodeDTO
|
||||
if !BindJSON(ctx, &qrCodeData) {
|
||||
return
|
||||
}
|
||||
|
||||
protocol := "http"
|
||||
if ctx.Request.TLS != nil {
|
||||
protocol = "https"
|
||||
}
|
||||
|
||||
qrCodeBase64, queryUrl, err := c.projectOrdersService.GenerateQRCode(
|
||||
serialNumber,
|
||||
qrCodeData.BaseUrl,
|
||||
ctx.Request.Host,
|
||||
protocol,
|
||||
)
|
||||
if err != nil {
|
||||
ErrorResponse(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(ctx, "二维码生成成功", gin.H{
|
||||
"qrCodeData": qrCodeBase64,
|
||||
"queryUrl": queryUrl,
|
||||
})
|
||||
}
|
||||
|
||||
// Reassign 重新分配技术员(仅管理员)
|
||||
// @Summary 重新分配技术员
|
||||
// @Tags 项目工单
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param serialNumber path string true "工单号"
|
||||
// @Param data body models.ReassignProjectOrderDTO true "新技术员 ID"
|
||||
// @Success 200 {object} models.DataResponse
|
||||
// @Failure 400 {object} models.ErrorResponse
|
||||
// @Failure 401 {object} models.ErrorResponse
|
||||
// @Router /project-orders/{serialNumber}/reassign [post]
|
||||
func (c *ProjectOrdersController) Reassign(ctx *gin.Context) {
|
||||
serialNumber := ctx.Param("serialNumber")
|
||||
var dto models.ReassignProjectOrderDTO
|
||||
if !BindJSON(ctx, &dto) {
|
||||
return
|
||||
}
|
||||
|
||||
order, err := c.projectOrdersService.Reassign(serialNumber, dto.TechnicianID)
|
||||
if err != nil {
|
||||
ErrorResponse(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(ctx, "重新分配成功", gin.H{
|
||||
"order": order,
|
||||
})
|
||||
}
|
||||
|
||||
// ForceClose 强制关闭工单(仅管理员)
|
||||
// @Summary 强制关闭工单
|
||||
// @Tags 项目工单
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param serialNumber path string true "工单号"
|
||||
// @Success 200 {object} models.DataResponse
|
||||
// @Failure 400 {object} models.ErrorResponse
|
||||
// @Failure 401 {object} models.ErrorResponse
|
||||
// @Router /project-orders/{serialNumber}/force-close [post]
|
||||
func (c *ProjectOrdersController) ForceClose(ctx *gin.Context) {
|
||||
serialNumber := ctx.Param("serialNumber")
|
||||
|
||||
order, err := c.projectOrdersService.ForceClose(serialNumber)
|
||||
if err != nil {
|
||||
ErrorResponse(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(ctx, "工单已强制关闭", gin.H{
|
||||
"order": order,
|
||||
})
|
||||
}
|
||||
|
||||
// Delete 删除项目工单(仅管理员)
|
||||
// @Summary 删除项目工单
|
||||
// @Tags 项目工单
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param serialNumber path string true "工单号"
|
||||
// @Success 200 {object} models.BaseResponse
|
||||
// @Failure 400 {object} models.ErrorResponse
|
||||
// @Failure 401 {object} models.ErrorResponse
|
||||
// @Router /project-orders/{serialNumber} [delete]
|
||||
func (c *ProjectOrdersController) Delete(ctx *gin.Context) {
|
||||
serialNumber := ctx.Param("serialNumber")
|
||||
|
||||
if err := c.projectOrdersService.Delete(serialNumber); err != nil {
|
||||
ErrorResponse(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(ctx, "项目工单删除成功")
|
||||
}
|
||||
|
||||
// PublicQuery 公开查询项目工单(无需登录,脱敏)
|
||||
// @Summary 公开查询项目工单
|
||||
// @Tags 项目工单查询
|
||||
// @Produce json
|
||||
// @Param serialNumber path string true "工单号"
|
||||
// @Success 200 {object} models.DataResponse
|
||||
// @Failure 404 {object} models.ErrorResponse
|
||||
// @Router /project-orders/{serialNumber}/query [get]
|
||||
func (c *ProjectOrdersController) PublicQuery(ctx *gin.Context) {
|
||||
serialNumber := ctx.Param("serialNumber")
|
||||
|
||||
view, err := c.projectOrdersService.PublicQuery(serialNumber)
|
||||
if err != nil {
|
||||
ErrorResponse(ctx, http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(ctx, "查询成功", gin.H{
|
||||
"order": view,
|
||||
})
|
||||
}
|
||||
|
||||
// EngineerComplete 项目完成提交
|
||||
// @Summary 工程师提交完成
|
||||
// @Description 工程师上传现场图片后签字提交,工单进入已完成状态
|
||||
// @Tags 项目工单查询
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param serialNumber path string true "工单号"
|
||||
// @Param data body models.ProjectEngineerCompleteDTO true "确认数据"
|
||||
// @Success 200 {object} models.DataResponse
|
||||
// @Failure 400 {object} models.ErrorResponse
|
||||
// @Failure 401 {object} models.ErrorResponse
|
||||
// @Failure 429 {object} models.ErrorResponse
|
||||
// @Router /project-orders/{serialNumber}/complete [post]
|
||||
func (c *ProjectOrdersController) EngineerComplete(ctx *gin.Context) {
|
||||
serialNumber := ctx.Param("serialNumber")
|
||||
|
||||
var dto models.ProjectEngineerCompleteDTO
|
||||
if !BindJSON(ctx, &dto) {
|
||||
return
|
||||
}
|
||||
|
||||
view, err := c.projectOrdersService.EngineerComplete(serialNumber, dto)
|
||||
if err != nil {
|
||||
// 频率限制单独返回 429
|
||||
if err.Error() == "操作过于频繁,请稍后再试" {
|
||||
ErrorResponse(ctx, http.StatusTooManyRequests, err.Error())
|
||||
return
|
||||
}
|
||||
ErrorResponse(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(ctx, "提交成功", gin.H{
|
||||
"order": view,
|
||||
})
|
||||
}
|
||||
|
||||
// UploadSiteImages 上传完成确认现场图片
|
||||
// @Summary 上传项目现场图片
|
||||
// @Tags 项目工单查询
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param serialNumber path string true "工单号"
|
||||
// @Param files formData file true "现场图片"
|
||||
// @Success 200 {object} models.DataResponse
|
||||
// @Failure 400 {object} models.ErrorResponse
|
||||
// @Failure 404 {object} models.ErrorResponse
|
||||
// @Router /project-orders/{serialNumber}/site-images [post]
|
||||
func (c *ProjectOrdersController) UploadSiteImages(ctx *gin.Context) {
|
||||
serialNumber := ctx.Param("serialNumber")
|
||||
|
||||
form, err := ctx.MultipartForm()
|
||||
if err != nil {
|
||||
ErrorResponse(ctx, http.StatusBadRequest, "请选择要上传的现场图片")
|
||||
return
|
||||
}
|
||||
|
||||
files := form.File["files"]
|
||||
if len(files) == 0 {
|
||||
files = form.File["file"]
|
||||
}
|
||||
|
||||
images, err := c.projectOrdersService.UploadSiteImages(serialNumber, files)
|
||||
if err != nil {
|
||||
ErrorResponse(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(ctx, "现场图片上传成功", gin.H{
|
||||
"siteImages": images,
|
||||
})
|
||||
}
|
||||
@@ -115,6 +115,7 @@ func AutoMigrate() {
|
||||
&models.Serial{},
|
||||
&models.EmployeeSerial{},
|
||||
&models.AftersalesOrder{},
|
||||
&models.ProjectOrder{},
|
||||
); err != nil {
|
||||
logger.Fatal("数据库迁移失败", logger.Err(err))
|
||||
}
|
||||
|
||||
@@ -345,3 +345,87 @@ type AftersalesPublicView struct {
|
||||
ResponsibleSignature string `json:"responsibleSignature,omitempty"`
|
||||
SiteImages []string `json:"siteImages,omitempty"`
|
||||
}
|
||||
|
||||
// ProjectOrder 项目工单模型
|
||||
type ProjectOrder struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
SerialNumber string `gorm:"uniqueIndex;size:64" json:"serialNumber"`
|
||||
CompanyName string `gorm:"index;size:255" json:"companyName"`
|
||||
CompanyAddress string `gorm:"size:500" json:"companyAddress"`
|
||||
ContactName string `gorm:"size:100" json:"contactName"`
|
||||
ContactPhone string `gorm:"size:32" json:"contactPhone"`
|
||||
ProjectType string `gorm:"size:32" json:"projectType"`
|
||||
SiteDescription string `gorm:"type:text" json:"siteDescription"`
|
||||
CompletionNote string `gorm:"type:text" json:"completionNote"`
|
||||
WorkOrderStatus string `gorm:"size:32;default:'created'" json:"workOrderStatus"`
|
||||
|
||||
TechnicianID *uint `json:"technicianId"`
|
||||
CreatedBy *uint `json:"createdBy"`
|
||||
ScannedAt *time.Time `json:"scannedAt"`
|
||||
CompletedAt *time.Time `json:"completedAt"`
|
||||
EngineerSignature string `gorm:"type:text" json:"engineerSignature,omitempty"`
|
||||
SiteImagesJSON string `gorm:"type:text;column:site_images" json:"-"`
|
||||
SiteImages []string `gorm:"-" json:"siteImages,omitempty"`
|
||||
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
Technician *User `gorm:"foreignKey:TechnicianID" json:"technician,omitempty"`
|
||||
Creator *User `gorm:"foreignKey:CreatedBy" json:"creator,omitempty"`
|
||||
}
|
||||
|
||||
// CreateProjectOrderDTO 创建项目工单请求数据
|
||||
type CreateProjectOrderDTO struct {
|
||||
CompanyName string `json:"companyName" validate:"required"`
|
||||
CompanyAddress string `json:"companyAddress" validate:"required"`
|
||||
ContactName string `json:"contactName" validate:"required"`
|
||||
ContactPhone string `json:"contactPhone" validate:"required,len=11"`
|
||||
ProjectType string `json:"projectType" validate:"required,oneof=survey implementation maintenance other"`
|
||||
SiteDescription string `json:"siteDescription" validate:"required"`
|
||||
TechnicianID *uint `json:"technicianId,omitempty"`
|
||||
}
|
||||
|
||||
// UpdateProjectOrderDTO 更新项目工单请求数据
|
||||
type UpdateProjectOrderDTO struct {
|
||||
CompanyAddress string `json:"companyAddress,omitempty"`
|
||||
ContactName string `json:"contactName,omitempty"`
|
||||
ContactPhone string `json:"contactPhone,omitempty" validate:"omitempty,len=11"`
|
||||
ProjectType string `json:"projectType,omitempty" validate:"omitempty,oneof=survey implementation maintenance other"`
|
||||
SiteDescription string `json:"siteDescription,omitempty"`
|
||||
CompletionNote string `json:"completionNote,omitempty"`
|
||||
TechnicianID *uint `json:"technicianId,omitempty"`
|
||||
}
|
||||
|
||||
// SubmitProjectCompletionDTO 提交完成资料请求
|
||||
type SubmitProjectCompletionDTO struct {
|
||||
CompletionNote string `json:"completionNote" validate:"required"`
|
||||
}
|
||||
|
||||
// ProjectEngineerCompleteDTO 工程师完成确认请求
|
||||
type ProjectEngineerCompleteDTO struct {
|
||||
EngineerSignature string `json:"engineerSignature" validate:"required"`
|
||||
CompletionNote string `json:"completionNote,omitempty"`
|
||||
}
|
||||
|
||||
// ReassignProjectOrderDTO 重新分配工程师请求
|
||||
type ReassignProjectOrderDTO struct {
|
||||
TechnicianID uint `json:"technicianId" validate:"required"`
|
||||
}
|
||||
|
||||
// ProjectOrderPublicView 公开查询返回视图
|
||||
type ProjectOrderPublicView struct {
|
||||
SerialNumber string `json:"serialNumber"`
|
||||
CompanyName string `json:"companyName"`
|
||||
CompanyAddress string `json:"companyAddress"`
|
||||
ContactName string `json:"contactName"`
|
||||
ProjectType string `json:"projectType"`
|
||||
SiteDescription string `json:"siteDescription"`
|
||||
CompletionNote string `json:"completionNote"`
|
||||
WorkOrderStatus string `json:"workOrderStatus"`
|
||||
TechnicianName string `json:"technicianName"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
CompletedAt *time.Time `json:"completedAt"`
|
||||
EngineerSignature string `json:"engineerSignature,omitempty"`
|
||||
SiteImages []string `json:"siteImages,omitempty"`
|
||||
}
|
||||
|
||||
@@ -112,4 +112,27 @@ func SetupAPIRoutes(r *gin.RouterGroup) {
|
||||
aftersalesRoutes.POST("/:serialNumber/force-close", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), aftersalesController.ForceClose)
|
||||
aftersalesRoutes.DELETE("/:serialNumber", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), aftersalesController.Delete)
|
||||
}
|
||||
|
||||
// 项目工单路由
|
||||
projectOrdersController := controllers.NewProjectOrdersController()
|
||||
projectOrdersRoutes := r.Group("/project-orders")
|
||||
{
|
||||
// 公开(无需登录)
|
||||
projectOrdersRoutes.GET("/:serialNumber/query", projectOrdersController.PublicQuery)
|
||||
projectOrdersRoutes.POST("/:serialNumber/site-images", projectOrdersController.UploadSiteImages)
|
||||
projectOrdersRoutes.POST("/:serialNumber/complete", projectOrdersController.EngineerComplete)
|
||||
|
||||
// 技术员 + 管理员
|
||||
projectOrdersRoutes.POST("", middleware.JWTAuthMiddleware(), middleware.TechnicianMiddleware(), projectOrdersController.Create)
|
||||
projectOrdersRoutes.GET("", middleware.JWTAuthMiddleware(), middleware.TechnicianMiddleware(), projectOrdersController.FindAll)
|
||||
projectOrdersRoutes.GET("/:serialNumber", middleware.JWTAuthMiddleware(), middleware.TechnicianMiddleware(), projectOrdersController.FindOne)
|
||||
projectOrdersRoutes.PATCH("/:serialNumber", middleware.JWTAuthMiddleware(), middleware.TechnicianMiddleware(), projectOrdersController.Update)
|
||||
projectOrdersRoutes.POST("/:serialNumber/qrcode", middleware.JWTAuthMiddleware(), middleware.TechnicianMiddleware(), projectOrdersController.GenerateQRCode)
|
||||
projectOrdersRoutes.POST("/:serialNumber/submit", middleware.JWTAuthMiddleware(), middleware.TechnicianMiddleware(), projectOrdersController.SubmitCompletion)
|
||||
|
||||
// 仅管理员
|
||||
projectOrdersRoutes.POST("/:serialNumber/reassign", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), projectOrdersController.Reassign)
|
||||
projectOrdersRoutes.POST("/:serialNumber/force-close", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), projectOrdersController.ForceClose)
|
||||
projectOrdersRoutes.DELETE("/:serialNumber", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), projectOrdersController.Delete)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,509 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
qr "github.com/yeqown/go-qrcode/v2"
|
||||
"github.com/yeqown/go-qrcode/writer/standard"
|
||||
|
||||
"git.beifan.cn/trace-system/backend-go/config"
|
||||
"git.beifan.cn/trace-system/backend-go/database"
|
||||
"git.beifan.cn/trace-system/backend-go/models"
|
||||
)
|
||||
|
||||
// ProjectOrdersService 项目工单服务
|
||||
type ProjectOrdersService struct{}
|
||||
|
||||
// 工单状态常量
|
||||
const (
|
||||
ProjectOrderStatusCreated = "created"
|
||||
ProjectOrderStatusPendingCompletion = "pending_completion"
|
||||
ProjectOrderStatusClosed = "closed"
|
||||
|
||||
projectOrderSerialPrefix = "zjbf-xm-"
|
||||
)
|
||||
|
||||
func normalizeProjectOrderSerial(sn string) string {
|
||||
return strings.ToLower(strings.TrimSpace(sn))
|
||||
}
|
||||
|
||||
func hydrateProjectOrder(order *models.ProjectOrder) {
|
||||
if order == nil {
|
||||
return
|
||||
}
|
||||
order.SiteImages = siteImageAccessURLs(parseSiteImages(order.SiteImagesJSON))
|
||||
}
|
||||
|
||||
// generateUniqueSerial 生成唯一的项目工单序列号
|
||||
// 格式:zjbf-xm-YYMMDDNN,YY=年份后两位,MM=月份,DD=日期,NN=当天第几单(至少 2 位,溢出自然加宽)
|
||||
func (s *ProjectOrdersService) generateUniqueSerial() (string, error) {
|
||||
now := time.Now()
|
||||
yy := now.Year() % 100
|
||||
mm := int(now.Month())
|
||||
dd := now.Day()
|
||||
|
||||
dayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
nextDay := dayStart.AddDate(0, 0, 1)
|
||||
|
||||
// 统计当天已创建工单数(含软删除,避免编号回收)
|
||||
var count int64
|
||||
if err := database.DB.Unscoped().Model(&models.ProjectOrder{}).
|
||||
Where("created_at >= ? AND created_at < ?", dayStart, nextDay).
|
||||
Count(&count).Error; err != nil {
|
||||
return "", fmt.Errorf("统计当日工单数失败: %w", err)
|
||||
}
|
||||
|
||||
seq := int(count) + 1
|
||||
for attempt := 0; attempt < 100; attempt++ {
|
||||
candidate := fmt.Sprintf("%s%02d%02d%02d%02d", projectOrderSerialPrefix, yy, mm, dd, seq)
|
||||
|
||||
var existing models.ProjectOrder
|
||||
result := database.DB.Unscoped().Where("serial_number = ?", candidate).First(&existing)
|
||||
if result.Error != nil {
|
||||
return candidate, nil
|
||||
}
|
||||
seq++
|
||||
}
|
||||
return "", errors.New("生成唯一序列号失败,请重试")
|
||||
}
|
||||
|
||||
// Create 创建项目工单
|
||||
func (s *ProjectOrdersService) Create(dto models.CreateProjectOrderDTO, userId uint) (*models.ProjectOrder, error) {
|
||||
serialNumber, err := s.generateUniqueSerial()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
technicianID := dto.TechnicianID
|
||||
if technicianID == nil {
|
||||
uid := userId
|
||||
technicianID = &uid
|
||||
}
|
||||
|
||||
order := models.ProjectOrder{
|
||||
SerialNumber: serialNumber,
|
||||
CompanyName: dto.CompanyName,
|
||||
CompanyAddress: dto.CompanyAddress,
|
||||
ContactName: dto.ContactName,
|
||||
ContactPhone: dto.ContactPhone,
|
||||
ProjectType: dto.ProjectType,
|
||||
SiteDescription: dto.SiteDescription,
|
||||
TechnicianID: technicianID,
|
||||
CreatedBy: &userId,
|
||||
WorkOrderStatus: ProjectOrderStatusCreated,
|
||||
}
|
||||
|
||||
if err := database.DB.Create(&order).Error; err != nil {
|
||||
return nil, fmt.Errorf("创建项目工单失败: %w", err)
|
||||
}
|
||||
|
||||
_ = database.DB.Preload("Technician").Preload("Creator").Where("serial_number = ?", order.SerialNumber).First(&order)
|
||||
return &order, nil
|
||||
}
|
||||
|
||||
// FindAll 获取项目工单列表
|
||||
func (s *ProjectOrdersService) FindAll(
|
||||
page int,
|
||||
limit int,
|
||||
search string,
|
||||
workOrderStatus string,
|
||||
serviceType string,
|
||||
technicianID *uint,
|
||||
) ([]models.ProjectOrder, int, int, error) {
|
||||
var orders []models.ProjectOrder
|
||||
var total int64
|
||||
|
||||
offset := (page - 1) * limit
|
||||
db := database.DB.Model(&models.ProjectOrder{}).Preload("Technician").Preload("Creator")
|
||||
|
||||
if search != "" {
|
||||
pattern := "%" + search + "%"
|
||||
db = db.Where("serial_number LIKE ? OR company_name LIKE ? OR contact_name LIKE ?", pattern, pattern, pattern)
|
||||
}
|
||||
if workOrderStatus != "" {
|
||||
db = db.Where("work_order_status = ?", workOrderStatus)
|
||||
}
|
||||
if serviceType != "" {
|
||||
db = db.Where("project_type = ?", serviceType)
|
||||
}
|
||||
if technicianID != nil {
|
||||
db = db.Where("technician_id = ?", *technicianID)
|
||||
}
|
||||
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, 0, fmt.Errorf("查询项目工单总数失败: %w", err)
|
||||
}
|
||||
|
||||
if err := db.Order("created_at DESC").Offset(offset).Limit(limit).Find(&orders).Error; err != nil {
|
||||
return nil, 0, 0, fmt.Errorf("查询项目工单列表失败: %w", err)
|
||||
}
|
||||
|
||||
totalPages := (int(total) + limit - 1) / limit
|
||||
return orders, int(total), totalPages, nil
|
||||
}
|
||||
|
||||
// FindOne 获取单个项目工单
|
||||
func (s *ProjectOrdersService) FindOne(serialNumber string) (*models.ProjectOrder, error) {
|
||||
var order models.ProjectOrder
|
||||
result := database.DB.Preload("Technician").Preload("Creator").
|
||||
Where("serial_number = ?", normalizeProjectOrderSerial(serialNumber)).First(&order)
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("查询项目工单失败: %w", errors.New("序列号不存在"))
|
||||
}
|
||||
hydrateProjectOrder(&order)
|
||||
return &order, nil
|
||||
}
|
||||
|
||||
// Update 更新项目工单基础信息
|
||||
func (s *ProjectOrdersService) Update(
|
||||
serialNumber string,
|
||||
dto models.UpdateProjectOrderDTO,
|
||||
currentUser models.User,
|
||||
) (*models.ProjectOrder, error) {
|
||||
var order models.ProjectOrder
|
||||
result := database.DB.Where("serial_number = ?", normalizeProjectOrderSerial(serialNumber)).First(&order)
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("查询项目工单失败: %w", errors.New("序列号不存在"))
|
||||
}
|
||||
|
||||
if order.WorkOrderStatus == ProjectOrderStatusClosed {
|
||||
return nil, errors.New("工单已关闭,不可修改")
|
||||
}
|
||||
|
||||
if currentUser.Role != "admin" {
|
||||
if order.TechnicianID == nil || *order.TechnicianID != currentUser.ID {
|
||||
return nil, errors.New("无权修改此工单")
|
||||
}
|
||||
}
|
||||
|
||||
if dto.CompanyAddress != "" {
|
||||
order.CompanyAddress = dto.CompanyAddress
|
||||
}
|
||||
if dto.ContactName != "" {
|
||||
order.ContactName = dto.ContactName
|
||||
}
|
||||
if dto.ContactPhone != "" {
|
||||
order.ContactPhone = dto.ContactPhone
|
||||
}
|
||||
if dto.ProjectType != "" {
|
||||
order.ProjectType = dto.ProjectType
|
||||
}
|
||||
if dto.SiteDescription != "" {
|
||||
order.SiteDescription = dto.SiteDescription
|
||||
}
|
||||
if dto.CompletionNote != "" {
|
||||
order.CompletionNote = dto.CompletionNote
|
||||
}
|
||||
if dto.TechnicianID != nil && currentUser.Role == "admin" {
|
||||
order.TechnicianID = dto.TechnicianID
|
||||
}
|
||||
|
||||
if err := database.DB.Save(&order).Error; err != nil {
|
||||
return nil, fmt.Errorf("更新项目工单失败: %w", err)
|
||||
}
|
||||
|
||||
_ = database.DB.Preload("Technician").Preload("Creator").Where("serial_number = ?", order.SerialNumber).First(&order)
|
||||
return &order, nil
|
||||
}
|
||||
|
||||
// SubmitCompletion 技术员提交完成资料,进入待完成确认状态
|
||||
func (s *ProjectOrdersService) SubmitCompletion(
|
||||
serialNumber string,
|
||||
dto models.SubmitProjectCompletionDTO,
|
||||
currentUser models.User,
|
||||
) (*models.ProjectOrder, error) {
|
||||
var order models.ProjectOrder
|
||||
result := database.DB.Where("serial_number = ?", normalizeProjectOrderSerial(serialNumber)).First(&order)
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("查询项目工单失败: %w", errors.New("序列号不存在"))
|
||||
}
|
||||
|
||||
if currentUser.Role != "admin" {
|
||||
if order.TechnicianID == nil || *order.TechnicianID != currentUser.ID {
|
||||
return nil, errors.New("无权操作此工单")
|
||||
}
|
||||
}
|
||||
|
||||
if order.WorkOrderStatus != ProjectOrderStatusCreated {
|
||||
return nil, errors.New("当前工单状态不可提交完成资料")
|
||||
}
|
||||
|
||||
order.CompletionNote = dto.CompletionNote
|
||||
order.WorkOrderStatus = ProjectOrderStatusPendingCompletion
|
||||
|
||||
if err := database.DB.Save(&order).Error; err != nil {
|
||||
return nil, fmt.Errorf("提交完成资料失败: %w", err)
|
||||
}
|
||||
|
||||
_ = database.DB.Preload("Technician").Preload("Creator").Where("serial_number = ?", order.SerialNumber).First(&order)
|
||||
return &order, nil
|
||||
}
|
||||
|
||||
// PublicQuery 公开查询(脱敏视图)
|
||||
func (s *ProjectOrdersService) PublicQuery(serialNumber string) (*models.ProjectOrderPublicView, error) {
|
||||
var order models.ProjectOrder
|
||||
result := database.DB.Preload("Technician").
|
||||
Where("serial_number = ?", normalizeProjectOrderSerial(serialNumber)).First(&order)
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("查询项目工单失败: %w", errors.New("序列号不存在"))
|
||||
}
|
||||
|
||||
// 首次扫码记录时间
|
||||
if order.ScannedAt == nil {
|
||||
now := time.Now()
|
||||
order.ScannedAt = &now
|
||||
_ = database.DB.Model(&order).Update("scanned_at", now).Error
|
||||
}
|
||||
|
||||
view := &models.ProjectOrderPublicView{
|
||||
SerialNumber: order.SerialNumber,
|
||||
CompanyName: order.CompanyName,
|
||||
CompanyAddress: order.CompanyAddress,
|
||||
ContactName: order.ContactName,
|
||||
ProjectType: order.ProjectType,
|
||||
SiteDescription: order.SiteDescription,
|
||||
CompletionNote: order.CompletionNote,
|
||||
WorkOrderStatus: order.WorkOrderStatus,
|
||||
CreatedAt: order.CreatedAt,
|
||||
CompletedAt: order.CompletedAt,
|
||||
EngineerSignature: order.EngineerSignature,
|
||||
SiteImages: siteImageAccessURLs(parseSiteImages(order.SiteImagesJSON)),
|
||||
}
|
||||
if order.Technician != nil {
|
||||
view.TechnicianName = order.Technician.Name
|
||||
}
|
||||
return view, nil
|
||||
}
|
||||
|
||||
// UploadSiteImages 上传完成确认现场图片到 OSS 并保存 object key。
|
||||
func (s *ProjectOrdersService) UploadSiteImages(serialNumber string, files []*multipart.FileHeader) ([]string, error) {
|
||||
normalized := normalizeProjectOrderSerial(serialNumber)
|
||||
if len(files) == 0 {
|
||||
return nil, errors.New("请选择要上传的现场图片")
|
||||
}
|
||||
|
||||
cfg := config.GetAppConfig().OSS
|
||||
maxFiles := cfg.MaxFiles
|
||||
if maxFiles <= 0 {
|
||||
maxFiles = 6
|
||||
}
|
||||
maxFileSize := int64(cfg.MaxFileSizeMB)
|
||||
if maxFileSize <= 0 {
|
||||
maxFileSize = 5
|
||||
}
|
||||
maxFileSize *= 1024 * 1024
|
||||
|
||||
var order models.ProjectOrder
|
||||
if err := database.DB.Where("serial_number = ?", normalized).First(&order).Error; err != nil {
|
||||
return nil, fmt.Errorf("查询项目工单失败: %w", errors.New("序列号不存在"))
|
||||
}
|
||||
if order.WorkOrderStatus != ProjectOrderStatusCreated && order.WorkOrderStatus != ProjectOrderStatusPendingCompletion {
|
||||
return nil, errors.New("当前工单状态不可上传现场图片")
|
||||
}
|
||||
|
||||
existing := parseSiteImages(order.SiteImagesJSON)
|
||||
if len(existing)+len(files) > maxFiles {
|
||||
return nil, fmt.Errorf("现场图片最多上传 %d 张", maxFiles)
|
||||
}
|
||||
|
||||
ossService := NewOSSService()
|
||||
uploadedKeys := make([]string, 0, len(files))
|
||||
for _, fh := range files {
|
||||
if fh.Size <= 0 {
|
||||
return nil, fmt.Errorf("图片 %s 内容为空", fh.Filename)
|
||||
}
|
||||
if fh.Size > maxFileSize {
|
||||
return nil, fmt.Errorf("图片 %s 超过 %dMB 限制", fh.Filename, maxFileSize/(1024*1024))
|
||||
}
|
||||
|
||||
contentType := strings.ToLower(strings.TrimSpace(fh.Header.Get("Content-Type")))
|
||||
if !allowedSiteImageContentTypes[contentType] {
|
||||
return nil, fmt.Errorf("图片 %s 格式不支持", fh.Filename)
|
||||
}
|
||||
|
||||
file, err := fh.Open()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取图片 %s 失败: %w", fh.Filename, err)
|
||||
}
|
||||
|
||||
objectKey := buildSiteImageObjectKey(cfg.Prefix, normalized, fh.Filename)
|
||||
err = ossService.UploadObject(objectKey, file, contentType)
|
||||
_ = file.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uploadedKeys = append(uploadedKeys, objectKey)
|
||||
}
|
||||
|
||||
merged := append(existing, uploadedKeys...)
|
||||
order.SiteImagesJSON = encodeSiteImages(merged)
|
||||
if err := database.DB.Save(&order).Error; err != nil {
|
||||
return nil, fmt.Errorf("保存现场图片失败: %w", err)
|
||||
}
|
||||
|
||||
return siteImageAccessURLs(merged), nil
|
||||
}
|
||||
|
||||
// EngineerComplete 项目完成提交
|
||||
func (s *ProjectOrdersService) EngineerComplete(serialNumber string, dto models.ProjectEngineerCompleteDTO) (*models.ProjectOrderPublicView, error) {
|
||||
normalized := normalizeProjectOrderSerial(serialNumber)
|
||||
|
||||
if !checkConfirmRateLimit(normalized) {
|
||||
return nil, errors.New("操作过于频繁,请稍后再试")
|
||||
}
|
||||
|
||||
var order models.ProjectOrder
|
||||
result := database.DB.Where("serial_number = ?", normalized).First(&order)
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("查询项目工单失败: %w", errors.New("序列号不存在"))
|
||||
}
|
||||
|
||||
if order.WorkOrderStatus == ProjectOrderStatusClosed {
|
||||
return nil, errors.New("工单已完成")
|
||||
}
|
||||
|
||||
if err := validateSignature(dto.EngineerSignature); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if strings.TrimSpace(dto.CompletionNote) != "" {
|
||||
order.CompletionNote = strings.TrimSpace(dto.CompletionNote)
|
||||
}
|
||||
if strings.TrimSpace(order.CompletionNote) == "" {
|
||||
return nil, errors.New("请填写完成说明")
|
||||
}
|
||||
if len(parseSiteImages(order.SiteImagesJSON)) == 0 {
|
||||
return nil, errors.New("请至少上传 1 张现场图片")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
order.WorkOrderStatus = ProjectOrderStatusClosed
|
||||
order.CompletedAt = &now
|
||||
order.EngineerSignature = strings.TrimSpace(dto.EngineerSignature)
|
||||
|
||||
if err := database.DB.Save(&order).Error; err != nil {
|
||||
return nil, fmt.Errorf("提交完成失败: %w", err)
|
||||
}
|
||||
|
||||
return s.PublicQuery(normalized)
|
||||
}
|
||||
|
||||
// Reassign 重新分配技术员(仅管理员)
|
||||
func (s *ProjectOrdersService) Reassign(serialNumber string, technicianID uint) (*models.ProjectOrder, error) {
|
||||
var order models.ProjectOrder
|
||||
result := database.DB.Where("serial_number = ?", normalizeProjectOrderSerial(serialNumber)).First(&order)
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("查询项目工单失败: %w", errors.New("序列号不存在"))
|
||||
}
|
||||
|
||||
if order.WorkOrderStatus == ProjectOrderStatusClosed {
|
||||
return nil, errors.New("工单已关闭,不可重新分配")
|
||||
}
|
||||
|
||||
var technician models.User
|
||||
if err := database.DB.First(&technician, technicianID).Error; err != nil {
|
||||
return nil, errors.New("指定的技术员不存在")
|
||||
}
|
||||
if technician.Role != "admin" && technician.Role != "technician" {
|
||||
return nil, errors.New("指定的用户不是技术员或管理员")
|
||||
}
|
||||
|
||||
order.TechnicianID = &technicianID
|
||||
if err := database.DB.Save(&order).Error; err != nil {
|
||||
return nil, fmt.Errorf("重新分配技术员失败: %w", err)
|
||||
}
|
||||
|
||||
_ = database.DB.Preload("Technician").Preload("Creator").Where("serial_number = ?", order.SerialNumber).First(&order)
|
||||
return &order, nil
|
||||
}
|
||||
|
||||
// ForceClose 管理员强制关闭工单
|
||||
func (s *ProjectOrdersService) ForceClose(serialNumber string) (*models.ProjectOrder, error) {
|
||||
var order models.ProjectOrder
|
||||
result := database.DB.Where("serial_number = ?", normalizeProjectOrderSerial(serialNumber)).First(&order)
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("查询项目工单失败: %w", errors.New("序列号不存在"))
|
||||
}
|
||||
|
||||
if order.WorkOrderStatus == ProjectOrderStatusClosed {
|
||||
return nil, errors.New("工单已关闭")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
order.WorkOrderStatus = ProjectOrderStatusClosed
|
||||
order.CompletedAt = &now
|
||||
|
||||
if err := database.DB.Save(&order).Error; err != nil {
|
||||
return nil, fmt.Errorf("强制关闭工单失败: %w", err)
|
||||
}
|
||||
|
||||
_ = database.DB.Preload("Technician").Preload("Creator").Where("serial_number = ?", order.SerialNumber).First(&order)
|
||||
return &order, nil
|
||||
}
|
||||
|
||||
// Delete 物理删除项目工单(仅管理员)
|
||||
func (s *ProjectOrdersService) Delete(serialNumber string) error {
|
||||
var order models.ProjectOrder
|
||||
result := database.DB.Where("serial_number = ?", normalizeProjectOrderSerial(serialNumber)).First(&order)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("查询项目工单失败: %w", errors.New("序列号不存在"))
|
||||
}
|
||||
|
||||
if err := database.DB.Delete(&order).Error; err != nil {
|
||||
return fmt.Errorf("删除项目工单失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateQRCode 生成项目工单二维码
|
||||
func (s *ProjectOrdersService) GenerateQRCode(
|
||||
serialNumber string,
|
||||
baseUrl string,
|
||||
requestHost string,
|
||||
protocol string,
|
||||
) (string, string, error) {
|
||||
var order models.ProjectOrder
|
||||
result := database.DB.Where("serial_number = ?", normalizeProjectOrderSerial(serialNumber)).First(&order)
|
||||
if result.Error != nil {
|
||||
return "", "", fmt.Errorf("查询项目工单失败: %w", errors.New("序列号不存在"))
|
||||
}
|
||||
|
||||
// 项目码二维码直接指向 /project-orders/{serialNumber}
|
||||
if baseUrl == "" {
|
||||
baseUrl = fmt.Sprintf("%s://%s/project-orders/%s", protocol, requestHost, order.SerialNumber)
|
||||
}
|
||||
|
||||
filePath := fmt.Sprintf("temp_qr_%s.png", uuid.New().String())
|
||||
writer, err := standard.New(filePath, standard.WithQRWidth(6))
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("二维码写入器创建失败: %w", err)
|
||||
}
|
||||
|
||||
qrc, errCode := qr.New(baseUrl)
|
||||
if errCode != nil {
|
||||
os.Remove(filePath)
|
||||
return "", "", fmt.Errorf("二维码创建失败: %w", errCode)
|
||||
}
|
||||
|
||||
if errSave := qrc.Save(writer); errSave != nil {
|
||||
os.Remove(filePath)
|
||||
return "", "", fmt.Errorf("二维码保存失败: %w", errSave)
|
||||
}
|
||||
|
||||
fileContent, errRead := os.ReadFile(filePath)
|
||||
if errRead != nil {
|
||||
os.Remove(filePath)
|
||||
return "", "", fmt.Errorf("二维码文件读取失败: %w", errRead)
|
||||
}
|
||||
os.Remove(filePath)
|
||||
|
||||
qrCodeBase64 := fmt.Sprintf("data:image/png;base64,%s", base64.StdEncoding.EncodeToString(fileContent))
|
||||
return qrCodeBase64, baseUrl, nil
|
||||
}
|
||||
Reference in New Issue
Block a user