Files
backend-go/controllers/aftersales_controller.go
T
Frudrax Cheng 0d82260fd9 Add aftersales work order feature
- AftersalesOrder model with state machine (created/pending_confirmation/closed/rejected)
- Public scan-to-confirm flow with phone last-4 verification and rate limiting
- Technician role and middleware for ownership-scoped operations
- QR code generation pointing to /aftersales/{serialNumber}
- Admin overrides: reassign, force-close, delete

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 10:39:49 +08:00

393 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"
)
// AftersalesController 售后工单控制器
type AftersalesController struct {
aftersalesService services.AftersalesService
}
// NewAftersalesController 创建售后工单控制器实例
func NewAftersalesController() *AftersalesController {
return &AftersalesController{
aftersalesService: services.AftersalesService{},
}
}
// Create 创建售后工单
// @Summary 创建售后工单
// @Description 创建一个新的售后工单并分配编号
// @Tags 售后工单
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param data body models.CreateAftersalesOrderDTO true "工单数据"
// @Success 200 {object} models.DataResponse
// @Failure 400 {object} models.ErrorResponse
// @Failure 401 {object} models.ErrorResponse
// @Failure 500 {object} models.ErrorResponse
// @Router /aftersales [post]
func (c *AftersalesController) Create(ctx *gin.Context) {
userModel, ok := GetCurrentUser(ctx)
if !ok {
return
}
var dto models.CreateAftersalesOrderDTO
if !BindJSON(ctx, &dto) {
return
}
order, err := c.aftersalesService.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 /aftersales [get]
func (c *AftersalesController) 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.aftersalesService.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 /aftersales/{serialNumber} [get]
func (c *AftersalesController) FindOne(ctx *gin.Context) {
serialNumber := ctx.Param("serialNumber")
order, err := c.aftersalesService.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.UpdateAftersalesOrderDTO true "更新数据"
// @Success 200 {object} models.DataResponse
// @Failure 400 {object} models.ErrorResponse
// @Failure 401 {object} models.ErrorResponse
// @Failure 403 {object} models.ErrorResponse
// @Router /aftersales/{serialNumber} [patch]
func (c *AftersalesController) Update(ctx *gin.Context) {
userModel, ok := GetCurrentUser(ctx)
if !ok {
return
}
serialNumber := ctx.Param("serialNumber")
var dto models.UpdateAftersalesOrderDTO
if !BindJSON(ctx, &dto) {
return
}
order, err := c.aftersalesService.Update(serialNumber, dto, userModel)
if err != nil {
ErrorResponse(ctx, http.StatusBadRequest, err.Error())
return
}
SuccessResponse(ctx, "工单更新成功", gin.H{
"order": order,
})
}
// SubmitForConfirmation 技术员提交客户确认
// @Summary 提交客户确认
// @Description 技术员填写处理结果后提交,工单进入"待客户确认"状态
// @Tags 售后工单
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param serialNumber path string true "工单号"
// @Param data body models.SubmitForConfirmationDTO true "处理结果"
// @Success 200 {object} models.DataResponse
// @Failure 400 {object} models.ErrorResponse
// @Failure 401 {object} models.ErrorResponse
// @Router /aftersales/{serialNumber}/submit [post]
func (c *AftersalesController) SubmitForConfirmation(ctx *gin.Context) {
userModel, ok := GetCurrentUser(ctx)
if !ok {
return
}
serialNumber := ctx.Param("serialNumber")
var dto models.SubmitForConfirmationDTO
if !BindJSON(ctx, &dto) {
return
}
order, err := c.aftersalesService.SubmitForConfirmation(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 /aftersales/{serialNumber}/qrcode [post]
func (c *AftersalesController) 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.aftersalesService.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.ReassignAftersalesDTO true "新技术员 ID"
// @Success 200 {object} models.DataResponse
// @Failure 400 {object} models.ErrorResponse
// @Failure 401 {object} models.ErrorResponse
// @Router /aftersales/{serialNumber}/reassign [post]
func (c *AftersalesController) Reassign(ctx *gin.Context) {
serialNumber := ctx.Param("serialNumber")
var dto models.ReassignAftersalesDTO
if !BindJSON(ctx, &dto) {
return
}
order, err := c.aftersalesService.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 /aftersales/{serialNumber}/force-close [post]
func (c *AftersalesController) ForceClose(ctx *gin.Context) {
serialNumber := ctx.Param("serialNumber")
order, err := c.aftersalesService.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 /aftersales/{serialNumber} [delete]
func (c *AftersalesController) Delete(ctx *gin.Context) {
serialNumber := ctx.Param("serialNumber")
if err := c.aftersalesService.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 /aftersales/{serialNumber}/query [get]
func (c *AftersalesController) PublicQuery(ctx *gin.Context) {
serialNumber := ctx.Param("serialNumber")
view, err := c.aftersalesService.PublicQuery(serialNumber)
if err != nil {
ErrorResponse(ctx, http.StatusNotFound, err.Error())
return
}
SuccessResponse(ctx, "查询成功", gin.H{
"order": view,
})
}
// CustomerConfirm 客户授权/未授权确认
// @Summary 客户授权确认
// @Description 客户输入手机号后四位后选择已授权或未授权
// @Tags 售后工单查询
// @Accept json
// @Produce json
// @Param serialNumber path string true "工单号"
// @Param data body models.CustomerConfirmDTO true "确认数据"
// @Success 200 {object} models.DataResponse
// @Failure 400 {object} models.ErrorResponse
// @Failure 401 {object} models.ErrorResponse
// @Failure 429 {object} models.ErrorResponse
// @Router /aftersales/{serialNumber}/confirm [post]
func (c *AftersalesController) CustomerConfirm(ctx *gin.Context) {
serialNumber := ctx.Param("serialNumber")
var dto models.CustomerConfirmDTO
if !BindJSON(ctx, &dto) {
return
}
view, err := c.aftersalesService.CustomerConfirm(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,
})
}